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 ADDED
@@ -0,0 +1,75 @@
1
+ """
2
+ SVO Chunker client package.
3
+
4
+ Transport is ``mcp_proxy_adapter`` ``JsonRpcClient`` only. JSON-RPC and file
5
+ transfers use HTTP(S); queued job completion uses WebSocket ``/ws`` as
6
+ implemented by the adapter (including jobs that appear only after the server
7
+ queue takes over from a short synchronous JSON-RPC window; adapter >= 8.10).
8
+
9
+ Author: Vasiliy Zdanovskiy
10
+ email: vasilyvz@gmail.com
11
+ """
12
+
13
+ from svo_client.chunk_presets import KNOWN_CHUNK_SET_PRESETS
14
+ from svo_client.client import (
15
+ SvoChunkerClient,
16
+ build_file_chunk_params,
17
+ extract_file_chunk_success_payload,
18
+ parse_chunk_ndjson,
19
+ parse_chunk_ndjson_file,
20
+ unwrap_command_result,
21
+ )
22
+ from svo_client.connect_defaults import (
23
+ chunker_client_kwargs_from_base_url,
24
+ chunker_client_kwargs_from_chunker_server_config,
25
+ default_chunker_client_kwargs,
26
+ resolve_mtls_triplet,
27
+ )
28
+ from svo_client.exceptions import (
29
+ SvoChunkingError,
30
+ SvoClientError,
31
+ SvoConnectionError,
32
+ SvoTimeoutError,
33
+ SvoValidationError,
34
+ )
35
+ from svo_client.file_processor import process_file_via_file_chunk
36
+
37
+
38
+ def __getattr__(name: str):
39
+ """Expose the version copied from the server source into release artifacts."""
40
+
41
+ if name != "__version__":
42
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
43
+ try:
44
+ from svo_client._version import __version__ as packaged_version
45
+
46
+ return packaged_version
47
+ except ModuleNotFoundError:
48
+ # A source checkout has no generated svo_client._version module. Read the
49
+ # same authoritative file that the release staging step copies.
50
+ import runpy
51
+ from pathlib import Path
52
+
53
+ version_path = Path(__file__).parents[1] / "svo_chunker" / "_version.py"
54
+ return runpy.run_path(str(version_path))["__version__"]
55
+
56
+ __all__ = [
57
+ "KNOWN_CHUNK_SET_PRESETS",
58
+ "SvoChunkerClient",
59
+ "chunker_client_kwargs_from_base_url",
60
+ "chunker_client_kwargs_from_chunker_server_config",
61
+ "default_chunker_client_kwargs",
62
+ "resolve_mtls_triplet",
63
+ "SvoClientError",
64
+ "SvoConnectionError",
65
+ "SvoTimeoutError",
66
+ "SvoValidationError",
67
+ "SvoChunkingError",
68
+ "build_file_chunk_params",
69
+ "extract_file_chunk_success_payload",
70
+ "parse_chunk_ndjson",
71
+ "parse_chunk_ndjson_file",
72
+ "unwrap_command_result",
73
+ "process_file_via_file_chunk",
74
+ "__version__",
75
+ ]
svo_client/_version.py ADDED
@@ -0,0 +1,3 @@
1
+ """Package version shared by runtime and packaging metadata."""
2
+
3
+ __version__ = "4.0.21"
@@ -0,0 +1,50 @@
1
+ """
2
+ aiohttp WebSocket inbound message size for large chunk job results.
3
+
4
+ ``mcp_proxy_adapter`` uses ``aiohttp.ClientSession.ws_connect`` with aiohttp's
5
+ default ``max_msg_size=4 MiB``. Large ``chunk`` / ``file_chunk`` JSON payloads
6
+ delivered over ``/ws`` can exceed that; the reader then fails with
7
+ ``Message size … exceeds limit 4194304``.
8
+
9
+ The Hypercorn server side already allows large WS frames (default
10
+ ``websocket_max_message_size`` is 16 MiB). This module aligns the **client**
11
+ default with that cap so ``svo_client`` can receive large terminal job messages.
12
+
13
+ Call :func:`apply_aiohttp_ws_max_message_patch` once before any WebSocket
14
+ connection from the adapter stack (invoked from ``svo_client.core`` before
15
+ ``JsonRpcClient`` import).
16
+
17
+ Author: Vasiliy Zdanovskiy
18
+ email: vasilyvz@gmail.com
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import functools
24
+ from typing import Any
25
+
26
+ # 16 MiB — matches typical Hypercorn ``websocket_max_message_size``; override
27
+ # aiohttp's 4 MiB default for ``/ws`` job completion payloads.
28
+ _SVO_WS_MAX_MESSAGE_BYTES = 16 * 1024 * 1024
29
+
30
+ _patch_applied = False
31
+
32
+
33
+ def apply_aiohttp_ws_max_message_patch() -> None:
34
+ """Monkeypatch ``ClientSession.ws_connect`` default ``max_msg_size`` to 16 MiB."""
35
+ global _patch_applied
36
+ if _patch_applied:
37
+ return
38
+ import aiohttp
39
+
40
+ _orig = aiohttp.ClientSession.ws_connect
41
+
42
+ @functools.wraps(_orig)
43
+ def _patched_ws_connect(self: Any, *args: Any, **kwargs: Any) -> Any:
44
+ # ws_connect is synchronous; returns _WSRequestContextManager (not a coroutine).
45
+ if "max_msg_size" not in kwargs:
46
+ kwargs["max_msg_size"] = _SVO_WS_MAX_MESSAGE_BYTES
47
+ return _orig(self, *args, **kwargs)
48
+
49
+ aiohttp.ClientSession.ws_connect = _patched_ws_connect
50
+ _patch_applied = True
@@ -0,0 +1,36 @@
1
+ """
2
+ Stable client-side names for server ``chunk_set`` presets (``config.chunk_sets``).
3
+
4
+ The server remains authoritative: new presets may appear without a client release.
5
+ ``KNOWN_CHUNK_SET_PRESETS`` documents the set shipped with the current server
6
+ contract used in this repo; unknown names are still forwarded on the wire.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import logging
12
+ from typing import Final, FrozenSet, Optional
13
+
14
+ __all__ = ["KNOWN_CHUNK_SET_PRESETS", "warn_if_unknown_chunk_set"]
15
+
16
+ KNOWN_CHUNK_SET_PRESETS: Final[tuple[str, ...]] = (
17
+ "plain_text",
18
+ "technical_text",
19
+ "docstring",
20
+ "scientific_text",
21
+ "fiction",
22
+ "chat",
23
+ "log",
24
+ )
25
+
26
+ _KNOWN: FrozenSet[str] = frozenset(KNOWN_CHUNK_SET_PRESETS)
27
+
28
+
29
+ def warn_if_unknown_chunk_set(chunk_set: Optional[str]) -> None:
30
+ """Log a single warning when *chunk_set* is set but not in ``KNOWN_CHUNK_SET_PRESETS``."""
31
+ if chunk_set is None or chunk_set in _KNOWN:
32
+ return
33
+ logging.getLogger(__name__).warning(
34
+ "chunk_set %r is not in KNOWN_CHUNK_SET_PRESETS; the server will validate the name",
35
+ chunk_set,
36
+ )
svo_client/cli.py ADDED
@@ -0,0 +1,225 @@
1
+ """
2
+ CLI: list / help / run (commands and help from server).
3
+
4
+ Author: Vasiliy Zdanovskiy
5
+ email: vasilyvz@gmail.com
6
+ """
7
+
8
+ import argparse
9
+ import asyncio
10
+ import json
11
+ import sys
12
+ from pathlib import Path
13
+ from typing import Any, Dict, List, Optional
14
+
15
+ _PROJECT_ROOT = Path(__file__).resolve().parent.parent
16
+ if str(_PROJECT_ROOT) not in sys.path:
17
+ sys.path.insert(0, str(_PROJECT_ROOT))
18
+
19
+ from svo_client import SvoChunkerClient # noqa: E402
20
+ from svo_client.exceptions import ( # noqa: E402
21
+ SvoChunkingError,
22
+ SvoConnectionError,
23
+ SvoTimeoutError,
24
+ SvoValidationError,
25
+ )
26
+
27
+
28
+ def _format_help_for_command(info: Dict[str, Any]) -> str:
29
+ lines = []
30
+ desc = info.get("description", "")
31
+ if desc:
32
+ lines.append(desc)
33
+ schema = info.get("schema", {})
34
+ props = schema.get("properties", {})
35
+ if props:
36
+ lines.append("")
37
+ lines.append("Parameters:")
38
+ for key, meta in props.items():
39
+ desc = meta.get("description", "")
40
+ typ = meta.get("type", "string")
41
+ lines.append(f" {key} ({typ}): {desc or '-'}")
42
+ return "\n".join(lines) if lines else json.dumps(info, indent=2)
43
+
44
+
45
+ def _format_help_list(data: Dict[str, Any]) -> str:
46
+ lines = []
47
+ tool = data.get("tool_info", {})
48
+ if tool:
49
+ lines.append(f"{tool.get('name', 'Service')} — {tool.get('description', '')}")
50
+ lines.append(f"Version: {tool.get('version', '')}")
51
+ lines.append("")
52
+ commands = data.get("commands", {})
53
+ if commands:
54
+ lines.append("Commands:")
55
+ for cmd_name, cmd_info in sorted(commands.items()):
56
+ if isinstance(cmd_info, dict):
57
+ desc = cmd_info.get("description", "")
58
+ lines.append(f" {cmd_name}: {desc or '(no description)'}")
59
+ else:
60
+ lines.append(f" {cmd_name}")
61
+ return "\n".join(lines) if lines else json.dumps(data, indent=2)
62
+
63
+
64
+ def _parse_params(args: List[str]) -> Dict[str, Any]:
65
+ out: Dict[str, Any] = {}
66
+ i = 0
67
+ while i < len(args):
68
+ arg = args[i]
69
+ if arg == "--params-json" and i + 1 < len(args):
70
+ try:
71
+ return json.loads(args[i + 1])
72
+ except json.JSONDecodeError as e:
73
+ raise ValueError(f"Invalid --params-json: {e}") from e
74
+ if arg.startswith("--") and "=" in arg:
75
+ key = arg[2:].split("=", 1)[0]
76
+ val = arg.split("=", 1)[1]
77
+ out[key] = val
78
+ elif arg.startswith("--") and i + 1 < len(args):
79
+ key = arg[2:]
80
+ out[key] = args[i + 1]
81
+ i += 1
82
+ elif "=" in arg:
83
+ key, val = arg.split("=", 1)
84
+ out[key] = val
85
+ i += 1
86
+ return out
87
+
88
+
89
+ def _add_network_args(parser: argparse.ArgumentParser) -> None:
90
+ g = parser.add_argument_group("Connection (defaults: mTLS localhost:8009)")
91
+ g.add_argument("--host", default="127.0.0.1", help="Server host")
92
+ g.add_argument("--port", type=int, default=8009, help="Server port")
93
+ g.add_argument("--protocol", default="mtls", choices=("http", "https", "mtls"))
94
+ g.add_argument("--certs-dir", default=None, help="Directory for cert/key/ca paths")
95
+ g.add_argument("--cert", default=None, help="Client certificate path")
96
+ g.add_argument("--key", default=None, help="Client key path")
97
+ g.add_argument("--ca", default=None, help="CA certificate path")
98
+ g.add_argument("--timeout", type=float, default=300.0, help="Command timeout (s)")
99
+
100
+
101
+ def _client_from_args(args: argparse.Namespace) -> SvoChunkerClient:
102
+ base = Path(args.certs_dir) if getattr(args, "certs_dir", None) else None
103
+ cert = getattr(args, "cert", None)
104
+ key = getattr(args, "key", None)
105
+ ca = getattr(args, "ca", None)
106
+ protocol = getattr(args, "protocol", "mtls")
107
+ if protocol == "mtls" and not cert and not key:
108
+ if base is None:
109
+ base = _PROJECT_ROOT / "mtls_certificates"
110
+ cert = str(base / "client" / "svo-chunker.crt")
111
+ key = str(base / "client" / "svo-chunker.key")
112
+ ca = str(base / "ca" / "ca.crt")
113
+ elif base and cert and not Path(cert).is_absolute():
114
+ cert = str(base / cert)
115
+ if base and key and not Path(key).is_absolute():
116
+ key = str(base / key)
117
+ if base and ca and not Path(ca).is_absolute():
118
+ ca = str(base / ca)
119
+ return SvoChunkerClient(
120
+ protocol=getattr(args, "protocol", "mtls"),
121
+ host=getattr(args, "host", "127.0.0.1"),
122
+ port=getattr(args, "port", 8009),
123
+ cert=cert or None,
124
+ key=key or None,
125
+ ca=ca or None,
126
+ timeout=getattr(args, "timeout", 300.0),
127
+ certs_dir=str(base) if base else None,
128
+ )
129
+
130
+
131
+ async def _cmd_list(args: argparse.Namespace) -> int:
132
+ client = _client_from_args(args)
133
+ try:
134
+ async with client:
135
+ names = await client.list_commands()
136
+ for n in names:
137
+ print(n)
138
+ return 0
139
+ except SvoConnectionError as e:
140
+ print(f"Error: {e}", file=sys.stderr)
141
+ return 1
142
+
143
+
144
+ async def _cmd_help(args: argparse.Namespace) -> int:
145
+ client = _client_from_args(args)
146
+ command_name = getattr(args, "command", None)
147
+ try:
148
+ async with client:
149
+ help_result = await client.help(command_name=command_name)
150
+ data = help_result.get("data", help_result)
151
+ if not isinstance(data, dict):
152
+ print(json.dumps(help_result, indent=2, ensure_ascii=False))
153
+ return 0
154
+ if command_name and (data.get("name") or data.get("schema")):
155
+ print(_format_help_for_command(data))
156
+ else:
157
+ print(_format_help_list(data))
158
+ return 0
159
+ except SvoConnectionError as e:
160
+ print(f"Error: {e}", file=sys.stderr)
161
+ return 1
162
+
163
+
164
+ async def _cmd_run(args: argparse.Namespace) -> int:
165
+ client = _client_from_args(args)
166
+ command = getattr(args, "command", None)
167
+ params = getattr(args, "params", {})
168
+ if not isinstance(command, str):
169
+ print("Error: command name required", file=sys.stderr)
170
+ return 1
171
+ try:
172
+ async with client:
173
+ result = await client.call(command, params)
174
+ print(json.dumps(result, indent=2, ensure_ascii=False))
175
+ return 0
176
+ except (
177
+ SvoConnectionError,
178
+ SvoTimeoutError,
179
+ SvoChunkingError,
180
+ SvoValidationError,
181
+ ) as e:
182
+ print(f"Error: {e}", file=sys.stderr)
183
+ return 1
184
+
185
+
186
+ def _build_parser() -> argparse.ArgumentParser:
187
+ parser = argparse.ArgumentParser(
188
+ description="SVO Chunker client. Commands and help from server.",
189
+ )
190
+ _add_network_args(parser)
191
+ sub = parser.add_subparsers(dest="subcommand", required=True)
192
+
193
+ p_list = sub.add_parser("list", help="List available commands (from server)")
194
+ p_list.set_defaults(_run=_cmd_list)
195
+
196
+ p_help = sub.add_parser("help", help="Show help from server (all or one command)")
197
+ p_help.add_argument("command", nargs="?", default=None)
198
+ p_help.set_defaults(_run=_cmd_help)
199
+
200
+ p_run = sub.add_parser("run", help="Run a server command")
201
+ p_run.add_argument("command", help="Command name")
202
+ p_run.add_argument(
203
+ "params", nargs="*", default=[], help="key=value or --params-json"
204
+ )
205
+ p_run.set_defaults(_run=_cmd_run)
206
+
207
+ return parser
208
+
209
+
210
+ def main() -> int:
211
+ parser = _build_parser()
212
+ args = parser.parse_args()
213
+
214
+ if getattr(args, "subcommand", None) == "run":
215
+ try:
216
+ args.params = _parse_params(getattr(args, "params", []))
217
+ except ValueError as e:
218
+ print(f"Error: {e}", file=sys.stderr)
219
+ return 1
220
+
221
+ return asyncio.run(args._run(args))
222
+
223
+
224
+ if __name__ == "__main__":
225
+ sys.exit(main())