vikingdb-python-sdk 0.1.9__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.
vikingdb/__init__.py ADDED
@@ -0,0 +1,36 @@
1
+ # Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from __future__ import annotations
5
+
6
+ from .auth import APIKey, IAM
7
+ from .request_options import RequestOptions
8
+ from . import vector
9
+ from . import memory
10
+ from .vector import (
11
+ CollectionClient,
12
+ EmbeddingClient,
13
+ IndexClient,
14
+ RerankClient,
15
+ VikingVector,
16
+ )
17
+ from .memory import (
18
+ VikingMem,
19
+ Collection,
20
+ )
21
+ from .version import __version__
22
+ __all__ = [
23
+ "IAM",
24
+ "APIKey",
25
+ "CollectionClient",
26
+ "EmbeddingClient",
27
+ "RerankClient",
28
+ "IndexClient",
29
+ "RequestOptions",
30
+ "VikingVector",
31
+ "vector",
32
+ "memory",
33
+ "VikingMem",
34
+ "Collection",
35
+ "__version__",
36
+ ]
vikingdb/_client.py ADDED
@@ -0,0 +1,262 @@
1
+ # Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Shared base client logic for Viking services."""
5
+
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ from abc import ABC, abstractmethod
10
+ from json import JSONDecodeError
11
+ from typing import Any, Mapping, Optional
12
+
13
+ import aiohttp
14
+
15
+ from volcengine.ApiInfo import ApiInfo
16
+ from volcengine.ServiceInfo import ServiceInfo
17
+ from volcengine.base.Request import Request
18
+ from volcengine.base.Service import Service
19
+ import requests
20
+
21
+ from .auth import Auth, IAM, APIKey
22
+ from .exceptions import (
23
+ DEFAULT_UNKNOWN_ERROR_CODE,
24
+ VikingAPIException,
25
+ )
26
+
27
+
28
+ _REQUEST_ID_HEADER = "X-Tt-Logid"
29
+
30
+
31
+ class Client(Service, ABC):
32
+ """Reusable base client built on top of volcengine Service."""
33
+
34
+ def __init__(
35
+ self,
36
+ *,
37
+ host: str,
38
+ region: str,
39
+ service: str,
40
+ auth: Auth,
41
+ sts_token: str = "",
42
+ scheme: str = "http",
43
+ timeout: int = 30,
44
+ ):
45
+ self.region = region
46
+ self.service = service
47
+ self.auth_provider = auth
48
+ credentials = auth.initialize(service=service, region=region)
49
+ self.service_info = self._build_service_info(
50
+ host=host,
51
+ credentials=credentials,
52
+ scheme=scheme,
53
+ timeout=timeout,
54
+ )
55
+ self.api_info = self._build_api_info()
56
+ # 判断auth是不是IAM 还是 APIKey类型
57
+ if isinstance(auth, IAM):
58
+ super().__init__(self.service_info, self.api_info)
59
+ elif isinstance(auth, APIKey):
60
+ self.session = requests.session()
61
+ else:
62
+ raise ValueError("auth must be IAM or APIKey type")
63
+
64
+ if sts_token:
65
+ self.set_session_token(session_token=sts_token)
66
+
67
+ @abstractmethod
68
+ def _build_api_info(self) -> Mapping[str, ApiInfo]:
69
+ """Return the API metadata mapping used by this client."""
70
+
71
+ @staticmethod
72
+ def _build_service_info(
73
+ *,
74
+ host: str,
75
+ credentials,
76
+ scheme: str,
77
+ timeout: int,
78
+ ) -> ServiceInfo:
79
+ return ServiceInfo(
80
+ host,
81
+ {},
82
+ credentials,
83
+ timeout,
84
+ timeout,
85
+ scheme=scheme,
86
+ )
87
+
88
+ def prepare_request(self, api_info: ApiInfo, params: Optional[Mapping[str, Any]], doseq: int = 0):
89
+ """Prepare a volcengine request without adding implicit headers."""
90
+ request = Request()
91
+ request.set_shema(self.service_info.scheme)
92
+ request.set_method(api_info.method)
93
+ request.set_host(self.service_info.host)
94
+ request.set_path(api_info.path)
95
+ request.set_connection_timeout(self.service_info.connection_timeout)
96
+ request.set_socket_timeout(self.service_info.socket_timeout)
97
+ request.set_headers(dict(api_info.header))
98
+ if params:
99
+ request.set_query(params)
100
+ return request
101
+
102
+ def _json(
103
+ self,
104
+ api: str,
105
+ params: Optional[Mapping[str, Any]],
106
+ body: Any,
107
+ headers: Optional[Mapping[str, str]] = None,
108
+ timeout: Optional[int] = None,
109
+ ) -> Any:
110
+ """Send a JSON request synchronously.
111
+
112
+ Args:
113
+ api: API name
114
+ params: Query parameters
115
+ body: Request body
116
+ headers: Additional headers
117
+ timeout: Timeout in seconds (optional). If not provided, uses default connection_timeout and socket_timeout.
118
+ """
119
+ if api not in self.api_info:
120
+ raise Exception("no such api")
121
+ api_info = self.api_info[api]
122
+ request = self.prepare_request(api_info, params)
123
+ if headers:
124
+ for key, value in headers.items():
125
+ request.headers[key] = value
126
+ request.headers["Content-Type"] = "application/json"
127
+ request.body = body
128
+ self.auth_provider.sign_request(request)
129
+ url = request.build()
130
+
131
+ request_id_value = request.headers.get(_REQUEST_ID_HEADER)
132
+ request_id = str(request_id_value) if request_id_value else "unknown"
133
+
134
+ # Use custom timeout if provided, otherwise use default
135
+ if timeout is not None:
136
+ request_timeout = (timeout, timeout)
137
+ else:
138
+ request_timeout = (
139
+ self.service_info.connection_timeout,
140
+ self.service_info.socket_timeout,
141
+ )
142
+
143
+ try:
144
+ response = self.session.post(
145
+ url,
146
+ headers=request.headers,
147
+ data=request.body,
148
+ timeout=request_timeout,
149
+ )
150
+ except Exception as exc:
151
+ raise VikingAPIException(
152
+ DEFAULT_UNKNOWN_ERROR_CODE,
153
+ request_id=request_id,
154
+ message=f"failed to run session.post {api}: {exc}",
155
+ ) from exc
156
+
157
+ payload_text_attr = getattr(response, "text", "")
158
+ payload_text = payload_text_attr if isinstance(payload_text_attr, str) else ""
159
+ payload_text = payload_text or ""
160
+
161
+ if response.status_code != 200:
162
+ error = VikingAPIException.from_response(
163
+ payload_text,
164
+ request_id=request_id,
165
+ status_code=response.status_code,
166
+ )
167
+ raise error
168
+
169
+ try:
170
+ return response.json()
171
+ except (ValueError, JSONDecodeError) as exc:
172
+ raise VikingAPIException(
173
+ DEFAULT_UNKNOWN_ERROR_CODE,
174
+ request_id=request_id,
175
+ message=f"failed to decode JSON response for {api}: {exc}",
176
+ status_code=response.status_code,
177
+ ) from exc
178
+
179
+ except Exception as exc: # pragma: no cover - defensive fallback
180
+ raise VikingAPIException(
181
+ DEFAULT_UNKNOWN_ERROR_CODE,
182
+ request_id=request_id,
183
+ message=f"unexpected error parsing response for {api}: {exc}",
184
+ status_code=response.status_code,
185
+ ) from exc
186
+
187
+ async def async_json(
188
+ self,
189
+ api: str,
190
+ params: Optional[Mapping[str, Any]],
191
+ body: Any,
192
+ headers: Optional[Mapping[str, str]] = None,
193
+ timeout: Optional[int] = None,
194
+ ) -> Any:
195
+ """Send a JSON request asynchronously.
196
+
197
+ Args:
198
+ api: API name
199
+ params: Query parameters
200
+ body: Request body
201
+ headers: Additional headers
202
+ timeout: Timeout in seconds (optional). If not provided, uses default connection_timeout and socket_timeout.
203
+ """
204
+ if api not in self.api_info:
205
+ raise Exception("no such api")
206
+ api_info = self.api_info[api]
207
+ request = self.prepare_request(api_info, params)
208
+ if headers:
209
+ for key, value in headers.items():
210
+ request.headers[key] = value
211
+ request.headers["Content-Type"] = "application/json"
212
+ request.body = body
213
+
214
+ self.auth_provider.sign_request(request)
215
+
216
+ # Use custom timeout if provided, otherwise use default
217
+ if timeout is not None:
218
+ client_timeout = aiohttp.ClientTimeout(
219
+ connect=timeout,
220
+ sock_connect=timeout,
221
+ sock_read=timeout,
222
+ )
223
+ else:
224
+ client_timeout = aiohttp.ClientTimeout(
225
+ connect=self.service_info.connection_timeout,
226
+ sock_connect=self.service_info.socket_timeout,
227
+ )
228
+
229
+ url = request.build()
230
+ try:
231
+ async with aiohttp.request(
232
+ "POST",
233
+ url,
234
+ headers=request.headers,
235
+ data=request.body,
236
+ timeout=client_timeout,
237
+ ) as response:
238
+ request_id_value = response.headers.get(_REQUEST_ID_HEADER)
239
+ request_id = str(request_id_value) if request_id_value else "unknown"
240
+ payload = await response.text(encoding="utf-8")
241
+ if response.status != 200:
242
+ error = VikingAPIException.from_response(
243
+ payload,
244
+ request_id=request_id,
245
+ status_code=response.status,
246
+ )
247
+ raise error
248
+ try:
249
+ return json.loads(payload)
250
+ except JSONDecodeError as exc:
251
+ raise VikingAPIException(
252
+ DEFAULT_UNKNOWN_ERROR_CODE,
253
+ request_id=request_id,
254
+ message=f"failed to decode JSON response for {api}: {exc}",
255
+ status_code=response.status,
256
+ ) from exc
257
+ except Exception as exc:
258
+ raise VikingAPIException(
259
+ DEFAULT_UNKNOWN_ERROR_CODE,
260
+ request_id=request_id,
261
+ message=f"failed to run aiohttp {api}: {exc}",
262
+ ) from exc
vikingdb/auth.py ADDED
@@ -0,0 +1,68 @@
1
+ # Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Common authentication helpers shared by Viking services."""
5
+
6
+ from __future__ import annotations
7
+
8
+ from abc import ABC, abstractmethod
9
+ from volcengine.Credentials import Credentials
10
+ from volcengine.auth.SignerV4 import SignerV4
11
+
12
+
13
+ class Auth(ABC):
14
+ """Base class for authentication providers compatible with volcengine."""
15
+
16
+ @abstractmethod
17
+ def initialize(self, *, service: str, region: str):
18
+ """Prepare the provider for the given service/region. Returns credentials understood by ServiceInfo."""
19
+
20
+ @abstractmethod
21
+ def sign_request(self, request) -> None:
22
+ """Sign or otherwise authorise the outgoing request."""
23
+
24
+
25
+ class IAM(Auth):
26
+ """IAM-style AK/SK signature authentication provider."""
27
+
28
+ def __init__(self, *, ak: str, sk: str):
29
+ if not ak or not sk:
30
+ raise ValueError("ak and sk must be provided for IAM authentication")
31
+ self._ak = ak
32
+ self._sk = sk
33
+ self._credentials = None
34
+ self._service = None
35
+ self._region = None
36
+
37
+ def initialize(self, *, service: str, region: str):
38
+ if not service or not region:
39
+ raise ValueError("service and region must be provided for IAM credentials")
40
+ if self._credentials is not None and (service != self._service or region != self._region):
41
+ raise ValueError(
42
+ f"IAM credentials already initialised for service={self._service!r}, region={self._region!r}"
43
+ )
44
+ self._service = service
45
+ self._region = region
46
+ if self._credentials is None:
47
+ self._credentials = Credentials(self._ak, self._sk, service, region)
48
+ return self._credentials
49
+
50
+ def sign_request(self, request) -> None:
51
+ if self._credentials is None:
52
+ raise ValueError("IAM provider must be initialised before signing requests")
53
+ SignerV4.sign(request, self._credentials)
54
+
55
+
56
+ class APIKey(Auth):
57
+ """API Key authentication provider (placeholder for future support)."""
58
+
59
+ def __init__(self, *, api_key: str):
60
+ if not api_key:
61
+ raise ValueError("api_key must be provided for API key authentication")
62
+ self.api_key = api_key
63
+
64
+ def initialize(self, *, service: str, region: str):
65
+ return None
66
+
67
+ def sign_request(self, request) -> None:
68
+ request.headers["Authorization"] = f"Bearer {self.api_key}"
vikingdb/exceptions.py ADDED
@@ -0,0 +1,160 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from dataclasses import dataclass
5
+ from typing import Any, Mapping, Optional, Type, TypeVar, Union
6
+
7
+ DEFAULT_UNKNOWN_ERROR_CODE: Union[int, str] = 1000028
8
+ NETWORK_ERROR_CODE = 1001
9
+
10
+
11
+ @dataclass
12
+ class ParsedError:
13
+ code: Union[int, str]
14
+ request_id: str
15
+ message: Optional[str]
16
+ payload: Any
17
+ raw: Optional[str] = None
18
+
19
+
20
+ def _normalize_code(value: Any, default: Union[int, str]) -> Union[int, str]:
21
+ if value is None:
22
+ return default
23
+ if isinstance(value, int):
24
+ return value
25
+ if isinstance(value, str):
26
+ stripped = value.strip()
27
+ return stripped or default
28
+ try:
29
+ return int(value)
30
+ except (TypeError, ValueError):
31
+ return default
32
+
33
+
34
+ def parse_error_payload(payload: Any) -> ParsedError:
35
+ """
36
+ Parse a server error payload into a consistent structure.
37
+
38
+ Supports both the legacy {"ResponseMetadata": {"Error": {...}}} format and the
39
+ newer flat {"code": ..., "request_id": ..., "message": ...} shape. When JSON
40
+ decoding fails, the raw text is preserved as the message.
41
+ """
42
+ code = DEFAULT_UNKNOWN_ERROR_CODE
43
+ request_id = "unknown"
44
+ message: Optional[str] = None
45
+ raw_text: Optional[str] = None
46
+ parsed_payload: Any = payload
47
+
48
+ if isinstance(payload, bytes):
49
+ raw_text = payload.decode("utf-8", errors="replace")
50
+ elif isinstance(payload, str):
51
+ raw_text = payload
52
+
53
+ if raw_text is not None:
54
+ try:
55
+ parsed_payload = json.loads(raw_text)
56
+ except json.JSONDecodeError:
57
+ message = raw_text.strip() or None
58
+ return ParsedError(code, request_id, message, raw_text, raw_text)
59
+
60
+ if isinstance(parsed_payload, Mapping):
61
+ metadata = parsed_payload.get("ResponseMetadata")
62
+ if isinstance(metadata, Mapping):
63
+ error = metadata.get("Error")
64
+ if isinstance(error, Mapping):
65
+ code_value = error.get("Code") if "Code" in error else error.get("code")
66
+ code = _normalize_code(code_value, code)
67
+ request_id = str(error.get("RequestId") or request_id)
68
+ if error.get("Message"):
69
+ message = str(error["Message"])
70
+ if metadata.get("RequestId"):
71
+ request_id = str(metadata["RequestId"])
72
+ else:
73
+ code_value = parsed_payload.get("code")
74
+ if code_value is None and "Code" in parsed_payload:
75
+ code_value = parsed_payload.get("Code")
76
+ code = _normalize_code(code_value, code)
77
+ if parsed_payload.get("request_id"):
78
+ request_id = str(parsed_payload["request_id"])
79
+ if parsed_payload.get("message"):
80
+ message = str(parsed_payload["message"])
81
+ else:
82
+ if message is None and raw_text is None:
83
+ message = str(parsed_payload)
84
+
85
+ if raw_text is None and isinstance(parsed_payload, (dict, list)):
86
+ try:
87
+ raw_text = json.dumps(parsed_payload, ensure_ascii=False)
88
+ except Exception:
89
+ raw_text = None
90
+
91
+ return ParsedError(code, request_id, message, parsed_payload, raw_text)
92
+
93
+
94
+ T_VikingException = TypeVar("T_VikingException", bound="VikingException")
95
+
96
+
97
+ class VikingException(Exception):
98
+ """
99
+ Base exception for all Viking SDK errors.
100
+
101
+ Captures standard fields returned by the service for consistent error handling.
102
+ """
103
+
104
+ def __init__(
105
+ self,
106
+ code: Union[int, str],
107
+ request_id: str = "unknown",
108
+ message: Optional[str] = None,
109
+ *,
110
+ status_code: Optional[int] = None,
111
+ ) -> None:
112
+ self.code = code
113
+ self.request_id = request_id or "unknown"
114
+ self.status_code = status_code
115
+ self.message = message or f"request failed (code={self.code})"
116
+ super().__init__(self.message)
117
+
118
+ def __str__(self) -> str:
119
+ status = f", http_status={self.status_code}" if self.status_code is not None else ""
120
+ return f"{self.message} (code={self.code}, request_id={self.request_id}{status})"
121
+
122
+ def promote(self, target_cls: Type[T_VikingException]) -> T_VikingException:
123
+ """
124
+ Promote this exception to a more specific subclass, reusing captured context.
125
+ """
126
+ if isinstance(self, target_cls):
127
+ return self # type: ignore[return-value]
128
+ return target_cls(
129
+ self.code,
130
+ self.request_id,
131
+ self.message,
132
+ status_code=self.status_code,
133
+ )
134
+
135
+
136
+ class VikingAPIException(VikingException):
137
+ """Raised when the remote API returns an error payload."""
138
+
139
+ @classmethod
140
+ def from_response(cls, payload: Any, *, request_id: Optional[str] = "unknown", status_code: Optional[int] = None) -> "VikingAPIException":
141
+ parsed = parse_error_payload(payload)
142
+ return cls(
143
+ parsed.code,
144
+ parsed.request_id or request_id,
145
+ parsed.message or "unknown api error",
146
+ status_code=status_code,
147
+ )
148
+
149
+
150
+ def promote_exception(
151
+ exc: VikingException,
152
+ *,
153
+ exception_map: Optional[Mapping[int, Type[T_VikingException]]] = None,
154
+ default_cls: Type[T_VikingException],
155
+ ) -> T_VikingException:
156
+ """
157
+ Promote a VikingException to a service-specific subclass using the provided map.
158
+ """
159
+ target_cls = (exception_map or {}).get(exc.code, default_cls)
160
+ return exc.promote(target_cls)
@@ -0,0 +1,70 @@
1
+ # coding:utf-8
2
+ # Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd.
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ """
6
+ Viking Memory SDK
7
+
8
+ Provides memory management features including:
9
+ - Collection management (create, delete, update, query)
10
+ - Profile management (add, delete, update)
11
+ - Event management (add, update, delete, batch delete)
12
+ - Session management (add messages)
13
+ - Memory search (semantic search)
14
+
15
+ All APIs return plain dictionaries (dict) or lists of dictionaries (list[dict]) without object encapsulation.
16
+
17
+ Authentication support:
18
+ - AK/SK signature authentication (standard volcengine SignerV4)
19
+ - API Key authentication (reserved interface)
20
+ """
21
+
22
+ from .client import VikingMem
23
+ from .collection import Collection
24
+ from .exceptions import (
25
+ VikingMemException,
26
+ UnauthorizedException,
27
+ InvalidRequestException,
28
+ CollectionExistException,
29
+ CollectionNotExistException,
30
+ IndexExistException,
31
+ IndexNotExistException,
32
+ DataNotFoundException,
33
+ DelOpFailedException,
34
+ UpsertOpFailedException,
35
+ InvalidVectorException,
36
+ InvalidPrimaryKeyException,
37
+ InvalidFilterException,
38
+ IndexSearchException,
39
+ IndexFetchException,
40
+ IndexInitializingException,
41
+ EmbeddingException,
42
+ InternalServerException,
43
+ QuotaLimiterException,
44
+ )
45
+
46
+ __all__ = [
47
+ # Main client classes
48
+ "VikingMem",
49
+ "Collection",
50
+ # Exception classes
51
+ "VikingMemException",
52
+ "UnauthorizedException",
53
+ "InvalidRequestException",
54
+ "CollectionExistException",
55
+ "CollectionNotExistException",
56
+ "IndexExistException",
57
+ "IndexNotExistException",
58
+ "DataNotFoundException",
59
+ "DelOpFailedException",
60
+ "UpsertOpFailedException",
61
+ "InvalidVectorException",
62
+ "InvalidPrimaryKeyException",
63
+ "InvalidFilterException",
64
+ "IndexSearchException",
65
+ "IndexFetchException",
66
+ "IndexInitializingException",
67
+ "EmbeddingException",
68
+ "InternalServerException",
69
+ "QuotaLimiterException",
70
+ ]