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/__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,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())
|