ikc-open-platform-sdk 0.1.0__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.
- ikc_open_platform_sdk-0.1.0/.gitignore +29 -0
- ikc_open_platform_sdk-0.1.0/PKG-INFO +49 -0
- ikc_open_platform_sdk-0.1.0/README.md +38 -0
- ikc_open_platform_sdk-0.1.0/ikc_open_platform_sdk/__init__.py +23 -0
- ikc_open_platform_sdk-0.1.0/ikc_open_platform_sdk/_version.py +3 -0
- ikc_open_platform_sdk-0.1.0/ikc_open_platform_sdk/client.py +289 -0
- ikc_open_platform_sdk-0.1.0/ikc_open_platform_sdk/errors.py +41 -0
- ikc_open_platform_sdk-0.1.0/pyproject.toml +19 -0
- ikc_open_platform_sdk-0.1.0/tests/conftest.py +8 -0
- ikc_open_platform_sdk-0.1.0/tests/test_client.py +78 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
.venv/
|
|
5
|
+
.pytest_cache/
|
|
6
|
+
*.egg-info/
|
|
7
|
+
dist/
|
|
8
|
+
build/
|
|
9
|
+
|
|
10
|
+
# Runtime data
|
|
11
|
+
data/
|
|
12
|
+
logs/
|
|
13
|
+
*.db
|
|
14
|
+
*.sqlite3
|
|
15
|
+
uploads/
|
|
16
|
+
|
|
17
|
+
# Secrets
|
|
18
|
+
.env
|
|
19
|
+
*.pem
|
|
20
|
+
*.key
|
|
21
|
+
|
|
22
|
+
# Frontend (portal)
|
|
23
|
+
portal/node_modules/
|
|
24
|
+
portal/dist/
|
|
25
|
+
|
|
26
|
+
# Editors / OS
|
|
27
|
+
.vscode/
|
|
28
|
+
.idea/
|
|
29
|
+
.DS_Store
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: ikc-open-platform-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: ikc-open-platform 第三方开发者 SDK:API Key 认证 + 统一壳解包 + 四类业务域(知识库/文档/解析/检索),业务模型复用 ikc-sdk-lib
|
|
5
|
+
Author: SITECH-iKM
|
|
6
|
+
Requires-Python: >=3.12
|
|
7
|
+
Requires-Dist: httpx<1.0,>=0.27
|
|
8
|
+
Requires-Dist: ikc-sdk-lib==0.6.1
|
|
9
|
+
Requires-Dist: pydantic<3.0,>=2.7
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# ikc-open-platform-sdk
|
|
13
|
+
|
|
14
|
+
ikc-open-platform 第三方开发者 SDK(导入包 `ikc_open_platform_sdk`)。
|
|
15
|
+
|
|
16
|
+
- 认证:应用 API Key(`Authorization: Bearer <api-key>`,由管理面 `/admin/apps/{appId}/keys` 创建)。
|
|
17
|
+
- 协议:统一响应壳 `errCode/errMsg/data/traceId/reqId` 解包;业务模型一律复用 `ikc-sdk-lib`(本 SDK 不自定义业务模型)。
|
|
18
|
+
- 追踪:每请求注入 23 位纯数字 `X-Request-Id`;`reqId` 可显式传入,缺省 SDK 生成 `req_` 前缀值并随壳回显。
|
|
19
|
+
- 能力面(四类业务域):
|
|
20
|
+
- `client.knowledge_bases`:create / update / query / get
|
|
21
|
+
- `client.documents`:ingest / ingest_and_parse / upload / get
|
|
22
|
+
- `client.parse`:parse / parse_direct / query_result / issue_download_ticket / download
|
|
23
|
+
- `client.search`:universal_search / deep_search / query(兼容别名)
|
|
24
|
+
- 错误模型:`OpenPlatformAPIError`(errCode/errMsg/traceId/reqId)、`OpenPlatformConnectionError`、`OpenPlatformTimeoutError`、`OpenPlatformProtocolError`。
|
|
25
|
+
|
|
26
|
+
## 安装
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install ikc-open-platform-sdk # PyPI(发布后)
|
|
30
|
+
pip install sdk/python/ # 或本地源码
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## 快速开始
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from ikc_open_platform_sdk import OpenPlatformClient
|
|
37
|
+
from ikc_sdk.core.api.search.universal import SearchQueryRequest
|
|
38
|
+
|
|
39
|
+
with OpenPlatformClient("http://localhost:18000", api_key="<app-api-key>") as client:
|
|
40
|
+
result = client.search.universal_search(SearchQueryRequest(query="IKC 平台"))
|
|
41
|
+
for hit in result.hits:
|
|
42
|
+
print(hit)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## 测试
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
python -m pytest sdk/python/tests -q
|
|
49
|
+
```
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# ikc-open-platform-sdk
|
|
2
|
+
|
|
3
|
+
ikc-open-platform 第三方开发者 SDK(导入包 `ikc_open_platform_sdk`)。
|
|
4
|
+
|
|
5
|
+
- 认证:应用 API Key(`Authorization: Bearer <api-key>`,由管理面 `/admin/apps/{appId}/keys` 创建)。
|
|
6
|
+
- 协议:统一响应壳 `errCode/errMsg/data/traceId/reqId` 解包;业务模型一律复用 `ikc-sdk-lib`(本 SDK 不自定义业务模型)。
|
|
7
|
+
- 追踪:每请求注入 23 位纯数字 `X-Request-Id`;`reqId` 可显式传入,缺省 SDK 生成 `req_` 前缀值并随壳回显。
|
|
8
|
+
- 能力面(四类业务域):
|
|
9
|
+
- `client.knowledge_bases`:create / update / query / get
|
|
10
|
+
- `client.documents`:ingest / ingest_and_parse / upload / get
|
|
11
|
+
- `client.parse`:parse / parse_direct / query_result / issue_download_ticket / download
|
|
12
|
+
- `client.search`:universal_search / deep_search / query(兼容别名)
|
|
13
|
+
- 错误模型:`OpenPlatformAPIError`(errCode/errMsg/traceId/reqId)、`OpenPlatformConnectionError`、`OpenPlatformTimeoutError`、`OpenPlatformProtocolError`。
|
|
14
|
+
|
|
15
|
+
## 安装
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install ikc-open-platform-sdk # PyPI(发布后)
|
|
19
|
+
pip install sdk/python/ # 或本地源码
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## 快速开始
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from ikc_open_platform_sdk import OpenPlatformClient
|
|
26
|
+
from ikc_sdk.core.api.search.universal import SearchQueryRequest
|
|
27
|
+
|
|
28
|
+
with OpenPlatformClient("http://localhost:18000", api_key="<app-api-key>") as client:
|
|
29
|
+
result = client.search.universal_search(SearchQueryRequest(query="IKC 平台"))
|
|
30
|
+
for hit in result.hits:
|
|
31
|
+
print(hit)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## 测试
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
python -m pytest sdk/python/tests -q
|
|
38
|
+
```
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""ikc-open-platform 第三方开发者 SDK。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ._version import __version__
|
|
6
|
+
from .client import OpenPlatformClient
|
|
7
|
+
from .errors import (
|
|
8
|
+
OpenPlatformAPIError,
|
|
9
|
+
OpenPlatformConnectionError,
|
|
10
|
+
OpenPlatformError,
|
|
11
|
+
OpenPlatformProtocolError,
|
|
12
|
+
OpenPlatformTimeoutError,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"__version__",
|
|
17
|
+
"OpenPlatformClient",
|
|
18
|
+
"OpenPlatformError",
|
|
19
|
+
"OpenPlatformAPIError",
|
|
20
|
+
"OpenPlatformConnectionError",
|
|
21
|
+
"OpenPlatformProtocolError",
|
|
22
|
+
"OpenPlatformTimeoutError",
|
|
23
|
+
]
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"""ikc-open-platform 第三方开发者同步客户端。
|
|
2
|
+
|
|
3
|
+
- 认证:``Authorization: Bearer <api-key>``(应用凭证,管理面创建)。
|
|
4
|
+
- 追踪:每次请求注入 23 位纯数字 ``X-Request-Id``;``reqId`` 调用方可传入
|
|
5
|
+
(POST 由 ikc_sdk 请求模型承载,GET 走 query 参数),缺省由 SDK 生成。
|
|
6
|
+
- 协议:统一壳 ``errCode/errMsg/data/traceId/reqId`` 解包;业务模型一律复用
|
|
7
|
+
``ikc_sdk.core.api.*``(本 SDK 不自定义业务模型)。
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import secrets
|
|
13
|
+
import time
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
import httpx
|
|
18
|
+
from pydantic import BaseModel
|
|
19
|
+
|
|
20
|
+
from ikc_sdk.core.api.knowledge_base.create import KnowledgeBaseCreateResult
|
|
21
|
+
from ikc_sdk.core.api.search.universal import SearchQueryResponse
|
|
22
|
+
from ikc_sdk.core.codes import CODE_SUCCESS
|
|
23
|
+
from ikc_sdk.core.headers import TRACE_HEADER_NAMES
|
|
24
|
+
|
|
25
|
+
from .errors import (
|
|
26
|
+
OpenPlatformAPIError,
|
|
27
|
+
OpenPlatformConnectionError,
|
|
28
|
+
OpenPlatformProtocolError,
|
|
29
|
+
OpenPlatformTimeoutError,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
_TRACE_HEADER = TRACE_HEADER_NAMES[0] # X-Request-Id
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _new_trace_id() -> str:
|
|
36
|
+
"""23 位纯数字:13 位毫秒 + 10 位随机数。"""
|
|
37
|
+
return f"{int(time.time() * 1000)}{secrets.randbelow(10**10):010d}"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _new_req_id() -> str:
|
|
41
|
+
return f"req_{_new_trace_id()}"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _dump(payload: BaseModel | dict[str, Any] | None) -> dict[str, Any] | None:
|
|
45
|
+
if payload is None:
|
|
46
|
+
return None
|
|
47
|
+
if isinstance(payload, BaseModel):
|
|
48
|
+
return payload.model_dump(mode="json", exclude_none=True)
|
|
49
|
+
return payload
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class _Transport:
|
|
53
|
+
def __init__(
|
|
54
|
+
self,
|
|
55
|
+
base_url: str,
|
|
56
|
+
*,
|
|
57
|
+
api_key: str,
|
|
58
|
+
timeout: float,
|
|
59
|
+
max_retries: int,
|
|
60
|
+
extra_headers: dict[str, str] | None,
|
|
61
|
+
http_client: httpx.Client | None,
|
|
62
|
+
) -> None:
|
|
63
|
+
self._owns_client = http_client is None
|
|
64
|
+
self._client = http_client or httpx.Client(base_url=base_url.rstrip("/"), timeout=timeout)
|
|
65
|
+
self._api_key = api_key
|
|
66
|
+
self._max_retries = max_retries
|
|
67
|
+
self._extra_headers = extra_headers or {}
|
|
68
|
+
|
|
69
|
+
def close(self) -> None:
|
|
70
|
+
if self._owns_client:
|
|
71
|
+
self._client.close()
|
|
72
|
+
|
|
73
|
+
def request(
|
|
74
|
+
self,
|
|
75
|
+
method: str,
|
|
76
|
+
path: str,
|
|
77
|
+
*,
|
|
78
|
+
json_body: dict[str, Any] | None = None,
|
|
79
|
+
params: dict[str, Any] | None = None,
|
|
80
|
+
content: bytes | None = None,
|
|
81
|
+
files: dict[str, Any] | None = None,
|
|
82
|
+
req_id: str | None = None,
|
|
83
|
+
) -> dict[str, Any]:
|
|
84
|
+
headers = {
|
|
85
|
+
"Authorization": f"Bearer {self._api_key}",
|
|
86
|
+
_TRACE_HEADER: _new_trace_id(),
|
|
87
|
+
**self._extra_headers,
|
|
88
|
+
}
|
|
89
|
+
attempt = 0
|
|
90
|
+
while True:
|
|
91
|
+
try:
|
|
92
|
+
resp = self._client.request(
|
|
93
|
+
method,
|
|
94
|
+
path,
|
|
95
|
+
json=json_body,
|
|
96
|
+
params=params,
|
|
97
|
+
content=content,
|
|
98
|
+
files=files,
|
|
99
|
+
headers=headers,
|
|
100
|
+
)
|
|
101
|
+
break
|
|
102
|
+
except httpx.TimeoutException as exc:
|
|
103
|
+
attempt += 1
|
|
104
|
+
if attempt > self._max_retries:
|
|
105
|
+
raise OpenPlatformTimeoutError(str(exc)) from exc
|
|
106
|
+
except httpx.HTTPError as exc:
|
|
107
|
+
raise OpenPlatformConnectionError(str(exc)) from exc
|
|
108
|
+
try:
|
|
109
|
+
payload = resp.json()
|
|
110
|
+
except ValueError as exc:
|
|
111
|
+
raise OpenPlatformProtocolError(f"响应非 JSON(HTTP {resp.status_code})") from exc
|
|
112
|
+
if not isinstance(payload, dict) or "errCode" not in payload:
|
|
113
|
+
raise OpenPlatformProtocolError(f"响应不符合统一协议(HTTP {resp.status_code})")
|
|
114
|
+
err_code = str(payload.get("errCode"))
|
|
115
|
+
if err_code != CODE_SUCCESS:
|
|
116
|
+
raise OpenPlatformAPIError(
|
|
117
|
+
err_code,
|
|
118
|
+
str(payload.get("errMsg") or ""),
|
|
119
|
+
trace_id=payload.get("traceId"),
|
|
120
|
+
req_id=payload.get("reqId"),
|
|
121
|
+
data=payload.get("data"),
|
|
122
|
+
)
|
|
123
|
+
return payload
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class KnowledgeBaseResource:
|
|
127
|
+
"""知识库域(/api/v1/knowledge-bases/*)。"""
|
|
128
|
+
|
|
129
|
+
def __init__(self, client: OpenPlatformClient) -> None:
|
|
130
|
+
self._client = client
|
|
131
|
+
|
|
132
|
+
def create(self, payload: BaseModel | dict[str, Any]) -> KnowledgeBaseCreateResult:
|
|
133
|
+
data = self._client._post("/api/v1/knowledge-bases/create", payload)
|
|
134
|
+
return KnowledgeBaseCreateResult.model_validate(data)
|
|
135
|
+
|
|
136
|
+
def update(self, payload: BaseModel | dict[str, Any]) -> dict[str, Any]:
|
|
137
|
+
return self._client._post("/api/v1/knowledge-bases/update", payload)
|
|
138
|
+
|
|
139
|
+
def query(self, payload: BaseModel | dict[str, Any]) -> dict[str, Any]:
|
|
140
|
+
return self._client._post("/api/v1/knowledge-bases/query", payload)
|
|
141
|
+
|
|
142
|
+
def get(self, kb_id: str) -> dict[str, Any]:
|
|
143
|
+
return self._client._get(f"/api/v1/knowledge-bases/{kb_id}")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class DocumentResource:
|
|
147
|
+
"""文档域(/api/v1/knowledge-documents/*)。"""
|
|
148
|
+
|
|
149
|
+
def __init__(self, client: OpenPlatformClient) -> None:
|
|
150
|
+
self._client = client
|
|
151
|
+
|
|
152
|
+
def ingest(self, payload: BaseModel | dict[str, Any]) -> dict[str, Any]:
|
|
153
|
+
return self._client._post("/api/v1/knowledge-documents/ingest", payload)
|
|
154
|
+
|
|
155
|
+
def ingest_and_parse(self, payload: BaseModel | dict[str, Any]) -> dict[str, Any]:
|
|
156
|
+
return self._client._post("/api/v1/knowledge-documents/ingest-and-parse", payload)
|
|
157
|
+
|
|
158
|
+
def upload(self, file_path: str | Path, *, req_id: str | None = None) -> dict[str, Any]:
|
|
159
|
+
path = Path(file_path)
|
|
160
|
+
with path.open("rb") as fh:
|
|
161
|
+
files = {"file": (path.name, fh)}
|
|
162
|
+
return self._client._request_data(
|
|
163
|
+
"POST", "/api/v1/knowledge-documents/upload", files=files, req_id=req_id
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
def get(self, doc_id: str) -> dict[str, Any]:
|
|
167
|
+
return self._client._get(f"/api/v1/knowledge-documents/{doc_id}")
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class ParseResource:
|
|
171
|
+
"""解析域(/api/v1/knowledge-documents/parse*)。"""
|
|
172
|
+
|
|
173
|
+
def __init__(self, client: OpenPlatformClient) -> None:
|
|
174
|
+
self._client = client
|
|
175
|
+
|
|
176
|
+
def parse(self, payload: BaseModel | dict[str, Any]) -> dict[str, Any]:
|
|
177
|
+
return self._client._post("/api/v1/knowledge-documents/parse", payload)
|
|
178
|
+
|
|
179
|
+
def parse_direct(self, payload: BaseModel | dict[str, Any]) -> dict[str, Any]:
|
|
180
|
+
return self._client._post("/api/v1/knowledge-documents/parse-direct", payload)
|
|
181
|
+
|
|
182
|
+
def query_result(self, task_id: str, *, req_id: str | None = None) -> dict[str, Any]:
|
|
183
|
+
return self._client._get(
|
|
184
|
+
"/api/v1/knowledge-documents/parse-result/query",
|
|
185
|
+
params={"taskId": task_id},
|
|
186
|
+
req_id=req_id,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
def issue_download_ticket(self) -> dict[str, Any]:
|
|
190
|
+
return self._client._get("/api/v1/knowledge-documents/parse-result/issue-download-ticket")
|
|
191
|
+
|
|
192
|
+
def download(self, task_id: str, *, req_id: str | None = None) -> dict[str, Any]:
|
|
193
|
+
return self._client._get(
|
|
194
|
+
"/api/v1/knowledge-documents/parse-result/download",
|
|
195
|
+
params={"taskId": task_id},
|
|
196
|
+
req_id=req_id,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class SearchResource:
|
|
201
|
+
"""检索域(/api/v1/knowledge-search/*)。"""
|
|
202
|
+
|
|
203
|
+
def __init__(self, client: OpenPlatformClient) -> None:
|
|
204
|
+
self._client = client
|
|
205
|
+
|
|
206
|
+
def universal_search(self, payload: BaseModel | dict[str, Any]) -> SearchQueryResponse:
|
|
207
|
+
data = self._client._post("/api/v1/knowledge-search/universal-search", payload)
|
|
208
|
+
return SearchQueryResponse.model_validate(data)
|
|
209
|
+
|
|
210
|
+
def deep_search(self, payload: BaseModel | dict[str, Any]) -> dict[str, Any]:
|
|
211
|
+
return self._client._post("/api/v1/knowledge-search/deep-search", payload)
|
|
212
|
+
|
|
213
|
+
def query(self, payload: BaseModel | dict[str, Any]) -> dict[str, Any]:
|
|
214
|
+
"""兼容别名(/api/v1/knowledge-search/query)。"""
|
|
215
|
+
return self._client._post("/api/v1/knowledge-search/query", payload)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class OpenPlatformClient:
|
|
219
|
+
"""ikc-open-platform 同步客户端(第三方开发者入口)。"""
|
|
220
|
+
|
|
221
|
+
def __init__(
|
|
222
|
+
self,
|
|
223
|
+
base_url: str,
|
|
224
|
+
*,
|
|
225
|
+
api_key: str,
|
|
226
|
+
timeout: float = 30.0,
|
|
227
|
+
max_retries: int = 2,
|
|
228
|
+
extra_headers: dict[str, str] | None = None,
|
|
229
|
+
http_client: httpx.Client | None = None,
|
|
230
|
+
) -> None:
|
|
231
|
+
self._transport = _Transport(
|
|
232
|
+
base_url,
|
|
233
|
+
api_key=api_key,
|
|
234
|
+
timeout=timeout,
|
|
235
|
+
max_retries=max_retries,
|
|
236
|
+
extra_headers=extra_headers,
|
|
237
|
+
http_client=http_client,
|
|
238
|
+
)
|
|
239
|
+
self.knowledge_bases = KnowledgeBaseResource(self)
|
|
240
|
+
self.documents = DocumentResource(self)
|
|
241
|
+
self.parse = ParseResource(self)
|
|
242
|
+
self.search = SearchResource(self)
|
|
243
|
+
|
|
244
|
+
def close(self) -> None:
|
|
245
|
+
self._transport.close()
|
|
246
|
+
|
|
247
|
+
def __enter__(self) -> OpenPlatformClient:
|
|
248
|
+
return self
|
|
249
|
+
|
|
250
|
+
def __exit__(self, *exc_info: object) -> None:
|
|
251
|
+
self.close()
|
|
252
|
+
|
|
253
|
+
def _fill_req_id(self, body: dict[str, Any] | None, req_id: str | None) -> dict[str, Any] | None:
|
|
254
|
+
if body is None:
|
|
255
|
+
return None
|
|
256
|
+
if "reqId" not in body or body["reqId"] in (None, ""):
|
|
257
|
+
body["reqId"] = req_id or _new_req_id()
|
|
258
|
+
return body
|
|
259
|
+
|
|
260
|
+
def _request_data(
|
|
261
|
+
self,
|
|
262
|
+
method: str,
|
|
263
|
+
path: str,
|
|
264
|
+
*,
|
|
265
|
+
json_body: dict[str, Any] | None = None,
|
|
266
|
+
params: dict[str, Any] | None = None,
|
|
267
|
+
files: dict[str, Any] | None = None,
|
|
268
|
+
req_id: str | None = None,
|
|
269
|
+
) -> dict[str, Any]:
|
|
270
|
+
payload = self._transport.request(
|
|
271
|
+
method, path, json_body=json_body, params=params, files=files, req_id=req_id
|
|
272
|
+
)
|
|
273
|
+
data = payload.get("data")
|
|
274
|
+
return data if isinstance(data, dict) else ({} if data is None else {"value": data})
|
|
275
|
+
|
|
276
|
+
def _post(self, path: str, payload: BaseModel | dict[str, Any], *, req_id: str | None = None) -> dict[str, Any]:
|
|
277
|
+
body = self._fill_req_id(_dump(payload), req_id)
|
|
278
|
+
return self._request_data("POST", path, json_body=body)
|
|
279
|
+
|
|
280
|
+
def _get(
|
|
281
|
+
self,
|
|
282
|
+
path: str,
|
|
283
|
+
*,
|
|
284
|
+
params: dict[str, Any] | None = None,
|
|
285
|
+
req_id: str | None = None,
|
|
286
|
+
) -> dict[str, Any]:
|
|
287
|
+
query = dict(params or {})
|
|
288
|
+
query.setdefault("reqId", req_id or _new_req_id())
|
|
289
|
+
return self._request_data("GET", path, params=query)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""ikc-open-platform 第三方开发者 SDK 统一异常。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class OpenPlatformError(Exception):
|
|
9
|
+
"""SDK 基础异常。"""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class OpenPlatformAPIError(OpenPlatformError):
|
|
13
|
+
"""平台返回业务错误(errCode != 000000)。"""
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
err_code: str,
|
|
18
|
+
err_msg: str,
|
|
19
|
+
*,
|
|
20
|
+
trace_id: str | None = None,
|
|
21
|
+
req_id: str | None = None,
|
|
22
|
+
data: Any = None,
|
|
23
|
+
) -> None:
|
|
24
|
+
super().__init__(f"[{err_code}] {err_msg}")
|
|
25
|
+
self.err_code = err_code
|
|
26
|
+
self.err_msg = err_msg
|
|
27
|
+
self.trace_id = trace_id
|
|
28
|
+
self.req_id = req_id
|
|
29
|
+
self.data = data
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class OpenPlatformConnectionError(OpenPlatformError):
|
|
33
|
+
"""网络连接失败。"""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class OpenPlatformTimeoutError(OpenPlatformError):
|
|
37
|
+
"""请求超时。"""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class OpenPlatformProtocolError(OpenPlatformError):
|
|
41
|
+
"""响应不符合统一协议(缺 errCode 等非壳结构)。"""
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ikc-open-platform-sdk"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "ikc-open-platform 第三方开发者 SDK:API Key 认证 + 统一壳解包 + 四类业务域(知识库/文档/解析/检索),业务模型复用 ikc-sdk-lib"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
|
+
authors = [{ name = "SITECH-iKM" }]
|
|
12
|
+
dependencies = [
|
|
13
|
+
"httpx>=0.27,<1.0",
|
|
14
|
+
"pydantic>=2.7,<3.0",
|
|
15
|
+
"ikc-sdk-lib==0.6.1",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[tool.hatch.build.targets.wheel]
|
|
19
|
+
packages = ["ikc_open_platform_sdk"]
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""ikc_open_platform_sdk 客户端测试(httpx MockTransport)。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from ikc_open_platform_sdk import (
|
|
11
|
+
OpenPlatformAPIError,
|
|
12
|
+
OpenPlatformClient,
|
|
13
|
+
OpenPlatformProtocolError,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _client(handler) -> OpenPlatformClient:
|
|
18
|
+
transport = httpx.MockTransport(handler)
|
|
19
|
+
http_client = httpx.Client(base_url="http://platform.test", transport=transport)
|
|
20
|
+
return OpenPlatformClient("http://platform.test", api_key="k-test", http_client=http_client)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_envelope_unwrap_success() -> None:
|
|
24
|
+
def handler(request: httpx.Request) -> httpx.Response:
|
|
25
|
+
assert request.headers["Authorization"] == "Bearer k-test"
|
|
26
|
+
assert len(request.headers["X-Request-Id"]) == 23
|
|
27
|
+
body = json.loads(request.content.decode())
|
|
28
|
+
assert body["reqId"].startswith("req_")
|
|
29
|
+
return httpx.Response(
|
|
30
|
+
200,
|
|
31
|
+
json={"errCode": "000000", "errMsg": "success", "data": {"kbId": "kb_1", "kbType": "general", "kbMode": "private", "accessMode": "private", "scopeKey": "s1", "status": "active", "rootUnitId": "u1"}, "traceId": "1" * 23, "reqId": body["reqId"]},
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
client = _client(handler)
|
|
35
|
+
result = client.knowledge_bases.create({"kbName": "测试", "kbType": "general", "kbMode": "private"})
|
|
36
|
+
assert result.kbId == "kb_1"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_api_error_raises_with_trace() -> None:
|
|
40
|
+
def handler(request: httpx.Request) -> httpx.Response:
|
|
41
|
+
return httpx.Response(200, json={"errCode": "100429", "errMsg": "限流", "data": {}, "traceId": "2" * 23})
|
|
42
|
+
|
|
43
|
+
client = _client(handler)
|
|
44
|
+
with pytest.raises(OpenPlatformAPIError) as exc_info:
|
|
45
|
+
client.search.query({"query": "x"})
|
|
46
|
+
assert exc_info.value.err_code == "100429"
|
|
47
|
+
assert exc_info.value.trace_id == "2" * 23
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_protocol_error_on_non_envelope() -> None:
|
|
51
|
+
def handler(request: httpx.Request) -> httpx.Response:
|
|
52
|
+
return httpx.Response(200, json={"unexpected": True})
|
|
53
|
+
|
|
54
|
+
client = _client(handler)
|
|
55
|
+
with pytest.raises(OpenPlatformProtocolError):
|
|
56
|
+
client.knowledge_bases.get("kb_1")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_get_passes_req_id_param() -> None:
|
|
60
|
+
captured: dict[str, str] = {}
|
|
61
|
+
|
|
62
|
+
def handler(request: httpx.Request) -> httpx.Response:
|
|
63
|
+
captured["reqId"] = httpx.QueryParams(request.url.params)["reqId"]
|
|
64
|
+
captured["taskId"] = httpx.QueryParams(request.url.params)["taskId"]
|
|
65
|
+
return httpx.Response(
|
|
66
|
+
200,
|
|
67
|
+
json={
|
|
68
|
+
"errCode": "000000",
|
|
69
|
+
"errMsg": "success",
|
|
70
|
+
"data": {"taskId": "t1", "status": "done", "result": {}},
|
|
71
|
+
"traceId": "3" * 23,
|
|
72
|
+
"reqId": captured["reqId"],
|
|
73
|
+
},
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
client = _client(handler)
|
|
77
|
+
client.parse.query_result("t1", req_id="req_fixed")
|
|
78
|
+
assert captured == {"reqId": "req_fixed", "taskId": "t1"}
|