coderpad-py 2026.7.22__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.
coderpad/types.py ADDED
@@ -0,0 +1,1045 @@
1
+ """Types for the CoderPad Interview API."""
2
+
3
+ import enum
4
+ from collections.abc import Iterable
5
+ from dataclasses import dataclass, field
6
+ from typing import Self, TypeVar
7
+
8
+ from beartype import beartype
9
+
10
+ from coderpad._dict_types import (
11
+ CandidateInstructionDict,
12
+ CustomDatabaseColumnDict,
13
+ CustomDatabaseDict,
14
+ CustomDatabaseSchemaDict,
15
+ CustomDatabaseTableDict,
16
+ CustomFileDict,
17
+ FileContentDict,
18
+ OrganizationDict,
19
+ OrganizationStatsDict,
20
+ OrganizationStatsUserDict,
21
+ OrganizationUserDict,
22
+ PadDict,
23
+ PadEnvironmentDict,
24
+ PadEventDict,
25
+ PadHistoryEntryDict,
26
+ PadInterviewerNotificationDict,
27
+ QuestionDict,
28
+ QuotaDict,
29
+ TeamDict,
30
+ TestCaseDict,
31
+ )
32
+
33
+ _T = TypeVar("_T")
34
+
35
+
36
+ @beartype
37
+ class PaginatedList(list[_T]):
38
+ """A list with pagination metadata from the API response."""
39
+
40
+ def __init__(
41
+ self,
42
+ iterable: Iterable[_T] = (),
43
+ /,
44
+ *,
45
+ total: int,
46
+ next_page: str | None = None,
47
+ ) -> None:
48
+ """Create a new paginated list.
49
+
50
+ Args:
51
+ iterable: The items for the list.
52
+ total: Total number of items across all pages.
53
+ next_page: URL for the next page, or ``None``.
54
+ """
55
+ super().__init__(iterable)
56
+ self.total = total
57
+ self.next_page = next_page
58
+
59
+
60
+ class SortOrder(enum.Enum):
61
+ """Sort order for list endpoints."""
62
+
63
+ CREATED_AT_ASC = "created_at,asc"
64
+ CREATED_AT_DESC = "created_at,desc"
65
+ UPDATED_AT_ASC = "updated_at,asc"
66
+ UPDATED_AT_DESC = "updated_at,desc"
67
+
68
+
69
+ class Language(enum.Enum):
70
+ """Programming language for pads and questions.
71
+
72
+ Single-file languages use a simple editor. Multi-file and
73
+ framework languages support ``file_contents`` and
74
+ ``zip_file`` uploads.
75
+ """
76
+
77
+ # Single-file languages
78
+ BASH = "bash"
79
+ C = "c"
80
+ CLOJURE = "clojure"
81
+ COFFEESCRIPT = "coffeescript"
82
+ CPP = "cpp"
83
+ CSHARP = "csharp"
84
+ DART = "dart"
85
+ ELIXIR = "elixir"
86
+ ERLANG = "erlang"
87
+ FSHARP = "fsharp"
88
+ GO = "go"
89
+ GSHEETS = "gsheets"
90
+ HACK = "hack"
91
+ HASKELL = "haskell"
92
+ HTML = "html"
93
+ JAVA = "java"
94
+ JAVASCRIPT = "javascript"
95
+ JULIA = "julia"
96
+ KOTLIN = "kotlin"
97
+ LUA = "lua"
98
+ MARKDOWN = "markdown"
99
+ MYSQL = "mysql"
100
+ OBJC = "objc"
101
+ OCAML = "ocaml"
102
+ PERL = "perl"
103
+ PHP = "php"
104
+ PLAINTEXT = "plaintext"
105
+ POSTGRESQL = "postgresql"
106
+ POWERSHELL = "powershell"
107
+ PYTHON = "python"
108
+ PYTHON3 = "python3"
109
+ R = "r"
110
+ RUBY = "ruby"
111
+ RUST = "rust"
112
+ SCALA = "scala"
113
+ SOLIDITY = "solidity"
114
+ SWIFT = "swift"
115
+ TCL = "tcl"
116
+ TYPESCRIPT = "typescript"
117
+ VB = "vb"
118
+ VERILOG = "verilog"
119
+
120
+ # Multi-file / framework languages
121
+ ANGULAR = "angular"
122
+ DJANGO = "django"
123
+ GIN = "gin"
124
+ KUBERNETES = "kubernetes"
125
+ MULTIFILE_C = "multifile_c"
126
+ MULTIFILE_CPP = "multifile_cpp"
127
+ MULTIFILE_CSHARP = "multifile_csharp"
128
+ MULTIFILE_GO = "multifile_go"
129
+ MULTIFILE_JAVA = "multifile_java"
130
+ MULTIFILE_KOTLIN = "multifile_kotlin"
131
+ MULTIFILE_PHP = "multifile_php"
132
+ MULTIFILE_PYTHON = "multifile_python"
133
+ MULTIFILE_RUBY = "multifile_ruby"
134
+ MULTIFILE_RUST = "multifile_rust"
135
+ MULTIFILE_SCALA = "multifile_scala"
136
+ MULTIFILE_SWIFT = "multifile_swift"
137
+ MULTIFILE_TERRAFORM = "multifile_terraform"
138
+ MULTIFILE_TYPESCRIPT = "multifile_typescript"
139
+ NEXTJS = "nextjs"
140
+ NODEJS = "nodejs"
141
+ RAILS = "rails"
142
+ REACT = "react"
143
+ REACT_NATIVE_WEB = "react-native-web"
144
+ SPRING = "spring"
145
+ SVELTE = "svelte"
146
+ UNIVERSAL = "universal"
147
+ VUE = "vue"
148
+
149
+
150
+ @beartype
151
+ @dataclass(frozen=True, kw_only=True)
152
+ class Team:
153
+ """A team within an organization."""
154
+
155
+ id: str
156
+ name: str
157
+
158
+ @classmethod
159
+ def from_dict(cls, data: TeamDict) -> Self:
160
+ """Create from an API response dictionary.
161
+
162
+ Args:
163
+ data: The dictionary to convert.
164
+
165
+ Returns:
166
+ A new instance.
167
+ """
168
+ return cls(
169
+ id=data["id"],
170
+ name=data["name"],
171
+ )
172
+
173
+
174
+ @beartype
175
+ @dataclass(frozen=True, kw_only=True)
176
+ class PadInterviewerNotification:
177
+ """An interviewer notification associated with a pad.
178
+
179
+ This structure is empirically observed in live API responses and is not
180
+ currently described by the published CoderPad API specification.
181
+ """
182
+
183
+ id: int
184
+ title: str
185
+ message: str
186
+ priority: str
187
+ request_id: str
188
+ auto_dismissed: bool
189
+ dismissed_at: str | None
190
+ useful: bool | None
191
+ created_at: str
192
+ updated_at: str
193
+
194
+ @classmethod
195
+ def from_dict(
196
+ cls,
197
+ data: PadInterviewerNotificationDict,
198
+ ) -> Self:
199
+ """Create from an API response dictionary.
200
+
201
+ Args:
202
+ data: The dictionary to convert.
203
+
204
+ Returns:
205
+ A new instance.
206
+ """
207
+ return cls(
208
+ id=data["id"],
209
+ title=data["title"],
210
+ message=data["message"],
211
+ priority=data["priority"],
212
+ request_id=data["request_id"],
213
+ auto_dismissed=data["auto_dismissed"],
214
+ dismissed_at=data.get("dismissed_at"),
215
+ useful=data.get("useful"),
216
+ created_at=data["created_at"],
217
+ updated_at=data["updated_at"],
218
+ )
219
+
220
+
221
+ def _empty_pad_interviewer_notifications() -> list[PadInterviewerNotification]:
222
+ """Return an empty, precisely typed notification list."""
223
+ return []
224
+
225
+
226
+ @beartype
227
+ @dataclass(frozen=True, kw_only=True)
228
+ class Pad:
229
+ """A CoderPad interview pad."""
230
+
231
+ id: str
232
+ title: str
233
+ state: str
234
+ owner_email: str
235
+ language: str | None
236
+ private: bool
237
+ execution_enabled: bool
238
+ contents: str | None
239
+ participants: list[str]
240
+ events: str
241
+ notes: str | None
242
+ created_at: str
243
+ updated_at: str
244
+ ended_at: str | None
245
+ url: str
246
+ playback: str
247
+ drawing: str | None
248
+ type: str
249
+ question_ids: list[int]
250
+ pad_environment_ids: list[int]
251
+ active_environment_id: int | None
252
+ team: Team
253
+ history: str | None = None
254
+ restrict_interviewer_access: bool | None = None
255
+ pad_interviewer_notifications: list[PadInterviewerNotification] = field(
256
+ default_factory=_empty_pad_interviewer_notifications,
257
+ )
258
+
259
+ @classmethod
260
+ def from_dict(cls, data: PadDict) -> Self:
261
+ """Create from an API response dictionary.
262
+
263
+ Args:
264
+ data: The dictionary to convert.
265
+
266
+ Returns:
267
+ A new instance.
268
+ """
269
+ raw_notifications = data.get("pad_interviewer_notifications")
270
+ notifications = (
271
+ [
272
+ PadInterviewerNotification.from_dict(data=item)
273
+ for item in raw_notifications
274
+ ]
275
+ if raw_notifications is not None
276
+ else _empty_pad_interviewer_notifications()
277
+ )
278
+ return cls(
279
+ id=data["id"],
280
+ title=data["title"],
281
+ state=data["state"],
282
+ owner_email=data["owner_email"],
283
+ language=data.get("language"),
284
+ private=data["private"],
285
+ execution_enabled=data["execution_enabled"],
286
+ contents=data.get("contents"),
287
+ participants=data["participants"],
288
+ events=data["events"],
289
+ notes=data.get("notes"),
290
+ created_at=data["created_at"],
291
+ updated_at=data["updated_at"],
292
+ ended_at=data.get("ended_at"),
293
+ url=data["url"],
294
+ playback=data["playback"],
295
+ drawing=data.get("drawing"),
296
+ type=data["type"],
297
+ question_ids=data["question_ids"],
298
+ pad_environment_ids=data["pad_environment_ids"],
299
+ active_environment_id=data.get(
300
+ "active_environment_id",
301
+ ),
302
+ team=Team.from_dict(data=data["team"]),
303
+ history=data.get("history"),
304
+ restrict_interviewer_access=data.get(
305
+ "restrict_interviewer_access",
306
+ ),
307
+ pad_interviewer_notifications=notifications,
308
+ )
309
+
310
+
311
+ @beartype
312
+ @dataclass(frozen=True, kw_only=True)
313
+ class PadEvent:
314
+ """An event associated with a pad."""
315
+
316
+ message: str
317
+ kind: str
318
+ metadata: str | None
319
+ user_name: str | None
320
+ user_email: str | None
321
+ created_at: str
322
+
323
+ @classmethod
324
+ def from_dict(
325
+ cls,
326
+ data: PadEventDict,
327
+ ) -> Self:
328
+ """Create from an API response dictionary.
329
+
330
+ Args:
331
+ data: The dictionary to convert.
332
+
333
+ Returns:
334
+ A new instance.
335
+ """
336
+ return cls(
337
+ message=data["message"],
338
+ kind=data["kind"],
339
+ metadata=data.get("metadata"),
340
+ user_name=data.get("user_name"),
341
+ user_email=data.get("user_email"),
342
+ created_at=data["created_at"],
343
+ )
344
+
345
+
346
+ @beartype
347
+ @dataclass(frozen=True, kw_only=True)
348
+ class PadHistoryEntry:
349
+ """An editor operation from a pad's Firebase history.
350
+
351
+ Positive integer operations retain existing characters, negative
352
+ integers delete characters, and strings insert text.
353
+ """
354
+
355
+ id: str
356
+ author: str
357
+ operations: list[int | str]
358
+ timestamp: int
359
+
360
+ @classmethod
361
+ def from_dict(
362
+ cls,
363
+ *,
364
+ entry_id: str,
365
+ data: PadHistoryEntryDict,
366
+ ) -> Self:
367
+ """Create from a Firebase history entry.
368
+
369
+ Args:
370
+ entry_id: The Firebase key for the entry.
371
+ data: The dictionary to convert.
372
+
373
+ Returns:
374
+ A new instance.
375
+ """
376
+ return cls(
377
+ id=entry_id,
378
+ author=data["a"],
379
+ operations=data["o"],
380
+ timestamp=data["t"],
381
+ )
382
+
383
+ def apply(self, *, contents: str) -> str:
384
+ """Apply this operation to existing file contents.
385
+
386
+ Args:
387
+ contents: The contents immediately before this operation.
388
+
389
+ Returns:
390
+ The updated contents.
391
+ """
392
+ cursor = 0
393
+ updated: list[str] = []
394
+ for operation in self.operations:
395
+ if isinstance(operation, str):
396
+ updated.append(operation)
397
+ elif operation > 0:
398
+ updated.append(contents[cursor : cursor + operation])
399
+ cursor += operation
400
+ else:
401
+ cursor -= operation
402
+ updated.append(contents[cursor:])
403
+ return "".join(updated)
404
+
405
+
406
+ @beartype
407
+ class PadHistory(list[PadHistoryEntry]):
408
+ """Chronologically ordered editor history for a pad file."""
409
+
410
+ @classmethod
411
+ def from_dict(
412
+ cls,
413
+ *,
414
+ data: dict[str, PadHistoryEntryDict],
415
+ ) -> Self:
416
+ """Create from a Firebase history response.
417
+
418
+ Args:
419
+ data: Firebase keys mapped to editor operations.
420
+
421
+ Returns:
422
+ A chronologically ordered history.
423
+ """
424
+ ordered_entries = sorted(
425
+ data.items(),
426
+ key=lambda item: (item[1]["t"], item[0]),
427
+ )
428
+ history = cls()
429
+ for entry_id, entry in ordered_entries:
430
+ history.append(
431
+ PadHistoryEntry.from_dict(
432
+ entry_id=entry_id,
433
+ data=entry,
434
+ ),
435
+ )
436
+ return history
437
+
438
+ def replay(self, *, initial_contents: str = "") -> str:
439
+ """Replay all operations and return the resulting contents.
440
+
441
+ Args:
442
+ initial_contents: Contents before the first operation.
443
+
444
+ Returns:
445
+ The contents after all operations.
446
+ """
447
+ contents = initial_contents
448
+ for entry in self:
449
+ contents = entry.apply(contents=contents)
450
+ return contents
451
+
452
+
453
+ @beartype
454
+ @dataclass(frozen=True, kw_only=True)
455
+ class FileContent:
456
+ """A file within a pad environment.
457
+
458
+ Binary files are empirically observed with ``binary`` set to ``True`` and
459
+ ``contents`` set to ``None``; these fields are not fully described by the
460
+ published API specification.
461
+ """
462
+
463
+ path: str
464
+ contents: str | None
465
+ history: str | None
466
+ binary: bool = False
467
+
468
+ @classmethod
469
+ def from_dict(
470
+ cls,
471
+ data: FileContentDict,
472
+ ) -> Self:
473
+ """Create from an API response dictionary.
474
+
475
+ Args:
476
+ data: The dictionary to convert.
477
+
478
+ Returns:
479
+ A new instance.
480
+ """
481
+ raw_binary = data.get("binary")
482
+ return cls(
483
+ path=data["path"],
484
+ contents=data["contents"],
485
+ history=data.get("history"),
486
+ binary=raw_binary if raw_binary is not None else False,
487
+ )
488
+
489
+
490
+ @beartype
491
+ @dataclass(frozen=True, kw_only=True)
492
+ class PadEnvironment:
493
+ """A pad environment."""
494
+
495
+ id: int
496
+ pad_id: int
497
+ question_id: int | None
498
+ example_question_id: int | None
499
+ language: str
500
+ file_contents: list[FileContent]
501
+ created_at: str
502
+ updated_at: str
503
+
504
+ @classmethod
505
+ def from_dict(
506
+ cls,
507
+ data: PadEnvironmentDict,
508
+ ) -> Self:
509
+ """Create from an API response dictionary.
510
+
511
+ Args:
512
+ data: The dictionary to convert.
513
+
514
+ Returns:
515
+ A new instance.
516
+ """
517
+ return cls(
518
+ id=data["id"],
519
+ pad_id=data["pad_id"],
520
+ question_id=data.get("question_id"),
521
+ example_question_id=data.get(
522
+ "example_question_id",
523
+ ),
524
+ language=data["language"],
525
+ file_contents=[
526
+ FileContent.from_dict(data=item)
527
+ for item in data["file_contents"]
528
+ ],
529
+ created_at=data["created_at"],
530
+ updated_at=data["updated_at"],
531
+ )
532
+
533
+
534
+ @beartype
535
+ @dataclass(frozen=True, kw_only=True)
536
+ class QuestionFileContent:
537
+ """A file for a multi-file question.
538
+
539
+ Used when creating or updating questions with
540
+ multi-file languages.
541
+ """
542
+
543
+ path: str
544
+ contents: str
545
+
546
+
547
+ @beartype
548
+ @dataclass(frozen=True, kw_only=True)
549
+ class CandidateInstruction:
550
+ """Instructions shown to a candidate."""
551
+
552
+ instructions: str
553
+ default_visible: bool = False
554
+
555
+ @classmethod
556
+ def from_dict(
557
+ cls,
558
+ data: CandidateInstructionDict,
559
+ ) -> Self:
560
+ """Create from an API response dictionary.
561
+
562
+ Args:
563
+ data: The dictionary to convert.
564
+
565
+ Returns:
566
+ A new instance.
567
+ """
568
+ return cls(
569
+ instructions=data["instructions"],
570
+ default_visible=(
571
+ "default_visible" in data and data["default_visible"]
572
+ ),
573
+ )
574
+
575
+
576
+ @beartype
577
+ @dataclass(frozen=True, kw_only=True)
578
+ class TestCase:
579
+ """A test case for a question."""
580
+
581
+ id: int
582
+ return_value: str
583
+ visible: bool
584
+ arguments: list[str]
585
+
586
+ @classmethod
587
+ def from_dict(
588
+ cls,
589
+ data: TestCaseDict,
590
+ ) -> Self:
591
+ """Create from an API response dictionary.
592
+
593
+ Args:
594
+ data: The dictionary to convert.
595
+
596
+ Returns:
597
+ A new instance.
598
+ """
599
+ return cls(
600
+ id=data["id"],
601
+ return_value=data["return_value"],
602
+ visible=data["visible"],
603
+ arguments=data["arguments"],
604
+ )
605
+
606
+
607
+ @beartype
608
+ @dataclass(frozen=True, kw_only=True)
609
+ class CustomFile:
610
+ """A custom file attached to a question."""
611
+
612
+ id: str
613
+ title: str
614
+ description: str
615
+ filename: str
616
+ filesize: str
617
+
618
+ @classmethod
619
+ def from_dict(
620
+ cls,
621
+ data: CustomFileDict,
622
+ ) -> Self:
623
+ """Create from an API response dictionary.
624
+
625
+ Args:
626
+ data: The dictionary to convert.
627
+
628
+ Returns:
629
+ A new instance.
630
+ """
631
+ return cls(
632
+ id=data["id"],
633
+ title=data["title"],
634
+ description=data["description"],
635
+ filename=data["filename"],
636
+ filesize=data["filesize"],
637
+ )
638
+
639
+
640
+ @beartype
641
+ @dataclass(frozen=True, kw_only=True)
642
+ class CustomDatabaseColumn:
643
+ """A column in a question's custom database schema."""
644
+
645
+ name: str
646
+ type: str
647
+ pk: bool
648
+ nn: bool
649
+
650
+ @classmethod
651
+ def from_dict(
652
+ cls,
653
+ data: CustomDatabaseColumnDict,
654
+ ) -> Self:
655
+ """Create from an API response dictionary.
656
+
657
+ Args:
658
+ data: The dictionary to convert.
659
+
660
+ Returns:
661
+ A new instance.
662
+ """
663
+ return cls(
664
+ name=data["name"],
665
+ type=data["type"],
666
+ pk=data["pk"],
667
+ nn=data["nn"],
668
+ )
669
+
670
+
671
+ @beartype
672
+ @dataclass(frozen=True, kw_only=True)
673
+ class CustomDatabaseTable:
674
+ """A table in a question's custom database schema."""
675
+
676
+ name: str
677
+ columns: list[CustomDatabaseColumn]
678
+
679
+ @classmethod
680
+ def from_dict(
681
+ cls,
682
+ data: CustomDatabaseTableDict,
683
+ ) -> Self:
684
+ """Create from an API response dictionary.
685
+
686
+ Args:
687
+ data: The dictionary to convert.
688
+
689
+ Returns:
690
+ A new instance.
691
+ """
692
+ return cls(
693
+ name=data["name"],
694
+ columns=[
695
+ CustomDatabaseColumn.from_dict(data=item)
696
+ for item in data["columns"]
697
+ ],
698
+ )
699
+
700
+
701
+ @beartype
702
+ @dataclass(frozen=True, kw_only=True)
703
+ class CustomDatabaseSchema:
704
+ """The structured schema for a question's custom database."""
705
+
706
+ arrangement: str
707
+ tables: list[CustomDatabaseTable]
708
+
709
+ @classmethod
710
+ def from_dict(
711
+ cls,
712
+ data: CustomDatabaseSchemaDict,
713
+ ) -> Self:
714
+ """Create from an API response dictionary.
715
+
716
+ Args:
717
+ data: The dictionary to convert.
718
+
719
+ Returns:
720
+ A new instance.
721
+ """
722
+ return cls(
723
+ arrangement=data["arrangement"],
724
+ tables=[
725
+ CustomDatabaseTable.from_dict(data=item)
726
+ for item in data["tables"]
727
+ ],
728
+ )
729
+
730
+
731
+ @beartype
732
+ @dataclass(frozen=True, kw_only=True)
733
+ class CustomDatabase:
734
+ """A custom database attached to a question.
735
+
736
+ This structure is empirically observed in live API responses and is not
737
+ currently described by the published CoderPad API specification.
738
+ """
739
+
740
+ id: int
741
+ title: str
742
+ description: str
743
+ language: str
744
+ schema: str
745
+ schema_json: CustomDatabaseSchema
746
+
747
+ @classmethod
748
+ def from_dict(
749
+ cls,
750
+ data: CustomDatabaseDict,
751
+ ) -> Self:
752
+ """Create from an API response dictionary.
753
+
754
+ Args:
755
+ data: The dictionary to convert.
756
+
757
+ Returns:
758
+ A new instance.
759
+ """
760
+ return cls(
761
+ id=data["id"],
762
+ title=data["title"],
763
+ description=data["description"],
764
+ language=data["language"],
765
+ schema=data["schema"],
766
+ schema_json=CustomDatabaseSchema.from_dict(
767
+ data=data["schema_json"],
768
+ ),
769
+ )
770
+
771
+
772
+ @beartype
773
+ @dataclass(frozen=True, kw_only=True)
774
+ class Question:
775
+ """A CoderPad question."""
776
+
777
+ id: int
778
+ title: str
779
+ owner_email: str
780
+ language: str | None
781
+ description: str | None
782
+ candidate_instructions: list[CandidateInstruction]
783
+ contents: str | None
784
+ shared: bool
785
+ used: int
786
+ take_home: bool
787
+ test_cases_enabled: bool
788
+ solution: str | None
789
+ pad_type: str
790
+ is_draft: bool
791
+ author_name: str
792
+ organization_name: str
793
+ custom_files: list[CustomFile]
794
+ created_at: str
795
+ updated_at: str
796
+ public_take_home_setting_id: int | None = None
797
+ contents_for_test_cases: str | None = None
798
+ test_cases: list[TestCase] | None = None
799
+ custom_database: CustomDatabase | None = None
800
+
801
+ @classmethod
802
+ def from_dict(
803
+ cls,
804
+ data: QuestionDict,
805
+ ) -> Self:
806
+ """Create from an API response dictionary.
807
+
808
+ Args:
809
+ data: The dictionary to convert.
810
+
811
+ Returns:
812
+ A new instance.
813
+ """
814
+ raw_test_cases = data.get("test_cases")
815
+ raw_custom_database = data.get("custom_database")
816
+ return cls(
817
+ id=data["id"],
818
+ title=data["title"],
819
+ owner_email=data["owner_email"],
820
+ language=data.get("language"),
821
+ description=data.get("description"),
822
+ candidate_instructions=[
823
+ CandidateInstruction.from_dict(data=item)
824
+ for item in data["candidate_instructions"]
825
+ ],
826
+ contents=data.get("contents"),
827
+ shared=data["shared"],
828
+ used=data["used"],
829
+ take_home=data["take_home"],
830
+ test_cases_enabled=data["test_cases_enabled"],
831
+ solution=data.get("solution"),
832
+ pad_type=data["pad_type"],
833
+ is_draft=data["is_draft"],
834
+ author_name=data["author_name"],
835
+ organization_name=data["organization_name"],
836
+ custom_files=[
837
+ CustomFile.from_dict(data=item)
838
+ for item in data["custom_files"]
839
+ ],
840
+ created_at=data["created_at"],
841
+ updated_at=data["updated_at"],
842
+ public_take_home_setting_id=data.get(
843
+ "public_take_home_setting_id",
844
+ ),
845
+ contents_for_test_cases=data.get(
846
+ "contents_for_test_cases",
847
+ ),
848
+ test_cases=[
849
+ TestCase.from_dict(data=item) for item in raw_test_cases
850
+ ]
851
+ if raw_test_cases is not None
852
+ else None,
853
+ custom_database=CustomDatabase.from_dict(
854
+ data=raw_custom_database,
855
+ )
856
+ if raw_custom_database is not None
857
+ else None,
858
+ )
859
+
860
+
861
+ @beartype
862
+ @dataclass(frozen=True, kw_only=True)
863
+ class OrganizationUser:
864
+ """A user within an organization."""
865
+
866
+ email: str
867
+ name: str
868
+ teams: list[str]
869
+
870
+ @classmethod
871
+ def from_dict(
872
+ cls,
873
+ data: OrganizationUserDict,
874
+ ) -> Self:
875
+ """Create from an API response dictionary.
876
+
877
+ Args:
878
+ data: The dictionary to convert.
879
+
880
+ Returns:
881
+ A new instance.
882
+ """
883
+ return cls(
884
+ email=data["email"],
885
+ name=data["name"],
886
+ teams=data["teams"],
887
+ )
888
+
889
+
890
+ @beartype
891
+ @dataclass(frozen=True, kw_only=True)
892
+ class OrganizationStatsUser:
893
+ """A user's pad usage statistics."""
894
+
895
+ email: str
896
+ name: str
897
+ pads_created: int
898
+
899
+ @classmethod
900
+ def from_dict(
901
+ cls,
902
+ data: OrganizationStatsUserDict,
903
+ ) -> Self:
904
+ """Create from an API response dictionary.
905
+
906
+ Args:
907
+ data: The dictionary to convert.
908
+
909
+ Returns:
910
+ A new instance.
911
+ """
912
+ return cls(
913
+ email=data["email"],
914
+ name=data["name"],
915
+ pads_created=data["pads_created"],
916
+ )
917
+
918
+
919
+ @beartype
920
+ @dataclass(frozen=True, kw_only=True)
921
+ class Quota:
922
+ """Quota information."""
923
+
924
+ trial_expires_at: str
925
+ pads_used: int
926
+ quota_reset_at: str
927
+ unlimited: bool
928
+ overages_enabled: bool
929
+
930
+ @classmethod
931
+ def from_dict(
932
+ cls,
933
+ data: QuotaDict,
934
+ ) -> Self:
935
+ """Create from an API response dictionary.
936
+
937
+ Args:
938
+ data: The dictionary to convert.
939
+
940
+ Returns:
941
+ A new instance.
942
+ """
943
+ return cls(
944
+ trial_expires_at=data["trial_expires_at"],
945
+ pads_used=data["pads_used"],
946
+ quota_reset_at=data["quota_reset_at"],
947
+ unlimited=data["unlimited"],
948
+ overages_enabled=data["overages_enabled"],
949
+ )
950
+
951
+
952
+ def _empty_child_organizations() -> list[dict[str, object]]:
953
+ """Return an empty list for a response structure not yet typed."""
954
+ return []
955
+
956
+
957
+ @beartype
958
+ @dataclass(frozen=True, kw_only=True)
959
+ class Organization:
960
+ """Organization information.
961
+
962
+ ``id`` and ``child_organizations`` are empirically observed fields that
963
+ are not currently described by the published API specification. Child
964
+ organizations remain raw mappings until their non-empty shape is known.
965
+ """
966
+
967
+ organization_name: str
968
+ user_count: int
969
+ users: list[OrganizationUser]
970
+ organization_default_language: str
971
+ single_sign_on_supported: bool
972
+ teams: list[Team]
973
+ single_sign_in_url: str | None = None
974
+ id: int | None = None
975
+ child_organizations: list[dict[str, object]] = field(
976
+ default_factory=_empty_child_organizations,
977
+ )
978
+
979
+ @classmethod
980
+ def from_dict(
981
+ cls,
982
+ data: OrganizationDict,
983
+ ) -> Self:
984
+ """Create from an API response dictionary.
985
+
986
+ Args:
987
+ data: The dictionary to convert.
988
+
989
+ Returns:
990
+ A new instance.
991
+ """
992
+ raw_child_organizations = data.get("child_organizations")
993
+ return cls(
994
+ organization_name=data["organization_name"],
995
+ user_count=data["user_count"],
996
+ users=[
997
+ OrganizationUser.from_dict(data=item) for item in data["users"]
998
+ ],
999
+ organization_default_language=data[
1000
+ "organization_default_language"
1001
+ ],
1002
+ single_sign_on_supported=data["single_sign_on_supported"],
1003
+ teams=[Team.from_dict(data=item) for item in data["teams"]],
1004
+ single_sign_in_url=data.get("single_sign_in_url"),
1005
+ id=data.get("id"),
1006
+ child_organizations=(
1007
+ raw_child_organizations
1008
+ if raw_child_organizations is not None
1009
+ else _empty_child_organizations()
1010
+ ),
1011
+ )
1012
+
1013
+
1014
+ @beartype
1015
+ @dataclass(frozen=True, kw_only=True)
1016
+ class OrganizationStats:
1017
+ """Organization usage statistics."""
1018
+
1019
+ start_time: str
1020
+ end_time: str
1021
+ pads_created: int
1022
+ users: list[OrganizationStatsUser]
1023
+
1024
+ @classmethod
1025
+ def from_dict(
1026
+ cls,
1027
+ data: OrganizationStatsDict,
1028
+ ) -> Self:
1029
+ """Create from an API response dictionary.
1030
+
1031
+ Args:
1032
+ data: The dictionary to convert.
1033
+
1034
+ Returns:
1035
+ A new instance.
1036
+ """
1037
+ return cls(
1038
+ start_time=data["start_time"],
1039
+ end_time=data["end_time"],
1040
+ pads_created=data["pads_created"],
1041
+ users=[
1042
+ OrganizationStatsUser.from_dict(data=item)
1043
+ for item in data["users"]
1044
+ ],
1045
+ )