Skip to content

Commit ef073d8

Browse files
committed
Merge 'feat/exceptions' into 'master'
feat: impl common exception in vikingdb See merge request: !15
2 parents b406fcd + 2b4f7be commit ef073d8

File tree

15 files changed

+475
-459
lines changed

15 files changed

+475
-459
lines changed
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
"""
5+
Example 4: Exceptions
6+
"""
7+
8+
import os
9+
import time
10+
11+
from vikingdb import IAM
12+
from vikingdb.memory import VikingMem
13+
14+
15+
# Initialize client
16+
_auth = IAM(
17+
ak=os.getenv("VIKINGDB_AK", "your_ak"),
18+
sk=os.getenv("VIKINGDB_SK", "your_sk"),
19+
)
20+
client = VikingMem(
21+
host="api-knowledgebase.mlp.cn-beijing.volces.com",
22+
region="cn-beijing",
23+
auth=_auth,
24+
scheme="http",
25+
)
26+
27+
28+
def add_session_exception():
29+
collection = client.get_collection(
30+
collection_name="sdk_missing",
31+
project_name="default"
32+
)
33+
now_ts = int(time.time() * 1000)
34+
# Add session messages
35+
result = collection.add_session(
36+
session_id="session_001",
37+
messages=[
38+
{
39+
"role": "user",
40+
"content": "Hello, how is the weather today?"
41+
},
42+
{
43+
"role": "assistant",
44+
"content": "Today is sunny with a temperature of 22 degrees, perfect for going out."
45+
}
46+
],
47+
metadata = {
48+
"default_user_id": "user_3",
49+
"default_user_name": "Li",
50+
"default_assistant_id": "111",
51+
"default_assistant_name": "Smart Assistant",
52+
# "group_id": "",
53+
"time": now_ts,
54+
}
55+
)
56+
57+
return result
58+
59+
60+
61+
62+
if __name__ == "__main__":
63+
print("Viking Memory Add Session Messages Example\n")
64+
65+
add_session_exception()
66+

examples/vector/E3_1_index_search_multimodal_test.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

