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,341 @@
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 Client
9
+ from codeapi.types import (
10
+ CodeInfo,
11
+ CodeType,
12
+ CodeZip,
13
+ Job,
14
+ JobStage,
15
+ JobStatus,
16
+ JobType,
17
+ JsonData,
18
+ )
19
+
20
+
21
+ class StoredWebClient:
22
+ def __init__(self, client: Client):
23
+ self._client = client
24
+
25
+ def run(
26
+ self,
27
+ code_id: str,
28
+ ) -> Job:
29
+ """Runs a stored Web page.
30
+
31
+ Args:
32
+ code_id (str): The code ID.
33
+
34
+ Returns:
35
+ Job: The created job.
36
+
37
+ Raises:
38
+ HTTPException: If the request fails.
39
+ """
40
+ url = f"{self._client.base_url}/jobs/code/{code_id}/run/web"
41
+
42
+ try:
43
+ response = httpx.post(
44
+ url,
45
+ headers=self._client.api_key_header,
46
+ )
47
+ response.raise_for_status()
48
+ return Job(**response.json())
49
+ except httpx.HTTPStatusError as e:
50
+ raise self._client._get_http_exception(httpx_error=e)
51
+
52
+ def list_info(self) -> list[CodeInfo]:
53
+ """List all stored Web code.
54
+
55
+ Returns:
56
+ list[CodeInfo]: List of Web code info.
57
+ """
58
+ return self._client.code.list_info(code_type=CodeType.WEB)
59
+
60
+ def delete(self, code_id: str) -> str:
61
+ """Delete stored Web code.
62
+
63
+ Args:
64
+ code_id (str): The code ID to delete.
65
+
66
+ Returns:
67
+ str: Deletion confirmation message.
68
+
69
+ Raises:
70
+ ValueError: If the code_id is not Web code.
71
+ """
72
+ # Verify this is actually Web code
73
+ code_info = self._client.code.get_info(code_id)
74
+ if code_info.code_type != CodeType.WEB:
75
+ raise ValueError(
76
+ f"Code '{code_id}' is {code_info.code_type}, not Web code. "
77
+ "Cannot delete non-Web code from Web client."
78
+ )
79
+
80
+ return self._client.code.delete(code_id)
81
+
82
+
83
+ class WebJobsClient:
84
+ def __init__(self, client):
85
+ self._client = client
86
+
87
+ def list(
88
+ self,
89
+ job_status: JobStatus | None = None,
90
+ job_stage: JobStage | None = None,
91
+ ) -> List[Job]:
92
+ """List Web jobs.
93
+
94
+ Args:
95
+ job_status (JobStatus | None): Filter by job status.
96
+ job_stage (JobStage | None): Filter by job stage.
97
+
98
+ Returns:
99
+ list[Job]: List of Web jobs.
100
+ """
101
+ return self._client.jobs.list(
102
+ job_type=JobType.RUN_WEB,
103
+ job_status=job_status,
104
+ job_stage=job_stage,
105
+ )
106
+
107
+ def get_latest(self) -> Job | None:
108
+ """Get the most recent Web job.
109
+
110
+ Returns:
111
+ Job | None: The most recent Web job, or None if no jobs exist.
112
+ """
113
+ jobs = self.list()
114
+ return jobs[0] if jobs else None
115
+
116
+ def list_queued(self) -> List[Job]:
117
+ """Get all queued Web jobs.
118
+
119
+ Returns:
120
+ List[Job]: List of queued Web jobs.
121
+ """
122
+ return self.list(job_status=JobStatus.QUEUED)
123
+
124
+ def list_scheduled(self) -> List[Job]:
125
+ """Get all scheduled Web jobs.
126
+
127
+ Returns:
128
+ List[Job]: List of scheduled Web jobs.
129
+ """
130
+ return self.list(job_status=JobStatus.SCHEDULED)
131
+
132
+ def list_started(self) -> List[Job]:
133
+ """Get all started Web jobs.
134
+
135
+ Returns:
136
+ list[Job]: List of started Web jobs.
137
+ """
138
+ return self.list(job_status=JobStatus.STARTED)
139
+
140
+ def list_deferred(self) -> List[Job]:
141
+ """Get all deferred Web jobs.
142
+
143
+ Returns:
144
+ List[Job]: List of deferred Web jobs.
145
+ """
146
+ return self.list(job_status=JobStatus.DEFERRED)
147
+
148
+ def list_canceled(self) -> List[Job]:
149
+ """Get all canceled Web jobs.
150
+
151
+ Returns:
152
+ List[Job]: List of canceled Web jobs.
153
+ """
154
+ return self.list(job_status=JobStatus.CANCELED)
155
+
156
+ def list_stopped(self) -> List[Job]:
157
+ """Get all stopped Web jobs.
158
+
159
+ Returns:
160
+ List[Job]: List of stopped Web jobs.
161
+ """
162
+ return self.list(job_status=JobStatus.STOPPED)
163
+
164
+ def list_failed(self) -> List[Job]:
165
+ """Get all failed Web jobs.
166
+
167
+ Returns:
168
+ list[Job]: List of failed Web jobs.
169
+ """
170
+ return self.list(job_status=JobStatus.FAILED)
171
+
172
+ def list_finished(self) -> List[Job]:
173
+ """Get all finished Web jobs.
174
+
175
+ Returns:
176
+ list[Job]: List of finished Web jobs.
177
+ """
178
+ return self.list(job_status=JobStatus.FINISHED)
179
+
180
+ def list_timed_out(self) -> List[Job]:
181
+ """Get all timed out Web jobs.
182
+
183
+ Returns:
184
+ List[Job]: List of timed out Web jobs.
185
+ """
186
+ return self.list(job_status=JobStatus.TIMEOUT)
187
+
188
+ def list_pre_running(self) -> List[Job]:
189
+ """Get all pre-running Web jobs.
190
+
191
+ Returns:
192
+ List[Job]: List of pre-running Web jobs.
193
+ """
194
+ return self.list(job_stage=JobStage.PRE_RUNNING)
195
+
196
+ def list_running(self) -> List[Job]:
197
+ """Get all running Web jobs.
198
+
199
+ Returns:
200
+ List[Job]: List of running Web jobs.
201
+ """
202
+ return self.list(job_stage=JobStage.RUNNING)
203
+
204
+ def list_post_running(self) -> List[Job]:
205
+ """Get all post-running Web jobs.
206
+
207
+ Returns:
208
+ List[Job]: List of post-running Web jobs.
209
+ """
210
+ return self.list(job_stage=JobStage.POST_RUNNING)
211
+
212
+
213
+ class WebClient:
214
+ def __init__(self, client: Client):
215
+ self._client = client
216
+ self.stored = StoredWebClient(client)
217
+ self.jobs = WebJobsClient(client)
218
+
219
+ def run(
220
+ self,
221
+ code_zip: CodeZip,
222
+ ) -> Job:
223
+ """Runs a Web page from code zip.
224
+
225
+ Args:
226
+ code_zip (CodeZip): The code zip.
227
+
228
+ Returns:
229
+ Job: The created job.
230
+
231
+ Raises:
232
+ HTTPException: If the request fails.
233
+ """
234
+ url = f"{self._client.base_url}/jobs/code/run/web"
235
+
236
+ files = self._client._prepare_files(code_zip=code_zip)
237
+
238
+ try:
239
+ response = httpx.post(
240
+ url,
241
+ headers=self._client.api_key_header,
242
+ files=files,
243
+ )
244
+ response.raise_for_status()
245
+ return Job(**response.json())
246
+ except httpx.HTTPStatusError as e:
247
+ raise self._client._get_http_exception(httpx_error=e)
248
+
249
+ def get_url(self, job_id: str) -> str:
250
+ """Gets the URL for a Web page.
251
+
252
+ Args:
253
+ job_id (str): The job ID.
254
+
255
+ Returns:
256
+ str: The Web page URL.
257
+ """
258
+ return self._client.get_subdomain_proxy_url(job_id)
259
+
260
+ def upload(
261
+ self,
262
+ code_zip: CodeZip,
263
+ code_name: str,
264
+ metadata: JsonData | dict | None = None,
265
+ ) -> str:
266
+ """Upload Web code.
267
+
268
+ Args:
269
+ code_zip (CodeZip): The code zip.
270
+ code_name (str): The name of the code.
271
+ metadata (JsonData | dict | None): The JSON metadata of the code.
272
+
273
+ Returns:
274
+ str: The code ID.
275
+ """
276
+ return self._client.code.upload(
277
+ code_zip=code_zip,
278
+ code_name=code_name,
279
+ code_type=CodeType.WEB,
280
+ metadata=metadata,
281
+ )
282
+
283
+ def is_healthy(self, job_id: str) -> bool:
284
+ """Checks whether launched Web page is healthy.
285
+
286
+ Args:
287
+ job_id (str): The ID of the Web page launch job.
288
+
289
+ Returns:
290
+ bool: True if Web page is healthy else False.
291
+
292
+ Raises:
293
+ HTTPException: If the request fails.
294
+ """
295
+ return self._client.jobs.is_healthy(job_id=job_id)
296
+
297
+ def await_healthy(self, job_id: str, timeout: float | None = None) -> Job:
298
+ """Waits for a Web page to become healthy.
299
+
300
+ Args:
301
+ job_id (str): The ID of the Web page run job.
302
+ timeout (float | None): Maximum time to wait in seconds. If None, waits indefinitely.
303
+
304
+ Returns:
305
+ Job: The job object of the Web page run job.
306
+
307
+ Raises:
308
+ HTTPException: If the request fails.
309
+ APIException: If the job enters stage POST_RUNNING unexpectedly.
310
+ TimeoutError: If the timeout is exceeded.
311
+ """
312
+ return self._client.jobs.await_healthy(job_id=job_id, timeout=timeout)
313
+
314
+ def ingest(
315
+ self,
316
+ code_zip: CodeZip,
317
+ code_name: str,
318
+ metadata: JsonData | dict | None = None,
319
+ build_pexenv: bool = False,
320
+ pexenv_python: str | None = None,
321
+ ) -> Job:
322
+ """Ingest Web code.
323
+
324
+ Args:
325
+ code_zip (CodeZip): The code zip.
326
+ code_name (str): The name of the code.
327
+ metadata (JsonData | dict | None): The JSON metadata of the code.
328
+ build_pexenv (bool): Whether to build the pex venv.
329
+ pexenv_python: (str | None): Python interpreter for the pex venv.
330
+
331
+ Returns:
332
+ Job: The code ingestion job.
333
+ """
334
+ return self._client.code.ingest(
335
+ code_zip=code_zip,
336
+ code_name=code_name,
337
+ code_type=CodeType.WEB,
338
+ metadata=metadata,
339
+ build_pexenv=build_pexenv,
340
+ pexenv_python=pexenv_python,
341
+ )
@@ -0,0 +1,61 @@
1
+ import os
2
+ from pathlib import Path
3
+
4
+ import aiofiles
5
+
6
+
7
+ def get_file_dir(filepath: Path | str) -> str:
8
+ return str(Path(filepath).parent)
9
+
10
+
11
+ def get_filename(filepath: Path | str) -> str:
12
+ return Path(filepath).name
13
+
14
+
15
+ def file_exists(path: Path | str) -> bool:
16
+ return os.path.isfile(path)
17
+
18
+
19
+ def read_file(path: Path | str) -> str:
20
+ return Path(path).read_text()
21
+
22
+
23
+ async def read_file_async(path: Path | str) -> str:
24
+ async with aiofiles.open(path, mode="r") as f:
25
+ return await f.read()
26
+
27
+
28
+ def read_bytes(path: Path | str) -> bytes:
29
+ return Path(path).read_bytes()
30
+
31
+
32
+ async def read_bytes_async(path: Path | str) -> bytes:
33
+ async with aiofiles.open(path, mode="rb") as f:
34
+ return await f.read()
35
+
36
+
37
+ def read_lines(path: Path | str) -> list[str]:
38
+ return read_file(path=path).splitlines()
39
+
40
+
41
+ async def read_lines_async(path: Path | str) -> list[str]:
42
+ content = await read_file_async(path)
43
+ return content.splitlines()
44
+
45
+
46
+ def write_file(path: Path | str, data: str) -> None:
47
+ Path(path).write_text(data=data)
48
+
49
+
50
+ async def write_file_async(path: Path | str, data: str) -> None:
51
+ async with aiofiles.open(path, "w") as f:
52
+ await f.write(data)
53
+
54
+
55
+ def write_bytes(path: Path | str, data: bytes) -> None:
56
+ Path(path).write_bytes(data=data)
57
+
58
+
59
+ async def write_bytes_async(path: Path | str, data: bytes) -> None:
60
+ async with aiofiles.open(path, "wb") as f:
61
+ await f.write(data)
File without changes
@@ -0,0 +1,77 @@
1
+ from ._api import (
2
+ API_KEY_HEADER,
3
+ ServerStatus,
4
+ ServicesHealth,
5
+ TokenRequest,
6
+ TokenResponse,
7
+ )
8
+ from ._base import CustomBaseModel
9
+ from ._code import CodeInfo
10
+ from ._enums import (
11
+ CodeAPIError,
12
+ CodeType,
13
+ ExitType,
14
+ JobStage,
15
+ JobStatus,
16
+ JobType,
17
+ WorkerDeployment,
18
+ )
19
+ from ._env import EnvVars
20
+ from ._exc import CodeAPIException
21
+ from ._job import Job
22
+ from ._json import JsonData
23
+ from ._stream import StreamOutput
24
+ from ._swarm import HiveHeartbeat, SwarmStats
25
+ from ._time import T_HOURS, T_MINUTES, T_SECONDS
26
+ from ._zips import (
27
+ DOTENV_FILE,
28
+ EMPTY_ZIP_BYTES,
29
+ PEXENV_CMD,
30
+ PEXENV_FILE,
31
+ REQUIREMENTS_TXT,
32
+ CodeZip,
33
+ DataZip,
34
+ JobResult,
35
+ ZipBytes,
36
+ )
37
+
38
+ __all__ = [
39
+ "API_KEY_HEADER",
40
+ "CodeAPIError",
41
+ "CodeAPIException",
42
+ "DOTENV_FILE",
43
+ "EMPTY_ZIP_BYTES",
44
+ "PEXENV_CMD",
45
+ "PEXENV_FILE",
46
+ "REQUIREMENTS_TXT",
47
+ "CodeInfo",
48
+ "CodeType",
49
+ "CodeZip",
50
+ "CustomBaseModel",
51
+ "DataZip",
52
+ "EnvVars",
53
+ "ExitType",
54
+ "HiveHeartbeat",
55
+ "SwarmStats",
56
+ "Job",
57
+ "JobResult",
58
+ "JobStage",
59
+ "JobStatus",
60
+ "JobType",
61
+ "JsonData",
62
+ "ServerStatus",
63
+ "ServicesHealth",
64
+ "StreamOutput",
65
+ "T_HOURS",
66
+ "T_MINUTES",
67
+ "T_SECONDS",
68
+ "TokenRequest",
69
+ "TokenResponse",
70
+ "WorkerDeployment",
71
+ "ZipBytes",
72
+ ]
73
+
74
+
75
+ for __name in __all__:
76
+ if not __name.startswith("__") and hasattr(locals()[__name], "__module__"):
77
+ setattr(locals()[__name], "__module__", __name__)
codeapi/types/_api.py ADDED
@@ -0,0 +1,30 @@
1
+ from pydantic import BaseModel
2
+
3
+ API_KEY_HEADER = "CodeAPI-Key"
4
+
5
+
6
+ class TokenRequest(BaseModel):
7
+ user: str
8
+ ulid: str | None = None
9
+ ttl: int = -1 # [s]
10
+
11
+
12
+ class TokenResponse(BaseModel):
13
+ token: str
14
+
15
+
16
+ class ServerStatus(BaseModel):
17
+ is_healthy: bool
18
+
19
+
20
+ class ServicesHealth(BaseModel):
21
+ redis_healthy: bool
22
+ storage_healthy: bool
23
+
24
+ @property
25
+ def all_healthy(self) -> bool:
26
+ return all(
27
+ getattr(self, attr)
28
+ for attr in self.__dict__
29
+ if isinstance(getattr(self, attr), bool)
30
+ )
codeapi/types/_base.py ADDED
@@ -0,0 +1,21 @@
1
+ from pydantic import BaseModel
2
+ from strenum import StrEnum
3
+
4
+
5
+ class CustomBaseModel(BaseModel):
6
+ def __str__(self):
7
+ fields = self.__class__.__pydantic_fields__
8
+ field_values = ", ".join(
9
+ f"{k}={repr(v)}" for k, v in self.model_dump().items() if fields[k].repr
10
+ )
11
+ return f"{self.__class__.__name__}({field_values})"
12
+
13
+
14
+ class CaseInsensitiveStrEnum(StrEnum):
15
+ @classmethod
16
+ def _missing_(cls, value):
17
+ if isinstance(value, str):
18
+ value = value.upper()
19
+ if value in cls._value2member_map_:
20
+ return cls._value2member_map_[value]
21
+ return None
codeapi/types/_code.py ADDED
@@ -0,0 +1,31 @@
1
+ from pydantic import BaseModel, ConfigDict, field_serializer, field_validator
2
+
3
+ from ._enums import CodeType
4
+ from ._json import JsonData
5
+
6
+
7
+ class CodeInfo(BaseModel):
8
+ model_config = ConfigDict(arbitrary_types_allowed=True)
9
+
10
+ code_id: str
11
+ code_name: str
12
+ code_type: CodeType
13
+ metadata: JsonData
14
+
15
+ @field_serializer("metadata")
16
+ def serialize_metadata(self, value: JsonData) -> dict:
17
+ return dict(value)
18
+
19
+ @field_validator("metadata", mode="before")
20
+ @classmethod
21
+ def deserialize_metadata(cls, value: JsonData | dict) -> JsonData:
22
+ if isinstance(value, JsonData):
23
+ return value
24
+ return JsonData(value)
25
+
26
+ def __str__(self):
27
+ fields = self.__class__.__pydantic_fields__
28
+ field_values = ", ".join(
29
+ f"{k}={repr(v)}" for k, v in self.model_dump().items() if fields[k].repr
30
+ )
31
+ return f"{self.__class__.__name__}({field_values})"
@@ -0,0 +1,150 @@
1
+ from __future__ import annotations
2
+
3
+ from enum import Enum, auto
4
+ from typing import Type
5
+
6
+ from strenum import StrEnum
7
+
8
+
9
+ class CodeType(StrEnum):
10
+ CLI = auto()
11
+ APP = auto()
12
+ MCP = auto()
13
+ WEB = auto()
14
+ LIB = auto()
15
+ UNKNOWN = auto()
16
+
17
+ def __repr__(self):
18
+ return f"{self.__class__.__name__}.{self.name}"
19
+
20
+
21
+ class ExitType(StrEnum):
22
+ NORMAL = auto()
23
+ ERROR = auto()
24
+ TIMEOUT = auto()
25
+ TERMINATED = auto()
26
+
27
+ def __repr__(self):
28
+ return f"{self.__class__.__name__}.{self.name}"
29
+
30
+
31
+ class JobType(StrEnum):
32
+ RUN_CLI = auto()
33
+ RUN_APP = auto()
34
+ RUN_MCP = auto()
35
+ RUN_WEB = auto()
36
+ INGEST = auto()
37
+ PEXENV = auto()
38
+ REQUIREMENTS = auto()
39
+
40
+ @classmethod
41
+ def from_code_type(cls, code_type: CodeType) -> JobType | None:
42
+ """Maps CodeType to corresponding JobType for run operations.
43
+
44
+ Args:
45
+ code_type (CodeType): The code type to map.
46
+
47
+ Returns:
48
+ JobType | None: The corresponding job type, or None if no mapping exists.
49
+ """
50
+ mapping = {
51
+ CodeType.CLI: cls.RUN_CLI,
52
+ CodeType.APP: cls.RUN_APP,
53
+ CodeType.MCP: cls.RUN_MCP,
54
+ CodeType.WEB: cls.RUN_WEB,
55
+ # CodeType.LIB has no corresponding run job type
56
+ }
57
+ return mapping.get(code_type)
58
+
59
+ def __repr__(self):
60
+ return f"{self.__class__.__name__}.{self.name}"
61
+
62
+
63
+ class JobStage(StrEnum):
64
+ UNKNOWN = auto()
65
+ PRE_RUNNING = auto()
66
+ RUNNING = auto()
67
+ POST_RUNNING = auto()
68
+
69
+ def __repr__(self):
70
+ return f"{self.__class__.__name__}.{self.name}"
71
+
72
+
73
+ class JobStatus(StrEnum):
74
+ QUEUED = auto()
75
+ SCHEDULED = auto()
76
+ STARTED = auto()
77
+ DEFERRED = auto()
78
+ CANCELED = auto()
79
+ STOPPED = auto()
80
+ FAILED = auto()
81
+ FINISHED = auto()
82
+ TIMEOUT = auto()
83
+ UNKNOWN = auto()
84
+
85
+ @classmethod
86
+ def from_rq(cls: Type[JobStatus], rq_status: Enum | None) -> JobStatus:
87
+ if not rq_status:
88
+ return cls.UNKNOWN
89
+ return cls(str(rq_status.value).upper())
90
+
91
+ @property
92
+ def stage(self) -> JobStage:
93
+ if self == JobStatus.UNKNOWN:
94
+ return JobStage.UNKNOWN
95
+ if self in [JobStatus.QUEUED, JobStatus.SCHEDULED, JobStatus.DEFERRED]:
96
+ return JobStage.PRE_RUNNING
97
+ if self == JobStatus.STARTED:
98
+ return JobStage.RUNNING
99
+ return JobStage.POST_RUNNING # [CANCELED, STOPPED, FAILED, FINISHED, TIMEOUT]
100
+
101
+ def __repr__(self):
102
+ return f"{self.__class__.__name__}.{self.name}"
103
+
104
+
105
+ class HiveStatus(StrEnum):
106
+ ALIVE = auto()
107
+ IDLE = auto()
108
+ BUSY = auto()
109
+ DEAD = auto()
110
+
111
+ @classmethod
112
+ def _missing_(cls, value):
113
+ if isinstance(value, str):
114
+ value = value.upper()
115
+ if value in cls._value2member_map_:
116
+ return cls._value2member_map_[value]
117
+ return None
118
+
119
+
120
+ class WorkerDeployment(StrEnum):
121
+ LOCAL = auto()
122
+ HIVES = auto()
123
+
124
+ @classmethod
125
+ def _missing_(cls, value):
126
+ if isinstance(value, str):
127
+ value = value.upper()
128
+ if value in cls._value2member_map_:
129
+ return cls._value2member_map_[value]
130
+ return None
131
+
132
+ def __repr__(self):
133
+ return f"{self.__class__.__name__}.{self.name}"
134
+
135
+
136
+ class CodeAPIError(StrEnum):
137
+ CODE_NOT_FOUND = auto()
138
+ JOB_FAILED = auto()
139
+ NO_JOB_META = auto()
140
+ JOB_NOT_FINISHED = auto()
141
+ JOB_NOT_FOUND = auto()
142
+ JOB_POST_RUNNING = auto()
143
+ JOB_STILL_QUEUED = auto()
144
+ JOB_TIMED_OUT = auto()
145
+ NO_ASSOCIATED_CODE_ID = auto()
146
+ NO_STORAGE_BACKEND = auto()
147
+ UNKNOWN = auto()
148
+
149
+ def __repr__(self):
150
+ return f"{self.__class__.__name__}.{self.name}"