svo-client-dev 4.0.21__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.
- svo_client/__init__.py +75 -0
- svo_client/_version.py +3 -0
- svo_client/aiohttp_ws_max_message.py +50 -0
- svo_client/chunk_presets.py +36 -0
- svo_client/cli.py +225 -0
- svo_client/cli_chunk.py +386 -0
- svo_client/client.py +1104 -0
- svo_client/config.py +149 -0
- svo_client/connect_defaults.py +230 -0
- svo_client/constants.py +4 -0
- svo_client/core.py +330 -0
- svo_client/debug_ndjson.py +14 -0
- svo_client/exceptions.py +48 -0
- svo_client/file_processor.py +58 -0
- svo_client/transfer_precheck.py +91 -0
- svo_client_dev-4.0.21.dist-info/METADATA +363 -0
- svo_client_dev-4.0.21.dist-info/RECORD +20 -0
- svo_client_dev-4.0.21.dist-info/WHEEL +5 -0
- svo_client_dev-4.0.21.dist-info/entry_points.txt +3 -0
- svo_client_dev-4.0.21.dist-info/top_level.txt +1 -0
svo_client/config.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration management for SVO Chunker client.
|
|
3
|
+
|
|
4
|
+
Hierarchy: CLI args > env > config file.
|
|
5
|
+
|
|
6
|
+
Author: Vasiliy Zdanovskiy
|
|
7
|
+
email: vasilyvz@gmail.com
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, Dict, Optional
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ClientConfig:
|
|
17
|
+
"""Load client-side settings for :class:`~svo_client.client.SvoChunkerClient`.
|
|
18
|
+
|
|
19
|
+
Priority: **CLI overrides** passed to the constructor, then **environment variables**
|
|
20
|
+
(see ``ENV_PREFIX`` + mapping in :meth:`_load_from_env`), then **JSON file** from
|
|
21
|
+
``config_file`` or the first existing path in ``DEFAULT_CONFIG_PATHS``.
|
|
22
|
+
|
|
23
|
+
Optional keys are for **operator documentation** and local tooling only. For example,
|
|
24
|
+
``file_batch_size`` may mirror the chunker server ``config`` value so scripts and humans
|
|
25
|
+
know the batch width used by ``file_chunk``; it is **not** sent on JSON-RPC and does not
|
|
26
|
+
change server behavior unless the server reads its own config.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
ENV_PREFIX = "SVO_CHUNKER_CLIENT_"
|
|
30
|
+
|
|
31
|
+
DEFAULT_CONFIG_PATHS = [
|
|
32
|
+
"svo_client_config.json",
|
|
33
|
+
"~/.svo_chunker_client.json",
|
|
34
|
+
"/etc/svo_chunker_client.json",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
config_file: Optional[str] = None,
|
|
40
|
+
cli_args: Optional[Dict[str, Any]] = None,
|
|
41
|
+
):
|
|
42
|
+
self.cli_args = cli_args or {}
|
|
43
|
+
self.config_file = config_file
|
|
44
|
+
self._config: Dict[str, Any] = {}
|
|
45
|
+
|
|
46
|
+
def load(self) -> Dict[str, Any]:
|
|
47
|
+
config = {}
|
|
48
|
+
config_file_path = self._find_config_file()
|
|
49
|
+
if config_file_path:
|
|
50
|
+
config.update(self._load_config_file(config_file_path))
|
|
51
|
+
config.update(self._load_from_env())
|
|
52
|
+
config.update(self.cli_args)
|
|
53
|
+
self._config = config
|
|
54
|
+
return config
|
|
55
|
+
|
|
56
|
+
def _find_config_file(self) -> Optional[Path]:
|
|
57
|
+
if self.config_file:
|
|
58
|
+
path = Path(self.config_file).expanduser()
|
|
59
|
+
if path.exists():
|
|
60
|
+
return path
|
|
61
|
+
return None
|
|
62
|
+
for config_path in self.DEFAULT_CONFIG_PATHS:
|
|
63
|
+
path = Path(config_path).expanduser()
|
|
64
|
+
if path.exists():
|
|
65
|
+
return path
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
def _load_config_file(self, config_path: Path) -> Dict[str, Any]:
|
|
69
|
+
try:
|
|
70
|
+
with open(config_path, "r", encoding="utf-8") as f:
|
|
71
|
+
return json.load(f)
|
|
72
|
+
except Exception as e:
|
|
73
|
+
raise ValueError(f"Failed to load config file {config_path}: {e}") from e
|
|
74
|
+
|
|
75
|
+
def _load_from_env(self) -> Dict[str, Any]:
|
|
76
|
+
config: Dict[str, Any] = {}
|
|
77
|
+
env_prefix = self.ENV_PREFIX
|
|
78
|
+
env_mapping = {
|
|
79
|
+
f"{env_prefix}PROTOCOL": "protocol",
|
|
80
|
+
f"{env_prefix}HOST": "host",
|
|
81
|
+
f"{env_prefix}PORT": "port",
|
|
82
|
+
f"{env_prefix}TOKEN_HEADER": "token_header",
|
|
83
|
+
f"{env_prefix}TOKEN": "token",
|
|
84
|
+
f"{env_prefix}CERT": "cert",
|
|
85
|
+
f"{env_prefix}KEY": "key",
|
|
86
|
+
f"{env_prefix}CA": "ca",
|
|
87
|
+
f"{env_prefix}CHECK_HOSTNAME": "check_hostname",
|
|
88
|
+
f"{env_prefix}TIMEOUT": "timeout",
|
|
89
|
+
f"{env_prefix}FILE_BATCH_SIZE": "file_batch_size",
|
|
90
|
+
}
|
|
91
|
+
for env_var, config_key in env_mapping.items():
|
|
92
|
+
value = os.getenv(env_var)
|
|
93
|
+
if value is not None:
|
|
94
|
+
if config_key in ("port", "timeout"):
|
|
95
|
+
try:
|
|
96
|
+
config[config_key] = (
|
|
97
|
+
int(value) if config_key == "port" else float(value)
|
|
98
|
+
)
|
|
99
|
+
except ValueError:
|
|
100
|
+
pass
|
|
101
|
+
elif config_key == "file_batch_size":
|
|
102
|
+
try:
|
|
103
|
+
config[config_key] = int(value)
|
|
104
|
+
except ValueError:
|
|
105
|
+
pass
|
|
106
|
+
elif config_key == "check_hostname":
|
|
107
|
+
config[config_key] = value.lower() in (
|
|
108
|
+
"true",
|
|
109
|
+
"1",
|
|
110
|
+
"yes",
|
|
111
|
+
"on",
|
|
112
|
+
)
|
|
113
|
+
else:
|
|
114
|
+
config[config_key] = value
|
|
115
|
+
return config
|
|
116
|
+
|
|
117
|
+
def get_client_config(self) -> Dict[str, Any]:
|
|
118
|
+
"""Get kwargs for SvoChunkerClient."""
|
|
119
|
+
config = self.load()
|
|
120
|
+
raw_timeout = config.get("timeout", 300.0)
|
|
121
|
+
# None or 0 = no timeout (wait until completion)
|
|
122
|
+
timeout = None if raw_timeout is None or raw_timeout == 0 else raw_timeout
|
|
123
|
+
raw_file_batch = config.get("file_batch_size")
|
|
124
|
+
file_batch_size: Optional[int]
|
|
125
|
+
if raw_file_batch is None:
|
|
126
|
+
file_batch_size = None
|
|
127
|
+
else:
|
|
128
|
+
try:
|
|
129
|
+
file_batch_size = int(raw_file_batch)
|
|
130
|
+
except (TypeError, ValueError):
|
|
131
|
+
file_batch_size = None
|
|
132
|
+
|
|
133
|
+
client_config = {
|
|
134
|
+
"protocol": config.get("protocol", "https"),
|
|
135
|
+
"host": config.get("host", "127.0.0.1"),
|
|
136
|
+
"port": config.get("port", 8009),
|
|
137
|
+
"token_header": config.get("token_header"),
|
|
138
|
+
"token": config.get("token"),
|
|
139
|
+
"cert": config.get("cert"),
|
|
140
|
+
"key": config.get("key"),
|
|
141
|
+
"ca": config.get("ca"),
|
|
142
|
+
"check_hostname": config.get("check_hostname", False),
|
|
143
|
+
"timeout": timeout,
|
|
144
|
+
"file_batch_size": file_batch_size,
|
|
145
|
+
}
|
|
146
|
+
# Include timeout even when None (no-timeout mode)
|
|
147
|
+
return {
|
|
148
|
+
k: v for k, v in client_config.items() if v is not None or k == "timeout"
|
|
149
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared defaults for connecting to a local chunker via ``SvoChunkerClient``.
|
|
3
|
+
|
|
4
|
+
Resolves mTLS certificate paths under the project tree when present; otherwise
|
|
5
|
+
uses plain HTTP to ``127.0.0.1:8009``.
|
|
6
|
+
|
|
7
|
+
Author: Vasiliy Zdanovskiy
|
|
8
|
+
email: vasilyvz@gmail.com
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any, Dict, Optional, Tuple, Union
|
|
16
|
+
from urllib.parse import urlparse
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def resolve_mtls_triplet(
|
|
20
|
+
project_root: Path,
|
|
21
|
+
) -> Tuple[Optional[str], Optional[str], Optional[str]]:
|
|
22
|
+
"""Return (cert, key, ca) paths if all three exist, else (None, None, None)."""
|
|
23
|
+
for cert, key, ca in (
|
|
24
|
+
(
|
|
25
|
+
project_root
|
|
26
|
+
/ "mtls_certificates/mtls_certificates/client/code-analysis.crt",
|
|
27
|
+
project_root
|
|
28
|
+
/ "mtls_certificates/mtls_certificates/client/code-analysis.key",
|
|
29
|
+
project_root / "mtls_certificates/mtls_certificates/ca/ca.crt",
|
|
30
|
+
),
|
|
31
|
+
(
|
|
32
|
+
project_root / "mtls_certificates/client/code-analysis.crt",
|
|
33
|
+
project_root / "mtls_certificates/client/code-analysis.key",
|
|
34
|
+
project_root / "mtls_certificates/ca/ca.crt",
|
|
35
|
+
),
|
|
36
|
+
(
|
|
37
|
+
project_root / "mtls_certificates/client/svo-chunker.crt",
|
|
38
|
+
project_root / "mtls_certificates/client/svo-chunker.key",
|
|
39
|
+
project_root / "mtls_certificates/ca/ca.crt",
|
|
40
|
+
),
|
|
41
|
+
):
|
|
42
|
+
if cert.is_file() and key.is_file() and ca.is_file():
|
|
43
|
+
return str(cert), str(key), str(ca)
|
|
44
|
+
return None, None, None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def default_chunker_client_kwargs(
|
|
48
|
+
project_root: Optional[Path] = None,
|
|
49
|
+
*,
|
|
50
|
+
host: str = "127.0.0.1",
|
|
51
|
+
port: int = 8009,
|
|
52
|
+
timeout: float = 300.0,
|
|
53
|
+
) -> Dict[str, Any]:
|
|
54
|
+
"""
|
|
55
|
+
Keyword arguments for ``SvoChunkerClient(...)`` for local development.
|
|
56
|
+
|
|
57
|
+
Uses HTTPS + mTLS when certificate files are found under *project_root*,
|
|
58
|
+
otherwise ``protocol=http`` without certs.
|
|
59
|
+
"""
|
|
60
|
+
root = project_root or Path.cwd()
|
|
61
|
+
cert, key, ca = resolve_mtls_triplet(root)
|
|
62
|
+
if cert and key and ca:
|
|
63
|
+
return {
|
|
64
|
+
"protocol": "https",
|
|
65
|
+
"host": host,
|
|
66
|
+
"port": port,
|
|
67
|
+
"cert": cert,
|
|
68
|
+
"key": key,
|
|
69
|
+
"ca": ca,
|
|
70
|
+
"check_hostname": False,
|
|
71
|
+
"timeout": timeout,
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
"protocol": "http",
|
|
75
|
+
"host": host,
|
|
76
|
+
"port": port,
|
|
77
|
+
"timeout": timeout,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def chunker_client_kwargs_from_base_url(
|
|
82
|
+
base_url: str,
|
|
83
|
+
project_root: Optional[Path] = None,
|
|
84
|
+
*,
|
|
85
|
+
timeout: float = 300.0,
|
|
86
|
+
) -> Dict[str, Any]:
|
|
87
|
+
"""
|
|
88
|
+
Build ``SvoChunkerClient`` kwargs from a base URL like ``http://127.0.0.1:8009``.
|
|
89
|
+
|
|
90
|
+
HTTPS uses mTLS paths from ``resolve_mtls_triplet`` when files exist; otherwise
|
|
91
|
+
HTTPS without client cert may fail at TLS handshake (caller responsibility).
|
|
92
|
+
"""
|
|
93
|
+
parsed = urlparse(base_url.strip())
|
|
94
|
+
scheme = (parsed.scheme or "http").lower()
|
|
95
|
+
host = parsed.hostname or "127.0.0.1"
|
|
96
|
+
port = parsed.port
|
|
97
|
+
if port is None:
|
|
98
|
+
port = 443 if scheme in ("https", "mtls") else 8009
|
|
99
|
+
|
|
100
|
+
root = project_root or Path.cwd()
|
|
101
|
+
cert, key, ca = resolve_mtls_triplet(root)
|
|
102
|
+
# region agent log
|
|
103
|
+
from svo_client.debug_ndjson import agent_debug_log
|
|
104
|
+
|
|
105
|
+
agent_debug_log(
|
|
106
|
+
"H1",
|
|
107
|
+
"connect_defaults:chunker_client_kwargs_from_base_url",
|
|
108
|
+
"mtls triplet resolution",
|
|
109
|
+
{
|
|
110
|
+
"base_url": base_url.strip(),
|
|
111
|
+
"scheme": scheme,
|
|
112
|
+
"host": host,
|
|
113
|
+
"port": port,
|
|
114
|
+
"triplet_found": bool(cert and key and ca),
|
|
115
|
+
"resolve_mtls_paths_checked": "code-analysis then svo-chunker",
|
|
116
|
+
},
|
|
117
|
+
)
|
|
118
|
+
# endregion
|
|
119
|
+
if scheme in ("https", "mtls") and cert and key and ca:
|
|
120
|
+
return {
|
|
121
|
+
"protocol": "https",
|
|
122
|
+
"host": host,
|
|
123
|
+
"port": port,
|
|
124
|
+
"cert": cert,
|
|
125
|
+
"key": key,
|
|
126
|
+
"ca": ca,
|
|
127
|
+
"check_hostname": False,
|
|
128
|
+
"timeout": timeout,
|
|
129
|
+
}
|
|
130
|
+
if scheme in ("https", "mtls"):
|
|
131
|
+
# region agent log
|
|
132
|
+
agent_debug_log(
|
|
133
|
+
"H1",
|
|
134
|
+
"connect_defaults:chunker_client_kwargs_from_base_url",
|
|
135
|
+
"https without client cert triplet — adapter may omit mTLS",
|
|
136
|
+
{"scheme": scheme, "outer_timeout": timeout},
|
|
137
|
+
)
|
|
138
|
+
# endregion
|
|
139
|
+
return {
|
|
140
|
+
"protocol": "https",
|
|
141
|
+
"host": host,
|
|
142
|
+
"port": port,
|
|
143
|
+
"timeout": timeout,
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
"protocol": "http",
|
|
147
|
+
"host": host,
|
|
148
|
+
"port": port,
|
|
149
|
+
"timeout": timeout,
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def chunker_client_kwargs_from_chunker_server_config(
|
|
154
|
+
chunker_config_path: Union[str, Path],
|
|
155
|
+
*,
|
|
156
|
+
host: str = "localhost",
|
|
157
|
+
project_root: Optional[Path] = None,
|
|
158
|
+
timeout: Optional[float] = None,
|
|
159
|
+
) -> Dict[str, Any]:
|
|
160
|
+
"""
|
|
161
|
+
Build ``SvoChunkerClient`` kwargs from a **chunker server** JSON (e.g.
|
|
162
|
+
``config/svo_chunker_container.json``): ``protocol``, ``port``, optional
|
|
163
|
+
``check_hostname`` from ``server.ssl``.
|
|
164
|
+
|
|
165
|
+
*host* is **not** taken from the file (container configs often use
|
|
166
|
+
``0.0.0.0``); pass ``localhost`` / ``127.0.0.1`` for local calls.
|
|
167
|
+
|
|
168
|
+
Client mTLS material is **not** the server's ``ssl.cert``/``key`` in that
|
|
169
|
+
file (those are in-container server paths). When no usable paths exist on
|
|
170
|
+
disk, :func:`resolve_mtls_triplet` under *project_root* supplies the client
|
|
171
|
+
cert, key, and CA (same as local dev).
|
|
172
|
+
"""
|
|
173
|
+
path = Path(chunker_config_path).expanduser().resolve()
|
|
174
|
+
with open(path, "r", encoding="utf-8") as fh:
|
|
175
|
+
data = json.load(fh)
|
|
176
|
+
if not isinstance(data, dict):
|
|
177
|
+
raise ValueError("chunker config root must be a JSON object")
|
|
178
|
+
|
|
179
|
+
srv = data.get("server") if isinstance(data.get("server"), dict) else {}
|
|
180
|
+
transport = (
|
|
181
|
+
data.get("transport") if isinstance(data.get("transport"), dict) else {}
|
|
182
|
+
)
|
|
183
|
+
raw_proto = srv.get("protocol") or transport.get("type") or "https"
|
|
184
|
+
protocol = str(raw_proto).strip().lower()
|
|
185
|
+
if protocol == "mtls":
|
|
186
|
+
protocol = "https"
|
|
187
|
+
|
|
188
|
+
port_raw = srv.get("port", transport.get("port", 8009))
|
|
189
|
+
try:
|
|
190
|
+
port = int(port_raw)
|
|
191
|
+
except (TypeError, ValueError) as e:
|
|
192
|
+
raise ValueError(f"invalid port in chunker config: {port_raw!r}") from e
|
|
193
|
+
|
|
194
|
+
ssl_block = srv.get("ssl") if isinstance(srv.get("ssl"), dict) else {}
|
|
195
|
+
ch_raw = ssl_block.get("check_hostname")
|
|
196
|
+
if isinstance(ch_raw, bool):
|
|
197
|
+
check_hostname = ch_raw
|
|
198
|
+
else:
|
|
199
|
+
check_hostname = False
|
|
200
|
+
|
|
201
|
+
if timeout is None:
|
|
202
|
+
emb = data.get("embedding_client")
|
|
203
|
+
if isinstance(emb, dict):
|
|
204
|
+
t_emb = emb.get("timeout")
|
|
205
|
+
if isinstance(t_emb, (int, float)) and t_emb > 0:
|
|
206
|
+
timeout = float(t_emb)
|
|
207
|
+
if timeout is None:
|
|
208
|
+
timeout = 300.0
|
|
209
|
+
|
|
210
|
+
root = project_root or Path.cwd()
|
|
211
|
+
cert, key, ca = resolve_mtls_triplet(root)
|
|
212
|
+
|
|
213
|
+
out: Dict[str, Any] = {
|
|
214
|
+
"protocol": protocol,
|
|
215
|
+
"host": host,
|
|
216
|
+
"port": port,
|
|
217
|
+
"check_hostname": check_hostname,
|
|
218
|
+
"timeout": float(timeout),
|
|
219
|
+
}
|
|
220
|
+
if protocol in ("https", "mtls") and cert and key and ca:
|
|
221
|
+
out["protocol"] = "https"
|
|
222
|
+
out["cert"] = cert
|
|
223
|
+
out["key"] = key
|
|
224
|
+
out["ca"] = ca
|
|
225
|
+
elif protocol in ("https", "mtls"):
|
|
226
|
+
out["protocol"] = "https"
|
|
227
|
+
else:
|
|
228
|
+
out["protocol"] = "http"
|
|
229
|
+
|
|
230
|
+
return {k: v for k, v in out.items() if v is not None or k == "timeout"}
|
svo_client/constants.py
ADDED