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,197 @@
1
+ """
2
+ Prepare Markdown (or plain text) files as UTF-8 ``json_array`` inputs for ``embed_file``.
3
+
4
+ Reads a ``.md`` file, optionally strips YAML front matter between the first pair of
5
+ ``---`` lines, splits the body on blank lines (``\\n\\n``) into paragraph blocks,
6
+ enforces a per-block UTF-8 byte cap via
7
+ :func:`~embed_client.json_embed_file_preflight.resolve_max_json_block_bytes`,
8
+ and writes a top-level JSON array of strings suitable for
9
+ :func:`~embed_client.local_json_embed_file.embed_file_from_local_json_file`.
10
+
11
+ Author: Vasiliy Zdanovskiy
12
+ email: vasilyvz@gmail.com
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import os
19
+ from typing import Any, List, Optional
20
+
21
+ from embed_client.exceptions import EmbedClientError
22
+ from embed_client.json_embed_file_preflight import (
23
+ resolve_max_json_block_bytes,
24
+ validate_local_json_embed_file,
25
+ )
26
+
27
+
28
+ def strip_optional_yaml_front_matter(text: str) -> str:
29
+ """
30
+ If *text* begins with a line ``---`` (after optional BOM, already decoded),
31
+ return the substring after the next line that is exactly ``---`` (stripped).
32
+ Otherwise return *text* unchanged.
33
+
34
+ If an opening ``---`` is present but no closing ``---`` line is found, returns
35
+ *text* unchanged (no strip).
36
+ """
37
+ if not text:
38
+ return text
39
+ lines = text.splitlines(keepends=True)
40
+ if not lines:
41
+ return text
42
+ if lines[0].strip() != "---":
43
+ return text
44
+ for i in range(1, len(lines)):
45
+ if lines[i].strip() == "---":
46
+ return "".join(lines[i + 1 :])
47
+ return text
48
+
49
+
50
+ def split_body_into_text_blocks(body: str, max_block_bytes: int) -> List[str]:
51
+ """
52
+ Split *body* on double newlines (paragraph boundaries) only.
53
+
54
+ Line endings are normalized to ``\\n`` (``\\r\\n`` and lone ``\\r`` → ``\\n``)
55
+ before splitting on the two-character separator ``\\n\\n``. Each segment is
56
+ stripped of leading/trailing whitespace, and segments empty after strip are
57
+ dropped (runs of blank lines collapse to one boundary).
58
+
59
+ Each non-empty block must encode to at most *max_block_bytes* UTF-8 bytes.
60
+ If any block exceeds the cap, raises :class:`~embed_client.exceptions.EmbedClientError`
61
+ (no byte-level re-chunking).
62
+ """
63
+ if max_block_bytes < 1:
64
+ raise EmbedClientError("max_block_bytes must be >= 1")
65
+ normalized = body.replace("\r\n", "\n").replace("\r", "\n")
66
+ paragraphs = normalized.split("\n\n")
67
+ out: List[str] = []
68
+ for raw in paragraphs:
69
+ block = raw.strip()
70
+ if not block:
71
+ continue
72
+ encoded = block.encode("utf-8")
73
+ if len(encoded) > max_block_bytes:
74
+ raise EmbedClientError(
75
+ "paragraph exceeds max_block_bytes after split on blank lines "
76
+ f"({len(encoded)} bytes > {max_block_bytes}); shorten the paragraph "
77
+ "or increase --max-block-bytes / EMBED_CLIENT_MAX_JSON_BLOCK_BYTES"
78
+ )
79
+ out.append(block)
80
+ return out
81
+
82
+
83
+ def build_embed_file_json_array_from_markdown_text(
84
+ text: str,
85
+ *,
86
+ max_block_bytes: Optional[int] = None,
87
+ strip_front_matter: bool = True,
88
+ max_chunks: Optional[int] = None,
89
+ ) -> List[str]:
90
+ """
91
+ Return a non-empty list of strings (``json_array`` items) from Markdown/plain text.
92
+
93
+ Raises:
94
+ EmbedClientError: Empty output, or size/schema issues after splitting.
95
+ """
96
+ max_b = resolve_max_json_block_bytes(max_block_bytes)
97
+ body = strip_optional_yaml_front_matter(text) if strip_front_matter else text
98
+ blocks = split_body_into_text_blocks(body, max_b)
99
+ if max_chunks is not None:
100
+ if max_chunks < 1:
101
+ raise EmbedClientError("max_chunks must be >= 1 when set")
102
+ blocks = blocks[:max_chunks]
103
+ if not blocks:
104
+ raise EmbedClientError(
105
+ "no non-empty text blocks after preparation (empty file or body?)"
106
+ )
107
+ return list(blocks)
108
+
109
+
110
+ def prepare_markdown_path_to_json_file(
111
+ md_path: str,
112
+ output_path: str,
113
+ *,
114
+ max_block_bytes: Optional[int] = None,
115
+ strip_front_matter: bool = True,
116
+ max_chunks: Optional[int] = None,
117
+ ) -> str:
118
+ """
119
+ Read UTF-8 (with optional BOM) from *md_path*, build ``json_array``, write *output_path*,
120
+ then run :func:`~embed_client.json_embed_file_preflight.validate_local_json_embed_file`
121
+ on the result.
122
+
123
+ Returns:
124
+ Absolute path to *output_path*.
125
+ """
126
+ if not md_path or not md_path.strip():
127
+ raise EmbedClientError("markdown input path is empty")
128
+ ap_in = os.path.abspath(md_path)
129
+ if not os.path.isfile(ap_in):
130
+ raise EmbedClientError(f"not a file or not found: {ap_in}")
131
+ if not os.access(ap_in, os.R_OK):
132
+ raise EmbedClientError(f"file not readable: {ap_in}")
133
+ try:
134
+ with open(ap_in, "rb") as f:
135
+ raw = f.read()
136
+ except OSError as e:
137
+ raise EmbedClientError(f"cannot read file: {ap_in}: {e}") from e
138
+ try:
139
+ decoded = raw.decode("utf-8-sig")
140
+ except UnicodeDecodeError as e:
141
+ raise EmbedClientError(
142
+ f"file must be UTF-8 (with optional BOM): {ap_in}"
143
+ ) from e
144
+
145
+ data = build_embed_file_json_array_from_markdown_text(
146
+ decoded,
147
+ max_block_bytes=max_block_bytes,
148
+ strip_front_matter=strip_front_matter,
149
+ max_chunks=max_chunks,
150
+ )
151
+ ap_out = os.path.abspath(output_path)
152
+ _parent = os.path.dirname(ap_out)
153
+ if _parent:
154
+ os.makedirs(_parent, exist_ok=True)
155
+ try:
156
+ with open(ap_out, "w", encoding="utf-8") as f:
157
+ json.dump(data, f, ensure_ascii=False)
158
+ except OSError as e:
159
+ raise EmbedClientError(f"cannot write file: {ap_out}: {e}") from e
160
+
161
+ validate_local_json_embed_file(ap_out, max_block_bytes=max_block_bytes)
162
+ return ap_out
163
+
164
+
165
+ def prepare_markdown_cli_main(args: Any) -> int:
166
+ """
167
+ CLI entry for ``prepare-embed-file-json`` (expects argparse namespace attributes).
168
+ """
169
+ import sys
170
+ from pathlib import Path
171
+
172
+ try:
173
+ out_path = getattr(args, "output", None)
174
+ if not out_path:
175
+ base = Path(args.md_path).resolve()
176
+ out_path = str(base.parent / f"{base.stem}_embed_file.json")
177
+ out = prepare_markdown_path_to_json_file(
178
+ args.md_path,
179
+ out_path,
180
+ max_block_bytes=getattr(args, "max_block_bytes", None),
181
+ strip_front_matter=not bool(getattr(args, "no_strip_front_matter", False)),
182
+ max_chunks=getattr(args, "max_chunks", None),
183
+ )
184
+ print(out, file=sys.stdout)
185
+ return 0
186
+ except EmbedClientError as e:
187
+ print(f"❌ prepare-embed-file-json: {e}", file=sys.stderr)
188
+ return 1
189
+
190
+
191
+ __all__ = [
192
+ "build_embed_file_json_array_from_markdown_text",
193
+ "prepare_markdown_cli_main",
194
+ "prepare_markdown_path_to_json_file",
195
+ "split_body_into_text_blocks",
196
+ "strip_optional_yaml_front_matter",
197
+ ]
@@ -0,0 +1,160 @@
1
+ """
2
+ Response Normalizer
3
+
4
+ This module provides functions to normalize adapter responses to legacy format.
5
+
6
+ Author: Vasiliy Zdanovskiy
7
+ email: vasilyvz@gmail.com
8
+ """
9
+
10
+ from typing import Any, Dict
11
+
12
+
13
+ class ResponseNormalizer:
14
+ """
15
+ Normalizes adapter responses to legacy format expected by EmbeddingClient.
16
+
17
+ Adapter returns responses in format:
18
+ - Immediate: {"mode": "immediate", "command": "...", "result": {...}, "queued": False}
19
+ - Queued: {"mode": "queued", "job_id": "...", "status": "...", "result": {...}, "queued": True}
20
+
21
+ Legacy format expected:
22
+ - {"result": {"success": bool, "data": {...}}}
23
+ - {"error": {"code": int, "message": str, "details": ...}}
24
+ """
25
+
26
+ @staticmethod
27
+ def normalize_command_response(adapter_response: Dict[str, Any]) -> Dict[str, Any]:
28
+ """
29
+ Normalize adapter command response to legacy format.
30
+
31
+ Args:
32
+ adapter_response: Response from adapter execute_command_unified
33
+
34
+ Returns:
35
+ Normalized response in legacy format
36
+ """
37
+ mode = adapter_response.get("mode", "immediate")
38
+
39
+ if mode == "queued":
40
+ return ResponseNormalizer._normalize_queued_response(adapter_response)
41
+ else:
42
+ return ResponseNormalizer._normalize_immediate_response(adapter_response)
43
+
44
+ @staticmethod
45
+ def _normalize_immediate_response(
46
+ adapter_response: Dict[str, Any],
47
+ ) -> Dict[str, Any]:
48
+ """Normalize immediate response."""
49
+ result = adapter_response.get("result", {})
50
+
51
+ # If result is already in legacy format, return as-is
52
+ if isinstance(result, dict) and ("success" in result or "data" in result):
53
+ return {"result": result}
54
+
55
+ # Wrap in legacy format
56
+ return {"result": {"success": True, "data": result}}
57
+
58
+ @staticmethod
59
+ def _normalize_queued_response(adapter_response: Dict[str, Any]) -> Dict[str, Any]:
60
+ """Normalize queued response."""
61
+ result = adapter_response.get("result")
62
+ status = adapter_response.get("status", "unknown")
63
+ job_id = adapter_response.get("job_id")
64
+
65
+ # If result is already in legacy format, return as-is
66
+ if isinstance(result, dict) and ("success" in result or "data" in result):
67
+ # Add job metadata if available
68
+ if job_id and "data" in result:
69
+ result["data"]["job_id"] = job_id
70
+ result["data"]["status"] = status
71
+ return {"result": result}
72
+
73
+ # Wrap in legacy format with job metadata
74
+ data: Dict[str, Any]
75
+ if isinstance(result, dict):
76
+ data = result
77
+ else:
78
+ data = {}
79
+
80
+ normalized: Dict[str, Any] = {
81
+ "result": {
82
+ "success": status in ("completed", "success"),
83
+ "data": data,
84
+ }
85
+ }
86
+
87
+ # Add job metadata
88
+ if job_id:
89
+ data["job_id"] = job_id
90
+ data["status"] = status
91
+
92
+ return normalized
93
+
94
+ @staticmethod
95
+ def extract_error_from_adapter(adapter_error: Exception) -> Dict[str, Any]:
96
+ """
97
+ Extract error information from adapter exception.
98
+
99
+ Args:
100
+ adapter_error: Exception raised by adapter
101
+
102
+ Returns:
103
+ Error dictionary in legacy format
104
+ """
105
+ error_msg = str(adapter_error)
106
+
107
+ # Try to extract structured error if available
108
+ if hasattr(adapter_error, "error"):
109
+ error_data = adapter_error.error
110
+ if isinstance(error_data, dict):
111
+ return {"error": error_data}
112
+
113
+ # Default error format
114
+ return {"error": {"code": -32000, "message": error_msg, "details": None}}
115
+
116
+ @staticmethod
117
+ def normalize_queue_status(queue_status: Dict[str, Any]) -> Dict[str, Any]:
118
+ """
119
+ Normalize queue status response to legacy format.
120
+
121
+ Args:
122
+ queue_status: Queue status from adapter (can be from queue_get_job_status or embed_job_status)
123
+
124
+ Returns:
125
+ Normalized status in legacy format
126
+ """
127
+ # Handle embed_job_status format
128
+ if "result" in queue_status and isinstance(queue_status["result"], dict):
129
+ result_data = queue_status["result"]
130
+ if result_data.get("success") and "data" in result_data:
131
+ data = result_data["data"]
132
+ status = data.get("status", "unknown")
133
+ # Check if done flag is set (embed_job_status specific)
134
+ if data.get("done", False) and status == "completed":
135
+ status = "completed"
136
+ result = data.get("result")
137
+ return {
138
+ "status": status,
139
+ "result": result,
140
+ "exists": data.get("exists", True),
141
+ }
142
+
143
+ # Extract status from nested structures (standard queue format)
144
+ status = queue_status.get("status")
145
+ if not status:
146
+ status = (
147
+ queue_status.get("data", {}).get("status")
148
+ or queue_status.get("result", {}).get("status")
149
+ or queue_status.get("result", {}).get("data", {}).get("status")
150
+ or "unknown"
151
+ )
152
+
153
+ # Extract result
154
+ result = queue_status.get("result")
155
+ if not result:
156
+ result = queue_status.get("data", {}).get("result") or queue_status.get(
157
+ "data", {}
158
+ )
159
+
160
+ return {"status": status, "result": result, "exists": status != "unknown"}
@@ -0,0 +1,219 @@
1
+ """
2
+ Response Parsers
3
+
4
+ This module provides functions to extract data from API responses.
5
+ These functions were extracted from EmbeddingClient to reduce file size.
6
+
7
+ Author: Vasiliy Zdanovskiy
8
+ email: vasilyvz@gmail.com
9
+ """
10
+
11
+ from typing import Any, Dict, List
12
+
13
+
14
+ def extract_embeddings(result: Dict[str, Any]) -> List[List[float]]:
15
+ """
16
+ Extract embeddings from API response, supporting both old and new formats.
17
+
18
+ Args:
19
+ result: API response dictionary
20
+
21
+ Returns:
22
+ List of embedding vectors (list of lists of floats)
23
+
24
+ Raises:
25
+ ValueError: If embeddings cannot be extracted from the response
26
+ """
27
+ # Handle direct embeddings field (old format compatibility)
28
+ if "embeddings" in result:
29
+ return result["embeddings"]
30
+
31
+ # Handle result wrapper
32
+ if "result" in result:
33
+ res = result["result"]
34
+
35
+ # Handle direct list in result (old format)
36
+ if isinstance(res, list):
37
+ return res
38
+
39
+ if isinstance(res, dict):
40
+ # Handle old format: result.embeddings
41
+ if "embeddings" in res:
42
+ return res["embeddings"]
43
+
44
+ # Handle old format: result.data.embeddings
45
+ if (
46
+ "data" in res
47
+ and isinstance(res["data"], dict)
48
+ and "embeddings" in res["data"]
49
+ ):
50
+ return res["data"]["embeddings"]
51
+
52
+ # Handle adapter format: result.data.results[].embedding
53
+ if (
54
+ "data" in res
55
+ and isinstance(res["data"], dict)
56
+ and "results" in res["data"]
57
+ ):
58
+ results = res["data"]["results"]
59
+ if isinstance(results, list):
60
+ embeddings = []
61
+ for item in results:
62
+ if isinstance(item, dict) and "embedding" in item:
63
+ embeddings.append(item["embedding"])
64
+ else:
65
+ raise ValueError(
66
+ f"Invalid item format in adapter response: {item}"
67
+ )
68
+ return embeddings
69
+
70
+ # Handle new format: result.data[].embedding
71
+ if "data" in res and isinstance(res["data"], list):
72
+ embeddings = []
73
+ for item in res["data"]:
74
+ if isinstance(item, dict) and "embedding" in item:
75
+ embeddings.append(item["embedding"])
76
+ else:
77
+ raise ValueError(
78
+ f"Invalid item format in new API response: {item}"
79
+ )
80
+ return embeddings
81
+
82
+ raise ValueError(f"Cannot extract embeddings from response: {result}")
83
+
84
+
85
+ def extract_embedding_data(result: Dict[str, Any]) -> List[Dict[str, Any]]:
86
+ """
87
+ Extract full embedding data from API response (new format only).
88
+
89
+ Args:
90
+ result: API response dictionary
91
+
92
+ Returns:
93
+ List of dictionaries with 'body', 'embedding', 'tokens', and 'bm25_tokens' fields
94
+
95
+ Raises:
96
+ ValueError: If data cannot be extracted or is in old format
97
+ """
98
+ if "result" in result and isinstance(result["result"], dict):
99
+ res = result["result"]
100
+ if "data" in res and isinstance(res["data"], dict) and "results" in res["data"]:
101
+ # New format: result.data.results[]
102
+ results = res["data"]["results"]
103
+ if isinstance(results, list):
104
+ # Validate that all items have required fields
105
+ for i, item in enumerate(results):
106
+ if not isinstance(item, dict):
107
+ raise ValueError(f"Item {i} is not a dictionary: {item}")
108
+ if "body" not in item:
109
+ raise ValueError(f"Item {i} missing 'body' field: {item}")
110
+ if "embedding" not in item:
111
+ raise ValueError(f"Item {i} missing 'embedding' field: {item}")
112
+ if "tokens" not in item:
113
+ raise ValueError(f"Item {i} missing 'tokens' field: {item}")
114
+ if "bm25_tokens" not in item:
115
+ raise ValueError(
116
+ f"Item {i} missing 'bm25_tokens' field: {item}"
117
+ )
118
+
119
+ return results
120
+
121
+ # Legacy support for old format: result.data[]
122
+ if "data" in res and isinstance(res["data"], list):
123
+ # Validate that all items have required fields
124
+ for i, item in enumerate(res["data"]):
125
+ if not isinstance(item, dict):
126
+ raise ValueError(f"Item {i} is not a dictionary: {item}")
127
+ if "body" not in item:
128
+ raise ValueError(f"Item {i} missing 'body' field: {item}")
129
+ if "embedding" not in item:
130
+ raise ValueError(f"Item {i} missing 'embedding' field: {item}")
131
+ # Old format had 'chunks' instead of 'tokens'
132
+ if "chunks" not in item and "tokens" not in item:
133
+ raise ValueError(
134
+ f"Item {i} missing 'chunks' or 'tokens' field: {item}"
135
+ )
136
+
137
+ return res["data"]
138
+
139
+ raise ValueError(
140
+ f"Cannot extract embedding data from response (new format required): {result}"
141
+ )
142
+
143
+
144
+ def extract_texts(result: Dict[str, Any]) -> List[str]:
145
+ """
146
+ Extract original texts from API response (new format only).
147
+
148
+ Args:
149
+ result: API response dictionary
150
+
151
+ Returns:
152
+ List of original text strings
153
+
154
+ Raises:
155
+ ValueError: If texts cannot be extracted or is in old format
156
+ """
157
+ data = extract_embedding_data(result)
158
+ return [item["body"] for item in data]
159
+
160
+
161
+ def extract_chunks(result: Dict[str, Any]) -> List[List[str]]:
162
+ """
163
+ Extract text chunks from API response (new format only).
164
+ Note: This method now extracts 'tokens' instead of 'chunks' for compatibility.
165
+
166
+ Args:
167
+ result: API response dictionary
168
+
169
+ Returns:
170
+ List of token lists for each text
171
+
172
+ Raises:
173
+ ValueError: If chunks cannot be extracted or is in old format
174
+ """
175
+ data = extract_embedding_data(result)
176
+ chunks = []
177
+ for item in data:
178
+ # New format uses 'tokens', old format used 'chunks'
179
+ if "tokens" in item:
180
+ chunks.append(item["tokens"])
181
+ elif "chunks" in item:
182
+ chunks.append(item["chunks"])
183
+ else:
184
+ raise ValueError(f"Item missing both 'tokens' and 'chunks' fields: {item}")
185
+ return chunks
186
+
187
+
188
+ def extract_tokens(result: Dict[str, Any]) -> List[List[str]]:
189
+ """
190
+ Extract tokens from API response (new format only).
191
+
192
+ Args:
193
+ result: API response dictionary
194
+
195
+ Returns:
196
+ List of token lists for each text
197
+
198
+ Raises:
199
+ ValueError: If tokens cannot be extracted or is in old format
200
+ """
201
+ data = extract_embedding_data(result)
202
+ return [item["tokens"] for item in data]
203
+
204
+
205
+ def extract_bm25_tokens(result: Dict[str, Any]) -> List[List[str]]:
206
+ """
207
+ Extract BM25 tokens from API response (new format only).
208
+
209
+ Args:
210
+ result: API response dictionary
211
+
212
+ Returns:
213
+ List of BM25 token lists for each text
214
+
215
+ Raises:
216
+ ValueError: If BM25 tokens cannot be extracted or is in old format
217
+ """
218
+ data = extract_embedding_data(result)
219
+ return [item["bm25_tokens"] for item in data]