arcade-google 0.1.6__py3-none-any.whl → 2.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.
- arcade_google/constants.py +24 -0
- arcade_google/critics.py +41 -0
- arcade_google/doc_to_html.py +99 -0
- arcade_google/doc_to_markdown.py +64 -0
- arcade_google/enums.py +0 -0
- arcade_google/exceptions.py +70 -0
- arcade_google/models.py +654 -0
- arcade_google/tools/__init__.py +96 -1
- arcade_google/tools/calendar.py +236 -32
- arcade_google/tools/contacts.py +96 -0
- arcade_google/tools/docs.py +24 -14
- arcade_google/tools/drive.py +256 -48
- arcade_google/tools/file_picker.py +54 -0
- arcade_google/tools/gmail.py +336 -116
- arcade_google/tools/sheets.py +144 -0
- arcade_google/utils.py +1564 -0
- arcade_google-2.0.0.dist-info/METADATA +27 -0
- arcade_google-2.0.0.dist-info/RECORD +21 -0
- {arcade_google-0.1.6.dist-info → arcade_google-2.0.0.dist-info}/WHEEL +1 -1
- arcade_google-2.0.0.dist-info/licenses/LICENSE +21 -0
- arcade_google/tools/models.py +0 -296
- arcade_google/tools/utils.py +0 -282
- arcade_google-0.1.6.dist-info/METADATA +0 -20
- arcade_google-0.1.6.dist-info/RECORD +0 -11
arcade_google/models.py
ADDED
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from datetime import date, datetime, time, timedelta
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Optional
|
|
5
|
+
from zoneinfo import ZoneInfo
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, field_validator, model_validator
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# ---------------------------------------------------------------------------- #
|
|
11
|
+
# Google Calendar Models and Enums
|
|
12
|
+
# ---------------------------------------------------------------------------- #
|
|
13
|
+
class DateRange(Enum):
|
|
14
|
+
TODAY = "today"
|
|
15
|
+
TOMORROW = "tomorrow"
|
|
16
|
+
THIS_WEEK = "this_week"
|
|
17
|
+
NEXT_WEEK = "next_week"
|
|
18
|
+
THIS_MONTH = "this_month"
|
|
19
|
+
NEXT_MONTH = "next_month"
|
|
20
|
+
|
|
21
|
+
def to_datetime_range(
|
|
22
|
+
self,
|
|
23
|
+
start_time: time | None = None,
|
|
24
|
+
end_time: time | None = None,
|
|
25
|
+
time_zone: ZoneInfo | None = None,
|
|
26
|
+
today: date | None = None,
|
|
27
|
+
) -> tuple[datetime, datetime]:
|
|
28
|
+
"""
|
|
29
|
+
Convert a DateRange enum value to a tuple with two datetime objects representing the start
|
|
30
|
+
and end of the date range.
|
|
31
|
+
|
|
32
|
+
:param start_time: The start time of the date range. Defaults to the current time.
|
|
33
|
+
:param end_time: The end time of the date range. Defaults to 23:59:59.
|
|
34
|
+
:param time_zone: The time zone to use for the date range. Defaults to UTC.
|
|
35
|
+
:param today: Today's date. Defaults to the current date provided by `datetime.now().date()`
|
|
36
|
+
"""
|
|
37
|
+
start_time = start_time or datetime.now().time()
|
|
38
|
+
end_time = end_time or time(23, 59, 59)
|
|
39
|
+
today = today or datetime.now().date()
|
|
40
|
+
|
|
41
|
+
if self == DateRange.TODAY:
|
|
42
|
+
start_date, end_date = today, today
|
|
43
|
+
elif self == DateRange.TOMORROW:
|
|
44
|
+
start_date, end_date = today + timedelta(days=1), today + timedelta(days=1)
|
|
45
|
+
elif self == DateRange.THIS_WEEK:
|
|
46
|
+
start_date = today - timedelta(days=today.weekday())
|
|
47
|
+
end_date = start_date + timedelta(days=6)
|
|
48
|
+
elif self == DateRange.NEXT_WEEK:
|
|
49
|
+
start_date = today + timedelta(days=7 - today.weekday())
|
|
50
|
+
end_date = start_date + timedelta(days=6)
|
|
51
|
+
elif self == DateRange.THIS_MONTH:
|
|
52
|
+
start_date = today.replace(day=1)
|
|
53
|
+
next_month = start_date + timedelta(days=31)
|
|
54
|
+
end_date = next_month.replace(day=1) - timedelta(days=1)
|
|
55
|
+
elif self == DateRange.NEXT_MONTH:
|
|
56
|
+
start_date = (today.replace(day=1) + timedelta(days=31)).replace(day=1)
|
|
57
|
+
next_month = start_date + timedelta(days=31)
|
|
58
|
+
end_date = next_month.replace(day=1) - timedelta(days=1)
|
|
59
|
+
else:
|
|
60
|
+
raise ValueError(
|
|
61
|
+
f"DateRange enum value: {self} is not supported for date range conversion"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
start_time = start_time or time(0, 0, 0)
|
|
65
|
+
end_time = end_time or time(23, 59, 59)
|
|
66
|
+
|
|
67
|
+
start_datetime = datetime.combine(start_date, start_time)
|
|
68
|
+
end_datetime = datetime.combine(end_date, end_time)
|
|
69
|
+
|
|
70
|
+
if time_zone:
|
|
71
|
+
start_datetime = start_datetime.replace(tzinfo=time_zone)
|
|
72
|
+
end_datetime = end_datetime.replace(tzinfo=time_zone)
|
|
73
|
+
|
|
74
|
+
return start_datetime, end_datetime
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class Day(Enum):
|
|
78
|
+
# TODO: THere are obvious limitations here. We should do better and support any date.
|
|
79
|
+
YESTERDAY = "yesterday"
|
|
80
|
+
TODAY = "today"
|
|
81
|
+
TOMORROW = "tomorrow"
|
|
82
|
+
THIS_SUNDAY = "this_sunday"
|
|
83
|
+
THIS_MONDAY = "this_monday"
|
|
84
|
+
THIS_TUESDAY = "this_tuesday"
|
|
85
|
+
THIS_WEDNESDAY = "this_wednesday"
|
|
86
|
+
THIS_THURSDAY = "this_thursday"
|
|
87
|
+
THIS_FRIDAY = "this_friday"
|
|
88
|
+
THIS_SATURDAY = "this_saturday"
|
|
89
|
+
NEXT_SUNDAY = "next_sunday"
|
|
90
|
+
NEXT_MONDAY = "next_monday"
|
|
91
|
+
NEXT_TUESDAY = "next_tuesday"
|
|
92
|
+
NEXT_WEDNESDAY = "next_wednesday"
|
|
93
|
+
NEXT_THURSDAY = "next_thursday"
|
|
94
|
+
NEXT_FRIDAY = "next_friday"
|
|
95
|
+
NEXT_SATURDAY = "next_saturday"
|
|
96
|
+
|
|
97
|
+
def to_date(self, time_zone_name: str) -> date:
|
|
98
|
+
time_zone = ZoneInfo(time_zone_name)
|
|
99
|
+
today = datetime.now(time_zone).date()
|
|
100
|
+
weekday = today.weekday()
|
|
101
|
+
|
|
102
|
+
if self == Day.YESTERDAY:
|
|
103
|
+
return today - timedelta(days=1)
|
|
104
|
+
elif self == Day.TODAY:
|
|
105
|
+
return today
|
|
106
|
+
elif self == Day.TOMORROW:
|
|
107
|
+
return today + timedelta(days=1)
|
|
108
|
+
|
|
109
|
+
day_offsets = {
|
|
110
|
+
Day.THIS_SUNDAY: 6,
|
|
111
|
+
Day.THIS_MONDAY: 0,
|
|
112
|
+
Day.THIS_TUESDAY: 1,
|
|
113
|
+
Day.THIS_WEDNESDAY: 2,
|
|
114
|
+
Day.THIS_THURSDAY: 3,
|
|
115
|
+
Day.THIS_FRIDAY: 4,
|
|
116
|
+
Day.THIS_SATURDAY: 5,
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if self in day_offsets:
|
|
120
|
+
return today + timedelta(days=(day_offsets[self] - weekday) % 7)
|
|
121
|
+
|
|
122
|
+
next_week_offsets = {
|
|
123
|
+
Day.NEXT_SUNDAY: 6,
|
|
124
|
+
Day.NEXT_MONDAY: 0,
|
|
125
|
+
Day.NEXT_TUESDAY: 1,
|
|
126
|
+
Day.NEXT_WEDNESDAY: 2,
|
|
127
|
+
Day.NEXT_THURSDAY: 3,
|
|
128
|
+
Day.NEXT_FRIDAY: 4,
|
|
129
|
+
Day.NEXT_SATURDAY: 5,
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if self in next_week_offsets:
|
|
133
|
+
return today + timedelta(days=(next_week_offsets[self] - weekday + 7) % 7)
|
|
134
|
+
|
|
135
|
+
raise ValueError(f"Invalid Day enum value: {self}")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class TimeSlot(Enum):
|
|
139
|
+
_0000 = "00:00"
|
|
140
|
+
_0015 = "00:15"
|
|
141
|
+
_0030 = "00:30"
|
|
142
|
+
_0045 = "00:45"
|
|
143
|
+
_0100 = "01:00"
|
|
144
|
+
_0115 = "01:15"
|
|
145
|
+
_0130 = "01:30"
|
|
146
|
+
_0145 = "01:45"
|
|
147
|
+
_0200 = "02:00"
|
|
148
|
+
_0215 = "02:15"
|
|
149
|
+
_0230 = "02:30"
|
|
150
|
+
_0245 = "02:45"
|
|
151
|
+
_0300 = "03:00"
|
|
152
|
+
_0315 = "03:15"
|
|
153
|
+
_0330 = "03:30"
|
|
154
|
+
_0345 = "03:45"
|
|
155
|
+
_0400 = "04:00"
|
|
156
|
+
_0415 = "04:15"
|
|
157
|
+
_0430 = "04:30"
|
|
158
|
+
_0445 = "04:45"
|
|
159
|
+
_0500 = "05:00"
|
|
160
|
+
_0515 = "05:15"
|
|
161
|
+
_0530 = "05:30"
|
|
162
|
+
_0545 = "05:45"
|
|
163
|
+
_0600 = "06:00"
|
|
164
|
+
_0615 = "06:15"
|
|
165
|
+
_0630 = "06:30"
|
|
166
|
+
_0645 = "06:45"
|
|
167
|
+
_0700 = "07:00"
|
|
168
|
+
_0715 = "07:15"
|
|
169
|
+
_0730 = "07:30"
|
|
170
|
+
_0745 = "07:45"
|
|
171
|
+
_0800 = "08:00"
|
|
172
|
+
_0815 = "08:15"
|
|
173
|
+
_0830 = "08:30"
|
|
174
|
+
_0845 = "08:45"
|
|
175
|
+
_0900 = "09:00"
|
|
176
|
+
_0915 = "09:15"
|
|
177
|
+
_0930 = "09:30"
|
|
178
|
+
_0945 = "09:45"
|
|
179
|
+
_1000 = "10:00"
|
|
180
|
+
_1015 = "10:15"
|
|
181
|
+
_1030 = "10:30"
|
|
182
|
+
_1045 = "10:45"
|
|
183
|
+
_1100 = "11:00"
|
|
184
|
+
_1115 = "11:15"
|
|
185
|
+
_1130 = "11:30"
|
|
186
|
+
_1145 = "11:45"
|
|
187
|
+
_1200 = "12:00"
|
|
188
|
+
_1215 = "12:15"
|
|
189
|
+
_1230 = "12:30"
|
|
190
|
+
_1245 = "12:45"
|
|
191
|
+
_1300 = "13:00"
|
|
192
|
+
_1315 = "13:15"
|
|
193
|
+
_1330 = "13:30"
|
|
194
|
+
_1345 = "13:45"
|
|
195
|
+
_1400 = "14:00"
|
|
196
|
+
_1415 = "14:15"
|
|
197
|
+
_1430 = "14:30"
|
|
198
|
+
_1445 = "14:45"
|
|
199
|
+
_1500 = "15:00"
|
|
200
|
+
_1515 = "15:15"
|
|
201
|
+
_1530 = "15:30"
|
|
202
|
+
_1545 = "15:45"
|
|
203
|
+
_1600 = "16:00"
|
|
204
|
+
_1615 = "16:15"
|
|
205
|
+
_1630 = "16:30"
|
|
206
|
+
_1645 = "16:45"
|
|
207
|
+
_1700 = "17:00"
|
|
208
|
+
_1715 = "17:15"
|
|
209
|
+
_1730 = "17:30"
|
|
210
|
+
_1745 = "17:45"
|
|
211
|
+
_1800 = "18:00"
|
|
212
|
+
_1815 = "18:15"
|
|
213
|
+
_1830 = "18:30"
|
|
214
|
+
_1845 = "18:45"
|
|
215
|
+
_1900 = "19:00"
|
|
216
|
+
_1915 = "19:15"
|
|
217
|
+
_1930 = "19:30"
|
|
218
|
+
_1945 = "19:45"
|
|
219
|
+
_2000 = "20:00"
|
|
220
|
+
_2015 = "20:15"
|
|
221
|
+
_2030 = "20:30"
|
|
222
|
+
_2045 = "20:45"
|
|
223
|
+
_2100 = "21:00"
|
|
224
|
+
_2115 = "21:15"
|
|
225
|
+
_2130 = "21:30"
|
|
226
|
+
_2145 = "21:45"
|
|
227
|
+
_2200 = "22:00"
|
|
228
|
+
_2215 = "22:15"
|
|
229
|
+
_2230 = "22:30"
|
|
230
|
+
_2245 = "22:45"
|
|
231
|
+
_2300 = "23:00"
|
|
232
|
+
_2315 = "23:15"
|
|
233
|
+
_2330 = "23:30"
|
|
234
|
+
_2345 = "23:45"
|
|
235
|
+
|
|
236
|
+
def to_time(self) -> time:
|
|
237
|
+
return datetime.strptime(self.value, "%H:%M").time()
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
class EventVisibility(Enum):
|
|
241
|
+
DEFAULT = "default"
|
|
242
|
+
PUBLIC = "public"
|
|
243
|
+
PRIVATE = "private"
|
|
244
|
+
CONFIDENTIAL = "confidential"
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
class EventType(Enum):
|
|
248
|
+
BIRTHDAY = "birthday" # Special all-day events with an annual recurrence.
|
|
249
|
+
DEFAULT = "default" # Regular events
|
|
250
|
+
FOCUS_TIME = "focusTime" # Focus time events
|
|
251
|
+
FROM_GMAIL = "fromGmail" # Events from Gmail
|
|
252
|
+
OUT_OF_OFFICE = "outOfOffice" # Out of office events
|
|
253
|
+
WORKING_LOCATION = "workingLocation" # Working location events
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
class SendUpdatesOptions(Enum):
|
|
257
|
+
NONE = "none" # No notifications are sent
|
|
258
|
+
ALL = "all" # Notifications are sent to all guests
|
|
259
|
+
EXTERNAL_ONLY = "externalOnly" # Notifications are sent to non-Google Calendar guests only.
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
# ---------------------------------------------------------------------------- #
|
|
263
|
+
# Google Drive Models and Enums
|
|
264
|
+
# ---------------------------------------------------------------------------- #
|
|
265
|
+
class Corpora(str, Enum):
|
|
266
|
+
"""
|
|
267
|
+
Bodies of items (files/documents) to which the query applies.
|
|
268
|
+
Prefer 'user' or 'drive' to 'allDrives' for efficiency.
|
|
269
|
+
By default, corpora is set to 'user'.
|
|
270
|
+
"""
|
|
271
|
+
|
|
272
|
+
USER = "user"
|
|
273
|
+
DOMAIN = "domain"
|
|
274
|
+
DRIVE = "drive"
|
|
275
|
+
ALL_DRIVES = "allDrives"
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
class OrderBy(str, Enum):
|
|
279
|
+
"""
|
|
280
|
+
Sort keys for ordering files in Google Drive.
|
|
281
|
+
Each key has both ascending and descending options.
|
|
282
|
+
"""
|
|
283
|
+
|
|
284
|
+
CREATED_TIME = (
|
|
285
|
+
# When the file was created (ascending)
|
|
286
|
+
"createdTime"
|
|
287
|
+
)
|
|
288
|
+
CREATED_TIME_DESC = (
|
|
289
|
+
# When the file was created (descending)
|
|
290
|
+
"createdTime desc"
|
|
291
|
+
)
|
|
292
|
+
FOLDER = (
|
|
293
|
+
# The folder ID, sorted using alphabetical ordering (ascending)
|
|
294
|
+
"folder"
|
|
295
|
+
)
|
|
296
|
+
FOLDER_DESC = (
|
|
297
|
+
# The folder ID, sorted using alphabetical ordering (descending)
|
|
298
|
+
"folder desc"
|
|
299
|
+
)
|
|
300
|
+
MODIFIED_BY_ME_TIME = (
|
|
301
|
+
# The last time the file was modified by the user (ascending)
|
|
302
|
+
"modifiedByMeTime"
|
|
303
|
+
)
|
|
304
|
+
MODIFIED_BY_ME_TIME_DESC = (
|
|
305
|
+
# The last time the file was modified by the user (descending)
|
|
306
|
+
"modifiedByMeTime desc"
|
|
307
|
+
)
|
|
308
|
+
MODIFIED_TIME = (
|
|
309
|
+
# The last time the file was modified by anyone (ascending)
|
|
310
|
+
"modifiedTime"
|
|
311
|
+
)
|
|
312
|
+
MODIFIED_TIME_DESC = (
|
|
313
|
+
# The last time the file was modified by anyone (descending)
|
|
314
|
+
"modifiedTime desc"
|
|
315
|
+
)
|
|
316
|
+
NAME = (
|
|
317
|
+
# The name of the file, sorted using alphabetical ordering (e.g., 1, 12, 2, 22) (ascending)
|
|
318
|
+
"name"
|
|
319
|
+
)
|
|
320
|
+
NAME_DESC = (
|
|
321
|
+
# The name of the file, sorted using alphabetical ordering (e.g., 1, 12, 2, 22) (descending)
|
|
322
|
+
"name desc"
|
|
323
|
+
)
|
|
324
|
+
NAME_NATURAL = (
|
|
325
|
+
# The name of the file, sorted using natural sort ordering (e.g., 1, 2, 12, 22) (ascending)
|
|
326
|
+
"name_natural"
|
|
327
|
+
)
|
|
328
|
+
NAME_NATURAL_DESC = (
|
|
329
|
+
# The name of the file, sorted using natural sort ordering (e.g., 1, 2, 12, 22) (descending)
|
|
330
|
+
"name_natural desc"
|
|
331
|
+
)
|
|
332
|
+
QUOTA_BYTES_USED = (
|
|
333
|
+
# The number of storage quota bytes used by the file (ascending)
|
|
334
|
+
"quotaBytesUsed"
|
|
335
|
+
)
|
|
336
|
+
QUOTA_BYTES_USED_DESC = (
|
|
337
|
+
# The number of storage quota bytes used by the file (descending)
|
|
338
|
+
"quotaBytesUsed desc"
|
|
339
|
+
)
|
|
340
|
+
RECENCY = (
|
|
341
|
+
# The most recent timestamp from the file's date-time fields (ascending)
|
|
342
|
+
"recency"
|
|
343
|
+
)
|
|
344
|
+
RECENCY_DESC = (
|
|
345
|
+
# The most recent timestamp from the file's date-time fields (descending)
|
|
346
|
+
"recency desc"
|
|
347
|
+
)
|
|
348
|
+
SHARED_WITH_ME_TIME = (
|
|
349
|
+
# When the file was shared with the user, if applicable (ascending)
|
|
350
|
+
"sharedWithMeTime"
|
|
351
|
+
)
|
|
352
|
+
SHARED_WITH_ME_TIME_DESC = (
|
|
353
|
+
# When the file was shared with the user, if applicable (descending)
|
|
354
|
+
"sharedWithMeTime desc"
|
|
355
|
+
)
|
|
356
|
+
STARRED = (
|
|
357
|
+
# Whether the user has starred the file (ascending)
|
|
358
|
+
"starred"
|
|
359
|
+
)
|
|
360
|
+
STARRED_DESC = (
|
|
361
|
+
# Whether the user has starred the file (descending)
|
|
362
|
+
"starred desc"
|
|
363
|
+
)
|
|
364
|
+
VIEWED_BY_ME_TIME = (
|
|
365
|
+
# The last time the file was viewed by the user (ascending)
|
|
366
|
+
"viewedByMeTime"
|
|
367
|
+
)
|
|
368
|
+
VIEWED_BY_ME_TIME_DESC = (
|
|
369
|
+
# The last time the file was viewed by the user (descending)
|
|
370
|
+
"viewedByMeTime desc"
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
class DocumentFormat(str, Enum):
|
|
375
|
+
MARKDOWN = "markdown"
|
|
376
|
+
HTML = "html"
|
|
377
|
+
GOOGLE_API_JSON = "google_api_json"
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
# ---------------------------------------------------------------------------- #
|
|
381
|
+
# Google Gmail Models and Enums
|
|
382
|
+
# ---------------------------------------------------------------------------- #
|
|
383
|
+
class GmailReplyToWhom(str, Enum):
|
|
384
|
+
EVERY_RECIPIENT = "every_recipient"
|
|
385
|
+
ONLY_THE_SENDER = "only_the_sender"
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
class GmailAction(str, Enum):
|
|
389
|
+
SEND = "send"
|
|
390
|
+
DRAFT = "draft"
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
# ---------------------------------------------------------------------------- #
|
|
394
|
+
# Google Sheets Models and Enums
|
|
395
|
+
# ---------------------------------------------------------------------------- #
|
|
396
|
+
class CellErrorType(str, Enum):
|
|
397
|
+
"""The type of error in a cell
|
|
398
|
+
|
|
399
|
+
Implementation of https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/other#ErrorType
|
|
400
|
+
"""
|
|
401
|
+
|
|
402
|
+
ERROR_TYPE_UNSPECIFIED = "ERROR_TYPE_UNSPECIFIED" # The default error type, do not use this.
|
|
403
|
+
ERROR = "ERROR" # Corresponds to the #ERROR! error.
|
|
404
|
+
NULL_VALUE = "NULL_VALUE" # Corresponds to the #NULL! error.
|
|
405
|
+
DIVIDE_BY_ZERO = "DIVIDE_BY_ZERO" # Corresponds to the #DIV/0 error.
|
|
406
|
+
VALUE = "VALUE" # Corresponds to the #VALUE! error.
|
|
407
|
+
REF = "REF" # Corresponds to the #REF! error.
|
|
408
|
+
NAME = "NAME" # Corresponds to the #NAME? error.
|
|
409
|
+
NUM = "NUM" # Corresponds to the #NUM! error.
|
|
410
|
+
N_A = "N_A" # Corresponds to the #N/A error.
|
|
411
|
+
LOADING = "LOADING" # Corresponds to the Loading... state.
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
class CellErrorValue(BaseModel):
|
|
415
|
+
"""An error in a cell
|
|
416
|
+
|
|
417
|
+
Implementation of https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/other#ErrorValue
|
|
418
|
+
"""
|
|
419
|
+
|
|
420
|
+
type: CellErrorType
|
|
421
|
+
message: str
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
class CellExtendedValue(BaseModel):
|
|
425
|
+
"""The kinds of value that a cell in a spreadsheet can have
|
|
426
|
+
|
|
427
|
+
Implementation of https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/other#ExtendedValue
|
|
428
|
+
"""
|
|
429
|
+
|
|
430
|
+
numberValue: float | None = None
|
|
431
|
+
stringValue: str | None = None
|
|
432
|
+
boolValue: bool | None = None
|
|
433
|
+
formulaValue: str | None = None
|
|
434
|
+
errorValue: Optional["CellErrorValue"] = None
|
|
435
|
+
|
|
436
|
+
@model_validator(mode="after")
|
|
437
|
+
def check_exactly_one_value(cls, instance): # type: ignore[no-untyped-def]
|
|
438
|
+
provided = [v for v in instance.__dict__.values() if v is not None]
|
|
439
|
+
if len(provided) != 1:
|
|
440
|
+
raise ValueError(
|
|
441
|
+
"Exactly one of numberValue, stringValue, boolValue, "
|
|
442
|
+
"formulaValue, or errorValue must be set."
|
|
443
|
+
)
|
|
444
|
+
return instance
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
class NumberFormatType(str, Enum):
|
|
448
|
+
NUMBER = "NUMBER"
|
|
449
|
+
PERCENT = "PERCENT"
|
|
450
|
+
CURRENCY = "CURRENCY"
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
class NumberFormat(BaseModel):
|
|
454
|
+
"""The format of a number
|
|
455
|
+
|
|
456
|
+
Implementation of https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/cells#NumberFormat
|
|
457
|
+
"""
|
|
458
|
+
|
|
459
|
+
pattern: str
|
|
460
|
+
type: NumberFormatType
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
class CellFormat(BaseModel):
|
|
464
|
+
"""The format of a cell
|
|
465
|
+
|
|
466
|
+
Partial implementation of https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/cells#CellFormat
|
|
467
|
+
"""
|
|
468
|
+
|
|
469
|
+
numberFormat: NumberFormat
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
class CellData(BaseModel):
|
|
473
|
+
"""Data about a specific cell
|
|
474
|
+
|
|
475
|
+
A partial implementation of https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/cells#CellData
|
|
476
|
+
"""
|
|
477
|
+
|
|
478
|
+
userEnteredValue: CellExtendedValue
|
|
479
|
+
userEnteredFormat: CellFormat | None = None
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
class RowData(BaseModel):
|
|
483
|
+
"""Data about each cellin a row
|
|
484
|
+
|
|
485
|
+
A partial implementation of https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/sheets#RowData
|
|
486
|
+
"""
|
|
487
|
+
|
|
488
|
+
values: list[CellData]
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
class GridData(BaseModel):
|
|
492
|
+
"""Data in the grid
|
|
493
|
+
|
|
494
|
+
A partial implementation of https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/sheets#GridData
|
|
495
|
+
"""
|
|
496
|
+
|
|
497
|
+
startRow: int
|
|
498
|
+
startColumn: int
|
|
499
|
+
rowData: list[RowData]
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
class GridProperties(BaseModel):
|
|
503
|
+
"""Properties of a grid
|
|
504
|
+
|
|
505
|
+
A partial implementation of https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/sheets#GridProperties
|
|
506
|
+
"""
|
|
507
|
+
|
|
508
|
+
rowCount: int
|
|
509
|
+
columnCount: int
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
class SheetProperties(BaseModel):
|
|
513
|
+
"""Properties of a Sheet
|
|
514
|
+
|
|
515
|
+
A partial implementation of https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/sheets#SheetProperties
|
|
516
|
+
"""
|
|
517
|
+
|
|
518
|
+
sheetId: int
|
|
519
|
+
title: str
|
|
520
|
+
gridProperties: GridProperties | None = None
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
class Sheet(BaseModel):
|
|
524
|
+
"""A Sheet in a spreadsheet
|
|
525
|
+
|
|
526
|
+
A partial implementation of https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/sheets#Sheet
|
|
527
|
+
"""
|
|
528
|
+
|
|
529
|
+
properties: SheetProperties
|
|
530
|
+
data: list[GridData] | None = None
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
class SpreadsheetProperties(BaseModel):
|
|
534
|
+
"""Properties of a spreadsheet
|
|
535
|
+
|
|
536
|
+
A partial implementation of https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets#SpreadsheetProperties
|
|
537
|
+
"""
|
|
538
|
+
|
|
539
|
+
title: str
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
class Spreadsheet(BaseModel):
|
|
543
|
+
"""A spreadsheet
|
|
544
|
+
|
|
545
|
+
A partial implementation of https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets
|
|
546
|
+
"""
|
|
547
|
+
|
|
548
|
+
properties: SpreadsheetProperties
|
|
549
|
+
sheets: list[Sheet]
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
CellValue = int | float | str | bool
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
class SheetDataInput(BaseModel):
|
|
556
|
+
"""
|
|
557
|
+
SheetDataInput models the cell data of a spreadsheet in a custom format.
|
|
558
|
+
|
|
559
|
+
It is a dictionary mapping row numbers (as ints) to dictionaries that map
|
|
560
|
+
column letters (as uppercase strings) to cell values (int, float, str, or bool).
|
|
561
|
+
|
|
562
|
+
This model enforces that:
|
|
563
|
+
- The outer keys are convertible to int.
|
|
564
|
+
- The inner keys are alphabetic strings (normalized to uppercase).
|
|
565
|
+
- All cell values are only of type int, float, str, or bool.
|
|
566
|
+
|
|
567
|
+
The model automatically serializes (via `json_data()`)
|
|
568
|
+
and validates the inner types.
|
|
569
|
+
"""
|
|
570
|
+
|
|
571
|
+
data: dict[int, dict[str, CellValue]]
|
|
572
|
+
|
|
573
|
+
@classmethod
|
|
574
|
+
def _parse_json_if_string(cls, value): # type: ignore[no-untyped-def]
|
|
575
|
+
"""Parses the value if it is a JSON string, otherwise returns it.
|
|
576
|
+
|
|
577
|
+
Helper method for when validating the `data` field.
|
|
578
|
+
"""
|
|
579
|
+
if isinstance(value, str):
|
|
580
|
+
try:
|
|
581
|
+
return json.loads(value)
|
|
582
|
+
except json.JSONDecodeError as e:
|
|
583
|
+
raise TypeError(f"Invalid JSON: {e}")
|
|
584
|
+
return value
|
|
585
|
+
|
|
586
|
+
@classmethod
|
|
587
|
+
def _validate_row_key(cls, row_key) -> int: # type: ignore[no-untyped-def]
|
|
588
|
+
"""Converts the row key to an integer, raising an error if conversion fails.
|
|
589
|
+
|
|
590
|
+
Helper method for when validating the `data` field.
|
|
591
|
+
"""
|
|
592
|
+
try:
|
|
593
|
+
return int(row_key)
|
|
594
|
+
except (ValueError, TypeError):
|
|
595
|
+
raise TypeError(f"Row key '{row_key}' is not convertible to int.")
|
|
596
|
+
|
|
597
|
+
@classmethod
|
|
598
|
+
def _validate_inner_cells(cls, cells, row_int: int) -> dict: # type: ignore[no-untyped-def]
|
|
599
|
+
"""Validates that 'cells' is a dict mapping column letters to valid cell values
|
|
600
|
+
and normalizes the keys.
|
|
601
|
+
|
|
602
|
+
Helper method for when validating the `data` field.
|
|
603
|
+
"""
|
|
604
|
+
if not isinstance(cells, dict):
|
|
605
|
+
raise TypeError(
|
|
606
|
+
f"Value for row '{row_int}' must be a dict mapping column letters to cell values."
|
|
607
|
+
)
|
|
608
|
+
new_inner = {}
|
|
609
|
+
for col_key, cell_value in cells.items():
|
|
610
|
+
if not isinstance(col_key, str):
|
|
611
|
+
raise TypeError(f"Column key '{col_key}' must be a string.")
|
|
612
|
+
col_string = col_key.upper()
|
|
613
|
+
if not col_string.isalpha():
|
|
614
|
+
raise TypeError(f"Column key '{col_key}' is invalid. Must be alphabetic.")
|
|
615
|
+
if not isinstance(cell_value, int | float | str | bool):
|
|
616
|
+
raise TypeError(
|
|
617
|
+
f"Cell value for {col_string}{row_int} must be an int, float, str, or bool."
|
|
618
|
+
)
|
|
619
|
+
new_inner[col_string] = cell_value
|
|
620
|
+
return new_inner
|
|
621
|
+
|
|
622
|
+
@field_validator("data", mode="before")
|
|
623
|
+
@classmethod
|
|
624
|
+
def validate_and_convert_keys(cls, value): # type: ignore[no-untyped-def]
|
|
625
|
+
"""
|
|
626
|
+
Validates data when SheetDataInput is instantiated and converts it to the correct format.
|
|
627
|
+
Uses private helper methods to parse JSON, validate row keys, and validate inner cell data.
|
|
628
|
+
"""
|
|
629
|
+
if value is None:
|
|
630
|
+
return {}
|
|
631
|
+
|
|
632
|
+
value = cls._parse_json_if_string(value)
|
|
633
|
+
if isinstance(value, dict):
|
|
634
|
+
new_value = {}
|
|
635
|
+
for row_key, cells in value.items():
|
|
636
|
+
row_int = cls._validate_row_key(row_key)
|
|
637
|
+
inner_cells = cls._validate_inner_cells(cells, row_int)
|
|
638
|
+
new_value[row_int] = inner_cells
|
|
639
|
+
return new_value
|
|
640
|
+
|
|
641
|
+
raise TypeError("data must be a dict or a valid JSON string representing a dict")
|
|
642
|
+
|
|
643
|
+
def json_data(self) -> str:
|
|
644
|
+
"""
|
|
645
|
+
Serialize the sheet data to a JSON string.
|
|
646
|
+
"""
|
|
647
|
+
return json.dumps(self.data)
|
|
648
|
+
|
|
649
|
+
@classmethod
|
|
650
|
+
def from_json(cls, json_str: str) -> "SheetDataInput":
|
|
651
|
+
"""
|
|
652
|
+
Create a SheetData instance from a JSON string.
|
|
653
|
+
"""
|
|
654
|
+
return cls.model_validate_json(json_str)
|