doc-store-client-dev 0.1.185__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.
@@ -0,0 +1,1509 @@
1
+ """Thin typed facade over an injected ``mcp-proxy-adapter`` client."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import inspect
7
+ import re
8
+ from collections.abc import Awaitable, Callable, Mapping
9
+ from dataclasses import is_dataclass, replace
10
+ from typing import TYPE_CHECKING, Any, ClassVar, Protocol, TypeVar, cast
11
+
12
+ from chunk_metadata_adapter import ChunkQuery
13
+
14
+ from .models import (
15
+ BackupEmergencyResult,
16
+ BackupInspectResult,
17
+ BackupPointsResult,
18
+ BackupRecoverCatalogResult,
19
+ BackupRestoreResult,
20
+ BackupRunResult,
21
+ BackupStatusResult,
22
+ ChapterGetRequest,
23
+ ChapterGetResult,
24
+ ChapterTextGetRequest,
25
+ DocumentChunkRequest,
26
+ DocumentChunkResult,
27
+ DocumentCreateRequest,
28
+ DocumentCreateResult,
29
+ DocumentDeleteRequest,
30
+ DocumentDeleteResult,
31
+ DocumentGetRequest,
32
+ DocumentGetResult,
33
+ DocumentRebindRequest,
34
+ DocumentRebindResult,
35
+ DocumentUpdateRequest,
36
+ DocumentUpdateResult,
37
+ EntityGetRequest,
38
+ EntityGetResult,
39
+ EntityTextGetRequest,
40
+ EntityTextGetResult,
41
+ EntityIdsRequest,
42
+ EntityLifecycleResult,
43
+ EntityListRequest,
44
+ EntityListResult,
45
+ EntityOwnerTreeRequest,
46
+ EntityOwnerTreeResult,
47
+ EntityReferencesRequest,
48
+ EntityReferencesResult,
49
+ ParagraphGetByNumberRequest,
50
+ ParagraphGetByNumberResult,
51
+ ParagraphGetRequest,
52
+ ParagraphGetResult,
53
+ ProcessingStatusRequest,
54
+ ProcessingStatusResult,
55
+ SearchResult,
56
+ ServerError,
57
+ SemanticChunkMetadataUpdateRequest,
58
+ SemanticChunkMetadataUpdateResult,
59
+ SourceFileReconstructRequest,
60
+ TextReconstructionResult,
61
+ )
62
+
63
+ if TYPE_CHECKING:
64
+ from .models import (
65
+ ChunkHistoryRequest,
66
+ ChunkHistoryResult,
67
+ ChunkVersionAddRequest,
68
+ ChunkVersionAddResult,
69
+ ChunkVersionDeleteRequest,
70
+ ChunkVersionDeleteResult,
71
+ ChunkVersionDiffRequest,
72
+ ChunkVersionDiffResult,
73
+ ChunkVersionGetRequest,
74
+ ChunkVersionGetResult,
75
+ ChunkVersionListRequest,
76
+ ChunkVersionListResult,
77
+ ChunkVersionRestoreRequest,
78
+ ChunkVersionRestoreResult,
79
+ ChunkVersionRetireRequest,
80
+ ChunkVersionRetireResult,
81
+ ChunkVersionSetCurrentRequest,
82
+ ChunkVersionSetCurrentResult,
83
+ ChunkVersionUpdateRequest,
84
+ ChunkVersionUpdateResult,
85
+ )
86
+
87
+ DOC_STORE_COMMANDS: tuple[str, ...] = (
88
+ "echo",
89
+ "long_task",
90
+ "job_status",
91
+ "queue_add_job",
92
+ "queue_start_job",
93
+ "queue_stop_job",
94
+ "queue_delete_job",
95
+ "queue_get_job_status",
96
+ "queue_get_job_logs",
97
+ "queue_list_jobs",
98
+ "queue_health",
99
+ "health",
100
+ "info",
101
+ "uuid4",
102
+ "document_get",
103
+ "chapter_get",
104
+ "paragraph_get",
105
+ "paragraph_get_by_number",
106
+ "document_create",
107
+ "document_update",
108
+ "document_chunk",
109
+ "document_export",
110
+ "chapter_text_get",
111
+ "source_file_reconstruct",
112
+ "chunk_version_list",
113
+ "chunk_history",
114
+ "chunk_version_get",
115
+ "chunk_version_add",
116
+ "chunk_version_update",
117
+ "chunk_version_set_current",
118
+ "chunk_version_restore",
119
+ "chunk_version_retire",
120
+ "chunk_version_diff",
121
+ "chunk_version_delete",
122
+ "document_rebind",
123
+ "processing_status",
124
+ "document_delete",
125
+ "entity_create",
126
+ "entity_list",
127
+ "entity_get",
128
+ "entity_text_get",
129
+ "entity_content_get",
130
+ "entity_update",
131
+ "entity_rebind_owner",
132
+ "entity_owner_tree",
133
+ "entity_soft_delete",
134
+ "entity_undelete",
135
+ "entity_hard_delete",
136
+ "entity_references",
137
+ "chunk_query_search",
138
+ "semantic_relations",
139
+ "semantic_chunk_metadata_update",
140
+ "corpus_audit",
141
+ "embeddings_rebuild",
142
+ "help",
143
+ "config",
144
+ "reload",
145
+ "settings",
146
+ "load",
147
+ "unload",
148
+ "plugins",
149
+ "transport_management",
150
+ "proxy_registration",
151
+ "roletest",
152
+ "transfer_upload_begin",
153
+ "transfer_upload_status",
154
+ "transfer_upload_complete",
155
+ "transfer_download_begin",
156
+ "transfer_download_status",
157
+ "backup_run",
158
+ "backup_emergency",
159
+ "backup_status",
160
+ "backup_points",
161
+ "backup_inspect",
162
+ "backup_restore",
163
+ "backup_recover_catalog",
164
+ )
165
+
166
+
167
+ class _Omitted:
168
+ """Sentinel that distinguishes an omitted field from an explicit JSON null."""
169
+
170
+
171
+ _OMITTED = _Omitted()
172
+
173
+
174
+ class JsonRpcClientLike(Protocol):
175
+ """Minimal adapter surface used by the typed facade."""
176
+
177
+ async def execute_command_unified(self, command: str, params: Mapping[str, Any]) -> Any:
178
+ """Execute a doc-store command through the adapter."""
179
+
180
+
181
+ class TransferClientLike(JsonRpcClientLike, Protocol):
182
+ """Adapter surface when file-transfer ingestion is used."""
183
+
184
+ async def upload_file(
185
+ self,
186
+ source_path: str,
187
+ *,
188
+ filename: str | None = None,
189
+ compression: str = "identity",
190
+ chunk_size: int | None = None,
191
+ on_progress: Any = None,
192
+ ) -> Any:
193
+ """Delegate file transfer to the adapter implementation."""
194
+
195
+
196
+ T = TypeVar("T")
197
+
198
+
199
+ class DocStoreClientError(RuntimeError):
200
+ """Raised when the adapter or server returns a structured command error."""
201
+
202
+ def __init__(self, error: ServerError) -> None:
203
+ self.error = error
204
+ super().__init__(error.message)
205
+
206
+
207
+ _LOCK_ERROR_CODES = ("FILE_ALREADY_LOCKED", "SESSION_NOT_FOUND", "LOCK_SERVICE_UNAVAILABLE")
208
+
209
+
210
+ def _lock_field(error: ServerError, name: str) -> Any:
211
+ """Read a lock data field: ``details['data']`` when it is a mapping, else ``details``."""
212
+
213
+ details = error.details
214
+ if not isinstance(details, Mapping):
215
+ return None
216
+ data = details.get("data")
217
+ source = data if isinstance(data, Mapping) else details
218
+ return source.get(name)
219
+
220
+
221
+ class FileAlreadyLockedError(DocStoreClientError):
222
+ """The write was refused because another session holds the document."""
223
+
224
+ @property
225
+ def document_id(self) -> Any:
226
+ return _lock_field(self.error, "document_id")
227
+
228
+ @property
229
+ def holder_session_id(self) -> Any:
230
+ return _lock_field(self.error, "holder_session_id")
231
+
232
+ @property
233
+ def holder_idle_seconds(self) -> Any:
234
+ return _lock_field(self.error, "holder_idle_seconds")
235
+
236
+ @property
237
+ def seconds_left_before_timeout(self) -> Any:
238
+ return _lock_field(self.error, "seconds_left_before_timeout")
239
+
240
+
241
+ class SessionNotFoundError(DocStoreClientError):
242
+ """The write was refused because the CAS session is unknown or expired."""
243
+
244
+ @property
245
+ def session_id(self) -> Any:
246
+ return _lock_field(self.error, "session_id")
247
+
248
+
249
+ class LockServiceUnavailableError(DocStoreClientError):
250
+ """The write was refused because the lock check could not be made."""
251
+
252
+ @property
253
+ def reason(self) -> Any:
254
+ return _lock_field(self.error, "reason")
255
+
256
+
257
+ _LOCK_ERROR_CLASSES: dict[str, type[DocStoreClientError]] = {
258
+ "FILE_ALREADY_LOCKED": FileAlreadyLockedError,
259
+ "SESSION_NOT_FOUND": SessionNotFoundError,
260
+ "LOCK_SERVICE_UNAVAILABLE": LockServiceUnavailableError,
261
+ }
262
+
263
+
264
+ def _lock_code(error: ServerError) -> str | None:
265
+ """The lock code of ``error``: ``error.code``, else the code carried in its data."""
266
+
267
+ if error.code in _LOCK_ERROR_CODES:
268
+ return str(error.code)
269
+ details = error.details
270
+ if isinstance(details, Mapping):
271
+ data = details.get("data")
272
+ for source in (data, details):
273
+ if isinstance(source, Mapping) and source.get("code") in _LOCK_ERROR_CODES:
274
+ return str(source["code"])
275
+ return None
276
+
277
+
278
+ _NOT_FOUND = "NOT_FOUND"
279
+ _UUID_IN_TEXT = re.compile(r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}")
280
+
281
+
282
+ class DocStoreNotFoundError(DocStoreClientError):
283
+ """The requested target does not exist (server code ``NOT_FOUND``).
284
+
285
+ ``code`` is always ``"NOT_FOUND"``. ``missing_id`` is the id of the absent target when the
286
+ server named it (in the error data or in the message), else ``None``. ``str()`` keeps the
287
+ server message, so callers that read the text keep working.
288
+ """
289
+
290
+ code: str = _NOT_FOUND
291
+
292
+ @property
293
+ def missing_id(self) -> str | None:
294
+ details = self.error.details
295
+ if isinstance(details, Mapping):
296
+ data = details.get("data")
297
+ for source in (data, details):
298
+ if not isinstance(source, Mapping):
299
+ continue
300
+ for key in ("missing_id", "document_id", "chapter_id", "paragraph_id", "chunk_id", "entity_id", "id"):
301
+ value = source.get(key)
302
+ if isinstance(value, str) and value:
303
+ return value
304
+ match = _UUID_IN_TEXT.search(self.error.message)
305
+ return match.group(0) if match else None
306
+
307
+
308
+ def _is_not_found(error: ServerError) -> bool:
309
+ if error.code == _NOT_FOUND:
310
+ return True
311
+ details = error.details
312
+ if isinstance(details, Mapping):
313
+ data = details.get("data")
314
+ for source in (data, details):
315
+ if isinstance(source, Mapping) and source.get("code") == _NOT_FOUND:
316
+ return True
317
+ return False
318
+
319
+
320
+ def _client_error(error: ServerError) -> DocStoreClientError:
321
+ """The typed lock or not-found error for its code, else the plain ``DocStoreClientError``."""
322
+
323
+ code = _lock_code(error)
324
+ if code is not None:
325
+ return _LOCK_ERROR_CLASSES[code](error)
326
+ if _is_not_found(error):
327
+ if error.code != _NOT_FOUND:
328
+ error = replace(error, code=_NOT_FOUND)
329
+ return DocStoreNotFoundError(error)
330
+ return DocStoreClientError(error)
331
+
332
+
333
+ class DocStoreClient:
334
+ """Typed doc-store operations backed by an injected adapter client."""
335
+
336
+ commands: ClassVar[tuple[str, ...]] = DOC_STORE_COMMANDS
337
+
338
+ def __init__(
339
+ self,
340
+ adapter_client: JsonRpcClientLike,
341
+ *,
342
+ event_callback: Callable[[Mapping[str, Any]], Awaitable[None] | None] | None = None,
343
+ ) -> None:
344
+ self._adapter_client = adapter_client
345
+ self._event_callback = event_callback
346
+
347
+ async def call(
348
+ self,
349
+ command: str,
350
+ params: Mapping[str, Any] | None = None,
351
+ **kwargs: Any,
352
+ ) -> Any:
353
+ """Execute any server command through the injected adapter client.
354
+
355
+ The adapter owns networking, TLS/mTLS, authentication, queue delivery, and
356
+ retries. This method only gives callers a stable command-level facade.
357
+ """
358
+
359
+ command_name = command.strip()
360
+ if not command_name:
361
+ raise ValueError("command must be non-empty")
362
+ return _unwrap_response(
363
+ await self._dispatch(command_name, _merge_params(params, kwargs))
364
+ )
365
+
366
+ async def echo(self, params: Mapping[str, Any] | None = None, **kwargs: Any) -> Any:
367
+ return await self.call("echo", params, **kwargs)
368
+
369
+ async def long_task(self, params: Mapping[str, Any] | None = None, **kwargs: Any) -> Any:
370
+ return await self.call("long_task", params, **kwargs)
371
+
372
+ async def job_status(self, params: Mapping[str, Any] | None = None, **kwargs: Any) -> Any:
373
+ return await self.call("job_status", params, **kwargs)
374
+
375
+ async def queue_add_job(
376
+ self, params: Mapping[str, Any] | None = None, **kwargs: Any
377
+ ) -> Any:
378
+ return await self.call("queue_add_job", params, **kwargs)
379
+
380
+ async def queue_start_job(
381
+ self, params: Mapping[str, Any] | None = None, **kwargs: Any
382
+ ) -> Any:
383
+ return await self.call("queue_start_job", params, **kwargs)
384
+
385
+ async def queue_stop_job(
386
+ self, params: Mapping[str, Any] | None = None, **kwargs: Any
387
+ ) -> Any:
388
+ return await self.call("queue_stop_job", params, **kwargs)
389
+
390
+ async def queue_delete_job(
391
+ self, params: Mapping[str, Any] | None = None, **kwargs: Any
392
+ ) -> Any:
393
+ return await self.call("queue_delete_job", params, **kwargs)
394
+
395
+ async def queue_get_job_status(
396
+ self, params: Mapping[str, Any] | None = None, **kwargs: Any
397
+ ) -> Any:
398
+ return await self.call("queue_get_job_status", params, **kwargs)
399
+
400
+ async def queue_get_job_logs(
401
+ self, params: Mapping[str, Any] | None = None, **kwargs: Any
402
+ ) -> Any:
403
+ return await self.call("queue_get_job_logs", params, **kwargs)
404
+
405
+ async def queue_list_jobs(
406
+ self, params: Mapping[str, Any] | None = None, **kwargs: Any
407
+ ) -> Any:
408
+ return await self.call("queue_list_jobs", params, **kwargs)
409
+
410
+ async def queue_health(
411
+ self, params: Mapping[str, Any] | None = None, **kwargs: Any
412
+ ) -> Any:
413
+ return await self.call("queue_health", params, **kwargs)
414
+
415
+ async def document_get(
416
+ self, params: Mapping[str, Any] | None = None, **kwargs: Any
417
+ ) -> Any:
418
+ return await self.call("document_get", params, **kwargs)
419
+
420
+ async def chapter_get(
421
+ self, params: Mapping[str, Any] | None = None, **kwargs: Any
422
+ ) -> Any:
423
+ return await self.call("chapter_get", params, **kwargs)
424
+
425
+ async def paragraph_get(
426
+ self, params: Mapping[str, Any] | None = None, **kwargs: Any
427
+ ) -> Any:
428
+ return await self.call("paragraph_get", params, **kwargs)
429
+
430
+ async def paragraph_get_by_number(
431
+ self, params: Mapping[str, Any] | None = None, **kwargs: Any
432
+ ) -> Any:
433
+ return await self.call("paragraph_get_by_number", params, **kwargs)
434
+
435
+ async def document_create(
436
+ self,
437
+ params: Mapping[str, Any] | None = None,
438
+ *,
439
+ session_id: str | None = None,
440
+ **kwargs: Any,
441
+ ) -> Any:
442
+ return await self.call("document_create", params, **kwargs, **_session_kwargs(session_id))
443
+
444
+ async def document_update(
445
+ self,
446
+ params: Mapping[str, Any] | None = None,
447
+ *,
448
+ session_id: str | None = None,
449
+ **kwargs: Any,
450
+ ) -> Any:
451
+ return await self.call("document_update", params, **kwargs, **_session_kwargs(session_id))
452
+
453
+ async def document_chunk(
454
+ self,
455
+ params: Mapping[str, Any] | None = None,
456
+ *,
457
+ session_id: str | None = None,
458
+ **kwargs: Any,
459
+ ) -> Any:
460
+ return await self.call("document_chunk", params, **kwargs, **_session_kwargs(session_id))
461
+
462
+ async def document_export(
463
+ self, params: Mapping[str, Any] | None = None, **kwargs: Any
464
+ ) -> Any:
465
+ return await self.call("document_export", params, **kwargs)
466
+
467
+ async def chapter_text_get(
468
+ self, params: Mapping[str, Any] | None = None, **kwargs: Any
469
+ ) -> Any:
470
+ return await self.call("chapter_text_get", params, **kwargs)
471
+
472
+ async def source_file_reconstruct(
473
+ self, params: Mapping[str, Any] | None = None, **kwargs: Any
474
+ ) -> Any:
475
+ return await self.call("source_file_reconstruct", params, **kwargs)
476
+
477
+ async def chunk_version_list(
478
+ self, params: Mapping[str, Any] | None = None, **kwargs: Any
479
+ ) -> Any:
480
+ return await self.call("chunk_version_list", params, **kwargs)
481
+
482
+ async def chunk_history(
483
+ self, params: Mapping[str, Any] | None = None, **kwargs: Any
484
+ ) -> Any:
485
+ return await self.call("chunk_history", params, **kwargs)
486
+
487
+ async def chunk_version_get(
488
+ self, params: Mapping[str, Any] | None = None, **kwargs: Any
489
+ ) -> Any:
490
+ return await self.call("chunk_version_get", params, **kwargs)
491
+
492
+ async def chunk_version_add(
493
+ self,
494
+ params: Mapping[str, Any] | None = None,
495
+ *,
496
+ session_id: str | None = None,
497
+ **kwargs: Any,
498
+ ) -> Any:
499
+ return await self.call("chunk_version_add", params, **kwargs, **_session_kwargs(session_id))
500
+
501
+ async def chunk_version_update(
502
+ self,
503
+ params: Mapping[str, Any] | None = None,
504
+ *,
505
+ session_id: str | None = None,
506
+ **kwargs: Any,
507
+ ) -> Any:
508
+ return await self.call("chunk_version_update", params, **kwargs, **_session_kwargs(session_id))
509
+
510
+ async def chunk_version_set_current(
511
+ self,
512
+ params: Mapping[str, Any] | None = None,
513
+ *,
514
+ session_id: str | None = None,
515
+ **kwargs: Any,
516
+ ) -> Any:
517
+ return await self.call("chunk_version_set_current", params, **kwargs, **_session_kwargs(session_id))
518
+
519
+ async def chunk_version_restore(
520
+ self,
521
+ params: Mapping[str, Any] | None = None,
522
+ *,
523
+ session_id: str | None = None,
524
+ **kwargs: Any,
525
+ ) -> Any:
526
+ return await self.call("chunk_version_restore", params, **kwargs, **_session_kwargs(session_id))
527
+
528
+ async def chunk_version_retire(
529
+ self,
530
+ params: Mapping[str, Any] | None = None,
531
+ *,
532
+ session_id: str | None = None,
533
+ **kwargs: Any,
534
+ ) -> Any:
535
+ return await self.call("chunk_version_retire", params, **kwargs, **_session_kwargs(session_id))
536
+
537
+ async def chunk_version_diff(
538
+ self, params: Mapping[str, Any] | None = None, **kwargs: Any
539
+ ) -> Any:
540
+ return await self.call("chunk_version_diff", params, **kwargs)
541
+
542
+ async def chunk_version_delete(
543
+ self,
544
+ params: Mapping[str, Any] | None = None,
545
+ *,
546
+ session_id: str | None = None,
547
+ **kwargs: Any,
548
+ ) -> Any:
549
+ return await self.call("chunk_version_delete", params, **kwargs, **_session_kwargs(session_id))
550
+
551
+ async def document_rebind(
552
+ self,
553
+ params: Mapping[str, Any] | None = None,
554
+ *,
555
+ session_id: str | None = None,
556
+ **kwargs: Any,
557
+ ) -> Any:
558
+ return await self.call("document_rebind", params, **kwargs, **_session_kwargs(session_id))
559
+
560
+ async def processing_status(
561
+ self, params: Mapping[str, Any] | None = None, **kwargs: Any
562
+ ) -> Any:
563
+ return await self.call("processing_status", params, **kwargs)
564
+
565
+ async def document_delete(
566
+ self,
567
+ params: Mapping[str, Any] | None = None,
568
+ *,
569
+ session_id: str | None = None,
570
+ **kwargs: Any,
571
+ ) -> Any:
572
+ return await self.call("document_delete", params, **kwargs, **_session_kwargs(session_id))
573
+
574
+ async def entity_create(
575
+ self,
576
+ params: Mapping[str, Any] | None = None,
577
+ *,
578
+ session_id: str | None = None,
579
+ **kwargs: Any,
580
+ ) -> Any:
581
+ return await self.call("entity_create", params, **kwargs, **_session_kwargs(session_id))
582
+
583
+ async def entity_list(
584
+ self, params: Mapping[str, Any] | None = None, **kwargs: Any
585
+ ) -> Any:
586
+ return await self.call("entity_list", params, **kwargs)
587
+
588
+ async def entity_get(
589
+ self, params: Mapping[str, Any] | None = None, **kwargs: Any
590
+ ) -> Any:
591
+ return await self.call("entity_get", params, **kwargs)
592
+
593
+ async def entity_update(
594
+ self,
595
+ params: Mapping[str, Any] | None = None,
596
+ *,
597
+ session_id: str | None = None,
598
+ **kwargs: Any,
599
+ ) -> Any:
600
+ return await self.call("entity_update", params, **kwargs, **_session_kwargs(session_id))
601
+
602
+ async def entity_text_get(
603
+ self, params: Mapping[str, Any] | None = None, **kwargs: Any
604
+ ) -> EntityTextGetResult:
605
+ """Raw public invocation preserving scalar or list text results."""
606
+ return await self.call("entity_text_get", params, **kwargs)
607
+
608
+ async def entity_content_get(
609
+ self, ids: list[str], as_file: bool = False, **kwargs: Any
610
+ ) -> Any:
611
+ return await self.call(
612
+ "entity_content_get", {"ids": list(ids), "as_file": as_file}, **kwargs
613
+ )
614
+
615
+ async def entity_content_get_download(
616
+ self, receipt: Mapping[str, Any], **kwargs: Any
617
+ ) -> Any:
618
+ return await self.transfer_download_begin(receipt["download"], **kwargs)
619
+
620
+ async def entity_rebind_owner(
621
+ self,
622
+ params: Mapping[str, Any] | None = None,
623
+ *,
624
+ session_id: str | None = None,
625
+ **kwargs: Any,
626
+ ) -> Any:
627
+ return await self.call("entity_rebind_owner", params, **kwargs, **_session_kwargs(session_id))
628
+
629
+ async def entity_owner_tree(
630
+ self, params: Mapping[str, Any] | None = None, **kwargs: Any
631
+ ) -> Any:
632
+ return await self.call("entity_owner_tree", params, **kwargs)
633
+
634
+ async def entity_soft_delete(
635
+ self,
636
+ params: Mapping[str, Any] | None = None,
637
+ *,
638
+ session_id: str | None = None,
639
+ **kwargs: Any,
640
+ ) -> Any:
641
+ return await self.call("entity_soft_delete", params, **kwargs, **_session_kwargs(session_id))
642
+
643
+ async def entity_undelete(
644
+ self,
645
+ params: Mapping[str, Any] | None = None,
646
+ *,
647
+ session_id: str | None = None,
648
+ **kwargs: Any,
649
+ ) -> Any:
650
+ return await self.call("entity_undelete", params, **kwargs, **_session_kwargs(session_id))
651
+
652
+ async def entity_hard_delete(
653
+ self,
654
+ params: Mapping[str, Any] | None = None,
655
+ *,
656
+ session_id: str | None = None,
657
+ **kwargs: Any,
658
+ ) -> Any:
659
+ return await self.call("entity_hard_delete", params, **kwargs, **_session_kwargs(session_id))
660
+
661
+ async def entity_references(
662
+ self, params: Mapping[str, Any] | None = None, **kwargs: Any
663
+ ) -> Any:
664
+ return await self.call("entity_references", params, **kwargs)
665
+
666
+ async def chunk_query_search(
667
+ self, params: Mapping[str, Any] | None = None, **kwargs: Any
668
+ ) -> Any:
669
+ return await self.call("chunk_query_search", params, **kwargs)
670
+
671
+ async def semantic_relations(
672
+ self, params: Mapping[str, Any] | None = None, **kwargs: Any
673
+ ) -> Any:
674
+ return await self.call("semantic_relations", params, **kwargs)
675
+
676
+ async def semantic_chunk_metadata_update(
677
+ self,
678
+ params: Mapping[str, Any] | None = None,
679
+ *,
680
+ session_id: str | None = None,
681
+ **kwargs: Any,
682
+ ) -> Any:
683
+ return await self.call("semantic_chunk_metadata_update", params, **kwargs, **_session_kwargs(session_id))
684
+
685
+ async def corpus_audit(
686
+ self, params: Mapping[str, Any] | None = None, **kwargs: Any
687
+ ) -> Any:
688
+ return await self.call("corpus_audit", params, **kwargs)
689
+
690
+ async def embeddings_rebuild(
691
+ self, params: Mapping[str, Any] | None = None, **kwargs: Any
692
+ ) -> Any:
693
+ return await self.call("embeddings_rebuild", params, **kwargs)
694
+
695
+ async def help(
696
+ self,
697
+ cmdname: str | None = None,
698
+ params: Mapping[str, Any] | None = None,
699
+ **kwargs: Any,
700
+ ) -> Any:
701
+ request = _merge_params(params, kwargs)
702
+ if cmdname is not None:
703
+ if "cmdname" in request:
704
+ raise ValueError("cmdname supplied twice")
705
+ request["cmdname"] = cmdname
706
+ return await self.call("help", request)
707
+
708
+ async def health(self, params: Mapping[str, Any] | None = None, **kwargs: Any) -> Any:
709
+ return await self.call("health", params, **kwargs)
710
+
711
+ async def info(self, params: Mapping[str, Any] | None = None, **kwargs: Any) -> Any:
712
+ return await self.call("info", params, **kwargs)
713
+
714
+ async def uuid4(self, params: Mapping[str, Any] | None = None, **kwargs: Any) -> Any:
715
+ return await self.call("uuid4", params, **kwargs)
716
+
717
+ async def config(self, params: Mapping[str, Any] | None = None, **kwargs: Any) -> Any:
718
+ return await self.call("config", params, **kwargs)
719
+
720
+ async def reload(self, params: Mapping[str, Any] | None = None, **kwargs: Any) -> Any:
721
+ return await self.call("reload", params, **kwargs)
722
+
723
+ async def settings(self, params: Mapping[str, Any] | None = None, **kwargs: Any) -> Any:
724
+ return await self.call("settings", params, **kwargs)
725
+
726
+ async def load(self, params: Mapping[str, Any] | None = None, **kwargs: Any) -> Any:
727
+ return await self.call("load", params, **kwargs)
728
+
729
+ async def unload(self, params: Mapping[str, Any] | None = None, **kwargs: Any) -> Any:
730
+ return await self.call("unload", params, **kwargs)
731
+
732
+ async def plugins(self, params: Mapping[str, Any] | None = None, **kwargs: Any) -> Any:
733
+ return await self.call("plugins", params, **kwargs)
734
+
735
+ async def transport_management(
736
+ self, params: Mapping[str, Any] | None = None, **kwargs: Any
737
+ ) -> Any:
738
+ return await self.call("transport_management", params, **kwargs)
739
+
740
+ async def proxy_registration(
741
+ self, params: Mapping[str, Any] | None = None, **kwargs: Any
742
+ ) -> Any:
743
+ return await self.call("proxy_registration", params, **kwargs)
744
+
745
+ async def roletest(self, params: Mapping[str, Any] | None = None, **kwargs: Any) -> Any:
746
+ return await self.call("roletest", params, **kwargs)
747
+
748
+ async def transfer_upload_begin(
749
+ self, params: Mapping[str, Any] | None = None, **kwargs: Any
750
+ ) -> Any:
751
+ return await self.call("transfer_upload_begin", params, **kwargs)
752
+
753
+ async def transfer_upload_status(
754
+ self, params: Mapping[str, Any] | None = None, **kwargs: Any
755
+ ) -> Any:
756
+ return await self.call("transfer_upload_status", params, **kwargs)
757
+
758
+ async def transfer_upload_complete(
759
+ self, params: Mapping[str, Any] | None = None, **kwargs: Any
760
+ ) -> Any:
761
+ return await self.call("transfer_upload_complete", params, **kwargs)
762
+
763
+ async def transfer_download_begin(
764
+ self, params: Mapping[str, Any] | None = None, **kwargs: Any
765
+ ) -> Any:
766
+ return await self.call("transfer_download_begin", params, **kwargs)
767
+
768
+ async def transfer_download_status(
769
+ self, params: Mapping[str, Any] | None = None, **kwargs: Any
770
+ ) -> Any:
771
+ return await self.call("transfer_download_status", params, **kwargs)
772
+
773
+ async def backup_emergency(self) -> BackupEmergencyResult:
774
+ """Save an explicit independent FULL salvage archive and await its final result."""
775
+ return cast(BackupEmergencyResult, await self.call("backup_emergency", {}))
776
+
777
+ async def backup_run(
778
+ self,
779
+ *,
780
+ catalog_directory: str | None | _Omitted = _OMITTED,
781
+ dry_run: bool | _Omitted = _OMITTED,
782
+ ) -> BackupRunResult:
783
+ """Create or preview a backup using the configured repository.
784
+
785
+ Omitted options stay off the wire, so server configuration and the
786
+ command's ``dry_run=False`` default remain authoritative. Explicit
787
+ ``catalog_directory=None`` is sent as JSON null, which also means the
788
+ configured catalog directory. Queued delivery is handled by the
789
+ adapter exactly once.
790
+ """
791
+
792
+ result = await self.call(
793
+ "backup_run",
794
+ _provided_params(
795
+ catalog_directory=catalog_directory,
796
+ dry_run=dry_run,
797
+ ),
798
+ )
799
+ return cast(BackupRunResult, result)
800
+
801
+ async def backup_status(
802
+ self,
803
+ *,
804
+ catalog_directory: str | None | _Omitted = _OMITTED,
805
+ ) -> BackupStatusResult:
806
+ """Read backup configuration, worker state, and journal progress."""
807
+
808
+ result = await self.call(
809
+ "backup_status",
810
+ _provided_params(catalog_directory=catalog_directory),
811
+ )
812
+ return cast(BackupStatusResult, result)
813
+
814
+ async def backup_points(
815
+ self,
816
+ *,
817
+ catalog_directory: str | None | _Omitted = _OMITTED,
818
+ ) -> BackupPointsResult:
819
+ """List committed saved points in package order."""
820
+
821
+ result = await self.call(
822
+ "backup_points",
823
+ _provided_params(catalog_directory=catalog_directory),
824
+ )
825
+ return cast(BackupPointsResult, result)
826
+
827
+ async def backup_inspect(
828
+ self,
829
+ *,
830
+ point: str,
831
+ catalog_directory: str | None | _Omitted = _OMITTED,
832
+ ) -> BackupInspectResult:
833
+ """Inspect a saved point's chain, files, and restorable state."""
834
+
835
+ result = await self.call(
836
+ "backup_inspect",
837
+ _provided_params(
838
+ point=point,
839
+ catalog_directory=catalog_directory,
840
+ ),
841
+ )
842
+ return cast(BackupInspectResult, result)
843
+
844
+ async def backup_restore(
845
+ self,
846
+ *,
847
+ point: str,
848
+ catalog_directory: str | None | _Omitted = _OMITTED,
849
+ dry_run: bool | _Omitted = _OMITTED,
850
+ replace: bool | _Omitted = _OMITTED,
851
+ ) -> BackupRestoreResult:
852
+ """Restore a saved point, or preview the restore when ``dry_run`` is true.
853
+
854
+ Omitted booleans preserve the server's false defaults. The adapter
855
+ owns queued delivery and final-result polling.
856
+ """
857
+
858
+ result = await self.call(
859
+ "backup_restore",
860
+ _provided_params(
861
+ point=point,
862
+ catalog_directory=catalog_directory,
863
+ dry_run=dry_run,
864
+ replace=replace,
865
+ ),
866
+ )
867
+ return cast(BackupRestoreResult, result)
868
+
869
+ async def backup_recover_catalog(
870
+ self,
871
+ *,
872
+ destination: str,
873
+ catalog_copy: str,
874
+ catalog_directory: str | None | _Omitted = _OMITTED,
875
+ dry_run: bool | _Omitted = _OMITTED,
876
+ ) -> BackupRecoverCatalogResult:
877
+ """Recover a catalog copy to a new destination without activating it.
878
+
879
+ Omitted options preserve the configured catalog and server default.
880
+ The adapter owns queued delivery and final-result polling.
881
+ """
882
+
883
+ result = await self.call(
884
+ "backup_recover_catalog",
885
+ _provided_params(
886
+ destination=destination,
887
+ catalog_copy=catalog_copy,
888
+ catalog_directory=catalog_directory,
889
+ dry_run=dry_run,
890
+ ),
891
+ )
892
+ return cast(BackupRecoverCatalogResult, result)
893
+
894
+ async def create_document(
895
+ self,
896
+ request: DocumentCreateRequest,
897
+ *,
898
+ source_path: str | None = None,
899
+ filename: str | None = None,
900
+ session_id: str | None = None,
901
+ ) -> DocumentCreateResult:
902
+ params = await self._write_params(request, source_path=source_path, filename=filename)
903
+ return await self._execute("document_create", _with_session(params, session_id), DocumentCreateResult)
904
+
905
+ async def update_document(
906
+ self,
907
+ request: DocumentUpdateRequest,
908
+ *,
909
+ source_path: str | None = None,
910
+ filename: str | None = None,
911
+ session_id: str | None = None,
912
+ ) -> DocumentUpdateResult:
913
+ params = await self._write_params(request, source_path=source_path, filename=filename)
914
+ return await self._execute("document_update", _with_session(params, session_id), DocumentUpdateResult)
915
+
916
+ async def chunk_document(
917
+ self, request: DocumentChunkRequest, *, session_id: str | None = None
918
+ ) -> DocumentChunkResult:
919
+ return await self._execute("document_chunk", _with_session(request.to_params(), session_id), DocumentChunkResult)
920
+
921
+ async def rebind_document(
922
+ self, request: DocumentRebindRequest, *, session_id: str | None = None
923
+ ) -> DocumentRebindResult:
924
+ return await self._execute(
925
+ "document_rebind", _with_session(request.to_params(), session_id), DocumentRebindResult
926
+ )
927
+
928
+ async def get_processing_status(
929
+ self, request: ProcessingStatusRequest
930
+ ) -> ProcessingStatusResult:
931
+ return await self._execute(
932
+ "processing_status", request.to_params(), ProcessingStatusResult
933
+ )
934
+
935
+ async def get_document(self, request: DocumentGetRequest) -> DocumentGetResult:
936
+ return await self._execute("document_get", request.to_params(), DocumentGetResult)
937
+
938
+ async def get_chapter(self, request: ChapterGetRequest) -> ChapterGetResult:
939
+ return await self._execute("chapter_get", request.to_params(), ChapterGetResult)
940
+
941
+ async def get_chapter_text(
942
+ self,
943
+ request: ChapterTextGetRequest,
944
+ ) -> TextReconstructionResult:
945
+ """Reconstruct chapter text (chapter_text_get).
946
+
947
+ The request field text_offset (0 to 1000000000, default absent means 0) selects the
948
+ start in unicode code points. The result attributes total_chars, total_sha256,
949
+ text_offset and next_text_offset allow continuation: repeat the call with
950
+ text_offset=next_text_offset until next_text_offset is None.
951
+ """
952
+ return await self._execute(
953
+ "chapter_text_get",
954
+ request.to_params(),
955
+ TextReconstructionResult,
956
+ )
957
+
958
+ async def reconstruct_source_file(
959
+ self,
960
+ request: SourceFileReconstructRequest,
961
+ ) -> TextReconstructionResult:
962
+ """Reconstruct stored source text (source_file_reconstruct).
963
+
964
+ The request field text_offset (0 to 1000000000, default absent means 0) selects the
965
+ start in unicode code points. The result attributes total_chars, total_sha256,
966
+ text_offset and next_text_offset allow continuation: repeat the call with
967
+ text_offset=next_text_offset until next_text_offset is None.
968
+ """
969
+ return await self._execute(
970
+ "source_file_reconstruct",
971
+ request.to_params(),
972
+ TextReconstructionResult,
973
+ )
974
+
975
+ async def list_chunk_versions(
976
+ self,
977
+ request: "ChunkVersionListRequest",
978
+ ) -> "ChunkVersionListResult":
979
+ return await self._execute(
980
+ "chunk_version_list",
981
+ request.to_params(),
982
+ _model_type("ChunkVersionListResult"),
983
+ )
984
+
985
+ async def get_chunk_history(
986
+ self,
987
+ request: "ChunkHistoryRequest",
988
+ ) -> "ChunkHistoryResult":
989
+ return await self._execute(
990
+ "chunk_history",
991
+ request.to_params(),
992
+ _model_type("ChunkHistoryResult"),
993
+ )
994
+
995
+ async def get_chunk_version(
996
+ self,
997
+ request: "ChunkVersionGetRequest",
998
+ ) -> "ChunkVersionGetResult":
999
+ return await self._execute(
1000
+ "chunk_version_get",
1001
+ request.to_params(),
1002
+ _model_type("ChunkVersionGetResult"),
1003
+ )
1004
+
1005
+ async def add_chunk_version(
1006
+ self,
1007
+ request: "ChunkVersionAddRequest",
1008
+ *,
1009
+ session_id: str | None = None,
1010
+ ) -> "ChunkVersionAddResult":
1011
+ return await self._execute(
1012
+ "chunk_version_add",
1013
+ _with_session(request.to_params(), session_id),
1014
+ _model_type("ChunkVersionAddResult"),
1015
+ )
1016
+
1017
+ async def update_chunk_version(
1018
+ self,
1019
+ request: "ChunkVersionUpdateRequest",
1020
+ *,
1021
+ session_id: str | None = None,
1022
+ ) -> "ChunkVersionUpdateResult":
1023
+ return await self._execute(
1024
+ "chunk_version_update",
1025
+ _with_session(request.to_params(), session_id),
1026
+ _model_type("ChunkVersionUpdateResult"),
1027
+ )
1028
+
1029
+ async def set_current_chunk_version(
1030
+ self,
1031
+ request: "ChunkVersionSetCurrentRequest",
1032
+ *,
1033
+ session_id: str | None = None,
1034
+ ) -> "ChunkVersionSetCurrentResult":
1035
+ return await self._execute(
1036
+ "chunk_version_set_current",
1037
+ _with_session(request.to_params(), session_id),
1038
+ _model_type("ChunkVersionSetCurrentResult"),
1039
+ )
1040
+
1041
+ async def restore_chunk_version(
1042
+ self,
1043
+ request: "ChunkVersionRestoreRequest",
1044
+ *,
1045
+ session_id: str | None = None,
1046
+ ) -> "ChunkVersionRestoreResult":
1047
+ return await self._execute(
1048
+ "chunk_version_restore",
1049
+ _with_session(request.to_params(), session_id),
1050
+ _model_type("ChunkVersionRestoreResult"),
1051
+ )
1052
+
1053
+ async def retire_chunk_version(
1054
+ self,
1055
+ request: "ChunkVersionRetireRequest",
1056
+ *,
1057
+ session_id: str | None = None,
1058
+ ) -> "ChunkVersionRetireResult":
1059
+ return await self._execute(
1060
+ "chunk_version_retire",
1061
+ _with_session(request.to_params(), session_id),
1062
+ _model_type("ChunkVersionRetireResult"),
1063
+ )
1064
+
1065
+ async def diff_chunk_versions(
1066
+ self,
1067
+ request: "ChunkVersionDiffRequest",
1068
+ ) -> "ChunkVersionDiffResult":
1069
+ return await self._execute(
1070
+ "chunk_version_diff",
1071
+ request.to_params(),
1072
+ _model_type("ChunkVersionDiffResult"),
1073
+ )
1074
+
1075
+ async def delete_chunk_version(
1076
+ self,
1077
+ request: "ChunkVersionDeleteRequest",
1078
+ *,
1079
+ session_id: str | None = None,
1080
+ ) -> "ChunkVersionDeleteResult":
1081
+ return await self._execute(
1082
+ "chunk_version_delete",
1083
+ _with_session(request.to_params(), session_id),
1084
+ _model_type("ChunkVersionDeleteResult"),
1085
+ )
1086
+
1087
+ async def get_paragraph(self, request: ParagraphGetRequest) -> ParagraphGetResult:
1088
+ return await self._execute("paragraph_get", request.to_params(), ParagraphGetResult)
1089
+
1090
+ async def get_paragraph_by_number(
1091
+ self,
1092
+ request: ParagraphGetByNumberRequest,
1093
+ ) -> ParagraphGetByNumberResult:
1094
+ return await self._execute(
1095
+ "paragraph_get_by_number",
1096
+ request.to_params(),
1097
+ ParagraphGetByNumberResult,
1098
+ )
1099
+
1100
+ async def delete_document(
1101
+ self, request: DocumentDeleteRequest, *, session_id: str | None = None
1102
+ ) -> DocumentDeleteResult:
1103
+ return await self._execute("document_delete", _with_session(request.to_params(), session_id), DocumentDeleteResult)
1104
+
1105
+ async def list_entities(self, request: EntityListRequest) -> EntityListResult:
1106
+ return await self._execute("entity_list", request.to_params(), EntityListResult)
1107
+
1108
+ async def get_entity(self, request: EntityGetRequest) -> EntityGetResult:
1109
+ return await self._execute("entity_get", request.to_params(), EntityGetResult)
1110
+
1111
+ async def get_entity_text(self, request: EntityTextGetRequest) -> EntityTextGetResult:
1112
+ """Return canonical text, preserving scalar/list shape, order and duplicates."""
1113
+ result = await self.call("entity_text_get", request.to_params())
1114
+ batch = isinstance(request.entity_id, list) or "," in request.entity_id
1115
+ if batch:
1116
+ count = len(request.entity_id) if isinstance(request.entity_id, list) else len(request.entity_id.split(","))
1117
+ if not isinstance(result, list) or len(result) != count or any(not isinstance(item, str) for item in result):
1118
+ raise TypeError("entity_text_get batch result must be an ordered list of strings matching the request count")
1119
+ elif not isinstance(result, str):
1120
+ raise TypeError("entity_text_get scalar result must be a string")
1121
+ return result
1122
+
1123
+ async def soft_delete_entities(
1124
+ self, request: EntityIdsRequest, *, session_id: str | None = None
1125
+ ) -> EntityLifecycleResult:
1126
+ return await self._execute("entity_soft_delete", _with_session(request.to_params(), session_id), EntityLifecycleResult)
1127
+
1128
+ async def undelete_entities(
1129
+ self, request: EntityIdsRequest, *, session_id: str | None = None
1130
+ ) -> EntityLifecycleResult:
1131
+ return await self._execute("entity_undelete", _with_session(request.to_params(), session_id), EntityLifecycleResult)
1132
+
1133
+ async def hard_delete_entities(
1134
+ self, request: EntityIdsRequest, *, session_id: str | None = None
1135
+ ) -> EntityLifecycleResult:
1136
+ return await self._execute("entity_hard_delete", _with_session(request.to_params(), session_id), EntityLifecycleResult)
1137
+
1138
+ async def get_entity_references(self, request: EntityReferencesRequest) -> EntityReferencesResult:
1139
+ return await self._execute("entity_references", request.to_params(), EntityReferencesResult)
1140
+
1141
+ async def get_entity_owner_tree(self, request: EntityOwnerTreeRequest) -> EntityOwnerTreeResult:
1142
+ return await self._execute("entity_owner_tree", request.to_params(), EntityOwnerTreeResult)
1143
+
1144
+ async def update_semantic_chunk_metadata(
1145
+ self,
1146
+ request: SemanticChunkMetadataUpdateRequest,
1147
+ *,
1148
+ session_id: str | None = None,
1149
+ ) -> SemanticChunkMetadataUpdateResult:
1150
+ return await self._execute(
1151
+ "semantic_chunk_metadata_update",
1152
+ _with_session(request.to_params(), session_id),
1153
+ SemanticChunkMetadataUpdateResult,
1154
+ )
1155
+
1156
+ async def search(self, query: ChunkQuery) -> SearchResult:
1157
+ return await self._execute("chunk_query_search", {"query": _dump_query(query)}, SearchResult)
1158
+
1159
+ async def upload_file(
1160
+ self,
1161
+ source_path: str,
1162
+ *,
1163
+ filename: str | None = None,
1164
+ compression: str = "identity",
1165
+ chunk_size: int | None = None,
1166
+ on_progress: Any = None,
1167
+ ) -> Mapping[str, Any]:
1168
+ """Upload a file through the injected adapter and return a transfer reference."""
1169
+
1170
+ upload = getattr(self._adapter_client, "upload_file", None)
1171
+ if upload is None:
1172
+ raise TypeError("adapter client does not expose upload_file")
1173
+ receipt = await upload(
1174
+ source_path,
1175
+ filename=filename,
1176
+ compression=compression,
1177
+ chunk_size=chunk_size,
1178
+ on_progress=on_progress,
1179
+ )
1180
+ return _completed_transfer_reference(receipt)
1181
+
1182
+ async def _write_params(
1183
+ self,
1184
+ request: DocumentCreateRequest | DocumentUpdateRequest,
1185
+ *,
1186
+ source_path: str | None,
1187
+ filename: str | None,
1188
+ ) -> dict[str, Any]:
1189
+ if source_path is None:
1190
+ if request.raw_text is None and request.transferred_file is None:
1191
+ raise ValueError("raw_text, transferred_file, or source_path is required")
1192
+ return request.to_params()
1193
+ transfer_ref = await self.upload_file(source_path, filename=filename)
1194
+ params = request.to_params()
1195
+ params.pop("raw_text", None)
1196
+ params["transferred_file"] = transfer_ref
1197
+ return params
1198
+
1199
+ async def _execute(
1200
+ self, command: str, params: Mapping[str, Any], result_type: type[T]
1201
+ ) -> T:
1202
+ response = await self._dispatch(command, dict(params))
1203
+ payload = _unwrap_response(response, expect_mapping=True)
1204
+ if hasattr(result_type, "from_payload"):
1205
+ return result_type.from_payload(payload) # type: ignore[attr-defined,no-any-return]
1206
+ return result_type(**payload) # type: ignore[call-arg]
1207
+
1208
+ async def _dispatch(self, command: str, params: Mapping[str, Any]) -> Any:
1209
+ """Detach accepted writes, observe their jobs, and fetch the final RPC result."""
1210
+ dispatch = self._adapter_client.execute_command_unified
1211
+ try:
1212
+ signature = inspect.signature(dispatch)
1213
+ supports_manual = "manual_event_handling" in signature.parameters or any(
1214
+ parameter.kind is inspect.Parameter.VAR_KEYWORD
1215
+ for parameter in signature.parameters.values()
1216
+ )
1217
+ except (TypeError, ValueError):
1218
+ supports_manual = False
1219
+ if (
1220
+ command not in {"document_create", "document_update", "document_import"}
1221
+ or not supports_manual
1222
+ ):
1223
+ return await dispatch(command, dict(params))
1224
+
1225
+ admission = await dispatch(
1226
+ command,
1227
+ dict(params),
1228
+ expect_queue=True,
1229
+ auto_poll=False,
1230
+ timeout=30.0,
1231
+ status_hook=self._event_callback,
1232
+ manual_event_handling=False,
1233
+ )
1234
+ if not isinstance(admission, Mapping) or admission.get("mode") != "queued":
1235
+ return admission
1236
+ job_id = str(admission.get("job_id") or "")
1237
+ if not job_id:
1238
+ raise RuntimeError(f"queued command {command!r} returned no job_id")
1239
+ terminal: Mapping[str, Any]
1240
+ try:
1241
+ try:
1242
+ waiter = getattr(self._adapter_client, "wait_for_job", None)
1243
+ if waiter is None:
1244
+ terminal = await self._recover_job(job_id)
1245
+ else:
1246
+ terminal = await waiter(
1247
+ job_id, timeout=0, on_event=self._event_callback
1248
+ )
1249
+ except Exception as first_error:
1250
+ try:
1251
+ terminal = await self._recover_job(job_id)
1252
+ except Exception as recovery_error:
1253
+ raise RuntimeError(
1254
+ f"queued command {command!r} lost WebSocket for job {job_id}; "
1255
+ f"recovery failed: {recovery_error}"
1256
+ ) from first_error
1257
+ except asyncio.CancelledError:
1258
+ # Explicit caller cancellation stops the job; observer loss never does.
1259
+ await self.queue_stop_job(job_id=job_id)
1260
+ raise
1261
+ event_name = str(terminal.get("event", "")).lower()
1262
+ status = str(terminal.get("status", "")).lower()
1263
+ if event_name not in {"command_completed", "job_completed"} and status != "completed":
1264
+ fetch_error: Exception | None = None
1265
+ try:
1266
+ failed = await self._fetch_job_result(job_id)
1267
+ except Exception as exc:
1268
+ fetch_error = exc
1269
+ else:
1270
+ if isinstance(failed, Mapping) and failed.get("success") is False:
1271
+ typed = _error_from_payload(failed)
1272
+ if _lock_code(typed) is not None:
1273
+ raise _client_error(typed)
1274
+ raise RuntimeError(
1275
+ f"queued command {command!r} failed for job {job_id}: {dict(terminal)!r}"
1276
+ ) from fetch_error
1277
+ result = await self._fetch_job_result(job_id)
1278
+ return {
1279
+ "mode": "queued",
1280
+ "job_id": job_id,
1281
+ "command": command,
1282
+ "result": result,
1283
+ }
1284
+
1285
+ async def _recover_job(self, job_id: str) -> Mapping[str, Any]:
1286
+ waiter = getattr(self._adapter_client, "wait_for_job", None)
1287
+ status_getter = getattr(self._adapter_client, "queue_get_job_status", None)
1288
+ if waiter is not None:
1289
+ try:
1290
+ return await waiter(job_id, timeout=0, on_event=self._event_callback)
1291
+ except Exception:
1292
+ if status_getter is None:
1293
+ raise
1294
+ if status_getter is None:
1295
+ raise RuntimeError("adapter exposes neither wait_for_job nor queue_get_job_status")
1296
+ while True:
1297
+ status = await status_getter(job_id)
1298
+ payload = status.get("data", status) if isinstance(status, Mapping) else status
1299
+ if isinstance(payload, Mapping):
1300
+ state = str(payload.get("status", "")).lower()
1301
+ if state in {"completed", "failed", "stopped", "cancelled"}:
1302
+ return {
1303
+ "event": "job_completed" if state == "completed" else "job_failed",
1304
+ "status": state,
1305
+ "job_id": job_id,
1306
+ }
1307
+ await asyncio.sleep(1.0)
1308
+
1309
+ async def _fetch_job_result(self, job_id: str) -> Any:
1310
+ fetcher = getattr(self._adapter_client, "fetch_command_session_result", None)
1311
+ if fetcher is not None:
1312
+ return _final_result_payload(await fetcher(job_id))
1313
+ getter = getattr(self._adapter_client, "queue_get_job_status", None)
1314
+ if getter is None:
1315
+ raise RuntimeError(f"adapter cannot fetch final result for job {job_id}")
1316
+ return _final_result_payload(await getter(job_id))
1317
+
1318
+
1319
+ def _merge_params(
1320
+ params: Mapping[str, Any] | None,
1321
+ kwargs: Mapping[str, Any],
1322
+ ) -> dict[str, Any]:
1323
+ if params is None:
1324
+ result: dict[str, Any] = {}
1325
+ elif isinstance(params, Mapping):
1326
+ result = dict(params)
1327
+ else:
1328
+ raise TypeError("params must be a mapping")
1329
+ duplicate = sorted(set(result).intersection(kwargs))
1330
+ if duplicate:
1331
+ raise ValueError(f"duplicate parameters: {', '.join(duplicate)}")
1332
+ result.update(kwargs)
1333
+ return result
1334
+
1335
+
1336
+ def _session_kwargs(session_id: str | None) -> dict[str, Any]:
1337
+ """The keyword carrying ``session_id``; only ``None`` is omitted, nothing is validated."""
1338
+
1339
+ return {} if session_id is None else {"session_id": session_id}
1340
+
1341
+
1342
+ def _with_session(params: Mapping[str, Any], session_id: str | None) -> dict[str, Any]:
1343
+ """Copy of ``params`` with ``session_id`` added unchanged when it is not ``None``."""
1344
+
1345
+ result = dict(params)
1346
+ if session_id is not None:
1347
+ result["session_id"] = session_id
1348
+ return result
1349
+
1350
+
1351
+ def _provided_params(**values: Any) -> dict[str, Any]:
1352
+ """Build a command payload while preserving explicit ``None`` values."""
1353
+
1354
+ return {name: value for name, value in values.items() if not isinstance(value, _Omitted)}
1355
+
1356
+
1357
+ def _final_result_payload(value: Any) -> Any:
1358
+ """Normalize queue status wrappers while retaining the server result."""
1359
+ if isinstance(value, Mapping) and "result" in value:
1360
+ value = value["result"]
1361
+ if isinstance(value, Mapping) and isinstance(value.get("data"), Mapping):
1362
+ return value["data"]
1363
+ return value
1364
+
1365
+
1366
+ def _model_type(name: str) -> type[Any]:
1367
+ """Resolve concurrently added public models without importing them eagerly."""
1368
+
1369
+ from . import models
1370
+
1371
+ return getattr(models, name)
1372
+
1373
+
1374
+ def _unwrap_response(response: Any, *, expect_mapping: bool = False) -> Any:
1375
+ if isinstance(response, Mapping):
1376
+ if _is_jsonrpc_envelope(response):
1377
+ if "error" in response:
1378
+ return _unwrap_response(
1379
+ {"success": False, "error": response["error"]},
1380
+ expect_mapping=expect_mapping,
1381
+ )
1382
+ return _unwrap_response(response["result"], expect_mapping=expect_mapping)
1383
+ if _is_adapter_envelope(response):
1384
+ return _unwrap_response(response["result"], expect_mapping=expect_mapping)
1385
+ if _is_queue_status_payload(response):
1386
+ return _unwrap_response(response["result"], expect_mapping=expect_mapping)
1387
+ if response.get("success") is False:
1388
+ raise _client_error(_error_from_payload(response))
1389
+ data = response.get("data", response)
1390
+ if isinstance(data, Mapping) or not expect_mapping:
1391
+ return data
1392
+ data = getattr(response, "data", response)
1393
+ success = getattr(response, "success", True)
1394
+ if success is False:
1395
+ raise _client_error(
1396
+ ServerError(
1397
+ code=str(getattr(response, "code", "SERVER_ERROR")),
1398
+ message=str(getattr(response, "error", "command failed")),
1399
+ details=getattr(response, "details", None),
1400
+ )
1401
+ )
1402
+ if isinstance(data, Mapping) or not expect_mapping:
1403
+ return data
1404
+ raise TypeError("adapter response data must be an object")
1405
+
1406
+
1407
+ def _is_adapter_envelope(response: Mapping[str, Any]) -> bool:
1408
+ return "mode" in response and "result" in response
1409
+
1410
+
1411
+ def _is_jsonrpc_envelope(response: Mapping[str, Any]) -> bool:
1412
+ return response.get("jsonrpc") == "2.0" and (
1413
+ "result" in response or "error" in response
1414
+ )
1415
+
1416
+
1417
+ def _is_queue_status_payload(response: Mapping[str, Any]) -> bool:
1418
+ result = response.get("result")
1419
+ return (
1420
+ "job_id" in response
1421
+ and "command" in response
1422
+ and isinstance(result, Mapping)
1423
+ )
1424
+
1425
+
1426
+ def _error_from_payload(payload: Mapping[str, Any]) -> ServerError:
1427
+ details = payload.get("details")
1428
+ error = payload.get("error")
1429
+ if isinstance(error, Mapping):
1430
+ values = dict(error)
1431
+ values.setdefault("code", str(payload.get("code", "SERVER_ERROR")))
1432
+ values.setdefault("message", str(values.get("message", "command failed")))
1433
+ return ServerError.from_payload(values)
1434
+ return ServerError(
1435
+ code=str(payload.get("code", "SERVER_ERROR")),
1436
+ message=str(error or "command failed"),
1437
+ details=details if isinstance(details, Mapping) else None,
1438
+ )
1439
+
1440
+
1441
+ def _completed_transfer_reference(receipt: Any) -> Mapping[str, Any]:
1442
+ completed = (
1443
+ receipt.get("completed")
1444
+ if isinstance(receipt, Mapping)
1445
+ else getattr(receipt, "completed", False)
1446
+ )
1447
+ if completed is not True:
1448
+ raise DocStoreClientError(
1449
+ ServerError(code="TRANSFER_INCOMPLETE", message="file transfer did not complete")
1450
+ )
1451
+ if isinstance(receipt, Mapping):
1452
+ transfer_id = receipt.get("transfer_id") or receipt.get("id")
1453
+ values = dict(receipt)
1454
+ else:
1455
+ transfer_id = getattr(receipt, "transfer_id", None) or getattr(receipt, "id", None)
1456
+ values = {
1457
+ key: getattr(receipt, key, None)
1458
+ for key in (
1459
+ "transfer_id",
1460
+ "id",
1461
+ "filename",
1462
+ "path",
1463
+ "size_bytes",
1464
+ "checksum_algorithm",
1465
+ "checksum_value",
1466
+ "compression",
1467
+ "chunk_size",
1468
+ "status",
1469
+ "plaintext_size_bytes",
1470
+ )
1471
+ }
1472
+ values["transfer_id"] = transfer_id
1473
+ result = {
1474
+ key: value
1475
+ for key, value in values.items()
1476
+ if key != "completed" and value is not None and key != "id"
1477
+ }
1478
+ if not result:
1479
+ raise DocStoreClientError(
1480
+ ServerError(code="TRANSFER_REFERENCE_MISSING", message="file transfer reference is missing")
1481
+ )
1482
+ return result
1483
+
1484
+
1485
+ def _dump_query(query: ChunkQuery) -> Mapping[str, Any]:
1486
+ if hasattr(query, "model_dump"):
1487
+ return query.model_dump(mode="python", exclude_none=True, exclude_unset=True)
1488
+ if is_dataclass(query):
1489
+ return {
1490
+ key: value
1491
+ for key, value in query.__dict__.items()
1492
+ if value is not None
1493
+ }
1494
+ if isinstance(query, Mapping):
1495
+ return dict(query)
1496
+ raise TypeError("query must be a chunk_metadata_adapter.ChunkQuery")
1497
+
1498
+
1499
+ __all__ = [
1500
+ "DOC_STORE_COMMANDS",
1501
+ "DocStoreClient",
1502
+ "DocStoreClientError",
1503
+ "DocStoreNotFoundError",
1504
+ "FileAlreadyLockedError",
1505
+ "JsonRpcClientLike",
1506
+ "LockServiceUnavailableError",
1507
+ "SessionNotFoundError",
1508
+ "TransferClientLike",
1509
+ ]