superdoc-sdk 1.0.0__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.
- superdoc/__init__.py +28 -0
- superdoc/client.py +424 -0
- superdoc/embedded_cli.py +113 -0
- superdoc/errors.py +19 -0
- superdoc/generated/__init__.py +2 -0
- superdoc/generated/client.py +55952 -0
- superdoc/generated/contract.py +7 -0
- superdoc/protocol.py +312 -0
- superdoc/runtime.py +119 -0
- superdoc/skill_api.py +154 -0
- superdoc/skills/__init__.py +1 -0
- superdoc/skills/editing-docx.md +31 -0
- superdoc/test_parity_helper.py +76 -0
- superdoc/tools/catalog.json +3913 -0
- superdoc/tools/intent_dispatch_generated.py +122 -0
- superdoc/tools/tools-policy.json +43 -0
- superdoc/tools/tools.anthropic.json +3621 -0
- superdoc/tools/tools.generic.json +3710 -0
- superdoc/tools/tools.openai.json +3648 -0
- superdoc/tools/tools.vercel.json +3648 -0
- superdoc/tools_api.py +209 -0
- superdoc/transport.py +725 -0
- superdoc_sdk-1.0.0.dist-info/METADATA +407 -0
- superdoc_sdk-1.0.0.dist-info/RECORD +26 -0
- superdoc_sdk-1.0.0.dist-info/WHEEL +5 -0
- superdoc_sdk-1.0.0.dist-info/top_level.txt +1 -0
superdoc/__init__.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from .client import AsyncSuperDocClient, AsyncSuperDocDocument, SuperDocClient, SuperDocDocument
|
|
2
|
+
from .errors import SuperDocError
|
|
3
|
+
from .skill_api import get_skill, install_skill, list_skills
|
|
4
|
+
from .tools_api import (
|
|
5
|
+
choose_tools,
|
|
6
|
+
dispatch_superdoc_tool,
|
|
7
|
+
dispatch_superdoc_tool_async,
|
|
8
|
+
get_system_prompt,
|
|
9
|
+
get_tool_catalog,
|
|
10
|
+
list_tools,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"SuperDocClient",
|
|
15
|
+
"AsyncSuperDocClient",
|
|
16
|
+
"SuperDocDocument",
|
|
17
|
+
"AsyncSuperDocDocument",
|
|
18
|
+
"SuperDocError",
|
|
19
|
+
"get_skill",
|
|
20
|
+
"install_skill",
|
|
21
|
+
"list_skills",
|
|
22
|
+
"get_tool_catalog",
|
|
23
|
+
"list_tools",
|
|
24
|
+
"choose_tools",
|
|
25
|
+
"dispatch_superdoc_tool",
|
|
26
|
+
"dispatch_superdoc_tool_async",
|
|
27
|
+
"get_system_prompt",
|
|
28
|
+
]
|
superdoc/client.py
ADDED
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
"""SuperDoc client and document handle classes.
|
|
2
|
+
|
|
3
|
+
The client manages transport lifecycle and acts as a document factory.
|
|
4
|
+
Document handles bind a single open session and expose all document operations.
|
|
5
|
+
|
|
6
|
+
client = AsyncSuperDocClient(user={"name": "bot"})
|
|
7
|
+
await client.connect()
|
|
8
|
+
doc = await client.open({"doc": "path/to/file.docx"})
|
|
9
|
+
markdown = await doc.get_markdown()
|
|
10
|
+
await doc.close()
|
|
11
|
+
await client.dispose()
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from typing import Any, Dict, Literal, Optional
|
|
17
|
+
|
|
18
|
+
from .errors import SuperDocError
|
|
19
|
+
from .generated.client import _AsyncDocApi, _SyncDocApi, _AsyncBoundDocApi, _SyncBoundDocApi
|
|
20
|
+
from .runtime import SuperDocAsyncRuntime, SuperDocSyncRuntime
|
|
21
|
+
|
|
22
|
+
UserIdentity = Dict[str, str]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
# Session-bound runtime wrapper
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
class _BoundSyncRuntime:
|
|
30
|
+
"""Wraps a raw runtime and injects a fixed sessionId into every invoke call."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, runtime: SuperDocSyncRuntime, session_id: str) -> None:
|
|
33
|
+
self._runtime = runtime
|
|
34
|
+
self._session_id = session_id
|
|
35
|
+
self._closed = False
|
|
36
|
+
|
|
37
|
+
def invoke(
|
|
38
|
+
self,
|
|
39
|
+
operation_id: str,
|
|
40
|
+
params: Optional[Dict[str, Any]] = None,
|
|
41
|
+
*,
|
|
42
|
+
timeout_ms: Optional[int] = None,
|
|
43
|
+
stdin_bytes: Optional[bytes] = None,
|
|
44
|
+
) -> Dict[str, Any]:
|
|
45
|
+
if self._closed:
|
|
46
|
+
raise SuperDocError(
|
|
47
|
+
'Document handle is closed.',
|
|
48
|
+
code='DOCUMENT_CLOSED',
|
|
49
|
+
details={'sessionId': self._session_id},
|
|
50
|
+
)
|
|
51
|
+
merged = {**(params or {}), 'sessionId': self._session_id}
|
|
52
|
+
return self._runtime.invoke(operation_id, merged, timeout_ms=timeout_ms, stdin_bytes=stdin_bytes)
|
|
53
|
+
|
|
54
|
+
def mark_closed(self) -> None:
|
|
55
|
+
self._closed = True
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class _BoundAsyncRuntime:
|
|
59
|
+
"""Async version of _BoundSyncRuntime."""
|
|
60
|
+
|
|
61
|
+
def __init__(self, runtime: SuperDocAsyncRuntime, session_id: str) -> None:
|
|
62
|
+
self._runtime = runtime
|
|
63
|
+
self._session_id = session_id
|
|
64
|
+
self._closed = False
|
|
65
|
+
|
|
66
|
+
async def invoke(
|
|
67
|
+
self,
|
|
68
|
+
operation_id: str,
|
|
69
|
+
params: Optional[Dict[str, Any]] = None,
|
|
70
|
+
*,
|
|
71
|
+
timeout_ms: Optional[int] = None,
|
|
72
|
+
stdin_bytes: Optional[bytes] = None,
|
|
73
|
+
) -> Dict[str, Any]:
|
|
74
|
+
if self._closed:
|
|
75
|
+
raise SuperDocError(
|
|
76
|
+
'Document handle is closed.',
|
|
77
|
+
code='DOCUMENT_CLOSED',
|
|
78
|
+
details={'sessionId': self._session_id},
|
|
79
|
+
)
|
|
80
|
+
merged = {**(params or {}), 'sessionId': self._session_id}
|
|
81
|
+
return await self._runtime.invoke(operation_id, merged, timeout_ms=timeout_ms, stdin_bytes=stdin_bytes)
|
|
82
|
+
|
|
83
|
+
def mark_closed(self) -> None:
|
|
84
|
+
self._closed = True
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# ---------------------------------------------------------------------------
|
|
88
|
+
# Document handles
|
|
89
|
+
# ---------------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
class SuperDocDocument:
|
|
92
|
+
"""Bound document handle for synchronous workflows.
|
|
93
|
+
|
|
94
|
+
All document operations are available as methods on this handle.
|
|
95
|
+
The handle injects its session id automatically — callers never pass
|
|
96
|
+
doc or sessionId.
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
def __init__(
|
|
100
|
+
self,
|
|
101
|
+
bound_runtime: _BoundSyncRuntime,
|
|
102
|
+
session_id: str,
|
|
103
|
+
open_result: Dict[str, Any],
|
|
104
|
+
client: SuperDocClient,
|
|
105
|
+
) -> None:
|
|
106
|
+
self._bound_runtime = bound_runtime
|
|
107
|
+
self._session_id = session_id
|
|
108
|
+
self._open_result = open_result
|
|
109
|
+
self._client = client
|
|
110
|
+
self._api = _SyncBoundDocApi(bound_runtime)
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def session_id(self) -> str:
|
|
114
|
+
return self._session_id
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def open_result(self) -> Dict[str, Any]:
|
|
118
|
+
"""Read-only snapshot of the initial doc.open response metadata."""
|
|
119
|
+
return self._open_result
|
|
120
|
+
|
|
121
|
+
def __getattr__(self, name: str) -> Any:
|
|
122
|
+
return getattr(self._api, name)
|
|
123
|
+
|
|
124
|
+
def close(
|
|
125
|
+
self,
|
|
126
|
+
params: Optional[Dict[str, Any]] = None,
|
|
127
|
+
*,
|
|
128
|
+
timeout_ms: Optional[int] = None,
|
|
129
|
+
stdin_bytes: Optional[bytes] = None,
|
|
130
|
+
) -> Any:
|
|
131
|
+
result = self._bound_runtime.invoke(
|
|
132
|
+
'doc.close', params or {}, timeout_ms=timeout_ms, stdin_bytes=stdin_bytes,
|
|
133
|
+
)
|
|
134
|
+
self._bound_runtime.mark_closed()
|
|
135
|
+
self._client._remove_handle(self._session_id)
|
|
136
|
+
return result
|
|
137
|
+
|
|
138
|
+
def save(
|
|
139
|
+
self,
|
|
140
|
+
params: Optional[Dict[str, Any]] = None,
|
|
141
|
+
*,
|
|
142
|
+
timeout_ms: Optional[int] = None,
|
|
143
|
+
stdin_bytes: Optional[bytes] = None,
|
|
144
|
+
) -> Any:
|
|
145
|
+
return self._bound_runtime.invoke(
|
|
146
|
+
'doc.save', params or {}, timeout_ms=timeout_ms, stdin_bytes=stdin_bytes,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
def mark_closed(self) -> None:
|
|
150
|
+
"""Mark this handle as closed. Called by client.dispose()."""
|
|
151
|
+
self._bound_runtime.mark_closed()
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class AsyncSuperDocDocument:
|
|
155
|
+
"""Bound document handle for asynchronous workflows.
|
|
156
|
+
|
|
157
|
+
All document operations are available as methods on this handle.
|
|
158
|
+
The handle injects its session id automatically — callers never pass
|
|
159
|
+
doc or sessionId.
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
def __init__(
|
|
163
|
+
self,
|
|
164
|
+
bound_runtime: _BoundAsyncRuntime,
|
|
165
|
+
session_id: str,
|
|
166
|
+
open_result: Dict[str, Any],
|
|
167
|
+
client: AsyncSuperDocClient,
|
|
168
|
+
) -> None:
|
|
169
|
+
self._bound_runtime = bound_runtime
|
|
170
|
+
self._session_id = session_id
|
|
171
|
+
self._open_result = open_result
|
|
172
|
+
self._client = client
|
|
173
|
+
self._api = _AsyncBoundDocApi(bound_runtime)
|
|
174
|
+
|
|
175
|
+
@property
|
|
176
|
+
def session_id(self) -> str:
|
|
177
|
+
return self._session_id
|
|
178
|
+
|
|
179
|
+
@property
|
|
180
|
+
def open_result(self) -> Dict[str, Any]:
|
|
181
|
+
"""Read-only snapshot of the initial doc.open response metadata."""
|
|
182
|
+
return self._open_result
|
|
183
|
+
|
|
184
|
+
def __getattr__(self, name: str) -> Any:
|
|
185
|
+
return getattr(self._api, name)
|
|
186
|
+
|
|
187
|
+
async def close(
|
|
188
|
+
self,
|
|
189
|
+
params: Optional[Dict[str, Any]] = None,
|
|
190
|
+
*,
|
|
191
|
+
timeout_ms: Optional[int] = None,
|
|
192
|
+
stdin_bytes: Optional[bytes] = None,
|
|
193
|
+
) -> Any:
|
|
194
|
+
result = await self._bound_runtime.invoke(
|
|
195
|
+
'doc.close', params or {}, timeout_ms=timeout_ms, stdin_bytes=stdin_bytes,
|
|
196
|
+
)
|
|
197
|
+
self._bound_runtime.mark_closed()
|
|
198
|
+
self._client._remove_handle(self._session_id)
|
|
199
|
+
return result
|
|
200
|
+
|
|
201
|
+
async def save(
|
|
202
|
+
self,
|
|
203
|
+
params: Optional[Dict[str, Any]] = None,
|
|
204
|
+
*,
|
|
205
|
+
timeout_ms: Optional[int] = None,
|
|
206
|
+
stdin_bytes: Optional[bytes] = None,
|
|
207
|
+
) -> Any:
|
|
208
|
+
return await self._bound_runtime.invoke(
|
|
209
|
+
'doc.save', params or {}, timeout_ms=timeout_ms, stdin_bytes=stdin_bytes,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
def mark_closed(self) -> None:
|
|
213
|
+
"""Mark this handle as closed. Called by client.dispose()."""
|
|
214
|
+
self._bound_runtime.mark_closed()
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
# ---------------------------------------------------------------------------
|
|
218
|
+
# Clients
|
|
219
|
+
# ---------------------------------------------------------------------------
|
|
220
|
+
|
|
221
|
+
class SuperDocClient:
|
|
222
|
+
"""Synchronous SuperDoc client — transport manager and document factory.
|
|
223
|
+
|
|
224
|
+
Use client.open() to get bound document handles. Each handle is
|
|
225
|
+
independently session-scoped and safe for concurrent use.
|
|
226
|
+
"""
|
|
227
|
+
|
|
228
|
+
def __init__(
|
|
229
|
+
self,
|
|
230
|
+
*,
|
|
231
|
+
env: dict[str, str] | None = None,
|
|
232
|
+
startup_timeout_ms: int = 5_000,
|
|
233
|
+
shutdown_timeout_ms: int = 5_000,
|
|
234
|
+
request_timeout_ms: int | None = None,
|
|
235
|
+
watchdog_timeout_ms: int = 30_000,
|
|
236
|
+
default_change_mode: Literal['direct', 'tracked'] | None = None,
|
|
237
|
+
user: UserIdentity | None = None,
|
|
238
|
+
) -> None:
|
|
239
|
+
self._runtime = SuperDocSyncRuntime(
|
|
240
|
+
env=env,
|
|
241
|
+
startup_timeout_ms=startup_timeout_ms,
|
|
242
|
+
shutdown_timeout_ms=shutdown_timeout_ms,
|
|
243
|
+
request_timeout_ms=request_timeout_ms,
|
|
244
|
+
watchdog_timeout_ms=watchdog_timeout_ms,
|
|
245
|
+
default_change_mode=default_change_mode,
|
|
246
|
+
user=user,
|
|
247
|
+
)
|
|
248
|
+
self._raw_api = _SyncDocApi(self._runtime)
|
|
249
|
+
self._handles: Dict[str, SuperDocDocument] = {}
|
|
250
|
+
|
|
251
|
+
def connect(self) -> None:
|
|
252
|
+
"""Explicitly connect to the host process.
|
|
253
|
+
|
|
254
|
+
Optional — the first invoke() call will auto-connect if needed.
|
|
255
|
+
"""
|
|
256
|
+
self._runtime.connect()
|
|
257
|
+
|
|
258
|
+
def open(
|
|
259
|
+
self,
|
|
260
|
+
params: Dict[str, Any],
|
|
261
|
+
*,
|
|
262
|
+
timeout_ms: Optional[int] = None,
|
|
263
|
+
stdin_bytes: Optional[bytes] = None,
|
|
264
|
+
) -> SuperDocDocument:
|
|
265
|
+
"""Open a document and return a bound document handle.
|
|
266
|
+
|
|
267
|
+
The returned handle injects its session id into every operation
|
|
268
|
+
automatically. The same file can be opened multiple times with
|
|
269
|
+
different session ids (useful for diff workflows).
|
|
270
|
+
"""
|
|
271
|
+
explicit_session_id = params.get('sessionId')
|
|
272
|
+
if explicit_session_id and explicit_session_id in self._handles:
|
|
273
|
+
raise SuperDocError(
|
|
274
|
+
f'Session id already open in this client: {explicit_session_id}',
|
|
275
|
+
code='SESSION_ALREADY_OPEN',
|
|
276
|
+
details={'sessionId': explicit_session_id},
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
result = self._raw_api.open(params, timeout_ms=timeout_ms, stdin_bytes=stdin_bytes)
|
|
280
|
+
context_id = result.get('contextId', '')
|
|
281
|
+
|
|
282
|
+
bound = _BoundSyncRuntime(self._runtime, context_id)
|
|
283
|
+
handle = SuperDocDocument(bound, context_id, result, self)
|
|
284
|
+
self._handles[context_id] = handle
|
|
285
|
+
return handle
|
|
286
|
+
|
|
287
|
+
def describe(
|
|
288
|
+
self,
|
|
289
|
+
params: Dict[str, Any] | None = None,
|
|
290
|
+
*,
|
|
291
|
+
timeout_ms: Optional[int] = None,
|
|
292
|
+
stdin_bytes: Optional[bytes] = None,
|
|
293
|
+
) -> Any:
|
|
294
|
+
return self._raw_api.describe(params, timeout_ms=timeout_ms, stdin_bytes=stdin_bytes)
|
|
295
|
+
|
|
296
|
+
def describe_command(
|
|
297
|
+
self,
|
|
298
|
+
params: Dict[str, Any] | None = None,
|
|
299
|
+
*,
|
|
300
|
+
timeout_ms: Optional[int] = None,
|
|
301
|
+
stdin_bytes: Optional[bytes] = None,
|
|
302
|
+
) -> Any:
|
|
303
|
+
return self._raw_api.describe_command(params, timeout_ms=timeout_ms, stdin_bytes=stdin_bytes)
|
|
304
|
+
|
|
305
|
+
def dispose(self) -> None:
|
|
306
|
+
"""Gracefully shut down the host process and invalidate all open handles."""
|
|
307
|
+
for handle in self._handles.values():
|
|
308
|
+
handle.mark_closed()
|
|
309
|
+
self._handles.clear()
|
|
310
|
+
self._runtime.dispose()
|
|
311
|
+
|
|
312
|
+
def _remove_handle(self, session_id: str) -> None:
|
|
313
|
+
self._handles.pop(session_id, None)
|
|
314
|
+
|
|
315
|
+
def __enter__(self) -> SuperDocClient:
|
|
316
|
+
self.connect()
|
|
317
|
+
return self
|
|
318
|
+
|
|
319
|
+
def __exit__(self, *exc: object) -> None:
|
|
320
|
+
self.dispose()
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
class AsyncSuperDocClient:
|
|
324
|
+
"""Asynchronous SuperDoc client — transport manager and document factory.
|
|
325
|
+
|
|
326
|
+
Use client.open() to get bound document handles. Each handle is
|
|
327
|
+
independently session-scoped and safe for concurrent use.
|
|
328
|
+
"""
|
|
329
|
+
|
|
330
|
+
def __init__(
|
|
331
|
+
self,
|
|
332
|
+
*,
|
|
333
|
+
env: dict[str, str] | None = None,
|
|
334
|
+
startup_timeout_ms: int = 5_000,
|
|
335
|
+
shutdown_timeout_ms: int = 5_000,
|
|
336
|
+
request_timeout_ms: int | None = None,
|
|
337
|
+
watchdog_timeout_ms: int = 30_000,
|
|
338
|
+
max_queue_depth: int = 100,
|
|
339
|
+
default_change_mode: Literal['direct', 'tracked'] | None = None,
|
|
340
|
+
user: UserIdentity | None = None,
|
|
341
|
+
) -> None:
|
|
342
|
+
self._runtime = SuperDocAsyncRuntime(
|
|
343
|
+
env=env,
|
|
344
|
+
startup_timeout_ms=startup_timeout_ms,
|
|
345
|
+
shutdown_timeout_ms=shutdown_timeout_ms,
|
|
346
|
+
request_timeout_ms=request_timeout_ms,
|
|
347
|
+
watchdog_timeout_ms=watchdog_timeout_ms,
|
|
348
|
+
max_queue_depth=max_queue_depth,
|
|
349
|
+
default_change_mode=default_change_mode,
|
|
350
|
+
user=user,
|
|
351
|
+
)
|
|
352
|
+
self._raw_api = _AsyncDocApi(self._runtime)
|
|
353
|
+
self._handles: Dict[str, AsyncSuperDocDocument] = {}
|
|
354
|
+
|
|
355
|
+
async def connect(self) -> None:
|
|
356
|
+
"""Explicitly connect to the host process.
|
|
357
|
+
|
|
358
|
+
Optional — the first invoke() call will auto-connect if needed.
|
|
359
|
+
"""
|
|
360
|
+
await self._runtime.connect()
|
|
361
|
+
|
|
362
|
+
async def open(
|
|
363
|
+
self,
|
|
364
|
+
params: Dict[str, Any],
|
|
365
|
+
*,
|
|
366
|
+
timeout_ms: Optional[int] = None,
|
|
367
|
+
stdin_bytes: Optional[bytes] = None,
|
|
368
|
+
) -> AsyncSuperDocDocument:
|
|
369
|
+
"""Open a document and return a bound document handle.
|
|
370
|
+
|
|
371
|
+
The returned handle injects its session id into every operation
|
|
372
|
+
automatically. The same file can be opened multiple times with
|
|
373
|
+
different session ids (useful for diff workflows).
|
|
374
|
+
"""
|
|
375
|
+
explicit_session_id = params.get('sessionId')
|
|
376
|
+
if explicit_session_id and explicit_session_id in self._handles:
|
|
377
|
+
raise SuperDocError(
|
|
378
|
+
f'Session id already open in this client: {explicit_session_id}',
|
|
379
|
+
code='SESSION_ALREADY_OPEN',
|
|
380
|
+
details={'sessionId': explicit_session_id},
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
result = await self._raw_api.open(params, timeout_ms=timeout_ms, stdin_bytes=stdin_bytes)
|
|
384
|
+
context_id = result.get('contextId', '')
|
|
385
|
+
|
|
386
|
+
bound = _BoundAsyncRuntime(self._runtime, context_id)
|
|
387
|
+
handle = AsyncSuperDocDocument(bound, context_id, result, self)
|
|
388
|
+
self._handles[context_id] = handle
|
|
389
|
+
return handle
|
|
390
|
+
|
|
391
|
+
async def describe(
|
|
392
|
+
self,
|
|
393
|
+
params: Dict[str, Any] | None = None,
|
|
394
|
+
*,
|
|
395
|
+
timeout_ms: Optional[int] = None,
|
|
396
|
+
stdin_bytes: Optional[bytes] = None,
|
|
397
|
+
) -> Any:
|
|
398
|
+
return await self._raw_api.describe(params, timeout_ms=timeout_ms, stdin_bytes=stdin_bytes)
|
|
399
|
+
|
|
400
|
+
async def describe_command(
|
|
401
|
+
self,
|
|
402
|
+
params: Dict[str, Any] | None = None,
|
|
403
|
+
*,
|
|
404
|
+
timeout_ms: Optional[int] = None,
|
|
405
|
+
stdin_bytes: Optional[bytes] = None,
|
|
406
|
+
) -> Any:
|
|
407
|
+
return await self._raw_api.describe_command(params, timeout_ms=timeout_ms, stdin_bytes=stdin_bytes)
|
|
408
|
+
|
|
409
|
+
async def dispose(self) -> None:
|
|
410
|
+
"""Gracefully shut down the host process and invalidate all open handles."""
|
|
411
|
+
for handle in self._handles.values():
|
|
412
|
+
handle.mark_closed()
|
|
413
|
+
self._handles.clear()
|
|
414
|
+
await self._runtime.dispose()
|
|
415
|
+
|
|
416
|
+
def _remove_handle(self, session_id: str) -> None:
|
|
417
|
+
self._handles.pop(session_id, None)
|
|
418
|
+
|
|
419
|
+
async def __aenter__(self) -> AsyncSuperDocClient:
|
|
420
|
+
await self.connect()
|
|
421
|
+
return self
|
|
422
|
+
|
|
423
|
+
async def __aexit__(self, *exc: object) -> None:
|
|
424
|
+
await self.dispose()
|
superdoc/embedded_cli.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import platform
|
|
5
|
+
from importlib import resources
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from .errors import SuperDocError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# Maps target triple → companion package module name.
|
|
13
|
+
_TARGET_TO_COMPANION_MODULE = {
|
|
14
|
+
'darwin-arm64': 'superdoc_sdk_cli_darwin_arm64',
|
|
15
|
+
'darwin-x64': 'superdoc_sdk_cli_darwin_x64',
|
|
16
|
+
'linux-x64': 'superdoc_sdk_cli_linux_x64',
|
|
17
|
+
'linux-arm64': 'superdoc_sdk_cli_linux_arm64',
|
|
18
|
+
'windows-x64': 'superdoc_sdk_cli_windows_x64',
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _normalized_machine(value: str) -> str:
|
|
23
|
+
normalized = value.strip().lower()
|
|
24
|
+
if normalized in {'x86_64', 'amd64'}:
|
|
25
|
+
return 'x64'
|
|
26
|
+
if normalized in {'aarch64', 'arm64'}:
|
|
27
|
+
return 'arm64'
|
|
28
|
+
return normalized
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _resolve_target() -> Optional[str]:
|
|
32
|
+
system = platform.system().lower()
|
|
33
|
+
machine = _normalized_machine(platform.machine())
|
|
34
|
+
|
|
35
|
+
if system == 'darwin' and machine == 'arm64':
|
|
36
|
+
return 'darwin-arm64'
|
|
37
|
+
if system == 'darwin' and machine == 'x64':
|
|
38
|
+
return 'darwin-x64'
|
|
39
|
+
if system == 'linux' and machine == 'x64':
|
|
40
|
+
return 'linux-x64'
|
|
41
|
+
if system == 'linux' and machine == 'arm64':
|
|
42
|
+
return 'linux-arm64'
|
|
43
|
+
if system == 'windows' and machine == 'x64':
|
|
44
|
+
return 'windows-x64'
|
|
45
|
+
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _resolve_binary_name(target: str) -> str:
|
|
50
|
+
return 'superdoc.exe' if target.startswith('windows-') else 'superdoc'
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _resolve_from_companion_package(target: str) -> Optional[str]:
|
|
54
|
+
"""Try #1: import the installed platform companion package."""
|
|
55
|
+
module_name = _TARGET_TO_COMPANION_MODULE.get(target)
|
|
56
|
+
if not module_name:
|
|
57
|
+
return None
|
|
58
|
+
try:
|
|
59
|
+
module = __import__(module_name)
|
|
60
|
+
return module.get_binary_path()
|
|
61
|
+
except (ImportError, FileNotFoundError):
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _resolve_from_vendor_fallback(target: str) -> Optional[str]:
|
|
66
|
+
"""Try #2: legacy _vendor/cli/ path (source/dev environments only).
|
|
67
|
+
|
|
68
|
+
This path only exists when running from a source checkout with
|
|
69
|
+
manually staged binaries — it is NOT shipped in published wheels.
|
|
70
|
+
"""
|
|
71
|
+
binary_name = _resolve_binary_name(target)
|
|
72
|
+
resource = resources.files('superdoc').joinpath('_vendor', 'cli', target, binary_name)
|
|
73
|
+
try:
|
|
74
|
+
candidate = Path(str(resource))
|
|
75
|
+
except Exception:
|
|
76
|
+
return None
|
|
77
|
+
return str(candidate) if candidate.exists() else None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def resolve_embedded_cli_path() -> str:
|
|
81
|
+
target = _resolve_target()
|
|
82
|
+
if target is None:
|
|
83
|
+
raise SuperDocError(
|
|
84
|
+
'No embedded SuperDoc CLI binary is available for this platform.',
|
|
85
|
+
code='UNSUPPORTED_PLATFORM',
|
|
86
|
+
details={'platform': platform.system(), 'machine': platform.machine()},
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Companion package (primary — used in published wheels)
|
|
90
|
+
path = _resolve_from_companion_package(target)
|
|
91
|
+
|
|
92
|
+
# Legacy vendor fallback (source/dev only — not shipped in wheels)
|
|
93
|
+
if path is None:
|
|
94
|
+
path = _resolve_from_vendor_fallback(target)
|
|
95
|
+
|
|
96
|
+
if path is None:
|
|
97
|
+
raise SuperDocError(
|
|
98
|
+
f'Embedded SuperDoc CLI binary is missing for this platform.\n'
|
|
99
|
+
f'Install the companion package: pip install superdoc-sdk-cli-{target}\n'
|
|
100
|
+
f'Or set SUPERDOC_CLI_BIN to a compatible superdoc binary path.',
|
|
101
|
+
code='CLI_BINARY_MISSING',
|
|
102
|
+
details={'target': target},
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# Ensure binary is executable on unix
|
|
106
|
+
if os.name != 'nt':
|
|
107
|
+
try:
|
|
108
|
+
mode = os.stat(path).st_mode
|
|
109
|
+
os.chmod(path, mode | 0o111)
|
|
110
|
+
except Exception:
|
|
111
|
+
pass
|
|
112
|
+
|
|
113
|
+
return path
|
superdoc/errors.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""SuperDoc SDK error types and host transport error codes."""
|
|
2
|
+
|
|
3
|
+
# Host transport error codes — used by transport.py and protocol.py.
|
|
4
|
+
HOST_DISCONNECTED = 'HOST_DISCONNECTED'
|
|
5
|
+
HOST_TIMEOUT = 'HOST_TIMEOUT'
|
|
6
|
+
HOST_QUEUE_FULL = 'HOST_QUEUE_FULL'
|
|
7
|
+
HOST_HANDSHAKE_FAILED = 'HOST_HANDSHAKE_FAILED'
|
|
8
|
+
HOST_PROTOCOL_ERROR = 'HOST_PROTOCOL_ERROR'
|
|
9
|
+
|
|
10
|
+
# JSON-RPC error code emitted by the CLI for operation timeouts.
|
|
11
|
+
JSON_RPC_TIMEOUT_CODE = -32011
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SuperDocError(Exception):
|
|
15
|
+
def __init__(self, message: str, code: str, details=None, exit_code=None):
|
|
16
|
+
super().__init__(message)
|
|
17
|
+
self.code = code
|
|
18
|
+
self.details = details
|
|
19
|
+
self.exit_code = exit_code
|