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