1717
from .guide_helpers import (
1818
Clients,
19-
assign_chapter_ids_via_search,
2019
bool_and_filters,
2120
build_clients,
2221
build_request_options,
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
"""Scenario 6 – Exception handling for the vector client."""
2+
3+
from __future__ import annotations
4+
5+
import pytest
6+
7+
from vikingdb import IAM
8+
from vikingdb.exceptions import NETWORK_ERROR_CODE
9+
from vikingdb.vector import SearchByRandomRequest, VikingVector
10+
from vikingdb.vector.exceptions import VikingVectorException
11+
12+
from .guide_helpers import EnvConfig, load_config
13+
14+
15+
def _build_vector_client(*, host_override: str | None = None, timeout: int | None = None) -> tuple[EnvConfig, VikingVector]:
16+
"""
17+
Create a VikingVector client using the shared guide configuration.
18+
19+
``host_override`` allows tests to force failure scenarios (for example, invalid hosts),
20+
while ``timeout`` lets them shorten request durations for network errors.
21+
"""
22+
config = load_config()
23+
auth = IAM(ak=config.access_key, sk=config.secret_key)
24+
client = VikingVector(
25+
host=host_override if host_override is not None else config.host,
26+
region=config.region,
27+
scheme=config.scheme,
28+
auth=auth,
29+
timeout=timeout if timeout is not None else 30,
30+
)
31+
return config, client
32+
33+
34+
def test_exception_collection_not_exist() -> None:
35+
"""Calling a non-existent collection should surface a VikingVectorException with a helpful code."""
36+
config, client = _build_vector_client()
37+
missing_collection = f"{config.collection}-missing"
38+
39+
collection_client = client.collection(
40+
collection_name=missing_collection,
41+
project_name=config.project_name,
42+
resource_id=config.resource_id,
43+
)
44+
45+
with pytest.raises(VikingVectorException) as exc_info:
46+
collection_client.fetch({"ids": ["non-existent-id"]})
47+
48+
error_code = str(exc_info.value.code)
49+
assert "NotFound" in error_code
50+
51+
52+
def test_exception_wrong_host_raises_network() -> None:
53+
"""Using an invalid host should raise a network exception promoted to VikingVectorException."""
54+
config, client = _build_vector_client(host_override="this-host-does-not-exist.invalid", timeout=1)
55+
56+
index_client = client.index(
57+
collection_name=config.collection,
58+
index_name=config.index,
59+
project_name=config.project_name,
60+
resource_id=config.resource_id,
61+
)
62+
63+
with pytest.raises(VikingVectorException) as exc_info:
64+
index_client.search_by_random(SearchByRandomRequest(limit=1))
65+
66+
error_code = str(exc_info.value.code)
67+
assert "InternalServerError" in error_code

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,5 @@ version = {attr = "vikingdb.version.__version__"}
4646
[dependency-groups]
4747
dev = [
4848
"pytest>=7.4.4",
49+
"ruff>=0.14.3",
4950
]

uv.lock

Lines changed: 32 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

vikingdb/_client.py

Lines changed: 86 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import json
99
from abc import ABC, abstractmethod
10+
from json import JSONDecodeError
1011
from typing import Any, Mapping, Optional
1112

1213
import aiohttp
@@ -17,6 +18,13 @@
1718
from volcengine.base.Service import Service
1819

1920
from .auth import Auth
21+
from .exceptions import (
22+
DEFAULT_UNKNOWN_ERROR_CODE,
23+
VikingAPIException,
24+
)
25+
26+
27+
_REQUEST_ID_HEADER = "X-Tt-Logid"
2028

2129

2230
class Client(Service, ABC):
@@ -113,6 +121,9 @@ def _json(
113121
self.auth_provider.sign_request(request)
114122
url = request.build()
115123

124+
request_id_value = request.headers.get(_REQUEST_ID_HEADER)
125+
request_id = str(request_id_value) if request_id_value else "unknown"
126+
116127
# Use custom timeout if provided, otherwise use default
117128
if timeout is not None:
118129
request_timeout = (timeout, timeout)
@@ -122,15 +133,49 @@ def _json(
122133
self.service_info.socket_timeout,
123134
)
124135

125-
response = self.session.post(
126-
url,
127-
headers=request.headers,
128-
data=request.body,
129-
timeout=request_timeout,
130-
)
131-
if response.status_code == 200:
136+
try:
137+
response = self.session.post(
138+
url,
139+
headers=request.headers,
140+
data=request.body,
141+
timeout=request_timeout,
142+
)
143+
except Exception as exc:
144+
raise VikingAPIException(
145+
DEFAULT_UNKNOWN_ERROR_CODE,
146+
request_id=request_id,
147+
message=f"failed to run session.post {api}: {exc}",
148+
) from exc
149+
150+
payload_text_attr = getattr(response, "text", "")
151+
payload_text = payload_text_attr if isinstance(payload_text_attr, str) else ""
152+
payload_text = payload_text or ""
153+
154+
if response.status_code != 200:
155+
error = VikingAPIException.from_response(
156+
payload_text,
157+
request_id=request_id,
158+
status_code=response.status_code,
159+
)
160+
raise error
161+
162+
try:
132163
return response.json()
133-
raise Exception(response.text.encode("utf-8"))
164+
except (ValueError, JSONDecodeError) as exc:
165+
raise VikingAPIException(
166+
DEFAULT_UNKNOWN_ERROR_CODE,
167+
request_id=request_id,
168+
message=f"failed to decode JSON response for {api}: {exc}",
169+
status_code=response.status_code,
170+
) from exc
171+
172+
except Exception as exc: # pragma: no cover - defensive fallback
173+
raise VikingAPIException(
174+
DEFAULT_UNKNOWN_ERROR_CODE,
175+
request_id=request_id,
176+
message=f"unexpected error parsing response for {api}: {exc}",
177+
status_code=response.status_code,
178+
) from exc
134179

135180
async def async_json(
136181
self,
@@ -175,14 +220,36 @@ async def async_json(
175220
)
176221

177222
url = request.build()
178-
async with aiohttp.request(
179-
"POST",
180-
url,
181-
headers=request.headers,
182-
data=request.body,
183-
timeout=client_timeout,
184-
) as response:
185-
payload = await response.text(encoding="utf-8")
186-
if response.status == 200:
187-
return json.loads(payload)
188-
raise Exception(payload)
223+
try:
224+
async with aiohttp.request(
225+
"POST",
226+
url,
227+
headers=request.headers,
228+
data=request.body,
229+
timeout=client_timeout,
230+
) as response:
231+
request_id_value = response.headers.get(_REQUEST_ID_HEADER)
232+
request_id = str(request_id_value) if request_id_value else "unknown"
233+
payload = await response.text(encoding="utf-8")
234+
if response.status != 200:
235+
error = VikingAPIException.from_response(
236+
payload,
237+
request_id=request_id,
238+
status_code=response.status,
239+
)
240+
raise error
241+
try:
242+
return json.loads(payload)
243+
except JSONDecodeError as exc:
244+
raise VikingAPIException(
245+
DEFAULT_UNKNOWN_ERROR_CODE,
246+
request_id=request_id,
247+
message=f"failed to decode JSON response for {api}: {exc}",
248+
status_code=response.status,
249+
) from exc
250+
except Exception as exc:
251+
raise VikingAPIException(
252+
DEFAULT_UNKNOWN_ERROR_CODE,
253+
request_id=request_id,
254+
message=f"failed to run aiohttp {api}: {exc}",
255+
) from exc

0 commit comments

Comments
 (0)