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/async_client.py
ADDED
|
@@ -0,0 +1,810 @@
|
|
|
1
|
+
"""Async CoderPad Interview API client."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from collections.abc import Sequence
|
|
5
|
+
from http import HTTPStatus
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Self
|
|
8
|
+
|
|
9
|
+
from beartype import beartype
|
|
10
|
+
|
|
11
|
+
from coderpad.exceptions import CoderPadError
|
|
12
|
+
from coderpad.transports import (
|
|
13
|
+
AsyncHTTPXTransport,
|
|
14
|
+
AsyncTransport,
|
|
15
|
+
TransportResponse,
|
|
16
|
+
)
|
|
17
|
+
from coderpad.types import (
|
|
18
|
+
CandidateInstruction,
|
|
19
|
+
Language,
|
|
20
|
+
Organization,
|
|
21
|
+
OrganizationStats,
|
|
22
|
+
Pad,
|
|
23
|
+
PadEnvironment,
|
|
24
|
+
PadEvent,
|
|
25
|
+
PadHistory,
|
|
26
|
+
PaginatedList,
|
|
27
|
+
Question,
|
|
28
|
+
QuestionFileContent,
|
|
29
|
+
Quota,
|
|
30
|
+
SortOrder,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@beartype
|
|
35
|
+
class _AsyncNamespace:
|
|
36
|
+
"""Base class providing shared async request logic."""
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
*,
|
|
41
|
+
transport: AsyncTransport,
|
|
42
|
+
base_url: str,
|
|
43
|
+
headers: dict[str, str],
|
|
44
|
+
) -> None:
|
|
45
|
+
"""Create a new async namespace.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
transport: The async HTTP transport.
|
|
49
|
+
base_url: The base URL for the API.
|
|
50
|
+
headers: Headers to send with every request.
|
|
51
|
+
"""
|
|
52
|
+
self.transport = transport
|
|
53
|
+
self.base_url = base_url
|
|
54
|
+
self.headers = headers
|
|
55
|
+
|
|
56
|
+
async def _request(
|
|
57
|
+
self,
|
|
58
|
+
*,
|
|
59
|
+
method: str,
|
|
60
|
+
url: str,
|
|
61
|
+
params: dict[str, str | int] | None = None,
|
|
62
|
+
data: dict[str, str] | None = None,
|
|
63
|
+
files: (dict[str, tuple[str, bytes, str]] | None) = None,
|
|
64
|
+
) -> TransportResponse:
|
|
65
|
+
"""Make an async HTTP request.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
method: The HTTP method.
|
|
69
|
+
url: The URL path.
|
|
70
|
+
params: Query parameters.
|
|
71
|
+
data: Form data.
|
|
72
|
+
files: Files to upload as multipart form data.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
The transport response.
|
|
76
|
+
|
|
77
|
+
Raises:
|
|
78
|
+
CoderPadError: If the response has an error
|
|
79
|
+
status code.
|
|
80
|
+
"""
|
|
81
|
+
response = await self.transport(
|
|
82
|
+
method=method,
|
|
83
|
+
url=self.base_url + url,
|
|
84
|
+
headers=self.headers,
|
|
85
|
+
params=params,
|
|
86
|
+
data=data,
|
|
87
|
+
files=files,
|
|
88
|
+
)
|
|
89
|
+
if response.status_code >= HTTPStatus.BAD_REQUEST:
|
|
90
|
+
raise CoderPadError.from_response(response=response)
|
|
91
|
+
return response
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@beartype
|
|
95
|
+
class AsyncPadsNamespace(_AsyncNamespace):
|
|
96
|
+
"""Namespace for async pad operations."""
|
|
97
|
+
|
|
98
|
+
async def list(
|
|
99
|
+
self,
|
|
100
|
+
*,
|
|
101
|
+
sort: SortOrder | None = None,
|
|
102
|
+
page: int | None = None,
|
|
103
|
+
) -> PaginatedList[Pad]:
|
|
104
|
+
"""Retrieve a list of pads.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
sort: Sort order.
|
|
108
|
+
page: Page number for pagination.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
The list of pads with pagination metadata.
|
|
112
|
+
"""
|
|
113
|
+
params: dict[str, str | int] = {}
|
|
114
|
+
if sort is not None:
|
|
115
|
+
params["sort"] = sort.value
|
|
116
|
+
if page is not None:
|
|
117
|
+
params["page"] = page
|
|
118
|
+
response = await self._request(
|
|
119
|
+
method="GET",
|
|
120
|
+
url="/api/pads/",
|
|
121
|
+
params=params,
|
|
122
|
+
)
|
|
123
|
+
data = response.json()
|
|
124
|
+
return PaginatedList(
|
|
125
|
+
[Pad.from_dict(data=item) for item in data["pads"]],
|
|
126
|
+
total=data["total"],
|
|
127
|
+
next_page=data.get("next_page"),
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
async def create(
|
|
131
|
+
self,
|
|
132
|
+
*,
|
|
133
|
+
title: str | None = None,
|
|
134
|
+
language: Language | str | None = None,
|
|
135
|
+
contents: str | None = None,
|
|
136
|
+
notes: str | None = None,
|
|
137
|
+
question_id: str | int | None = None,
|
|
138
|
+
) -> Pad:
|
|
139
|
+
"""Create a new pad.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
title: Title for the pad.
|
|
143
|
+
language: Programming language for the pad.
|
|
144
|
+
contents: Initial contents of the pad editor.
|
|
145
|
+
notes: Private notes for the interviewer.
|
|
146
|
+
question_id: Id of an existing question to seed
|
|
147
|
+
the pad from.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
The created pad.
|
|
151
|
+
"""
|
|
152
|
+
data: dict[str, str] = {}
|
|
153
|
+
if title is not None:
|
|
154
|
+
data["title"] = title
|
|
155
|
+
if language is not None:
|
|
156
|
+
lang = (
|
|
157
|
+
language.value if isinstance(language, Language) else language
|
|
158
|
+
)
|
|
159
|
+
data["language"] = lang
|
|
160
|
+
if contents is not None:
|
|
161
|
+
data["contents"] = contents
|
|
162
|
+
if notes is not None:
|
|
163
|
+
data["notes"] = notes
|
|
164
|
+
if question_id is not None:
|
|
165
|
+
data["question_id"] = str(object=question_id)
|
|
166
|
+
response = await self._request(
|
|
167
|
+
method="POST",
|
|
168
|
+
url="/api/pads/",
|
|
169
|
+
data=data,
|
|
170
|
+
)
|
|
171
|
+
return Pad.from_dict(data=response.json())
|
|
172
|
+
|
|
173
|
+
async def get(self, *, pad_id: str) -> Pad:
|
|
174
|
+
"""Retrieve a pad by id.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
pad_id: The id of the pad.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
The pad.
|
|
181
|
+
"""
|
|
182
|
+
response = await self._request(
|
|
183
|
+
method="GET",
|
|
184
|
+
url=f"/api/pads/{pad_id}",
|
|
185
|
+
)
|
|
186
|
+
return Pad.from_dict(data=response.json())
|
|
187
|
+
|
|
188
|
+
async def update(
|
|
189
|
+
self,
|
|
190
|
+
*,
|
|
191
|
+
pad_id: str,
|
|
192
|
+
title: str | None = None,
|
|
193
|
+
language: Language | str | None = None,
|
|
194
|
+
contents: str | None = None,
|
|
195
|
+
notes: str | None = None,
|
|
196
|
+
ended: bool | None = None,
|
|
197
|
+
deleted: bool | None = None,
|
|
198
|
+
) -> None:
|
|
199
|
+
"""Modify an existing pad.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
pad_id: The id of the pad.
|
|
203
|
+
title: New title for the pad.
|
|
204
|
+
language: New programming language.
|
|
205
|
+
contents: New contents of the pad editor.
|
|
206
|
+
notes: New private notes.
|
|
207
|
+
ended: Set to ``True`` to end the interview.
|
|
208
|
+
deleted: Set to ``True`` to delete the pad.
|
|
209
|
+
"""
|
|
210
|
+
data: dict[str, str] = {}
|
|
211
|
+
if title is not None:
|
|
212
|
+
data["title"] = title
|
|
213
|
+
if language is not None:
|
|
214
|
+
lang = (
|
|
215
|
+
language.value if isinstance(language, Language) else language
|
|
216
|
+
)
|
|
217
|
+
data["language"] = lang
|
|
218
|
+
if contents is not None:
|
|
219
|
+
data["contents"] = contents
|
|
220
|
+
if notes is not None:
|
|
221
|
+
data["notes"] = notes
|
|
222
|
+
if ended is not None:
|
|
223
|
+
data["ended"] = "true" if ended else "false"
|
|
224
|
+
if deleted is not None:
|
|
225
|
+
data["deleted"] = "true" if deleted else "false"
|
|
226
|
+
await self._request(
|
|
227
|
+
method="PUT",
|
|
228
|
+
url=f"/api/pads/{pad_id}",
|
|
229
|
+
data=data,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
async def get_events(
|
|
233
|
+
self,
|
|
234
|
+
*,
|
|
235
|
+
pad_id: str,
|
|
236
|
+
sort: SortOrder | None = None,
|
|
237
|
+
page: int | None = None,
|
|
238
|
+
) -> PaginatedList[PadEvent]:
|
|
239
|
+
"""Retrieve a list of pad events.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
pad_id: The id of the pad.
|
|
243
|
+
sort: Sort order.
|
|
244
|
+
page: Page number for pagination.
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
The list of pad events with pagination
|
|
248
|
+
metadata.
|
|
249
|
+
"""
|
|
250
|
+
params: dict[str, str | int] = {}
|
|
251
|
+
if sort is not None:
|
|
252
|
+
params["sort"] = sort.value
|
|
253
|
+
if page is not None:
|
|
254
|
+
params["page"] = page
|
|
255
|
+
response = await self._request(
|
|
256
|
+
method="GET",
|
|
257
|
+
url=f"/api/pads/{pad_id}/events",
|
|
258
|
+
params=params,
|
|
259
|
+
)
|
|
260
|
+
data = response.json()
|
|
261
|
+
return PaginatedList(
|
|
262
|
+
[PadEvent.from_dict(data=item) for item in data["events"]],
|
|
263
|
+
total=data["total"],
|
|
264
|
+
next_page=data.get("next_page"),
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
async def get_environment(
|
|
268
|
+
self,
|
|
269
|
+
*,
|
|
270
|
+
environment_id: str,
|
|
271
|
+
) -> PadEnvironment:
|
|
272
|
+
"""Retrieve pad environment information.
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
environment_id: The id of the pad environment.
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
The pad environment.
|
|
279
|
+
"""
|
|
280
|
+
response = await self._request(
|
|
281
|
+
method="GET",
|
|
282
|
+
url=f"/api/pad_environments/{environment_id}",
|
|
283
|
+
)
|
|
284
|
+
return PadEnvironment.from_dict(
|
|
285
|
+
data=response.json(),
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
async def get_history(self, *, history_url: str) -> PadHistory:
|
|
289
|
+
"""Retrieve editor history from a Firebase history URL.
|
|
290
|
+
|
|
291
|
+
The URL is available as ``FileContent.history``. The CoderPad
|
|
292
|
+
API key is deliberately not sent to the Firebase host.
|
|
293
|
+
|
|
294
|
+
Args:
|
|
295
|
+
history_url: The history URL returned for a file in a pad
|
|
296
|
+
environment.
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
The chronologically ordered editor history. An empty
|
|
300
|
+
history is returned when Firebase responds with ``null``.
|
|
301
|
+
"""
|
|
302
|
+
response = await self.transport(
|
|
303
|
+
method="GET",
|
|
304
|
+
url=history_url,
|
|
305
|
+
headers={"Accept": "application/json"},
|
|
306
|
+
params=None,
|
|
307
|
+
data=None,
|
|
308
|
+
files=None,
|
|
309
|
+
)
|
|
310
|
+
if response.status_code >= HTTPStatus.BAD_REQUEST:
|
|
311
|
+
raise CoderPadError.from_response(response=response)
|
|
312
|
+
data = response.json()
|
|
313
|
+
if data is None:
|
|
314
|
+
return PadHistory()
|
|
315
|
+
return PadHistory.from_dict(data=data)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
@beartype
|
|
319
|
+
class AsyncQuestionsNamespace(_AsyncNamespace):
|
|
320
|
+
"""Namespace for async question operations."""
|
|
321
|
+
|
|
322
|
+
async def list(
|
|
323
|
+
self,
|
|
324
|
+
*,
|
|
325
|
+
sort: SortOrder | None = None,
|
|
326
|
+
page: int | None = None,
|
|
327
|
+
) -> PaginatedList[Question]:
|
|
328
|
+
"""Retrieve a list of questions.
|
|
329
|
+
|
|
330
|
+
Args:
|
|
331
|
+
sort: Sort order.
|
|
332
|
+
page: Page number for pagination.
|
|
333
|
+
|
|
334
|
+
Returns:
|
|
335
|
+
The list of questions with pagination
|
|
336
|
+
metadata.
|
|
337
|
+
"""
|
|
338
|
+
params: dict[str, str | int] = {}
|
|
339
|
+
if sort is not None:
|
|
340
|
+
params["sort"] = sort.value
|
|
341
|
+
if page is not None:
|
|
342
|
+
params["page"] = page
|
|
343
|
+
response = await self._request(
|
|
344
|
+
method="GET",
|
|
345
|
+
url="/api/questions/",
|
|
346
|
+
params=params,
|
|
347
|
+
)
|
|
348
|
+
data = response.json()
|
|
349
|
+
return PaginatedList(
|
|
350
|
+
[Question.from_dict(data=item) for item in data["questions"]],
|
|
351
|
+
total=data["total"],
|
|
352
|
+
next_page=data.get("next_page"),
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
async def create(
|
|
356
|
+
self,
|
|
357
|
+
*,
|
|
358
|
+
title: str,
|
|
359
|
+
language: Language | str,
|
|
360
|
+
description: str | None = None,
|
|
361
|
+
contents: str | None = None,
|
|
362
|
+
solution: str | None = None,
|
|
363
|
+
ai_assist_custom_system_prompt: str | None = None,
|
|
364
|
+
candidate_instructions: (Sequence[CandidateInstruction] | None) = None,
|
|
365
|
+
file_contents: (Sequence[QuestionFileContent] | None) = None,
|
|
366
|
+
zip_file: Path | None = None,
|
|
367
|
+
) -> Question:
|
|
368
|
+
"""Create a new question.
|
|
369
|
+
|
|
370
|
+
Args:
|
|
371
|
+
title: Title for the question.
|
|
372
|
+
language: Programming language for the
|
|
373
|
+
question.
|
|
374
|
+
description: Notes about the question.
|
|
375
|
+
contents: Text inserted into the interview
|
|
376
|
+
session. Cannot be combined with
|
|
377
|
+
``file_contents``.
|
|
378
|
+
solution: The solution to the question.
|
|
379
|
+
ai_assist_custom_system_prompt: Custom system prompt for AI Assist.
|
|
380
|
+
candidate_instructions: Progressively-revealed
|
|
381
|
+
instruction blocks shown to the candidate.
|
|
382
|
+
file_contents: Files for a multi-file question.
|
|
383
|
+
Cannot be combined with ``contents`` or
|
|
384
|
+
``zip_file``.
|
|
385
|
+
zip_file: Path to a zip archive containing
|
|
386
|
+
files for a multi-file question. Cannot be
|
|
387
|
+
combined with ``file_contents``.
|
|
388
|
+
|
|
389
|
+
Returns:
|
|
390
|
+
The created question.
|
|
391
|
+
"""
|
|
392
|
+
lang = language.value if isinstance(language, Language) else language
|
|
393
|
+
data: dict[str, str] = {
|
|
394
|
+
"question[title]": title,
|
|
395
|
+
"question[language]": lang,
|
|
396
|
+
}
|
|
397
|
+
if description is not None:
|
|
398
|
+
data["question[description]"] = description
|
|
399
|
+
if contents is not None:
|
|
400
|
+
data["question[contents]"] = contents
|
|
401
|
+
if solution is not None:
|
|
402
|
+
data["question[solution]"] = solution
|
|
403
|
+
if ai_assist_custom_system_prompt is not None:
|
|
404
|
+
data["question[ai_assist_custom_system_prompt]"] = (
|
|
405
|
+
ai_assist_custom_system_prompt
|
|
406
|
+
)
|
|
407
|
+
if candidate_instructions is not None:
|
|
408
|
+
data["question[candidate_instructions]"] = json.dumps(
|
|
409
|
+
obj=[
|
|
410
|
+
{
|
|
411
|
+
"instructions": ci.instructions,
|
|
412
|
+
"default_visible": ci.default_visible,
|
|
413
|
+
}
|
|
414
|
+
for ci in candidate_instructions
|
|
415
|
+
],
|
|
416
|
+
)
|
|
417
|
+
if file_contents is not None:
|
|
418
|
+
data["question[file_contents]"] = json.dumps(
|
|
419
|
+
obj=[
|
|
420
|
+
{
|
|
421
|
+
"path": fc.path,
|
|
422
|
+
"contents": fc.contents,
|
|
423
|
+
}
|
|
424
|
+
for fc in file_contents
|
|
425
|
+
],
|
|
426
|
+
)
|
|
427
|
+
files: dict[str, tuple[str, bytes, str]] | None = None
|
|
428
|
+
if zip_file is not None:
|
|
429
|
+
files = {
|
|
430
|
+
"question[zip_file]": (
|
|
431
|
+
zip_file.name,
|
|
432
|
+
zip_file.read_bytes(), # noqa: ASYNC240
|
|
433
|
+
"application/zip",
|
|
434
|
+
),
|
|
435
|
+
}
|
|
436
|
+
response = await self._request(
|
|
437
|
+
method="POST",
|
|
438
|
+
url="/api/questions/",
|
|
439
|
+
data=data,
|
|
440
|
+
files=files,
|
|
441
|
+
)
|
|
442
|
+
return Question.from_dict(
|
|
443
|
+
data=response.json(),
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
async def get(
|
|
447
|
+
self,
|
|
448
|
+
*,
|
|
449
|
+
question_id: str,
|
|
450
|
+
) -> Question:
|
|
451
|
+
"""Retrieve a question by id.
|
|
452
|
+
|
|
453
|
+
Args:
|
|
454
|
+
question_id: The id of the question.
|
|
455
|
+
|
|
456
|
+
Returns:
|
|
457
|
+
The question.
|
|
458
|
+
"""
|
|
459
|
+
response = await self._request(
|
|
460
|
+
method="GET",
|
|
461
|
+
url=f"/api/questions/{question_id}",
|
|
462
|
+
)
|
|
463
|
+
return Question.from_dict(
|
|
464
|
+
data=response.json(),
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
async def update(
|
|
468
|
+
self,
|
|
469
|
+
*,
|
|
470
|
+
question_id: str,
|
|
471
|
+
title: str | None = None,
|
|
472
|
+
language: Language | str | None = None,
|
|
473
|
+
description: str | None = None,
|
|
474
|
+
contents: str | None = None,
|
|
475
|
+
solution: str | None = None,
|
|
476
|
+
candidate_instructions: (Sequence[CandidateInstruction] | None) = None,
|
|
477
|
+
file_contents: (Sequence[QuestionFileContent] | None) = None,
|
|
478
|
+
zip_file: Path | None = None,
|
|
479
|
+
) -> None:
|
|
480
|
+
"""Modify an existing question.
|
|
481
|
+
|
|
482
|
+
Args:
|
|
483
|
+
question_id: The id of the question.
|
|
484
|
+
title: New title for the question.
|
|
485
|
+
language: New programming language.
|
|
486
|
+
description: New description.
|
|
487
|
+
contents: New contents. Cannot be combined with
|
|
488
|
+
``file_contents``.
|
|
489
|
+
solution: New solution.
|
|
490
|
+
candidate_instructions: Progressively-revealed
|
|
491
|
+
instruction blocks shown to the candidate.
|
|
492
|
+
file_contents: Files for a multi-file question.
|
|
493
|
+
Cannot be combined with ``contents`` or
|
|
494
|
+
``zip_file``.
|
|
495
|
+
zip_file: Path to a zip archive containing
|
|
496
|
+
files for a multi-file question. Cannot be
|
|
497
|
+
combined with ``file_contents``.
|
|
498
|
+
"""
|
|
499
|
+
data: dict[str, str] = {}
|
|
500
|
+
if title is not None:
|
|
501
|
+
data["question[title]"] = title
|
|
502
|
+
if language is not None:
|
|
503
|
+
lang = (
|
|
504
|
+
language.value if isinstance(language, Language) else language
|
|
505
|
+
)
|
|
506
|
+
data["question[language]"] = lang
|
|
507
|
+
if description is not None:
|
|
508
|
+
data["question[description]"] = description
|
|
509
|
+
if contents is not None:
|
|
510
|
+
data["question[contents]"] = contents
|
|
511
|
+
if solution is not None:
|
|
512
|
+
data["question[solution]"] = solution
|
|
513
|
+
if candidate_instructions is not None:
|
|
514
|
+
data["question[candidate_instructions]"] = json.dumps(
|
|
515
|
+
obj=[
|
|
516
|
+
{
|
|
517
|
+
"instructions": ci.instructions,
|
|
518
|
+
"default_visible": ci.default_visible,
|
|
519
|
+
}
|
|
520
|
+
for ci in candidate_instructions
|
|
521
|
+
],
|
|
522
|
+
)
|
|
523
|
+
if file_contents is not None:
|
|
524
|
+
data["question[file_contents]"] = json.dumps(
|
|
525
|
+
obj=[
|
|
526
|
+
{
|
|
527
|
+
"path": fc.path,
|
|
528
|
+
"contents": fc.contents,
|
|
529
|
+
}
|
|
530
|
+
for fc in file_contents
|
|
531
|
+
],
|
|
532
|
+
)
|
|
533
|
+
files: dict[str, tuple[str, bytes, str]] | None = None
|
|
534
|
+
if zip_file is not None:
|
|
535
|
+
files = {
|
|
536
|
+
"question[zip_file]": (
|
|
537
|
+
zip_file.name,
|
|
538
|
+
zip_file.read_bytes(), # noqa: ASYNC240
|
|
539
|
+
"application/zip",
|
|
540
|
+
),
|
|
541
|
+
}
|
|
542
|
+
await self._request(
|
|
543
|
+
method="PUT",
|
|
544
|
+
url=f"/api/questions/{question_id}",
|
|
545
|
+
data=data,
|
|
546
|
+
files=files,
|
|
547
|
+
)
|
|
548
|
+
|
|
549
|
+
async def delete(
|
|
550
|
+
self,
|
|
551
|
+
*,
|
|
552
|
+
question_id: str,
|
|
553
|
+
) -> None:
|
|
554
|
+
"""Delete a question.
|
|
555
|
+
|
|
556
|
+
Args:
|
|
557
|
+
question_id: The id of the question.
|
|
558
|
+
"""
|
|
559
|
+
await self._request(
|
|
560
|
+
method="DELETE",
|
|
561
|
+
url=f"/api/questions/{question_id}",
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
@beartype
|
|
566
|
+
class AsyncOrganizationPadsNamespace(_AsyncNamespace):
|
|
567
|
+
"""Namespace for async organization pad operations."""
|
|
568
|
+
|
|
569
|
+
async def list(
|
|
570
|
+
self,
|
|
571
|
+
*,
|
|
572
|
+
sort: SortOrder | None = None,
|
|
573
|
+
page: int | None = None,
|
|
574
|
+
) -> PaginatedList[Pad]:
|
|
575
|
+
"""Retrieve pads for the entire organization.
|
|
576
|
+
|
|
577
|
+
Args:
|
|
578
|
+
sort: Sort order.
|
|
579
|
+
page: Page number for pagination.
|
|
580
|
+
|
|
581
|
+
Returns:
|
|
582
|
+
The list of pads with pagination metadata.
|
|
583
|
+
"""
|
|
584
|
+
params: dict[str, str | int] = {}
|
|
585
|
+
if sort is not None:
|
|
586
|
+
params["sort"] = sort.value
|
|
587
|
+
if page is not None:
|
|
588
|
+
params["page"] = page
|
|
589
|
+
response = await self._request(
|
|
590
|
+
method="GET",
|
|
591
|
+
url="/api/organization/pads",
|
|
592
|
+
params=params,
|
|
593
|
+
)
|
|
594
|
+
data = response.json()
|
|
595
|
+
return PaginatedList(
|
|
596
|
+
[Pad.from_dict(data=item) for item in data["pads"]],
|
|
597
|
+
total=data["total"],
|
|
598
|
+
next_page=data.get("next_page"),
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
@beartype
|
|
603
|
+
class AsyncOrganizationQuestionsNamespace(
|
|
604
|
+
_AsyncNamespace,
|
|
605
|
+
):
|
|
606
|
+
"""Namespace for async organization question
|
|
607
|
+
operations.
|
|
608
|
+
"""
|
|
609
|
+
|
|
610
|
+
async def list(
|
|
611
|
+
self,
|
|
612
|
+
*,
|
|
613
|
+
sort: SortOrder | None = None,
|
|
614
|
+
page: int | None = None,
|
|
615
|
+
) -> PaginatedList[Question]:
|
|
616
|
+
"""Retrieve questions for the entire organization.
|
|
617
|
+
|
|
618
|
+
Args:
|
|
619
|
+
sort: Sort order.
|
|
620
|
+
page: Page number for pagination.
|
|
621
|
+
|
|
622
|
+
Returns:
|
|
623
|
+
The list of questions with pagination
|
|
624
|
+
metadata.
|
|
625
|
+
"""
|
|
626
|
+
params: dict[str, str | int] = {}
|
|
627
|
+
if sort is not None:
|
|
628
|
+
params["sort"] = sort.value
|
|
629
|
+
if page is not None:
|
|
630
|
+
params["page"] = page
|
|
631
|
+
response = await self._request(
|
|
632
|
+
method="GET",
|
|
633
|
+
url="/api/organization/questions",
|
|
634
|
+
params=params,
|
|
635
|
+
)
|
|
636
|
+
data = response.json()
|
|
637
|
+
return PaginatedList(
|
|
638
|
+
[Question.from_dict(data=item) for item in data["questions"]],
|
|
639
|
+
total=data["total"],
|
|
640
|
+
next_page=data.get("next_page"),
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
@beartype
|
|
645
|
+
class AsyncOrganizationNamespace(_AsyncNamespace):
|
|
646
|
+
"""Namespace for async organization operations."""
|
|
647
|
+
|
|
648
|
+
def __init__(
|
|
649
|
+
self,
|
|
650
|
+
*,
|
|
651
|
+
transport: AsyncTransport,
|
|
652
|
+
base_url: str,
|
|
653
|
+
headers: dict[str, str],
|
|
654
|
+
) -> None:
|
|
655
|
+
"""Create a new async organization namespace.
|
|
656
|
+
|
|
657
|
+
Args:
|
|
658
|
+
transport: The async HTTP transport.
|
|
659
|
+
base_url: The base URL for the API.
|
|
660
|
+
headers: Headers to send with every request.
|
|
661
|
+
"""
|
|
662
|
+
super().__init__(
|
|
663
|
+
transport=transport,
|
|
664
|
+
base_url=base_url,
|
|
665
|
+
headers=headers,
|
|
666
|
+
)
|
|
667
|
+
self.pads: AsyncOrganizationPadsNamespace = (
|
|
668
|
+
AsyncOrganizationPadsNamespace(
|
|
669
|
+
transport=transport,
|
|
670
|
+
base_url=base_url,
|
|
671
|
+
headers=headers,
|
|
672
|
+
)
|
|
673
|
+
)
|
|
674
|
+
self.questions: AsyncOrganizationQuestionsNamespace = (
|
|
675
|
+
AsyncOrganizationQuestionsNamespace(
|
|
676
|
+
transport=transport,
|
|
677
|
+
base_url=base_url,
|
|
678
|
+
headers=headers,
|
|
679
|
+
)
|
|
680
|
+
)
|
|
681
|
+
|
|
682
|
+
async def get(self) -> Organization:
|
|
683
|
+
"""Retrieve organization information.
|
|
684
|
+
|
|
685
|
+
Returns:
|
|
686
|
+
The organization details.
|
|
687
|
+
"""
|
|
688
|
+
response = await self._request(
|
|
689
|
+
method="GET",
|
|
690
|
+
url="/api/organization",
|
|
691
|
+
)
|
|
692
|
+
return Organization.from_dict(
|
|
693
|
+
data=response.json(),
|
|
694
|
+
)
|
|
695
|
+
|
|
696
|
+
async def get_stats(
|
|
697
|
+
self,
|
|
698
|
+
*,
|
|
699
|
+
start_time: str | None = None,
|
|
700
|
+
end_time: str | None = None,
|
|
701
|
+
) -> OrganizationStats:
|
|
702
|
+
"""Retrieve pad usage stats for the organization.
|
|
703
|
+
|
|
704
|
+
Args:
|
|
705
|
+
start_time: ISO 8601 start of the search
|
|
706
|
+
window.
|
|
707
|
+
end_time: ISO 8601 end of the search window.
|
|
708
|
+
|
|
709
|
+
Returns:
|
|
710
|
+
The usage statistics.
|
|
711
|
+
"""
|
|
712
|
+
params: dict[str, str | int] = {}
|
|
713
|
+
if start_time is not None:
|
|
714
|
+
params["start_time"] = start_time
|
|
715
|
+
if end_time is not None:
|
|
716
|
+
params["end_time"] = end_time
|
|
717
|
+
response = await self._request(
|
|
718
|
+
method="GET",
|
|
719
|
+
url="/api/organization/stats",
|
|
720
|
+
params=params,
|
|
721
|
+
)
|
|
722
|
+
return OrganizationStats.from_dict(
|
|
723
|
+
data=response.json(),
|
|
724
|
+
)
|
|
725
|
+
|
|
726
|
+
async def get_quota(self) -> Quota:
|
|
727
|
+
"""Retrieve quota information.
|
|
728
|
+
|
|
729
|
+
Returns:
|
|
730
|
+
The quota details.
|
|
731
|
+
"""
|
|
732
|
+
response = await self._request(
|
|
733
|
+
method="GET",
|
|
734
|
+
url="/api/quota",
|
|
735
|
+
)
|
|
736
|
+
return Quota.from_dict(data=response.json())
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
@beartype
|
|
740
|
+
class AsyncCoderPad:
|
|
741
|
+
"""An async client for the CoderPad Interview API."""
|
|
742
|
+
|
|
743
|
+
def __init__(
|
|
744
|
+
self,
|
|
745
|
+
*,
|
|
746
|
+
api_key: str,
|
|
747
|
+
base_url: str = ("https://app.coderpad.io"),
|
|
748
|
+
transport: AsyncTransport | None = None,
|
|
749
|
+
) -> None:
|
|
750
|
+
"""Create a new async CoderPad client.
|
|
751
|
+
|
|
752
|
+
Args:
|
|
753
|
+
api_key: The API key for authentication.
|
|
754
|
+
base_url: The base URL for the API.
|
|
755
|
+
transport: The async HTTP transport. Defaults
|
|
756
|
+
to ``AsyncHTTPXTransport()``.
|
|
757
|
+
"""
|
|
758
|
+
self.base_url = base_url
|
|
759
|
+
resolved_transport = transport or AsyncHTTPXTransport()
|
|
760
|
+
headers = {"Authorization": f'Token token="{api_key}"'}
|
|
761
|
+
self.pads: AsyncPadsNamespace = AsyncPadsNamespace(
|
|
762
|
+
transport=resolved_transport,
|
|
763
|
+
base_url=base_url,
|
|
764
|
+
headers=headers,
|
|
765
|
+
)
|
|
766
|
+
self.questions: AsyncQuestionsNamespace = AsyncQuestionsNamespace(
|
|
767
|
+
transport=resolved_transport,
|
|
768
|
+
base_url=base_url,
|
|
769
|
+
headers=headers,
|
|
770
|
+
)
|
|
771
|
+
if isinstance(
|
|
772
|
+
resolved_transport,
|
|
773
|
+
AsyncHTTPXTransport,
|
|
774
|
+
):
|
|
775
|
+
self._aclose = resolved_transport.aclose
|
|
776
|
+
else:
|
|
777
|
+
|
|
778
|
+
async def _noop() -> None:
|
|
779
|
+
"""No-op close for transports without aclose."""
|
|
780
|
+
|
|
781
|
+
self._aclose = _noop
|
|
782
|
+
self.organization: AsyncOrganizationNamespace = (
|
|
783
|
+
AsyncOrganizationNamespace(
|
|
784
|
+
transport=resolved_transport,
|
|
785
|
+
base_url=base_url,
|
|
786
|
+
headers=headers,
|
|
787
|
+
)
|
|
788
|
+
)
|
|
789
|
+
|
|
790
|
+
async def aclose(self) -> None:
|
|
791
|
+
"""Close the underlying transport."""
|
|
792
|
+
await self._aclose()
|
|
793
|
+
|
|
794
|
+
async def __aenter__(self) -> Self:
|
|
795
|
+
"""Enter the async context manager.
|
|
796
|
+
|
|
797
|
+
Returns:
|
|
798
|
+
This client instance.
|
|
799
|
+
"""
|
|
800
|
+
return self
|
|
801
|
+
|
|
802
|
+
async def __aexit__(
|
|
803
|
+
self,
|
|
804
|
+
_exc_type: type[BaseException] | None,
|
|
805
|
+
_exc_val: BaseException | None,
|
|
806
|
+
_exc_tb: object,
|
|
807
|
+
/,
|
|
808
|
+
) -> None:
|
|
809
|
+
"""Exit the async context manager and close."""
|
|
810
|
+
await self.aclose()
|