codeapi-client 0.4.1__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,512 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import time
5
+ from pathlib import Path
6
+ from typing import TYPE_CHECKING, AsyncGenerator, List
7
+
8
+ import httpx
9
+
10
+ from codeapi.types import (
11
+ CodeAPIError,
12
+ CodeAPIException,
13
+ CodeType,
14
+ Job,
15
+ JobResult,
16
+ JobStage,
17
+ JobStatus,
18
+ JobType,
19
+ StreamOutput,
20
+ )
21
+
22
+ if TYPE_CHECKING:
23
+ from . import AsyncClient
24
+
25
+
26
+ class AsyncJobsClient:
27
+ def __init__(self, client: AsyncClient):
28
+ self._client = client
29
+
30
+ async def get(self, job_id: str) -> Job:
31
+ """Gets a job.
32
+
33
+ Args:
34
+ job_id (str): The ID of the job.
35
+
36
+ Returns:
37
+ Job: The job.
38
+
39
+ Raises:
40
+ HTTPException: If the request fails.
41
+ """
42
+ url = f"{self._client.base_url}/jobs/{job_id}"
43
+ async with httpx.AsyncClient() as client:
44
+ try:
45
+ response = await client.get(url, headers=self._client.api_key_header)
46
+ response.raise_for_status()
47
+ return Job(**response.json())
48
+ except httpx.HTTPStatusError as e:
49
+ raise self._client._get_http_exception(httpx_error=e)
50
+
51
+ async def list(
52
+ self,
53
+ job_type: JobType | None = None,
54
+ job_status: JobStatus | None = None,
55
+ job_stage: JobStage | None = None,
56
+ ) -> List[Job]:
57
+ """Gets a list of jobs.
58
+
59
+ Args:
60
+ job_type (JobType | None): Filter by job type.
61
+ job_status (JobStatus | None): Filter by job status.
62
+ job_stage (JobStage | None): Filter by job stage.
63
+
64
+ Returns:
65
+ list[Job]: List of jobs.
66
+
67
+ Raises:
68
+ HTTPException: If the request fails.
69
+ """
70
+ url = f"{self._client.base_url}/jobs"
71
+ params = {}
72
+ if job_type is not None:
73
+ params["job_type"] = str(job_type)
74
+ if job_status is not None:
75
+ params["job_status"] = str(job_status)
76
+ if job_stage is not None:
77
+ params["job_stage"] = str(job_stage)
78
+
79
+ async with httpx.AsyncClient() as client:
80
+ try:
81
+ response = await client.get(
82
+ url, headers=self._client.api_key_header, params=params or None
83
+ )
84
+ response.raise_for_status()
85
+ return [Job(**item) for item in response.json()]
86
+ except httpx.HTTPStatusError as e:
87
+ raise self._client._get_http_exception(httpx_error=e)
88
+
89
+ async def get_url(self, job_id: str) -> str:
90
+ """Gets the URL for a job.
91
+
92
+ Args:
93
+ job_id (str): The job ID.
94
+
95
+ Returns:
96
+ str: The job URL.
97
+ """
98
+ return self._client.get_proxy_url(job_id)
99
+
100
+ async def get_status(self, job_id: str) -> JobStatus:
101
+ """Gets the status of a job.
102
+
103
+ Args:
104
+ job_id (str): The ID of the job.
105
+
106
+ Returns:
107
+ JobStatus: The status of the job.
108
+
109
+ Raises:
110
+ HTTPException: If the request fails.
111
+ """
112
+ url = f"{self._client.base_url}/jobs/{job_id}/status"
113
+ async with httpx.AsyncClient() as client:
114
+ try:
115
+ response = await client.get(url, headers=self._client.api_key_header)
116
+ response.raise_for_status()
117
+ return JobStatus(response.json())
118
+ except httpx.HTTPStatusError as e:
119
+ raise self._client._get_http_exception(httpx_error=e)
120
+
121
+ async def get_stage(self, job_id: str) -> JobStage:
122
+ """Gets the stage of a job.
123
+
124
+ Args:
125
+ job_id (str): The ID of the job.
126
+
127
+ Returns:
128
+ JobStage: The stage of the job.
129
+
130
+ Raises:
131
+ HTTPException: If the request fails.
132
+ """
133
+ url = f"{self._client.base_url}/jobs/{job_id}/stage"
134
+ async with httpx.AsyncClient() as client:
135
+ try:
136
+ response = await client.get(url, headers=self._client.api_key_header)
137
+ response.raise_for_status()
138
+ return JobStage(response.json())
139
+ except httpx.HTTPStatusError as e:
140
+ raise self._client._get_http_exception(httpx_error=e)
141
+
142
+ async def get_error(self, job_id: str) -> str | None:
143
+ """Gets the error message of a job.
144
+
145
+ Args:
146
+ job_id (str): The ID of the job.
147
+
148
+ Returns:
149
+ str | None: The error message of the job, or None if no error.
150
+
151
+ Raises:
152
+ HTTPException: If the request fails.
153
+ """
154
+ url = f"{self._client.base_url}/jobs/{job_id}/error"
155
+ async with httpx.AsyncClient() as client:
156
+ try:
157
+ response = await client.get(url, headers=self._client.api_key_header)
158
+ response.raise_for_status()
159
+ return response.json()
160
+ except httpx.HTTPStatusError as e:
161
+ raise self._client._get_http_exception(httpx_error=e)
162
+
163
+ async def get_code_id(self, job_id: str) -> str | None:
164
+ """Gets the code ID associated with a job.
165
+
166
+ Args:
167
+ job_id (str): The ID of the job.
168
+
169
+ Returns:
170
+ str | None: The code ID associated with the job, or None if no code ID.
171
+
172
+ Raises:
173
+ HTTPException: If the request fails.
174
+ """
175
+ url = f"{self._client.base_url}/jobs/{job_id}/code_id"
176
+ async with httpx.AsyncClient() as client:
177
+ try:
178
+ response = await client.get(url, headers=self._client.api_key_header)
179
+ response.raise_for_status()
180
+ return response.json()
181
+ except httpx.HTTPStatusError as e:
182
+ raise self._client._get_http_exception(httpx_error=e)
183
+
184
+ async def get_result(
185
+ self, job_id: str, zip_path: Path | str | None = None
186
+ ) -> JobResult:
187
+ """Gets the result of a job and optionally saves it to a file.
188
+
189
+ Args:
190
+ job_id (str): The ID of the job.
191
+ zip_path (Path | str | None): The path to save the result zip file.
192
+
193
+ Returns:
194
+ JobResult: The job result.
195
+
196
+ Raises:
197
+ HTTPException: If the request fails.
198
+ """
199
+ url = f"{self._client.base_url}/jobs/{job_id}/result"
200
+
201
+ async with httpx.AsyncClient() as client:
202
+ try:
203
+ async with client.stream(
204
+ "GET", url, headers=self._client.api_key_header
205
+ ) as response:
206
+ zip_bytes = await self._client._handle_stream_download_async(
207
+ response, zip_path
208
+ )
209
+ except httpx.HTTPStatusError as e:
210
+ raise self._client._get_http_exception(httpx_error=e)
211
+
212
+ return JobResult(zip_bytes=zip_bytes)
213
+
214
+ async def terminate(self, job_id: str) -> None:
215
+ """Terminates a job.
216
+
217
+ Args:
218
+ job_id (str): The ID of the job.
219
+
220
+ Raises:
221
+ HTTPException: If the request fails.
222
+ """
223
+ url = f"{self._client.base_url}/jobs/{job_id}/terminate"
224
+
225
+ async with httpx.AsyncClient() as client:
226
+ try:
227
+ response = await client.post(url, headers=self._client.api_key_header)
228
+ response.raise_for_status()
229
+ except httpx.HTTPStatusError as e:
230
+ raise self._client._get_http_exception(httpx_error=e)
231
+
232
+ async def stream_output(
233
+ self, job_id: str, read_timeout: float | None = 60
234
+ ) -> AsyncGenerator[StreamOutput, None]:
235
+ """Asynchronously yields stream output of a job line by line.
236
+
237
+ Args:
238
+ job_id (str): The ID of the job.
239
+ read_timeout (float | None): Read timeout for the stream request.
240
+
241
+ Yields:
242
+ StreamOutput: Stream output line by line.
243
+
244
+ Raises:
245
+ HTTPException: If the request fails.
246
+ """
247
+ url = f"{self._client.base_url}/jobs/{job_id}/stream"
248
+
249
+ timeout = httpx.Timeout(connect=5.0, read=read_timeout, write=5.0, pool=5.0)
250
+ async with httpx.AsyncClient() as client:
251
+ async with client.stream(
252
+ "GET", url, timeout=timeout, headers=self._client.api_key_header
253
+ ) as response:
254
+ async for line in response.aiter_lines():
255
+ if line.strip():
256
+ yield StreamOutput.model_validate_json(line)
257
+
258
+ async def await_stage(
259
+ self,
260
+ job_id: str,
261
+ timeout: float | None = None,
262
+ stage: JobStage = JobStage.POST_RUNNING,
263
+ ) -> Job:
264
+ """Waits for a job to reach a specific stage.
265
+
266
+ Args:
267
+ job_id (str): The ID of the job to wait for.
268
+ timeout (float | None): Maximum time to wait in seconds. If None, waits indefinitely.
269
+ stage (JobStage): The stage to wait for. Defaults to POST_RUNNING.
270
+
271
+ Returns:
272
+ Job: The final job object when the desired stage is reached.
273
+
274
+ Raises:
275
+ HTTPException: If the request fails.
276
+ APIException: If the job enters stage POST_RUNNING unexpectedly.
277
+ TimeoutError: If the timeout is exceeded.
278
+ """
279
+ start_time = time.time()
280
+
281
+ while True:
282
+ job = await self.get(job_id)
283
+ current_stage = job.job_stage
284
+ if stage == JobStage.RUNNING and current_stage == JobStage.POST_RUNNING:
285
+ raise CodeAPIException(
286
+ error=CodeAPIError.JOB_POST_RUNNING,
287
+ message=f"Job {job_id} is in stage POST_RUNNING",
288
+ )
289
+ if current_stage in [stage, JobStage.POST_RUNNING, JobStage.UNKNOWN]:
290
+ return job
291
+
292
+ if timeout is not None:
293
+ elapsed_time = time.time() - start_time
294
+ if elapsed_time >= timeout:
295
+ raise TimeoutError(
296
+ f"Job {job_id} did not reach stage {stage} "
297
+ f"within {timeout} seconds"
298
+ )
299
+
300
+ await asyncio.sleep(1)
301
+
302
+ async def is_healthy(self, job_id: str) -> bool:
303
+ """Checks whether a runner job is healthy.
304
+
305
+ Args:
306
+ job_id (str): The ID of the runner job.
307
+
308
+ Returns:
309
+ bool: True if runner job is healthy else False.
310
+
311
+ Raises:
312
+ HTTPException: If the request fails.
313
+ """
314
+ url = f"{self._client.base_url}/{job_id}/ping"
315
+ async with httpx.AsyncClient() as client:
316
+ try:
317
+ response = await client.get(url, headers=self._client.api_key_header)
318
+ response.raise_for_status()
319
+ return response.status_code == 200
320
+ except Exception:
321
+ return False
322
+
323
+ async def await_healthy(self, job_id: str, timeout: float | None = None) -> Job:
324
+ """Waits for a runner job to become healthy.
325
+
326
+ Args:
327
+ job_id (str): The ID of the runner job.
328
+ timeout (float | None): Maximum time to wait in seconds. If None, waits indefinitely.
329
+
330
+ Returns:
331
+ Job: The job object of the runner job.
332
+
333
+ Raises:
334
+ HTTPException: If the request fails.
335
+ APIException: If the job enters stage POST_RUNNING unexpectedly.
336
+ TimeoutError: If the timeout is exceeded.
337
+ """
338
+ start_time = time.time()
339
+
340
+ while not await self.is_healthy(job_id=job_id):
341
+ job = await self._client.jobs.get(job_id)
342
+ if job.job_stage == JobStage.POST_RUNNING:
343
+ raise CodeAPIException(
344
+ error=CodeAPIError.JOB_POST_RUNNING,
345
+ message=f"Job {job_id} unexpectedly entered stage POST_RUNNING",
346
+ )
347
+ if timeout is not None:
348
+ elapsed_time = time.time() - start_time
349
+ if elapsed_time >= timeout:
350
+ raise TimeoutError(
351
+ f"Job {job_id} did not become healthy within {timeout} seconds"
352
+ )
353
+ await asyncio.sleep(1)
354
+
355
+ return await self._client.jobs.get(job_id)
356
+
357
+ async def get_latest(self, code_type: CodeType | None = None) -> Job | None:
358
+ """Get the most recent job.
359
+
360
+ Args:
361
+ code_type (CodeType | None): Filter by code type.
362
+
363
+ Returns:
364
+ Job | None: The most recent job, or None if no jobs exist.
365
+ """
366
+ job_type = JobType.from_code_type(code_type) if code_type else None
367
+ jobs = await self.list(job_type=job_type)
368
+ return jobs[0] if jobs else None
369
+
370
+ async def list_queued(self, code_type: CodeType | None = None) -> List[Job]:
371
+ """Get all queued jobs.
372
+
373
+ Args:
374
+ code_type (CodeType | None): Filter by code type.
375
+
376
+ Returns:
377
+ List[Job]: List of queued jobs.
378
+ """
379
+ job_type = JobType.from_code_type(code_type) if code_type else None
380
+ return await self.list(job_type=job_type, job_status=JobStatus.QUEUED)
381
+
382
+ async def list_scheduled(self, code_type: CodeType | None = None) -> List[Job]:
383
+ """Get all scheduled jobs.
384
+
385
+ Args:
386
+ code_type (CodeType | None): Filter by code type.
387
+
388
+ Returns:
389
+ List[Job]: List of scheduled jobs.
390
+ """
391
+ job_type = JobType.from_code_type(code_type) if code_type else None
392
+ return await self.list(job_type=job_type, job_status=JobStatus.SCHEDULED)
393
+
394
+ async def list_started(self, code_type: CodeType | None = None) -> List[Job]:
395
+ """Get all started jobs.
396
+
397
+ Args:
398
+ code_type (CodeType | None): Filter by code type.
399
+
400
+ Returns:
401
+ List[Job]: List of started jobs.
402
+ """
403
+ job_type = JobType.from_code_type(code_type) if code_type else None
404
+ return await self.list(job_type=job_type, job_status=JobStatus.STARTED)
405
+
406
+ async def list_deferred(self, code_type: CodeType | None = None) -> List[Job]:
407
+ """Get all deferred jobs.
408
+
409
+ Args:
410
+ code_type (CodeType | None): Filter by code type.
411
+
412
+ Returns:
413
+ List[Job]: List of deferred jobs.
414
+ """
415
+ job_type = JobType.from_code_type(code_type) if code_type else None
416
+ return await self.list(job_type=job_type, job_status=JobStatus.DEFERRED)
417
+
418
+ async def list_canceled(self, code_type: CodeType | None = None) -> List[Job]:
419
+ """Get all canceled jobs.
420
+
421
+ Args:
422
+ code_type (CodeType | None): Filter by code type.
423
+
424
+ Returns:
425
+ List[Job]: List of canceled jobs.
426
+ """
427
+ job_type = JobType.from_code_type(code_type) if code_type else None
428
+ return await self.list(job_type=job_type, job_status=JobStatus.CANCELED)
429
+
430
+ async def list_stopped(self, code_type: CodeType | None = None) -> List[Job]:
431
+ """Get all stopped jobs.
432
+
433
+ Args:
434
+ code_type (CodeType | None): Filter by code type.
435
+
436
+ Returns:
437
+ List[Job]: List of stopped jobs.
438
+ """
439
+ job_type = JobType.from_code_type(code_type) if code_type else None
440
+ return await self.list(job_type=job_type, job_status=JobStatus.STOPPED)
441
+
442
+ async def list_failed(self, code_type: CodeType | None = None) -> List[Job]:
443
+ """Get all failed jobs.
444
+
445
+ Args:
446
+ code_type (CodeType | None): Filter by code type.
447
+
448
+ Returns:
449
+ List[Job]: List of failed jobs.
450
+ """
451
+ job_type = JobType.from_code_type(code_type) if code_type else None
452
+ return await self.list(job_type=job_type, job_status=JobStatus.FAILED)
453
+
454
+ async def list_finished(self, code_type: CodeType | None = None) -> List[Job]:
455
+ """Get all finished jobs.
456
+
457
+ Args:
458
+ code_type (CodeType | None): Filter by code type.
459
+
460
+ Returns:
461
+ List[Job]: List of finished jobs.
462
+ """
463
+ job_type = JobType.from_code_type(code_type) if code_type else None
464
+ return await self.list(job_type=job_type, job_status=JobStatus.FINISHED)
465
+
466
+ async def list_timed_out(self, code_type: CodeType | None = None) -> List[Job]:
467
+ """Get all timed out jobs.
468
+
469
+ Args:
470
+ code_type (CodeType | None): Filter by code type.
471
+
472
+ Returns:
473
+ List[Job]: List of timed out jobs.
474
+ """
475
+ job_type = JobType.from_code_type(code_type) if code_type else None
476
+ return await self.list(job_type=job_type, job_status=JobStatus.TIMEOUT)
477
+
478
+ async def list_pre_running(self, code_type: CodeType | None = None) -> List[Job]:
479
+ """Get all pre-running jobs.
480
+
481
+ Args:
482
+ code_type (CodeType | None): Filter by code type.
483
+
484
+ Returns:
485
+ List[Job]: List of pre-running jobs.
486
+ """
487
+ job_type = JobType.from_code_type(code_type) if code_type else None
488
+ return await self.list(job_type=job_type, job_stage=JobStage.PRE_RUNNING)
489
+
490
+ async def list_running(self, code_type: CodeType | None = None) -> List[Job]:
491
+ """Get all running jobs.
492
+
493
+ Args:
494
+ code_type (CodeType | None): Filter by code type.
495
+
496
+ Returns:
497
+ List[Job]: List of running jobs.
498
+ """
499
+ job_type = JobType.from_code_type(code_type) if code_type else None
500
+ return await self.list(job_type=job_type, job_stage=JobStage.RUNNING)
501
+
502
+ async def list_post_running(self, code_type: CodeType | None = None) -> List[Job]:
503
+ """Get all post-running jobs.
504
+
505
+ Args:
506
+ code_type (CodeType | None): Filter by code type.
507
+
508
+ Returns:
509
+ List[Job]: List of post-running jobs.
510
+ """
511
+ job_type = JobType.from_code_type(code_type) if code_type else None
512
+ return await self.list(job_type=job_type, job_stage=JobStage.POST_RUNNING)