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 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 col_num_to_letter, float_sum, rowcol_to_a1
21
- from .api_request import ApiRequest
22
- from .google_sheets import GoogleSheets
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
+ )
@@ -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[int | float | bool | str]] | list[int | float | bool | str] = None,
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.__split_excel_range(range_)
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 add_conditional_format_rule(
113
+ def add_boolean_format_rule(
106
114
  *,
107
115
  sheet_id: int,
108
116
  ranges: list[str],
109
- gradient_colors: tuple[ColorStyle, ColorStyle, ColorStyle] | tuple[ColorStyle, ColorStyle],
110
- gradient_points: tuple[float, float, float] = None
117
+ condition_type: ConditionType,
118
+ condition_values: list[ConditionValue] = None,
119
+ cell_format: CellFormat,
111
120
  ) -> dict:
112
- grid_ranges = [GridRange(sheet_id=sheet_id, **ApiRequest.__split_excel_range(range_, return_as_dict=True)) for range_ in ranges]
113
- if gradient_points is None:
114
- gradient_points = (None,) * len(gradient_colors)
115
- if len(gradient_colors) == 2:
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=gradient_colors[0],
121
- type=InterpolationPointType.MIN,
122
- value=str(gradient_points[0])
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=gradient_colors[1],
126
- type=InterpolationPointType.MAX,
127
- value=str(gradient_points[1])
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
- return AddConditionalFormatRule(rule=ConditionalFormatRule(
132
- ranges=grid_ranges,
133
- gradient_rule=GradientRule(
134
- minpoint=InterpolationPoint(
135
- color_style=gradient_colors[0],
136
- type=InterpolationPointType.NUMBER,
137
- value=str(gradient_points[0])
138
- ),
139
- midpoint=InterpolationPoint(
140
- color_style=gradient_colors[1],
141
- type=InterpolationPointType.NUMBER,
142
- value=str(gradient_points[1])
143
- ),
144
- maxpoint=InterpolationPoint(
145
- color_style=gradient_colors[2],
146
- type=InterpolationPointType.NUMBER,
147
- value=str(gradient_points[2])
148
- )
149
- )
150
- )).dict()
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.__split_excel_range(range_)
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.__split_excel_range(range_)
351
+ start_row, end_row, start_column, end_column = ApiRequest._split_excel_range(range_)
227
352
  else:
228
- start_column = ApiRequest.__get_column_index(start_column) if isinstance(start_column, str) else start_column
229
- end_column = ApiRequest.__get_column_index(end_column) if isinstance(end_column, str) else end_column
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.__get_column_index(col_no_or_letter)
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 __split_excel_range(range_: str, return_as_dict: bool = False) -> tuple[int, int, int, int] | dict[str, int]:
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"([A-Z]+)(\d+):([A-Z]+)(\d+)$", range_)
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"([A-Z]+)(\d+)$", range_)
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.__get_column_index(start_column), ApiRequest.__get_column_index(end_column)
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 __get_column_index(column_letters: str) -> int:
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)
@@ -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
- SimpleType = str | int | float | bool
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=None) -> None:
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, destination_spreadsheet_id: str, source_sheet_id: str = 0, service=None) -> SheetProperties:
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=None) -> Spreadsheet:
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(spreadsheet_id: str, sheet: str | int, ranges: list[str], service=None) -> list[list[SimpleType] | list[list[SimpleType]]]:
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
- sheet (str | int): Name or ID of the sheet
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[str]]: List of values from the specified ranges in the same order
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
- if service is None:
142
- service = GoogleSheets.build_service()
142
+ assert len(sheets) == len(ranges), 'sheets and ranges must have the same length'
143
143
 
144
- if isinstance(sheet, int):
144
+ ranges_processed = []
145
+ if any(isinstance(sheet, int) for sheet in sheets):
145
146
  ss = GoogleSheets.get_spreadsheet(spreadsheet_id, service)
146
- try:
147
- sheet_name = next(sht.properties.title for sht in ss.sheets if sht.properties.sheet_id == sheet)
148
- except StopIteration:
149
- return []
150
- else:
151
- sheet_name = sheet
152
- ranges = [f'{sheet_name}!{range_}' for range_ in ranges]
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=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: str = None
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: dict
35
- format: dict
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
- boolean_rule: BooleanRule = Field(None, alias='booleanRule')
47
- gradient_rule: GradientRule = Field(None, alias='gradientRule')
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')
@@ -3,6 +3,9 @@ from enum import StrEnum
3
3
  from pydantic import BaseModel, Field, model_validator
4
4
 
5
5
 
6
+ SimpleType = str | int | float | bool
7
+
8
+
6
9
  class ThemeColorType(StrEnum):
7
10
  TEXT = 'TEXT'
8
11
  BACKGROUND = 'BACKGROUND'