embed-client 0.0.1__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.
- embed_client-0.0.1/PKG-INFO +9 -0
- embed_client-0.0.1/README.md +1 -0
- embed_client-0.0.1/embed_client/__init__.py +1 -0
- embed_client-0.0.1/embed_client/async_client.py +180 -0
- embed_client-0.0.1/embed_client/example_async_usage.py +94 -0
- embed_client-0.0.1/embed_client/example_async_usage_ru.py +94 -0
- embed_client-0.0.1/embed_client.egg-info/PKG-INFO +9 -0
- embed_client-0.0.1/embed_client.egg-info/SOURCES.txt +15 -0
- embed_client-0.0.1/embed_client.egg-info/dependency_links.txt +1 -0
- embed_client-0.0.1/embed_client.egg-info/requires.txt +5 -0
- embed_client-0.0.1/embed_client.egg-info/top_level.txt +1 -0
- embed_client-0.0.1/pyproject.toml +21 -0
- embed_client-0.0.1/setup.cfg +4 -0
- embed_client-0.0.1/tests/test_async_client.py +241 -0
- embed_client-0.0.1/tests/test_async_client_real.py +110 -0
- embed_client-0.0.1/tests/test_example_async_usage.py +29 -0
- embed_client-0.0.1/tests/test_example_async_usage_ru.py +25 -0
@@ -0,0 +1,9 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: embed-client
|
3
|
+
Version: 0.0.1
|
4
|
+
Summary: Async client for Embedding Service API
|
5
|
+
Author: Your Name
|
6
|
+
Requires-Dist: aiohttp
|
7
|
+
Provides-Extra: test
|
8
|
+
Requires-Dist: pytest; extra == "test"
|
9
|
+
Requires-Dist: pytest-asyncio; extra == "test"
|
@@ -0,0 +1 @@
|
|
1
|
+
# vvz-embed-client
|
@@ -0,0 +1 @@
|
|
1
|
+
|
@@ -0,0 +1,180 @@
|
|
1
|
+
"""
|
2
|
+
Async client for Embedding Service API (OpenAPI 3.0.2)
|
3
|
+
|
4
|
+
- 100% type-annotated
|
5
|
+
- English docstrings and examples
|
6
|
+
- Ready for PyPi
|
7
|
+
"""
|
8
|
+
|
9
|
+
from typing import Any, Dict, List, Optional, Union
|
10
|
+
import aiohttp
|
11
|
+
|
12
|
+
class EmbeddingServiceError(Exception):
|
13
|
+
"""Base exception for EmbeddingServiceAsyncClient."""
|
14
|
+
|
15
|
+
class EmbeddingServiceConnectionError(EmbeddingServiceError):
|
16
|
+
"""Raised when the service is unavailable or connection fails."""
|
17
|
+
|
18
|
+
class EmbeddingServiceHTTPError(EmbeddingServiceError):
|
19
|
+
"""Raised for HTTP errors (4xx, 5xx)."""
|
20
|
+
def __init__(self, status: int, message: str):
|
21
|
+
super().__init__(f"HTTP {status}: {message}")
|
22
|
+
self.status = status
|
23
|
+
self.message = message
|
24
|
+
|
25
|
+
class EmbeddingServiceAPIError(EmbeddingServiceError):
|
26
|
+
"""Raised for errors returned by the API in the response body."""
|
27
|
+
def __init__(self, error: Any):
|
28
|
+
super().__init__(f"API error: {error}")
|
29
|
+
self.error = error
|
30
|
+
|
31
|
+
class EmbeddingServiceAsyncClient:
|
32
|
+
"""
|
33
|
+
Asynchronous client for the Embedding Service API.
|
34
|
+
Args:
|
35
|
+
base_url (str): Base URL of the embedding service (e.g., "http://localhost").
|
36
|
+
port (int): Port of the embedding service (e.g., 8001).
|
37
|
+
Raises:
|
38
|
+
ValueError: If base_url or port is not provided.
|
39
|
+
"""
|
40
|
+
def __init__(self, base_url: str, port: int):
|
41
|
+
if not base_url:
|
42
|
+
raise ValueError("base_url must be provided.")
|
43
|
+
if port is None:
|
44
|
+
raise ValueError("port must be provided.")
|
45
|
+
self.base_url = base_url.rstrip("/")
|
46
|
+
self.port = port
|
47
|
+
self._session: Optional[aiohttp.ClientSession] = None
|
48
|
+
|
49
|
+
def _make_url(self, path: str, base_url: Optional[str] = None, port: Optional[int] = None) -> str:
|
50
|
+
url = (base_url or self.base_url).rstrip("/")
|
51
|
+
port_val = port if port is not None else self.port
|
52
|
+
return f"{url}:{port_val}{path}"
|
53
|
+
|
54
|
+
async def __aenter__(self):
|
55
|
+
self._session = aiohttp.ClientSession()
|
56
|
+
return self
|
57
|
+
|
58
|
+
async def __aexit__(self, exc_type, exc, tb):
|
59
|
+
if self._session:
|
60
|
+
await self._session.close()
|
61
|
+
self._session = None
|
62
|
+
|
63
|
+
async def health(self, base_url: Optional[str] = None, port: Optional[int] = None) -> Dict[str, Any]:
|
64
|
+
"""
|
65
|
+
Check the health of the service.
|
66
|
+
Args:
|
67
|
+
base_url (str, optional): Override base URL.
|
68
|
+
port (int, optional): Override port.
|
69
|
+
Returns:
|
70
|
+
dict: Health status and model info.
|
71
|
+
"""
|
72
|
+
url = self._make_url("/health", base_url, port)
|
73
|
+
try:
|
74
|
+
async with self._session.get(url) as resp:
|
75
|
+
await self._raise_for_status(resp)
|
76
|
+
return await resp.json()
|
77
|
+
except EmbeddingServiceHTTPError:
|
78
|
+
raise
|
79
|
+
except EmbeddingServiceConnectionError:
|
80
|
+
raise
|
81
|
+
except aiohttp.ClientConnectionError as e:
|
82
|
+
raise EmbeddingServiceConnectionError(f"Connection error: {e}") from e
|
83
|
+
except aiohttp.ClientResponseError as e:
|
84
|
+
raise EmbeddingServiceHTTPError(e.status, e.message) from e
|
85
|
+
except Exception as e:
|
86
|
+
raise EmbeddingServiceError(f"Unexpected error: {e}") from e
|
87
|
+
|
88
|
+
async def get_openapi_schema(self, base_url: Optional[str] = None, port: Optional[int] = None) -> Dict[str, Any]:
|
89
|
+
"""
|
90
|
+
Get the OpenAPI schema of the service.
|
91
|
+
Args:
|
92
|
+
base_url (str, optional): Override base URL.
|
93
|
+
port (int, optional): Override port.
|
94
|
+
Returns:
|
95
|
+
dict: OpenAPI schema.
|
96
|
+
"""
|
97
|
+
url = self._make_url("/openapi.json", base_url, port)
|
98
|
+
try:
|
99
|
+
async with self._session.get(url) as resp:
|
100
|
+
await self._raise_for_status(resp)
|
101
|
+
return await resp.json()
|
102
|
+
except EmbeddingServiceHTTPError:
|
103
|
+
raise
|
104
|
+
except EmbeddingServiceConnectionError:
|
105
|
+
raise
|
106
|
+
except aiohttp.ClientConnectionError as e:
|
107
|
+
raise EmbeddingServiceConnectionError(f"Connection error: {e}") from e
|
108
|
+
except aiohttp.ClientResponseError as e:
|
109
|
+
raise EmbeddingServiceHTTPError(e.status, e.message) from e
|
110
|
+
except Exception as e:
|
111
|
+
raise EmbeddingServiceError(f"Unexpected error: {e}") from e
|
112
|
+
|
113
|
+
async def get_commands(self, base_url: Optional[str] = None, port: Optional[int] = None) -> Dict[str, Any]:
|
114
|
+
"""
|
115
|
+
Get the list of available commands.
|
116
|
+
Args:
|
117
|
+
base_url (str, optional): Override base URL.
|
118
|
+
port (int, optional): Override port.
|
119
|
+
Returns:
|
120
|
+
dict: List of commands and their descriptions.
|
121
|
+
"""
|
122
|
+
url = self._make_url("/api/commands", base_url, port)
|
123
|
+
try:
|
124
|
+
async with self._session.get(url) as resp:
|
125
|
+
await self._raise_for_status(resp)
|
126
|
+
return await resp.json()
|
127
|
+
except EmbeddingServiceHTTPError:
|
128
|
+
raise
|
129
|
+
except EmbeddingServiceConnectionError:
|
130
|
+
raise
|
131
|
+
except aiohttp.ClientConnectionError as e:
|
132
|
+
raise EmbeddingServiceConnectionError(f"Connection error: {e}") from e
|
133
|
+
except aiohttp.ClientResponseError as e:
|
134
|
+
raise EmbeddingServiceHTTPError(e.status, e.message) from e
|
135
|
+
except Exception as e:
|
136
|
+
raise EmbeddingServiceError(f"Unexpected error: {e}") from e
|
137
|
+
|
138
|
+
async def cmd(self, command: str, params: Optional[Dict[str, Any]] = None, base_url: Optional[str] = None, port: Optional[int] = None) -> Dict[str, Any]:
|
139
|
+
"""
|
140
|
+
Execute a command via JSON-RPC protocol.
|
141
|
+
Args:
|
142
|
+
command (str): Command to execute (embed, models, health, help, config).
|
143
|
+
params (dict, optional): Parameters for the command.
|
144
|
+
base_url (str, optional): Override base URL.
|
145
|
+
port (int, optional): Override port.
|
146
|
+
Returns:
|
147
|
+
dict: Command execution result.
|
148
|
+
"""
|
149
|
+
url = self._make_url("/cmd", base_url, port)
|
150
|
+
payload = {"command": command}
|
151
|
+
if params is not None:
|
152
|
+
payload["params"] = params
|
153
|
+
try:
|
154
|
+
async with self._session.post(url, json=payload) as resp:
|
155
|
+
await self._raise_for_status(resp)
|
156
|
+
data = await resp.json()
|
157
|
+
# Обработка ошибок, возвращаемых сервером в теле ответа
|
158
|
+
if "error" in data:
|
159
|
+
raise EmbeddingServiceAPIError(data["error"])
|
160
|
+
return data
|
161
|
+
except EmbeddingServiceAPIError:
|
162
|
+
raise
|
163
|
+
except EmbeddingServiceHTTPError:
|
164
|
+
raise
|
165
|
+
except EmbeddingServiceConnectionError:
|
166
|
+
raise
|
167
|
+
except aiohttp.ClientConnectionError as e:
|
168
|
+
raise EmbeddingServiceConnectionError(f"Connection error: {e}") from e
|
169
|
+
except aiohttp.ClientResponseError as e:
|
170
|
+
raise EmbeddingServiceHTTPError(e.status, e.message) from e
|
171
|
+
except Exception as e:
|
172
|
+
raise EmbeddingServiceError(f"Unexpected error: {e}") from e
|
173
|
+
|
174
|
+
async def _raise_for_status(self, resp: aiohttp.ClientResponse):
|
175
|
+
try:
|
176
|
+
resp.raise_for_status()
|
177
|
+
except aiohttp.ClientResponseError as e:
|
178
|
+
raise EmbeddingServiceHTTPError(e.status, e.message) from e
|
179
|
+
|
180
|
+
# TODO: Add methods for /cmd, /api/commands, etc.
|
@@ -0,0 +1,94 @@
|
|
1
|
+
"""
|
2
|
+
Example usage of EmbeddingServiceAsyncClient.
|
3
|
+
|
4
|
+
This example demonstrates how to use the async client to check the health of the embedding service,
|
5
|
+
request embeddings, and handle all possible exceptions.
|
6
|
+
|
7
|
+
Run this script with:
|
8
|
+
python -m asyncio embed_client/example_async_usage.py --base-url http://localhost --port 8001
|
9
|
+
|
10
|
+
You can also set EMBED_CLIENT_BASE_URL and EMBED_CLIENT_PORT environment variables.
|
11
|
+
"""
|
12
|
+
|
13
|
+
import asyncio
|
14
|
+
import sys
|
15
|
+
import os
|
16
|
+
from embed_client.async_client import (
|
17
|
+
EmbeddingServiceAsyncClient,
|
18
|
+
EmbeddingServiceConnectionError,
|
19
|
+
EmbeddingServiceHTTPError,
|
20
|
+
EmbeddingServiceAPIError,
|
21
|
+
EmbeddingServiceError,
|
22
|
+
)
|
23
|
+
|
24
|
+
def get_params():
|
25
|
+
base_url = None
|
26
|
+
port = None
|
27
|
+
for i, arg in enumerate(sys.argv):
|
28
|
+
if arg in ("--base-url", "-b") and i + 1 < len(sys.argv):
|
29
|
+
base_url = sys.argv[i + 1]
|
30
|
+
if arg in ("--port", "-p") and i + 1 < len(sys.argv):
|
31
|
+
port = sys.argv[i + 1]
|
32
|
+
if not base_url:
|
33
|
+
base_url = os.environ.get("EMBED_CLIENT_BASE_URL")
|
34
|
+
if not port:
|
35
|
+
port = os.environ.get("EMBED_CLIENT_PORT")
|
36
|
+
if not base_url or not port:
|
37
|
+
print("Error: base_url and port must be provided via --base-url/--port arguments or EMBED_CLIENT_BASE_URL/EMBED_CLIENT_PORT environment variables.")
|
38
|
+
sys.exit(1)
|
39
|
+
return None, None
|
40
|
+
return base_url, int(port)
|
41
|
+
|
42
|
+
async def main():
|
43
|
+
base_url, port = get_params()
|
44
|
+
# Always use try/except to handle all possible errors
|
45
|
+
try:
|
46
|
+
async with EmbeddingServiceAsyncClient(base_url=base_url, port=port) as client:
|
47
|
+
# Check health
|
48
|
+
try:
|
49
|
+
health = await client.health()
|
50
|
+
print("Service health:", health)
|
51
|
+
except EmbeddingServiceConnectionError as e:
|
52
|
+
print("[Connection error]", e)
|
53
|
+
return
|
54
|
+
except EmbeddingServiceHTTPError as e:
|
55
|
+
print(f"[HTTP error] {e.status}: {e.message}")
|
56
|
+
return
|
57
|
+
except EmbeddingServiceError as e:
|
58
|
+
print("[Other error]", e)
|
59
|
+
return
|
60
|
+
|
61
|
+
# Request embeddings for a list of texts
|
62
|
+
texts = ["hello world", "test embedding"]
|
63
|
+
try:
|
64
|
+
result = await client.cmd("embed", params={"texts": texts})
|
65
|
+
vectors = result["result"]
|
66
|
+
print(f"Embeddings for {len(texts)} texts:")
|
67
|
+
for i, vec in enumerate(vectors):
|
68
|
+
print(f" Text: {texts[i]!r}\n Vector: {vec[:5]}... (total {len(vec)} dims)")
|
69
|
+
except EmbeddingServiceAPIError as e:
|
70
|
+
print("[API error]", e.error)
|
71
|
+
except EmbeddingServiceHTTPError as e:
|
72
|
+
print(f"[HTTP error] {e.status}: {e.message}")
|
73
|
+
except EmbeddingServiceConnectionError as e:
|
74
|
+
print("[Connection error]", e)
|
75
|
+
except EmbeddingServiceError as e:
|
76
|
+
print("[Other error]", e)
|
77
|
+
|
78
|
+
# Example: error handling for invalid command
|
79
|
+
try:
|
80
|
+
await client.cmd("not_a_command")
|
81
|
+
except EmbeddingServiceAPIError as e:
|
82
|
+
print("[API error for invalid command]", e.error)
|
83
|
+
|
84
|
+
# Example: error handling for empty texts
|
85
|
+
try:
|
86
|
+
await client.cmd("embed", params={"texts": []})
|
87
|
+
except EmbeddingServiceAPIError as e:
|
88
|
+
print("[API error for empty texts]", e.error)
|
89
|
+
|
90
|
+
except Exception as e:
|
91
|
+
print("[Unexpected error]", e)
|
92
|
+
|
93
|
+
if __name__ == "__main__":
|
94
|
+
asyncio.run(main())
|
@@ -0,0 +1,94 @@
|
|
1
|
+
"""
|
2
|
+
Example usage of EmbeddingServiceAsyncClient.
|
3
|
+
|
4
|
+
This example demonstrates how to use the async client to check the health of the embedding service,
|
5
|
+
request embeddings, and handle all possible exceptions.
|
6
|
+
|
7
|
+
Run this script with:
|
8
|
+
python -m asyncio embed_client/example_async_usage_ru.py --base-url http://localhost --port 8001
|
9
|
+
|
10
|
+
You can also set EMBED_CLIENT_BASE_URL and EMBED_CLIENT_PORT environment variables.
|
11
|
+
"""
|
12
|
+
|
13
|
+
import asyncio
|
14
|
+
import sys
|
15
|
+
import os
|
16
|
+
from embed_client.async_client import (
|
17
|
+
EmbeddingServiceAsyncClient,
|
18
|
+
EmbeddingServiceConnectionError,
|
19
|
+
EmbeddingServiceHTTPError,
|
20
|
+
EmbeddingServiceAPIError,
|
21
|
+
EmbeddingServiceError,
|
22
|
+
)
|
23
|
+
|
24
|
+
def get_params():
|
25
|
+
base_url = None
|
26
|
+
port = None
|
27
|
+
for i, arg in enumerate(sys.argv):
|
28
|
+
if arg in ("--base-url", "-b") and i + 1 < len(sys.argv):
|
29
|
+
base_url = sys.argv[i + 1]
|
30
|
+
if arg in ("--port", "-p") and i + 1 < len(sys.argv):
|
31
|
+
port = sys.argv[i + 1]
|
32
|
+
if not base_url:
|
33
|
+
base_url = os.environ.get("EMBED_CLIENT_BASE_URL")
|
34
|
+
if not port:
|
35
|
+
port = os.environ.get("EMBED_CLIENT_PORT")
|
36
|
+
if not base_url or not port:
|
37
|
+
print("Error: base_url and port must be provided via --base-url/--port arguments or EMBED_CLIENT_BASE_URL/EMBED_CLIENT_PORT environment variables.")
|
38
|
+
sys.exit(1)
|
39
|
+
return None, None
|
40
|
+
return base_url, int(port)
|
41
|
+
|
42
|
+
async def main():
|
43
|
+
base_url, port = get_params()
|
44
|
+
# Always use try/except to handle all possible errors
|
45
|
+
try:
|
46
|
+
async with EmbeddingServiceAsyncClient(base_url=base_url, port=port) as client:
|
47
|
+
# Check health
|
48
|
+
try:
|
49
|
+
health = await client.health()
|
50
|
+
print("Service health:", health)
|
51
|
+
except EmbeddingServiceConnectionError as e:
|
52
|
+
print("[Connection error]", e)
|
53
|
+
return
|
54
|
+
except EmbeddingServiceHTTPError as e:
|
55
|
+
print(f"[HTTP error] {e.status}: {e.message}")
|
56
|
+
return
|
57
|
+
except EmbeddingServiceError as e:
|
58
|
+
print("[Other error]", e)
|
59
|
+
return
|
60
|
+
|
61
|
+
# Request embeddings for a list of texts
|
62
|
+
texts = ["hello world", "test embedding"]
|
63
|
+
try:
|
64
|
+
result = await client.cmd("embed", params={"texts": texts})
|
65
|
+
vectors = result["result"]
|
66
|
+
print(f"Embeddings for {len(texts)} texts:")
|
67
|
+
for i, vec in enumerate(vectors):
|
68
|
+
print(f" Text: {texts[i]!r}\n Vector: {vec[:5]}... (total {len(vec)} dims)")
|
69
|
+
except EmbeddingServiceAPIError as e:
|
70
|
+
print("[API error]", e.error)
|
71
|
+
except EmbeddingServiceHTTPError as e:
|
72
|
+
print(f"[HTTP error] {e.status}: {e.message}")
|
73
|
+
except EmbeddingServiceConnectionError as e:
|
74
|
+
print("[Connection error]", e)
|
75
|
+
except EmbeddingServiceError as e:
|
76
|
+
print("[Other error]", e)
|
77
|
+
|
78
|
+
# Example: error handling for invalid command
|
79
|
+
try:
|
80
|
+
await client.cmd("not_a_command")
|
81
|
+
except EmbeddingServiceAPIError as e:
|
82
|
+
print("[API error for invalid command]", e.error)
|
83
|
+
|
84
|
+
# Example: error handling for empty texts
|
85
|
+
try:
|
86
|
+
await client.cmd("embed", params={"texts": []})
|
87
|
+
except EmbeddingServiceAPIError as e:
|
88
|
+
print("[API error for empty texts]", e.error)
|
89
|
+
|
90
|
+
except Exception as e:
|
91
|
+
print("[Unexpected error]", e)
|
92
|
+
|
93
|
+
if __name__ == "__main__":
|
94
|
+
asyncio.run(main())
|
@@ -0,0 +1,9 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: embed-client
|
3
|
+
Version: 0.0.1
|
4
|
+
Summary: Async client for Embedding Service API
|
5
|
+
Author: Your Name
|
6
|
+
Requires-Dist: aiohttp
|
7
|
+
Provides-Extra: test
|
8
|
+
Requires-Dist: pytest; extra == "test"
|
9
|
+
Requires-Dist: pytest-asyncio; extra == "test"
|
@@ -0,0 +1,15 @@
|
|
1
|
+
README.md
|
2
|
+
pyproject.toml
|
3
|
+
embed_client/__init__.py
|
4
|
+
embed_client/async_client.py
|
5
|
+
embed_client/example_async_usage.py
|
6
|
+
embed_client/example_async_usage_ru.py
|
7
|
+
embed_client.egg-info/PKG-INFO
|
8
|
+
embed_client.egg-info/SOURCES.txt
|
9
|
+
embed_client.egg-info/dependency_links.txt
|
10
|
+
embed_client.egg-info/requires.txt
|
11
|
+
embed_client.egg-info/top_level.txt
|
12
|
+
tests/test_async_client.py
|
13
|
+
tests/test_async_client_real.py
|
14
|
+
tests/test_example_async_usage.py
|
15
|
+
tests/test_example_async_usage_ru.py
|
@@ -0,0 +1 @@
|
|
1
|
+
|
@@ -0,0 +1 @@
|
|
1
|
+
embed_client
|
@@ -0,0 +1,21 @@
|
|
1
|
+
[build-system]
|
2
|
+
requires = ["setuptools"]
|
3
|
+
build-backend = "setuptools.build_meta"
|
4
|
+
|
5
|
+
[project]
|
6
|
+
name = "embed-client"
|
7
|
+
version = "0.0.1"
|
8
|
+
description = "Async client for Embedding Service API"
|
9
|
+
authors = [{name = "Your Name"}]
|
10
|
+
dependencies = [
|
11
|
+
"aiohttp",
|
12
|
+
]
|
13
|
+
|
14
|
+
[project.optional-dependencies]
|
15
|
+
test = [
|
16
|
+
"pytest",
|
17
|
+
"pytest-asyncio",
|
18
|
+
]
|
19
|
+
|
20
|
+
[tool.pytest.ini_options]
|
21
|
+
asyncio_default_fixture_loop_scope = "function"
|
@@ -0,0 +1,241 @@
|
|
1
|
+
import pytest
|
2
|
+
import pytest_asyncio
|
3
|
+
from unittest.mock import patch, MagicMock
|
4
|
+
from embed_client.async_client import EmbeddingServiceAsyncClient, EmbeddingServiceAPIError, EmbeddingServiceHTTPError, EmbeddingServiceError, EmbeddingServiceConnectionError
|
5
|
+
|
6
|
+
BASE_URL = "http://testserver"
|
7
|
+
PORT = 1234
|
8
|
+
|
9
|
+
class MockAiohttpResponse:
|
10
|
+
def __init__(self, json_data=None, status=200, raise_http=None, text_data=None):
|
11
|
+
self._json = json_data
|
12
|
+
self._status = status
|
13
|
+
self._raise_http = raise_http
|
14
|
+
self._text = text_data
|
15
|
+
async def __aenter__(self):
|
16
|
+
return self
|
17
|
+
async def __aexit__(self, exc_type, exc, tb):
|
18
|
+
return None
|
19
|
+
async def json(self):
|
20
|
+
if self._json is not None:
|
21
|
+
return self._json
|
22
|
+
raise ValueError("No JSON")
|
23
|
+
async def text(self):
|
24
|
+
return self._text or ""
|
25
|
+
def raise_for_status(self):
|
26
|
+
if self._raise_http:
|
27
|
+
raise self._raise_http
|
28
|
+
return None
|
29
|
+
@property
|
30
|
+
def status(self):
|
31
|
+
return self._status
|
32
|
+
|
33
|
+
@pytest_asyncio.fixture
|
34
|
+
async def client():
|
35
|
+
async with EmbeddingServiceAsyncClient(base_url=BASE_URL, port=PORT) as c:
|
36
|
+
yield c
|
37
|
+
|
38
|
+
def make_url(path):
|
39
|
+
return f"{BASE_URL}:{PORT}{path}"
|
40
|
+
|
41
|
+
@pytest.mark.asyncio
|
42
|
+
async def test_health(client):
|
43
|
+
with patch.object(client._session, 'get', return_value=MockAiohttpResponse({"status": "ok"})) as mock_get:
|
44
|
+
result = await client.health()
|
45
|
+
assert result == {"status": "ok"}
|
46
|
+
mock_get.assert_called_with(make_url("/health"))
|
47
|
+
|
48
|
+
@pytest.mark.asyncio
|
49
|
+
async def test_get_openapi_schema(client):
|
50
|
+
with patch.object(client._session, 'get', return_value=MockAiohttpResponse({"openapi": "3.0.2"})) as mock_get:
|
51
|
+
result = await client.get_openapi_schema()
|
52
|
+
assert result == {"openapi": "3.0.2"}
|
53
|
+
mock_get.assert_called_with(make_url("/openapi.json"))
|
54
|
+
|
55
|
+
@pytest.mark.asyncio
|
56
|
+
async def test_get_commands(client):
|
57
|
+
with patch.object(client._session, 'get', return_value=MockAiohttpResponse({"commands": ["embed", "models"]})) as mock_get:
|
58
|
+
result = await client.get_commands()
|
59
|
+
assert result == {"commands": ["embed", "models"]}
|
60
|
+
mock_get.assert_called_with(make_url("/api/commands"))
|
61
|
+
|
62
|
+
@pytest.mark.asyncio
|
63
|
+
async def test_cmd(client):
|
64
|
+
with patch.object(client._session, 'post', return_value=MockAiohttpResponse({"result": "ok"})) as mock_post:
|
65
|
+
result = await client.cmd("embed", params={"texts": ["abc"]})
|
66
|
+
assert result == {"result": "ok"}
|
67
|
+
mock_post.assert_called_with(make_url("/cmd"), json={"command": "embed", "params": {"texts": ["abc"]}})
|
68
|
+
|
69
|
+
@pytest.mark.asyncio
|
70
|
+
async def test_init_requires_base_url_and_port():
|
71
|
+
with pytest.raises(ValueError):
|
72
|
+
EmbeddingServiceAsyncClient(base_url=None, port=PORT)
|
73
|
+
with pytest.raises(ValueError):
|
74
|
+
EmbeddingServiceAsyncClient(base_url=BASE_URL, port=None)
|
75
|
+
|
76
|
+
# Некорректные параметры: не-строка в texts
|
77
|
+
@pytest.mark.asyncio
|
78
|
+
async def test_embed_non_string_text(client):
|
79
|
+
with patch.object(client._session, 'post', return_value=MockAiohttpResponse({"error": {"code": 422, "message": "Invalid input"}})) as mock_post:
|
80
|
+
with pytest.raises(EmbeddingServiceAPIError):
|
81
|
+
await client.cmd("embed", params={"texts": [123, "ok"]})
|
82
|
+
|
83
|
+
# Некорректные параметры: невалидный params
|
84
|
+
@pytest.mark.asyncio
|
85
|
+
async def test_embed_invalid_params_type(client):
|
86
|
+
with patch.object(client._session, 'post', return_value=MockAiohttpResponse({"error": {"code": 422, "message": "Invalid params"}})) as mock_post:
|
87
|
+
with pytest.raises(EmbeddingServiceAPIError):
|
88
|
+
await client.cmd("embed", params="not_a_dict")
|
89
|
+
|
90
|
+
# Не-JSON ответ
|
91
|
+
@pytest.mark.asyncio
|
92
|
+
async def test_non_json_response(client):
|
93
|
+
class BadResponse(MockAiohttpResponse):
|
94
|
+
async def json(self):
|
95
|
+
raise ValueError("Not a JSON")
|
96
|
+
with patch.object(client._session, 'post', return_value=BadResponse(text_data="<html>error</html>")) as mock_post:
|
97
|
+
with pytest.raises(EmbeddingServiceError):
|
98
|
+
await client.cmd("embed", params={"texts": ["abc"]})
|
99
|
+
|
100
|
+
# 500 ошибка сервера
|
101
|
+
@pytest.mark.asyncio
|
102
|
+
async def test_server_500_error(client):
|
103
|
+
from aiohttp import ClientResponseError
|
104
|
+
err = ClientResponseError(request_info=None, history=None, status=500, message="Internal Server Error")
|
105
|
+
with patch.object(client._session, 'post', return_value=MockAiohttpResponse(raise_http=err, status=500)) as mock_post:
|
106
|
+
with pytest.raises(EmbeddingServiceHTTPError):
|
107
|
+
await client.cmd("embed", params={"texts": ["abc"]})
|
108
|
+
|
109
|
+
# embed без params
|
110
|
+
@pytest.mark.asyncio
|
111
|
+
async def test_embed_no_params(client):
|
112
|
+
with patch.object(client._session, 'post', return_value=MockAiohttpResponse({"error": {"code": 422, "message": "Missing params"}})) as mock_post:
|
113
|
+
with pytest.raises(EmbeddingServiceAPIError):
|
114
|
+
await client.cmd("embed")
|
115
|
+
|
116
|
+
# Вектор не той размерности (например, сервер вернул 2D массив, а ожидали 3D)
|
117
|
+
@pytest.mark.asyncio
|
118
|
+
async def test_embed_wrong_vector_shape(client):
|
119
|
+
# Ожидаем список списков, но сервер вернул список
|
120
|
+
with patch.object(client._session, 'post', return_value=MockAiohttpResponse({"embeddings": [1.0, 2.0, 3.0]})) as mock_post:
|
121
|
+
result = await client.cmd("embed", params={"texts": ["abc"]})
|
122
|
+
vectors = result["embeddings"]
|
123
|
+
assert isinstance(vectors, list)
|
124
|
+
# Проверяем, что каждый элемент — список (будет False, тест покажет ошибку)
|
125
|
+
assert all(isinstance(vec, list) for vec in vectors) is False
|
126
|
+
|
127
|
+
# Покрытие: except EmbeddingServiceHTTPError
|
128
|
+
@pytest.mark.asyncio
|
129
|
+
async def test_health_http_error(client):
|
130
|
+
with patch.object(client._session, 'get', side_effect=EmbeddingServiceHTTPError(500, "fail")):
|
131
|
+
with pytest.raises(EmbeddingServiceHTTPError):
|
132
|
+
await client.health()
|
133
|
+
|
134
|
+
# Покрытие: except EmbeddingServiceConnectionError
|
135
|
+
@pytest.mark.asyncio
|
136
|
+
async def test_health_connection_error(client):
|
137
|
+
with patch.object(client._session, 'get', side_effect=EmbeddingServiceConnectionError("fail")):
|
138
|
+
with pytest.raises(EmbeddingServiceConnectionError):
|
139
|
+
await client.health()
|
140
|
+
|
141
|
+
# Покрытие: except Exception (ValueError)
|
142
|
+
@pytest.mark.asyncio
|
143
|
+
async def test_health_unexpected_error(client):
|
144
|
+
with patch.object(client._session, 'get', side_effect=ValueError("fail")):
|
145
|
+
with pytest.raises(EmbeddingServiceError):
|
146
|
+
await client.health()
|
147
|
+
|
148
|
+
# Аналогично для get_openapi_schema
|
149
|
+
@pytest.mark.asyncio
|
150
|
+
async def test_get_openapi_schema_http_error(client):
|
151
|
+
with patch.object(client._session, 'get', side_effect=EmbeddingServiceHTTPError(500, "fail")):
|
152
|
+
with pytest.raises(EmbeddingServiceHTTPError):
|
153
|
+
await client.get_openapi_schema()
|
154
|
+
|
155
|
+
@pytest.mark.asyncio
|
156
|
+
async def test_get_openapi_schema_connection_error(client):
|
157
|
+
with patch.object(client._session, 'get', side_effect=EmbeddingServiceConnectionError("fail")):
|
158
|
+
with pytest.raises(EmbeddingServiceConnectionError):
|
159
|
+
await client.get_openapi_schema()
|
160
|
+
|
161
|
+
@pytest.mark.asyncio
|
162
|
+
async def test_get_openapi_schema_unexpected_error(client):
|
163
|
+
with patch.object(client._session, 'get', side_effect=ValueError("fail")):
|
164
|
+
with pytest.raises(EmbeddingServiceError):
|
165
|
+
await client.get_openapi_schema()
|
166
|
+
|
167
|
+
# Аналогично для get_commands
|
168
|
+
@pytest.mark.asyncio
|
169
|
+
async def test_get_commands_http_error(client):
|
170
|
+
with patch.object(client._session, 'get', side_effect=EmbeddingServiceHTTPError(500, "fail")):
|
171
|
+
with pytest.raises(EmbeddingServiceHTTPError):
|
172
|
+
await client.get_commands()
|
173
|
+
|
174
|
+
@pytest.mark.asyncio
|
175
|
+
async def test_get_commands_connection_error(client):
|
176
|
+
with patch.object(client._session, 'get', side_effect=EmbeddingServiceConnectionError("fail")):
|
177
|
+
with pytest.raises(EmbeddingServiceConnectionError):
|
178
|
+
await client.get_commands()
|
179
|
+
|
180
|
+
@pytest.mark.asyncio
|
181
|
+
async def test_get_commands_unexpected_error(client):
|
182
|
+
with patch.object(client._session, 'get', side_effect=ValueError("fail")):
|
183
|
+
with pytest.raises(EmbeddingServiceError):
|
184
|
+
await client.get_commands()
|
185
|
+
|
186
|
+
# Аналогично для cmd
|
187
|
+
@pytest.mark.asyncio
|
188
|
+
async def test_cmd_http_error(client):
|
189
|
+
with patch.object(client._session, 'post', side_effect=EmbeddingServiceHTTPError(500, "fail")):
|
190
|
+
with pytest.raises(EmbeddingServiceHTTPError):
|
191
|
+
await client.cmd("embed", params={"texts": ["abc"]})
|
192
|
+
|
193
|
+
@pytest.mark.asyncio
|
194
|
+
async def test_cmd_connection_error(client):
|
195
|
+
with patch.object(client._session, 'post', side_effect=EmbeddingServiceConnectionError("fail")):
|
196
|
+
with pytest.raises(EmbeddingServiceConnectionError):
|
197
|
+
await client.cmd("embed", params={"texts": ["abc"]})
|
198
|
+
|
199
|
+
@pytest.mark.asyncio
|
200
|
+
async def test_cmd_unexpected_error(client):
|
201
|
+
with patch.object(client._session, 'post', side_effect=ValueError("fail")):
|
202
|
+
with pytest.raises(EmbeddingServiceError):
|
203
|
+
await client.cmd("embed", params={"texts": ["abc"]})
|
204
|
+
|
205
|
+
# Покрытие: _raise_for_status - ClientResponseError
|
206
|
+
@pytest.mark.asyncio
|
207
|
+
async def test_raise_for_status_http_error():
|
208
|
+
from aiohttp import ClientResponseError
|
209
|
+
client = EmbeddingServiceAsyncClient(base_url=BASE_URL, port=PORT)
|
210
|
+
resp = MagicMock()
|
211
|
+
resp.raise_for_status.side_effect = ClientResponseError(request_info=None, history=None, status=400, message="fail")
|
212
|
+
with pytest.raises(EmbeddingServiceHTTPError):
|
213
|
+
await client._raise_for_status(resp)
|
214
|
+
|
215
|
+
# Покрытие: _raise_for_status - не ClientResponseError
|
216
|
+
@pytest.mark.asyncio
|
217
|
+
async def test_raise_for_status_other_error():
|
218
|
+
client = EmbeddingServiceAsyncClient(base_url=BASE_URL, port=PORT)
|
219
|
+
resp = MagicMock()
|
220
|
+
resp.raise_for_status.side_effect = ValueError("fail")
|
221
|
+
with pytest.raises(ValueError):
|
222
|
+
await client._raise_for_status(resp)
|
223
|
+
|
224
|
+
# Покрытие: __aenter__ и __aexit__ - ошибка при создании/закрытии сессии
|
225
|
+
@pytest.mark.asyncio
|
226
|
+
async def test_aenter_aexit_exceptions():
|
227
|
+
client = EmbeddingServiceAsyncClient(base_url=BASE_URL, port=PORT)
|
228
|
+
# Исключение при создании сессии
|
229
|
+
orig = client._session
|
230
|
+
client._session = None
|
231
|
+
with patch("aiohttp.ClientSession", side_effect=RuntimeError("fail")):
|
232
|
+
with pytest.raises(RuntimeError):
|
233
|
+
async with client:
|
234
|
+
pass
|
235
|
+
# Исключение при закрытии сессии
|
236
|
+
class BadSession:
|
237
|
+
async def close(self):
|
238
|
+
raise RuntimeError("fail")
|
239
|
+
client._session = BadSession()
|
240
|
+
with pytest.raises(RuntimeError):
|
241
|
+
await client.__aexit__(None, None, None)
|
@@ -0,0 +1,110 @@
|
|
1
|
+
import pytest
|
2
|
+
import pytest_asyncio
|
3
|
+
from embed_client.async_client import EmbeddingServiceAsyncClient, EmbeddingServiceAPIError, EmbeddingServiceHTTPError
|
4
|
+
|
5
|
+
BASE_URL = "http://localhost"
|
6
|
+
PORT = 8001
|
7
|
+
|
8
|
+
async def is_service_available():
|
9
|
+
try:
|
10
|
+
async with EmbeddingServiceAsyncClient(base_url=BASE_URL, port=PORT) as client:
|
11
|
+
await client.health()
|
12
|
+
return True
|
13
|
+
except Exception:
|
14
|
+
return False
|
15
|
+
|
16
|
+
@pytest_asyncio.fixture
|
17
|
+
async def real_client():
|
18
|
+
async with EmbeddingServiceAsyncClient(base_url=BASE_URL, port=PORT) as client:
|
19
|
+
yield client
|
20
|
+
|
21
|
+
@pytest.mark.asyncio
|
22
|
+
@pytest.mark.integration
|
23
|
+
async def test_real_health(real_client):
|
24
|
+
if not await is_service_available():
|
25
|
+
pytest.skip("Real service on localhost:8001 is not available.")
|
26
|
+
result = await real_client.health()
|
27
|
+
assert "status" in result
|
28
|
+
assert result["status"] in ("ok", "error")
|
29
|
+
|
30
|
+
@pytest.mark.asyncio
|
31
|
+
@pytest.mark.integration
|
32
|
+
async def test_real_openapi(real_client):
|
33
|
+
if not await is_service_available():
|
34
|
+
pytest.skip("Real service on localhost:8001 is not available.")
|
35
|
+
result = await real_client.get_openapi_schema()
|
36
|
+
assert "openapi" in result
|
37
|
+
assert result["openapi"].startswith("3.")
|
38
|
+
|
39
|
+
@pytest.mark.asyncio
|
40
|
+
@pytest.mark.integration
|
41
|
+
async def test_real_get_commands(real_client):
|
42
|
+
if not await is_service_available():
|
43
|
+
pytest.skip("Real service on localhost:8001 is not available.")
|
44
|
+
result = await real_client.get_commands()
|
45
|
+
assert isinstance(result, dict)
|
46
|
+
|
47
|
+
@pytest.mark.asyncio
|
48
|
+
@pytest.mark.integration
|
49
|
+
async def test_real_cmd_help(real_client):
|
50
|
+
if not await is_service_available():
|
51
|
+
pytest.skip("Real service on localhost:8001 is not available.")
|
52
|
+
result = await real_client.cmd("help")
|
53
|
+
assert isinstance(result, dict)
|
54
|
+
|
55
|
+
def extract_vectors(result):
|
56
|
+
if "embeddings" in result:
|
57
|
+
return result["embeddings"]
|
58
|
+
elif "result" in result:
|
59
|
+
if isinstance(result["result"], list):
|
60
|
+
return result["result"]
|
61
|
+
elif isinstance(result["result"], dict) and "embeddings" in result["result"]:
|
62
|
+
return result["result"]["embeddings"]
|
63
|
+
else:
|
64
|
+
pytest.fail("No embeddings in result['result']")
|
65
|
+
else:
|
66
|
+
pytest.fail("No embeddings or result in response")
|
67
|
+
|
68
|
+
@pytest.mark.asyncio
|
69
|
+
@pytest.mark.integration
|
70
|
+
async def test_real_embed_vector(real_client):
|
71
|
+
if not await is_service_available():
|
72
|
+
pytest.skip("Real service on localhost:8001 is not available.")
|
73
|
+
texts = ["hello world", "test embedding"]
|
74
|
+
params = {"texts": texts}
|
75
|
+
result = await real_client.cmd("embed", params=params)
|
76
|
+
vectors = extract_vectors(result)
|
77
|
+
assert isinstance(vectors, list)
|
78
|
+
assert len(vectors) == len(texts)
|
79
|
+
assert all(isinstance(vec, list) for vec in vectors)
|
80
|
+
assert all(isinstance(x, (float, int)) for vec in vectors for x in vec)
|
81
|
+
|
82
|
+
@pytest.mark.asyncio
|
83
|
+
@pytest.mark.integration
|
84
|
+
async def test_real_embed_empty_texts(real_client):
|
85
|
+
if not await is_service_available():
|
86
|
+
pytest.skip("Real service on localhost:8001 is not available.")
|
87
|
+
result = await real_client.cmd("embed", params={"texts": []})
|
88
|
+
vectors = extract_vectors(result)
|
89
|
+
assert isinstance(vectors, list)
|
90
|
+
assert len(vectors) == 0
|
91
|
+
|
92
|
+
@pytest.mark.asyncio
|
93
|
+
@pytest.mark.integration
|
94
|
+
async def test_real_cmd_invalid_command(real_client):
|
95
|
+
if not await is_service_available():
|
96
|
+
pytest.skip("Real service on localhost:8001 is not available.")
|
97
|
+
with pytest.raises(EmbeddingServiceAPIError):
|
98
|
+
await real_client.cmd("not_a_command")
|
99
|
+
|
100
|
+
@pytest.mark.asyncio
|
101
|
+
@pytest.mark.integration
|
102
|
+
async def test_real_invalid_endpoint():
|
103
|
+
if not await is_service_available():
|
104
|
+
pytest.skip("Real service on localhost:8001 is not available.")
|
105
|
+
async with EmbeddingServiceAsyncClient(base_url=BASE_URL, port=PORT) as client:
|
106
|
+
with pytest.raises(EmbeddingServiceHTTPError):
|
107
|
+
# Пробуем обратиться к несуществующему endpoint
|
108
|
+
url = f"{BASE_URL}:{PORT}/notfound"
|
109
|
+
async with client._session.get(url) as resp:
|
110
|
+
await client._raise_for_status(resp)
|
@@ -0,0 +1,29 @@
|
|
1
|
+
import sys
|
2
|
+
import types
|
3
|
+
import pytest
|
4
|
+
import asyncio
|
5
|
+
from unittest.mock import patch, AsyncMock
|
6
|
+
|
7
|
+
@pytest.mark.asyncio
|
8
|
+
async def test_example_async_usage(monkeypatch):
|
9
|
+
# Подменяем sys.argv для передачи base_url и порта
|
10
|
+
monkeypatch.setattr(sys, 'argv', ['example_async_usage.py', '--base-url', 'http://test', '--port', '8001'])
|
11
|
+
# Мокаем EmbeddingServiceAsyncClient и его методы
|
12
|
+
with patch('embed_client.example_async_usage.EmbeddingServiceAsyncClient.__aenter__', new=AsyncMock(return_value=AsyncMock())), \
|
13
|
+
patch('embed_client.example_async_usage.EmbeddingServiceAsyncClient.health', new=AsyncMock(return_value={"status": "ok"})):
|
14
|
+
# Импортируем как модуль, чтобы выполнить main()
|
15
|
+
import importlib
|
16
|
+
import embed_client.example_async_usage as example
|
17
|
+
importlib.reload(example)
|
18
|
+
await example.main()
|
19
|
+
|
20
|
+
@pytest.mark.asyncio
|
21
|
+
async def test_example_async_usage_no_base_url(monkeypatch):
|
22
|
+
monkeypatch.setattr(sys, 'argv', ['example_async_usage.py'])
|
23
|
+
with patch('builtins.print') as mock_print, patch('sys.exit') as mock_exit:
|
24
|
+
import importlib
|
25
|
+
import embed_client.example_async_usage as example
|
26
|
+
importlib.reload(example)
|
27
|
+
await example.main()
|
28
|
+
mock_print.assert_called()
|
29
|
+
mock_exit.assert_called()
|
@@ -0,0 +1,25 @@
|
|
1
|
+
import sys
|
2
|
+
import pytest
|
3
|
+
import asyncio
|
4
|
+
from unittest.mock import patch, AsyncMock
|
5
|
+
|
6
|
+
@pytest.mark.asyncio
|
7
|
+
async def test_example_async_usage_ru(monkeypatch):
|
8
|
+
monkeypatch.setattr(sys, 'argv', ['example_async_usage_ru.py', '--base-url', 'http://test', '--port', '8001'])
|
9
|
+
with patch('embed_client.example_async_usage_ru.EmbeddingServiceAsyncClient.__aenter__', new=AsyncMock(return_value=AsyncMock())), \
|
10
|
+
patch('embed_client.example_async_usage_ru.EmbeddingServiceAsyncClient.health', new=AsyncMock(return_value={"status": "ok"})):
|
11
|
+
import importlib
|
12
|
+
import embed_client.example_async_usage_ru as example
|
13
|
+
importlib.reload(example)
|
14
|
+
await example.main()
|
15
|
+
|
16
|
+
@pytest.mark.asyncio
|
17
|
+
async def test_example_async_usage_ru_no_base_url(monkeypatch):
|
18
|
+
monkeypatch.setattr(sys, 'argv', ['example_async_usage_ru.py'])
|
19
|
+
with patch('builtins.print') as mock_print, patch('sys.exit') as mock_exit:
|
20
|
+
import importlib
|
21
|
+
import embed_client.example_async_usage_ru as example
|
22
|
+
importlib.reload(example)
|
23
|
+
await example.main()
|
24
|
+
mock_print.assert_called()
|
25
|
+
mock_exit.assert_called()
|