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/__init__.py +1 -0
- coderpad/_dict_types.py +240 -0
- coderpad/async_client.py +810 -0
- coderpad/client.py +789 -0
- coderpad/exceptions.py +115 -0
- coderpad/py.typed +0 -0
- coderpad/transports.py +279 -0
- coderpad/types.py +1045 -0
- coderpad_py-2026.7.22.dist-info/METADATA +117 -0
- coderpad_py-2026.7.22.dist-info/RECORD +13 -0
- coderpad_py-2026.7.22.dist-info/WHEEL +5 -0
- coderpad_py-2026.7.22.dist-info/licenses/LICENSE +21 -0
- coderpad_py-2026.7.22.dist-info/top_level.txt +1 -0
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
|
+
)
|