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.
- doc_store_client/__init__.py +194 -0
- doc_store_client/cli.py +192 -0
- doc_store_client/client.py +1509 -0
- doc_store_client/models.py +1635 -0
- doc_store_client/owned_document_cleanup.py +143 -0
- doc_store_client/py.typed +0 -0
- doc_store_client/text_verification.py +21 -0
- doc_store_client_cli.py +16 -0
- doc_store_client_dev-0.1.185.dist-info/METADATA +133 -0
- doc_store_client_dev-0.1.185.dist-info/RECORD +13 -0
- doc_store_client_dev-0.1.185.dist-info/WHEEL +5 -0
- doc_store_client_dev-0.1.185.dist-info/entry_points.txt +2 -0
- doc_store_client_dev-0.1.185.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Ownership-scoped cleanup for documents created by a single verification run."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import Protocol
|
|
8
|
+
from uuid import UUID
|
|
9
|
+
|
|
10
|
+
from .client import DocStoreClientError
|
|
11
|
+
from .models import (
|
|
12
|
+
DocumentDeleteRequest,
|
|
13
|
+
DocumentDeleteResult,
|
|
14
|
+
DocumentGetRequest,
|
|
15
|
+
DocumentGetResult,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class OwnedDocumentCleanupError(RuntimeError):
|
|
20
|
+
"""A verification fixture could not be removed through the public API."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class OwnedDocumentClient(Protocol):
|
|
24
|
+
"""The narrow public-client surface required for owned fixture cleanup."""
|
|
25
|
+
|
|
26
|
+
async def get_document(self, request: DocumentGetRequest) -> DocumentGetResult:
|
|
27
|
+
"""Read a document and its current mutation token."""
|
|
28
|
+
|
|
29
|
+
async def delete_document(
|
|
30
|
+
self, request: DocumentDeleteRequest, *, session_id: str | None = None
|
|
31
|
+
) -> DocumentDeleteResult:
|
|
32
|
+
"""Delete a document through the typed public command."""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _exact_uuid4(document_id: str) -> str:
|
|
36
|
+
"""Return one canonical UUID4, refusing non-document identifiers early."""
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
parsed = UUID(document_id)
|
|
40
|
+
except (AttributeError, TypeError, ValueError) as exc:
|
|
41
|
+
raise ValueError("owned document id must be a UUID4") from exc
|
|
42
|
+
if parsed.version != 4:
|
|
43
|
+
raise ValueError("owned document id must be a UUID4")
|
|
44
|
+
return str(parsed)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _is_exact_not_found(error: DocStoreClientError, document_id: str) -> bool:
|
|
48
|
+
"""Accept only an absence result bound to the document just deleted."""
|
|
49
|
+
|
|
50
|
+
server_error = error.error
|
|
51
|
+
details = server_error.details
|
|
52
|
+
detail_document_id: object | None = None
|
|
53
|
+
if isinstance(details, Mapping) and "document_id" in details:
|
|
54
|
+
detail_document_id = details["document_id"]
|
|
55
|
+
if detail_document_id != document_id:
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
exact_message = f"NOT_FOUND: {document_id}"
|
|
59
|
+
if server_error.code == "NOT_FOUND":
|
|
60
|
+
return detail_document_id == document_id or server_error.message == exact_message
|
|
61
|
+
return server_error.code == "SERVER_ERROR" and server_error.message == exact_message
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class OwnedDocumentCleanup:
|
|
66
|
+
"""Track and delete only UUID4 documents created by this caller.
|
|
67
|
+
|
|
68
|
+
``DocStoreClient`` resolves queued adapter delivery before it returns a typed
|
|
69
|
+
result. Consequently this helper accepts only terminal ``deleted`` or
|
|
70
|
+
``already_absent`` outcomes and rejects a queue acknowledgement as a
|
|
71
|
+
cleanup success.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
client: OwnedDocumentClient
|
|
75
|
+
_owned_document_ids: set[str] = field(default_factory=set, init=False, repr=False)
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def owned_document_ids(self) -> tuple[str, ...]:
|
|
79
|
+
"""Return the remaining owned IDs in deterministic order."""
|
|
80
|
+
|
|
81
|
+
return tuple(sorted(self._owned_document_ids))
|
|
82
|
+
|
|
83
|
+
def register(self, document_id: str) -> None:
|
|
84
|
+
"""Register the exact UUID4 returned by this caller's successful create."""
|
|
85
|
+
|
|
86
|
+
self._owned_document_ids.add(_exact_uuid4(document_id))
|
|
87
|
+
|
|
88
|
+
async def cleanup(
|
|
89
|
+
self, document_id: str, *, session_id: str | None = None
|
|
90
|
+
) -> DocumentDeleteResult:
|
|
91
|
+
"""Delete one owned document and prove its public readback is absent."""
|
|
92
|
+
|
|
93
|
+
exact_id = _exact_uuid4(document_id)
|
|
94
|
+
if exact_id not in self._owned_document_ids:
|
|
95
|
+
raise PermissionError(f"refusing to clean up unowned document {exact_id}")
|
|
96
|
+
|
|
97
|
+
current = await self.client.get_document(DocumentGetRequest(document_id=exact_id))
|
|
98
|
+
if current.source_version is None:
|
|
99
|
+
raise OwnedDocumentCleanupError(
|
|
100
|
+
f"owned document {exact_id} has no current source_version for deletion"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
delete_request = DocumentDeleteRequest(
|
|
104
|
+
document_id=exact_id,
|
|
105
|
+
version_token=str(current.source_version),
|
|
106
|
+
)
|
|
107
|
+
if session_id is None:
|
|
108
|
+
result = await self.client.delete_document(delete_request)
|
|
109
|
+
else:
|
|
110
|
+
result = await self.client.delete_document(delete_request, session_id=session_id)
|
|
111
|
+
if result.document_id != exact_id:
|
|
112
|
+
raise OwnedDocumentCleanupError(
|
|
113
|
+
f"document_delete returned {result.document_id!r} for owned {exact_id!r}"
|
|
114
|
+
)
|
|
115
|
+
if result.outcome not in {"deleted", "already_absent"}:
|
|
116
|
+
raise OwnedDocumentCleanupError(
|
|
117
|
+
f"document_delete did not reach a terminal outcome for {exact_id}: {result.outcome!r}"
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
await self.client.get_document(DocumentGetRequest(document_id=exact_id))
|
|
122
|
+
except DocStoreClientError as exc:
|
|
123
|
+
if _is_exact_not_found(exc, exact_id):
|
|
124
|
+
self._owned_document_ids.remove(exact_id)
|
|
125
|
+
return result
|
|
126
|
+
raise OwnedDocumentCleanupError(
|
|
127
|
+
f"document_get failed after deleting owned {exact_id}: {exc}"
|
|
128
|
+
) from exc
|
|
129
|
+
raise OwnedDocumentCleanupError(
|
|
130
|
+
f"owned document {exact_id} is still readable after document_delete"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
async def cleanup_all(self, *, session_id: str | None = None) -> None:
|
|
134
|
+
"""Attempt every owned cleanup and propagate every failure as one error."""
|
|
135
|
+
|
|
136
|
+
failures: list[str] = []
|
|
137
|
+
for document_id in self.owned_document_ids:
|
|
138
|
+
try:
|
|
139
|
+
await self.cleanup(document_id, session_id=session_id)
|
|
140
|
+
except Exception as exc:
|
|
141
|
+
failures.append(f"{document_id}: {exc}")
|
|
142
|
+
if failures:
|
|
143
|
+
raise OwnedDocumentCleanupError("; ".join(failures))
|
|
File without changes
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Short deterministic phrases for verifying received text payloads."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from hashlib import sha256
|
|
6
|
+
|
|
7
|
+
_VERIFICATION_VERSION = "rx1"
|
|
8
|
+
_DIGEST_HEX_CHARS = 24
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def text_verification_phrase(text: str) -> str:
|
|
12
|
+
"""Return a short fingerprint of the exact UTF-8 text received by a command."""
|
|
13
|
+
|
|
14
|
+
if not isinstance(text, str):
|
|
15
|
+
raise TypeError("text must be a string")
|
|
16
|
+
payload = text.encode("utf-8")
|
|
17
|
+
digest = sha256(payload).hexdigest()[:_DIGEST_HEX_CHARS]
|
|
18
|
+
return f"{_VERIFICATION_VERSION}:{len(payload)}:{digest}"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
__all__ = ["text_verification_phrase"]
|
doc_store_client_cli.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Installed console entrypoint kept dependency-light for --help and discovery."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib.util
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
_CLI_PATH = Path(__file__).with_name("doc_store_client") / "cli.py"
|
|
10
|
+
_SPEC = importlib.util.spec_from_file_location("_doc_store_client_cli", _CLI_PATH)
|
|
11
|
+
if _SPEC is None or _SPEC.loader is None:
|
|
12
|
+
raise ImportError(f"Unable to load CLI implementation: {_CLI_PATH}")
|
|
13
|
+
_MODULE = importlib.util.module_from_spec(_SPEC)
|
|
14
|
+
_SPEC.loader.exec_module(_MODULE)
|
|
15
|
+
main = _MODULE.main
|
|
16
|
+
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: doc-store-client-dev
|
|
3
|
+
Version: 0.1.185
|
|
4
|
+
Summary: Client library for the doc-store documentation service.
|
|
5
|
+
Author: Vasiliy Zdanovskiy
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: documentation,client,search,json-rpc
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
+
Classifier: Topic :: Documentation
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
Requires-Dist: chunk-metadata-adapter>=3.5.1
|
|
21
|
+
Requires-Dist: mcp-proxy-adapter>=8.11.11
|
|
22
|
+
Provides-Extra: test
|
|
23
|
+
Requires-Dist: pytest<10,>=9.2.0; extra == "test"
|
|
24
|
+
Requires-Dist: pytest-asyncio<2,>=1.4.1.post1; extra == "test"
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: doc-store-client-dev[test]; extra == "dev"
|
|
27
|
+
Requires-Dist: ruff>=0.17.4; extra == "dev"
|
|
28
|
+
Requires-Dist: mypy>=2.4.1; extra == "dev"
|
|
29
|
+
Requires-Dist: build<2,>=1.6.1; extra == "dev"
|
|
30
|
+
|
|
31
|
+
# doc-store-client backup methods
|
|
32
|
+
|
|
33
|
+
`DocStoreClient` exposes typed wrappers for the six backup commands supported by the server:
|
|
34
|
+
|
|
35
|
+
| Method | Required arguments | Result type |
|
|
36
|
+
| --- | --- | --- |
|
|
37
|
+
| `backup_run` | None | `BackupRunResult` |
|
|
38
|
+
| `backup_status` | None | `BackupStatusResult` |
|
|
39
|
+
| `backup_points` | None | `BackupPointsResult` |
|
|
40
|
+
| `backup_inspect` | `point` | `BackupInspectResult` |
|
|
41
|
+
| `backup_restore` | `point` | `BackupRestoreResult` |
|
|
42
|
+
| `backup_recover_catalog` | `destination`, `catalog_copy` | `BackupRecoverCatalogResult` |
|
|
43
|
+
|
|
44
|
+
All six methods accept the optional `catalog_directory`. `backup_run` and `backup_restore` also
|
|
45
|
+
accept `dry_run`; `backup_restore` accepts `replace`; `backup_recover_catalog` accepts `dry_run`.
|
|
46
|
+
Omitted options remain absent from the command payload, allowing server configuration and command
|
|
47
|
+
defaults to apply. Passing `catalog_directory=None` explicitly sends JSON null, which the server
|
|
48
|
+
defines as unspecified. Boolean defaults are false on the server.
|
|
49
|
+
|
|
50
|
+
The methods return the command's `data` object using the corresponding public `TypedDict` result
|
|
51
|
+
type. In particular, `backup_points` includes committed points only, `backup_inspect` describes
|
|
52
|
+
the requested saved point and its chain, and `backup_recover_catalog` reports `activated=False`:
|
|
53
|
+
catalog recovery creates a separate catalog and never switches the active catalog.
|
|
54
|
+
|
|
55
|
+
Queued commands (`backup_run`, `backup_restore`, and `backup_recover_catalog`) use the injected
|
|
56
|
+
adapter's normal delivery and final-result handling. The client does not submit a second command
|
|
57
|
+
or poll the queue a second time. Synchronous commands (`backup_status`, `backup_points`, and
|
|
58
|
+
`backup_inspect`) return their result directly.
|
|
59
|
+
|
|
60
|
+
Server parameter and backup-domain errors are surfaced through the adapter response. When an
|
|
61
|
+
error response is represented by the standard client envelope, `DocStoreClientError.error`
|
|
62
|
+
contains a `ServerError` with its code, message, and details. Adapter transport exceptions
|
|
63
|
+
continue to propagate from the adapter. The wrappers do not convert failures into success-shaped
|
|
64
|
+
results.
|
|
65
|
+
|
|
66
|
+
Use `DocStoreClient.call(command, params)` when a caller needs to exercise invalid parameters or
|
|
67
|
+
send a command that has no typed wrapper. The typed methods describe the valid command inputs;
|
|
68
|
+
the server remains responsible for validating values and path relationships.
|
|
69
|
+
|
|
70
|
+
## Write sessions and lock errors
|
|
71
|
+
|
|
72
|
+
The 18 raw methods `document_create`, `document_update`, `document_delete`, `document_rebind`,
|
|
73
|
+
`document_chunk`, `chunk_version_add`, `chunk_version_update`, `chunk_version_set_current`,
|
|
74
|
+
`chunk_version_restore`, `chunk_version_retire`, `chunk_version_delete`,
|
|
75
|
+
`semantic_chunk_metadata_update`, `entity_create`, `entity_update`, `entity_rebind_owner`,
|
|
76
|
+
`entity_soft_delete`, `entity_undelete` and `entity_hard_delete`, and the 15 typed wrappers
|
|
77
|
+
`create_document`, `update_document`, `delete_document`, `rebind_document`, `chunk_document`,
|
|
78
|
+
`add_chunk_version`, `update_chunk_version`, `set_current_chunk_version`,
|
|
79
|
+
`restore_chunk_version`, `retire_chunk_version`, `delete_chunk_version`, `soft_delete_entities`,
|
|
80
|
+
`undelete_entities`, `hard_delete_entities` and `update_semantic_chunk_metadata`, take a
|
|
81
|
+
keyword-only `session_id` with default `None`. `None` is not sent; any other value is sent
|
|
82
|
+
unchanged. The client does not check the format; the server refuses a malformed value with
|
|
83
|
+
-32602, raised by the adapter. A raw method given `session_id` as a keyword and inside `params`
|
|
84
|
+
raises `ValueError` (`duplicate parameters: session_id`).
|
|
85
|
+
|
|
86
|
+
`DocStoreClient` keeps no session, so pass `session_id` on every call, and repeat a CAS write-back
|
|
87
|
+
with the same `session_id`. `DocStoreClient.call` passes a `session_id` inside `params` unchanged,
|
|
88
|
+
the CLI takes `session_id` inside the `--params` JSON object (there is no extra flag), and
|
|
89
|
+
`OwnedDocumentCleanup.cleanup` and `cleanup_all` take `session_id` and pass it to every
|
|
90
|
+
`document_delete`. `cleanup_all` collects failures into one `OwnedDocumentCleanupError`; call
|
|
91
|
+
`cleanup` per document to catch a typed lock error.
|
|
92
|
+
|
|
93
|
+
`document_export` and every other non-gated method take no `session_id`; a `session_id` sent to
|
|
94
|
+
them through `params` is refused by the server with -32602.
|
|
95
|
+
|
|
96
|
+
A refused write raises one of three errors, all subclasses of `DocStoreClientError` and importable
|
|
97
|
+
from `doc_store_client`:
|
|
98
|
+
|
|
99
|
+
| class | code | properties |
|
|
100
|
+
| --- | --- | --- |
|
|
101
|
+
| `FileAlreadyLockedError` | `FILE_ALREADY_LOCKED` | `document_id`, `holder_session_id`, `holder_idle_seconds`, `seconds_left_before_timeout` |
|
|
102
|
+
| `SessionNotFoundError` | `SESSION_NOT_FOUND` | `session_id` |
|
|
103
|
+
| `LockServiceUnavailableError` | `LOCK_SERVICE_UNAVAILABLE` | `reason` |
|
|
104
|
+
|
|
105
|
+
`error.code` and the message are kept. A missing field reads as `None`. Values come from the
|
|
106
|
+
`data` mapping in `error.details`, else from `error.details`. A refused write changes nothing on
|
|
107
|
+
the server. A failed queued `document_create` or `document_update` job with a lock code raises the
|
|
108
|
+
typed error as well.
|
|
109
|
+
|
|
110
|
+
The client never retries a refused write. React to each error yourself:
|
|
111
|
+
|
|
112
|
+
- `FileAlreadyLockedError`: another session holds the document. Do not retry blindly; use
|
|
113
|
+
`holder_idle_seconds` and `seconds_left_before_timeout` to decide when to try again.
|
|
114
|
+
- `SessionNotFoundError`: the session is unknown or expired. Open a new CAS session and repeat the
|
|
115
|
+
call with its id.
|
|
116
|
+
- `LockServiceUnavailableError`: CAS did not answer or answered with an error, and the write was
|
|
117
|
+
refused. Retry later by your own decision; `reason` says why.
|
|
118
|
+
|
|
119
|
+
## Missing target
|
|
120
|
+
|
|
121
|
+
A command whose target does not exist (a document, chapter, paragraph, chunk, entity) is answered
|
|
122
|
+
by the server with a structured error whose code is `NOT_FOUND` and whose message contains the
|
|
123
|
+
missing id. The client raises `DocStoreNotFoundError`, a subclass of `DocStoreClientError`
|
|
124
|
+
importable from `doc_store_client`:
|
|
125
|
+
|
|
126
|
+
| class | code | properties |
|
|
127
|
+
| --- | --- | --- |
|
|
128
|
+
| `DocStoreNotFoundError` | `NOT_FOUND` | `code`, `missing_id` |
|
|
129
|
+
|
|
130
|
+
`str(error)` is the server message (it contains the id), so callers that read the text keep
|
|
131
|
+
working; `error.error.code` is `NOT_FOUND`. `missing_id` is read from the error data, else from the
|
|
132
|
+
id in the message, else `None`. `entity_content_get` does not raise: it reports an absent id as an
|
|
133
|
+
item with `error.code` `NOT_FOUND`.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
doc_store_client_cli.py,sha256=iXj4Ohif4SRb5PDJcrSeBWqt62xOQOlKFMRGLQ8kOdM,539
|
|
2
|
+
doc_store_client/__init__.py,sha256=zIz7kfZNSwyrg3Mw-7soPTI7sy6AxveB0GMsLDkNwY4,5557
|
|
3
|
+
doc_store_client/cli.py,sha256=IGRSVUiRxXBJvEOSevqPwtmk2N2DRcpUz-1EosPFFVA,7108
|
|
4
|
+
doc_store_client/client.py,sha256=JliKYkDJL3E5MVGEc7BT_wbEWTLXkiUzSjKJKFeZgx0,53524
|
|
5
|
+
doc_store_client/models.py,sha256=6qFNLA0fYSWbKGJJmZnyEViN5DkJQ5UVmzpM_ZRNihU,56172
|
|
6
|
+
doc_store_client/owned_document_cleanup.py,sha256=KoaVdzuyza-WFALR-3JOSRYfZgHxo1mLH8vgEP8ZTwE,5552
|
|
7
|
+
doc_store_client/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
doc_store_client/text_verification.py,sha256=B0n5RH8U23DE56qgQvWHLBQALqGtiPFZ7qf44_4TQM4,607
|
|
9
|
+
doc_store_client_dev-0.1.185.dist-info/METADATA,sha256=IAe7ROhOHPTbqY1F5vqUV5-sO2hRKjgltGd3qgJheCs,7514
|
|
10
|
+
doc_store_client_dev-0.1.185.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
11
|
+
doc_store_client_dev-0.1.185.dist-info/entry_points.txt,sha256=wchMyRXsuJAC6Ajt07CFkjQBGgfYb2ET16sVXRBqrFI,63
|
|
12
|
+
doc_store_client_dev-0.1.185.dist-info/top_level.txt,sha256=MC4GKF-Wn-7Yanr8iePj-77t5NS0UPsr25IzdLp7JaA,38
|
|
13
|
+
doc_store_client_dev-0.1.185.dist-info/RECORD,,
|