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.
Files changed (48) hide show
  1. embed_client/__init__.py +36 -0
  2. embed_client/__main__.py +15 -0
  3. embed_client/_version.py +3 -0
  4. embed_client/adapter_config_factory.py +213 -0
  5. embed_client/adapter_config_normalizer.py +135 -0
  6. embed_client/adapter_generator_bridge.py +166 -0
  7. embed_client/auth.py +189 -0
  8. embed_client/cli.py +483 -0
  9. embed_client/cli_core.py +427 -0
  10. embed_client/client_factory.py +515 -0
  11. embed_client/client_factory_examples.py +375 -0
  12. embed_client/client_factory_helpers.py +59 -0
  13. embed_client/config.py +408 -0
  14. embed_client/config_defaults.py +77 -0
  15. embed_client/config_examples.py +228 -0
  16. embed_client/config_generator.py +652 -0
  17. embed_client/config_generator_cli.py +151 -0
  18. embed_client/config_presets.py +245 -0
  19. embed_client/config_validator.py +246 -0
  20. embed_client/config_validator_cli.py +141 -0
  21. embed_client/constants.py +52 -0
  22. embed_client/demo_cli.py +78 -0
  23. embed_client/demo_security_cli.py +206 -0
  24. embed_client/embed_file_output_validate.py +348 -0
  25. embed_client/embed_file_status_store.py +80 -0
  26. embed_client/embedding_client.py +1929 -0
  27. embed_client/embedding_client_helpers.py +105 -0
  28. embed_client/example_async_usage.py +357 -0
  29. embed_client/example_async_usage_demo.py +282 -0
  30. embed_client/example_async_usage_ru.py +340 -0
  31. embed_client/example_async_usage_ru_demo.py +313 -0
  32. embed_client/exceptions.py +122 -0
  33. embed_client/identifier_types.py +56 -0
  34. embed_client/json_embed_file_preflight.py +128 -0
  35. embed_client/local_json_embed_file.py +73 -0
  36. embed_client/md_embed_file_prep.py +197 -0
  37. embed_client/response_normalizer.py +160 -0
  38. embed_client/response_parsers.py +219 -0
  39. embed_client/security_cli.py +200 -0
  40. embed_client/security_cli_core.py +227 -0
  41. embed_client/ssl_examples.py +278 -0
  42. embed_client/ssl_manager.py +141 -0
  43. embed_client_dev-4.0.78.dist-info/METADATA +155 -0
  44. embed_client_dev-4.0.78.dist-info/RECORD +48 -0
  45. embed_client_dev-4.0.78.dist-info/WHEEL +5 -0
  46. embed_client_dev-4.0.78.dist-info/entry_points.txt +4 -0
  47. embed_client_dev-4.0.78.dist-info/licenses/LICENSE +21 -0
  48. embed_client_dev-4.0.78.dist-info/top_level.txt +1 -0
