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.
@@ -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,3 @@
1
+ """SDK 版本(与发行版 pyproject 保持一致)。"""
2
+
3
+ __version__ = "0.1.0"
@@ -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,8 @@
1
+ """SDK 测试夹具:包路径注入(不依赖安装)。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
@@ -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"}