embed-client-dev 4.0.78__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.
- embed_client/__init__.py +36 -0
- embed_client/__main__.py +15 -0
- embed_client/_version.py +3 -0
- embed_client/adapter_config_factory.py +213 -0
- embed_client/adapter_config_normalizer.py +135 -0
- embed_client/adapter_generator_bridge.py +166 -0
- embed_client/auth.py +189 -0
- embed_client/cli.py +483 -0
- embed_client/cli_core.py +427 -0
- embed_client/client_factory.py +515 -0
- embed_client/client_factory_examples.py +375 -0
- embed_client/client_factory_helpers.py +59 -0
- embed_client/config.py +408 -0
- embed_client/config_defaults.py +77 -0
- embed_client/config_examples.py +228 -0
- embed_client/config_generator.py +652 -0
- embed_client/config_generator_cli.py +151 -0
- embed_client/config_presets.py +245 -0
- embed_client/config_validator.py +246 -0
- embed_client/config_validator_cli.py +141 -0
- embed_client/constants.py +52 -0
- embed_client/demo_cli.py +78 -0
- embed_client/demo_security_cli.py +206 -0
- embed_client/embed_file_output_validate.py +348 -0
- embed_client/embed_file_status_store.py +80 -0
- embed_client/embedding_client.py +1929 -0
- embed_client/embedding_client_helpers.py +105 -0
- embed_client/example_async_usage.py +357 -0
- embed_client/example_async_usage_demo.py +282 -0
- embed_client/example_async_usage_ru.py +340 -0
- embed_client/example_async_usage_ru_demo.py +313 -0
- embed_client/exceptions.py +122 -0
- embed_client/identifier_types.py +56 -0
- embed_client/json_embed_file_preflight.py +128 -0
- embed_client/local_json_embed_file.py +73 -0
- embed_client/md_embed_file_prep.py +197 -0
- embed_client/response_normalizer.py +160 -0
- embed_client/response_parsers.py +219 -0
- embed_client/security_cli.py +200 -0
- embed_client/security_cli_core.py +227 -0
- embed_client/ssl_examples.py +278 -0
- embed_client/ssl_manager.py +141 -0
- embed_client_dev-4.0.78.dist-info/METADATA +155 -0
- embed_client_dev-4.0.78.dist-info/RECORD +48 -0
- embed_client_dev-4.0.78.dist-info/WHEEL +5 -0
- embed_client_dev-4.0.78.dist-info/entry_points.txt +4 -0
- embed_client_dev-4.0.78.dist-info/licenses/LICENSE +21 -0
- embed_client_dev-4.0.78.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Вспомогательные демонстрационные функции для ``embed_client/example_async_usage_ru.py``.
|
|
3
|
+
|
|
4
|
+
Author: Vasiliy Zdanovskiy
|
|
5
|
+
email: vasilyvz@gmail.com
|
|
6
|
+
|
|
7
|
+
**run_client_examples** — короткий тур: health, OpenAPI, команды, ``cmd('embed')``.
|
|
8
|
+
|
|
9
|
+
**demonstrate_security_modes** и связанные функции — печать сценариев
|
|
10
|
+
``ClientFactory`` без обязательного живого сервера.
|
|
11
|
+
|
|
12
|
+
Используется :class:`~embed_client.embedding_client.EmbeddingClient` и фабрика.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import ssl
|
|
18
|
+
from typing import Any, Dict, List
|
|
19
|
+
|
|
20
|
+
from embed_client.embedding_client import EmbeddingClient
|
|
21
|
+
from embed_client.exceptions import EmbedError
|
|
22
|
+
from embed_client.client_factory import ClientFactory, detect_security_mode
|
|
23
|
+
from embed_client.response_parsers import extract_embeddings
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
async def run_client_examples(client: EmbeddingClient) -> None:
|
|
27
|
+
"""
|
|
28
|
+
Короткий интерактивный тур (health, OpenAPI, список команд, embed).
|
|
29
|
+
|
|
30
|
+
При ошибке health выводит сообщение и завершает функцию; остальные шаги ловят свои исключения.
|
|
31
|
+
"""
|
|
32
|
+
# Проверка здоровья сервиса
|
|
33
|
+
try:
|
|
34
|
+
health = await client.health()
|
|
35
|
+
print("Состояние сервиса:", health)
|
|
36
|
+
except EmbedError as exc:
|
|
37
|
+
print(f"Ошибка при проверке состояния сервиса: {exc}")
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
# Получение OpenAPI схемы
|
|
41
|
+
try:
|
|
42
|
+
schema = await client.fetch_openapi_schema()
|
|
43
|
+
version = schema.get("info", {}).get("version", "unknown")
|
|
44
|
+
print("Версия OpenAPI схемы:", version)
|
|
45
|
+
except EmbedError as exc:
|
|
46
|
+
print(f"Ошибка при получении OpenAPI схемы: {exc}")
|
|
47
|
+
|
|
48
|
+
# Получение списка доступных команд
|
|
49
|
+
try:
|
|
50
|
+
commands = await client.list_commands()
|
|
51
|
+
print(f"Доступные команды: {commands}")
|
|
52
|
+
except EmbedError as exc:
|
|
53
|
+
print(f"Ошибка при получении списка команд: {exc}")
|
|
54
|
+
|
|
55
|
+
# Генерация эмбеддингов
|
|
56
|
+
try:
|
|
57
|
+
texts = [
|
|
58
|
+
"Привет, мир!",
|
|
59
|
+
"Это тестовое предложение.",
|
|
60
|
+
"Сервис эмбеддингов работает!",
|
|
61
|
+
]
|
|
62
|
+
result = await client.cmd("embed", {"texts": texts})
|
|
63
|
+
|
|
64
|
+
embeddings = extract_embeddings(result)
|
|
65
|
+
count = len(embeddings) if embeddings is not None else 0
|
|
66
|
+
print(f"Сгенерировано эмбеддингов: {count}")
|
|
67
|
+
first_dim = (
|
|
68
|
+
len(embeddings[0])
|
|
69
|
+
if embeddings and len(embeddings) > 0 and embeddings[0] is not None
|
|
70
|
+
else 0
|
|
71
|
+
)
|
|
72
|
+
print("Размерность первого эмбеддинга:", first_dim)
|
|
73
|
+
except EmbedError as exc:
|
|
74
|
+
print(f"Ошибка при генерации эмбеддингов: {exc}")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
async def demonstrate_security_modes() -> None:
|
|
78
|
+
"""
|
|
79
|
+
Печать шести шаблонов создания клиентов через ``ClientFactory`` (HTTP … mTLS+roles).
|
|
80
|
+
|
|
81
|
+
Не подключается к серверу сама по себе.
|
|
82
|
+
"""
|
|
83
|
+
print("=== Демонстрация режимов безопасности ===")
|
|
84
|
+
print("Этот пример показывает, как создавать клиентов для всех 6 режимов.")
|
|
85
|
+
print(
|
|
86
|
+
"Важно: примеры создают конфигурации клиентов, но не подключаются "
|
|
87
|
+
"к реальным серверам."
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# 1. HTTP
|
|
91
|
+
print("\n1. HTTP режим (без аутентификации, без SSL):")
|
|
92
|
+
print(" Использование: разработка, внутренние сети, доверенная среда")
|
|
93
|
+
try:
|
|
94
|
+
client = ClientFactory.create_http_client("http://localhost", 8001)
|
|
95
|
+
print(f" ✓ HTTP клиент: {client.base_url}:{client.port}")
|
|
96
|
+
print(f" ✓ SSL включён: {client.is_ssl_enabled()}")
|
|
97
|
+
print(f" ✓ Аутентифицирован: {client.is_authenticated()}")
|
|
98
|
+
print(f" ✓ Метод аутентификации: {client.get_auth_method()}")
|
|
99
|
+
await client.close()
|
|
100
|
+
except Exception as exc: # noqa: BLE001
|
|
101
|
+
print(f" ✗ Ошибка: {exc}")
|
|
102
|
+
|
|
103
|
+
# 2. HTTP + Token
|
|
104
|
+
print("\n2. HTTP + Token (HTTP с API ключом):")
|
|
105
|
+
print(" Использование: управление доступом к API, простая аутентификация")
|
|
106
|
+
try:
|
|
107
|
+
client = ClientFactory.create_http_token_client(
|
|
108
|
+
"http://localhost", 8001, "api_key", api_key="demo_key"
|
|
109
|
+
)
|
|
110
|
+
print(f" ✓ HTTP + Token клиент: {client.base_url}:{client.port}")
|
|
111
|
+
print(f" ✓ SSL включён: {client.is_ssl_enabled()}")
|
|
112
|
+
print(f" ✓ Аутентифицирован: {client.is_authenticated()}")
|
|
113
|
+
print(f" ✓ Метод аутентификации: {client.get_auth_method()}")
|
|
114
|
+
if client.is_authenticated():
|
|
115
|
+
headers = client.get_auth_headers()
|
|
116
|
+
print(f" ✓ Заголовки аутентификации: {headers}")
|
|
117
|
+
await client.close()
|
|
118
|
+
except Exception as exc: # noqa: BLE001
|
|
119
|
+
print(f" ✗ Ошибка: {exc}")
|
|
120
|
+
|
|
121
|
+
# 3. HTTPS
|
|
122
|
+
print("\n3. HTTPS режим (шифрование с сертификатами сервера):")
|
|
123
|
+
print(" Использование: защищённое соединение, публичные сети")
|
|
124
|
+
try:
|
|
125
|
+
client = ClientFactory.create_https_client("https://localhost", 9443)
|
|
126
|
+
print(f" ✓ HTTPS клиент: {client.base_url}:{client.port}")
|
|
127
|
+
print(f" ✓ SSL включён: {client.is_ssl_enabled()}")
|
|
128
|
+
print(f" ✓ Аутентифицирован: {client.is_authenticated()}")
|
|
129
|
+
if client.is_ssl_enabled():
|
|
130
|
+
ssl_config = client.get_ssl_config()
|
|
131
|
+
print(f" ✓ SSL конфигурация: {ssl_config}")
|
|
132
|
+
print(f" ✓ OpenSSL / TLS: {ssl.OPENSSL_VERSION}")
|
|
133
|
+
await client.close()
|
|
134
|
+
except Exception as exc: # noqa: BLE001
|
|
135
|
+
print(f" ✗ Ошибка: {exc}")
|
|
136
|
+
|
|
137
|
+
# 4. HTTPS + Token
|
|
138
|
+
print("\n4. HTTPS + Token (HTTPS + аутентификация):")
|
|
139
|
+
print(" Использование: защищённый доступ к API, production")
|
|
140
|
+
try:
|
|
141
|
+
client = ClientFactory.create_https_token_client(
|
|
142
|
+
"https://localhost",
|
|
143
|
+
9443,
|
|
144
|
+
"basic",
|
|
145
|
+
username="admin",
|
|
146
|
+
password="secret",
|
|
147
|
+
)
|
|
148
|
+
print(f" ✓ HTTPS + Token клиент: {client.base_url}:{client.port}")
|
|
149
|
+
print(f" ✓ SSL включён: {client.is_ssl_enabled()}")
|
|
150
|
+
print(f" ✓ Аутентифицирован: {client.is_authenticated()}")
|
|
151
|
+
print(f" ✓ Метод аутентификации: {client.get_auth_method()}")
|
|
152
|
+
if client.is_authenticated():
|
|
153
|
+
headers = client.get_auth_headers()
|
|
154
|
+
print(f" ✓ Заголовки аутентификации: {headers}")
|
|
155
|
+
await client.close()
|
|
156
|
+
except Exception as exc: # noqa: BLE001
|
|
157
|
+
print(f" ✗ Ошибка: {exc}")
|
|
158
|
+
|
|
159
|
+
# 5. mTLS
|
|
160
|
+
print("\n5. mTLS (взаимная TLS, сертификаты клиента и сервера):")
|
|
161
|
+
print(" Использование: максимальная безопасность, аутентификация клиента")
|
|
162
|
+
try:
|
|
163
|
+
client = ClientFactory.create_mtls_client(
|
|
164
|
+
"https://localhost",
|
|
165
|
+
"mtls_certificates/client/embedding-service.crt",
|
|
166
|
+
"mtls_certificates/client/embedding-service.key",
|
|
167
|
+
8443,
|
|
168
|
+
)
|
|
169
|
+
print(f" ✓ mTLS клиент: {client.base_url}:{client.port}")
|
|
170
|
+
print(f" ✓ SSL включён: {client.is_ssl_enabled()}")
|
|
171
|
+
print(f" ✓ mTLS включён: {client.is_mtls_enabled()}")
|
|
172
|
+
print(f" ✓ Аутентифицирован: {client.is_authenticated()}")
|
|
173
|
+
if client.is_ssl_enabled():
|
|
174
|
+
ssl_config = client.get_ssl_config()
|
|
175
|
+
print(f" ✓ SSL конфигурация: {ssl_config}")
|
|
176
|
+
await client.close()
|
|
177
|
+
except Exception as exc: # noqa: BLE001
|
|
178
|
+
print(f" ✗ Ошибка: {exc}")
|
|
179
|
+
|
|
180
|
+
# 6. mTLS + Roles
|
|
181
|
+
print("\n6. mTLS + Roles (mTLS + роли):")
|
|
182
|
+
print(" Использование: корпоративная безопасность, права доступа по ролям")
|
|
183
|
+
try:
|
|
184
|
+
client = ClientFactory.create_mtls_roles_client(
|
|
185
|
+
"https://localhost",
|
|
186
|
+
"mtls_certificates/client/embedding-service.crt",
|
|
187
|
+
"mtls_certificates/client/embedding-service.key",
|
|
188
|
+
8443,
|
|
189
|
+
roles=["admin", "user"],
|
|
190
|
+
role_attributes={"department": "IT"},
|
|
191
|
+
)
|
|
192
|
+
print(f" ✓ mTLS + Roles клиент: {client.base_url}:{client.port}")
|
|
193
|
+
print(f" ✓ SSL включён: {client.is_ssl_enabled()}")
|
|
194
|
+
print(f" ✓ mTLS включён: {client.is_mtls_enabled()}")
|
|
195
|
+
print(f" ✓ Аутентифицирован: {client.is_authenticated()}")
|
|
196
|
+
if client.is_authenticated():
|
|
197
|
+
headers = client.get_auth_headers()
|
|
198
|
+
print(f" ✓ Заголовки аутентификации: {headers}")
|
|
199
|
+
await client.close()
|
|
200
|
+
except Exception as exc: # noqa: BLE001
|
|
201
|
+
print(f" ✗ Ошибка: {exc}")
|
|
202
|
+
|
|
203
|
+
print("\n=== Краткое резюме режимов безопасности ===")
|
|
204
|
+
print("1. HTTP: базовая связность, без безопасности")
|
|
205
|
+
print("2. HTTP + Token: API ключ поверх HTTP")
|
|
206
|
+
print("3. HTTPS: шифрование с сертификатами сервера")
|
|
207
|
+
print("4. HTTPS + Token: шифрование + аутентификация")
|
|
208
|
+
print("5. mTLS: взаимная аутентификация сертификатами")
|
|
209
|
+
print("6. mTLS + Roles: сертификаты + контроль доступа по ролям")
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
async def demonstrate_automatic_detection() -> None:
|
|
213
|
+
"""Демонстрация автоматического определения режима безопасности."""
|
|
214
|
+
print("\n=== Автоматическое определение режима безопасности ===")
|
|
215
|
+
print(
|
|
216
|
+
"Этот пример показывает, как клиент автоматически определяет "
|
|
217
|
+
"подходящий режим безопасности."
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
test_cases: List[Any] = [
|
|
221
|
+
("http://localhost", None, None, None, None, "HTTP"),
|
|
222
|
+
("http://localhost", "api_key", None, None, None, "HTTP + Token"),
|
|
223
|
+
("https://localhost", None, None, None, None, "HTTPS"),
|
|
224
|
+
("https://localhost", "api_key", None, None, None, "HTTPS + Token"),
|
|
225
|
+
("https://localhost", None, None, "cert.pem", "key.pem", "mTLS"),
|
|
226
|
+
(
|
|
227
|
+
"https://localhost",
|
|
228
|
+
None,
|
|
229
|
+
None,
|
|
230
|
+
"cert.pem",
|
|
231
|
+
"key.pem",
|
|
232
|
+
"mTLS + Roles",
|
|
233
|
+
{"roles": ["admin"]},
|
|
234
|
+
),
|
|
235
|
+
]
|
|
236
|
+
|
|
237
|
+
for case in test_cases:
|
|
238
|
+
if len(case) == 6:
|
|
239
|
+
base_url, auth_method, ssl_enabled, cert_file, key_file, expected = case
|
|
240
|
+
kwargs: Dict[str, Any] = {}
|
|
241
|
+
else:
|
|
242
|
+
(
|
|
243
|
+
base_url,
|
|
244
|
+
auth_method,
|
|
245
|
+
ssl_enabled,
|
|
246
|
+
cert_file,
|
|
247
|
+
key_file,
|
|
248
|
+
expected,
|
|
249
|
+
kwargs,
|
|
250
|
+
) = case
|
|
251
|
+
|
|
252
|
+
try:
|
|
253
|
+
mode = detect_security_mode(
|
|
254
|
+
base_url, auth_method, ssl_enabled, cert_file, key_file, **kwargs
|
|
255
|
+
)
|
|
256
|
+
print(
|
|
257
|
+
f" ✓ {base_url} + {auth_method or 'none'} + "
|
|
258
|
+
f"{cert_file or 'no cert'} -> {mode} ({expected})"
|
|
259
|
+
)
|
|
260
|
+
except Exception as exc: # noqa: BLE001
|
|
261
|
+
print(f" ✗ Ошибка при определении режима для {base_url}: {exc}")
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
async def demonstrate_with_auth_method() -> None:
|
|
265
|
+
"""Демонстрация метода with_auth для динамической аутентификации."""
|
|
266
|
+
print("\n=== Динамическая аутентификация с помощью with_auth() ===")
|
|
267
|
+
print(
|
|
268
|
+
"Пример показывает, как создавать клиентов с разными методами "
|
|
269
|
+
"аутентификации, используя метод класса with_auth."
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
auth_examples = [
|
|
273
|
+
("api_key", {"api_key": "dynamic_api_key"}, "API Key аутентификация"),
|
|
274
|
+
(
|
|
275
|
+
"jwt",
|
|
276
|
+
{"secret": "secret", "username": "user", "password": "pass"},
|
|
277
|
+
"JWT аутентификация",
|
|
278
|
+
),
|
|
279
|
+
("basic", {"username": "admin", "password": "secret"}, "Basic аутентификация"),
|
|
280
|
+
(
|
|
281
|
+
"certificate",
|
|
282
|
+
{"cert_file": "client.crt", "key_file": "client.key"},
|
|
283
|
+
"Certificate аутентификация",
|
|
284
|
+
),
|
|
285
|
+
]
|
|
286
|
+
|
|
287
|
+
for auth_method, kwargs, description in auth_examples:
|
|
288
|
+
try:
|
|
289
|
+
print(f"\n{description}:")
|
|
290
|
+
auth_client = EmbeddingClient.with_auth(
|
|
291
|
+
"http://localhost",
|
|
292
|
+
8001,
|
|
293
|
+
auth_method,
|
|
294
|
+
**kwargs,
|
|
295
|
+
)
|
|
296
|
+
print(f" ✓ Метод аутентификации: {auth_client.get_auth_method()}")
|
|
297
|
+
print(f" ✓ Аутентифицирован: {auth_client.is_authenticated()}")
|
|
298
|
+
if auth_client.is_authenticated():
|
|
299
|
+
headers = auth_client.get_auth_headers()
|
|
300
|
+
print(f" ✓ Заголовки аутентификации: {headers}")
|
|
301
|
+
await auth_client.close()
|
|
302
|
+
except Exception as exc: # noqa: BLE001
|
|
303
|
+
print(f" ✗ Ошибка для {auth_method}: {exc}")
|
|
304
|
+
|
|
305
|
+
print("\n✓ Демонстрация динамической аутентификации завершена.")
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
__all__ = [
|
|
309
|
+
"run_client_examples",
|
|
310
|
+
"demonstrate_security_modes",
|
|
311
|
+
"demonstrate_automatic_detection",
|
|
312
|
+
"demonstrate_with_auth_method",
|
|
313
|
+
]
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Project-wide custom exceptions for embed-client (aligned with embedding service server).
|
|
3
|
+
|
|
4
|
+
Author: Vasiliy Zdanovskiy
|
|
5
|
+
email: vasilyvz@gmail.com
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import copy
|
|
9
|
+
from typing import Any, Optional
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class EmbedError(Exception):
|
|
13
|
+
"""Base exception for all embed package errors."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, message: str, *args: object, **kwargs: object) -> None:
|
|
16
|
+
super().__init__(message, *args, **kwargs)
|
|
17
|
+
self.message = message
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class EmbedConfigError(EmbedError):
|
|
21
|
+
"""Config load, parse or validation failure."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class EmbedQueueError(EmbedError):
|
|
25
|
+
"""Queue not initialized, submit failed, or job not found in queue."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class EmbedClientError(EmbedError):
|
|
29
|
+
"""
|
|
30
|
+
Client-facing failure: embed failed, job not found, status check failed,
|
|
31
|
+
or models list failed.
|
|
32
|
+
|
|
33
|
+
Attributes:
|
|
34
|
+
error: Deep copy of the original terminal error object as received (a
|
|
35
|
+
dict, or a str when the producer sent a string); ``None`` when the
|
|
36
|
+
failure carries none.
|
|
37
|
+
code: ``error["code"]`` when ``error`` is a dict with a ``code`` key,
|
|
38
|
+
else ``None``.
|
|
39
|
+
data: Deep copy of ``error["data"]`` when ``error`` is a dict with a
|
|
40
|
+
``data`` key, else ``None``.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
message: str,
|
|
46
|
+
*args: object,
|
|
47
|
+
error: Optional[Any] = None,
|
|
48
|
+
**kwargs: object,
|
|
49
|
+
) -> None:
|
|
50
|
+
super().__init__(message, *args, **kwargs)
|
|
51
|
+
self.error: Any = copy.deepcopy(error)
|
|
52
|
+
self.code: Any = None
|
|
53
|
+
self.data: Any = None
|
|
54
|
+
if isinstance(self.error, dict):
|
|
55
|
+
self.code = self.error.get("code")
|
|
56
|
+
if "data" in self.error:
|
|
57
|
+
self.data = copy.deepcopy(self.error["data"])
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class EmbedServiceError(EmbedError):
|
|
61
|
+
"""Generic service or runtime failure (e.g. model load, encode, I/O)."""
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class ModelServerUnavailableError(EmbedError):
|
|
65
|
+
"""Model server health check failed or connection error; use direct service."""
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class EmbedGpuOutOfMemoryError(EmbedError):
|
|
69
|
+
"""
|
|
70
|
+
GPU ran out of memory while loading or running the embedding model.
|
|
71
|
+
User message suggests using CPU (embedding.device=cpu) or freeing GPU memory.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class EmbedHTTPError(EmbedClientError):
|
|
76
|
+
"""
|
|
77
|
+
HTTP layer error (4xx, 5xx) from transport or adapter.
|
|
78
|
+
|
|
79
|
+
Attributes:
|
|
80
|
+
status: HTTP status code returned by the server.
|
|
81
|
+
message: Short human-readable message (same as historical client).
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
def __init__(self, status: int, message: str) -> None:
|
|
85
|
+
super().__init__(f"HTTP {status}: {message}")
|
|
86
|
+
self.status = status
|
|
87
|
+
self.message = message
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class EmbedAPIError(EmbedClientError):
|
|
91
|
+
"""
|
|
92
|
+
Error returned by the Embedding Service API in the response body.
|
|
93
|
+
|
|
94
|
+
The raw error payload from the API is stored in the ``error`` attribute.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
def __init__(self, error: Any) -> None:
|
|
98
|
+
super().__init__(f"API error: {error}")
|
|
99
|
+
self.error = error
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class EmbedTimeoutError(EmbedClientError):
|
|
103
|
+
"""Request to the Embedding Service timed out."""
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class EmbedJSONError(EmbedClientError):
|
|
107
|
+
"""JSON parsing of a response body failed."""
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
__all__ = [
|
|
111
|
+
"EmbedError",
|
|
112
|
+
"EmbedConfigError",
|
|
113
|
+
"EmbedQueueError",
|
|
114
|
+
"EmbedClientError",
|
|
115
|
+
"EmbedServiceError",
|
|
116
|
+
"ModelServerUnavailableError",
|
|
117
|
+
"EmbedGpuOutOfMemoryError",
|
|
118
|
+
"EmbedHTTPError",
|
|
119
|
+
"EmbedAPIError",
|
|
120
|
+
"EmbedTimeoutError",
|
|
121
|
+
"EmbedJSONError",
|
|
122
|
+
]
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Type definitions and validation functions for identifier types.
|
|
3
|
+
|
|
4
|
+
This module provides type-safe identifiers to distinguish between:
|
|
5
|
+
- JobId: Queue job identifiers from server queue operations
|
|
6
|
+
- DeliverId: Adapter-managed WebSocket delivery/correlation identifiers
|
|
7
|
+
|
|
8
|
+
Author: Vasiliy Zdanovskiy
|
|
9
|
+
email: vasilyvz@gmail.com
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import NewType
|
|
15
|
+
|
|
16
|
+
JobId = NewType("JobId", str)
|
|
17
|
+
DeliverId = NewType("DeliverId", str)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def is_valid_job_id(identifier: str) -> bool:
|
|
21
|
+
"""
|
|
22
|
+
Validate that a string is a valid job_id.
|
|
23
|
+
|
|
24
|
+
A valid job_id is a non-empty string after stripping whitespace.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
identifier: String to validate.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
True if identifier is a non-empty string after stripping, False otherwise.
|
|
31
|
+
"""
|
|
32
|
+
if not isinstance(identifier, str):
|
|
33
|
+
return False
|
|
34
|
+
stripped = identifier.strip()
|
|
35
|
+
return len(stripped) > 0
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def is_valid_deliver_id(identifier: str) -> bool:
|
|
39
|
+
"""
|
|
40
|
+
Validate that a string is a valid deliver_id.
|
|
41
|
+
|
|
42
|
+
A valid deliver_id is a non-empty string after stripping whitespace.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
identifier: String to validate.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
True if identifier is a non-empty string after stripping, False otherwise.
|
|
49
|
+
"""
|
|
50
|
+
if not isinstance(identifier, str):
|
|
51
|
+
return False
|
|
52
|
+
stripped = identifier.strip()
|
|
53
|
+
return len(stripped) > 0
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
__all__ = ["JobId", "DeliverId", "is_valid_job_id", "is_valid_deliver_id"]
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Client-side validation for local JSON files used with ``embed_file`` (``json_array``).
|
|
3
|
+
|
|
4
|
+
Author: Vasiliy Zdanovskiy
|
|
5
|
+
email: vasilyvz@gmail.com
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
from typing import Any, List, Optional
|
|
13
|
+
|
|
14
|
+
from embed_client.constants import DEFAULT_MAX_JSON_EMBED_BLOCK_BYTES
|
|
15
|
+
from embed_client.exceptions import EmbedClientError
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def resolve_max_json_block_bytes(explicit: Optional[int] = None) -> int:
|
|
19
|
+
"""
|
|
20
|
+
Return max UTF-8 byte length per ``json_array`` item (one string per array element).
|
|
21
|
+
|
|
22
|
+
Order: *explicit* if set, else ``EMBED_CLIENT_MAX_JSON_BLOCK_BYTES``, else
|
|
23
|
+
:data:`DEFAULT_MAX_JSON_EMBED_BLOCK_BYTES`.
|
|
24
|
+
"""
|
|
25
|
+
if explicit is not None:
|
|
26
|
+
if explicit < 1:
|
|
27
|
+
raise EmbedClientError("max_block_bytes must be >= 1")
|
|
28
|
+
return explicit
|
|
29
|
+
raw = os.environ.get("EMBED_CLIENT_MAX_JSON_BLOCK_BYTES", "").strip()
|
|
30
|
+
if raw:
|
|
31
|
+
try:
|
|
32
|
+
v = int(raw, 10)
|
|
33
|
+
except ValueError as e:
|
|
34
|
+
raise EmbedClientError(
|
|
35
|
+
"EMBED_CLIENT_MAX_JSON_BLOCK_BYTES must be a positive integer"
|
|
36
|
+
) from e
|
|
37
|
+
if v < 1:
|
|
38
|
+
raise EmbedClientError("EMBED_CLIENT_MAX_JSON_BLOCK_BYTES must be >= 1")
|
|
39
|
+
return v
|
|
40
|
+
return DEFAULT_MAX_JSON_EMBED_BLOCK_BYTES
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def validate_local_json_embed_file(
|
|
44
|
+
path: str, *, max_block_bytes: Optional[int] = None
|
|
45
|
+
) -> None:
|
|
46
|
+
"""
|
|
47
|
+
Ensure *path* is a readable file, content is JSON, root is a non-empty array
|
|
48
|
+
of strings (UTF-8 text per item, as the server ``embed_file`` ``json_array``
|
|
49
|
+
format expects); each item's UTF-8 size is within the configured maximum.
|
|
50
|
+
|
|
51
|
+
Raises:
|
|
52
|
+
EmbedClientError: On missing file, non-JSON content, or schema/size violations.
|
|
53
|
+
"""
|
|
54
|
+
if not path or not path.strip():
|
|
55
|
+
raise EmbedClientError("local JSON embed file path is empty")
|
|
56
|
+
ap = os.path.abspath(path)
|
|
57
|
+
if not os.path.isfile(ap):
|
|
58
|
+
raise EmbedClientError(f"not a file or not found: {ap}")
|
|
59
|
+
if not os.access(ap, os.R_OK):
|
|
60
|
+
raise EmbedClientError(f"file not readable: {ap}")
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
with open(ap, "rb") as f:
|
|
64
|
+
raw = f.read()
|
|
65
|
+
except OSError as e:
|
|
66
|
+
raise EmbedClientError(f"cannot read file: {ap}: {e}") from e
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
text = raw.decode("utf-8-sig")
|
|
70
|
+
except UnicodeDecodeError as e:
|
|
71
|
+
raise EmbedClientError(f"file must be UTF-8 (with optional BOM): {ap}") from e
|
|
72
|
+
|
|
73
|
+
stripped = text.lstrip()
|
|
74
|
+
if not stripped:
|
|
75
|
+
raise EmbedClientError(f"file is empty: {ap}")
|
|
76
|
+
if stripped[0] != "[":
|
|
77
|
+
raise EmbedClientError(
|
|
78
|
+
f"json_array embed file must be a JSON array at root (starts with '['): {ap}"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
data: Any = json.loads(text)
|
|
83
|
+
except json.JSONDecodeError as e:
|
|
84
|
+
raise EmbedClientError(f"invalid JSON in {ap}: {e}") from e
|
|
85
|
+
|
|
86
|
+
if not isinstance(data, list):
|
|
87
|
+
raise EmbedClientError(
|
|
88
|
+
f"JSON root must be an array for json_array format: {ap}"
|
|
89
|
+
)
|
|
90
|
+
if len(data) == 0:
|
|
91
|
+
raise EmbedClientError(f"JSON array must not be empty: {ap}")
|
|
92
|
+
|
|
93
|
+
max_b = resolve_max_json_block_bytes(max_block_bytes)
|
|
94
|
+
for i, item in enumerate(data):
|
|
95
|
+
if not isinstance(item, str):
|
|
96
|
+
raise EmbedClientError(
|
|
97
|
+
f"item {i} must be a string (UTF-8 text to embed), "
|
|
98
|
+
f"got {type(item).__name__}: {ap}"
|
|
99
|
+
)
|
|
100
|
+
nbytes = len(item.encode("utf-8"))
|
|
101
|
+
if nbytes > max_b:
|
|
102
|
+
raise EmbedClientError(
|
|
103
|
+
f"item {i} exceeds max block size ({nbytes} > {max_b} bytes): {ap}"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def iter_json_array_block_texts(path: str) -> List[str]:
|
|
108
|
+
"""
|
|
109
|
+
After :func:`validate_local_json_embed_file`, load and return array strings
|
|
110
|
+
in order (for tests / introspection).
|
|
111
|
+
"""
|
|
112
|
+
with open(path, "rb") as f:
|
|
113
|
+
raw = f.read()
|
|
114
|
+
text = raw.decode("utf-8-sig")
|
|
115
|
+
data = json.loads(text)
|
|
116
|
+
assert isinstance(data, list)
|
|
117
|
+
out: List[str] = []
|
|
118
|
+
for item in data:
|
|
119
|
+
assert isinstance(item, str)
|
|
120
|
+
out.append(item)
|
|
121
|
+
return out
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
__all__ = [
|
|
125
|
+
"iter_json_array_block_texts",
|
|
126
|
+
"resolve_max_json_block_bytes",
|
|
127
|
+
"validate_local_json_embed_file",
|
|
128
|
+
]
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Upload a validated local JSON file, resolve the server-side storage path, and run
|
|
3
|
+
:meth:`EmbeddingClient.embed_file` with ``input_file`` set to that path (same pattern
|
|
4
|
+
as the reference ``upload_file`` → ``get_upload_session`` → ``embed_file`` flow).
|
|
5
|
+
|
|
6
|
+
Author: Vasiliy Zdanovskiy
|
|
7
|
+
email: vasilyvz@gmail.com
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
from typing import TYPE_CHECKING, Any, Dict, Optional
|
|
14
|
+
|
|
15
|
+
from embed_client.exceptions import EmbedClientError
|
|
16
|
+
from embed_client.json_embed_file_preflight import validate_local_json_embed_file
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from embed_client.embedding_client import EmbeddingClient
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
async def embed_file_from_local_json_file(
|
|
23
|
+
client: "EmbeddingClient",
|
|
24
|
+
local_path: str,
|
|
25
|
+
*,
|
|
26
|
+
max_block_bytes: Optional[int] = None,
|
|
27
|
+
upload_filename: Optional[str] = None,
|
|
28
|
+
**embed_file_kwargs: Any,
|
|
29
|
+
) -> Dict[str, Any]:
|
|
30
|
+
"""
|
|
31
|
+
Validate a local ``json_array`` file, upload via the adapter, resolve
|
|
32
|
+
``storage_path`` via ``JsonRpcClient.get_upload_session``, then
|
|
33
|
+
:meth:`~embed_client.embedding_client.EmbeddingClient.embed_file` with
|
|
34
|
+
``input_file`` set to that server-visible path and ``format=json_array``.
|
|
35
|
+
|
|
36
|
+
``local_source_path`` is set to *local_path* so optional server-side input
|
|
37
|
+
checksum verification can match the uploaded bytes.
|
|
38
|
+
"""
|
|
39
|
+
validate_local_json_embed_file(local_path, max_block_bytes=max_block_bytes)
|
|
40
|
+
ap = os.path.abspath(local_path)
|
|
41
|
+
receipt = await client.upload_file(ap, filename=upload_filename)
|
|
42
|
+
if receipt.completed is not True:
|
|
43
|
+
raise EmbedClientError(
|
|
44
|
+
"adapter upload did not complete before embed_file "
|
|
45
|
+
f"(transfer_id={receipt.transfer_id!r})"
|
|
46
|
+
)
|
|
47
|
+
session = await client.client.get_upload_session(receipt.transfer_id)
|
|
48
|
+
if not isinstance(session, dict):
|
|
49
|
+
raise EmbedClientError(
|
|
50
|
+
"get_upload_session did not return a mapping; cannot run embed_file "
|
|
51
|
+
f"(transfer_id={receipt.transfer_id!r})"
|
|
52
|
+
)
|
|
53
|
+
storage_path = session.get("storage_path")
|
|
54
|
+
if not isinstance(storage_path, str) or not storage_path.strip():
|
|
55
|
+
raise EmbedClientError(
|
|
56
|
+
"upload session did not return storage_path; cannot run embed_file "
|
|
57
|
+
f"(transfer_id={receipt.transfer_id!r})"
|
|
58
|
+
)
|
|
59
|
+
server_input = storage_path.strip()
|
|
60
|
+
# Drop kwargs that would collide with our contract
|
|
61
|
+
embed_file_kwargs.pop("file_format", None)
|
|
62
|
+
embed_file_kwargs.pop("input_file", None)
|
|
63
|
+
embed_file_kwargs.pop("inbound_transfer", None)
|
|
64
|
+
embed_file_kwargs.pop("local_source_path", None)
|
|
65
|
+
return await client.embed_file(
|
|
66
|
+
server_input,
|
|
67
|
+
local_source_path=ap,
|
|
68
|
+
file_format="json_array",
|
|
69
|
+
**embed_file_kwargs,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
__all__ = ["embed_file_from_local_json_file"]
|