python-google-sheets 0.1.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.
@@ -0,0 +1,60 @@
1
+ from .conditional_format_rule import (
2
+ AddConditionalFormatRule,
3
+ DeleteConditionalFormatRule,
4
+ UpdateConditionalFormatRule,
5
+ ConditionalFormatRule,
6
+ GradientRule,
7
+ InterpolationPoint,
8
+ InterpolationPointType,
9
+ )
10
+ from .update_sheet_properties import (
11
+ UpdateSheetProperties,
12
+ SheetProperties,
13
+ GridProperties,
14
+ )
15
+ from .merge_cells import (
16
+ MergeType,
17
+ MergeCells,
18
+ UnmergeCells,
19
+ )
20
+ from .dimension import (
21
+ InsertDimension,
22
+ UpdateDimensionProperties,
23
+ AddDimensionGroup,
24
+ DeleteDimensionGroup,
25
+ DimensionProperties,
26
+ DimensionRange,
27
+ Dimension,
28
+ )
29
+ from .update_cells import (
30
+ UpdateCells,
31
+ RowData,
32
+ CellData,
33
+ ExtendedValue,
34
+ CellFormat,
35
+ NumberFormat,
36
+ NumberFormatType,
37
+ TextFormat,
38
+ TextDirection,
39
+ TextRotation,
40
+ Borders,
41
+ Border,
42
+ Style as BorderStyle,
43
+ HorizontalAlignment,
44
+ VerticalAlignment,
45
+ WrapStrategy,
46
+ )
47
+ from .spreadsheet import (
48
+ Spreadsheet,
49
+ SpreadsheetProperties,
50
+ Sheet,
51
+ AddSheet,
52
+ DeleteSheet,
53
+ )
54
+
55
+ from .general_models import (
56
+ Color,
57
+ ColorStyle,
58
+ GridRange,
59
+ FieldMask,
60
+ )
@@ -0,0 +1,95 @@
1
+ """
2
+ This module contains the models for the following Google Sheets API requests:
3
+
4
+ - AddConditionalFormatRule
5
+ - DeleteConditionalFormatRule
6
+ """
7
+
8
+ import json
9
+ from enum import StrEnum
10
+
11
+ from pydantic import BaseModel, Field, model_validator
12
+
13
+ from .general_models import ColorStyle, GridRange
14
+
15
+
16
+ class InterpolationPointType(StrEnum):
17
+ MIN = 'MIN'
18
+ MAX = 'MAX'
19
+ NUMBER = 'NUMBER'
20
+ PERCENT = 'PERCENT'
21
+ PERCENTILE = 'PERCENTILE'
22
+
23
+
24
+ class InterpolationPoint(BaseModel):
25
+ color_style: ColorStyle = Field(..., alias='colorStyle')
26
+ type: InterpolationPointType
27
+ value: str = None
28
+
29
+ class Config:
30
+ populate_by_name = True
31
+
32
+
33
+ class BooleanRule(BaseModel):
34
+ condition: dict
35
+ format: dict
36
+
37
+
38
+ class GradientRule(BaseModel):
39
+ minpoint: InterpolationPoint
40
+ midpoint: InterpolationPoint = None
41
+ maxpoint: InterpolationPoint
42
+
43
+
44
+ class ConditionalFormatRule(BaseModel):
45
+ ranges: list[GridRange]
46
+ boolean_rule: BooleanRule = Field(None, alias='booleanRule')
47
+ gradient_rule: GradientRule = Field(None, alias='gradientRule')
48
+
49
+ @model_validator(mode='before')
50
+ def init_before(cls, values: dict):
51
+ bool_rule = values.get('boolean_rule', values.get('booleanRule'))
52
+ grad_rule = values.get('gradient_rule', values.get('gradientRule'))
53
+ if (bool_rule is None) == (grad_rule is None):
54
+ raise ValueError('either boolean_rule or gradient_rule must be set, but not both')
55
+ return values
56
+
57
+ class Config:
58
+ populate_by_name = True
59
+
60
+
61
+ class AddConditionalFormatRule(BaseModel):
62
+ rule: ConditionalFormatRule
63
+ index: int = 0
64
+
65
+ def dict(self, *args, **kwargs):
66
+ class_name = self.__class__.__name__[0].lower() + self.__class__.__name__[1:]
67
+ return {class_name: json.loads(super().json(*args, **kwargs, by_alias=True, exclude_none=True))}
68
+
69
+
70
+ class DeleteConditionalFormatRule(BaseModel):
71
+ index: int
72
+ sheet_id: int
73
+
74
+ def dict(self, *args, **kwargs):
75
+ class_name = self.__class__.__name__[0].lower() + self.__class__.__name__[1:]
76
+ return {class_name: json.loads(super().json(*args, **kwargs, by_alias=True, exclude_none=True))}
77
+
78
+
79
+ class UpdateConditionalFormatRule(BaseModel):
80
+ index: int
81
+ sheet_id: int = Field(..., serialization_alias='sheetId')
82
+
83
+ # Union field instruction can be only one of the following:
84
+ rule: ConditionalFormatRule = None
85
+ new_index: int = Field(None, serialization_alias='newIndex')
86
+
87
+ @model_validator(mode='before')
88
+ def init_before(cls, values: dict):
89
+ if ('rule' in values) == ('new_index' in values):
90
+ raise ValueError('either rule or new_index must be set, but not both')
91
+ return values
92
+
93
+ def dict(self, *args, **kwargs):
94
+ class_name = self.__class__.__name__[0].lower() + self.__class__.__name__[1:]
95
+ return {class_name: json.loads(super().json(*args, **kwargs, by_alias=True, exclude_none=True))}
@@ -0,0 +1,77 @@
1
+ """
2
+ This module contains the models for the following Google Sheets API requests:
3
+
4
+ - InsertDimension
5
+ - UpdateDimensionProperties
6
+ """
7
+
8
+ import json
9
+ from enum import StrEnum
10
+
11
+ from pydantic import BaseModel, Field, model_validator
12
+
13
+
14
+ class Dimension(StrEnum):
15
+ ROWS = 'ROWS'
16
+ COLUMNS = 'COLUMNS'
17
+
18
+
19
+ class DimensionRange(BaseModel):
20
+ sheet_id: int = Field(..., serialization_alias='sheetId', ge=0)
21
+ dimension: Dimension
22
+ start_index: int = Field(..., serialization_alias='startIndex', ge=0)
23
+ end_index: int = Field(..., serialization_alias='endIndex', gt=0)
24
+
25
+ @model_validator(mode='after')
26
+ def check_indexes(self):
27
+ if self.start_index >= self.end_index:
28
+ raise ValueError(f'start_index ({self.start_index}) must be less than end_index ({self.end_index})')
29
+ return self
30
+
31
+
32
+ class DimensionProperties(BaseModel):
33
+ hidden_by_user: bool = Field(None, alias='hiddenByUser')
34
+ pixel_size: int = Field(None, alias='pixelSize')
35
+ developer_metadata: dict = Field(None, alias='developerMetadata')
36
+ data_source_column_reference: dict = Field(None, alias='dataSourceColumnReference')
37
+
38
+ # Read-only
39
+ hidden_by_filter: bool = Field(None, alias='hiddenByFilter')
40
+
41
+ class Config:
42
+ populate_by_name = True
43
+
44
+
45
+ class InsertDimension(BaseModel):
46
+ range: DimensionRange
47
+ inherit_from_before: bool = Field(..., serialization_alias='inheritFromBefore')
48
+
49
+ def dict(self, *args, **kwargs):
50
+ class_name = self.__class__.__name__[0].lower() + self.__class__.__name__[1:]
51
+ return {class_name: json.loads(super().json(*args, **kwargs, by_alias=True, exclude_none=True))}
52
+
53
+
54
+ class UpdateDimensionProperties(BaseModel):
55
+ range: DimensionRange
56
+ properties: DimensionProperties
57
+ fields: str
58
+
59
+ def dict(self, *args, **kwargs):
60
+ class_name = self.__class__.__name__[0].lower() + self.__class__.__name__[1:]
61
+ return {class_name: json.loads(super().json(*args, **kwargs, by_alias=True, exclude_none=True))}
62
+
63
+
64
+ class AddDimensionGroup(BaseModel):
65
+ range: DimensionRange
66
+
67
+ def dict(self, *args, **kwargs):
68
+ class_name = self.__class__.__name__[0].lower() + self.__class__.__name__[1:]
69
+ return {class_name: json.loads(super().json(*args, **kwargs, by_alias=True, exclude_none=True))}
70
+
71
+
72
+ class DeleteDimensionGroup(BaseModel):
73
+ range: DimensionRange
74
+
75
+ def dict(self, *args, **kwargs):
76
+ class_name = self.__class__.__name__[0].lower() + self.__class__.__name__[1:]
77
+ return {class_name: json.loads(super().json(*args, **kwargs, by_alias=True, exclude_none=True))}
@@ -0,0 +1,79 @@
1
+ from enum import StrEnum
2
+
3
+ from pydantic import BaseModel, Field, model_validator
4
+
5
+
6
+ class ThemeColorType(StrEnum):
7
+ TEXT = 'TEXT'
8
+ BACKGROUND = 'BACKGROUND'
9
+ ACCENT1 = 'ACCENT1'
10
+ ACCENT2 = 'ACCENT2'
11
+ ACCENT3 = 'ACCENT3'
12
+ ACCENT4 = 'ACCENT4'
13
+ ACCENT5 = 'ACCENT5'
14
+ ACCENT6 = 'ACCENT6'
15
+ LINK = 'LINK'
16
+
17
+
18
+ class Color(BaseModel):
19
+ red: float = Field(1, ge=0, le=1)
20
+ green: float = Field(1, ge=0, le=1)
21
+ blue: float = Field(1, ge=0, le=1)
22
+ alpha: float = Field(1, ge=0, le=1)
23
+
24
+
25
+ class ColorStyle(BaseModel):
26
+ rgb_color: Color = Field(..., alias='rgbColor')
27
+ theme_color: ThemeColorType = Field(None, alias='themeColor')
28
+
29
+ class Config:
30
+ populate_by_name = True
31
+
32
+
33
+ class GridRange(BaseModel):
34
+ """
35
+ A range on a sheet. All indexes are zero-based. Indexes are half open, i.e. the start index is inclusive and the
36
+ end index is exclusive -- [startIndex, endIndex). Missing indexes indicate the range is unbounded on that side.
37
+ """
38
+
39
+ sheet_id: int = Field(None, alias='sheetId', ge=0)
40
+ start_row_index: int = Field(..., alias='startRowIndex', ge=0)
41
+ end_row_index: int = Field(..., alias='endRowIndex', gt=0)
42
+ start_column_index: int = Field(..., alias='startColumnIndex', ge=0)
43
+ end_column_index: int = Field(..., alias='endColumnIndex', gt=0)
44
+
45
+ @model_validator(mode='after')
46
+ def check_indexes(self):
47
+ if self.start_row_index >= self.end_row_index:
48
+ raise ValueError(f'start_row_index ({self.start_row_index}) must be less than end_row_index ({self.end_row_index})')
49
+ if self.start_column_index >= self.end_column_index:
50
+ raise ValueError(f'start_column_index ({self.start_column_index}) must be less than end_column_index ({self.end_column_index})')
51
+ return self
52
+
53
+ class Config:
54
+ populate_by_name = True
55
+
56
+
57
+ class FieldMask:
58
+ TITLE = 'title'
59
+ PIXEL_SIZE = 'pixelSize'
60
+
61
+ class GridProperties:
62
+ ALL = 'gridProperties'
63
+ ROW_COUNT = 'gridProperties.rowCount'
64
+ COLUMN_COUNT = 'gridProperties.columnCount'
65
+ FROZEN_ROW_COUNT = 'gridProperties.frozenRowCount'
66
+ FROZEN_COLUMN_COUNT = 'gridProperties.frozenColumnCount'
67
+ HIDE_GRID_LINES = 'gridProperties.hideGridlines'
68
+ ROW_GROUP_CONTROL_AFTER = 'gridProperties.rowGroupControlAfter'
69
+ COLUMN_GROUP_CONTROL_AFTER = 'gridProperties.columnGroupControlAfter'
70
+
71
+ class CellData:
72
+ USER_ENTERED_VALUE = 'userEnteredValue'
73
+ USER_ENTERED_FORMAT = 'userEnteredFormat'
74
+
75
+ class Spreadsheet:
76
+ ID = 'spreadsheetId'
77
+ URL = 'spreadsheetUrl'
78
+ PROPERTIES = 'properties'
79
+ SHEETS = 'sheets'
@@ -0,0 +1,35 @@
1
+ """
2
+ This module contains the models for the following Google Sheets API requests:
3
+
4
+ - MergeCells
5
+ - UnmergeCells
6
+ """
7
+
8
+ import json
9
+ from enum import StrEnum
10
+ from pydantic import BaseModel, Field
11
+
12
+ from .general_models import GridRange
13
+
14
+
15
+ class MergeType(StrEnum):
16
+ MERGE_ALL = 'MERGE_ALL'
17
+ MERGE_COLUMNS = 'MERGE_COLUMNS'
18
+ MERGE_ROWS = 'MERGE_ROWS'
19
+
20
+
21
+ class MergeCells(BaseModel):
22
+ range: GridRange
23
+ merge_type: MergeType = Field(..., serialization_alias='mergeType')
24
+
25
+ def dict(self, *args, **kwargs):
26
+ class_name = self.__class__.__name__[0].lower() + self.__class__.__name__[1:]
27
+ return {class_name: json.loads(super().json(*args, **kwargs, by_alias=True, exclude_none=True))}
28
+
29
+
30
+ class UnmergeCells(BaseModel):
31
+ range: GridRange
32
+
33
+ def dict(self, *args, **kwargs):
34
+ class_name = self.__class__.__name__[0].lower() + self.__class__.__name__[1:]
35
+ return {class_name: json.loads(super().json(*args, **kwargs, by_alias=True, exclude_none=True))}
@@ -0,0 +1,91 @@
1
+ import json
2
+ from enum import StrEnum
3
+
4
+ from pydantic import BaseModel, Field
5
+
6
+ from .update_sheet_properties import SheetProperties
7
+ from .update_cells import RowData
8
+ from .dimension import DimensionProperties
9
+ from .conditional_format_rule import ConditionalFormatRule
10
+ from .general_models import GridRange
11
+
12
+
13
+ class RecalculationInterval(StrEnum):
14
+ ON_CHANGE = 'ON_CHANGE'
15
+ MINUTE = 'MINUTE'
16
+ HOUR = 'HOUR'
17
+
18
+
19
+ class SpreadsheetProperties(BaseModel):
20
+ title: str
21
+ locale: str = 'ru_RU'
22
+ auto_recalc: RecalculationInterval = Field(None, alias='autoRecalc')
23
+ time_zone: str = Field('Europe/Moscow', alias='timeZone')
24
+ iterative_calculation_settings: dict = Field(None, alias='iterativeCalculationSettings')
25
+ spreadsheet_theme: dict = Field(None, alias='spreadsheetTheme')
26
+ import_functions_external_url_access_allowed: bool = Field(None, alias='importFunctionsExternalUrlAccessAllowed')
27
+
28
+ # Read-only
29
+ default_format: dict = Field(None, alias='defaultFormat')
30
+
31
+ class Config:
32
+ populate_by_name = True
33
+
34
+
35
+ class GridData(BaseModel):
36
+ start_row: int = Field(None, alias='startRow')
37
+ start_column: int = Field(None, alias='startColumn')
38
+ row_data: list[RowData] = Field(None, alias='rowData')
39
+ row_metadata: list[DimensionProperties] = Field(None, alias='rowMetadata')
40
+ column_metadata: list[DimensionProperties] = Field(None, alias='columnMetadata')
41
+
42
+
43
+ class Sheet(BaseModel):
44
+ properties: SheetProperties = None
45
+ data: list[GridData] = None
46
+ merges: list[GridRange] = None
47
+ conditional_formats: list[ConditionalFormatRule] = Field(None, alias='conditionalFormats')
48
+ filter_views: list[dict] = Field(None, alias='filterViews')
49
+ protected_ranges: list[dict] = Field(None, alias='protectedRanges')
50
+ basic_filter: dict = Field(None, alias='basicFilter')
51
+ charts: list[dict] = None
52
+ banded_ranges: list[dict] = Field(None, alias='bandedRanges')
53
+ developer_metadata: list[dict] = Field(None, alias='developerMetadata')
54
+ row_groups: list[dict] = Field(None, alias='rowGroups')
55
+ column_groups: list[dict] = Field(None, alias='columnGroups')
56
+ slicers: list[dict] = None
57
+
58
+ class Config:
59
+ populate_by_name = True
60
+
61
+
62
+ class Spreadsheet(BaseModel):
63
+ properties: SpreadsheetProperties = None
64
+ sheets: list[Sheet] = None
65
+ named_ranges: list[dict] = Field(None, alias='namedRanges')
66
+ developer_metadata: list[dict] = Field(None, alias='developerMetadata')
67
+ data_sources: list[dict] = Field(None, alias='dataSources')
68
+
69
+ # Read-only
70
+ spreadsheet_id: str = Field(None, alias='spreadsheetId')
71
+ spreadsheet_url: str = Field(None, alias='spreadsheetUrl')
72
+ data_source_schedules: list[dict] = Field(None, alias='dataSourceSchedules')
73
+
74
+ class Config:
75
+ populate_by_name = True
76
+
77
+
78
+ class DeleteSheet(BaseModel):
79
+ sheet_id: int = Field(..., serialization_alias='sheetId')
80
+
81
+ def dict(self, *args, **kwargs):
82
+ class_name = self.__class__.__name__[0].lower() + self.__class__.__name__[1:]
83
+ return {class_name: json.loads(super().json(*args, **kwargs, by_alias=True, exclude_none=True))}
84
+
85
+
86
+ class AddSheet(BaseModel):
87
+ properties: SheetProperties = None
88
+
89
+ def dict(self, *args, **kwargs):
90
+ class_name = self.__class__.__name__[0].lower() + self.__class__.__name__[1:]
91
+ return {class_name: json.loads(super().json(*args, **kwargs, by_alias=True, exclude_none=True))}
@@ -0,0 +1,242 @@
1
+ """
2
+ This module contains models for the following Google Sheets API requests:
3
+ - UpdateCells
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ from enum import StrEnum
10
+ from typing import Optional
11
+
12
+ from pydantic import BaseModel, Field, model_validator
13
+
14
+ from .general_models import GridRange, ColorStyle
15
+
16
+
17
+ class NumberFormatType(StrEnum):
18
+ TEXT = 'TEXT'
19
+ NUMBER = 'NUMBER'
20
+ PERCENT = 'PERCENT'
21
+ CURRENCY = 'CURRENCY'
22
+ DATE = 'DATE'
23
+ TIME = 'TIME'
24
+ DATE_TIME = 'DATE_TIME'
25
+ SCIENTIFIC = 'SCIENTIFIC'
26
+
27
+ class NumberFormat(BaseModel):
28
+ type_: NumberFormatType = Field(..., alias='type')
29
+ pattern: str = None
30
+
31
+
32
+ class Style(StrEnum):
33
+ DOTTED = 'DOTTED'
34
+ DASHED = 'DASHED'
35
+ SOLID = THIN = 'SOLID'
36
+ SOLID_MEDIUM = MEDIUM = 'SOLID_MEDIUM'
37
+ SOLID_THICK = THICK = 'SOLID_THICK'
38
+ NONE = 'NONE'
39
+ DOUBLE = 'DOUBLE'
40
+
41
+ class Border(BaseModel):
42
+ style: Style
43
+ color_style: ColorStyle = Field(None, serialization_alias='colorStyle')
44
+
45
+ class Borders(BaseModel):
46
+ top: Optional[Border] = None
47
+ bottom: Optional[Border] = None
48
+ left: Optional[Border] = None
49
+ right: Optional[Border] = None
50
+
51
+ @model_validator(mode='before')
52
+ def check_exclusive_fields(cls, values):
53
+ filled_fields = [key for key, value in values.items() if value is not None]
54
+ if len(filled_fields) == 0:
55
+ raise ValueError('at least one border must be set')
56
+ return values
57
+
58
+
59
+ class Padding(BaseModel):
60
+ top: Optional[int]
61
+ right: Optional[int]
62
+ bottom: Optional[int]
63
+ left: Optional[int]
64
+
65
+ @model_validator(mode='before')
66
+ def check_exclusive_fields(cls, values):
67
+ filled_fields = [key for key, value in values.items() if value is not None]
68
+ if len(filled_fields) == 0:
69
+ raise ValueError('at least one padding must be set')
70
+ return values
71
+
72
+
73
+ class HorizontalAlignment(StrEnum):
74
+ LEFT = 'LEFT'
75
+ CENTER = 'CENTER'
76
+ RIGHT = 'RIGHT'
77
+
78
+ class VerticalAlignment(StrEnum):
79
+ TOP = 'TOP'
80
+ MIDDLE = 'MIDDLE'
81
+ BOTTOM = 'BOTTOM'
82
+
83
+
84
+ class WrapStrategy(StrEnum):
85
+ OVERFLOW_CELL = 'OVERFLOW_CELL'
86
+ # LEGACY_WRAP = 'LEGACY_WRAP' # Not supported on all platforms
87
+ CLIP = 'CLIP'
88
+ WRAP = 'WRAP'
89
+
90
+
91
+ class TextDirection(StrEnum):
92
+ LEFT_TO_RIGHT = 'LEFT_TO_RIGHT'
93
+ RIGHT_TO_LEFT = 'RIGHT_TO_LEFT'
94
+
95
+
96
+ class Link:
97
+ uri: str = None
98
+
99
+ class TextFormat(BaseModel):
100
+ foreground_color_style: Optional[ColorStyle] = Field(None, serialization_alias='foregroundColorStyle')
101
+ font_family: Optional[str] = Field(None, serialization_alias='fontFamily')
102
+ font_size: Optional[int] = Field(None, serialization_alias='fontSize')
103
+ bold: Optional[bool] = None
104
+ italic: Optional[bool] = None
105
+ strikethrough: Optional[bool] = None
106
+ underline: Optional[bool] = None
107
+ link: Optional[Link] = None
108
+
109
+ class Config:
110
+ arbitrary_types_allowed = True
111
+
112
+
113
+ class HyperlinkDisplayType(StrEnum):
114
+ LINKED = 'LINKED'
115
+ PLAIN_TEXT = 'PLAIN_TEXT'
116
+
117
+
118
+ class TextRotation(BaseModel):
119
+ angle: int = Field(None, ge=-90, le=90) # Angle between the standard orientation and the desired orientation, direction is counterclockwise
120
+ vertical: bool = None # Text reads top to bottom, but the orientation of individual characters is unchanged
121
+
122
+ @model_validator(mode='before')
123
+ def check_exclusive_fields(cls, values):
124
+ filled_fields = [key for key, value in values.items() if value is not None]
125
+ if len(filled_fields) != 1:
126
+ raise ValueError(f'only one field must be set, but got {filled_fields}')
127
+ return values
128
+
129
+
130
+ class CellFormat(BaseModel):
131
+ number_format: Optional[NumberFormat] = Field(None, alias='numberFormat')
132
+ background_color_style: Optional[ColorStyle] = Field(None, alias='backgroundColorStyle')
133
+ borders: Optional[Borders] = None
134
+ padding: Optional[Padding] = None
135
+ horizontal_alignment: Optional[HorizontalAlignment] = Field(None, alias='horizontalAlignment')
136
+ vertical_alignment: Optional[VerticalAlignment] = Field(None, alias='verticalAlignment')
137
+ wrap_strategy: Optional[WrapStrategy] = Field(None, alias='wrapStrategy')
138
+ text_direction: Optional[TextDirection] = Field(None, alias='textDirection')
139
+ text_format: Optional[TextFormat] = Field(None, alias='textFormat')
140
+ hyperlink_display_type: Optional[HyperlinkDisplayType] = Field(None, alias='hyperlinkDisplayType')
141
+ text_rotation: Optional[TextRotation] = Field(None, alias='textRotation')
142
+
143
+ def __add__(self, other) -> CellFormat:
144
+ number_format = other.number_format or self.number_format
145
+ if other.number_format and self.number_format:
146
+ number_format = NumberFormat(
147
+ type=other.number_format.type_ or self.number_format.type_,
148
+ pattern=other.number_format.pattern or self.number_format.pattern,
149
+ )
150
+
151
+ borders = other.borders or self.borders
152
+ if other.borders and self.borders:
153
+ borders = Borders(
154
+ top=other.borders.top or self.borders.top,
155
+ bottom=other.borders.bottom or self.borders.bottom,
156
+ left=other.borders.left or self.borders.left,
157
+ right=other.borders.right or self.borders.right,
158
+ )
159
+
160
+ text_format = other.text_format or self.text_format
161
+ if other.text_format and self.text_format:
162
+ text_format = TextFormat(
163
+ foreground_color_style=other.text_format.foreground_color_style or self.text_format.foreground_color_style,
164
+ font_family=other.text_format.font_family or self.text_format.font_family,
165
+ font_size=other.text_format.font_size or self.text_format.font_size,
166
+ bold=other.text_format.bold or self.text_format.bold,
167
+ italic=other.text_format.italic or self.text_format.italic,
168
+ strikethrough=other.text_format.strikethrough or self.text_format.strikethrough,
169
+ underline=other.text_format.underline or self.text_format.underline,
170
+ link=other.text_format.link or self.text_format.link,
171
+ )
172
+
173
+ return CellFormat(
174
+ number_format=number_format,
175
+ background_color_style=other.background_color_style or self.background_color_style,
176
+ borders=borders,
177
+ padding=other.padding or self.padding,
178
+ horizontal_alignment=other.horizontal_alignment or self.horizontal_alignment,
179
+ vertical_alignment=other.vertical_alignment or self.vertical_alignment,
180
+ wrap_strategy=other.wrap_strategy or self.wrap_strategy,
181
+ text_direction=other.text_direction or self.text_direction,
182
+ text_format=text_format,
183
+ hyperlink_display_type=other.hyperlink_display_type or self.hyperlink_display_type,
184
+ text_rotation=other.text_rotation or self.text_rotation,
185
+ )
186
+
187
+ class Config:
188
+ populate_by_name = True
189
+
190
+
191
+ class ExtendedValue(BaseModel):
192
+ number_value: Optional[float] = Field(None, alias='numberValue')
193
+ string_value: Optional[str] = Field(None, alias='stringValue')
194
+ bool_value: Optional[bool] = Field(None, alias='boolValue')
195
+ formula_value: Optional[str] = Field(None, alias='formulaValue')
196
+
197
+ # Read-only
198
+ error_value: dict = Field(None, alias='errorValue')
199
+
200
+ @model_validator(mode='before')
201
+ def check_exclusive_fields(cls, values):
202
+ filled_fields = [key for key, value in values.items() if value is not None]
203
+ if len(filled_fields) != 1:
204
+ raise ValueError(f'only one field must be set, but got {filled_fields}')
205
+ return values
206
+
207
+ class Config:
208
+ populate_by_name = True
209
+
210
+
211
+ class CellData(BaseModel):
212
+ user_entered_value: Optional[ExtendedValue] = Field(None, alias='userEnteredValue')
213
+ user_entered_format: Optional[CellFormat] = Field(None, alias='userEnteredFormat')
214
+ note: str = None
215
+ text_format_runs: list[dict] = Field(None, alias='textFormatRuns')
216
+ data_validation: dict = Field(None, alias='dataValidation')
217
+ pivot_table: dict = Field(None, alias='pivotTable')
218
+ data_source_table: dict = Field(None, alias='dataSourceTable')
219
+ data_source_formula: dict = Field(None, alias='dataSourceFormula')
220
+
221
+ # Read-only
222
+ effective_value: ExtendedValue = Field(None, alias='effectiveValue')
223
+ formatted_value: str = Field(None, alias='formattedValue')
224
+ effective_format: CellFormat = Field(None, alias='effectiveFormat')
225
+ hyperlink: str = None
226
+
227
+ class Config:
228
+ populate_by_name = True
229
+
230
+
231
+ class RowData(BaseModel):
232
+ values: list[CellData]
233
+
234
+
235
+ class UpdateCells(BaseModel):
236
+ range: GridRange
237
+ rows: list[RowData] = []
238
+ fields: str
239
+
240
+ def dict(self, *args, **kwargs):
241
+ class_name = self.__class__.__name__[0].lower() + self.__class__.__name__[1:]
242
+ return {class_name: json.loads(super().json(*args, **kwargs, by_alias=True, exclude_none=True))}