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.
@@ -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)