python-google-sheets 0.1.2__py3-none-any.whl → 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- google_sheets/__init__.py +21 -3
- google_sheets/api_request.py +174 -49
- google_sheets/google_sheets.py +31 -45
- google_sheets/spreadsheet_requests/__init__.py +6 -0
- google_sheets/spreadsheet_requests/conditional_format_rule.py +87 -10
- google_sheets/spreadsheet_requests/general_models.py +3 -0
- google_sheets/spreadsheet_requests/spreadsheet.py +30 -30
- google_sheets/spreadsheet_requests/update_cells.py +51 -48
- google_sheets/spreadsheet_requests/update_sheet_properties.py +16 -17
- google_sheets/styles.py +14 -0
- google_sheets/utils.py +43 -20
- python_google_sheets-1.0.0.dist-info/METADATA +491 -0
- python_google_sheets-1.0.0.dist-info/RECORD +18 -0
- {python_google_sheets-0.1.2.dist-info → python_google_sheets-1.0.0.dist-info}/WHEEL +1 -1
- python_google_sheets-0.1.2.dist-info/METADATA +0 -53
- python_google_sheets-0.1.2.dist-info/RECORD +0 -18
- {python_google_sheets-0.1.2.dist-info → python_google_sheets-1.0.0.dist-info}/licenses/LICENSE +0 -0
- {python_google_sheets-0.1.2.dist-info → python_google_sheets-1.0.0.dist-info}/top_level.txt +0 -0
google_sheets/__init__.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from .google_sheets import GoogleSheets
|
|
2
|
+
from .api_request import ApiRequest
|
|
1
3
|
from .styles import Color_, Border_
|
|
2
4
|
from .spreadsheet_requests import (
|
|
3
5
|
ColorStyle,
|
|
@@ -16,7 +18,23 @@ from .spreadsheet_requests import (
|
|
|
16
18
|
WrapStrategy,
|
|
17
19
|
Spreadsheet,
|
|
18
20
|
SheetProperties,
|
|
21
|
+
ConditionType,
|
|
22
|
+
ConditionValue,
|
|
23
|
+
InterpolationPointType,
|
|
24
|
+
InterpolationPoint,
|
|
25
|
+
RelativeDate,
|
|
26
|
+
BooleanCondition,
|
|
27
|
+
BooleanRule,
|
|
28
|
+
GradientRule,
|
|
29
|
+
GridRange,
|
|
30
|
+
ConditionalFormatRule,
|
|
31
|
+
MergeType,
|
|
19
32
|
)
|
|
20
|
-
from .utils import
|
|
21
|
-
|
|
22
|
-
|
|
33
|
+
from .utils import (
|
|
34
|
+
get_spreadsheet_id_from_url,
|
|
35
|
+
col_num_to_letter,
|
|
36
|
+
col_letter_to_num,
|
|
37
|
+
rowcol_to_a1,
|
|
38
|
+
a1_to_rowcol,
|
|
39
|
+
float_sum,
|
|
40
|
+
)
|
google_sheets/api_request.py
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import re
|
|
2
|
+
from enum import StrEnum
|
|
2
3
|
|
|
4
|
+
from .styles import Color_
|
|
3
5
|
from .spreadsheet_requests import (
|
|
4
6
|
# conditional_format_rule
|
|
5
7
|
AddConditionalFormatRule,
|
|
6
8
|
DeleteConditionalFormatRule,
|
|
7
9
|
UpdateConditionalFormatRule,
|
|
8
10
|
ConditionalFormatRule,
|
|
11
|
+
BooleanRule,
|
|
12
|
+
BooleanCondition,
|
|
13
|
+
ConditionType,
|
|
14
|
+
ConditionValue,
|
|
15
|
+
RelativeDate,
|
|
9
16
|
GradientRule,
|
|
10
17
|
InterpolationPoint,
|
|
11
18
|
InterpolationPointType,
|
|
@@ -36,14 +43,15 @@ from .spreadsheet_requests import (
|
|
|
36
43
|
ExtendedValue,
|
|
37
44
|
CellFormat,
|
|
38
45
|
|
|
46
|
+
# spreadsheet
|
|
47
|
+
AddSheet,
|
|
48
|
+
DeleteSheet,
|
|
49
|
+
|
|
39
50
|
# general_models
|
|
40
51
|
ColorStyle,
|
|
41
52
|
GridRange,
|
|
42
53
|
FieldMask,
|
|
43
|
-
|
|
44
|
-
# spreadsheet
|
|
45
|
-
AddSheet,
|
|
46
|
-
DeleteSheet,
|
|
54
|
+
SimpleType,
|
|
47
55
|
)
|
|
48
56
|
|
|
49
57
|
|
|
@@ -52,11 +60,11 @@ class ApiRequest:
|
|
|
52
60
|
def update_cells(
|
|
53
61
|
sheet_id: int,
|
|
54
62
|
range_: str,
|
|
55
|
-
values: list[list[
|
|
63
|
+
values: list[list[SimpleType]] | list[SimpleType] = None,
|
|
56
64
|
cell_formats: list[list[CellFormat]] | list[CellFormat] = None
|
|
57
65
|
) -> dict:
|
|
58
66
|
assert values or cell_formats, 'At least one of the parameters must be specified: values or cell_formats'
|
|
59
|
-
start_row, end_row, start_col, end_col = ApiRequest.
|
|
67
|
+
start_row, end_row, start_col, end_col = ApiRequest._split_excel_range(range_)
|
|
60
68
|
|
|
61
69
|
# syntax sugar for single row or column
|
|
62
70
|
if values and not isinstance(values[0], list):
|
|
@@ -102,52 +110,169 @@ class ApiRequest:
|
|
|
102
110
|
).dict()
|
|
103
111
|
|
|
104
112
|
@staticmethod
|
|
105
|
-
def
|
|
113
|
+
def add_boolean_format_rule(
|
|
106
114
|
*,
|
|
107
115
|
sheet_id: int,
|
|
108
116
|
ranges: list[str],
|
|
109
|
-
|
|
110
|
-
|
|
117
|
+
condition_type: ConditionType,
|
|
118
|
+
condition_values: list[ConditionValue] = None,
|
|
119
|
+
cell_format: CellFormat,
|
|
111
120
|
) -> dict:
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
121
|
+
assert not any([
|
|
122
|
+
cell_format.number_format, cell_format.borders, cell_format.padding,
|
|
123
|
+
cell_format.horizontal_alignment, cell_format.vertical_alignment,
|
|
124
|
+
cell_format.wrap_strategy, cell_format.text_direction, cell_format.text_rotation,
|
|
125
|
+
cell_format.hyperlink_display_type
|
|
126
|
+
]), 'Conditional formatting can only apply a subset of formatting: bold, italic, strikethrough, foreground ' \
|
|
127
|
+
'color and background color (background_color_style and text_format)'
|
|
128
|
+
grid_ranges = [GridRange(sheet_id=sheet_id, **ApiRequest._split_excel_range(range_, return_as_dict=True)) for range_ in ranges]
|
|
129
|
+
return AddConditionalFormatRule(rule=ConditionalFormatRule(
|
|
130
|
+
ranges=grid_ranges,
|
|
131
|
+
boolean_rule=BooleanRule(
|
|
132
|
+
condition=BooleanCondition(
|
|
133
|
+
type=condition_type,
|
|
134
|
+
values=condition_values or []
|
|
135
|
+
),
|
|
136
|
+
format=cell_format
|
|
137
|
+
)
|
|
138
|
+
)).dict()
|
|
139
|
+
|
|
140
|
+
class GradientRule:
|
|
141
|
+
IPTypeAndValue = tuple[InterpolationPointType, int | None] # Type and Value of Interpolation Point
|
|
142
|
+
|
|
143
|
+
@staticmethod
|
|
144
|
+
def add(
|
|
145
|
+
*,
|
|
146
|
+
sheet_id: int,
|
|
147
|
+
ranges: list[str],
|
|
148
|
+
interpolation_points: tuple[IPTypeAndValue, IPTypeAndValue] | tuple[IPTypeAndValue, IPTypeAndValue, IPTypeAndValue],
|
|
149
|
+
interpolation_point_colors: tuple[ColorStyle, ColorStyle] | tuple[ColorStyle, ColorStyle, ColorStyle],
|
|
150
|
+
) -> dict:
|
|
151
|
+
assert len(interpolation_points) == len(interpolation_point_colors), 'The number of interpolation points must match the number of its colors'
|
|
152
|
+
grid_ranges = [GridRange(sheet_id=sheet_id, **ApiRequest._split_excel_range(range_, return_as_dict=True)) for range_ in ranges]
|
|
153
|
+
|
|
154
|
+
if len(interpolation_points) == 2:
|
|
155
|
+
return AddConditionalFormatRule(rule=ConditionalFormatRule(
|
|
156
|
+
ranges=grid_ranges,
|
|
157
|
+
gradient_rule=GradientRule(
|
|
158
|
+
minpoint=InterpolationPoint(
|
|
159
|
+
color_style=interpolation_point_colors[0],
|
|
160
|
+
type=interpolation_points[0][0],
|
|
161
|
+
value=str(interpolation_points[0][1]) if interpolation_points[0][1] is not None else None
|
|
162
|
+
),
|
|
163
|
+
maxpoint=InterpolationPoint(
|
|
164
|
+
color_style=interpolation_point_colors[1],
|
|
165
|
+
type=interpolation_points[1][0],
|
|
166
|
+
value=str(interpolation_points[1][1]) if interpolation_points[1][1] is not None else None
|
|
167
|
+
)
|
|
168
|
+
)
|
|
169
|
+
)).dict()
|
|
170
|
+
|
|
116
171
|
return AddConditionalFormatRule(rule=ConditionalFormatRule(
|
|
117
172
|
ranges=grid_ranges,
|
|
118
173
|
gradient_rule=GradientRule(
|
|
119
174
|
minpoint=InterpolationPoint(
|
|
120
|
-
color_style=
|
|
121
|
-
type=
|
|
122
|
-
value=str(
|
|
175
|
+
color_style=interpolation_point_colors[0],
|
|
176
|
+
type=interpolation_points[0][0],
|
|
177
|
+
value=str(interpolation_points[0][1]) if interpolation_points[0][1] is not None else None
|
|
178
|
+
),
|
|
179
|
+
midpoint=InterpolationPoint(
|
|
180
|
+
color_style=interpolation_point_colors[1],
|
|
181
|
+
type=interpolation_points[1][0],
|
|
182
|
+
value=str(interpolation_points[1][1]) if interpolation_points[1][1] is not None else None
|
|
123
183
|
),
|
|
124
184
|
maxpoint=InterpolationPoint(
|
|
125
|
-
color_style=
|
|
126
|
-
type=
|
|
127
|
-
value=str(
|
|
185
|
+
color_style=interpolation_point_colors[2],
|
|
186
|
+
type=interpolation_points[2][0],
|
|
187
|
+
value=str(interpolation_points[2][1]) if interpolation_points[2][1] is not None else None
|
|
128
188
|
)
|
|
129
189
|
)
|
|
130
190
|
)).dict()
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
191
|
+
|
|
192
|
+
class Preset(StrEnum):
|
|
193
|
+
# Two interpolation points
|
|
194
|
+
WHITE_GREEN = 'WHITE_GREEN'
|
|
195
|
+
WHITE_YELLOW = 'WHITE_YELLOW'
|
|
196
|
+
WHITE_RED = 'WHITE_RED'
|
|
197
|
+
GREEN_WHITE = 'GREEN_WHITE'
|
|
198
|
+
YELLOW_WHITE = 'YELLOW_WHITE'
|
|
199
|
+
RED_WHITE = 'RED_WHITE'
|
|
200
|
+
|
|
201
|
+
# Three interpolation points
|
|
202
|
+
RED_WHITE_GREEN_PERCENTILE = 'RED_WHITE_GREEN_PERCENTILE'
|
|
203
|
+
RED_WHITE_GREEN_PERCENT = 'RED_WHITE_GREEN_PERCENT'
|
|
204
|
+
GREEN_YELLOW_RED_PERCENTILE = 'GREEN_YELLOW_RED_PERCENTILE'
|
|
205
|
+
GREEN_YELLOW_RED_PERCENT = 'GREEN_YELLOW_RED_PERCENT'
|
|
206
|
+
GREEN_WHITE_RED_PERCENTILE = 'GREEN_WHITE_RED_PERCENTILE'
|
|
207
|
+
GREEN_WHITE_RED_PERCENT = 'GREEN_WHITE_RED_PERCENT'
|
|
208
|
+
RED_YELLOW_GREEN_PERCENTILE = 'RED_YELLOW_GREEN_PERCENTILE'
|
|
209
|
+
RED_YELLOW_GREEN_PERCENT = 'RED_YELLOW_GREEN_PERCENT'
|
|
210
|
+
|
|
211
|
+
@staticmethod
|
|
212
|
+
def add_preset(*, sheet_id: int, ranges: list[str], preset: Preset) -> dict:
|
|
213
|
+
grid_ranges = [GridRange(sheet_id=sheet_id, **ApiRequest._split_excel_range(range_, return_as_dict=True)) for range_ in ranges]
|
|
214
|
+
AGP = ApiRequest.GradientRule.Preset
|
|
215
|
+
|
|
216
|
+
if preset in (AGP.WHITE_GREEN, AGP.WHITE_YELLOW, AGP.WHITE_RED, AGP.GREEN_WHITE, AGP.YELLOW_WHITE, AGP.RED_WHITE):
|
|
217
|
+
if preset == AGP.WHITE_YELLOW:
|
|
218
|
+
minpoint_color_style, maxpoint_color_style = Color_.Basic.WHITE, Color_.ConditionalFormatting.YELLOW
|
|
219
|
+
elif preset == AGP.WHITE_RED:
|
|
220
|
+
minpoint_color_style, maxpoint_color_style = Color_.Basic.WHITE, Color_.ConditionalFormatting.RED
|
|
221
|
+
elif preset == AGP.GREEN_WHITE:
|
|
222
|
+
minpoint_color_style, maxpoint_color_style = Color_.ConditionalFormatting.GREEN, Color_.Basic.WHITE
|
|
223
|
+
elif preset == AGP.YELLOW_WHITE:
|
|
224
|
+
minpoint_color_style, maxpoint_color_style = Color_.ConditionalFormatting.YELLOW, Color_.Basic.WHITE
|
|
225
|
+
elif preset == AGP.RED_WHITE:
|
|
226
|
+
minpoint_color_style, maxpoint_color_style = Color_.ConditionalFormatting.RED, Color_.Basic.WHITE
|
|
227
|
+
else: # preset == AGP.WHITE_GREEN and default
|
|
228
|
+
minpoint_color_style, maxpoint_color_style = Color_.Basic.WHITE, Color_.ConditionalFormatting.GREEN
|
|
229
|
+
|
|
230
|
+
return AddConditionalFormatRule(rule=ConditionalFormatRule(
|
|
231
|
+
ranges=grid_ranges,
|
|
232
|
+
gradient_rule=GradientRule(
|
|
233
|
+
minpoint=InterpolationPoint(
|
|
234
|
+
color_style=minpoint_color_style,
|
|
235
|
+
type=InterpolationPointType.MIN,
|
|
236
|
+
),
|
|
237
|
+
maxpoint=InterpolationPoint(
|
|
238
|
+
color_style=maxpoint_color_style,
|
|
239
|
+
type=InterpolationPointType.MAX,
|
|
240
|
+
)
|
|
241
|
+
)
|
|
242
|
+
)).dict()
|
|
243
|
+
|
|
244
|
+
else: # three interpolation points
|
|
245
|
+
if preset in (AGP.RED_WHITE_GREEN_PERCENTILE, AGP.RED_WHITE_GREEN_PERCENT):
|
|
246
|
+
minpoint_cs, midpoint_cs, maxpoint_cs = Color_.ConditionalFormatting.RED, Color_.Basic.WHITE, Color_.ConditionalFormatting.GREEN
|
|
247
|
+
midpoint_type = InterpolationPointType.PERCENTILE if preset == AGP.RED_WHITE_GREEN_PERCENTILE else InterpolationPointType.PERCENT
|
|
248
|
+
elif preset in (AGP.GREEN_YELLOW_RED_PERCENTILE, AGP.GREEN_YELLOW_RED_PERCENT):
|
|
249
|
+
minpoint_cs, midpoint_cs, maxpoint_cs = Color_.ConditionalFormatting.GREEN, Color_.ConditionalFormatting.YELLOW, Color_.ConditionalFormatting.RED
|
|
250
|
+
midpoint_type = InterpolationPointType.PERCENTILE if preset == AGP.GREEN_YELLOW_RED_PERCENTILE else InterpolationPointType.PERCENT
|
|
251
|
+
elif preset in (AGP.GREEN_WHITE_RED_PERCENTILE, AGP.GREEN_WHITE_RED_PERCENT):
|
|
252
|
+
minpoint_cs, midpoint_cs, maxpoint_cs = Color_.ConditionalFormatting.GREEN, Color_.Basic.WHITE, Color_.ConditionalFormatting.RED
|
|
253
|
+
midpoint_type = InterpolationPointType.PERCENTILE if preset == AGP.GREEN_WHITE_RED_PERCENTILE else InterpolationPointType.PERCENT
|
|
254
|
+
else: # preset in (AGP.RED_YELLOW_GREEN_PERCENTILE, AGP.RED_YELLOW_GREEN_PERCENT):
|
|
255
|
+
minpoint_cs, midpoint_cs, maxpoint_cs = Color_.ConditionalFormatting.RED, Color_.ConditionalFormatting.YELLOW, Color_.ConditionalFormatting.GREEN
|
|
256
|
+
midpoint_type = InterpolationPointType.PERCENTILE if preset == AGP.RED_YELLOW_GREEN_PERCENTILE else InterpolationPointType.PERCENT
|
|
257
|
+
|
|
258
|
+
return AddConditionalFormatRule(rule=ConditionalFormatRule(
|
|
259
|
+
ranges=grid_ranges,
|
|
260
|
+
gradient_rule=GradientRule(
|
|
261
|
+
minpoint=InterpolationPoint(
|
|
262
|
+
color_style=minpoint_cs,
|
|
263
|
+
type=InterpolationPointType.MIN,
|
|
264
|
+
),
|
|
265
|
+
midpoint=InterpolationPoint(
|
|
266
|
+
color_style=midpoint_cs,
|
|
267
|
+
type=midpoint_type,
|
|
268
|
+
value=50
|
|
269
|
+
),
|
|
270
|
+
maxpoint=InterpolationPoint(
|
|
271
|
+
color_style=maxpoint_cs,
|
|
272
|
+
type=InterpolationPointType.MAX,
|
|
273
|
+
)
|
|
274
|
+
)
|
|
275
|
+
)).dict()
|
|
151
276
|
|
|
152
277
|
@staticmethod
|
|
153
278
|
def delete_conditional_format_rule(*, sheet_id: int, index: int) -> dict:
|
|
@@ -200,7 +325,7 @@ class ApiRequest:
|
|
|
200
325
|
|
|
201
326
|
@staticmethod
|
|
202
327
|
def merge_cells(sheet_id: int, range_: str, merge_type: MergeType = MergeType.MERGE_ALL) -> dict:
|
|
203
|
-
start_row, end_row, start_col, end_col = ApiRequest.
|
|
328
|
+
start_row, end_row, start_col, end_col = ApiRequest._split_excel_range(range_)
|
|
204
329
|
return MergeCells(
|
|
205
330
|
range=GridRange(
|
|
206
331
|
sheet_id=sheet_id,
|
|
@@ -223,10 +348,10 @@ class ApiRequest:
|
|
|
223
348
|
) -> dict:
|
|
224
349
|
assert range_ or (start_row and end_row and start_column and end_column), 'Either range_ or start_row, end_row, start_column, end_column must be specified'
|
|
225
350
|
if range_: # range_ has priority
|
|
226
|
-
start_row, end_row, start_column, end_column = ApiRequest.
|
|
351
|
+
start_row, end_row, start_column, end_column = ApiRequest._split_excel_range(range_)
|
|
227
352
|
else:
|
|
228
|
-
start_column = ApiRequest.
|
|
229
|
-
end_column = ApiRequest.
|
|
353
|
+
start_column = ApiRequest._get_column_index(start_column) if isinstance(start_column, str) else start_column
|
|
354
|
+
end_column = ApiRequest._get_column_index(end_column) if isinstance(end_column, str) else end_column
|
|
230
355
|
return UnmergeCells(
|
|
231
356
|
range=GridRange(
|
|
232
357
|
sheet_id=sheet_id,
|
|
@@ -304,7 +429,7 @@ class ApiRequest:
|
|
|
304
429
|
@staticmethod
|
|
305
430
|
def set_column_width(sheet_id: int, col_no_or_letter: int | str, width: int) -> dict:
|
|
306
431
|
if isinstance(col_no_or_letter, str):
|
|
307
|
-
col_no = ApiRequest.
|
|
432
|
+
col_no = ApiRequest._get_column_index(col_no_or_letter)
|
|
308
433
|
else:
|
|
309
434
|
col_no = col_no_or_letter
|
|
310
435
|
return UpdateDimensionProperties(
|
|
@@ -417,22 +542,22 @@ class ApiRequest:
|
|
|
417
542
|
)).dict()
|
|
418
543
|
|
|
419
544
|
@staticmethod
|
|
420
|
-
def
|
|
545
|
+
def _split_excel_range(range_: str, return_as_dict: bool = False) -> tuple[int, int, int, int] | dict[str, int]:
|
|
421
546
|
if ':' in range_:
|
|
422
|
-
match = re.match(r
|
|
547
|
+
match = re.match(r'([A-Z]+)(\d+):([A-Z]+)(\d+)$', range_)
|
|
423
548
|
if not match:
|
|
424
549
|
raise ValueError(f'Unsupported range format: {range_}')
|
|
425
550
|
start_column, start_row, end_column, end_row = match.groups()
|
|
426
551
|
|
|
427
552
|
else:
|
|
428
|
-
match = re.match(r
|
|
553
|
+
match = re.match(r'([A-Z]+)(\d+)$', range_)
|
|
429
554
|
if not match:
|
|
430
555
|
raise ValueError(f'Unsupported range format: {range_}')
|
|
431
556
|
start_column, start_row = match.groups()
|
|
432
557
|
end_column, end_row = start_column, start_row
|
|
433
558
|
|
|
434
559
|
start_row, end_row = int(start_row), int(end_row)
|
|
435
|
-
start_column, end_column = ApiRequest.
|
|
560
|
+
start_column, end_column = ApiRequest._get_column_index(start_column), ApiRequest._get_column_index(end_column)
|
|
436
561
|
if start_row > end_row or start_column > end_column:
|
|
437
562
|
raise ValueError(f'Invalid range: {range_}')
|
|
438
563
|
|
|
@@ -446,7 +571,7 @@ class ApiRequest:
|
|
|
446
571
|
return start_row, end_row, start_column, end_column
|
|
447
572
|
|
|
448
573
|
@staticmethod
|
|
449
|
-
def
|
|
574
|
+
def _get_column_index(column_letters: str) -> int:
|
|
450
575
|
column_index = 0
|
|
451
576
|
for i, letter in enumerate(column_letters[::-1].upper()):
|
|
452
577
|
column_index += (ord(letter) - 64) * (26 ** i)
|
google_sheets/google_sheets.py
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING
|
|
2
|
+
|
|
1
3
|
from google.oauth2.service_account import Credentials
|
|
2
4
|
from googleapiclient.discovery import build
|
|
3
5
|
from googleapiclient.errors import HttpError
|
|
4
6
|
|
|
5
|
-
from .spreadsheet_requests import Spreadsheet, SheetProperties
|
|
7
|
+
from .spreadsheet_requests import Spreadsheet, SheetProperties, SimpleType
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from googleapiclient.discovery import Resource # noqa
|
|
8
11
|
|
|
9
12
|
DEFAULT_PATH_TO_CREDS = 'service_account.json'
|
|
10
13
|
|
|
11
14
|
|
|
12
15
|
class GoogleSheets:
|
|
13
16
|
@staticmethod
|
|
14
|
-
def build_service(path_to_creds: str = DEFAULT_PATH_TO_CREDS):
|
|
17
|
+
def build_service(path_to_creds: str = DEFAULT_PATH_TO_CREDS) -> 'Resource':
|
|
15
18
|
SCOPES = ['https://www.googleapis.com/auth/spreadsheets']
|
|
16
19
|
try:
|
|
17
20
|
credentials = Credentials.from_service_account_file(path_to_creds, scopes=SCOPES)
|
|
@@ -82,7 +85,7 @@ class GoogleSheets:
|
|
|
82
85
|
return spreadsheet_id, f'https://docs.google.com/spreadsheets/d/{spreadsheet_id}'
|
|
83
86
|
|
|
84
87
|
@staticmethod
|
|
85
|
-
def update_spreadsheet(spreadsheet_id: str, api_requests: list[dict], service
|
|
88
|
+
def update_spreadsheet(spreadsheet_id: str, api_requests: list[dict], service: 'Resource') -> None:
|
|
86
89
|
"""
|
|
87
90
|
Updates Google Sheet with the specified API requests.
|
|
88
91
|
|
|
@@ -91,19 +94,13 @@ class GoogleSheets:
|
|
|
91
94
|
api_requests (list[dict]): List of API requests to update the table
|
|
92
95
|
service (googleapiclient.discovery.Resource): Google Sheets service object
|
|
93
96
|
"""
|
|
94
|
-
if service is None:
|
|
95
|
-
service = GoogleSheets.build_service()
|
|
96
|
-
|
|
97
97
|
try:
|
|
98
98
|
service.spreadsheets().batchUpdate(spreadsheetId=spreadsheet_id, body={'requests': api_requests}).execute(num_retries=5)
|
|
99
99
|
except HttpError as e:
|
|
100
100
|
raise e
|
|
101
101
|
|
|
102
102
|
@staticmethod
|
|
103
|
-
def copy_sheet(source_spreadsheet_id: str,
|
|
104
|
-
if service is None:
|
|
105
|
-
service = GoogleSheets.build_service()
|
|
106
|
-
|
|
103
|
+
def copy_sheet(source_spreadsheet_id: str, source_sheet_id: str, destination_spreadsheet_id: str, service: 'Resource') -> SheetProperties:
|
|
107
104
|
request = service.spreadsheets().sheets().copyTo(
|
|
108
105
|
spreadsheetId=source_spreadsheet_id,
|
|
109
106
|
sheetId=source_sheet_id,
|
|
@@ -115,14 +112,16 @@ class GoogleSheets:
|
|
|
115
112
|
raise e
|
|
116
113
|
|
|
117
114
|
@staticmethod
|
|
118
|
-
def get_spreadsheet(spreadsheet_id: str, service
|
|
119
|
-
if service is None:
|
|
120
|
-
service = GoogleSheets.build_service()
|
|
121
|
-
|
|
115
|
+
def get_spreadsheet(spreadsheet_id: str, service: 'Resource') -> Spreadsheet:
|
|
122
116
|
return Spreadsheet.model_validate(service.spreadsheets().get(spreadsheetId=spreadsheet_id).execute(num_retries=5))
|
|
123
117
|
|
|
124
118
|
@staticmethod
|
|
125
|
-
def get_spreadsheet_range_values(
|
|
119
|
+
def get_spreadsheet_range_values(
|
|
120
|
+
spreadsheet_id: str,
|
|
121
|
+
sheets: list[str | int],
|
|
122
|
+
ranges: list[list[str]],
|
|
123
|
+
service: 'Resource'
|
|
124
|
+
) -> list[list[SimpleType] | list[list[SimpleType]]]:
|
|
126
125
|
"""
|
|
127
126
|
Reads values from the specified ranges of the table.
|
|
128
127
|
IMPORTANT: If the last cells in the range are empty, they will be omitted. If all cells are empty, an empty
|
|
@@ -131,30 +130,32 @@ class GoogleSheets:
|
|
|
131
130
|
|
|
132
131
|
Args:
|
|
133
132
|
spreadsheet_id (str): ID of the table
|
|
134
|
-
|
|
135
|
-
ranges (list[str]): List of ranges to read in A1 notation
|
|
133
|
+
sheets (list[str | int]): Name or ID of the sheets to read from
|
|
134
|
+
ranges (list[list[str]]): List of ranges from each sheet to read in A1 notation
|
|
136
135
|
service (googleapiclient.discovery.Resource): Google Sheets service object
|
|
137
136
|
|
|
138
137
|
Returns:
|
|
139
|
-
list[list[
|
|
138
|
+
list[list[SimpleType] | list[list[SimpleType]]]: List of values from the specified ranges. Each range
|
|
139
|
+
corresponds to an element in the list. If the range is a single row or a single column, a list of values
|
|
140
|
+
is returned Otherwise, a list of lists of values is returned.
|
|
140
141
|
"""
|
|
141
|
-
|
|
142
|
-
service = GoogleSheets.build_service()
|
|
142
|
+
assert len(sheets) == len(ranges), 'sheets and ranges must have the same length'
|
|
143
143
|
|
|
144
|
-
|
|
144
|
+
ranges_processed = []
|
|
145
|
+
if any(isinstance(sheet, int) for sheet in sheets):
|
|
145
146
|
ss = GoogleSheets.get_spreadsheet(spreadsheet_id, service)
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
147
|
+
for i in range(len(sheets)):
|
|
148
|
+
if isinstance(sheets[i], int):
|
|
149
|
+
try:
|
|
150
|
+
sheets[i] = next(sht.properties.title for sht in ss.sheets if sht.properties.sheet_id == sheets[i])
|
|
151
|
+
except StopIteration:
|
|
152
|
+
return [[]]
|
|
153
|
+
ranges_processed.extend([f'{sheets[i]}!{range_}' for range_ in ranges[i]])
|
|
153
154
|
|
|
154
155
|
try:
|
|
155
156
|
response = service.spreadsheets().values().batchGet(
|
|
156
157
|
spreadsheetId=spreadsheet_id,
|
|
157
|
-
ranges=
|
|
158
|
+
ranges=ranges_processed,
|
|
158
159
|
valueRenderOption='UNFORMATTED_VALUE',
|
|
159
160
|
dateTimeRenderOption='FORMATTED_STRING'
|
|
160
161
|
).execute(num_retries=5)
|
|
@@ -177,18 +178,3 @@ class GoogleSheets:
|
|
|
177
178
|
result.append(values)
|
|
178
179
|
|
|
179
180
|
return result
|
|
180
|
-
|
|
181
|
-
@staticmethod
|
|
182
|
-
def get_spreadsheet_id_from_url(url: str) -> str:
|
|
183
|
-
"""
|
|
184
|
-
Extracts the ID of the spreadsheet from the URL.
|
|
185
|
-
|
|
186
|
-
Args:
|
|
187
|
-
url (str): URL of the table
|
|
188
|
-
|
|
189
|
-
Returns:
|
|
190
|
-
str: ID of the table
|
|
191
|
-
"""
|
|
192
|
-
if '/edit' in url:
|
|
193
|
-
url = url[:url.index('/edit')]
|
|
194
|
-
return url.split('/')[-1]
|
|
@@ -3,6 +3,11 @@ from .conditional_format_rule import (
|
|
|
3
3
|
DeleteConditionalFormatRule,
|
|
4
4
|
UpdateConditionalFormatRule,
|
|
5
5
|
ConditionalFormatRule,
|
|
6
|
+
BooleanRule,
|
|
7
|
+
BooleanCondition,
|
|
8
|
+
ConditionType,
|
|
9
|
+
ConditionValue,
|
|
10
|
+
RelativeDate,
|
|
6
11
|
GradientRule,
|
|
7
12
|
InterpolationPoint,
|
|
8
13
|
InterpolationPointType,
|
|
@@ -57,4 +62,5 @@ from .general_models import (
|
|
|
57
62
|
ColorStyle,
|
|
58
63
|
GridRange,
|
|
59
64
|
FieldMask,
|
|
65
|
+
SimpleType,
|
|
60
66
|
)
|
|
@@ -8,9 +8,10 @@ This module contains the models for the following Google Sheets API requests:
|
|
|
8
8
|
import json
|
|
9
9
|
from enum import StrEnum
|
|
10
10
|
|
|
11
|
-
from pydantic import BaseModel, Field, model_validator
|
|
11
|
+
from pydantic import BaseModel, Field, model_validator, field_validator
|
|
12
12
|
|
|
13
|
-
from .general_models import ColorStyle, GridRange
|
|
13
|
+
from .general_models import ColorStyle, GridRange, SimpleType
|
|
14
|
+
from .update_cells import CellFormat
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
class InterpolationPointType(StrEnum):
|
|
@@ -24,29 +25,104 @@ class InterpolationPointType(StrEnum):
|
|
|
24
25
|
class InterpolationPoint(BaseModel):
|
|
25
26
|
color_style: ColorStyle = Field(..., alias='colorStyle')
|
|
26
27
|
type: InterpolationPointType
|
|
27
|
-
value:
|
|
28
|
+
value: SimpleType | None = None # Required only for types NUMBER, PERCENT and PERCENTILE
|
|
29
|
+
|
|
30
|
+
@field_validator('value', mode='before')
|
|
31
|
+
@classmethod
|
|
32
|
+
def coerce_to_str(cls, v: SimpleType | None):
|
|
33
|
+
if v is not None and not isinstance(v, str):
|
|
34
|
+
return str(v)
|
|
35
|
+
return v
|
|
36
|
+
|
|
37
|
+
class Config:
|
|
38
|
+
populate_by_name = True
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class RelativeDate(StrEnum):
|
|
42
|
+
PAST_YEAR = 'PAST_YEAR' # One year before today
|
|
43
|
+
PAST_MONTH = 'PAST_MONTH' # One month before today
|
|
44
|
+
PAST_WEEK = 'PAST_WEEK' # One week before today
|
|
45
|
+
YESTERDAY = 'YESTERDAY'
|
|
46
|
+
TODAY = 'TODAY'
|
|
47
|
+
TOMORROW = 'TOMORROW'
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ConditionValue(BaseModel):
|
|
51
|
+
""" Union field, exactly one must be set """
|
|
52
|
+
relative_date: RelativeDate | None = Field(None, alias='relativeDate')
|
|
53
|
+
user_entered_value: SimpleType | None = Field(None, alias='userEnteredValue')
|
|
54
|
+
|
|
55
|
+
@field_validator('user_entered_value', mode='before')
|
|
56
|
+
@classmethod
|
|
57
|
+
def coerce_to_str(cls, v: SimpleType | None):
|
|
58
|
+
if v is not None and not isinstance(v, str):
|
|
59
|
+
return str(v)
|
|
60
|
+
return v
|
|
28
61
|
|
|
29
62
|
class Config:
|
|
30
63
|
populate_by_name = True
|
|
31
64
|
|
|
32
65
|
|
|
66
|
+
class ConditionType(StrEnum):
|
|
67
|
+
NUMBER_GREATER = 'NUMBER_GREATER' # Requires ONE ConditionValue
|
|
68
|
+
NUMBER_GREATER_THAN_EQ = 'NUMBER_GREATER_THAN_EQ' # Requires ONE ConditionValue
|
|
69
|
+
NUMBER_LESS = 'NUMBER_LESS' # Requires ONE ConditionValue
|
|
70
|
+
NUMBER_LESS_THAN_EQ = 'NUMBER_LESS_THAN_EQ' # Requires ONE ConditionValue
|
|
71
|
+
NUMBER_EQ = 'NUMBER_EQ' # Requires ONE ConditionValue ...
|
|
72
|
+
NUMBER_NOT_EQ = 'NUMBER_NOT_EQ' # Requires ONE ConditionValue ...
|
|
73
|
+
NUMBER_BETWEEN = 'NUMBER_BETWEEN' # Requires TWO ConditionValue
|
|
74
|
+
NUMBER_NOT_BETWEEN = 'NUMBER_NOT_BETWEEN' # Requires TWO ConditionValue
|
|
75
|
+
TEXT_CONTAINS = 'TEXT_CONTAINS' # Requires ONE ConditionValue
|
|
76
|
+
TEXT_NOT_CONTAINS = 'TEXT_NOT_CONTAINS' # Requires ONE ConditionValue
|
|
77
|
+
TEXT_STARTS_WITH = 'TEXT_STARTS_WITH' # Requires ONE ConditionValue
|
|
78
|
+
TEXT_ENDS_WITH = 'TEXT_ENDS_WITH' # Requires ONE ConditionValue
|
|
79
|
+
TEXT_EQ = 'TEXT_EQ' # Requires ONE ConditionValue ...
|
|
80
|
+
TEXT_IS_EMAIL = 'TEXT_IS_EMAIL' # Requires NO ConditionValue
|
|
81
|
+
TEXT_IS_URL = 'TEXT_IS_URL' # Requires NO ConditionValue
|
|
82
|
+
DATE_EQ = 'DATE_EQ' # Requires ONE ConditionValue ...
|
|
83
|
+
DATE_BEFORE = 'DATE_BEFORE' # Requires ONE ConditionValue (may be a RelativeDate)
|
|
84
|
+
DATE_AFTER = 'DATE_AFTER' # Requires ONE ConditionValue (may be a RelativeDate)
|
|
85
|
+
DATE_ON_OR_BEFORE = 'DATE_ON_OR_BEFORE' # Requires ONE ConditionValue (may be a RelativeDate)
|
|
86
|
+
DATE_ON_OR_AFTER = 'DATE_ON_OR_AFTER' # Requires ONE ConditionValue (may be a RelativeDate)
|
|
87
|
+
DATE_BETWEEN = 'DATE_BETWEEN' # Requires TWO ConditionValue
|
|
88
|
+
DATE_NOT_BETWEEN = 'DATE_NOT_BETWEEN' # Requires TWO ConditionValue
|
|
89
|
+
DATE_IS_VALID = 'DATE_IS_VALID' # Requires NO ConditionValue
|
|
90
|
+
ONE_OF_RANGE = 'ONE_OF_RANGE' # Requires ONE ConditionValue
|
|
91
|
+
ONE_OF_LIST = 'ONE_OF_LIST' # Supports ANY NUMBER of ConditionValue
|
|
92
|
+
BLANK = 'BLANK' # Requires NO ConditionValue
|
|
93
|
+
NOT_BLANK = 'NOT_BLANK' # Requires NO ConditionValue
|
|
94
|
+
CUSTOM_FORMULA = 'CUSTOM_FORMULA' # Requires ONE ConditionValue
|
|
95
|
+
BOOLEAN = 'BOOLEAN' # Supports ZERO, ONE or TWO ConditionValue
|
|
96
|
+
TEXT_NOT_EQ = 'TEXT_NOT_EQ' # Requires AT LEAST ONE ConditionValue
|
|
97
|
+
DATE_NOT_EQ = 'DATE_NOT_EQ' # Requires AT LEAST ONE ConditionValue
|
|
98
|
+
FILTER_EXPRESSION = 'FILTER_EXPRESSION' # Requires ONE ConditionValue
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class BooleanCondition(BaseModel):
|
|
102
|
+
type: ConditionType
|
|
103
|
+
values: list[ConditionValue]
|
|
104
|
+
|
|
105
|
+
|
|
33
106
|
class BooleanRule(BaseModel):
|
|
34
|
-
condition:
|
|
35
|
-
format:
|
|
107
|
+
condition: BooleanCondition
|
|
108
|
+
format: CellFormat
|
|
36
109
|
|
|
37
110
|
|
|
38
111
|
class GradientRule(BaseModel):
|
|
39
112
|
minpoint: InterpolationPoint
|
|
40
|
-
midpoint: InterpolationPoint = None
|
|
113
|
+
midpoint: InterpolationPoint | None = None
|
|
41
114
|
maxpoint: InterpolationPoint
|
|
42
115
|
|
|
43
116
|
|
|
44
117
|
class ConditionalFormatRule(BaseModel):
|
|
45
118
|
ranges: list[GridRange]
|
|
46
|
-
|
|
47
|
-
|
|
119
|
+
|
|
120
|
+
# Union field rule, exactly one must be set
|
|
121
|
+
boolean_rule: BooleanRule | None = Field(None, alias='booleanRule')
|
|
122
|
+
gradient_rule: GradientRule | None = Field(None, alias='gradientRule')
|
|
48
123
|
|
|
49
124
|
@model_validator(mode='before')
|
|
125
|
+
@classmethod
|
|
50
126
|
def init_before(cls, values: dict):
|
|
51
127
|
bool_rule = values.get('boolean_rule', values.get('booleanRule'))
|
|
52
128
|
grad_rule = values.get('gradient_rule', values.get('gradientRule'))
|
|
@@ -81,10 +157,11 @@ class UpdateConditionalFormatRule(BaseModel):
|
|
|
81
157
|
sheet_id: int = Field(..., serialization_alias='sheetId')
|
|
82
158
|
|
|
83
159
|
# Union field instruction can be only one of the following:
|
|
84
|
-
rule: ConditionalFormatRule = None
|
|
85
|
-
new_index: int = Field(None, serialization_alias='newIndex')
|
|
160
|
+
rule: ConditionalFormatRule | None = None
|
|
161
|
+
new_index: int | None = Field(None, serialization_alias='newIndex')
|
|
86
162
|
|
|
87
163
|
@model_validator(mode='before')
|
|
164
|
+
@classmethod
|
|
88
165
|
def init_before(cls, values: dict):
|
|
89
166
|
if ('rule' in values) == ('new_index' in values):
|
|
90
167
|
raise ValueError('either rule or new_index must be set, but not both')
|