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.
@@ -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 as _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 Exception as exc:
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
- try:
84
- err_data = resp.json()
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, job_id: str, container: Optional[str] = None, tail: Optional[int] = None
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
- """Get job logs."""
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 tail:
364
- params["tail"] = tail
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 _json.dumps(value, ensure_ascii=False, default=str)
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")