@@ -0,0 +1,427 @@
1
+ """
2
+ Core implementation of the embed-client CLI.
3
+
4
+ Provides the `VectorizationCLI` class and configuration helpers used by the
5
+ user-facing `embed_client.cli` entry point.
6
+
7
+ Author: Vasiliy Zdanovskiy
8
+ email: vasilyvz@gmail.com
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import sys
15
+ import time
16
+ from typing import Any, Callable, Dict, List, Optional
17
+
18
+ from embed_client.embedding_client import EmbeddingClient
19
+ from embed_client.client_factory import ClientFactory
20
+ from embed_client.config import ClientConfig
21
+ from embed_client.constants import DEFAULT_POLL_INTERVAL
22
+ from embed_client.embed_file_output_validate import (
23
+ EmbedFileJobPerfReport,
24
+ format_perf_report_lines,
25
+ format_validation_checklist_lines,
26
+ validate_embed_file_job_result,
27
+ )
28
+ from embed_client.exceptions import EmbedClientError
29
+ from embed_client.json_embed_file_preflight import iter_json_array_block_texts
30
+ from embed_client.response_parsers import extract_embeddings
31
+
32
+
33
+ def _embed_file_ws_progress_stderr(msg: Dict[str, Any]) -> None:
34
+ """Print one WebSocket/status JSON object per line to stderr (embed_file wait)."""
35
+ print(json.dumps(msg, ensure_ascii=False), file=sys.stderr, flush=True)
36
+
37
+
38
+ class VectorizationCLI:
39
+ """CLI application for text vectorization using embed-client library.
40
+
41
+ This class encapsulates the core logic of the `embed-vectorize` command,
42
+ including connection management, health checks, embedding requests, and
43
+ queue operations. It relies exclusively on `EmbeddingClient`
44
+ and related helpers from the embed-client package.
45
+ """
46
+
47
+ def __init__(self) -> None:
48
+ """Initialize a new VectorizationCLI instance."""
49
+ self.client: Optional[EmbeddingClient] = None
50
+
51
+ async def connect(
52
+ self,
53
+ config: Optional[ClientConfig] = None,
54
+ config_dict: Optional[Dict[str, Any]] = None,
55
+ ) -> bool:
56
+ """Connect to the embedding service using provided configuration.
57
+
58
+ The client is created via `ClientFactory` or directly from a
59
+ `ClientConfig` / configuration dictionary. All low-level transport and
60
+ authentication details are handled by the embed-client library.
61
+
62
+ Args:
63
+ config: Optional `ClientConfig` instance.
64
+ config_dict: Optional raw configuration dictionary.
65
+
66
+ Returns:
67
+ True if connection was established successfully, False otherwise.
68
+ """
69
+ try:
70
+ if config is not None:
71
+ self.client = EmbeddingClient.from_config(config)
72
+ elif config_dict is not None:
73
+ from embed_client.embedding_client import (
74
+ embedding_client_from_config_dict,
75
+ )
76
+
77
+ self.client = embedding_client_from_config_dict(config_dict)
78
+ else:
79
+ # Use ClientFactory.from_environment() as fallback
80
+ self.client = ClientFactory.from_environment()
81
+
82
+ await self.client.__aenter__()
83
+ return True
84
+ except Exception as exc: # noqa: BLE001
85
+ print(f"❌ Failed to connect: {exc}", file=sys.stderr)
86
+ return False
87
+
88
+ async def disconnect(self) -> None:
89
+ """Close the underlying client connection if it exists."""
90
+ if self.client is not None:
91
+ await self.client.close()
92
+
93
+ async def health_check(self) -> bool:
94
+ """Perform a health check against the embedding service."""
95
+ try:
96
+ assert self.client is not None
97
+ result = await self.client.health()
98
+ print(json.dumps(result, indent=2))
99
+ return True
100
+ except Exception as exc: # noqa: BLE001
101
+ print(f"❌ Health check failed: {exc}", file=sys.stderr)
102
+ return False
103
+
104
+ async def get_models(self) -> bool:
105
+ """Fetch and print available embedding models from the service."""
106
+ try:
107
+ assert self.client is not None
108
+ result = await self.client.models()
109
+ print(json.dumps(result, indent=2))
110
+ return True
111
+ except Exception as exc: # noqa: BLE001
112
+ print(f"❌ Failed to get models: {exc}", file=sys.stderr)
113
+ return False
114
+
115
+ async def get_timing_stats(self, file_path: Optional[str] = None) -> bool:
116
+ """Fetch and print timing statistics from the service."""
117
+ try:
118
+ assert self.client is not None
119
+ result = await self.client.timing_stats(file_path=file_path)
120
+ print(json.dumps(result, indent=2))
121
+ return True
122
+ except Exception as exc: # noqa: BLE001
123
+ print(f"❌ Failed to get timing stats: {exc}", file=sys.stderr)
124
+ return False
125
+
126
+ async def embed_file_local_json(
127
+ self,
128
+ json_path: str,
129
+ *,
130
+ max_block_bytes: Optional[int] = None,
131
+ upload_filename: Optional[str] = None,
132
+ wait_timeout: Optional[float] = None,
133
+ download_to: Optional[str] = None,
134
+ skip_download: bool = False,
135
+ model: Optional[str] = None,
136
+ dimension: Optional[int] = None,
137
+ device: Optional[str] = None,
138
+ error_policy: str = "continue",
139
+ show_progress: bool = False,
140
+ ) -> bool:
141
+ """Local JSON array file → adapter upload → ``embed_file`` (json_array)."""
142
+ try:
143
+ assert self.client is not None
144
+ on_ws: Optional[Callable[[Dict[str, Any]], None]] = None
145
+ if show_progress:
146
+ on_ws = _embed_file_ws_progress_stderr
147
+ ef_kw: Dict[str, Any] = {
148
+ "max_block_bytes": max_block_bytes,
149
+ "upload_filename": upload_filename,
150
+ "wait": True,
151
+ "download_to": download_to,
152
+ "skip_download": skip_download,
153
+ "model": model,
154
+ "dimension": dimension,
155
+ "device": device,
156
+ "error_policy": error_policy,
157
+ "on_ws_event": on_ws,
158
+ }
159
+ if wait_timeout is not None:
160
+ ef_kw["wait_timeout"] = int(wait_timeout)
161
+ blocks = iter_json_array_block_texts(json_path)
162
+ t0 = time.perf_counter()
163
+ result = await self.client.embed_file_from_local_json_file(
164
+ json_path,
165
+ **ef_kw,
166
+ )
167
+ elapsed = time.perf_counter() - t0
168
+ perf = EmbedFileJobPerfReport(
169
+ total_seconds=elapsed, block_count=len(blocks)
170
+ )
171
+ for ln in format_perf_report_lines(perf):
172
+ print(ln, file=sys.stderr, flush=True)
173
+ try:
174
+ vreport = validate_embed_file_job_result(
175
+ blocks, result, rpc_dimension=dimension
176
+ )
177
+ except EmbedClientError as ve:
178
+ print(f"❌ embed_file output validation: {ve}", file=sys.stderr)
179
+ print(json.dumps(result, indent=2))
180
+ return False
181
+ for ln in format_validation_checklist_lines(vreport):
182
+ print(ln, file=sys.stderr, flush=True)
183
+ if not vreport.ok:
184
+ print("❌ embed_file output validation failed.", file=sys.stderr)
185
+ print(json.dumps(result, indent=2))
186
+ return False
187
+ print(json.dumps(result, indent=2))
188
+ return True
189
+ except Exception as exc: # noqa: BLE001
190
+ print(f"❌ embed-file-json failed: {exc}", file=sys.stderr)
191
+ return False
192
+
193
+ async def vectorize_texts(
194
+ self,
195
+ texts: List[str],
196
+ output_format: str = "json",
197
+ show_full_data: bool = False,
198
+ timeout: Optional[float] = None,
199
+ model: Optional[str] = None,
200
+ dimension: Optional[int] = None,
201
+ error_policy: str = "continue",
202
+ device: Optional[str] = None,
203
+ ) -> Optional[List[List[float]]]:
204
+ """Vectorize texts using the high-level `embed` helper.
205
+
206
+ Notes:
207
+ - Default ``error_policy=\"continue\"`` so per-item errors are
208
+ exposed via ``data[\"results\"][i][\"error\"]``.
209
+ - All input validation is delegated to the server.
210
+
211
+ Args:
212
+ texts: List of input texts to embed.
213
+ output_format: One of ``\"json\"``, ``\"csv\"``, or ``\"vectors\"``.
214
+ show_full_data: If True, print full embedding payload instead of
215
+ only vectors.
216
+ timeout: Maximum time to wait for vectorization completion in seconds.
217
+ If None, return immediately without waiting.
218
+ If 0, wait indefinitely (no timeout).
219
+ If > 0, wait up to specified seconds.
220
+ model: Optional model name (server-side).
221
+ dimension: Optional expected vector dimension (server-side).
222
+ error_policy: "continue" or "fail_fast" (default: "continue").
223
+ device: Optional device: "auto", "cpu", "cuda", "cuda:0" (server-side).
224
+
225
+ Returns:
226
+ A list of embedding vectors on success (possibly empty when
227
+ `show_full_data` is True and no plain vectors are available),
228
+ or ``None`` on failure.
229
+ """
230
+ try:
231
+ assert self.client is not None
232
+
233
+ wt = 60 if timeout is None else int(timeout)
234
+ data = await self.client.embed(
235
+ texts,
236
+ model=model,
237
+ dimension=dimension,
238
+ error_policy=error_policy,
239
+ device=device,
240
+ wait=True,
241
+ wait_timeout=wt,
242
+ )
243
+
244
+ if show_full_data:
245
+ if output_format == "json":
246
+ print(json.dumps(data, indent=2))
247
+ return data.get("embeddings") or []
248
+ embeddings = data.get("embeddings") or []
249
+ if output_format == "csv":
250
+ self._print_csv(embeddings)
251
+ elif output_format == "vectors":
252
+ self._print_vectors(embeddings)
253
+ return embeddings
254
+
255
+ embeddings = data.get("embeddings")
256
+ if embeddings is None:
257
+ # Fallback to response_parsers for legacy-style responses
258
+ wrapped: Dict[str, Any] = {"result": {"success": True, "data": data}}
259
+ embeddings = extract_embeddings(wrapped)
260
+
261
+ if output_format == "json":
262
+ print(json.dumps(embeddings, indent=2))
263
+ elif output_format == "csv":
264
+ self._print_csv(embeddings)
265
+ elif output_format == "vectors":
266
+ self._print_vectors(embeddings)
267
+
268
+ return embeddings
269
+ except Exception as exc: # noqa: BLE001
270
+ print(f"❌ Vectorization failed: {exc}", file=sys.stderr)
271
+ return None
272
+
273
+ def _print_csv(self, embeddings: List[List[float]]) -> None:
274
+ """Print embeddings in CSV format."""
275
+ for idx, embedding in enumerate(embeddings):
276
+ print(f"text_{idx}," + ",".join(map(str, embedding)))
277
+
278
+ def _print_vectors(self, embeddings: List[List[float]]) -> None:
279
+ """Print embeddings as human-readable vectors."""
280
+ for idx, embedding in enumerate(embeddings):
281
+ print(f"Text {idx}: [{', '.join(map(str, embedding))}]")
282
+
283
+ async def get_help(self, command: Optional[str] = None) -> bool:
284
+ """Fetch help information from the embedding service."""
285
+ try:
286
+ assert self.client is not None
287
+ if command:
288
+ result = await self.client.cmd("help", params={"command": command})
289
+ else:
290
+ result = await self.client.cmd("help")
291
+ print(json.dumps(result, indent=2))
292
+ return True
293
+ except Exception as exc: # noqa: BLE001
294
+ print(f"❌ Help request failed: {exc}", file=sys.stderr)
295
+ return False
296
+
297
+ async def get_commands(self) -> bool:
298
+ """List all available commands from the embedding service."""
299
+ try:
300
+ assert self.client is not None
301
+ result = await self.client.list_commands()
302
+ print(json.dumps(result, indent=2))
303
+ return True
304
+ except Exception as exc: # noqa: BLE001
305
+ print(f"❌ Commands request failed: {exc}", file=sys.stderr)
306
+ return False
307
+
308
+ async def queue_list(
309
+ self, status: Optional[str] = None, limit: Optional[int] = None
310
+ ) -> bool:
311
+ """List queued commands."""
312
+ try:
313
+ assert self.client is not None
314
+ result = await self.client.list_queued_commands(status=status, limit=limit)
315
+ print(json.dumps(result, indent=2))
316
+ return True
317
+ except Exception as exc: # noqa: BLE001
318
+ print(f"❌ Failed to list queue: {exc}", file=sys.stderr)
319
+ return False
320
+
321
+ async def queue_status(self, job_id: str) -> bool:
322
+ """Get the status of a specific job."""
323
+ try:
324
+ assert self.client is not None
325
+ result = await self.client.job_status(job_id)
326
+ print(json.dumps(result, indent=2))
327
+ return True
328
+ except Exception as exc: # noqa: BLE001
329
+ print(f"❌ Failed to get job status: {exc}", file=sys.stderr)
330
+ return False
331
+
332
+ async def queue_cancel(self, job_id: str) -> bool:
333
+ """Cancel a queued or running job."""
334
+ try:
335
+ assert self.client is not None
336
+ result = await self.client.cancel_command(job_id)
337
+ print(json.dumps(result, indent=2))
338
+ return True
339
+ except Exception as exc: # noqa: BLE001
340
+ print(f"❌ Failed to cancel job: {exc}", file=sys.stderr)
341
+ return False
342
+
343
+ async def queue_wait(
344
+ self,
345
+ job_id: str,
346
+ timeout: float = 60.0,
347
+ poll_interval: float = DEFAULT_POLL_INTERVAL,
348
+ ) -> bool:
349
+ """Wait for completion of a specific job."""
350
+ try:
351
+ assert self.client is not None
352
+ result = await self.client.wait_for_job(
353
+ job_id, timeout=timeout, poll_interval=poll_interval
354
+ )
355
+ print(json.dumps(result, indent=2))
356
+ return True
357
+ except Exception as exc: # noqa: BLE001
358
+ print(f"❌ Failed to wait for job: {exc}", file=sys.stderr)
359
+ return False
360
+
361
+ async def queue_logs(self, job_id: str) -> bool:
362
+ """Fetch logs for a specific job."""
363
+ try:
364
+ assert self.client is not None
365
+ result = await self.client.get_job_logs(job_id)
366
+ print(json.dumps(result, indent=2))
367
+ return True
368
+ except Exception as exc: # noqa: BLE001
369
+ print(f"❌ Failed to get job logs: {exc}", file=sys.stderr)
370
+ return False
371
+
372
+
373
+ def create_config_from_args(args: Any) -> Dict[str, Any]:
374
+ """Create a configuration dictionary from parsed CLI arguments.
375
+
376
+ This helper mirrors the shape of the JSON configuration files used by
377
+ `ClientConfig` and `ClientFactory`, allowing the CLI to construct a
378
+ `ClientConfig` instance without requiring callers to manage JSON files.
379
+
380
+ The ``--insecure`` (``--no-check-hostname``) flag maps to
381
+ ``ssl.check_hostname = False``, which the library propagates as
382
+ ``check_hostname=False`` to both the HTTPS transport and the WebSocket
383
+ (wss) SSL context. Default (no flag) keeps strict TLS verification.
384
+ """
385
+ insecure = bool(getattr(args, "insecure", False))
386
+ config: Dict[str, Any] = {
387
+ "server": {"host": args.host, "port": args.port},
388
+ "auth": {"method": "none"},
389
+ "ssl": {"enabled": False, "check_hostname": not insecure},
390
+ "client": {"timeout": getattr(args, "timeout", 30.0)},
391
+ }
392
+
393
+ # Add authentication if specified
394
+ if getattr(args, "api_key", None):
395
+ config["auth"]["method"] = "api_key"
396
+ config["auth"]["api_keys"] = {"user": args.api_key}
397
+ if getattr(args, "api_key_header", None):
398
+ config["auth"]["api_key_header"] = args.api_key_header
399
+ elif getattr(args, "jwt_secret", None):
400
+ config["auth"]["method"] = "jwt"
401
+ config["auth"]["jwt"] = {
402
+ "secret": args.jwt_secret,
403
+ "username": getattr(args, "jwt_username", ""),
404
+ "password": getattr(args, "jwt_password", ""),
405
+ }
406
+ elif getattr(args, "basic_username", None):
407
+ config["auth"]["method"] = "basic"
408
+ config["auth"]["basic"] = {
409
+ "username": args.basic_username,
410
+ "password": getattr(args, "basic_password", ""),
411
+ }
412
+
413
+ # Add SSL if specified
414
+ if (
415
+ getattr(args, "ssl", False)
416
+ or getattr(args, "cert_file", None)
417
+ or getattr(args, "key_file", None)
418
+ ):
419
+ config["ssl"]["enabled"] = True
420
+ if getattr(args, "cert_file", None):
421
+ config["ssl"]["cert_file"] = args.cert_file
422
+ if getattr(args, "key_file", None):
423
+ config["ssl"]["key_file"] = args.key_file
424
+ if getattr(args, "ca_cert_file", None):
425
+ config["ssl"]["ca_cert_file"] = args.ca_cert_file
426
+
427
+ return config