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.
Files changed (67) hide show
  1. atomicmemory/__init__.py +166 -0
  2. atomicmemory/_version.py +3 -0
  3. atomicmemory/client/__init__.py +22 -0
  4. atomicmemory/client/async_memory_client.py +202 -0
  5. atomicmemory/client/atomic_memory_client.py +181 -0
  6. atomicmemory/client/memory_client.py +292 -0
  7. atomicmemory/core/__init__.py +34 -0
  8. atomicmemory/core/errors.py +122 -0
  9. atomicmemory/core/events.py +65 -0
  10. atomicmemory/core/logging.py +37 -0
  11. atomicmemory/core/retry.py +124 -0
  12. atomicmemory/core/validation.py +22 -0
  13. atomicmemory/embeddings/__init__.py +16 -0
  14. atomicmemory/embeddings/base.py +39 -0
  15. atomicmemory/embeddings/sentence_transformers.py +104 -0
  16. atomicmemory/kv_cache/__init__.py +17 -0
  17. atomicmemory/kv_cache/adapter.py +50 -0
  18. atomicmemory/kv_cache/memory_storage.py +98 -0
  19. atomicmemory/kv_cache/sqlite_storage.py +122 -0
  20. atomicmemory/memory/__init__.py +82 -0
  21. atomicmemory/memory/filters.py +68 -0
  22. atomicmemory/memory/pipeline.py +42 -0
  23. atomicmemory/memory/provider.py +397 -0
  24. atomicmemory/memory/registry.py +95 -0
  25. atomicmemory/memory/service.py +199 -0
  26. atomicmemory/memory/types.py +398 -0
  27. atomicmemory/providers/__init__.py +5 -0
  28. atomicmemory/providers/atomicmemory/__init__.py +43 -0
  29. atomicmemory/providers/atomicmemory/agents.py +156 -0
  30. atomicmemory/providers/atomicmemory/async_handle_impl.py +198 -0
  31. atomicmemory/providers/atomicmemory/async_provider.py +245 -0
  32. atomicmemory/providers/atomicmemory/audit.py +74 -0
  33. atomicmemory/providers/atomicmemory/config.py +38 -0
  34. atomicmemory/providers/atomicmemory/config_handle.py +123 -0
  35. atomicmemory/providers/atomicmemory/handle.py +513 -0
  36. atomicmemory/providers/atomicmemory/handle_impl.py +325 -0
  37. atomicmemory/providers/atomicmemory/http.py +255 -0
  38. atomicmemory/providers/atomicmemory/lessons.py +133 -0
  39. atomicmemory/providers/atomicmemory/lifecycle.py +202 -0
  40. atomicmemory/providers/atomicmemory/mappers.py +125 -0
  41. atomicmemory/providers/atomicmemory/path.py +20 -0
  42. atomicmemory/providers/atomicmemory/provider.py +300 -0
  43. atomicmemory/providers/atomicmemory/scope_mapper.py +98 -0
  44. atomicmemory/providers/mem0/__init__.py +41 -0
  45. atomicmemory/providers/mem0/async_provider.py +191 -0
  46. atomicmemory/providers/mem0/config.py +51 -0
  47. atomicmemory/providers/mem0/http.py +195 -0
  48. atomicmemory/providers/mem0/mappers.py +145 -0
  49. atomicmemory/providers/mem0/provider.py +202 -0
  50. atomicmemory/py.typed +0 -0
  51. atomicmemory/search/__init__.py +47 -0
  52. atomicmemory/search/chunking.py +161 -0
  53. atomicmemory/search/ranking.py +94 -0
  54. atomicmemory/search/semantic_search.py +130 -0
  55. atomicmemory/search/similarity.py +110 -0
  56. atomicmemory/storage/__init__.py +63 -0
  57. atomicmemory/storage/_mapping.py +305 -0
  58. atomicmemory/storage/async_client.py +208 -0
  59. atomicmemory/storage/client.py +339 -0
  60. atomicmemory/storage/errors.py +115 -0
  61. atomicmemory/storage/types.py +305 -0
  62. atomicmemory/utils/__init__.py +5 -0
  63. atomicmemory/utils/environment.py +23 -0
  64. atomicmemory-1.0.0.dist-info/METADATA +146 -0
  65. atomicmemory-1.0.0.dist-info/RECORD +67 -0
  66. atomicmemory-1.0.0.dist-info/WHEEL +4 -0
  67. atomicmemory-1.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,305 @@
