atomicmemory 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- atomicmemory/__init__.py +166 -0
- atomicmemory/_version.py +3 -0
- atomicmemory/client/__init__.py +22 -0
- atomicmemory/client/async_memory_client.py +202 -0
- atomicmemory/client/atomic_memory_client.py +181 -0
- atomicmemory/client/memory_client.py +292 -0
- atomicmemory/core/__init__.py +34 -0
- atomicmemory/core/errors.py +122 -0
- atomicmemory/core/events.py +65 -0
- atomicmemory/core/logging.py +37 -0
- atomicmemory/core/retry.py +124 -0
- atomicmemory/core/validation.py +22 -0
- atomicmemory/embeddings/__init__.py +16 -0
- atomicmemory/embeddings/base.py +39 -0
- atomicmemory/embeddings/sentence_transformers.py +104 -0
- atomicmemory/kv_cache/__init__.py +17 -0
- atomicmemory/kv_cache/adapter.py +50 -0
- atomicmemory/kv_cache/memory_storage.py +98 -0
- atomicmemory/kv_cache/sqlite_storage.py +122 -0
- atomicmemory/memory/__init__.py +82 -0
- atomicmemory/memory/filters.py +68 -0
- atomicmemory/memory/pipeline.py +42 -0
- atomicmemory/memory/provider.py +397 -0
- atomicmemory/memory/registry.py +95 -0
- atomicmemory/memory/service.py +199 -0
- atomicmemory/memory/types.py +398 -0
- atomicmemory/providers/__init__.py +5 -0
- atomicmemory/providers/atomicmemory/__init__.py +43 -0
- atomicmemory/providers/atomicmemory/agents.py +156 -0
- atomicmemory/providers/atomicmemory/async_handle_impl.py +198 -0
- atomicmemory/providers/atomicmemory/async_provider.py +245 -0
- atomicmemory/providers/atomicmemory/audit.py +74 -0
- atomicmemory/providers/atomicmemory/config.py +38 -0
- atomicmemory/providers/atomicmemory/config_handle.py +123 -0
- atomicmemory/providers/atomicmemory/handle.py +513 -0
- atomicmemory/providers/atomicmemory/handle_impl.py +325 -0
- atomicmemory/providers/atomicmemory/http.py +255 -0
- atomicmemory/providers/atomicmemory/lessons.py +133 -0
- atomicmemory/providers/atomicmemory/lifecycle.py +202 -0
- atomicmemory/providers/atomicmemory/mappers.py +125 -0
- atomicmemory/providers/atomicmemory/path.py +20 -0
- atomicmemory/providers/atomicmemory/provider.py +300 -0
- atomicmemory/providers/atomicmemory/scope_mapper.py +98 -0
- atomicmemory/providers/mem0/__init__.py +41 -0
- atomicmemory/providers/mem0/async_provider.py +191 -0
- atomicmemory/providers/mem0/config.py +51 -0
- atomicmemory/providers/mem0/http.py +195 -0
- atomicmemory/providers/mem0/mappers.py +145 -0
- atomicmemory/providers/mem0/provider.py +202 -0
- atomicmemory/py.typed +0 -0
- atomicmemory/search/__init__.py +47 -0
- atomicmemory/search/chunking.py +161 -0
- atomicmemory/search/ranking.py +94 -0
- atomicmemory/search/semantic_search.py +130 -0
- atomicmemory/search/similarity.py +110 -0
- atomicmemory/storage/__init__.py +63 -0
- atomicmemory/storage/_mapping.py +305 -0
- atomicmemory/storage/async_client.py +208 -0
- atomicmemory/storage/client.py +339 -0
- atomicmemory/storage/errors.py +115 -0
- atomicmemory/storage/types.py +305 -0
- atomicmemory/utils/__init__.py +5 -0
- atomicmemory/utils/environment.py +23 -0
- atomicmemory-1.0.0.dist-info/METADATA +146 -0
- atomicmemory-1.0.0.dist-info/RECORD +67 -0
- atomicmemory-1.0.0.dist-info/WHEEL +4 -0
- atomicmemory-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
"""Synchronous client for backend artifact storage.
|
|
2
|
+
|
|
3
|
+
This module calls core's `/v1/storage/artifacts/*` API and mirrors the
|
|
4
|
+
TypeScript SDK's `ConcreteStorageClient`. It sends bearer auth plus
|
|
5
|
+
``X-AtomicMemory-User-Id`` on every request and never serializes the
|
|
6
|
+
legacy ``?user_id=`` URL parameter.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import base64
|
|
12
|
+
import json
|
|
13
|
+
from collections.abc import Iterator
|
|
14
|
+
from contextlib import contextmanager
|
|
15
|
+
from types import TracebackType
|
|
16
|
+
from typing import Any
|
|
17
|
+
from urllib.parse import quote, urlencode
|
|
18
|
+
|
|
19
|
+
import httpx
|
|
20
|
+
from pydantic import ValidationError as PydanticValidationError
|
|
21
|
+
|
|
22
|
+
from atomicmemory.core.validation import sanitized_pydantic_errors
|
|
23
|
+
from atomicmemory.storage._mapping import (
|
|
24
|
+
map_delete_result,
|
|
25
|
+
map_head_headers,
|
|
26
|
+
map_stored_artifact,
|
|
27
|
+
map_verify_result,
|
|
28
|
+
raise_for_storage_response,
|
|
29
|
+
validate_response_model,
|
|
30
|
+
)
|
|
31
|
+
from atomicmemory.storage.errors import StorageClientError
|
|
32
|
+
from atomicmemory.storage.types import (
|
|
33
|
+
ArtifactHead,
|
|
34
|
+
ArtifactMetadata,
|
|
35
|
+
ArtifactRef,
|
|
36
|
+
DeleteArtifactOptions,
|
|
37
|
+
DeleteArtifactResult,
|
|
38
|
+
ManagedBody,
|
|
39
|
+
PutArtifactInput,
|
|
40
|
+
PutManagedInput,
|
|
41
|
+
PutPointerInput,
|
|
42
|
+
StorageCapabilities,
|
|
43
|
+
StorageClientConfig,
|
|
44
|
+
StoredArtifact,
|
|
45
|
+
VerificationResult,
|
|
46
|
+
VerifyArtifactOptions,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
METADATA_HEADER = "X-AtomicMemory-Metadata"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class StorageClient:
|
|
53
|
+
"""Sync entry point for the direct artifact-storage API."""
|
|
54
|
+
|
|
55
|
+
def __init__(self, config: StorageClientConfig | dict[str, Any]) -> None:
|
|
56
|
+
self._config = _coerce_config(config)
|
|
57
|
+
self._api_url = self._config.api_url.rstrip("/")
|
|
58
|
+
self._client = httpx.Client(timeout=self._config.timeout_seconds)
|
|
59
|
+
|
|
60
|
+
def capabilities(self) -> StorageCapabilities:
|
|
61
|
+
response = self._request("GET", "/v1/storage/capabilities")
|
|
62
|
+
return validate_response_model(StorageCapabilities, _json_response(response), "StorageCapabilities")
|
|
63
|
+
|
|
64
|
+
def put(self, input: PutArtifactInput | dict[str, Any]) -> StoredArtifact:
|
|
65
|
+
value = _coerce_put_input(input)
|
|
66
|
+
if isinstance(value, PutPointerInput):
|
|
67
|
+
return self._put_pointer(value)
|
|
68
|
+
return self._put_managed(value)
|
|
69
|
+
|
|
70
|
+
def get(self, ref: ArtifactRef | dict[str, Any]) -> StoredArtifact:
|
|
71
|
+
artifact_id = _require_artifact_id(ref)
|
|
72
|
+
response = self._request("GET", f"/v1/storage/artifacts/{_quote_id(artifact_id)}", artifact_id=artifact_id)
|
|
73
|
+
return map_stored_artifact(_json_response(response))
|
|
74
|
+
|
|
75
|
+
def get_content(self, ref: ArtifactRef | dict[str, Any]) -> httpx.Response:
|
|
76
|
+
"""Return a fully buffered content response for small artifacts.
|
|
77
|
+
|
|
78
|
+
For large artifacts, use :meth:`stream_content` so the response
|
|
79
|
+
body is consumed incrementally inside a context manager.
|
|
80
|
+
"""
|
|
81
|
+
artifact_id = _require_artifact_id(ref)
|
|
82
|
+
return self._request("GET", f"/v1/storage/artifacts/{_quote_id(artifact_id)}/content", artifact_id=artifact_id)
|
|
83
|
+
|
|
84
|
+
@contextmanager
|
|
85
|
+
def stream_content(self, ref: ArtifactRef | dict[str, Any]) -> Iterator[httpx.Response]:
|
|
86
|
+
"""Stream artifact bytes without loading the whole response into memory."""
|
|
87
|
+
artifact_id = _require_artifact_id(ref)
|
|
88
|
+
path = f"/v1/storage/artifacts/{_quote_id(artifact_id)}/content"
|
|
89
|
+
try:
|
|
90
|
+
with self._client.stream("GET", f"{self._api_url}{path}", headers=self._headers(None)) as response:
|
|
91
|
+
if response.is_success:
|
|
92
|
+
yield response
|
|
93
|
+
return
|
|
94
|
+
response.read()
|
|
95
|
+
raise_for_storage_response(response, artifact_id)
|
|
96
|
+
except httpx.RequestError as exc:
|
|
97
|
+
raise _network_error("GET", path, exc) from exc
|
|
98
|
+
raise AssertionError("unreachable")
|
|
99
|
+
|
|
100
|
+
def head(self, ref: ArtifactRef | dict[str, Any]) -> ArtifactHead:
|
|
101
|
+
artifact_id = _require_artifact_id(ref)
|
|
102
|
+
response = self._request("HEAD", f"/v1/storage/artifacts/{_quote_id(artifact_id)}", artifact_id=artifact_id)
|
|
103
|
+
return map_head_headers(response.headers, artifact_id)
|
|
104
|
+
|
|
105
|
+
def delete(
|
|
106
|
+
self,
|
|
107
|
+
ref: ArtifactRef | dict[str, Any],
|
|
108
|
+
options: DeleteArtifactOptions | dict[str, Any] | None = None,
|
|
109
|
+
) -> DeleteArtifactResult:
|
|
110
|
+
artifact_id = _require_artifact_id(ref)
|
|
111
|
+
path = _delete_path(artifact_id, options)
|
|
112
|
+
response = self._request("DELETE", path, artifact_id=artifact_id)
|
|
113
|
+
return map_delete_result(_json_response(response))
|
|
114
|
+
|
|
115
|
+
def verify(
|
|
116
|
+
self,
|
|
117
|
+
ref: ArtifactRef | dict[str, Any],
|
|
118
|
+
options: VerifyArtifactOptions | dict[str, Any] | None = None,
|
|
119
|
+
) -> VerificationResult:
|
|
120
|
+
_coerce_verify_options(options)
|
|
121
|
+
artifact_id = _require_artifact_id(ref)
|
|
122
|
+
path = f"/v1/storage/artifacts/{_quote_id(artifact_id)}/verify"
|
|
123
|
+
response = self._request("POST", path, artifact_id=artifact_id)
|
|
124
|
+
return map_verify_result(_json_response(response))
|
|
125
|
+
|
|
126
|
+
def close(self) -> None:
|
|
127
|
+
self._client.close()
|
|
128
|
+
|
|
129
|
+
def __enter__(self) -> StorageClient:
|
|
130
|
+
return self
|
|
131
|
+
|
|
132
|
+
def __exit__(
|
|
133
|
+
self,
|
|
134
|
+
exc_type: type[BaseException] | None,
|
|
135
|
+
exc: BaseException | None,
|
|
136
|
+
tb: TracebackType | None,
|
|
137
|
+
) -> None:
|
|
138
|
+
self.close()
|
|
139
|
+
|
|
140
|
+
def _put_pointer(self, input: PutPointerInput) -> StoredArtifact:
|
|
141
|
+
payload = _pointer_payload(input)
|
|
142
|
+
response = self._request(
|
|
143
|
+
"POST",
|
|
144
|
+
"/v1/storage/artifacts",
|
|
145
|
+
headers={"Content-Type": "application/json"},
|
|
146
|
+
content=json.dumps(payload, separators=(",", ":")).encode(),
|
|
147
|
+
)
|
|
148
|
+
return map_stored_artifact(_json_response(response))
|
|
149
|
+
|
|
150
|
+
def _put_managed(self, input: PutManagedInput) -> StoredArtifact:
|
|
151
|
+
body = _coerce_managed_body(input.body)
|
|
152
|
+
path = _managed_path(input.disclose_content_hash)
|
|
153
|
+
headers = {"Content-Type": input.content_type, "Content-Length": str(len(body))}
|
|
154
|
+
if input.metadata is not None:
|
|
155
|
+
headers[METADATA_HEADER] = _encode_metadata_header(input.metadata)
|
|
156
|
+
response = self._request("POST", path, headers=headers, content=body)
|
|
157
|
+
return map_stored_artifact(_json_response(response))
|
|
158
|
+
|
|
159
|
+
def _request(
|
|
160
|
+
self,
|
|
161
|
+
method: str,
|
|
162
|
+
path: str,
|
|
163
|
+
*,
|
|
164
|
+
headers: dict[str, str] | None = None,
|
|
165
|
+
content: bytes | None = None,
|
|
166
|
+
artifact_id: str | None = None,
|
|
167
|
+
) -> httpx.Response:
|
|
168
|
+
try:
|
|
169
|
+
response = self._client.request(
|
|
170
|
+
method,
|
|
171
|
+
f"{self._api_url}{path}",
|
|
172
|
+
headers=self._headers(headers),
|
|
173
|
+
content=content,
|
|
174
|
+
)
|
|
175
|
+
except httpx.RequestError as exc:
|
|
176
|
+
raise _network_error(method, path, exc) from exc
|
|
177
|
+
if response.is_success:
|
|
178
|
+
return response
|
|
179
|
+
raise_for_storage_response(response, artifact_id)
|
|
180
|
+
raise AssertionError("unreachable")
|
|
181
|
+
|
|
182
|
+
def _headers(self, extra: dict[str, str] | None) -> dict[str, str]:
|
|
183
|
+
headers = {
|
|
184
|
+
"Authorization": f"Bearer {self._config.api_key}",
|
|
185
|
+
"X-AtomicMemory-User-Id": self._config.user_id,
|
|
186
|
+
}
|
|
187
|
+
if extra:
|
|
188
|
+
headers.update(extra)
|
|
189
|
+
return headers
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _coerce_config(value: StorageClientConfig | dict[str, Any]) -> StorageClientConfig:
|
|
193
|
+
if isinstance(value, StorageClientConfig):
|
|
194
|
+
return value
|
|
195
|
+
try:
|
|
196
|
+
return StorageClientConfig.model_validate(value)
|
|
197
|
+
except PydanticValidationError as exc:
|
|
198
|
+
raise _validation_error("StorageClientConfig", exc) from exc
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _coerce_put_input(value: PutArtifactInput | dict[str, Any]) -> PutArtifactInput:
|
|
202
|
+
if isinstance(value, PutPointerInput | PutManagedInput):
|
|
203
|
+
return value
|
|
204
|
+
if not isinstance(value, dict):
|
|
205
|
+
raise _input_error("PutArtifactInput must be a model or dict")
|
|
206
|
+
try:
|
|
207
|
+
if value.get("mode") == "pointer":
|
|
208
|
+
return PutPointerInput.model_validate(value)
|
|
209
|
+
if value.get("mode") == "managed":
|
|
210
|
+
return PutManagedInput.model_validate(value)
|
|
211
|
+
except PydanticValidationError as exc:
|
|
212
|
+
raise _validation_error("PutArtifactInput", exc) from exc
|
|
213
|
+
raise _input_error("PutArtifactInput.mode must be 'pointer' or 'managed'")
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _coerce_ref(value: ArtifactRef | dict[str, Any]) -> ArtifactRef:
|
|
217
|
+
if isinstance(value, ArtifactRef):
|
|
218
|
+
return value
|
|
219
|
+
try:
|
|
220
|
+
return ArtifactRef.model_validate(value)
|
|
221
|
+
except PydanticValidationError as exc:
|
|
222
|
+
raise _validation_error("ArtifactRef", exc) from exc
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _coerce_delete_options(value: DeleteArtifactOptions | dict[str, Any] | None) -> DeleteArtifactOptions:
|
|
226
|
+
if value is None:
|
|
227
|
+
return DeleteArtifactOptions()
|
|
228
|
+
if isinstance(value, DeleteArtifactOptions):
|
|
229
|
+
return value
|
|
230
|
+
try:
|
|
231
|
+
return DeleteArtifactOptions.model_validate(value)
|
|
232
|
+
except PydanticValidationError as exc:
|
|
233
|
+
raise _validation_error("DeleteArtifactOptions", exc) from exc
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _coerce_verify_options(value: VerifyArtifactOptions | dict[str, Any] | None) -> VerifyArtifactOptions:
|
|
237
|
+
if value is None:
|
|
238
|
+
return VerifyArtifactOptions()
|
|
239
|
+
if isinstance(value, VerifyArtifactOptions):
|
|
240
|
+
return value
|
|
241
|
+
try:
|
|
242
|
+
return VerifyArtifactOptions.model_validate(value)
|
|
243
|
+
except PydanticValidationError as exc:
|
|
244
|
+
raise _validation_error("VerifyArtifactOptions", exc) from exc
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _require_artifact_id(ref: ArtifactRef | dict[str, Any]) -> str:
|
|
248
|
+
artifact_id = _coerce_ref(ref).artifact_id
|
|
249
|
+
if artifact_id is None:
|
|
250
|
+
raise StorageClientError(
|
|
251
|
+
"ArtifactRef.artifact_id is required for this operation in v1",
|
|
252
|
+
error_code="missing_artifact_id",
|
|
253
|
+
status=0,
|
|
254
|
+
body_text="",
|
|
255
|
+
)
|
|
256
|
+
return artifact_id
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _pointer_payload(input: PutPointerInput) -> dict[str, Any]:
|
|
260
|
+
payload: dict[str, Any] = {"mode": "pointer", "uri": input.uri, "content_type": input.content_type}
|
|
261
|
+
if input.size_bytes is not None:
|
|
262
|
+
payload["size_bytes"] = input.size_bytes
|
|
263
|
+
if input.content_hash is not None:
|
|
264
|
+
payload["content_hash"] = input.content_hash
|
|
265
|
+
if input.metadata is not None:
|
|
266
|
+
payload["metadata"] = input.metadata
|
|
267
|
+
return payload
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _managed_path(disclose_content_hash: bool) -> str:
|
|
271
|
+
query = {"mode": "managed"}
|
|
272
|
+
if disclose_content_hash:
|
|
273
|
+
query["disclose_content_hash"] = "true"
|
|
274
|
+
return f"/v1/storage/artifacts?{urlencode(query)}"
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _delete_path(artifact_id: str, options: DeleteArtifactOptions | dict[str, Any] | None) -> str:
|
|
278
|
+
path = f"/v1/storage/artifacts/{_quote_id(artifact_id)}"
|
|
279
|
+
policy = _coerce_delete_options(options).policy
|
|
280
|
+
return f"{path}?{urlencode({'policy': policy})}" if policy else path
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _coerce_managed_body(body: ManagedBody) -> bytes:
|
|
284
|
+
if isinstance(body, bytes):
|
|
285
|
+
return body
|
|
286
|
+
if isinstance(body, bytearray):
|
|
287
|
+
return bytes(body)
|
|
288
|
+
if isinstance(body, memoryview):
|
|
289
|
+
return body.tobytes()
|
|
290
|
+
raise StorageClientError(
|
|
291
|
+
"StorageClient.put: only bytes, bytearray, or memoryview are accepted in v1",
|
|
292
|
+
error_code="streaming_body_not_supported",
|
|
293
|
+
status=0,
|
|
294
|
+
body_text="",
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _encode_metadata_header(metadata: ArtifactMetadata) -> str:
|
|
299
|
+
encoded = json.dumps(metadata, separators=(",", ":")).encode()
|
|
300
|
+
return base64.b64encode(encoded).decode()
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _json_response(response: httpx.Response) -> Any:
|
|
304
|
+
try:
|
|
305
|
+
return response.json()
|
|
306
|
+
except json.JSONDecodeError as exc:
|
|
307
|
+
raise StorageClientError(
|
|
308
|
+
"storage API response is not valid JSON",
|
|
309
|
+
error_code="invalid_storage_response",
|
|
310
|
+
status=response.status_code,
|
|
311
|
+
body_text=response.text,
|
|
312
|
+
) from exc
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _quote_id(artifact_id: str) -> str:
|
|
316
|
+
return quote(artifact_id, safe="")
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _validation_error(type_name: str, exc: PydanticValidationError) -> StorageClientError:
|
|
320
|
+
return StorageClientError(
|
|
321
|
+
f"Invalid {type_name}: {exc}",
|
|
322
|
+
error_code="invalid_storage_input",
|
|
323
|
+
status=0,
|
|
324
|
+
body_text="",
|
|
325
|
+
context={"type": type_name, "errors": sanitized_pydantic_errors(exc)},
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _input_error(message: str) -> StorageClientError:
|
|
330
|
+
return StorageClientError(message, error_code="invalid_storage_input", status=0, body_text="")
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _network_error(method: str, path: str, exc: httpx.RequestError) -> StorageClientError:
|
|
334
|
+
return StorageClientError(
|
|
335
|
+
f"Network error while calling {method} {path}: {exc}",
|
|
336
|
+
error_code="network_error",
|
|
337
|
+
status=0,
|
|
338
|
+
body_text="",
|
|
339
|
+
)
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Typed errors for the backend artifact-storage API.
|
|
2
|
+
|
|
3
|
+
These classes mirror `atomicmemory-sdk/src/storage/errors.ts` while
|
|
4
|
+
following the Python SDK error contract: every SDK-raised exception
|
|
5
|
+
inherits from :class:`atomicmemory.core.errors.AtomicMemoryError`.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from atomicmemory.core.errors import AtomicMemoryError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class StorageClientError(AtomicMemoryError):
|
|
16
|
+
"""Base error for ``client.storage.*`` failures."""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
message: str,
|
|
21
|
+
*,
|
|
22
|
+
error_code: str,
|
|
23
|
+
status: int,
|
|
24
|
+
body_text: str,
|
|
25
|
+
context: dict[str, Any] | None = None,
|
|
26
|
+
) -> None:
|
|
27
|
+
merged = {"error_code": error_code, "status": status}
|
|
28
|
+
if context:
|
|
29
|
+
merged.update(context)
|
|
30
|
+
super().__init__(message, context=merged)
|
|
31
|
+
self.error_code = error_code
|
|
32
|
+
self.status = status
|
|
33
|
+
self.body_text = body_text
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class UnsupportedCapabilityError(StorageClientError):
|
|
37
|
+
"""A requested storage capability is not supported by the backend."""
|
|
38
|
+
|
|
39
|
+
def __init__(self, *, capability: str, message: str, body_text: str) -> None:
|
|
40
|
+
super().__init__(
|
|
41
|
+
message,
|
|
42
|
+
error_code="unsupported_capability",
|
|
43
|
+
status=400,
|
|
44
|
+
body_text=body_text,
|
|
45
|
+
context={"capability": capability},
|
|
46
|
+
)
|
|
47
|
+
self.capability = capability
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ArtifactNotFoundError(StorageClientError):
|
|
51
|
+
"""The requested artifact does not exist for the caller."""
|
|
52
|
+
|
|
53
|
+
def __init__(self, *, artifact_id: str, body_text: str) -> None:
|
|
54
|
+
super().__init__(
|
|
55
|
+
f"Storage artifact {artifact_id} not found",
|
|
56
|
+
error_code="artifact_not_found",
|
|
57
|
+
status=404,
|
|
58
|
+
body_text=body_text,
|
|
59
|
+
context={"artifact_id": artifact_id},
|
|
60
|
+
)
|
|
61
|
+
self.artifact_id = artifact_id
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class ArtifactInUseError(StorageClientError):
|
|
65
|
+
"""The artifact is still referenced by one or more documents."""
|
|
66
|
+
|
|
67
|
+
def __init__(
|
|
68
|
+
self,
|
|
69
|
+
*,
|
|
70
|
+
artifact_id: str,
|
|
71
|
+
referenced_by_document_count: int,
|
|
72
|
+
body_text: str,
|
|
73
|
+
) -> None:
|
|
74
|
+
super().__init__(
|
|
75
|
+
"Storage artifact "
|
|
76
|
+
f"{artifact_id} is referenced by {referenced_by_document_count} document(s); "
|
|
77
|
+
"pass policy='with_documents' to cascade",
|
|
78
|
+
error_code="artifact_in_use",
|
|
79
|
+
status=409,
|
|
80
|
+
body_text=body_text,
|
|
81
|
+
context={
|
|
82
|
+
"artifact_id": artifact_id,
|
|
83
|
+
"referenced_by_document_count": referenced_by_document_count,
|
|
84
|
+
},
|
|
85
|
+
)
|
|
86
|
+
self.artifact_id = artifact_id
|
|
87
|
+
self.referenced_by_document_count = referenced_by_document_count
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class PointerContentNotManagedError(StorageClientError):
|
|
91
|
+
"""Raised when ``get_content`` targets a pointer-mode artifact."""
|
|
92
|
+
|
|
93
|
+
def __init__(self, *, artifact_id: str, uri: str, body_text: str) -> None:
|
|
94
|
+
super().__init__(
|
|
95
|
+
f"Artifact {artifact_id} is pointer-mode; fetch the URI directly",
|
|
96
|
+
error_code="pointer_content_not_managed",
|
|
97
|
+
status=409,
|
|
98
|
+
body_text=body_text,
|
|
99
|
+
context={"artifact_id": artifact_id, "uri": uri},
|
|
100
|
+
)
|
|
101
|
+
self.artifact_id = artifact_id
|
|
102
|
+
self.uri = uri
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class FilecoinDirectStorageNotSupportedError(StorageClientError):
|
|
106
|
+
"""Direct managed Filecoin uploads are not supported by this API version."""
|
|
107
|
+
|
|
108
|
+
def __init__(self, *, body_text: str) -> None:
|
|
109
|
+
super().__init__(
|
|
110
|
+
"Direct Filecoin artifact uploads are not supported in this version. "
|
|
111
|
+
"Use document ingestion or pointer mode.",
|
|
112
|
+
error_code="filecoin_direct_storage_not_yet_supported",
|
|
113
|
+
status=501,
|
|
114
|
+
body_text=body_text,
|
|
115
|
+
)
|