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,369 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any, List
4
+
5
+ import httpx
6
+
7
+ from codeapi.types import (
8
+ CodeInfo,
9
+ CodeType,
10
+ CodeZip,
11
+ DataZip,
12
+ EnvVars,
13
+ Job,
14
+ JobStage,
15
+ JobStatus,
16
+ JobType,
17
+ JsonData,
18
+ )
19
+
20
+ if TYPE_CHECKING:
21
+ from . import AsyncClient
22
+
23
+
24
+ class AsyncStoredAppClient:
25
+ def __init__(self, client: AsyncClient):
26
+ self._client = client
27
+
28
+ async def run(
29
+ self,
30
+ code_id: str,
31
+ app_name: str,
32
+ env_vars: EnvVars | dict | None = None,
33
+ data_zip: DataZip | None = None,
34
+ ) -> Job:
35
+ """Runs a stored APP.
36
+
37
+ Args:
38
+ code_id (str): The code ID.
39
+ app_name (str): The name of the APP.
40
+ env_vars (EnvVars | dict | None): Optional environment variables.
41
+ data_zip (DataZip | None): Optional data zip file.
42
+
43
+ Returns:
44
+ Job: The created job.
45
+
46
+ Raises:
47
+ HTTPException: If the request fails.
48
+ """
49
+ url = f"{self._client.base_url}/jobs/code/{code_id}/run/app"
50
+
51
+ files = self._client._prepare_files(data_zip=data_zip)
52
+ data: dict[str, Any] = {"app_name": app_name}
53
+ if env_vars:
54
+ data["env_vars"] = EnvVars(env_vars).json_str
55
+
56
+ async with httpx.AsyncClient() as client:
57
+ try:
58
+ response = await client.post(
59
+ url,
60
+ headers=self._client.api_key_header,
61
+ files=files if files else None,
62
+ data=data,
63
+ )
64
+ response.raise_for_status()
65
+ return Job(**response.json())
66
+ except httpx.HTTPStatusError as e:
67
+ raise self._client._get_http_exception(httpx_error=e)
68
+
69
+ async def list_info(self) -> list[CodeInfo]:
70
+ """List all stored APP code.
71
+
72
+ Returns:
73
+ list[CodeInfo]: List of APP code info.
74
+ """
75
+ return await self._client.code.list_info(code_type=CodeType.APP)
76
+
77
+ async def delete(self, code_id: str) -> str:
78
+ """Delete stored APP code.
79
+
80
+ Args:
81
+ code_id (str): The code ID to delete.
82
+
83
+ Returns:
84
+ str: Deletion confirmation message.
85
+
86
+ Raises:
87
+ ValueError: If the code_id is not APP code.
88
+ """
89
+ # Verify this is actually APP code
90
+ code_info = await self._client.code.get_info(code_id)
91
+ if code_info.code_type != CodeType.APP:
92
+ raise ValueError(
93
+ f"Code '{code_id}' is {code_info.code_type}, not APP code. "
94
+ "Cannot delete non-APP code from APP client."
95
+ )
96
+
97
+ return await self._client.code.delete(code_id)
98
+
99
+
100
+ class AsyncAppJobsClient:
101
+ def __init__(self, client):
102
+ self._client = client
103
+
104
+ async def list(
105
+ self,
106
+ job_status: JobStatus | None = None,
107
+ job_stage: JobStage | None = None,
108
+ ) -> List[Job]:
109
+ """List APP jobs.
110
+
111
+ Args:
112
+ job_status (JobStatus | None): Filter by job status.
113
+ job_stage (JobStage | None): Filter by job stage.
114
+
115
+ Returns:
116
+ list[Job]: List of APP jobs.
117
+ """
118
+ return await self._client.jobs.list(
119
+ job_type=JobType.RUN_APP,
120
+ job_status=job_status,
121
+ job_stage=job_stage,
122
+ )
123
+
124
+ async def get_latest(self) -> Job | None:
125
+ """Get the most recent APP job.
126
+
127
+ Returns:
128
+ Job | None: The most recent APP job, or None if no jobs exist.
129
+ """
130
+ jobs = await self.list()
131
+ return jobs[0] if jobs else None
132
+
133
+ async def list_queued(self) -> List[Job]:
134
+ """Get all queued APP jobs.
135
+
136
+ Returns:
137
+ List[Job]: List of queued APP jobs.
138
+ """
139
+ return await self.list(job_status=JobStatus.QUEUED)
140
+
141
+ async def list_scheduled(self) -> List[Job]:
142
+ """Get all scheduled APP jobs.
143
+
144
+ Returns:
145
+ List[Job]: List of scheduled APP jobs.
146
+ """
147
+ return await self.list(job_status=JobStatus.SCHEDULED)
148
+
149
+ async def list_started(self) -> List[Job]:
150
+ """Get all started APP jobs.
151
+
152
+ Returns:
153
+ list[Job]: List of started APP jobs.
154
+ """
155
+ return await self.list(job_status=JobStatus.STARTED)
156
+
157
+ async def list_deferred(self) -> List[Job]:
158
+ """Get all deferred APP jobs.
159
+
160
+ Returns:
161
+ List[Job]: List of deferred APP jobs.
162
+ """
163
+ return await self.list(job_status=JobStatus.DEFERRED)
164
+
165
+ async def list_canceled(self) -> List[Job]:
166
+ """Get all canceled APP jobs.
167
+
168
+ Returns:
169
+ List[Job]: List of canceled APP jobs.
170
+ """
171
+ return await self.list(job_status=JobStatus.CANCELED)
172
+
173
+ async def list_stopped(self) -> List[Job]:
174
+ """Get all stopped APP jobs.
175
+
176
+ Returns:
177
+ List[Job]: List of stopped APP jobs.
178
+ """
179
+ return await self.list(job_status=JobStatus.STOPPED)
180
+
181
+ async def list_failed(self) -> List[Job]:
182
+ """Get all failed APP jobs.
183
+
184
+ Returns:
185
+ list[Job]: List of failed APP jobs.
186
+ """
187
+ return await self.list(job_status=JobStatus.FAILED)
188
+
189
+ async def list_finished(self) -> List[Job]:
190
+ """Get all finished APP jobs.
191
+
192
+ Returns:
193
+ list[Job]: List of finished APP jobs.
194
+ """
195
+ return await self.list(job_status=JobStatus.FINISHED)
196
+
197
+ async def list_timed_out(self) -> List[Job]:
198
+ """Get all timed out APP jobs.
199
+
200
+ Returns:
201
+ List[Job]: List of timed out APP jobs.
202
+ """
203
+ return await self.list(job_status=JobStatus.TIMEOUT)
204
+
205
+ async def list_pre_running(self) -> List[Job]:
206
+ """Get all pre-running APP jobs.
207
+
208
+ Returns:
209
+ List[Job]: List of pre-running APP jobs.
210
+ """
211
+ return await self.list(job_stage=JobStage.PRE_RUNNING)
212
+
213
+ async def list_running(self) -> List[Job]:
214
+ """Get all running APP jobs.
215
+
216
+ Returns:
217
+ List[Job]: List of running APP jobs.
218
+ """
219
+ return await self.list(job_stage=JobStage.RUNNING)
220
+
221
+ async def list_post_running(self) -> List[Job]:
222
+ """Get all post-running APP jobs.
223
+
224
+ Returns:
225
+ List[Job]: List of post-running APP jobs.
226
+ """
227
+ return await self.list(job_stage=JobStage.POST_RUNNING)
228
+
229
+
230
+ class AsyncAppClient:
231
+ def __init__(self, client: AsyncClient):
232
+ self._client = client
233
+ self.stored = AsyncStoredAppClient(client)
234
+ self.jobs = AsyncAppJobsClient(client)
235
+
236
+ async def run(
237
+ self,
238
+ code_zip: CodeZip,
239
+ app_name: str,
240
+ env_vars: EnvVars | dict | None = None,
241
+ data_zip: DataZip | None = None,
242
+ ) -> Job:
243
+ """Runs an APP from code zip.
244
+
245
+ Args:
246
+ code_zip (CodeZip): The code zip.
247
+ app_name (str): The name of the APP.
248
+ env_vars (EnvVars | dict | None): Optional environment variables.
249
+ data_zip (DataZip | None): Optional data zip file.
250
+
251
+ Returns:
252
+ Job: The created job.
253
+
254
+ Raises:
255
+ HTTPException: If the request fails.
256
+ """
257
+ url = f"{self._client.base_url}/jobs/code/run/app"
258
+
259
+ files = self._client._prepare_files(code_zip=code_zip, data_zip=data_zip)
260
+ data: dict[str, Any] = {"app_name": app_name}
261
+ if env_vars:
262
+ data["env_vars"] = EnvVars(env_vars).json_str
263
+
264
+ async with httpx.AsyncClient() as client:
265
+ try:
266
+ response = await client.post(
267
+ url,
268
+ headers=self._client.api_key_header,
269
+ files=files,
270
+ data=data,
271
+ )
272
+ response.raise_for_status()
273
+ return Job(**response.json())
274
+ except httpx.HTTPStatusError as e:
275
+ raise self._client._get_http_exception(httpx_error=e)
276
+
277
+ async def is_healthy(self, job_id: str) -> bool:
278
+ """Checks whether launched APP is healthy.
279
+
280
+ Args:
281
+ job_id (str): The ID of the APP launch job.
282
+
283
+ Returns:
284
+ bool: True if APP is healthy else False.
285
+
286
+ Raises:
287
+ HTTPException: If the request fails.
288
+ """
289
+ return await self._client.jobs.is_healthy(job_id=job_id)
290
+
291
+ async def await_healthy(self, job_id: str, timeout: float | None = None) -> Job:
292
+ """Waits for a custom APP to become healthy.
293
+
294
+ Args:
295
+ job_id (str): The ID of the custom APP run job.
296
+ timeout (float | None): Maximum time to wait in seconds. If None, waits indefinitely.
297
+
298
+ Returns:
299
+ Job: The job object of the APP run job.
300
+
301
+ Raises:
302
+ HTTPException: If the request fails.
303
+ APIException: If the job enters stage POST_RUNNING unexpectedly.
304
+ TimeoutError: If the timeout is exceeded.
305
+ """
306
+ return await self._client.jobs.await_healthy(job_id=job_id, timeout=timeout)
307
+
308
+ def get_url(self, job_id: str) -> str:
309
+ """Gets the URL for an APP.
310
+
311
+ Args:
312
+ job_id (str): The job ID.
313
+
314
+ Returns:
315
+ str: The APP URL.
316
+ """
317
+ return self._client.get_proxy_url(job_id)
318
+
319
+ async def upload(
320
+ self,
321
+ code_zip: CodeZip,
322
+ code_name: str,
323
+ metadata: JsonData | dict | None = None,
324
+ ) -> str:
325
+ """Upload APP code.
326
+
327
+ Args:
328
+ code_zip (CodeZip): The code zip.
329
+ code_name (str): The name of the code.
330
+ metadata (JsonData | dict | None): The JSON metadata of the code.
331
+
332
+ Returns:
333
+ str: The code ID.
334
+ """
335
+ return await self._client.code.upload(
336
+ code_zip=code_zip,
337
+ code_name=code_name,
338
+ code_type=CodeType.APP,
339
+ metadata=metadata,
340
+ )
341
+
342
+ async def ingest(
343
+ self,
344
+ code_zip: CodeZip,
345
+ code_name: str,
346
+ metadata: JsonData | dict | None = None,
347
+ build_pexenv: bool = False,
348
+ pexenv_python: str | None = None,
349
+ ) -> Job:
350
+ """Ingest APP code.
351
+
352
+ Args:
353
+ code_zip (CodeZip): The code zip.
354
+ code_name (str): The name of the code.
355
+ metadata (JsonData | dict | None): The JSON metadata of the code.
356
+ build_pexenv (bool): Whether to build the pex venv.
357
+ pexenv_python: (str | None): Python interpreter for the pex venv.
358
+
359
+ Returns:
360
+ Job: The code ingestion job.
361
+ """
362
+ return await self._client.code.ingest(
363
+ code_zip=code_zip,
364
+ code_name=code_name,
365
+ code_type=CodeType.APP,
366
+ metadata=metadata,
367
+ build_pexenv=build_pexenv,
368
+ pexenv_python=pexenv_python,
369
+ )
@@ -0,0 +1,334 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, List
4
+
5
+ import httpx
6
+
7
+ if TYPE_CHECKING:
8
+ from . import AsyncClient
9
+ from codeapi.types import (
10
+ CodeInfo,
11
+ CodeType,
12
+ CodeZip,
13
+ DataZip,
14
+ EnvVars,
15
+ Job,
16
+ JobStage,
17
+ JobStatus,
18
+ JobType,
19
+ JsonData,
20
+ )
21
+
22
+
23
+ class AsyncStoredCliClient:
24
+ def __init__(self, client: AsyncClient):
25
+ self._client = client
26
+
27
+ async def run(
28
+ self,
29
+ code_id: str,
30
+ command: str,
31
+ env_vars: EnvVars | dict | None = None,
32
+ data_zip: DataZip | None = None,
33
+ timeout: float | None = None,
34
+ ) -> Job:
35
+ """Runs stored code.
36
+
37
+ Args:
38
+ code_id (str): The code ID.
39
+ command (str): The command to run.
40
+ env_vars (EnvVars | dict | None): Optional environment variables.
41
+ data_zip (DataZip | None): Optional data zip.
42
+ timeout (float | None): The timeout for the job.
43
+
44
+ Returns:
45
+ Job: The created job.
46
+
47
+ Raises:
48
+ HTTPException: If the request fails.
49
+ """
50
+ url = f"{self._client.base_url}/jobs/code/{code_id}/run/cli"
51
+
52
+ files = self._client._prepare_files(data_zip=data_zip)
53
+ data = {"command": command}
54
+ if env_vars:
55
+ data["env_vars"] = EnvVars(env_vars).json_str
56
+ params = {"timeout": timeout} if timeout else {}
57
+
58
+ async with httpx.AsyncClient() as client:
59
+ try:
60
+ response = await client.post(
61
+ url,
62
+ headers=self._client.api_key_header,
63
+ files=files or None,
64
+ data=data,
65
+ params=params,
66
+ )
67
+ response.raise_for_status()
68
+ return Job(**response.json())
69
+ except httpx.HTTPStatusError as e:
70
+ raise self._client._get_http_exception(httpx_error=e)
71
+
72
+ async def list_info(self) -> list[CodeInfo]:
73
+ """List all stored CLI code.
74
+
75
+ Returns:
76
+ list[CodeInfo]: List of CLI code info.
77
+ """
78
+ return await self._client.code.list_info(code_type=CodeType.CLI)
79
+
80
+ async def delete(self, code_id: str) -> str:
81
+ """Delete stored CLI code.
82
+
83
+ Args:
84
+ code_id (str): The code ID to delete.
85
+
86
+ Returns:
87
+ str: Deletion confirmation message.
88
+
89
+ Raises:
90
+ ValueError: If the code_id is not CLI code.
91
+ """
92
+ # Verify this is actually CLI code
93
+ code_info = await self._client.code.get_info(code_id)
94
+ if code_info.code_type != CodeType.CLI:
95
+ raise ValueError(
96
+ f"Code '{code_id}' is {code_info.code_type}, not CLI code. "
97
+ "Cannot delete non-CLI code from CLI client."
98
+ )
99
+
100
+ return await self._client.code.delete(code_id)
101
+
102
+
103
+ class AsyncCliJobsClient:
104
+ def __init__(self, client):
105
+ self._client = client
106
+
107
+ async def list(
108
+ self,
109
+ job_status: JobStatus | None = None,
110
+ job_stage: JobStage | None = None,
111
+ ) -> List[Job]:
112
+ """List CLI jobs.
113
+
114
+ Args:
115
+ job_status (JobStatus | None): Filter by job status.
116
+ job_stage (JobStage | None): Filter by job stage.
117
+
118
+ Returns:
119
+ list[Job]: List of CLI jobs.
120
+ """
121
+ return await self._client.jobs.list(
122
+ job_type=JobType.RUN_CLI,
123
+ job_status=job_status,
124
+ job_stage=job_stage,
125
+ )
126
+
127
+ async def get_latest(self) -> Job | None:
128
+ """Get the most recent CLI job.
129
+
130
+ Returns:
131
+ Job | None: The most recent CLI job, or None if no jobs exist.
132
+ """
133
+ jobs = await self.list()
134
+ return jobs[0] if jobs else None
135
+
136
+ async def list_queued(self) -> List[Job]:
137
+ """Get all queued CLI jobs.
138
+
139
+ Returns:
140
+ List[Job]: List of queued CLI jobs.
141
+ """
142
+ return await self.list(job_status=JobStatus.QUEUED)
143
+
144
+ async def list_scheduled(self) -> List[Job]:
145
+ """Get all scheduled CLI jobs.
146
+
147
+ Returns:
148
+ List[Job]: List of scheduled CLI jobs.
149
+ """
150
+ return await self.list(job_status=JobStatus.SCHEDULED)
151
+
152
+ async def list_started(self) -> List[Job]:
153
+ """Get all started CLI jobs.
154
+
155
+ Returns:
156
+ list[Job]: List of started CLI jobs.
157
+ """
158
+ return await self.list(job_status=JobStatus.STARTED)
159
+
160
+ async def list_deferred(self) -> List[Job]:
161
+ """Get all deferred CLI jobs.
162
+
163
+ Returns:
164
+ List[Job]: List of deferred CLI jobs.
165
+ """
166
+ return await self.list(job_status=JobStatus.DEFERRED)
167
+
168
+ async def list_canceled(self) -> List[Job]:
169
+ """Get all canceled CLI jobs.
170
+
171
+ Returns:
172
+ List[Job]: List of canceled CLI jobs.
173
+ """
174
+ return await self.list(job_status=JobStatus.CANCELED)
175
+
176
+ async def list_stopped(self) -> List[Job]:
177
+ """Get all stopped CLI jobs.
178
+
179
+ Returns:
180
+ List[Job]: List of stopped CLI jobs.
181
+ """
182
+ return await self.list(job_status=JobStatus.STOPPED)
183
+
184
+ async def list_failed(self) -> List[Job]:
185
+ """Get all failed CLI jobs.
186
+
187
+ Returns:
188
+ list[Job]: List of failed CLI jobs.
189
+ """
190
+ return await self.list(job_status=JobStatus.FAILED)
191
+
192
+ async def list_finished(self) -> List[Job]:
193
+ """Get all finished CLI jobs.
194
+
195
+ Returns:
196
+ list[Job]: List of finished CLI jobs.
197
+ """
198
+ return await self.list(job_status=JobStatus.FINISHED)
199
+
200
+ async def list_timed_out(self) -> List[Job]:
201
+ """Get all timed out CLI jobs.
202
+
203
+ Returns:
204
+ List[Job]: List of timed out CLI jobs.
205
+ """
206
+ return await self.list(job_status=JobStatus.TIMEOUT)
207
+
208
+ async def list_pre_running(self) -> List[Job]:
209
+ """Get all pre-running CLI jobs.
210
+
211
+ Returns:
212
+ List[Job]: List of pre-running CLI jobs.
213
+ """
214
+ return await self.list(job_stage=JobStage.PRE_RUNNING)
215
+
216
+ async def list_running(self) -> List[Job]:
217
+ """Get all running CLI jobs.
218
+
219
+ Returns:
220
+ List[Job]: List of running CLI jobs.
221
+ """
222
+ return await self.list(job_stage=JobStage.RUNNING)
223
+
224
+ async def list_post_running(self) -> List[Job]:
225
+ """Get all post-running CLI jobs.
226
+
227
+ Returns:
228
+ List[Job]: List of post-running CLI jobs.
229
+ """
230
+ return await self.list(job_stage=JobStage.POST_RUNNING)
231
+
232
+
233
+ class AsyncCliClient:
234
+ def __init__(self, client: AsyncClient):
235
+ self._client = client
236
+ self.stored = AsyncStoredCliClient(client)
237
+ self.jobs = AsyncCliJobsClient(client)
238
+
239
+ async def run(
240
+ self,
241
+ code_zip: CodeZip,
242
+ command: str,
243
+ env_vars: EnvVars | dict | None = None,
244
+ data_zip: DataZip | None = None,
245
+ timeout: float | None = None,
246
+ ) -> Job:
247
+ """Starts a CLI job from code zip.
248
+
249
+ Args:
250
+ code_zip (CodeZip): The code zip.
251
+ command (str): The command to run.
252
+ env_vars (EnvVars | dict | None): Optional environment variables.
253
+ data_zip (DataZip | None): Optional data zip.
254
+ timeout (float | None): The timeout for the job.
255
+
256
+ Returns:
257
+ Job: The created job.
258
+
259
+ Raises:
260
+ HTTPException: If the request fails.
261
+ """
262
+ url = f"{self._client.base_url}/jobs/code/run/cli"
263
+
264
+ files = self._client._prepare_files(code_zip=code_zip, data_zip=data_zip)
265
+ data = {"command": command}
266
+ if env_vars:
267
+ data["env_vars"] = EnvVars(env_vars).json_str
268
+ params = {"timeout": timeout} if timeout else {}
269
+
270
+ async with httpx.AsyncClient() as client:
271
+ try:
272
+ response = await client.post(
273
+ url,
274
+ headers=self._client.api_key_header,
275
+ files=files or None,
276
+ data=data,
277
+ params=params,
278
+ )
279
+ response.raise_for_status()
280
+ return Job(**response.json())
281
+ except httpx.HTTPStatusError as e:
282
+ raise self._client._get_http_exception(httpx_error=e)
283
+
284
+ async def upload(
285
+ self,
286
+ code_zip: CodeZip,
287
+ code_name: str,
288
+ metadata: JsonData | dict | None = None,
289
+ ) -> str:
290
+ """Upload CLI code.
291
+
292
+ Args:
293
+ code_zip (CodeZip): The code zip.
294
+ code_name (str): The name of the code.
295
+ metadata (JsonData | dict | None): The JSON metadata of the code.
296
+
297
+ Returns:
298
+ str: The code ID.
299
+ """
300
+ return await self._client.code.upload(
301
+ code_zip=code_zip,
302
+ code_name=code_name,
303
+ code_type=CodeType.CLI,
304
+ metadata=metadata,
305
+ )
306
+
307
+ async def ingest(
308
+ self,
309
+ code_zip: CodeZip,
310
+ code_name: str,
311
+ metadata: JsonData | dict | None = None,
312
+ build_pexenv: bool = False,
313
+ pexenv_python: str | None = None,
314
+ ) -> Job:
315
+ """Ingest CLI code.
316
+
317
+ Args:
318
+ code_zip (CodeZip): The code zip.
319
+ code_name (str): The name of the code.
320
+ metadata (JsonData | dict | None): The JSON metadata of the code.
321
+ build_pexenv (bool): Whether to build the pex venv.
322
+ pexenv_python: (str | None): Python interpreter for the pex venv.
323
+
324
+ Returns:
325
+ Job: The code ingestion job.
326
+ """
327
+ return await self._client.code.ingest(
328
+ code_zip=code_zip,
329
+ code_name=code_name,
330
+ code_type=CodeType.CLI,
331
+ metadata=metadata,
332
+ build_pexenv=build_pexenv,
333
+ pexenv_python=pexenv_python,
334
+ )