quillwork-server 0.0.3__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.
- quillwork_server/__init__.py +9 -0
- quillwork_server/config.py +86 -0
- quillwork_server/core/__init__.py +61 -0
- quillwork_server/core/engine.py +149 -0
- quillwork_server/core/errors.py +110 -0
- quillwork_server/core/fake.py +1105 -0
- quillwork_server/core/locks.py +100 -0
- quillwork_server/core/service.py +621 -0
- quillwork_server/core/store.py +393 -0
- quillwork_server/core/types.py +97 -0
- quillwork_server/http_app.py +461 -0
- quillwork_server/mcp_server.py +419 -0
- quillwork_server/py.typed +0 -0
- quillwork_server/spec/tools/advanced_op.json +14 -0
- quillwork_server/spec/tools/comment.json +17 -0
- quillwork_server/spec/tools/edit.json +31 -0
- quillwork_server/spec/tools/find.json +13 -0
- quillwork_server/spec/tools/format.json +31 -0
- quillwork_server/spec/tools/read_document.json +13 -0
- quillwork_server/spec/tools/review.json +14 -0
- quillwork_server/spec/tools/table.json +17 -0
- quillwork_server/tools/__init__.py +27 -0
- quillwork_server/tools/compile.py +515 -0
- quillwork_server/tools/dispatch.py +103 -0
- quillwork_server/tools/render.py +116 -0
- quillwork_server/tools/spec.py +84 -0
- quillwork_server-0.0.3.dist-info/METADATA +67 -0
- quillwork_server-0.0.3.dist-info/RECORD +32 -0
- quillwork_server-0.0.3.dist-info/WHEEL +4 -0
- quillwork_server-0.0.3.dist-info/entry_points.txt +3 -0
- quillwork_server-0.0.3.dist-info/licenses/LICENSE +202 -0
- quillwork_server-0.0.3.dist-info/licenses/NOTICE +4 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Quillwork's reference service: a document service library, an MCP server and an HTTP API.
|
|
2
|
+
|
|
3
|
+
The library is :mod:`quillwork_server.core` (embed it in your application) and
|
|
4
|
+
:mod:`quillwork_server.tools` (the agent tool layer). The front ends are
|
|
5
|
+
:mod:`quillwork_server.mcp_server` (``quillwork-mcp``) and :mod:`quillwork_server.http_app`
|
|
6
|
+
(``quillwork-server``).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
__version__ = "0.0.3"
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Settings shared by the two front ends (``quillwork-mcp``, ``quillwork-server``).
|
|
2
|
+
|
|
3
|
+
Every setting has an environment variable (``QUILLWORK_*``) and a command-line flag; the flag
|
|
4
|
+
wins. Library users skip this module and build a :class:`~.core.DocumentService` themselves.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import os
|
|
11
|
+
from collections.abc import Mapping
|
|
12
|
+
from dataclasses import dataclass, replace
|
|
13
|
+
|
|
14
|
+
from .core import (
|
|
15
|
+
DocumentService,
|
|
16
|
+
Engine,
|
|
17
|
+
FakeEngine,
|
|
18
|
+
FileStore,
|
|
19
|
+
MemoryStore,
|
|
20
|
+
QuillworkEngine,
|
|
21
|
+
ServiceConfig,
|
|
22
|
+
Store,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
_DEFAULTS = ServiceConfig()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True, slots=True)
|
|
29
|
+
class Settings:
|
|
30
|
+
#: Directory for :class:`~.core.FileStore`; empty keeps documents in memory.
|
|
31
|
+
store: str = ""
|
|
32
|
+
#: ``quillwork`` (the real binding) or ``fake`` (plain text; for demos and tests).
|
|
33
|
+
engine: str = "quillwork"
|
|
34
|
+
#: Author on every transaction an agent makes through these front ends.
|
|
35
|
+
author: str = "AI"
|
|
36
|
+
#: HTTP: required ``Authorization: Bearer <key>`` (or ``X-API-Key``); empty disables auth.
|
|
37
|
+
api_key: str = ""
|
|
38
|
+
max_upload_bytes: int = _DEFAULTS.max_upload_bytes
|
|
39
|
+
max_ops_per_transaction: int = _DEFAULTS.max_ops_per_transaction
|
|
40
|
+
host: str = "127.0.0.1"
|
|
41
|
+
port: int = 8765
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def from_env(cls, env: Mapping[str, str] | None = None) -> Settings:
|
|
45
|
+
env = os.environ if env is None else env
|
|
46
|
+
base = cls()
|
|
47
|
+
return cls(
|
|
48
|
+
store=env.get("QUILLWORK_STORE", base.store),
|
|
49
|
+
engine=env.get("QUILLWORK_ENGINE", base.engine),
|
|
50
|
+
author=env.get("QUILLWORK_AUTHOR", base.author),
|
|
51
|
+
api_key=env.get("QUILLWORK_API_KEY", base.api_key),
|
|
52
|
+
max_upload_bytes=int(env.get("QUILLWORK_MAX_UPLOAD_BYTES", base.max_upload_bytes)),
|
|
53
|
+
max_ops_per_transaction=int(env.get("QUILLWORK_MAX_OPS", base.max_ops_per_transaction)),
|
|
54
|
+
host=env.get("QUILLWORK_HOST", base.host),
|
|
55
|
+
port=int(env.get("QUILLWORK_PORT", base.port)),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
def service(self) -> DocumentService:
|
|
59
|
+
engine: Engine = (
|
|
60
|
+
FakeEngine(author=self.author) if self.engine == "fake" else QuillworkEngine()
|
|
61
|
+
)
|
|
62
|
+
store: Store = FileStore(self.store) if self.store else MemoryStore()
|
|
63
|
+
config = ServiceConfig(
|
|
64
|
+
max_upload_bytes=self.max_upload_bytes,
|
|
65
|
+
max_ops_per_transaction=self.max_ops_per_transaction,
|
|
66
|
+
default_author=self.author,
|
|
67
|
+
)
|
|
68
|
+
return DocumentService(engine, store, config=config)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def add_arguments(parser: argparse.ArgumentParser) -> None:
|
|
72
|
+
parser.add_argument("--store", help="directory to keep documents in (default: memory)")
|
|
73
|
+
parser.add_argument("--engine", choices=("quillwork", "fake"))
|
|
74
|
+
parser.add_argument("--author", help="author name on agent edits (default: AI)")
|
|
75
|
+
parser.add_argument("--host")
|
|
76
|
+
parser.add_argument("--port", type=int)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def from_args(args: argparse.Namespace, base: Settings | None = None) -> Settings:
|
|
80
|
+
settings = base or Settings.from_env()
|
|
81
|
+
changes = {
|
|
82
|
+
key: getattr(args, key)
|
|
83
|
+
for key in ("store", "engine", "author", "host", "port")
|
|
84
|
+
if getattr(args, key, None) is not None
|
|
85
|
+
}
|
|
86
|
+
return replace(settings, **changes)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""The library core: store, locks, engine seam and the document service.
|
|
2
|
+
|
|
3
|
+
Import from here; the submodules are an implementation detail.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .engine import Engine, EngineDocument, QuillworkEngine, asset_id, sniff_image
|
|
7
|
+
from .errors import SPEC_VERSION, document_not_found, envelope, error_object, make_error
|
|
8
|
+
from .fake import FakeDocument, FakeEngine, fake_bytes
|
|
9
|
+
from .locks import LocalLocks, LocalNotifier, LockProvider, LockTimeout, Notifier
|
|
10
|
+
from .service import DocumentService, ExportVariant, ServiceConfig, Session
|
|
11
|
+
from .store import (
|
|
12
|
+
DocumentExists,
|
|
13
|
+
DocumentNotFound,
|
|
14
|
+
FileStore,
|
|
15
|
+
HeadMoved,
|
|
16
|
+
MemoryStore,
|
|
17
|
+
Store,
|
|
18
|
+
StoreError,
|
|
19
|
+
VersionNotFound,
|
|
20
|
+
)
|
|
21
|
+
from .types import JSON, Asset, DocumentInfo, PatchFeed, StoredVersion, VersionKind, VersionRecord
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"JSON",
|
|
25
|
+
"SPEC_VERSION",
|
|
26
|
+
"Asset",
|
|
27
|
+
"DocumentExists",
|
|
28
|
+
"DocumentInfo",
|
|
29
|
+
"DocumentNotFound",
|
|
30
|
+
"DocumentService",
|
|
31
|
+
"Engine",
|
|
32
|
+
"EngineDocument",
|
|
33
|
+
"ExportVariant",
|
|
34
|
+
"FakeDocument",
|
|
35
|
+
"FakeEngine",
|
|
36
|
+
"FileStore",
|
|
37
|
+
"HeadMoved",
|
|
38
|
+
"LocalLocks",
|
|
39
|
+
"LocalNotifier",
|
|
40
|
+
"LockProvider",
|
|
41
|
+
"LockTimeout",
|
|
42
|
+
"MemoryStore",
|
|
43
|
+
"Notifier",
|
|
44
|
+
"PatchFeed",
|
|
45
|
+
"QuillworkEngine",
|
|
46
|
+
"ServiceConfig",
|
|
47
|
+
"Session",
|
|
48
|
+
"Store",
|
|
49
|
+
"StoreError",
|
|
50
|
+
"StoredVersion",
|
|
51
|
+
"VersionKind",
|
|
52
|
+
"VersionNotFound",
|
|
53
|
+
"VersionRecord",
|
|
54
|
+
"asset_id",
|
|
55
|
+
"document_not_found",
|
|
56
|
+
"envelope",
|
|
57
|
+
"error_object",
|
|
58
|
+
"fake_bytes",
|
|
59
|
+
"make_error",
|
|
60
|
+
"sniff_image",
|
|
61
|
+
]
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""The engine seam: what the service needs from a document engine.
|
|
2
|
+
|
|
3
|
+
:class:`QuillworkEngine` adapts the real binding (``quillwork.Document``); ``fake.FakeEngine``
|
|
4
|
+
implements the same protocol on plain text for tests. Both speak the spec's JSON shapes: a
|
|
5
|
+
Transaction in, an OpResult out; a ReadOp in, a ReadResult out; errors as
|
|
6
|
+
:class:`quillwork.errors.QuillworkError`.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import hashlib
|
|
12
|
+
import json
|
|
13
|
+
from typing import Any, Protocol, runtime_checkable
|
|
14
|
+
|
|
15
|
+
from .types import JSON, Asset
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@runtime_checkable
|
|
19
|
+
class EngineDocument(Protocol):
|
|
20
|
+
"""One open document. Not thread-safe: the service serialises calls on it."""
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def version(self) -> int: ...
|
|
24
|
+
|
|
25
|
+
def apply(self, transaction: JSON) -> JSON:
|
|
26
|
+
"""Apply a Transaction atomically; return the OpResult. Raises QuillworkError."""
|
|
27
|
+
...
|
|
28
|
+
|
|
29
|
+
def read_op(self, request: JSON) -> JSON:
|
|
30
|
+
"""Run a ReadOp; return the ReadResult. Raises QuillworkError."""
|
|
31
|
+
...
|
|
32
|
+
|
|
33
|
+
def view_model(self) -> JSON: ...
|
|
34
|
+
|
|
35
|
+
def save(self) -> bytes: ...
|
|
36
|
+
|
|
37
|
+
def sidecar(self) -> JSON | None:
|
|
38
|
+
"""Engine state that is not in the .docx (anchor map, change sets), versioned with it."""
|
|
39
|
+
...
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@runtime_checkable
|
|
43
|
+
class Engine(Protocol):
|
|
44
|
+
name: str
|
|
45
|
+
|
|
46
|
+
def open(self, data: bytes, *, version: int = 1, sidecar: JSON | None = None) -> EngineDocument:
|
|
47
|
+
"""Open ``data`` as ``version``, restoring ``sidecar`` when given.
|
|
48
|
+
|
|
49
|
+
Raises QuillworkError (BAD_PACKAGE, PROTECTED, LIMIT) for unusable input.
|
|
50
|
+
"""
|
|
51
|
+
...
|
|
52
|
+
|
|
53
|
+
def asset(self, data: bytes, asset_id: str) -> Asset | None:
|
|
54
|
+
"""The bytes of an image the document at ``data`` holds, by asset id (``a_<16 hex>``)."""
|
|
55
|
+
...
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def asset_id(data: bytes) -> str:
|
|
59
|
+
"""Asset ids are the first 16 hex digits of sha256(bytes) (spec/anchors.md)."""
|
|
60
|
+
return "a_" + hashlib.sha256(data).hexdigest()[:16]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
_MAGIC: tuple[tuple[bytes, str], ...] = (
|
|
64
|
+
(b"\x89PNG\r\n\x1a\n", "image/png"),
|
|
65
|
+
(b"\xff\xd8\xff", "image/jpeg"),
|
|
66
|
+
(b"GIF87a", "image/gif"),
|
|
67
|
+
(b"GIF89a", "image/gif"),
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def sniff_image(data: bytes) -> str | None:
|
|
72
|
+
"""The image MIME type from magic bytes, or ``None``. Never trusts a declared type."""
|
|
73
|
+
for magic, mime in _MAGIC:
|
|
74
|
+
if data.startswith(magic):
|
|
75
|
+
return mime
|
|
76
|
+
if data[:4] == b"RIFF" and data[8:12] == b"WEBP":
|
|
77
|
+
return "image/webp"
|
|
78
|
+
head = data[:512].lstrip().lower()
|
|
79
|
+
if head.startswith(b"<svg") or (head.startswith(b"<?xml") and b"<svg" in head):
|
|
80
|
+
return "image/svg+xml"
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# -- the real binding ------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class QuillworkEngine:
|
|
88
|
+
""":class:`Engine` over :class:`quillwork.Document`.
|
|
89
|
+
|
|
90
|
+
Versions and sidecars pass straight through: ``Document.open(data, sidecar=..., version=...)``
|
|
91
|
+
opens at ``max(version, the sidecar's)``, which is how the service reopens an import or a
|
|
92
|
+
restore at ``head + 1`` with an older sidecar.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
name = "quillwork"
|
|
96
|
+
|
|
97
|
+
def __init__(self, limits: Any | None = None) -> None:
|
|
98
|
+
import quillwork
|
|
99
|
+
|
|
100
|
+
self._quillwork = quillwork
|
|
101
|
+
self._limits = limits
|
|
102
|
+
|
|
103
|
+
def open(self, data: bytes, *, version: int = 1, sidecar: JSON | None = None) -> EngineDocument:
|
|
104
|
+
doc = self._quillwork.Document.open(data, self._limits, sidecar=sidecar, version=version)
|
|
105
|
+
return _QuillworkDocument(doc)
|
|
106
|
+
|
|
107
|
+
def asset(self, data: bytes, asset_id_: str) -> Asset | None:
|
|
108
|
+
package = self._quillwork.Package.open(data)
|
|
109
|
+
for name in package.names():
|
|
110
|
+
if not name.startswith("word/media/"):
|
|
111
|
+
continue
|
|
112
|
+
part = bytes(package.part(name))
|
|
113
|
+
if asset_id(part) == asset_id_:
|
|
114
|
+
mime = (
|
|
115
|
+
sniff_image(part) or package.content_type_of(name) or "application/octet-stream"
|
|
116
|
+
)
|
|
117
|
+
return Asset(part, mime)
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class _QuillworkDocument:
|
|
122
|
+
""":class:`EngineDocument` over an open :class:`quillwork.Document`."""
|
|
123
|
+
|
|
124
|
+
def __init__(self, doc: Any) -> None:
|
|
125
|
+
self._doc = doc
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def version(self) -> int:
|
|
129
|
+
return int(self._doc.version)
|
|
130
|
+
|
|
131
|
+
def apply(self, transaction: JSON) -> JSON:
|
|
132
|
+
result: JSON = self._doc.apply(transaction)
|
|
133
|
+
return result
|
|
134
|
+
|
|
135
|
+
def read_op(self, request: JSON) -> JSON:
|
|
136
|
+
result: JSON = self._doc.read_op(request)
|
|
137
|
+
return result
|
|
138
|
+
|
|
139
|
+
def view_model(self) -> JSON:
|
|
140
|
+
result: JSON = self._doc.view_model()
|
|
141
|
+
return result
|
|
142
|
+
|
|
143
|
+
def save(self) -> bytes:
|
|
144
|
+
return bytes(self._doc.save())
|
|
145
|
+
|
|
146
|
+
def sidecar(self) -> JSON | None:
|
|
147
|
+
"""The sidecar as a JSON object (the binding caches it from the last save)."""
|
|
148
|
+
result: JSON = json.loads(self._doc.sidecar())
|
|
149
|
+
return result
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Spec error objects (``spec/schema/error.schema.json``) built and rendered on the service side.
|
|
2
|
+
|
|
3
|
+
The service raises the binding's own exception classes (:mod:`quillwork.errors`), so a host sees
|
|
4
|
+
one error type whether the engine or the service refused a call. :func:`make_error` builds one
|
|
5
|
+
with a complete spec ``error`` object; :func:`envelope` turns any exception into the
|
|
6
|
+
``{spec_version, ok: false, error}`` response the front ends return.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any, Final
|
|
12
|
+
|
|
13
|
+
from quillwork.errors import QuillworkError, error_from_dict
|
|
14
|
+
|
|
15
|
+
from .types import JSON
|
|
16
|
+
|
|
17
|
+
SPEC_VERSION: Final = "0.1.0"
|
|
18
|
+
|
|
19
|
+
#: retryable per code (spec/errors.md). UNSUPPORTED depends on the reason; see make_error.
|
|
20
|
+
RETRYABLE: Final[dict[str, bool]] = {
|
|
21
|
+
"STALE_ANCHOR": True,
|
|
22
|
+
"AMBIGUOUS": True,
|
|
23
|
+
"NOT_FOUND": True,
|
|
24
|
+
"IN_FIELD": True,
|
|
25
|
+
"INVALID_OP": True,
|
|
26
|
+
"UNSUPPORTED": False,
|
|
27
|
+
"CONFIRM_REQUIRED": True,
|
|
28
|
+
"PROTECTED": False,
|
|
29
|
+
"LIMIT": True,
|
|
30
|
+
"BAD_VERSION": True,
|
|
31
|
+
"CONFLICT": True,
|
|
32
|
+
"BAD_PACKAGE": False,
|
|
33
|
+
"INVARIANT": False,
|
|
34
|
+
"INTERNAL": True,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
#: UNSUPPORTED reasons a retry with other arguments can get past.
|
|
38
|
+
_RETRYABLE_UNSUPPORTED: Final = frozenset({"direct_only", "track_only"})
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def make_error(
|
|
42
|
+
code: str,
|
|
43
|
+
message: str,
|
|
44
|
+
*,
|
|
45
|
+
version: int,
|
|
46
|
+
op_index: int | None = None,
|
|
47
|
+
retryable: bool | None = None,
|
|
48
|
+
**fields: Any,
|
|
49
|
+
) -> QuillworkError:
|
|
50
|
+
"""The binding exception for ``code`` carrying a full spec error object.
|
|
51
|
+
|
|
52
|
+
``fields`` are the per-code members (``anchor``, ``current``, ``what``, ``reason``, ``limit``,
|
|
53
|
+
``path``, ``usage``, ``candidates``...); ``None`` values are left out.
|
|
54
|
+
"""
|
|
55
|
+
if retryable is None:
|
|
56
|
+
retryable = RETRYABLE.get(code, False)
|
|
57
|
+
if code == "UNSUPPORTED":
|
|
58
|
+
retryable = fields.get("reason") in _RETRYABLE_UNSUPPORTED
|
|
59
|
+
error: JSON = {
|
|
60
|
+
"code": code,
|
|
61
|
+
"message": message,
|
|
62
|
+
"retryable": retryable,
|
|
63
|
+
"version": max(1, int(version)),
|
|
64
|
+
"op_index": op_index,
|
|
65
|
+
}
|
|
66
|
+
for key, value in fields.items():
|
|
67
|
+
if value is not None:
|
|
68
|
+
error[key] = value
|
|
69
|
+
return error_from_dict(error)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def error_object(exc: BaseException, *, version: int | None = None) -> JSON:
|
|
73
|
+
"""The spec ``error`` object for any exception.
|
|
74
|
+
|
|
75
|
+
``version`` fills in a missing current version (the binding leaves it ``None`` for errors raised
|
|
76
|
+
before a document exists, e.g. BAD_PACKAGE on upload).
|
|
77
|
+
"""
|
|
78
|
+
if isinstance(exc, QuillworkError):
|
|
79
|
+
error = dict(exc.error)
|
|
80
|
+
error.setdefault("code", exc.code)
|
|
81
|
+
error.setdefault("message", exc.message or str(exc))
|
|
82
|
+
error.setdefault("retryable", exc.retryable)
|
|
83
|
+
error.setdefault("op_index", exc.op_index)
|
|
84
|
+
if not isinstance(error.get("version"), int) or error["version"] < 1:
|
|
85
|
+
error["version"] = max(1, version or exc.version or 1)
|
|
86
|
+
if error["code"] in ("CONFLICT", "INTERNAL"):
|
|
87
|
+
error["retryable"] = True
|
|
88
|
+
if error["code"] in ("INVARIANT", "BAD_PACKAGE", "PROTECTED"):
|
|
89
|
+
error["retryable"] = False
|
|
90
|
+
return error
|
|
91
|
+
return {
|
|
92
|
+
"code": "INTERNAL",
|
|
93
|
+
"message": "The engine failed unexpectedly; retry once, then report it.",
|
|
94
|
+
"retryable": True,
|
|
95
|
+
"version": max(1, version or 1),
|
|
96
|
+
"op_index": None,
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def envelope(exc: BaseException, *, version: int | None = None) -> JSON:
|
|
101
|
+
"""``{spec_version, ok: false, error}`` for ``exc``."""
|
|
102
|
+
return {"spec_version": SPEC_VERSION, "ok": False, "error": error_object(exc, version=version)}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def document_not_found(document_id: str) -> QuillworkError:
|
|
106
|
+
# `what: "document"` is not in the 0.1.0 enum yet (proposed in docs/SERVER.md). `version` is
|
|
107
|
+
# required (minimum 1) even though no document exists, so it is 1.
|
|
108
|
+
return make_error(
|
|
109
|
+
"NOT_FOUND", f"There is no document {document_id!r}.", version=1, what="document"
|
|
110
|
+
)
|