1
+ """Wire mappers for backend artifact-storage responses.
2
+
3
+ Core emits snake_case JSON and storage HEAD headers. This module is the
4
+ single translation seam into Python SDK models, with closed-enum
5
+ validation before any public model is returned.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ from collections.abc import Iterable
12
+ from typing import Any, TypeVar
13
+
14
+ import httpx
15
+ from pydantic import BaseModel
16
+ from pydantic import ValidationError as PydanticValidationError
17
+
18
+ from atomicmemory.storage.errors import (
19
+ ArtifactInUseError,
20
+ ArtifactNotFoundError,
21
+ FilecoinDirectStorageNotSupportedError,
22
+ PointerContentNotManagedError,
23
+ StorageClientError,
24
+ UnsupportedCapabilityError,
25
+ )
26
+ from atomicmemory.storage.types import (
27
+ ArtifactHead,
28
+ DeleteArtifactResult,
29
+ StorageArtifactStatus,
30
+ StoredArtifact,
31
+ VerificationResult,
32
+ )
33
+
34
+ ModelT = TypeVar("ModelT", bound=BaseModel)
35
+
36
+ STORAGE_MODES = ("pointer", "managed")
37
+ STORAGE_STATUSES: tuple[StorageArtifactStatus, ...] = (
38
+ "stored",
39
+ "pending",
40
+ "available",
41
+ "unavailable",
42
+ "deleting",
43
+ "deleted",
44
+ "delete_failed",
45
+ "failed",
46
+ )
47
+ CONTENT_ENCODINGS = ("identity", "aes_gcm")
48
+
49
+
50
+ def map_stored_artifact(raw: Any) -> StoredArtifact:
51
+ """Translate and validate a snake_case artifact response."""
52
+ body = _require_object(raw, "StoredArtifact")
53
+ artifact_id = _require_wire_string(body, "artifact_id")
54
+ provider = _require_wire_string(body, "provider")
55
+ mode = _require_wire_enum(body, "mode", STORAGE_MODES)
56
+ status = _require_wire_enum(body, "status", STORAGE_STATUSES)
57
+ content_encoding = _require_wire_enum(body, "content_encoding", CONTENT_ENCODINGS)
58
+ artifact = {
59
+ "artifactId": artifact_id,
60
+ "provider": provider,
61
+ "mode": mode,
62
+ "uri": _optional_string(body, "uri"),
63
+ "status": status,
64
+ "sizeBytes": _optional_non_negative_int(body, "size_bytes"),
65
+ "contentType": _optional_string(body, "content_type"),
66
+ "contentEncoding": content_encoding,
67
+ "identifiers": _string_dict(body, "identifiers"),
68
+ "lifecycle": _object_or_empty(body, "lifecycle"),
69
+ "metadata": _metadata_dict(body, "metadata"),
70
+ "createdAt": _require_wire_string(body, "created_at"),
71
+ "updatedAt": _require_wire_string(body, "updated_at"),
72
+ }
73
+ content_hash = _optional_string(body, "content_hash")
74
+ if content_hash is not None:
75
+ artifact["contentHash"] = content_hash
76
+ _copy_optional_object(artifact, "providerDetails", body, "provider_details")
77
+ _copy_optional_object(artifact, "replication", body, "replication")
78
+ _copy_optional_object(artifact, "verification", body, "verification")
79
+ _copy_optional_object(artifact, "retrieval", body, "retrieval")
80
+ return validate_response_model(StoredArtifact, artifact, "StoredArtifact")
81
+
82
+
83
+ def map_head_headers(headers: httpx.Headers, fallback_id: str) -> ArtifactHead:
84
+ """Project storage HEAD response headers into an ``ArtifactHead``."""
85
+ mode = _validate_header_enum(headers, "x-atomicmemory-storage-mode", STORAGE_MODES)
86
+ status = _validate_header_enum(headers, "x-atomicmemory-storage-status", STORAGE_STATUSES)
87
+ return validate_response_model(
88
+ ArtifactHead,
89
+ {
90
+ "artifactId": headers.get("x-atomicmemory-artifact-id") or fallback_id,
91
+ "provider": headers.get("x-atomicmemory-provider") or "",
92
+ "mode": mode,
93
+ "status": status,
94
+ "sizeBytes": _parse_size(headers.get("content-length")),
95
+ "contentType": headers.get("content-type"),
96
+ },
97
+ "ArtifactHead",
98
+ )
99
+
100
+
101
+ def map_delete_result(raw: Any) -> DeleteArtifactResult:
102
+ """Translate and validate a snake_case delete response."""
103
+ body = _require_object(raw, "DeleteArtifactResult")
104
+ out: dict[str, Any] = {
105
+ "artifactId": _require_wire_string(body, "artifact_id"),
106
+ "status": _require_wire_enum(body, "status", STORAGE_STATUSES),
107
+ }
108
+ if isinstance(body.get("cascaded_document_ids"), list):
109
+ out["cascadedDocumentIds"] = [str(v) for v in body["cascaded_document_ids"]]
110
+ return validate_response_model(DeleteArtifactResult, out, "DeleteArtifactResult")
111
+
112
+
113
+ def map_verify_result(raw: Any) -> VerificationResult:
114
+ """Map verification response variants to a single Python model."""
115
+ body = raw if isinstance(raw, dict) else {}
116
+ if body.get("kind") == "verified":
117
+ details = body.get("details") if isinstance(body.get("details"), dict) else {}
118
+ return validate_response_model(
119
+ VerificationResult, {"kind": "verified", "details": details}, "VerificationResult"
120
+ )
121
+ if body.get("kind") == "failed":
122
+ failed = {"kind": "failed", "reason": str(body.get("reason", "unknown failure"))}
123
+ return validate_response_model(VerificationResult, failed, "VerificationResult")
124
+ unsupported = {"kind": "unsupported", "reason": str(body.get("reason", "unsupported"))}
125
+ return validate_response_model(VerificationResult, unsupported, "VerificationResult")
126
+
127
+
128
+ def validate_response_model(model: type[ModelT], data: dict[str, Any], type_name: str) -> ModelT:
129
+ """Validate a response model without leaking Pydantic errors."""
130
+ try:
131
+ return model.model_validate(data)
132
+ except PydanticValidationError as exc:
133
+ raise StorageClientError(
134
+ f"{type_name}: server response failed validation",
135
+ error_code="invalid_storage_response",
136
+ status=200,
137
+ body_text="",
138
+ context={"type": type_name, "errors": exc.errors()},
139
+ ) from exc
140
+
141
+
142
+ def raise_for_storage_response(response: httpx.Response, artifact_id: str | None = None) -> None:
143
+ """Raise the typed storage error for a non-success response."""
144
+ body_text = response.text
145
+ envelope = _parse_error_envelope(body_text)
146
+ code = envelope.get("error_code")
147
+ message = str(envelope.get("error") or f"request failed with status {response.status_code}")
148
+ known_id = artifact_id or ""
149
+ if code == "artifact_in_use":
150
+ count = envelope.get("referenced_by_document_count")
151
+ raise ArtifactInUseError(
152
+ artifact_id=known_id,
153
+ referenced_by_document_count=count if isinstance(count, int) else 0,
154
+ body_text=body_text,
155
+ )
156
+ if code == "pointer_content_not_managed":
157
+ uri = envelope.get("uri")
158
+ raise PointerContentNotManagedError(
159
+ artifact_id=known_id,
160
+ uri=uri if isinstance(uri, str) else "",
161
+ body_text=body_text,
162
+ )
163
+ if code == "filecoin_direct_storage_not_yet_supported":
164
+ raise FilecoinDirectStorageNotSupportedError(body_text=body_text)
165
+ if code == "artifact_not_found" or response.status_code == 404:
166
+ raise ArtifactNotFoundError(artifact_id=known_id, body_text=body_text)
167
+ if code == "unsupported_capability":
168
+ raise UnsupportedCapabilityError(capability="unknown", message=message, body_text=body_text)
169
+ raise StorageClientError(
170
+ message,
171
+ error_code=code if isinstance(code, str) else f"http_{response.status_code}",
172
+ status=response.status_code,
173
+ body_text=body_text,
174
+ )
175
+
176
+
177
+ def _require_object(raw: Any, type_name: str) -> dict[str, Any]:
178
+ if not isinstance(raw, dict):
179
+ raise StorageClientError(
180
+ f"{type_name}: server response is not a JSON object",
181
+ error_code="invalid_storage_response",
182
+ status=200,
183
+ body_text="",
184
+ )
185
+ return raw
186
+
187
+
188
+ def _require_wire_string(raw: dict[str, Any], field: str) -> str:
189
+ value = raw.get(field)
190
+ if not isinstance(value, str) or value == "":
191
+ raise StorageClientError(
192
+ f"mapStoredArtifact: server response is missing required `{field}`",
193
+ error_code="invalid_storage_response",
194
+ status=200,
195
+ body_text="",
196
+ )
197
+ return str(value)
198
+
199
+
200
+ def _require_wire_enum(raw: dict[str, Any], field: str, allowed: Iterable[str]) -> str:
201
+ value = raw.get(field)
202
+ allowed_values = tuple(allowed)
203
+ if not isinstance(value, str) or value not in allowed_values:
204
+ raise StorageClientError(
205
+ f"mapStoredArtifact: `{field}` must be one of {', '.join(allowed_values)}",
206
+ error_code="invalid_storage_response",
207
+ status=200,
208
+ body_text="",
209
+ )
210
+ return value
211
+
212
+
213
+ def _validate_header_enum(headers: httpx.Headers, name: str, allowed: Iterable[str]) -> str:
214
+ value = headers.get(name)
215
+ allowed_values = tuple(allowed)
216
+ if not isinstance(value, str) or value not in allowed_values:
217
+ raise StorageClientError(
218
+ f"head(): server returned an unrecognized {name} value",
219
+ error_code="invalid_head_response",
220
+ status=200,
221
+ body_text="",
222
+ )
223
+ return value
224
+
225
+
226
+ def _parse_size(value: str | None) -> int | None:
227
+ if value is None:
228
+ return None
229
+ try:
230
+ parsed = int(value)
231
+ except ValueError:
232
+ return None
233
+ return parsed if parsed >= 0 else None
234
+
235
+
236
+ def _optional_string(body: dict[str, Any], field: str) -> str | None:
237
+ if field not in body or body[field] is None:
238
+ return None
239
+ value = body[field]
240
+ if isinstance(value, str):
241
+ return value
242
+ raise _invalid_response(f"mapStoredArtifact: `{field}` must be a string or null")
243
+
244
+
245
+ def _optional_non_negative_int(body: dict[str, Any], field: str) -> int | None:
246
+ if field not in body or body[field] is None:
247
+ return None
248
+ value = body[field]
249
+ if isinstance(value, int) and not isinstance(value, bool) and value >= 0:
250
+ return value
251
+ raise _invalid_response(f"mapStoredArtifact: `{field}` must be a non-negative integer or null")
252
+
253
+
254
+ def _string_dict(body: dict[str, Any], field: str) -> dict[str, str]:
255
+ if field not in body or body[field] is None:
256
+ return {}
257
+ value = body[field]
258
+ if not isinstance(value, dict):
259
+ raise _invalid_response(f"mapStoredArtifact: `{field}` must be an object")
260
+ if not all(isinstance(v, str) for v in value.values()):
261
+ raise _invalid_response(f"mapStoredArtifact: `{field}` values must be strings")
262
+ return {str(k): v for k, v in value.items()}
263
+
264
+
265
+ def _object_or_empty(body: dict[str, Any], field: str) -> dict[str, Any]:
266
+ if field not in body or body[field] is None:
267
+ return {}
268
+ value = body[field]
269
+ if not isinstance(value, dict):
270
+ raise _invalid_response(f"mapStoredArtifact: `{field}` must be an object")
271
+ return value
272
+
273
+
274
+ def _metadata_dict(body: dict[str, Any], field: str) -> dict[str, str | int | float | bool]:
275
+ if field not in body or body[field] is None:
276
+ return {}
277
+ value = body[field]
278
+ if not isinstance(value, dict):
279
+ raise _invalid_response(f"mapStoredArtifact: `{field}` must be an object")
280
+ if not all(isinstance(v, str | int | float | bool) for v in value.values()):
281
+ raise _invalid_response(f"mapStoredArtifact: `{field}` values must be scalar")
282
+ return {str(k): v for k, v in value.items() if isinstance(v, str | int | float | bool)}
283
+
284
+
285
+ def _copy_optional_object(out: dict[str, Any], out_key: str, body: dict[str, Any], wire_key: str) -> None:
286
+ if wire_key not in body or body[wire_key] is None:
287
+ return
288
+ value = body[wire_key]
289
+ if not isinstance(value, dict):
290
+ raise _invalid_response(f"mapStoredArtifact: `{wire_key}` must be an object")
291
+ out[out_key] = value
292
+
293
+
294
+ def _invalid_response(message: str) -> StorageClientError:
295
+ return StorageClientError(message, error_code="invalid_storage_response", status=200, body_text="")
296
+
297
+
298
+ def _parse_error_envelope(body_text: str) -> dict[str, Any]:
299
+ if body_text == "":
300
+ return {}
301
+ try:
302
+ parsed = json.loads(body_text)
303
+ except json.JSONDecodeError:
304
+ return {}
305
+ return parsed if isinstance(parsed, dict) else {}
@@ -0,0 +1,208 @@
1
+ """Async client for backend artifact storage.
2
+
3
+ The async surface mirrors :mod:`atomicmemory.storage.client` while
4
+ using ``httpx.AsyncClient`` for every storage API request.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ from collections.abc import AsyncIterator
11
+ from contextlib import asynccontextmanager
12
+ from types import TracebackType
13
+ from typing import Any
14
+
15
+ import httpx
16
+
17
+ from atomicmemory.storage._mapping import (
18
+ map_delete_result,
19
+ map_head_headers,
20
+ map_stored_artifact,
21
+ map_verify_result,
22
+ raise_for_storage_response,
23
+ validate_response_model,
24
+ )
25
+ from atomicmemory.storage.client import (
26
+ METADATA_HEADER,
27
+ _coerce_config,
28
+ _coerce_delete_options,
29
+ _coerce_managed_body,
30
+ _coerce_put_input,
31
+ _coerce_verify_options,
32
+ _encode_metadata_header,
33
+ _json_response,
34
+ _managed_path,
35
+ _network_error,
36
+ _pointer_payload,
37
+ _quote_id,
38
+ _require_artifact_id,
39
+ )
40
+ from atomicmemory.storage.types import (
41
+ ArtifactHead,
42
+ ArtifactRef,
43
+ DeleteArtifactOptions,
44
+ DeleteArtifactResult,
45
+ PutArtifactInput,
46
+ PutManagedInput,
47
+ PutPointerInput,
48
+ StorageCapabilities,
49
+ StorageClientConfig,
50
+ StoredArtifact,
51
+ VerificationResult,
52
+ VerifyArtifactOptions,
53
+ )
54
+
55
+
56
+ class AsyncStorageClient:
57
+ """Async entry point for the direct artifact-storage API."""
58
+
59
+ def __init__(self, config: StorageClientConfig | dict[str, Any]) -> None:
60
+ self._config = _coerce_config(config)
61
+ self._api_url = self._config.api_url.rstrip("/")
62
+ self._client = httpx.AsyncClient(timeout=self._config.timeout_seconds)
63
+
64
+ async def capabilities(self) -> StorageCapabilities:
65
+ response = await self._request("GET", "/v1/storage/capabilities")
66
+ return validate_response_model(StorageCapabilities, _json_response(response), "StorageCapabilities")
67
+
68
+ async def put(self, input: PutArtifactInput | dict[str, Any]) -> StoredArtifact:
69
+ value = _coerce_put_input(input)
70
+ if isinstance(value, PutPointerInput):
71
+ return await self._put_pointer(value)
72
+ return await self._put_managed(value)
73
+
74
+ async def get(self, ref: ArtifactRef | dict[str, Any]) -> StoredArtifact:
75
+ artifact_id = _require_artifact_id(ref)
76
+ response = await self._request(
77
+ "GET",
78
+ f"/v1/storage/artifacts/{_quote_id(artifact_id)}",
79
+ artifact_id=artifact_id,
80
+ )
81
+ return map_stored_artifact(_json_response(response))
82
+
83
+ async def get_content(self, ref: ArtifactRef | dict[str, Any]) -> httpx.Response:
84
+ """Return a fully buffered content response for small artifacts."""
85
+ artifact_id = _require_artifact_id(ref)
86
+ path = f"/v1/storage/artifacts/{_quote_id(artifact_id)}/content"
87
+ return await self._request("GET", path, artifact_id=artifact_id)
88
+
89
+ @asynccontextmanager
90
+ async def stream_content(self, ref: ArtifactRef | dict[str, Any]) -> AsyncIterator[httpx.Response]:
91
+ """Stream artifact bytes without loading the whole response into memory."""
92
+ artifact_id = _require_artifact_id(ref)
93
+ path = f"/v1/storage/artifacts/{_quote_id(artifact_id)}/content"
94
+ try:
95
+ async with self._client.stream("GET", f"{self._api_url}{path}", headers=self._headers(None)) as response:
96
+ if response.is_success:
97
+ yield response
98
+ return
99
+ await response.aread()
100
+ raise_for_storage_response(response, artifact_id)
101
+ except httpx.RequestError as exc:
102
+ raise _network_error("GET", path, exc) from exc
103
+ raise AssertionError("unreachable")
104
+
105
+ async def head(self, ref: ArtifactRef | dict[str, Any]) -> ArtifactHead:
106
+ artifact_id = _require_artifact_id(ref)
107
+ response = await self._request(
108
+ "HEAD",
109
+ f"/v1/storage/artifacts/{_quote_id(artifact_id)}",
110
+ artifact_id=artifact_id,
111
+ )
112
+ return map_head_headers(response.headers, artifact_id)
113
+
114
+ async def delete(
115
+ self,
116
+ ref: ArtifactRef | dict[str, Any],
117
+ options: DeleteArtifactOptions | dict[str, Any] | None = None,
118
+ ) -> DeleteArtifactResult:
119
+ artifact_id = _require_artifact_id(ref)
120
+ path = f"/v1/storage/artifacts/{_quote_id(artifact_id)}"
121
+ policy = _coerce_delete_options(options).policy
122
+ if policy:
123
+ path = f"{path}?policy={policy}"
124
+ response = await self._request("DELETE", path, artifact_id=artifact_id)
125
+ return map_delete_result(_json_response(response))
126
+
127
+ async def verify(
128
+ self,
129
+ ref: ArtifactRef | dict[str, Any],
130
+ options: VerifyArtifactOptions | dict[str, Any] | None = None,
131
+ ) -> VerificationResult:
132
+ _coerce_verify_options(options)
133
+ artifact_id = _require_artifact_id(ref)
134
+ path = f"/v1/storage/artifacts/{_quote_id(artifact_id)}/verify"
135
+ response = await self._request("POST", path, artifact_id=artifact_id)
136
+ return map_verify_result(_json_response(response))
137
+
138
+ async def close(self) -> None:
139
+ await self._client.aclose()
140
+
141
+ async def __aenter__(self) -> AsyncStorageClient:
142
+ return self
143
+
144
+ async def __aexit__(
145
+ self,
146
+ exc_type: type[BaseException] | None,
147
+ exc: BaseException | None,
148
+ tb: TracebackType | None,
149
+ ) -> None:
150
+ await self.close()
151
+
152
+ async def _put_pointer(self, input: PutPointerInput) -> StoredArtifact:
153
+ response = await self._request(
154
+ "POST",
155
+ "/v1/storage/artifacts",
156
+ headers={"Content-Type": "application/json"},
157
+ content=_json_bytes(_pointer_payload(input)),
158
+ )
159
+ return map_stored_artifact(_json_response(response))
160
+
161
+ async def _put_managed(self, input: PutManagedInput) -> StoredArtifact:
162
+ body = _coerce_managed_body(input.body)
163
+ headers = {"Content-Type": input.content_type, "Content-Length": str(len(body))}
164
+ if input.metadata is not None:
165
+ headers[METADATA_HEADER] = _encode_metadata_header(input.metadata)
166
+ response = await self._request(
167
+ "POST",
168
+ _managed_path(input.disclose_content_hash),
169
+ headers=headers,
170
+ content=body,
171
+ )
172
+ return map_stored_artifact(_json_response(response))
173
+
174
+ async def _request(
175
+ self,
176
+ method: str,
177
+ path: str,
178
+ *,
179
+ headers: dict[str, str] | None = None,
180
+ content: bytes | None = None,
181
+ artifact_id: str | None = None,
182
+ ) -> httpx.Response:
183
+ try:
184
+ response = await self._client.request(
185
+ method,
186
+ f"{self._api_url}{path}",
187
+ headers=self._headers(headers),
188
+ content=content,
189
+ )
190
+ except httpx.RequestError as exc:
191
+ raise _network_error(method, path, exc) from exc
192
+ if response.is_success:
193
+ return response
194
+ raise_for_storage_response(response, artifact_id)
195
+ raise AssertionError("unreachable")
196
+
197
+ def _headers(self, extra: dict[str, str] | None) -> dict[str, str]:
198
+ headers = {
199
+ "Authorization": f"Bearer {self._config.api_key}",
200
+ "X-AtomicMemory-User-Id": self._config.user_id,
201
+ }
202
+ if extra:
203
+ headers.update(extra)
204
+ return headers
205
+
206
+
207
+ def _json_bytes(payload: dict[str, Any]) -> bytes:
208
+ return json.dumps(payload, separators=(",", ":")).encode()