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.
- codeapi/__init__.py +17 -0
- codeapi/client/__init__.py +37 -0
- codeapi/client/_async/__init__.py +357 -0
- codeapi/client/_async/_app.py +369 -0
- codeapi/client/_async/_cli.py +334 -0
- codeapi/client/_async/_code.py +211 -0
- codeapi/client/_async/_jobs.py +512 -0
- codeapi/client/_async/_mcp.py +445 -0
- codeapi/client/_async/_web.py +343 -0
- codeapi/client/_base.py +118 -0
- codeapi/client/_sync/__init__.py +347 -0
- codeapi/client/_sync/_app.py +367 -0
- codeapi/client/_sync/_cli.py +332 -0
- codeapi/client/_sync/_code.py +203 -0
- codeapi/client/_sync/_jobs.py +497 -0
- codeapi/client/_sync/_mcp.py +442 -0
- codeapi/client/_sync/_web.py +341 -0
- codeapi/client/_utils.py +61 -0
- codeapi/client/py.typed +0 -0
- codeapi/types/__init__.py +77 -0
- codeapi/types/_api.py +30 -0
- codeapi/types/_base.py +21 -0
- codeapi/types/_code.py +31 -0
- codeapi/types/_enums.py +150 -0
- codeapi/types/_env.py +65 -0
- codeapi/types/_exc.py +35 -0
- codeapi/types/_job.py +67 -0
- codeapi/types/_json.py +67 -0
- codeapi/types/_stream.py +36 -0
- codeapi/types/_swarm.py +85 -0
- codeapi/types/_time.py +46 -0
- codeapi/types/_zips.py +466 -0
- codeapi/types/py.typed +0 -0
- codeapi_client-0.4.1.dist-info/METADATA +14 -0
- codeapi_client-0.4.1.dist-info/RECORD +36 -0
- codeapi_client-0.4.1.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import TYPE_CHECKING, Any, List, Literal
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
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
|
+
if TYPE_CHECKING:
|
|
23
|
+
from fastmcp.mcp_config import MCPConfig
|
|
24
|
+
|
|
25
|
+
from . import AsyncClient
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class AsyncStoredMcpClient:
|
|
29
|
+
def __init__(self, client: AsyncClient):
|
|
30
|
+
self._client = client
|
|
31
|
+
|
|
32
|
+
async def run(
|
|
33
|
+
self,
|
|
34
|
+
code_id: str,
|
|
35
|
+
mcp_module: str,
|
|
36
|
+
env_vars: EnvVars | dict | None = None,
|
|
37
|
+
transport: Literal["streamable-http", "http", "sse"] = "streamable-http",
|
|
38
|
+
data_zip: DataZip | None = None,
|
|
39
|
+
) -> Job:
|
|
40
|
+
"""Runs a stored MCP server.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
code_id (str): The code ID.
|
|
44
|
+
mcp_module (str): The name of the MCP server module.
|
|
45
|
+
env_vars (EnvVars | dict | None): Optional environment variables.
|
|
46
|
+
transport (Literal["streamable-http", "http", "sse"]): The transport protocol.
|
|
47
|
+
data_zip (DataZip | None): Optional data zip file.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Job: The created job.
|
|
51
|
+
|
|
52
|
+
Raises:
|
|
53
|
+
HTTPException: If the request fails.
|
|
54
|
+
"""
|
|
55
|
+
url = f"{self._client.base_url}/jobs/code/{code_id}/run/mcp"
|
|
56
|
+
|
|
57
|
+
files = self._client._prepare_files(data_zip=data_zip)
|
|
58
|
+
data: dict[str, Any] = {
|
|
59
|
+
"mcp_module": mcp_module,
|
|
60
|
+
"transport": transport,
|
|
61
|
+
}
|
|
62
|
+
if env_vars:
|
|
63
|
+
data["env_vars"] = EnvVars(env_vars).json_str
|
|
64
|
+
|
|
65
|
+
async with httpx.AsyncClient() as client:
|
|
66
|
+
try:
|
|
67
|
+
response = await client.post(
|
|
68
|
+
url,
|
|
69
|
+
headers=self._client.api_key_header,
|
|
70
|
+
files=files if files else None,
|
|
71
|
+
data=data,
|
|
72
|
+
)
|
|
73
|
+
response.raise_for_status()
|
|
74
|
+
return Job(**response.json())
|
|
75
|
+
except httpx.HTTPStatusError as e:
|
|
76
|
+
raise self._client._get_http_exception(httpx_error=e)
|
|
77
|
+
|
|
78
|
+
async def list_info(self) -> list[CodeInfo]:
|
|
79
|
+
"""List all stored MCP code.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
list[CodeInfo]: List of MCP code info.
|
|
83
|
+
"""
|
|
84
|
+
return await self._client.code.list_info(code_type=CodeType.MCP)
|
|
85
|
+
|
|
86
|
+
async def delete(self, code_id: str) -> str:
|
|
87
|
+
"""Delete stored MCP code.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
code_id (str): The code ID to delete.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
str: Deletion confirmation message.
|
|
94
|
+
|
|
95
|
+
Raises:
|
|
96
|
+
ValueError: If the code_id is not MCP code.
|
|
97
|
+
"""
|
|
98
|
+
# Verify this is actually MCP code
|
|
99
|
+
code_info = await self._client.code.get_info(code_id)
|
|
100
|
+
if code_info.code_type != CodeType.MCP:
|
|
101
|
+
raise ValueError(
|
|
102
|
+
f"Code '{code_id}' is {code_info.code_type}, not MCP code. "
|
|
103
|
+
"Cannot delete non-MCP code from MCP client."
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
return await self._client.code.delete(code_id)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class AsyncMcpConfigClient:
|
|
110
|
+
def __init__(self, client: AsyncClient):
|
|
111
|
+
self._client = client
|
|
112
|
+
|
|
113
|
+
async def run(
|
|
114
|
+
self,
|
|
115
|
+
mcp_config: MCPConfig | Path | str | dict,
|
|
116
|
+
transport: Literal["streamable-http", "http", "sse"] = "streamable-http",
|
|
117
|
+
) -> Job:
|
|
118
|
+
"""Runs a MCP server from MCP configuration.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
mcp_config (MCPConfig | Path | str | dict): The MCP configuration. Can be:
|
|
122
|
+
- FastMCP MCPConfig object
|
|
123
|
+
- Path to a .json file containing MCP configuration
|
|
124
|
+
- JSON string containing MCP configuration
|
|
125
|
+
- Dictionary containing MCP configuration
|
|
126
|
+
transport (Literal["streamable-http", "http", "sse"]): The transport protocol.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
Job: The created job.
|
|
130
|
+
|
|
131
|
+
Raises:
|
|
132
|
+
HTTPException: If the request fails.
|
|
133
|
+
"""
|
|
134
|
+
url = f"{self._client.base_url}/jobs/config/run/mcp"
|
|
135
|
+
|
|
136
|
+
if isinstance(mcp_config, (Path, str)):
|
|
137
|
+
if str(mcp_config).endswith(".json"):
|
|
138
|
+
config_path = Path(mcp_config)
|
|
139
|
+
with open(config_path, "r") as f:
|
|
140
|
+
config_data = json.load(f)
|
|
141
|
+
else:
|
|
142
|
+
config_data = json.loads(str(mcp_config))
|
|
143
|
+
elif isinstance(mcp_config, dict):
|
|
144
|
+
config_data = mcp_config
|
|
145
|
+
else:
|
|
146
|
+
config_data = mcp_config.model_dump()
|
|
147
|
+
|
|
148
|
+
data = {
|
|
149
|
+
"transport": transport,
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async with httpx.AsyncClient() as client:
|
|
153
|
+
try:
|
|
154
|
+
response = await client.post(
|
|
155
|
+
url,
|
|
156
|
+
headers=self._client.api_key_header,
|
|
157
|
+
json=config_data,
|
|
158
|
+
params=data,
|
|
159
|
+
)
|
|
160
|
+
response.raise_for_status()
|
|
161
|
+
return Job(**response.json())
|
|
162
|
+
except httpx.HTTPStatusError as e:
|
|
163
|
+
raise self._client._get_http_exception(httpx_error=e)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class AsyncMcpJobsClient:
|
|
167
|
+
def __init__(self, client):
|
|
168
|
+
self._client = client
|
|
169
|
+
|
|
170
|
+
async def list(
|
|
171
|
+
self,
|
|
172
|
+
job_status: JobStatus | None = None,
|
|
173
|
+
job_stage: JobStage | None = None,
|
|
174
|
+
) -> List[Job]:
|
|
175
|
+
"""List MCP jobs.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
job_status (JobStatus | None): Filter by job status.
|
|
179
|
+
job_stage (JobStage | None): Filter by job stage.
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
list[Job]: List of MCP jobs.
|
|
183
|
+
"""
|
|
184
|
+
return await self._client.jobs.list(
|
|
185
|
+
job_type=JobType.RUN_MCP,
|
|
186
|
+
job_status=job_status,
|
|
187
|
+
job_stage=job_stage,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
async def get_latest(self) -> Job | None:
|
|
191
|
+
"""Get the most recent MCP job.
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
Job | None: The most recent MCP job, or None if no jobs exist.
|
|
195
|
+
"""
|
|
196
|
+
jobs = await self.list()
|
|
197
|
+
return jobs[0] if jobs else None
|
|
198
|
+
|
|
199
|
+
async def list_queued(self) -> List[Job]:
|
|
200
|
+
"""Get all queued MCP jobs.
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
List[Job]: List of queued MCP jobs.
|
|
204
|
+
"""
|
|
205
|
+
return await self.list(job_status=JobStatus.QUEUED)
|
|
206
|
+
|
|
207
|
+
async def list_scheduled(self) -> List[Job]:
|
|
208
|
+
"""Get all scheduled MCP jobs.
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
List[Job]: List of scheduled MCP jobs.
|
|
212
|
+
"""
|
|
213
|
+
return await self.list(job_status=JobStatus.SCHEDULED)
|
|
214
|
+
|
|
215
|
+
async def list_started(self) -> List[Job]:
|
|
216
|
+
"""Get all started MCP jobs.
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
list[Job]: List of started MCP jobs.
|
|
220
|
+
"""
|
|
221
|
+
return await self.list(job_status=JobStatus.STARTED)
|
|
222
|
+
|
|
223
|
+
async def list_deferred(self) -> List[Job]:
|
|
224
|
+
"""Get all deferred MCP jobs.
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
List[Job]: List of deferred MCP jobs.
|
|
228
|
+
"""
|
|
229
|
+
return await self.list(job_status=JobStatus.DEFERRED)
|
|
230
|
+
|
|
231
|
+
async def list_canceled(self) -> List[Job]:
|
|
232
|
+
"""Get all canceled MCP jobs.
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
List[Job]: List of canceled MCP jobs.
|
|
236
|
+
"""
|
|
237
|
+
return await self.list(job_status=JobStatus.CANCELED)
|
|
238
|
+
|
|
239
|
+
async def list_stopped(self) -> List[Job]:
|
|
240
|
+
"""Get all stopped MCP jobs.
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
List[Job]: List of stopped MCP jobs.
|
|
244
|
+
"""
|
|
245
|
+
return await self.list(job_status=JobStatus.STOPPED)
|
|
246
|
+
|
|
247
|
+
async def list_failed(self) -> List[Job]:
|
|
248
|
+
"""Get all failed MCP jobs.
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
list[Job]: List of failed MCP jobs.
|
|
252
|
+
"""
|
|
253
|
+
return await self.list(job_status=JobStatus.FAILED)
|
|
254
|
+
|
|
255
|
+
async def list_finished(self) -> List[Job]:
|
|
256
|
+
"""Get all finished MCP jobs.
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
list[Job]: List of finished MCP jobs.
|
|
260
|
+
"""
|
|
261
|
+
return await self.list(job_status=JobStatus.FINISHED)
|
|
262
|
+
|
|
263
|
+
async def list_timed_out(self) -> List[Job]:
|
|
264
|
+
"""Get all timed out MCP jobs.
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
List[Job]: List of timed out MCP jobs.
|
|
268
|
+
"""
|
|
269
|
+
return await self.list(job_status=JobStatus.TIMEOUT)
|
|
270
|
+
|
|
271
|
+
async def list_pre_running(self) -> List[Job]:
|
|
272
|
+
"""Get all pre-running MCP jobs.
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
List[Job]: List of pre-running MCP jobs.
|
|
276
|
+
"""
|
|
277
|
+
return await self.list(job_stage=JobStage.PRE_RUNNING)
|
|
278
|
+
|
|
279
|
+
async def list_running(self) -> List[Job]:
|
|
280
|
+
"""Get all running MCP jobs.
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
List[Job]: List of running MCP jobs.
|
|
284
|
+
"""
|
|
285
|
+
return await self.list(job_stage=JobStage.RUNNING)
|
|
286
|
+
|
|
287
|
+
async def list_post_running(self) -> List[Job]:
|
|
288
|
+
"""Get all post-running MCP jobs.
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
List[Job]: List of post-running MCP jobs.
|
|
292
|
+
"""
|
|
293
|
+
return await self.list(job_stage=JobStage.POST_RUNNING)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
class AsyncMcpClient:
|
|
297
|
+
def __init__(self, client: AsyncClient):
|
|
298
|
+
self._client = client
|
|
299
|
+
self.stored = AsyncStoredMcpClient(client)
|
|
300
|
+
self.config = AsyncMcpConfigClient(client)
|
|
301
|
+
self.jobs = AsyncMcpJobsClient(client)
|
|
302
|
+
|
|
303
|
+
async def run(
|
|
304
|
+
self,
|
|
305
|
+
code_zip: CodeZip,
|
|
306
|
+
mcp_module: str,
|
|
307
|
+
env_vars: EnvVars | dict | None = None,
|
|
308
|
+
transport: Literal["streamable-http", "http", "sse"] = "streamable-http",
|
|
309
|
+
data_zip: DataZip | None = None,
|
|
310
|
+
) -> Job:
|
|
311
|
+
"""Runs a MCP server from code zip.
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
code_zip (CodeZip): The code zip.
|
|
315
|
+
mcp_module (str): The name of the MCP server module.
|
|
316
|
+
env_vars (EnvVars | dict | None): Optional environment variables.
|
|
317
|
+
transport (Literal["streamable-http", "http", "sse"]): The transport protocol.
|
|
318
|
+
data_zip (DataZip | None): Optional data zip file.
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
Job: The created job.
|
|
322
|
+
|
|
323
|
+
Raises:
|
|
324
|
+
HTTPException: If the request fails.
|
|
325
|
+
"""
|
|
326
|
+
url = f"{self._client.base_url}/jobs/code/run/mcp"
|
|
327
|
+
|
|
328
|
+
files = self._client._prepare_files(code_zip=code_zip, data_zip=data_zip)
|
|
329
|
+
data: dict[str, Any] = {
|
|
330
|
+
"mcp_module": mcp_module,
|
|
331
|
+
"transport": transport,
|
|
332
|
+
}
|
|
333
|
+
if env_vars:
|
|
334
|
+
data["env_vars"] = EnvVars(env_vars).json_str
|
|
335
|
+
|
|
336
|
+
async with httpx.AsyncClient() as client:
|
|
337
|
+
try:
|
|
338
|
+
response = await client.post(
|
|
339
|
+
url,
|
|
340
|
+
headers=self._client.api_key_header,
|
|
341
|
+
files=files,
|
|
342
|
+
data=data,
|
|
343
|
+
)
|
|
344
|
+
response.raise_for_status()
|
|
345
|
+
return Job(**response.json())
|
|
346
|
+
except httpx.HTTPStatusError as e:
|
|
347
|
+
raise self._client._get_http_exception(httpx_error=e)
|
|
348
|
+
|
|
349
|
+
async def is_healthy(self, job_id: str) -> bool:
|
|
350
|
+
"""Checks whether launched MCP server is healthy.
|
|
351
|
+
|
|
352
|
+
Args:
|
|
353
|
+
job_id (str): The ID of the MCP launch job.
|
|
354
|
+
|
|
355
|
+
Returns:
|
|
356
|
+
bool: True if MCP is healthy else False.
|
|
357
|
+
|
|
358
|
+
Raises:
|
|
359
|
+
HTTPException: If the request fails.
|
|
360
|
+
"""
|
|
361
|
+
return await self._client.jobs.is_healthy(job_id=job_id)
|
|
362
|
+
|
|
363
|
+
async def await_healthy(self, job_id: str, timeout: float | None = None) -> Job:
|
|
364
|
+
"""Waits for a MCP server to become healthy.
|
|
365
|
+
|
|
366
|
+
Args:
|
|
367
|
+
job_id (str): The ID of the MCP run job.
|
|
368
|
+
timeout (float | None): Maximum time to wait in seconds. If None, waits indefinitely.
|
|
369
|
+
|
|
370
|
+
Returns:
|
|
371
|
+
Job: The job object of the MCP run job.
|
|
372
|
+
|
|
373
|
+
Raises:
|
|
374
|
+
HTTPException: If the request fails.
|
|
375
|
+
APIException: If the job enters stage POST_RUNNING unexpectedly.
|
|
376
|
+
TimeoutError: If the timeout is exceeded.
|
|
377
|
+
"""
|
|
378
|
+
return await self._client.jobs.await_healthy(job_id=job_id, timeout=timeout)
|
|
379
|
+
|
|
380
|
+
def get_url(
|
|
381
|
+
self, job_id: str, transport: Literal["streamable-http", "http", "sse"] = "http"
|
|
382
|
+
) -> str:
|
|
383
|
+
"""Gets the URL for a MCP server.
|
|
384
|
+
|
|
385
|
+
Args:
|
|
386
|
+
job_id (str): The job ID.
|
|
387
|
+
transport (Literal["streamable-http", "http", "sse"]): The transport protocol.
|
|
388
|
+
|
|
389
|
+
Returns:
|
|
390
|
+
str: The MCP URL.
|
|
391
|
+
"""
|
|
392
|
+
mcp_path = "sse" if transport == "sse" else "mcp"
|
|
393
|
+
return f"{self._client.get_proxy_url(job_id)}/{mcp_path}"
|
|
394
|
+
|
|
395
|
+
async def upload(
|
|
396
|
+
self,
|
|
397
|
+
code_zip: CodeZip,
|
|
398
|
+
code_name: str,
|
|
399
|
+
metadata: JsonData | dict | None = None,
|
|
400
|
+
) -> str:
|
|
401
|
+
"""Upload MCP code.
|
|
402
|
+
|
|
403
|
+
Args:
|
|
404
|
+
code_zip (CodeZip): The code zip.
|
|
405
|
+
code_name (str): The name of the code.
|
|
406
|
+
metadata (JsonData | dict | None): The JSON metadata of the code.
|
|
407
|
+
|
|
408
|
+
Returns:
|
|
409
|
+
str: The code ID.
|
|
410
|
+
"""
|
|
411
|
+
return await self._client.code.upload(
|
|
412
|
+
code_zip=code_zip,
|
|
413
|
+
code_name=code_name,
|
|
414
|
+
code_type=CodeType.MCP,
|
|
415
|
+
metadata=metadata,
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
async def ingest(
|
|
419
|
+
self,
|
|
420
|
+
code_zip: CodeZip,
|
|
421
|
+
code_name: str,
|
|
422
|
+
metadata: JsonData | dict | None = None,
|
|
423
|
+
build_pexenv: bool = False,
|
|
424
|
+
pexenv_python: str | None = None,
|
|
425
|
+
) -> Job:
|
|
426
|
+
"""Ingest MCP code.
|
|
427
|
+
|
|
428
|
+
Args:
|
|
429
|
+
code_zip (CodeZip): The code zip.
|
|
430
|
+
code_name (str): The name of the code.
|
|
431
|
+
metadata (JsonData | dict | None): The JSON metadata of the code.
|
|
432
|
+
build_pexenv (bool): Whether to build the pex venv.
|
|
433
|
+
pexenv_python: (str | None): Python interpreter for the pex venv.
|
|
434
|
+
|
|
435
|
+
Returns:
|
|
436
|
+
Job: The code ingestion job.
|
|
437
|
+
"""
|
|
438
|
+
return await self._client.code.ingest(
|
|
439
|
+
code_zip=code_zip,
|
|
440
|
+
code_name=code_name,
|
|
441
|
+
code_type=CodeType.MCP,
|
|
442
|
+
metadata=metadata,
|
|
443
|
+
build_pexenv=build_pexenv,
|
|
444
|
+
pexenv_python=pexenv_python,
|
|
445
|
+
)
|