superdoc-sdk 1.6.0.dev23__tar.gz → 1.6.0.dev29__tar.gz
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_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/PKG-INFO +6 -6
- {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/pyproject.toml +6 -6
- {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/superdoc/__init__.py +2 -0
- {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/superdoc/client.py +5 -0
- {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/superdoc/runtime.py +7 -1
- {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/superdoc/transport.py +179 -35
- {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/superdoc_sdk.egg-info/PKG-INFO +6 -6
- {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/superdoc_sdk.egg-info/requires.txt +5 -5
- superdoc_sdk-1.6.0.dev29/tests/test_transport.py +943 -0
- superdoc_sdk-1.6.0.dev23/tests/test_transport.py +0 -495
- {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/README.md +0 -0
- {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/setup.cfg +0 -0
- {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/superdoc/embedded_cli.py +0 -0
- {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/superdoc/errors.py +0 -0
- {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/superdoc/generated/__init__.py +0 -0
- {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/superdoc/generated/client.py +0 -0
- {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/superdoc/generated/contract.py +0 -0
- {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/superdoc/protocol.py +0 -0
- {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/superdoc/skill_api.py +0 -0
- {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/superdoc/skills/__init__.py +0 -0
- {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/superdoc/skills/editing-docx.md +0 -0
- {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/superdoc/test_parity_helper.py +0 -0
- {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/superdoc/tools/catalog.json +0 -0
- {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/superdoc/tools/intent_dispatch_generated.py +0 -0
- {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/superdoc/tools/system-prompt-mcp.md +0 -0
- {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/superdoc/tools/system-prompt.md +0 -0
- {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/superdoc/tools/tools-policy.json +0 -0
- {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/superdoc/tools/tools.anthropic.json +0 -0
- {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/superdoc/tools/tools.generic.json +0 -0
- {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/superdoc/tools/tools.openai.json +0 -0
- {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/superdoc/tools/tools.vercel.json +0 -0
- {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/superdoc/tools_api.py +0 -0
- {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/superdoc_sdk.egg-info/SOURCES.txt +0 -0
- {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/superdoc_sdk.egg-info/dependency_links.txt +0 -0
- {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/superdoc_sdk.egg-info/top_level.txt +0 -0
- {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/tests/test_client_method_naming.py +0 -0
- {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/tests/test_helpers_reexports.py +0 -0
- {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/tests/test_protocol.py +0 -0
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: superdoc-sdk
|
|
3
|
-
Version: 1.6.0.
|
|
3
|
+
Version: 1.6.0.dev29
|
|
4
4
|
Summary: SuperDoc SDK (CLI-backed)
|
|
5
5
|
Author: SuperDoc
|
|
6
6
|
License-Expression: AGPL-3.0
|
|
7
7
|
Requires-Python: >=3.9
|
|
8
8
|
Description-Content-Type: text/markdown
|
|
9
|
-
Requires-Dist: superdoc-sdk-cli-darwin-arm64==1.6.0.
|
|
10
|
-
Requires-Dist: superdoc-sdk-cli-darwin-x64==1.6.0.
|
|
11
|
-
Requires-Dist: superdoc-sdk-cli-linux-x64==1.6.0.
|
|
12
|
-
Requires-Dist: superdoc-sdk-cli-linux-arm64==1.6.0.
|
|
13
|
-
Requires-Dist: superdoc-sdk-cli-windows-x64==1.6.0.
|
|
9
|
+
Requires-Dist: superdoc-sdk-cli-darwin-arm64==1.6.0.dev29; platform_system == "Darwin" and (platform_machine == "arm64" or platform_machine == "aarch64" or platform_machine == "ARM64")
|
|
10
|
+
Requires-Dist: superdoc-sdk-cli-darwin-x64==1.6.0.dev29; platform_system == "Darwin" and (platform_machine == "x86_64" or platform_machine == "AMD64" or platform_machine == "amd64")
|
|
11
|
+
Requires-Dist: superdoc-sdk-cli-linux-x64==1.6.0.dev29; platform_system == "Linux" and (platform_machine == "x86_64" or platform_machine == "AMD64" or platform_machine == "amd64")
|
|
12
|
+
Requires-Dist: superdoc-sdk-cli-linux-arm64==1.6.0.dev29; platform_system == "Linux" and (platform_machine == "arm64" or platform_machine == "aarch64" or platform_machine == "ARM64")
|
|
13
|
+
Requires-Dist: superdoc-sdk-cli-windows-x64==1.6.0.dev29; platform_system == "Windows" and (platform_machine == "x86_64" or platform_machine == "AMD64" or platform_machine == "amd64")
|
|
14
14
|
|
|
15
15
|
# superdoc-sdk
|
|
16
16
|
|
|
@@ -4,18 +4,18 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "superdoc-sdk"
|
|
7
|
-
version = "1.6.0.
|
|
7
|
+
version = "1.6.0.dev29"
|
|
8
8
|
description = "SuperDoc SDK (CLI-backed)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
11
11
|
license = "AGPL-3.0"
|
|
12
12
|
authors = [{ name = "SuperDoc" }]
|
|
13
13
|
dependencies = [
|
|
14
|
-
"superdoc-sdk-cli-darwin-arm64==1.6.0.
|
|
15
|
-
"superdoc-sdk-cli-darwin-x64==1.6.0.
|
|
16
|
-
"superdoc-sdk-cli-linux-x64==1.6.0.
|
|
17
|
-
"superdoc-sdk-cli-linux-arm64==1.6.0.
|
|
18
|
-
"superdoc-sdk-cli-windows-x64==1.6.0.
|
|
14
|
+
"superdoc-sdk-cli-darwin-arm64==1.6.0.dev29; platform_system == 'Darwin' and (platform_machine == 'arm64' or platform_machine == 'aarch64' or platform_machine == 'ARM64')",
|
|
15
|
+
"superdoc-sdk-cli-darwin-x64==1.6.0.dev29; platform_system == 'Darwin' and (platform_machine == 'x86_64' or platform_machine == 'AMD64' or platform_machine == 'amd64')",
|
|
16
|
+
"superdoc-sdk-cli-linux-x64==1.6.0.dev29; platform_system == 'Linux' and (platform_machine == 'x86_64' or platform_machine == 'AMD64' or platform_machine == 'amd64')",
|
|
17
|
+
"superdoc-sdk-cli-linux-arm64==1.6.0.dev29; platform_system == 'Linux' and (platform_machine == 'arm64' or platform_machine == 'aarch64' or platform_machine == 'ARM64')",
|
|
18
|
+
"superdoc-sdk-cli-windows-x64==1.6.0.dev29; platform_system == 'Windows' and (platform_machine == 'x86_64' or platform_machine == 'AMD64' or platform_machine == 'amd64')",
|
|
19
19
|
]
|
|
20
20
|
|
|
21
21
|
[tool.setuptools]
|
|
@@ -10,6 +10,7 @@ from .tools_api import (
|
|
|
10
10
|
get_tool_catalog,
|
|
11
11
|
list_tools,
|
|
12
12
|
)
|
|
13
|
+
from .transport import DEFAULT_STDOUT_BUFFER_LIMIT_BYTES
|
|
13
14
|
|
|
14
15
|
__all__ = [
|
|
15
16
|
"SuperDocClient",
|
|
@@ -17,6 +18,7 @@ __all__ = [
|
|
|
17
18
|
"SuperDocDocument",
|
|
18
19
|
"AsyncSuperDocDocument",
|
|
19
20
|
"SuperDocError",
|
|
21
|
+
"DEFAULT_STDOUT_BUFFER_LIMIT_BYTES",
|
|
20
22
|
"get_skill",
|
|
21
23
|
"install_skill",
|
|
22
24
|
"list_skills",
|
|
@@ -22,6 +22,7 @@ from .generated.client import (
|
|
|
22
22
|
DocOpenResult as GeneratedDocOpenResult,
|
|
23
23
|
)
|
|
24
24
|
from .runtime import SuperDocAsyncRuntime, SuperDocSyncRuntime
|
|
25
|
+
from .transport import DEFAULT_STDOUT_BUFFER_LIMIT_BYTES
|
|
25
26
|
|
|
26
27
|
UserIdentity = Dict[str, str]
|
|
27
28
|
|
|
@@ -340,6 +341,9 @@ class AsyncSuperDocClient:
|
|
|
340
341
|
request_timeout_ms: int | None = None,
|
|
341
342
|
watchdog_timeout_ms: int = 30_000,
|
|
342
343
|
max_queue_depth: int = 100,
|
|
344
|
+
# Raise if a single host response can exceed this size (e.g. reading
|
|
345
|
+
# very large documents); otherwise the default is safe.
|
|
346
|
+
stdout_buffer_limit_bytes: int = DEFAULT_STDOUT_BUFFER_LIMIT_BYTES,
|
|
343
347
|
default_change_mode: Literal['direct', 'tracked'] | None = None,
|
|
344
348
|
user: UserIdentity | None = None,
|
|
345
349
|
) -> None:
|
|
@@ -350,6 +354,7 @@ class AsyncSuperDocClient:
|
|
|
350
354
|
request_timeout_ms=request_timeout_ms,
|
|
351
355
|
watchdog_timeout_ms=watchdog_timeout_ms,
|
|
352
356
|
max_queue_depth=max_queue_depth,
|
|
357
|
+
stdout_buffer_limit_bytes=stdout_buffer_limit_bytes,
|
|
353
358
|
default_change_mode=default_change_mode,
|
|
354
359
|
user=user,
|
|
355
360
|
)
|
|
@@ -14,7 +14,11 @@ from typing import Any, Dict, Optional
|
|
|
14
14
|
from .embedded_cli import resolve_embedded_cli_path
|
|
15
15
|
from .generated.contract import OPERATION_INDEX
|
|
16
16
|
from .protocol import normalize_default_change_mode
|
|
17
|
-
from .transport import
|
|
17
|
+
from .transport import (
|
|
18
|
+
DEFAULT_STDOUT_BUFFER_LIMIT_BYTES,
|
|
19
|
+
AsyncHostTransport,
|
|
20
|
+
SyncHostTransport,
|
|
21
|
+
)
|
|
18
22
|
|
|
19
23
|
|
|
20
24
|
class SuperDocSyncRuntime:
|
|
@@ -79,6 +83,7 @@ class SuperDocAsyncRuntime:
|
|
|
79
83
|
request_timeout_ms: Optional[int] = None,
|
|
80
84
|
watchdog_timeout_ms: int = 30_000,
|
|
81
85
|
max_queue_depth: int = 100,
|
|
86
|
+
stdout_buffer_limit_bytes: int = DEFAULT_STDOUT_BUFFER_LIMIT_BYTES,
|
|
82
87
|
default_change_mode: Optional[str] = None,
|
|
83
88
|
user: Optional[Dict[str, str]] = None,
|
|
84
89
|
) -> None:
|
|
@@ -93,6 +98,7 @@ class SuperDocAsyncRuntime:
|
|
|
93
98
|
request_timeout_ms=request_timeout_ms,
|
|
94
99
|
watchdog_timeout_ms=watchdog_timeout_ms,
|
|
95
100
|
max_queue_depth=max_queue_depth,
|
|
101
|
+
stdout_buffer_limit_bytes=stdout_buffer_limit_bytes,
|
|
96
102
|
default_change_mode=self._default_change_mode,
|
|
97
103
|
user=user,
|
|
98
104
|
)
|
|
@@ -43,6 +43,12 @@ from .protocol import (
|
|
|
43
43
|
|
|
44
44
|
logger = logging.getLogger('superdoc.transport')
|
|
45
45
|
|
|
46
|
+
# Default stdout StreamReader buffer for the async transport. Host responses
|
|
47
|
+
# are single newline-delimited JSON lines, so this caps the largest individual
|
|
48
|
+
# response a caller can receive. Raise it if your workload routinely produces
|
|
49
|
+
# responses above this size (e.g. whole-document reads on very large docs).
|
|
50
|
+
DEFAULT_STDOUT_BUFFER_LIMIT_BYTES = 64 * 1024 * 1024
|
|
51
|
+
|
|
46
52
|
# Opt-in debug logging via SUPERDOC_DEBUG=1 or SUPERDOC_LOG_LEVEL=debug.
|
|
47
53
|
# Only configures the named logger — never mutates root logging config.
|
|
48
54
|
_log_level = os.environ.get('SUPERDOC_LOG_LEVEL', '').lower()
|
|
@@ -399,6 +405,7 @@ class AsyncHostTransport:
|
|
|
399
405
|
request_timeout_ms: Optional[int] = None,
|
|
400
406
|
watchdog_timeout_ms: int = 30_000,
|
|
401
407
|
max_queue_depth: int = 100,
|
|
408
|
+
stdout_buffer_limit_bytes: int = DEFAULT_STDOUT_BUFFER_LIMIT_BYTES,
|
|
402
409
|
default_change_mode: Optional[ChangeMode] = None,
|
|
403
410
|
user: Optional[Dict[str, str]] = None,
|
|
404
411
|
) -> None:
|
|
@@ -409,11 +416,13 @@ class AsyncHostTransport:
|
|
|
409
416
|
self._request_timeout_ms = request_timeout_ms
|
|
410
417
|
self._watchdog_timeout_ms = watchdog_timeout_ms
|
|
411
418
|
self._max_queue_depth = max_queue_depth
|
|
419
|
+
self._stdout_buffer_limit_bytes = stdout_buffer_limit_bytes
|
|
412
420
|
self._default_change_mode = default_change_mode
|
|
413
421
|
self._user = user
|
|
414
422
|
|
|
415
423
|
self._process: Optional[asyncio.subprocess.Process] = None
|
|
416
424
|
self._reader_task: Optional[asyncio.Task] = None
|
|
425
|
+
self._cleanup_task: Optional[asyncio.Task] = None
|
|
417
426
|
self._pending: Dict[int, asyncio.Future] = {}
|
|
418
427
|
self._state = _State.DISCONNECTED
|
|
419
428
|
self._next_request_id = 1
|
|
@@ -428,7 +437,22 @@ class AsyncHostTransport:
|
|
|
428
437
|
|
|
429
438
|
async def dispose(self) -> None:
|
|
430
439
|
"""Gracefully shut down the host process."""
|
|
431
|
-
if self._state == _State.DISCONNECTED
|
|
440
|
+
if self._state == _State.DISCONNECTED:
|
|
441
|
+
return
|
|
442
|
+
if self._state == _State.DISPOSING:
|
|
443
|
+
# A reader-triggered cleanup is in flight (or an earlier teardown
|
|
444
|
+
# left state in DISPOSING briefly). Wait for it so the caller
|
|
445
|
+
# observes "host fully torn down" by the time dispose() returns.
|
|
446
|
+
# shield() so a cancelled dispose() doesn't interrupt _cleanup
|
|
447
|
+
# mid-flight and leak the host process.
|
|
448
|
+
existing = self._cleanup_task
|
|
449
|
+
if existing and not existing.done():
|
|
450
|
+
try:
|
|
451
|
+
await asyncio.shield(existing)
|
|
452
|
+
except asyncio.CancelledError:
|
|
453
|
+
raise
|
|
454
|
+
except Exception:
|
|
455
|
+
pass
|
|
432
456
|
return
|
|
433
457
|
|
|
434
458
|
self._stopping = True
|
|
@@ -507,6 +531,20 @@ class AsyncHostTransport:
|
|
|
507
531
|
|
|
508
532
|
async def _ensure_connected(self) -> None:
|
|
509
533
|
"""Lazy connect: spawn and handshake if not already connected."""
|
|
534
|
+
# Drain any in-flight teardown before spawning a new host. Without
|
|
535
|
+
# this, a concurrent reader-triggered cleanup would still be running
|
|
536
|
+
# when _start_host reassigns self._process / self._reader_task; the
|
|
537
|
+
# cleanup task would then cancel the fresh reader and kill the fresh
|
|
538
|
+
# process. shield() so we don't cancel the cleanup if our caller is.
|
|
539
|
+
cleanup = self._cleanup_task
|
|
540
|
+
if cleanup and not cleanup.done():
|
|
541
|
+
try:
|
|
542
|
+
await asyncio.shield(cleanup)
|
|
543
|
+
except asyncio.CancelledError:
|
|
544
|
+
raise
|
|
545
|
+
except Exception:
|
|
546
|
+
pass
|
|
547
|
+
|
|
510
548
|
if self._state == _State.CONNECTED and self._process and self._process.returncode is None:
|
|
511
549
|
return
|
|
512
550
|
|
|
@@ -531,12 +569,15 @@ class AsyncHostTransport:
|
|
|
531
569
|
args = [*prefix_args, 'host', '--stdio']
|
|
532
570
|
|
|
533
571
|
try:
|
|
572
|
+
# ``limit`` raises asyncio's StreamReader buffer above its 64 KiB
|
|
573
|
+
# default; host responses are single JSON lines and can exceed it.
|
|
534
574
|
self._process = await asyncio.create_subprocess_exec(
|
|
535
575
|
command, *args,
|
|
536
576
|
stdin=asyncio.subprocess.PIPE,
|
|
537
577
|
stdout=asyncio.subprocess.PIPE,
|
|
538
578
|
stderr=asyncio.subprocess.DEVNULL,
|
|
539
579
|
env={**os.environ, **self._env},
|
|
580
|
+
limit=self._stdout_buffer_limit_bytes,
|
|
540
581
|
)
|
|
541
582
|
logger.debug('Host spawned (pid=%s, bin=%s).', self._process.pid, self._cli_bin)
|
|
542
583
|
except Exception as exc:
|
|
@@ -582,7 +623,29 @@ class AsyncHostTransport:
|
|
|
582
623
|
|
|
583
624
|
try:
|
|
584
625
|
while True:
|
|
585
|
-
|
|
626
|
+
try:
|
|
627
|
+
raw = await process.stdout.readline()
|
|
628
|
+
except ValueError as exc:
|
|
629
|
+
# asyncio.StreamReader.readline() re-raises LimitOverrunError
|
|
630
|
+
# from readuntil() as ValueError when a single line exceeds
|
|
631
|
+
# `limit` (see CPython asyncio/streams.py). The host is still
|
|
632
|
+
# alive — schedule cleanup so a later dispose() doesn't
|
|
633
|
+
# short-circuit on DISCONNECTED state. Scoped to readline()
|
|
634
|
+
# only so unrelated ValueErrors from dispatch aren't
|
|
635
|
+
# reclassified as a buffer-limit error. _schedule_cleanup
|
|
636
|
+
# is a no-op when _stopping is set (graceful dispose path).
|
|
637
|
+
logger.debug('Reader loop buffer overflow: %s', exc)
|
|
638
|
+
self._schedule_cleanup(SuperDocError(
|
|
639
|
+
'Host response exceeded stdout buffer limit. '
|
|
640
|
+
'Raise stdout_buffer_limit_bytes to accommodate larger responses.',
|
|
641
|
+
code=HOST_PROTOCOL_ERROR,
|
|
642
|
+
details={
|
|
643
|
+
'message': str(exc),
|
|
644
|
+
'stdout_buffer_limit_bytes': self._stdout_buffer_limit_bytes,
|
|
645
|
+
},
|
|
646
|
+
))
|
|
647
|
+
return
|
|
648
|
+
|
|
586
649
|
if not raw:
|
|
587
650
|
# EOF — process died.
|
|
588
651
|
break
|
|
@@ -614,16 +677,16 @@ class AsyncHostTransport:
|
|
|
614
677
|
except Exception as exc:
|
|
615
678
|
logger.debug('Reader loop error: %s', exc)
|
|
616
679
|
|
|
617
|
-
# Reader exited (EOF or error) —
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
680
|
+
# Reader exited (EOF or unexpected error) — tear down the process so
|
|
681
|
+
# no orphaned host is left running, then reject pending futures.
|
|
682
|
+
# _schedule_cleanup is a no-op when _stopping is set (graceful
|
|
683
|
+
# dispose path) so we don't race the dispose teardown.
|
|
684
|
+
exit_code = process.returncode
|
|
685
|
+
self._schedule_cleanup(SuperDocError(
|
|
686
|
+
'Host process disconnected.',
|
|
687
|
+
code=HOST_DISCONNECTED,
|
|
688
|
+
details={'exit_code': exit_code, 'signal': None},
|
|
689
|
+
))
|
|
627
690
|
|
|
628
691
|
async def _send_request(self, method: str, params: Any, watchdog_ms: int) -> Any:
|
|
629
692
|
"""Send a JSON-RPC request and await the matching response future."""
|
|
@@ -687,34 +750,88 @@ class AsyncHostTransport:
|
|
|
687
750
|
future.set_exception(error)
|
|
688
751
|
|
|
689
752
|
async def _kill_and_reset(self) -> None:
|
|
690
|
-
"""Kill the host process and reset to DISCONNECTED.
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
)
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
753
|
+
"""Kill the host process and reset to DISCONNECTED.
|
|
754
|
+
|
|
755
|
+
Coordinates with `_schedule_cleanup` so callers (e.g. `_send_request`
|
|
756
|
+
on watchdog timeout or stdin write failure) don't run a parallel
|
|
757
|
+
`_cleanup` that races a reader-triggered cleanup on
|
|
758
|
+
`_reject_all_pending` and `process.kill`. If a cleanup is already in
|
|
759
|
+
flight, await it; otherwise own a fresh task in the same slot so a
|
|
760
|
+
later concurrent caller sees us instead of starting its own.
|
|
761
|
+
|
|
762
|
+
shield() the await so caller cancellation (e.g. an `invoke()` task
|
|
763
|
+
that times out and is then cancelled by the user) does NOT propagate
|
|
764
|
+
into `_cleanup` — interrupting cleanup mid-flight would leak the
|
|
765
|
+
subprocess and wedge state in DISPOSING.
|
|
766
|
+
"""
|
|
767
|
+
existing = self._cleanup_task
|
|
768
|
+
if existing and not existing.done():
|
|
699
769
|
try:
|
|
700
|
-
await
|
|
701
|
-
except
|
|
770
|
+
await asyncio.shield(existing)
|
|
771
|
+
except asyncio.CancelledError:
|
|
772
|
+
raise
|
|
773
|
+
except Exception:
|
|
702
774
|
pass
|
|
703
|
-
|
|
775
|
+
return
|
|
776
|
+
self._state = _State.DISPOSING
|
|
777
|
+
task = asyncio.create_task(self._cleanup(
|
|
778
|
+
SuperDocError('Host process disconnected.', code=HOST_DISCONNECTED),
|
|
779
|
+
))
|
|
780
|
+
self._cleanup_task = task
|
|
781
|
+
try:
|
|
782
|
+
await asyncio.shield(task)
|
|
783
|
+
except asyncio.CancelledError:
|
|
784
|
+
raise
|
|
785
|
+
except Exception:
|
|
786
|
+
pass
|
|
704
787
|
|
|
788
|
+
def _schedule_cleanup(self, error: SuperDocError) -> None:
|
|
789
|
+
"""Fire-and-forget teardown from inside the reader task.
|
|
790
|
+
|
|
791
|
+
Why a separate task: `_cleanup` cancels and awaits `self._reader_task`.
|
|
792
|
+
Awaiting it from inside the reader itself would deadlock — so we punt
|
|
793
|
+
to a fresh task, and by the time it runs the reader has already
|
|
794
|
+
returned (so cancel+await is a no-op).
|
|
795
|
+
|
|
796
|
+
Synchronously flips state to DISPOSING so concurrent `invoke()` callers
|
|
797
|
+
observe the failed transport immediately rather than passing the
|
|
798
|
+
CONNECTED fast path and blocking on a future the dead reader can never
|
|
799
|
+
resolve until `watchdog_timeout_ms`.
|
|
800
|
+
|
|
801
|
+
Skips when `_stopping` is set: a graceful `dispose()` is already
|
|
802
|
+
tearing down, and a parallel cleanup task would race on
|
|
803
|
+
`_reject_all_pending` and `process.kill`.
|
|
804
|
+
|
|
805
|
+
Idempotent: if a cleanup is already in flight, subsequent errors are
|
|
806
|
+
dropped — the first one wins. Callers may observe completion via
|
|
807
|
+
`self._cleanup_task`.
|
|
808
|
+
"""
|
|
809
|
+
if self._stopping:
|
|
810
|
+
return
|
|
811
|
+
if self._cleanup_task and not self._cleanup_task.done():
|
|
812
|
+
return
|
|
813
|
+
self._state = _State.DISPOSING
|
|
814
|
+
self._cleanup_task = asyncio.create_task(self._cleanup(error))
|
|
815
|
+
|
|
816
|
+
async def _cleanup(self, error: Optional[SuperDocError]) -> None:
|
|
817
|
+
"""Cancel reader, kill process, reject pending, reset state.
|
|
818
|
+
|
|
819
|
+
Capture handles and flip user-visible state SYNCHRONOUSLY at the top
|
|
820
|
+
before any awaits. That way, even if cancellation arrives during
|
|
821
|
+
`process.wait()`, observers see a consistent "torn down" transport
|
|
822
|
+
(state DISCONNECTED, _process None, pending futures rejected) rather
|
|
823
|
+
than a half-disposed one. The async work below is best-effort
|
|
824
|
+
process reaping.
|
|
825
|
+
"""
|
|
826
|
+
# Snapshot and clear before any await so concurrent callers see a
|
|
827
|
+
# fully torn-down transport from this point on.
|
|
828
|
+
reader_task = self._reader_task
|
|
705
829
|
process = self._process
|
|
706
|
-
|
|
707
|
-
try:
|
|
708
|
-
process.kill()
|
|
709
|
-
except Exception:
|
|
710
|
-
pass
|
|
711
|
-
try:
|
|
712
|
-
await asyncio.wait_for(process.wait(), timeout=2)
|
|
713
|
-
except (asyncio.TimeoutError, Exception):
|
|
714
|
-
pass
|
|
830
|
+
self._reader_task = None
|
|
715
831
|
self._process = None
|
|
832
|
+
self._state = _State.DISCONNECTED
|
|
716
833
|
|
|
717
|
-
if error:
|
|
834
|
+
if error is not None:
|
|
718
835
|
self._reject_all_pending(error)
|
|
719
836
|
else:
|
|
720
837
|
# Dispose path — reject remaining with generic disconnect.
|
|
@@ -722,4 +839,31 @@ class AsyncHostTransport:
|
|
|
722
839
|
SuperDocError('Host process was disposed.', code=HOST_DISCONNECTED),
|
|
723
840
|
)
|
|
724
841
|
|
|
725
|
-
|
|
842
|
+
try:
|
|
843
|
+
if reader_task and not reader_task.done():
|
|
844
|
+
reader_task.cancel()
|
|
845
|
+
try:
|
|
846
|
+
await reader_task
|
|
847
|
+
except (asyncio.CancelledError, Exception):
|
|
848
|
+
pass
|
|
849
|
+
|
|
850
|
+
if process:
|
|
851
|
+
try:
|
|
852
|
+
process.kill()
|
|
853
|
+
except Exception:
|
|
854
|
+
pass
|
|
855
|
+
try:
|
|
856
|
+
await asyncio.wait_for(process.wait(), timeout=2)
|
|
857
|
+
except (asyncio.TimeoutError, asyncio.CancelledError, Exception):
|
|
858
|
+
pass
|
|
859
|
+
finally:
|
|
860
|
+
# Release the task handle if we are the in-flight cleanup task,
|
|
861
|
+
# so introspection doesn't surface a stale done handle and the
|
|
862
|
+
# next teardown gets a fresh slot. Skip when called inline (e.g.
|
|
863
|
+
# from dispose) — that current task is not our cleanup task.
|
|
864
|
+
try:
|
|
865
|
+
current = asyncio.current_task()
|
|
866
|
+
except RuntimeError:
|
|
867
|
+
current = None
|
|
868
|
+
if current is not None and self._cleanup_task is current:
|
|
869
|
+
self._cleanup_task = None
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: superdoc-sdk
|
|
3
|
-
Version: 1.6.0.
|
|
3
|
+
Version: 1.6.0.dev29
|
|
4
4
|
Summary: SuperDoc SDK (CLI-backed)
|
|
5
5
|
Author: SuperDoc
|
|
6
6
|
License-Expression: AGPL-3.0
|
|
7
7
|
Requires-Python: >=3.9
|
|
8
8
|
Description-Content-Type: text/markdown
|
|
9
|
-
Requires-Dist: superdoc-sdk-cli-darwin-arm64==1.6.0.
|
|
10
|
-
Requires-Dist: superdoc-sdk-cli-darwin-x64==1.6.0.
|
|
11
|
-
Requires-Dist: superdoc-sdk-cli-linux-x64==1.6.0.
|
|
12
|
-
Requires-Dist: superdoc-sdk-cli-linux-arm64==1.6.0.
|
|
13
|
-
Requires-Dist: superdoc-sdk-cli-windows-x64==1.6.0.
|
|
9
|
+
Requires-Dist: superdoc-sdk-cli-darwin-arm64==1.6.0.dev29; platform_system == "Darwin" and (platform_machine == "arm64" or platform_machine == "aarch64" or platform_machine == "ARM64")
|
|
10
|
+
Requires-Dist: superdoc-sdk-cli-darwin-x64==1.6.0.dev29; platform_system == "Darwin" and (platform_machine == "x86_64" or platform_machine == "AMD64" or platform_machine == "amd64")
|
|
11
|
+
Requires-Dist: superdoc-sdk-cli-linux-x64==1.6.0.dev29; platform_system == "Linux" and (platform_machine == "x86_64" or platform_machine == "AMD64" or platform_machine == "amd64")
|
|
12
|
+
Requires-Dist: superdoc-sdk-cli-linux-arm64==1.6.0.dev29; platform_system == "Linux" and (platform_machine == "arm64" or platform_machine == "aarch64" or platform_machine == "ARM64")
|
|
13
|
+
Requires-Dist: superdoc-sdk-cli-windows-x64==1.6.0.dev29; platform_system == "Windows" and (platform_machine == "x86_64" or platform_machine == "AMD64" or platform_machine == "amd64")
|
|
14
14
|
|
|
15
15
|
# superdoc-sdk
|
|
16
16
|
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
|
|
2
2
|
[:platform_system == "Darwin" and (platform_machine == "arm64" or platform_machine == "aarch64" or platform_machine == "ARM64")]
|
|
3
|
-
superdoc-sdk-cli-darwin-arm64==1.6.0.
|
|
3
|
+
superdoc-sdk-cli-darwin-arm64==1.6.0.dev29
|
|
4
4
|
|
|
5
5
|
[:platform_system == "Darwin" and (platform_machine == "x86_64" or platform_machine == "AMD64" or platform_machine == "amd64")]
|
|
6
|
-
superdoc-sdk-cli-darwin-x64==1.6.0.
|
|
6
|
+
superdoc-sdk-cli-darwin-x64==1.6.0.dev29
|
|
7
7
|
|
|
8
8
|
[:platform_system == "Linux" and (platform_machine == "arm64" or platform_machine == "aarch64" or platform_machine == "ARM64")]
|
|
9
|
-
superdoc-sdk-cli-linux-arm64==1.6.0.
|
|
9
|
+
superdoc-sdk-cli-linux-arm64==1.6.0.dev29
|
|
10
10
|
|
|
11
11
|
[:platform_system == "Linux" and (platform_machine == "x86_64" or platform_machine == "AMD64" or platform_machine == "amd64")]
|
|
12
|
-
superdoc-sdk-cli-linux-x64==1.6.0.
|
|
12
|
+
superdoc-sdk-cli-linux-x64==1.6.0.dev29
|
|
13
13
|
|
|
14
14
|
[:platform_system == "Windows" and (platform_machine == "x86_64" or platform_machine == "AMD64" or platform_machine == "amd64")]
|
|
15
|
-
superdoc-sdk-cli-windows-x64==1.6.0.
|
|
15
|
+
superdoc-sdk-cli-windows-x64==1.6.0.dev29
|