ap-client 0.1.4.dev0__tar.gz → 0.1.5.post0__tar.gz
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.
- ap_client-0.1.5.post0/PKG-INFO +12 -0
- ap_client-0.1.5.post0/ap_client/__init__.py +21 -0
- {ap_client-0.1.4.dev0 → ap_client-0.1.5.post0}/ap_client/api.py +111 -17
- {ap_client-0.1.4.dev0 → ap_client-0.1.5.post0}/ap_client/cli.py +313 -48
- {ap_client-0.1.4.dev0 → ap_client-0.1.5.post0}/ap_client/config.py +36 -6
- {ap_client-0.1.4.dev0 → ap_client-0.1.5.post0}/ap_client/exporter.py +101 -21
- {ap_client-0.1.4.dev0 → ap_client-0.1.5.post0}/ap_client/waiter.py +64 -1
- {ap_client-0.1.4.dev0 → ap_client-0.1.5.post0}/pyproject.toml +7 -1
- ap_client-0.1.4.dev0/.gitignore +0 -235
- ap_client-0.1.4.dev0/PKG-INFO +0 -9
- ap_client-0.1.4.dev0/ap_client/__init__.py +0 -13
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ap-client
|
|
3
|
+
Version: 0.1.5.post0
|
|
4
|
+
Summary: Agent Platform API Client & CLI
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Requires-Dist: pyyaml>=6.0
|
|
7
|
+
Requires-Dist: requests>=2.28.0
|
|
8
|
+
Requires-Dist: rich>=13.0.0
|
|
9
|
+
Requires-Dist: typer>=0.9.0
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
A lightweight Python SDK and command line interface for Agent Platform. It provides helpers for configuring API access and managing templates, datasets, jobs, and groups.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Agent Platform API Client"""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
4
|
+
|
|
5
|
+
from .api import APIClient, APIError, get_client
|
|
6
|
+
from .config import Config, ConfigurationError, get_config
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
__version__ = version("ap-client")
|
|
10
|
+
except PackageNotFoundError:
|
|
11
|
+
__version__ = "0+unknown"
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"APIClient",
|
|
15
|
+
"APIError",
|
|
16
|
+
"Config",
|
|
17
|
+
"ConfigurationError",
|
|
18
|
+
"__version__",
|
|
19
|
+
"get_client",
|
|
20
|
+
"get_config",
|
|
21
|
+
]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""API client."""
|
|
2
2
|
|
|
3
|
-
import json
|
|
3
|
+
import json
|
|
4
4
|
import platform
|
|
5
5
|
import sys
|
|
6
6
|
import time
|
|
@@ -10,7 +10,7 @@ from urllib.parse import quote
|
|
|
10
10
|
|
|
11
11
|
import requests
|
|
12
12
|
|
|
13
|
-
from .config import Config, get_config
|
|
13
|
+
from .config import Config, ConfigurationError, get_config, has_cluster_header
|
|
14
14
|
|
|
15
15
|
_GROUP_ARTIFACTS_PAGE_SIZE = 500
|
|
16
16
|
_VERBOSE_SAMPLE_KEYS = 5
|
|
@@ -18,6 +18,28 @@ _VERBOSE_SAMPLE_ITEMS = 3
|
|
|
18
18
|
_VERBOSE_VALUE_PREVIEW = 120
|
|
19
19
|
|
|
20
20
|
|
|
21
|
+
class APIError(Exception):
|
|
22
|
+
"""Raised when an API request fails."""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
status_code: Optional[int],
|
|
27
|
+
detail: Any,
|
|
28
|
+
request_id: str,
|
|
29
|
+
response: Optional[requests.Response],
|
|
30
|
+
):
|
|
31
|
+
self.status_code = status_code
|
|
32
|
+
self.detail = detail
|
|
33
|
+
self.request_id = request_id
|
|
34
|
+
self.response = response
|
|
35
|
+
super().__init__(self._build_message())
|
|
36
|
+
|
|
37
|
+
def _build_message(self) -> str:
|
|
38
|
+
if self.status_code is None:
|
|
39
|
+
return f"API Network Error: {self.detail} (request_id={self.request_id})"
|
|
40
|
+
return f"API Error {self.status_code}: {self.detail} (request_id={self.request_id})"
|
|
41
|
+
|
|
42
|
+
|
|
21
43
|
class APIClient:
|
|
22
44
|
"""Agent Platform API client."""
|
|
23
45
|
|
|
@@ -27,6 +49,11 @@ class APIClient:
|
|
|
27
49
|
headers = dict(config.headers)
|
|
28
50
|
user_agent = _build_user_agent(headers.get("User-Agent"))
|
|
29
51
|
headers["User-Agent"] = user_agent
|
|
52
|
+
|
|
53
|
+
# 认证 header: AP_TOKEN_KEY -> X-API-Key
|
|
54
|
+
if config.token_key:
|
|
55
|
+
headers["X-API-Key"] = config.token_key
|
|
56
|
+
|
|
30
57
|
self.session.headers.update(
|
|
31
58
|
{
|
|
32
59
|
"Content-Type": "application/json",
|
|
@@ -55,6 +82,7 @@ class APIClient:
|
|
|
55
82
|
timeout: float = 30,
|
|
56
83
|
) -> Any:
|
|
57
84
|
"""Send an HTTP request with X-Request-ID and optional verbose logging."""
|
|
85
|
+
self._require_cluster_config()
|
|
58
86
|
url = self._url(path)
|
|
59
87
|
request_id = str(uuid.uuid4())
|
|
60
88
|
headers = {"X-Request-ID": request_id}
|
|
@@ -71,27 +99,29 @@ class APIClient:
|
|
|
71
99
|
headers=headers,
|
|
72
100
|
timeout=timeout,
|
|
73
101
|
)
|
|
74
|
-
except
|
|
102
|
+
except requests.RequestException as exc:
|
|
75
103
|
if verbose:
|
|
76
104
|
duration_ms = (time.perf_counter() - start) * 1000
|
|
77
105
|
self._log_exception(method, url, request_id, duration_ms, exc)
|
|
78
|
-
raise
|
|
106
|
+
raise APIError(None, str(exc), request_id, None) from exc
|
|
79
107
|
duration_ms = (time.perf_counter() - start) * 1000
|
|
80
108
|
if verbose:
|
|
81
109
|
self._log_response(method, url, request_id, resp, duration_ms)
|
|
82
110
|
if not resp.ok:
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
detail = err_data.get("detail", resp.text)
|
|
86
|
-
except Exception:
|
|
87
|
-
detail = resp.text
|
|
88
|
-
raise Exception(
|
|
89
|
-
f"API Error {resp.status_code}: {detail} (request_id={request_id})"
|
|
90
|
-
)
|
|
111
|
+
detail = _response_error_detail(resp)
|
|
112
|
+
raise APIError(resp.status_code, detail, request_id, resp)
|
|
91
113
|
if not resp.content:
|
|
92
114
|
return None
|
|
93
115
|
return resp.json()
|
|
94
116
|
|
|
117
|
+
def _require_cluster_config(self) -> None:
|
|
118
|
+
if has_cluster_header(self.config.headers):
|
|
119
|
+
return
|
|
120
|
+
raise ConfigurationError(
|
|
121
|
+
"Missing cluster configuration. Set AP_CLUSTER (e.g. export AP_CLUSTER=test) "
|
|
122
|
+
"or include a non-empty X-Cluster in AP_HEADERS (e.g. export AP_HEADERS='X-Cluster: test')."
|
|
123
|
+
)
|
|
124
|
+
|
|
95
125
|
def _get(self, path: str, params: Optional[dict] = None) -> Any:
|
|
96
126
|
"""Send a GET request."""
|
|
97
127
|
return self._request("GET", path, params=params)
|
|
@@ -354,16 +384,62 @@ class APIClient:
|
|
|
354
384
|
return self._post(f"/groups/{quote(group_id)}/cancel", {})
|
|
355
385
|
|
|
356
386
|
def get_job_logs(
|
|
357
|
-
self,
|
|
387
|
+
self,
|
|
388
|
+
job_id: str,
|
|
389
|
+
container: Optional[str] = None,
|
|
390
|
+
offset: Optional[int] = 0,
|
|
391
|
+
limit: Optional[int] = 1000,
|
|
358
392
|
) -> dict:
|
|
359
|
-
"""
|
|
393
|
+
"""获取任务日志
|
|
394
|
+
|
|
395
|
+
Args:
|
|
396
|
+
job_id: 任务ID
|
|
397
|
+
container: 容器名称,如果为None则返回所有容器日志
|
|
398
|
+
offset: 分页偏移量,用于获取后续日志页
|
|
399
|
+
limit: 分页大小,默认为1000
|
|
400
|
+
"""
|
|
360
401
|
params = {}
|
|
361
402
|
if container:
|
|
362
403
|
params["container"] = container
|
|
363
|
-
if
|
|
364
|
-
params["
|
|
404
|
+
if offset is not None:
|
|
405
|
+
params["offset"] = offset
|
|
406
|
+
if limit is not None:
|
|
407
|
+
params["limit"] = limit
|
|
365
408
|
return self._get(f"/jobs/{quote(job_id)}/logs", params=params or None)
|
|
366
409
|
|
|
410
|
+
def get_job_logs_complete(self, job_id: str, container: str) -> dict:
|
|
411
|
+
"""获取完整日志(自动处理分页)
|
|
412
|
+
|
|
413
|
+
一直调用接口直到返回的next_offset为空,标识日志获取完成
|
|
414
|
+
|
|
415
|
+
Args:
|
|
416
|
+
job_id: 任务ID
|
|
417
|
+
container: 容器名称
|
|
418
|
+
|
|
419
|
+
Returns:
|
|
420
|
+
dict: 所有日志
|
|
421
|
+
"""
|
|
422
|
+
all_logs = []
|
|
423
|
+
offset = 0
|
|
424
|
+
|
|
425
|
+
while True:
|
|
426
|
+
result = self.get_job_logs(
|
|
427
|
+
job_id=job_id, container=container, offset=offset, limit=1000
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
# 提取日志数据
|
|
431
|
+
if "logs" in result and isinstance(result["logs"], list):
|
|
432
|
+
all_logs.extend(result["logs"])
|
|
433
|
+
|
|
434
|
+
# 检查是否还有更多日志
|
|
435
|
+
next_offset = result.get("next_offset")
|
|
436
|
+
if next_offset is None:
|
|
437
|
+
break
|
|
438
|
+
|
|
439
|
+
offset = next_offset
|
|
440
|
+
|
|
441
|
+
return {"logs": all_logs, "container": container, "job_id": job_id}
|
|
442
|
+
|
|
367
443
|
def get_job_events(self, job_id: str, container: Optional[str] = None) -> dict:
|
|
368
444
|
"""Get job events."""
|
|
369
445
|
params = {}
|
|
@@ -371,6 +447,14 @@ class APIClient:
|
|
|
371
447
|
params["container"] = container
|
|
372
448
|
return self._get(f"/jobs/{quote(job_id)}/events", params=params or None)
|
|
373
449
|
|
|
450
|
+
def get_job_containers(self, job_id: str) -> dict:
|
|
451
|
+
"""Get all container names for a job.
|
|
452
|
+
|
|
453
|
+
Args:
|
|
454
|
+
job_id: The job ID to get containers for
|
|
455
|
+
"""
|
|
456
|
+
return self._get(f"/jobs/{quote(job_id)}/containers")
|
|
457
|
+
|
|
374
458
|
def get_job_metrics(self, job_id: str) -> dict:
|
|
375
459
|
"""Get job metrics."""
|
|
376
460
|
return self._get(f"/metrics/{quote(job_id)}")
|
|
@@ -471,11 +555,21 @@ def _write_stderr(lines: list[str]) -> None:
|
|
|
471
555
|
|
|
472
556
|
def _safe_json(value: Any) -> str:
|
|
473
557
|
try:
|
|
474
|
-
return
|
|
558
|
+
return json.dumps(value, ensure_ascii=False, default=str)
|
|
475
559
|
except Exception:
|
|
476
560
|
return str(value)
|
|
477
561
|
|
|
478
562
|
|
|
563
|
+
def _response_error_detail(resp: requests.Response) -> Any:
|
|
564
|
+
try:
|
|
565
|
+
err_data = resp.json()
|
|
566
|
+
except Exception:
|
|
567
|
+
return resp.text
|
|
568
|
+
if isinstance(err_data, dict):
|
|
569
|
+
return err_data.get("detail", resp.text)
|
|
570
|
+
return err_data
|
|
571
|
+
|
|
572
|
+
|
|
479
573
|
def _safe_decode(raw: bytes) -> str:
|
|
480
574
|
try:
|
|
481
575
|
return raw.decode("utf-8", errors="replace")
|