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.
@@ -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,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,5 @@
1
+ aiohttp
2
+
3
+ [test]
4
+ pytest
5
+ pytest-asyncio
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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()