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/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"}
@@ -0,0 +1,4 @@
1
+ """Constants used by the standalone SVO client."""
2
+
3
+ MAX_CLI_FILE_CHUNK_UPLOAD_BYTES: int = 48 * 1024 * 1024
4
+ """Largest raw file accepted by the base64 ``file_chunk`` CLI path."""