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 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()
@@ -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
@@ -0,0 +1,2 @@
1
+ from .client import _SyncDocApi, _AsyncDocApi
2
+ from .client import _SyncBoundDocApi, _AsyncBoundDocApi