loop-memory 0.4.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.
- loop_memory/__init__.py +62 -0
- loop_memory/backends/__init__.py +13 -0
- loop_memory/backends/embedding.py +82 -0
- loop_memory/backends/sentence_embedder.py +30 -0
- loop_memory/backends/vector_store.py +139 -0
- loop_memory/cli/__init__.py +0 -0
- loop_memory/cli/_common.py +68 -0
- loop_memory/cli/commands/__init__.py +13 -0
- loop_memory/cli/commands/cognitive.py +205 -0
- loop_memory/cli/commands/diag.py +346 -0
- loop_memory/cli/commands/graph.py +21 -0
- loop_memory/cli/commands/hooks.py +212 -0
- loop_memory/cli/commands/read.py +362 -0
- loop_memory/cli/commands/serve.py +147 -0
- loop_memory/cli/commands/write.py +138 -0
- loop_memory/cli/main.py +115 -0
- loop_memory/engine/__init__.py +0 -0
- loop_memory/engine/loop.py +247 -0
- loop_memory/engine/reflect.py +89 -0
- loop_memory/examples/__init__.py +0 -0
- loop_memory/examples/demo.py +39 -0
- loop_memory/export/__init__.py +39 -0
- loop_memory/export/memory_md.py +629 -0
- loop_memory/graph/__init__.py +0 -0
- loop_memory/graph/build.py +259 -0
- loop_memory/graph/extract.py +197 -0
- loop_memory/ingest/__init__.py +0 -0
- loop_memory/ingest/loader.py +782 -0
- loop_memory/ingest/pipeline.py +458 -0
- loop_memory/jobs/__init__.py +0 -0
- loop_memory/jobs/cognitive.py +353 -0
- loop_memory/jobs/compact.py +371 -0
- loop_memory/jobs/consolidate.py +95 -0
- loop_memory/jobs/contradiction.py +281 -0
- loop_memory/jobs/evolution.py +2021 -0
- loop_memory/jobs/graph.py +395 -0
- loop_memory/jobs/llm_compact_pass.py +24 -0
- loop_memory/jobs/llm_consolidate.py +980 -0
- loop_memory/jobs/scheduler.py +495 -0
- loop_memory/llm/__init__.py +0 -0
- loop_memory/llm/base.py +80 -0
- loop_memory/llm/openai_adapter.py +31 -0
- loop_memory/llm/providers.py +517 -0
- loop_memory/mcp/__init__.py +804 -0
- loop_memory/memory/__init__.py +0 -0
- loop_memory/memory/types.py +199 -0
- loop_memory/privacy/__init__.py +22 -0
- loop_memory/privacy/private.py +46 -0
- loop_memory/privacy/redact.py +188 -0
- loop_memory/py.typed +0 -0
- loop_memory/sdk.py +875 -0
- loop_memory/sdk_extensions.py +384 -0
- loop_memory/security/__init__.py +20 -0
- loop_memory/security/secrets.py +464 -0
- loop_memory/serve/__init__.py +0 -0
- loop_memory/serve/app.py +506 -0
- loop_memory/serve/handlers.py +316 -0
- loop_memory/serve/routes/_shared.py +59 -0
- loop_memory/serve/routes/admin.py +970 -0
- loop_memory/serve/routes/cognitive.py +64 -0
- loop_memory/serve/routes/export.py +65 -0
- loop_memory/serve/routes/graph.py +101 -0
- loop_memory/serve/routes/insights.py +702 -0
- loop_memory/serve/routes/memories.py +435 -0
- loop_memory/serve/routes/sessions.py +75 -0
- loop_memory/serve/routes/system.py +493 -0
- loop_memory/serve/routes/wiki.py +812 -0
- loop_memory/serve/static/__init__.py +0 -0
- loop_memory/serve/static/index.html +15 -0
- loop_memory/serve/watcher.py +451 -0
- loop_memory/storage/__init__.py +5 -0
- loop_memory/storage/retrieval.py +365 -0
- loop_memory/storage/sqlite_store.py +3627 -0
- loop_memory/wiki/__init__.py +41 -0
- loop_memory/wiki/backfill.py +143 -0
- loop_memory/wiki/classifier.py +238 -0
- loop_memory/wiki/prompts.py +295 -0
- loop_memory/wiki/scope.py +227 -0
- loop_memory-0.4.0.dist-info/METADATA +627 -0
- loop_memory-0.4.0.dist-info/RECORD +84 -0
- loop_memory-0.4.0.dist-info/WHEEL +5 -0
- loop_memory-0.4.0.dist-info/entry_points.txt +2 -0
- loop_memory-0.4.0.dist-info/licenses/LICENSE +21 -0
- loop_memory-0.4.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
"""Extra surface on top of ``loop_memory.sdk.MemoryClient``.
|
|
2
|
+
|
|
3
|
+
The base ``MemoryClient`` covers the 4-verb contract (remember /
|
|
4
|
+
recall / feedback / forget). The Universal Agent Memory
|
|
5
|
+
contract adds four more first-class operations an Agent needs:
|
|
6
|
+
|
|
7
|
+
* **namespace** — `for_user(...)` / `for_agent(...)` sugar that
|
|
8
|
+
returns a proxy whose every call auto-stamps the triple, so
|
|
9
|
+
multi-tenant code reads like English instead of plumbing.
|
|
10
|
+
|
|
11
|
+
* **graph** — push high-signal relations (Mem0's differentiator),
|
|
12
|
+
query the subgraph for a free-text query, and rebuild the
|
|
13
|
+
entity-mention index.
|
|
14
|
+
|
|
15
|
+
* **cognitive** — run a single ``cognitive_sleep`` sweep, dry-run
|
|
16
|
+
by default, and read the audit trail.
|
|
17
|
+
|
|
18
|
+
* **export / import / fork** — write a white-box ``MEMORY.md``
|
|
19
|
+
bundle, re-hydrate it, or snapshot a branch of the wiki.
|
|
20
|
+
|
|
21
|
+
These are mixed in via the ``MemoryClientExt`` class — the SDK
|
|
22
|
+
exposes a single ``MemoryClient`` symbol that inherits from both
|
|
23
|
+
``MemoryClient`` and ``MemoryClientExt`` so the call sites stay
|
|
24
|
+
unchanged.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import json
|
|
30
|
+
import os
|
|
31
|
+
import urllib.error
|
|
32
|
+
import urllib.parse
|
|
33
|
+
import urllib.request
|
|
34
|
+
from dataclasses import dataclass, field
|
|
35
|
+
from typing import Any, Iterable
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# Re-export the symbols the caller will need.
|
|
39
|
+
__all__ = [
|
|
40
|
+
"MemoryClientExt",
|
|
41
|
+
"MemoryNamespace",
|
|
42
|
+
"GraphHit",
|
|
43
|
+
"SubgraphView",
|
|
44
|
+
"CognitiveActionView",
|
|
45
|
+
"CognitiveReportView",
|
|
46
|
+
"ExportView",
|
|
47
|
+
"ImportView",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
# Dataclasses
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class GraphHit:
|
|
58
|
+
kind: str
|
|
59
|
+
src: str
|
|
60
|
+
dst: str
|
|
61
|
+
weight: float
|
|
62
|
+
evidence_id: str | None = None
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def from_dict(cls, d: dict[str, Any]) -> GraphHit:
|
|
66
|
+
return cls(
|
|
67
|
+
kind=d.get("kind", "co_occurs_with"),
|
|
68
|
+
src=d.get("src") or d.get("name") or "",
|
|
69
|
+
dst=d.get("dst") or "",
|
|
70
|
+
weight=float(d.get("weight", 0) or 0),
|
|
71
|
+
evidence_id=d.get("evidence_id"),
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass
|
|
76
|
+
class SubgraphView:
|
|
77
|
+
nodes: list[dict[str, Any]] = field(default_factory=list)
|
|
78
|
+
edges: list[GraphHit] = field(default_factory=list)
|
|
79
|
+
memory_ids: list[str] = field(default_factory=list)
|
|
80
|
+
node_count: int = 0
|
|
81
|
+
edge_count: int = 0
|
|
82
|
+
|
|
83
|
+
@classmethod
|
|
84
|
+
def from_dict(cls, d: dict[str, Any]) -> SubgraphView:
|
|
85
|
+
edges = [GraphHit.from_dict(e) for e in d.get("edges", [])]
|
|
86
|
+
return cls(
|
|
87
|
+
nodes=list(d.get("nodes", [])),
|
|
88
|
+
edges=edges,
|
|
89
|
+
memory_ids=list(d.get("memory_ids", [])),
|
|
90
|
+
node_count=int(d.get("node_count", len(d.get("nodes", [])))),
|
|
91
|
+
edge_count=int(d.get("edge_count", len(edges))),
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass
|
|
96
|
+
class CognitiveActionView:
|
|
97
|
+
kind: str
|
|
98
|
+
target_kind: str
|
|
99
|
+
target_id: str
|
|
100
|
+
target_text: str
|
|
101
|
+
reason: str
|
|
102
|
+
score: float
|
|
103
|
+
action: str
|
|
104
|
+
payload: dict = field(default_factory=dict)
|
|
105
|
+
|
|
106
|
+
@classmethod
|
|
107
|
+
def from_dict(cls, d: dict[str, Any]) -> CognitiveActionView:
|
|
108
|
+
return cls(
|
|
109
|
+
kind=d.get("kind", ""),
|
|
110
|
+
target_kind=d.get("target_kind", ""),
|
|
111
|
+
target_id=d.get("target_id") or "",
|
|
112
|
+
target_text=d.get("target_text") or "",
|
|
113
|
+
reason=d.get("reason") or "",
|
|
114
|
+
score=float(d.get("score", 0) or 0),
|
|
115
|
+
action=d.get("action", "suggest"),
|
|
116
|
+
payload=dict(d.get("payload") or {}),
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@dataclass
|
|
121
|
+
class CognitiveReportView:
|
|
122
|
+
actions: list[CognitiveActionView] = field(default_factory=list)
|
|
123
|
+
counts: dict[str, int] = field(default_factory=dict)
|
|
124
|
+
applied: bool = False
|
|
125
|
+
elapsed_ms: float = 0.0
|
|
126
|
+
total: int = 0
|
|
127
|
+
|
|
128
|
+
@classmethod
|
|
129
|
+
def from_dict(cls, d: dict[str, Any]) -> CognitiveReportView:
|
|
130
|
+
return cls(
|
|
131
|
+
actions=[CognitiveActionView.from_dict(a) for a in d.get("actions", [])],
|
|
132
|
+
counts=dict(d.get("counts", {})),
|
|
133
|
+
applied=bool(d.get("applied", False)),
|
|
134
|
+
elapsed_ms=float(d.get("elapsed_ms", 0) or 0),
|
|
135
|
+
total=int(d.get("total", 0)),
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@dataclass
|
|
140
|
+
class ExportView:
|
|
141
|
+
out_dir: str
|
|
142
|
+
memory_md_path: str
|
|
143
|
+
pages: list[str] = field(default_factory=list)
|
|
144
|
+
memories: int = 0
|
|
145
|
+
graph_entities: int = 0
|
|
146
|
+
graph_relations: int = 0
|
|
147
|
+
sessions: int = 0
|
|
148
|
+
elapsed_ms: float = 0.0
|
|
149
|
+
|
|
150
|
+
@classmethod
|
|
151
|
+
def from_dict(cls, d: dict[str, Any]) -> ExportView:
|
|
152
|
+
return cls(
|
|
153
|
+
out_dir=d.get("out_dir", ""),
|
|
154
|
+
memory_md_path=d.get("memory_md_path", ""),
|
|
155
|
+
pages=list(d.get("pages", [])),
|
|
156
|
+
memories=int(d.get("memories", 0) or 0),
|
|
157
|
+
graph_entities=int(d.get("graph_entities", 0) or 0),
|
|
158
|
+
graph_relations=int(d.get("graph_relations", 0) or 0),
|
|
159
|
+
sessions=int(d.get("sessions", 0) or 0),
|
|
160
|
+
elapsed_ms=float(d.get("elapsed_ms", 0) or 0),
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@dataclass
|
|
165
|
+
class ImportView:
|
|
166
|
+
pages_upserted: int = 0
|
|
167
|
+
memories_upserted: int = 0
|
|
168
|
+
entities_upserted: int = 0
|
|
169
|
+
relations_upserted: int = 0
|
|
170
|
+
sessions_upserted: int = 0
|
|
171
|
+
elapsed_ms: float = 0.0
|
|
172
|
+
bundle_path: str = ""
|
|
173
|
+
|
|
174
|
+
@classmethod
|
|
175
|
+
def from_dict(cls, d: dict[str, Any]) -> ImportView:
|
|
176
|
+
return cls(**{k: d.get(k, getattr(cls(), k)) for k in cls.__dataclass_fields__})
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# ---------------------------------------------------------------------------
|
|
180
|
+
# Namespace proxy
|
|
181
|
+
# ---------------------------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class MemoryNamespace:
|
|
185
|
+
"""A thin proxy returned by ``MemoryClient.for_user(...)`` /
|
|
186
|
+
``for_agent(...)`` that auto-stamps the namespace triple on
|
|
187
|
+
every call.
|
|
188
|
+
|
|
189
|
+
Read ``client.remember(...)`` once and then ``ns.remember(...)``
|
|
190
|
+
in every call site — no need to repeat ``user_id=`` and
|
|
191
|
+
``external_id=`` plumbing. The proxy is itself a context
|
|
192
|
+
manager so ``with client.for_user("alice") as alice_ns:`` is
|
|
193
|
+
a documented pattern even though it doesn't own any
|
|
194
|
+
resources.
|
|
195
|
+
"""
|
|
196
|
+
|
|
197
|
+
def __init__(self, parent, *, agent_id: str | None = None,
|
|
198
|
+
user_id: str | None = None) -> None:
|
|
199
|
+
self._parent = parent
|
|
200
|
+
self._agent_id = agent_id
|
|
201
|
+
self._user_id = user_id
|
|
202
|
+
|
|
203
|
+
@property
|
|
204
|
+
def agent_id(self) -> str | None:
|
|
205
|
+
return self._agent_id
|
|
206
|
+
|
|
207
|
+
@property
|
|
208
|
+
def user_id(self) -> str | None:
|
|
209
|
+
return self._user_id
|
|
210
|
+
|
|
211
|
+
def __enter__(self) -> MemoryNamespace:
|
|
212
|
+
return self
|
|
213
|
+
|
|
214
|
+
def __exit__(self, *exc) -> None:
|
|
215
|
+
return None
|
|
216
|
+
|
|
217
|
+
# ----- 4-verb contract -----------------------------------------
|
|
218
|
+
|
|
219
|
+
def remember(self, text: str, **kwargs: Any) -> Any:
|
|
220
|
+
kwargs.setdefault("agent_id", self._agent_id)
|
|
221
|
+
kwargs.setdefault("user_id", self._user_id)
|
|
222
|
+
return self._parent.remember(text, **kwargs)
|
|
223
|
+
|
|
224
|
+
def recall(self, query: str, **kwargs: Any) -> Any:
|
|
225
|
+
kwargs.setdefault("agent_id", self._agent_id)
|
|
226
|
+
kwargs.setdefault("user_id", self._user_id)
|
|
227
|
+
return self._parent.recall(query, **kwargs)
|
|
228
|
+
|
|
229
|
+
def feedback(self, **kwargs: Any) -> bool:
|
|
230
|
+
kwargs.setdefault("agent_id", self._agent_id)
|
|
231
|
+
kwargs.setdefault("user_id", self._user_id)
|
|
232
|
+
return self._parent.feedback(**kwargs)
|
|
233
|
+
|
|
234
|
+
def forget(self, **kwargs: Any) -> int:
|
|
235
|
+
kwargs.setdefault("agent_id", self._agent_id)
|
|
236
|
+
kwargs.setdefault("user_id", self._user_id)
|
|
237
|
+
return self._parent.forget(**kwargs)
|
|
238
|
+
|
|
239
|
+
def list(self, **kwargs: Any) -> list:
|
|
240
|
+
kwargs.setdefault("agent_id", self._agent_id)
|
|
241
|
+
kwargs.setdefault("user_id", self._user_id)
|
|
242
|
+
return self._parent.list(**kwargs)
|
|
243
|
+
|
|
244
|
+
# ----- extra ops ----------------------------------------------
|
|
245
|
+
|
|
246
|
+
def subgraph(self, query: str, **kwargs: Any) -> SubgraphView:
|
|
247
|
+
return self._parent.subgraph(query, **kwargs)
|
|
248
|
+
|
|
249
|
+
def remember_edge(self, src: str, dst: str, *, kind: str = "relates_to",
|
|
250
|
+
weight: float = 0.5) -> GraphHit:
|
|
251
|
+
return self._parent.remember_edge(src, dst, kind=kind, weight=weight)
|
|
252
|
+
|
|
253
|
+
def cognitive_sleep(self, **kwargs: Any) -> CognitiveReportView:
|
|
254
|
+
return self._parent.cognitive_sleep(**kwargs)
|
|
255
|
+
|
|
256
|
+
def audit(self, **kwargs: Any) -> list[CognitiveActionView]:
|
|
257
|
+
return self._parent.audit(**kwargs)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
# ---------------------------------------------------------------------------
|
|
261
|
+
# Mixin
|
|
262
|
+
# ---------------------------------------------------------------------------
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
class MemoryClientExt:
|
|
266
|
+
"""Mixin that adds namespace / graph / cognitive / export to any
|
|
267
|
+
MemoryClient. The base class is a stub — concrete behaviour
|
|
268
|
+
lives in the in-process and HTTP backends.
|
|
269
|
+
"""
|
|
270
|
+
|
|
271
|
+
# ---- namespace sugar -------------------------------------------
|
|
272
|
+
|
|
273
|
+
def for_user(self, user_id: str, *, agent_id: str | None = None) -> MemoryNamespace:
|
|
274
|
+
a = agent_id if agent_id is not None else getattr(self, "_default_agent_id", None)
|
|
275
|
+
return MemoryNamespace(self, agent_id=a, user_id=user_id)
|
|
276
|
+
|
|
277
|
+
def for_agent(self, agent_id: str, *, user_id: str | None = None) -> MemoryNamespace:
|
|
278
|
+
u = user_id if user_id is not None else getattr(self, "_default_user_id", None)
|
|
279
|
+
return MemoryNamespace(self, agent_id=agent_id, user_id=u)
|
|
280
|
+
|
|
281
|
+
# ---- graph ----------------------------------------------------
|
|
282
|
+
|
|
283
|
+
def remember_edge(self, src: str, dst: str, *,
|
|
284
|
+
kind: str = "relates_to", weight: float = 0.5,
|
|
285
|
+
evidence_id: str | None = None) -> GraphHit:
|
|
286
|
+
raise NotImplementedError
|
|
287
|
+
|
|
288
|
+
def subgraph(self, query: str, *, max_nodes: int = 32,
|
|
289
|
+
max_edges: int = 64) -> SubgraphView:
|
|
290
|
+
raise NotImplementedError
|
|
291
|
+
|
|
292
|
+
def rebuild_graph(self) -> int:
|
|
293
|
+
"""Re-extract entities from every memory and rebuild the
|
|
294
|
+
``entity_mentions`` table. Returns the number of mentions
|
|
295
|
+
written."""
|
|
296
|
+
raise NotImplementedError
|
|
297
|
+
|
|
298
|
+
def recall_adaptive(self, query: str, *, limit: int = 8, **kwargs) -> Any:
|
|
299
|
+
"""Recall with the 3D AdaptiveScore + graph boost enabled.
|
|
300
|
+
|
|
301
|
+
Falls back to plain ``recall`` on stores that don't support
|
|
302
|
+
adaptive scoring yet, so the call site is forward-compatible.
|
|
303
|
+
"""
|
|
304
|
+
raise NotImplementedError
|
|
305
|
+
|
|
306
|
+
# ---- cognitive ------------------------------------------------
|
|
307
|
+
|
|
308
|
+
def cognitive_sleep(self, *, apply: bool = False, **kwargs) -> CognitiveReportView:
|
|
309
|
+
raise NotImplementedError
|
|
310
|
+
|
|
311
|
+
def audit(self, *, kind: str | None = None, action: str | None = None,
|
|
312
|
+
limit: int = 200) -> list[CognitiveActionView]:
|
|
313
|
+
raise NotImplementedError
|
|
314
|
+
|
|
315
|
+
def revert_audit(self, audit_id: str) -> bool:
|
|
316
|
+
"""Mark an audit row as ``reverted`` (does not actually
|
|
317
|
+
re-insert the deleted memory — it just records the
|
|
318
|
+
decision so a future re-import of a bundle can restore
|
|
319
|
+
it)."""
|
|
320
|
+
raise NotImplementedError
|
|
321
|
+
|
|
322
|
+
# ---- export / import / fork -----------------------------------
|
|
323
|
+
|
|
324
|
+
def export(self, out_dir: str, *, agent_id: str | None = None,
|
|
325
|
+
user_id: str | None = None, scope: str = "global",
|
|
326
|
+
min_importance: float = 0.0) -> ExportView:
|
|
327
|
+
raise NotImplementedError
|
|
328
|
+
|
|
329
|
+
def import_bundle(self, in_dir: str, *, agent_id: str | None = None,
|
|
330
|
+
user_id: str | None = None, dry_run: bool = False) -> ImportView:
|
|
331
|
+
raise NotImplementedError
|
|
332
|
+
|
|
333
|
+
def fork(self, branch_tag: str | None = None) -> dict[str, Any]:
|
|
334
|
+
raise NotImplementedError
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
# ---------------------------------------------------------------------------
|
|
338
|
+
# Helpers shared by both backends
|
|
339
|
+
# ---------------------------------------------------------------------------
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def http_post_json(url: str, body: dict, *, timeout: float = 10.0) -> dict:
|
|
343
|
+
data = json.dumps(body).encode("utf-8")
|
|
344
|
+
req = urllib.request.Request(
|
|
345
|
+
url, data=data, method="POST",
|
|
346
|
+
headers={"Content-Type": "application/json", "Accept": "application/json"},
|
|
347
|
+
)
|
|
348
|
+
try:
|
|
349
|
+
with urllib.request.urlopen(req, timeout=timeout) as r:
|
|
350
|
+
return json.loads(r.read().decode("utf-8") or "{}")
|
|
351
|
+
except urllib.error.HTTPError as e:
|
|
352
|
+
raw = e.read().decode("utf-8", errors="ignore")
|
|
353
|
+
raise MemoryClientError(f"POST {url} → {e.code}: {raw[:300]}") from e
|
|
354
|
+
except urllib.error.URLError as e:
|
|
355
|
+
raise MemoryClientError(f"cannot reach {url}: {e}") from e
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def http_get_json(url: str, *, timeout: float = 10.0) -> dict:
|
|
359
|
+
req = urllib.request.Request(url, headers={"Accept": "application/json"})
|
|
360
|
+
try:
|
|
361
|
+
with urllib.request.urlopen(req, timeout=timeout) as r:
|
|
362
|
+
return json.loads(r.read().decode("utf-8") or "{}")
|
|
363
|
+
except urllib.error.HTTPError as e:
|
|
364
|
+
raw = e.read().decode("utf-8", errors="ignore")
|
|
365
|
+
raise MemoryClientError(f"GET {url} → {e.code}: {raw[:300]}") from e
|
|
366
|
+
except urllib.error.URLError as e:
|
|
367
|
+
raise MemoryClientError(f"cannot reach {url}: {e}") from e
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def http_delete_json(url: str, *, timeout: float = 10.0) -> dict:
|
|
371
|
+
req = urllib.request.Request(url, method="DELETE",
|
|
372
|
+
headers={"Accept": "application/json"})
|
|
373
|
+
try:
|
|
374
|
+
with urllib.request.urlopen(req, timeout=timeout) as r:
|
|
375
|
+
return json.loads(r.read().decode("utf-8") or "{}")
|
|
376
|
+
except urllib.error.HTTPError as e:
|
|
377
|
+
raw = e.read().decode("utf-8", errors="ignore")
|
|
378
|
+
raise MemoryClientError(f"DELETE {url} → {e.code}: {raw[:300]}") from e
|
|
379
|
+
except urllib.error.URLError as e:
|
|
380
|
+
raise MemoryClientError(f"cannot reach {url}: {e}") from e
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
class MemoryClientError(RuntimeError):
|
|
384
|
+
"""Raised when the HTTP backend cannot serve a request."""
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Cross-platform secret storage. See ``secrets.py`` for details."""
|
|
2
|
+
from .secrets import (
|
|
3
|
+
account_for,
|
|
4
|
+
backend_display_name,
|
|
5
|
+
backend_name,
|
|
6
|
+
delete_secret,
|
|
7
|
+
get_secret,
|
|
8
|
+
has_secret,
|
|
9
|
+
set_secret,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"account_for",
|
|
14
|
+
"backend_display_name",
|
|
15
|
+
"backend_name",
|
|
16
|
+
"delete_secret",
|
|
17
|
+
"get_secret",
|
|
18
|
+
"has_secret",
|
|
19
|
+
"set_secret",
|
|
20
|
+
]
|