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,348 @@
1
+ """
2
+ Validate ``embed_file`` downloaded JSON against local ``json_array`` input.
3
+
4
+ Aligned with ``../embed`` incremental writer shape:
5
+ ``{"success": true, "data": {"model", "dimension", "device?", "results": [...]}}``.
6
+
7
+ Author: Vasiliy Zdanovskiy
8
+ email: vasilyvz@gmail.com
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import os
15
+ from dataclasses import dataclass, field
16
+ from typing import Any, Dict, List, Optional, Tuple
17
+
18
+ from embed_client.exceptions import EmbedClientError
19
+
20
+
21
+ def _is_meaningful_error(err: Any) -> bool:
22
+ if err is None:
23
+ return False
24
+ if isinstance(err, str):
25
+ return bool(err.strip())
26
+ if isinstance(err, dict):
27
+ return bool(str(err.get("message", "")).strip()) or bool(err)
28
+ return True
29
+
30
+
31
+ def load_embed_file_output_json_file(path: str) -> Dict[str, Any]:
32
+ """Load UTF-8 JSON object from a local file (downloaded embed_file output)."""
33
+ ap = os.path.abspath(path)
34
+ if not os.path.isfile(ap):
35
+ raise EmbedClientError(f"embed_file output file not found: {ap}")
36
+ if not os.access(ap, os.R_OK):
37
+ raise EmbedClientError(f"embed_file output file not readable: {ap}")
38
+ try:
39
+ with open(ap, encoding="utf-8-sig") as f:
40
+ raw = f.read()
41
+ except OSError as e:
42
+ raise EmbedClientError(f"cannot read embed_file output: {ap}: {e}") from e
43
+ try:
44
+ obj: Any = json.loads(raw)
45
+ except json.JSONDecodeError as e:
46
+ raise EmbedClientError(
47
+ f"invalid JSON in embed_file output file {ap}: {e}"
48
+ ) from e
49
+ if not isinstance(obj, dict):
50
+ raise EmbedClientError(
51
+ f"embed_file output JSON root must be an object, got {type(obj).__name__}"
52
+ )
53
+ return obj
54
+
55
+
56
+ def _inner_data_dict(payload: Dict[str, Any]) -> Optional[Dict[str, Any]]:
57
+ d = payload.get("data")
58
+ return d if isinstance(d, dict) else None
59
+
60
+
61
+ def parse_embed_file_output_records(
62
+ payload: Dict[str, Any],
63
+ ) -> Tuple[List[Dict[str, Any]], Optional[int], Optional[str]]:
64
+ """
65
+ Extract ``results`` rows and ``dimension`` from a downloaded or inline payload.
66
+
67
+ Supports:
68
+ - Canonical: ``success`` + ``data.results`` + ``data.dimension``
69
+ - ``data.results`` without top-level ``success``
70
+ - Top-level ``results`` only (tests / simplified files)
71
+ """
72
+ inner = _inner_data_dict(payload)
73
+ if inner is not None and isinstance(inner.get("results"), list):
74
+ dim = inner.get("dimension")
75
+ dim_i = int(dim) if isinstance(dim, int) else None
76
+ rows = inner["results"]
77
+ if not all(isinstance(r, dict) for r in rows):
78
+ raise EmbedClientError("embed_file data.results must be a list of objects")
79
+ return rows, dim_i, "data.results"
80
+
81
+ if isinstance(payload.get("results"), list):
82
+ dim = payload.get("dimension")
83
+ dim_i = int(dim) if isinstance(dim, int) else None
84
+ rows = payload["results"]
85
+ if not all(isinstance(r, dict) for r in rows):
86
+ raise EmbedClientError("embed_file results must be a list of objects")
87
+ return rows, dim_i, "results"
88
+
89
+ raise EmbedClientError(
90
+ "unrecognized embed_file output shape: expected data.results or top-level results"
91
+ )
92
+
93
+
94
+ def resolve_embed_file_output_payload(
95
+ embed_result: Dict[str, Any],
96
+ ) -> Tuple[Dict[str, Any], str]:
97
+ """
98
+ Load JSON payload from ``local_output_path`` or parse inline structure in *embed_result*.
99
+
100
+ Returns ``(payload_dict, source_description)`` for checklist reporting.
101
+ """
102
+ path = embed_result.get("local_output_path")
103
+ if isinstance(path, str) and path.strip():
104
+ ap = os.path.abspath(path.strip())
105
+ if os.path.isfile(ap):
106
+ pl = load_embed_file_output_json_file(ap)
107
+ return pl, f"local_output_path file ({ap})"
108
+
109
+ # Inline: full document nested under merged job data (unusual but supported)
110
+ for key in ("embed_file_output", "output_json"):
111
+ v = embed_result.get(key)
112
+ if isinstance(v, dict) and (
113
+ _inner_data_dict(v) is not None or isinstance(v.get("results"), list)
114
+ ):
115
+ return v, f"inline key {key!r}"
116
+
117
+ # Try canonical nested under result (adapter echo)
118
+ res = embed_result.get("result")
119
+ if isinstance(res, dict):
120
+ inner = res.get("data") if isinstance(res.get("data"), dict) else res
121
+ if isinstance(inner, dict) and (
122
+ isinstance(inner.get("results"), list)
123
+ or (
124
+ isinstance(inner.get("data"), dict)
125
+ and isinstance(inner["data"].get("results"), list)
126
+ )
127
+ ):
128
+ pl = res if "success" in res else {"success": True, "data": inner}
129
+ return pl, "embed_result.result"
130
+
131
+ raise EmbedClientError(
132
+ "cannot validate embed_file output: no readable local_output_path and no "
133
+ "inline results payload in the job result"
134
+ )
135
+
136
+
137
+ @dataclass
138
+ class EmbedFileOutputValidationReport:
139
+ """Structured validation outcome + human-readable checklist lines."""
140
+
141
+ ok: bool
142
+ checklist_lines: List[str] = field(default_factory=list)
143
+ errors: List[str] = field(default_factory=list)
144
+
145
+
146
+ def _expected_dimension(
147
+ *,
148
+ payload_dim: Optional[int],
149
+ rpc_dimension: Optional[int],
150
+ ) -> Optional[int]:
151
+ if rpc_dimension is not None:
152
+ return int(rpc_dimension)
153
+ return payload_dim
154
+
155
+
156
+ def validate_embed_file_output_against_blocks(
157
+ input_block_texts: List[str],
158
+ payload: Dict[str, Any],
159
+ *,
160
+ rpc_dimension: Optional[int] = None,
161
+ ) -> EmbedFileOutputValidationReport:
162
+ """
163
+ Check record count, non-empty embeddings (or per-row error), dimension, and
164
+ ``body`` order vs *input_block_texts* when bodies are present.
165
+ """
166
+ lines: List[str] = []
167
+ errors: List[str] = []
168
+
169
+ if payload.get("success") is False:
170
+ errors.append("payload.success is false in embed_file output document")
171
+ lines.append("FAIL: payload.success is false.")
172
+ return EmbedFileOutputValidationReport(
173
+ ok=False, checklist_lines=lines, errors=errors
174
+ )
175
+
176
+ rows, payload_dim, path_hint = parse_embed_file_output_records(payload)
177
+ lines.append(f"Parsed output records from {path_hint}.")
178
+ dim = _expected_dimension(payload_dim=payload_dim, rpc_dimension=rpc_dimension)
179
+ if dim is not None:
180
+ lines.append(f"Expected embedding dimension: {dim} (RPC or payload).")
181
+ else:
182
+ lines.append(
183
+ "Expected embedding dimension: not specified (skipping length checks)."
184
+ )
185
+
186
+ n_in = len(input_block_texts)
187
+ n_out = len(rows)
188
+ if n_out != n_in:
189
+ msg = f"record count mismatch: output has {n_out} rows, input has {n_in} blocks"
190
+ errors.append(msg)
191
+ lines.append(f"FAIL: {msg}")
192
+ else:
193
+ lines.append(f"Record count matches input blocks ({n_in}).")
194
+
195
+ ordering_warn: Optional[str] = None
196
+ for i, row in enumerate(rows):
197
+ emb = row.get("embedding")
198
+ err = row.get("error")
199
+ body = row.get("body")
200
+ if isinstance(emb, list):
201
+ if len(emb) == 0:
202
+ errors.append(f"row {i}: empty embedding list")
203
+ elif dim is not None and len(emb) != dim:
204
+ errors.append(
205
+ f"row {i}: embedding length {len(emb)} != expected dimension {dim}"
206
+ )
207
+ elif not _is_meaningful_error(err):
208
+ errors.append(
209
+ f"row {i}: missing embedding and no meaningful error "
210
+ f"(got embedding={type(emb).__name__!r})"
211
+ )
212
+ if isinstance(body, str) and i < n_in:
213
+ if body != input_block_texts[i]:
214
+ ordering_warn = (
215
+ f"row {i}: body text does not match input block at same index "
216
+ "(possible reorder or transform)"
217
+ )
218
+
219
+ if ordering_warn:
220
+ errors.append(ordering_warn)
221
+ lines.append(f"ORDERING: {ordering_warn}")
222
+ else:
223
+ comparable = sum(
224
+ 1
225
+ for i, row in enumerate(rows)
226
+ if i < n_in and isinstance(row.get("body"), str)
227
+ )
228
+ if comparable == n_in and n_in > 0:
229
+ lines.append("Body text matches input block order (index-wise).")
230
+ elif n_in > 0:
231
+ lines.append(
232
+ "Body fields absent or not comparable; skipped strict order check."
233
+ )
234
+
235
+ ok = len(errors) == 0
236
+ if ok:
237
+ lines.append("All embedding / error-field checks passed for parsed rows.")
238
+ else:
239
+ lines.append(f"Validation failed with {len(errors)} issue(s).")
240
+
241
+ return EmbedFileOutputValidationReport(ok=ok, checklist_lines=lines, errors=errors)
242
+
243
+
244
+ def validate_embed_file_job_result(
245
+ input_block_texts: List[str],
246
+ embed_result: Dict[str, Any],
247
+ *,
248
+ rpc_dimension: Optional[int] = None,
249
+ ) -> EmbedFileOutputValidationReport:
250
+ """
251
+ Resolve payload from *embed_result*, then run
252
+ :func:`validate_embed_file_output_against_blocks`.
253
+ """
254
+ checklist: List[str] = []
255
+ payload, src = resolve_embed_file_output_payload(embed_result)
256
+ checklist.append(f"Artifact source: {src}.")
257
+ inner = validate_embed_file_output_against_blocks(
258
+ input_block_texts,
259
+ payload,
260
+ rpc_dimension=rpc_dimension,
261
+ )
262
+ checklist.extend(inner.checklist_lines)
263
+ return EmbedFileOutputValidationReport(
264
+ ok=inner.ok,
265
+ checklist_lines=checklist,
266
+ errors=list(inner.errors),
267
+ )
268
+
269
+
270
+ @dataclass
271
+ class EmbedFileJobPerfReport:
272
+ """Wall-clock metrics for a single embed_file_from_local_json_file run."""
273
+
274
+ total_seconds: float
275
+ block_count: int
276
+ note: str = (
277
+ "Single perf_counter interval covers upload, queue wait, download, and ACK; "
278
+ "per-block timings are not available from this client."
279
+ )
280
+
281
+ @property
282
+ def mean_seconds_per_block(self) -> float:
283
+ if self.block_count <= 0:
284
+ return 0.0
285
+ return self.total_seconds / float(self.block_count)
286
+
287
+ @property
288
+ def median_seconds_per_block(self) -> float:
289
+ # One interval → median time per block equals mean (total/N).
290
+ return self.mean_seconds_per_block
291
+
292
+ @property
293
+ def blocks_per_second(self) -> float:
294
+ if self.total_seconds <= 0:
295
+ return 0.0
296
+ return float(self.block_count) / self.total_seconds
297
+
298
+ @property
299
+ def mean_blocks_per_second(self) -> float:
300
+ return self.blocks_per_second
301
+
302
+ @property
303
+ def median_blocks_per_second(self) -> float:
304
+ return self.blocks_per_second
305
+
306
+
307
+ def format_perf_report_lines(report: EmbedFileJobPerfReport) -> List[str]:
308
+ """Return printable lines for stderr or stdout."""
309
+ lines = [
310
+ "--- embed_file job performance ---",
311
+ report.note,
312
+ f"Total wall time: {report.total_seconds:.6f} s",
313
+ f"Input blocks (N): {report.block_count}",
314
+ f"Throughput: {report.blocks_per_second:.6f} blocks/s",
315
+ f"Mean time per block: {report.mean_seconds_per_block:.6f} s/block "
316
+ "(total_seconds / N; only one wall-clock interval measured).",
317
+ f"Median time per block: {report.median_seconds_per_block:.6f} s/block "
318
+ "(equals mean when per-block timings are unavailable).",
319
+ f"Mean speed: {report.mean_blocks_per_second:.6f} blocks/s",
320
+ f"Median speed: {report.median_blocks_per_second:.6f} blocks/s",
321
+ ]
322
+ return lines
323
+
324
+
325
+ def format_validation_checklist_lines(
326
+ report: EmbedFileOutputValidationReport,
327
+ ) -> List[str]:
328
+ """Prefix lines for clear CLI / example output."""
329
+ out = ["--- embed_file output validation ---"]
330
+ out.extend(report.checklist_lines)
331
+ if not report.ok:
332
+ out.append("Issues:")
333
+ for e in report.errors:
334
+ out.append(f" - {e}")
335
+ return out
336
+
337
+
338
+ __all__ = [
339
+ "EmbedFileJobPerfReport",
340
+ "EmbedFileOutputValidationReport",
341
+ "format_perf_report_lines",
342
+ "format_validation_checklist_lines",
343
+ "load_embed_file_output_json_file",
344
+ "parse_embed_file_output_records",
345
+ "resolve_embed_file_output_payload",
346
+ "validate_embed_file_job_result",
347
+ "validate_embed_file_output_against_blocks",
348
+ ]
@@ -0,0 +1,80 @@
1
+ """
2
+ In-memory embed_file job status store (per EmbeddingClient instance).
3
+
4
+ Author: Vasiliy Zdanovskiy
5
+ email: vasilyvz@gmail.com
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import copy
11
+ import threading
12
+ import time
13
+ from typing import Any, Dict, List, Optional
14
+
15
+
16
+ class EmbedFileJobStatusStore:
17
+ """
18
+ Thread-safe accumulation of embed_file RPC submit payload and WebSocket frames.
19
+
20
+ Keys are ``job_id`` strings. Each entry may include ``submit``, ``messages``,
21
+ ``last_event``, and timestamps. Payloads are deep-copied on write.
22
+ """
23
+
24
+ def __init__(self) -> None:
25
+ self._lock = threading.Lock()
26
+ self._jobs: Dict[str, Dict[str, Any]] = {}
27
+
28
+ def record_submit(self, job_id: str, submit_payload: Dict[str, Any]) -> None:
29
+ """Record the initial ``embed_file`` RPC success payload for ``job_id``."""
30
+ with self._lock:
31
+ entry = self._jobs.setdefault(
32
+ job_id,
33
+ {
34
+ "job_id": job_id,
35
+ "messages": [],
36
+ },
37
+ )
38
+ entry["submit"] = copy.deepcopy(submit_payload)
39
+ entry["submit_at"] = time.time()
40
+
41
+ def record_message(self, job_id: str, event: Dict[str, Any]) -> None:
42
+ """Append one WebSocket JSON payload (queue job subscription) for ``job_id``."""
43
+ with self._lock:
44
+ entry = self._jobs.setdefault(
45
+ job_id,
46
+ {
47
+ "job_id": job_id,
48
+ "messages": [],
49
+ },
50
+ )
51
+ entry["messages"].append(copy.deepcopy(event))
52
+ entry["last_event"] = copy.deepcopy(event)
53
+ entry["last_message_at"] = time.time()
54
+
55
+ def snapshot(self, job_id: str) -> Optional[Dict[str, Any]]:
56
+ """Return a deep copy of stored state for ``job_id``, or ``None`` if unknown."""
57
+ with self._lock:
58
+ if job_id not in self._jobs:
59
+ return None
60
+ return copy.deepcopy(self._jobs[job_id])
61
+
62
+ def messages(self, job_id: str) -> List[Dict[str, Any]]:
63
+ """Return a deep copy of accumulated WS messages for ``job_id``."""
64
+ with self._lock:
65
+ if job_id not in self._jobs:
66
+ return []
67
+ return copy.deepcopy(self._jobs[job_id].get("messages", []))
68
+
69
+ def clear(self, job_id: str) -> None:
70
+ """Remove stored data for ``job_id``."""
71
+ with self._lock:
72
+ self._jobs.pop(job_id, None)
73
+
74
+ def job_ids(self) -> List[str]:
75
+ """List all ``job_id`` keys currently present."""
76
+ with self._lock:
77
+ return list(self._jobs.keys())
78
+
79
+
80
+ __all__ = ["EmbedFileJobStatusStore"]