lorebase-store 0.2.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.
Files changed (163) hide show
  1. lorebase_store/__init__.py +1 -0
  2. lorebase_store/_migrations/env.py +106 -0
  3. lorebase_store/_migrations/script.py.mako +27 -0
  4. lorebase_store/_migrations/versions/0001_initial_schema.py +1071 -0
  5. lorebase_store/_migrations/versions/0002_merge_attempt_details.py +74 -0
  6. lorebase_store/_migrations/versions/0003_webhooks.py +127 -0
  7. lorebase_store/_migrations/versions/0004_defer_revision_references.py +38 -0
  8. lorebase_store/_migrations/versions/0005_security_definer_reads.py +396 -0
  9. lorebase_store/_migrations/versions/0006_api_key_usage.py +48 -0
  10. lorebase_store/_migrations/versions/0007_document_extraction.py +154 -0
  11. lorebase_store/_migrations/versions/0008_definer_argument_checks.py +178 -0
  12. lorebase_store/_migrations/versions/0009_api_key_networks.py +33 -0
  13. lorebase_store/_migrations/versions/0010_overlay_branches.py +582 -0
  14. lorebase_store/_migrations/versions/0011_branch_scoped_search_index.py +229 -0
  15. lorebase_store/_migrations/versions/0012_overlay_neighborhood_lookups.py +185 -0
  16. lorebase_store/_migrations/versions/0013_web_sessions.py +72 -0
  17. lorebase_store/_migrations/versions/0014_embedding_jobs.py +70 -0
  18. lorebase_store/_migrations/versions/0015_append_only_audit_log.py +83 -0
  19. lorebase_store/_migrations/versions/0016_sealed_webhook_keys.py +55 -0
  20. lorebase_store/_migrations/versions/0017_background_branch_copies.py +77 -0
  21. lorebase_store/_migrations/versions/0018_search_lexicon.py +477 -0
  22. lorebase_store/_migrations/versions/0019_vault_bindings.py +56 -0
  23. lorebase_store/_migrations/versions/0020_outbox_consumers.py +150 -0
  24. lorebase_store/_migrations/versions/0021_schema_suggestions.py +114 -0
  25. lorebase_store/_migrations/versions/0022_read_audit_and_shared_rate_limits.py +133 -0
  26. lorebase_store/_migrations/versions/0023_agent_trace.py +96 -0
  27. lorebase_store/_migrations/versions/0024_embedding_template_keys.py +205 -0
  28. lorebase_store/_migrations/versions/0025_snapshot_refs.py +35 -0
  29. lorebase_store/_migrations/versions/0026_plain_reverse_link_index.py +112 -0
  30. lorebase_store/_migrations/versions/0027_vault_rotation_and_tombstones.py +145 -0
  31. lorebase_store/_migrations/versions/0028_operation_deferred_events.py +71 -0
  32. lorebase_store/_migrations/versions/0029_agent_trace_sessions.py +108 -0
  33. lorebase_store/_migrations/versions/0030_path_fanout.py +31 -0
  34. lorebase_store/_migrations/versions/0031_branch_builds.py +91 -0
  35. lorebase_store/_migrations/versions/0032_lineage_revisions.py +35 -0
  36. lorebase_store/_migrations/versions/0033_vault_moved_tokens.py +30 -0
  37. lorebase_store/_migrations/versions/0034_inference_endpoint_modes.py +73 -0
  38. lorebase_store/_migrations/versions/0035_search_truncation.py +104 -0
  39. lorebase_store/_migrations/versions/0036_lineage_maps.py +325 -0
  40. lorebase_store/_migrations/versions/0037_bm25_search.py +346 -0
  41. lorebase_store/auth/__init__.py +52 -0
  42. lorebase_store/auth/api_keys.py +161 -0
  43. lorebase_store/auth/audit.py +382 -0
  44. lorebase_store/auth/checkpoints.py +185 -0
  45. lorebase_store/auth/grants.py +161 -0
  46. lorebase_store/auth/principals.py +158 -0
  47. lorebase_store/auth/scope.py +70 -0
  48. lorebase_store/auth/sessions.py +153 -0
  49. lorebase_store/auth/unit.py +53 -0
  50. lorebase_store/auth/usage.py +70 -0
  51. lorebase_store/blobs.py +73 -0
  52. lorebase_store/bm25.py +346 -0
  53. lorebase_store/bulk.py +147 -0
  54. lorebase_store/bundle.py +1830 -0
  55. lorebase_store/changefeed.py +261 -0
  56. lorebase_store/commit/__init__.py +42 -0
  57. lorebase_store/commit/changes.py +67 -0
  58. lorebase_store/commit/errors.py +60 -0
  59. lorebase_store/commit/migrations.py +335 -0
  60. lorebase_store/commit/pipeline.py +754 -0
  61. lorebase_store/commit/schema.py +123 -0
  62. lorebase_store/db.py +58 -0
  63. lorebase_store/definer.py +50 -0
  64. lorebase_store/digests.py +99 -0
  65. lorebase_store/embeddings.py +444 -0
  66. lorebase_store/endpoint_modes.py +59 -0
  67. lorebase_store/extraction/__init__.py +61 -0
  68. lorebase_store/extraction/items.py +187 -0
  69. lorebase_store/extraction/materialize.py +364 -0
  70. lorebase_store/extraction/proposal.py +65 -0
  71. lorebase_store/extraction/review.py +168 -0
  72. lorebase_store/gc.py +970 -0
  73. lorebase_store/indexes.py +1033 -0
  74. lorebase_store/jobs.py +651 -0
  75. lorebase_store/keyring.py +72 -0
  76. lorebase_store/layout.py +335 -0
  77. lorebase_store/materialize/__init__.py +103 -0
  78. lorebase_store/materialize/apply.py +436 -0
  79. lorebase_store/materialize/background.py +1184 -0
  80. lorebase_store/materialize/branch.py +268 -0
  81. lorebase_store/materialize/build.py +536 -0
  82. lorebase_store/materialize/derived.py +540 -0
  83. lorebase_store/materialize/lineage_backfill.py +502 -0
  84. lorebase_store/materialize/links.py +266 -0
  85. lorebase_store/materialize/overlay.py +292 -0
  86. lorebase_store/materialize/paths.py +315 -0
  87. lorebase_store/materialize/rebuild.py +326 -0
  88. lorebase_store/materialize/rows.py +153 -0
  89. lorebase_store/materialize/sensitivity.py +66 -0
  90. lorebase_store/materialize/snapshot.py +205 -0
  91. lorebase_store/materialize/stale.py +444 -0
  92. lorebase_store/materialize/verify.py +321 -0
  93. lorebase_store/merging/__init__.py +39 -0
  94. lorebase_store/merging/attempts.py +334 -0
  95. lorebase_store/merging/lineage.py +123 -0
  96. lorebase_store/merging/service.py +652 -0
  97. lorebase_store/migrations.py +133 -0
  98. lorebase_store/models/__init__.py +136 -0
  99. lorebase_store/models/base.py +64 -0
  100. lorebase_store/models/enums.py +132 -0
  101. lorebase_store/models/governance.py +268 -0
  102. lorebase_store/models/ingestion.py +288 -0
  103. lorebase_store/models/materialized.py +277 -0
  104. lorebase_store/models/proposals.py +195 -0
  105. lorebase_store/models/queues.py +87 -0
  106. lorebase_store/models/tracing.py +116 -0
  107. lorebase_store/models/versioning.py +222 -0
  108. lorebase_store/models/webhooks.py +96 -0
  109. lorebase_store/nodestore.py +384 -0
  110. lorebase_store/objectstore.py +162 -0
  111. lorebase_store/outbox_events.py +166 -0
  112. lorebase_store/profiling/__init__.py +58 -0
  113. lorebase_store/profiling/delimited.py +109 -0
  114. lorebase_store/profiling/postgres.py +211 -0
  115. lorebase_store/profiling/record_files.py +127 -0
  116. lorebase_store/profiling/records.py +92 -0
  117. lorebase_store/profiling/sql_databases.py +298 -0
  118. lorebase_store/ratelimit.py +129 -0
  119. lorebase_store/reads.py +1491 -0
  120. lorebase_store/replicas.py +434 -0
  121. lorebase_store/repositories/__init__.py +152 -0
  122. lorebase_store/repositories/changesets.py +59 -0
  123. lorebase_store/repositories/deferred_events.py +142 -0
  124. lorebase_store/repositories/documents.py +305 -0
  125. lorebase_store/repositories/domains.py +97 -0
  126. lorebase_store/repositories/events.py +205 -0
  127. lorebase_store/repositories/index_view.py +489 -0
  128. lorebase_store/repositories/invocations.py +68 -0
  129. lorebase_store/repositories/materializations.py +276 -0
  130. lorebase_store/repositories/materialized.py +1129 -0
  131. lorebase_store/repositories/merges.py +417 -0
  132. lorebase_store/repositories/proposal_items.py +199 -0
  133. lorebase_store/repositories/proposals.py +184 -0
  134. lorebase_store/repositories/provenance.py +77 -0
  135. lorebase_store/repositories/refs.py +452 -0
  136. lorebase_store/repositories/revisions.py +273 -0
  137. lorebase_store/repositories/scope.py +33 -0
  138. lorebase_store/repositories/source_runs.py +123 -0
  139. lorebase_store/repositories/suggestions.py +638 -0
  140. lorebase_store/runtime.py +87 -0
  141. lorebase_store/schema_cache.py +50 -0
  142. lorebase_store/settings.py +879 -0
  143. lorebase_store/storage.py +1478 -0
  144. lorebase_store/telemetry.py +142 -0
  145. lorebase_store/testing.py +108 -0
  146. lorebase_store/tracing.py +505 -0
  147. lorebase_store/traverse.py +578 -0
  148. lorebase_store/vault/__init__.py +50 -0
  149. lorebase_store/vault/keys.py +216 -0
  150. lorebase_store/vault/quotes.py +133 -0
  151. lorebase_store/vault/rotation.py +209 -0
  152. lorebase_store/vault/settings.py +152 -0
  153. lorebase_store/vault/store.py +987 -0
  154. lorebase_store/vault/transfer.py +558 -0
  155. lorebase_store/webhooks/__init__.py +24 -0
  156. lorebase_store/webhooks/delivery.py +637 -0
  157. lorebase_store/webhooks/keys.py +126 -0
  158. lorebase_store/webhooks/repository.py +142 -0
  159. lorebase_store/webhooks/settings.py +138 -0
  160. lorebase_store/webhooks/signing.py +50 -0
  161. lorebase_store-0.2.3.dist-info/METADATA +13 -0
  162. lorebase_store-0.2.3.dist-info/RECORD +163 -0
  163. lorebase_store-0.2.3.dist-info/WHEEL +4 -0
@@ -0,0 +1,68 @@
1
+ """Operation invocations (design 10.1): one row per execution, idempotent per principal and key."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from datetime import datetime
7
+ from typing import Any
8
+
9
+ from sqlalchemy import Table, insert, select
10
+ from sqlalchemy.ext.asyncio import AsyncSession
11
+
12
+ from lorebase_store.models import metadata
13
+ from lorebase_store.models.enums import InvocationStatus
14
+
15
+ INVOCATIONS: Table = metadata.tables["operation_invocations"]
16
+
17
+
18
+ @dataclass(frozen=True, slots=True)
19
+ class InvocationRow:
20
+ id: str
21
+ operation_id: str
22
+ principal: str
23
+ params: dict[str, Any]
24
+ status: InvocationStatus
25
+ revision_id: bytes | None
26
+ proposal_id: str | None
27
+ idempotency_key: str | None
28
+ created_at: datetime
29
+ duration_ms: int | None
30
+
31
+
32
+ class InvocationRepository:
33
+ """Invocations of one Domain."""
34
+
35
+ def __init__(self, session: AsyncSession, domain_id: str) -> None:
36
+ self._session = session
37
+ self._domain = domain_id
38
+
39
+ async def insert(self, row: InvocationRow) -> None:
40
+ await self._session.execute(
41
+ insert(INVOCATIONS).values(
42
+ domain_id=self._domain,
43
+ id=row.id,
44
+ operation_id=row.operation_id,
45
+ principal=row.principal,
46
+ params=row.params,
47
+ status=row.status,
48
+ revision_id=row.revision_id,
49
+ proposal_id=row.proposal_id,
50
+ idempotency_key=row.idempotency_key,
51
+ created_at=row.created_at,
52
+ duration_ms=row.duration_ms,
53
+ )
54
+ )
55
+
56
+ async def by_key(self, principal: str, idempotency_key: str) -> InvocationRow | None:
57
+ """The invocation a principal recorded under an idempotency key."""
58
+ result = await self._session.execute(
59
+ select(INVOCATIONS).where(
60
+ INVOCATIONS.c.domain_id == self._domain,
61
+ INVOCATIONS.c.principal == principal,
62
+ INVOCATIONS.c.idempotency_key == idempotency_key,
63
+ )
64
+ )
65
+ found = result.mappings().one_or_none()
66
+ if found is None:
67
+ return None
68
+ return InvocationRow(**{k: v for k, v in found.items() if k != "domain_id"})
@@ -0,0 +1,276 @@
1
+ """Progress of background branch copies (`branch_materializations`, revisions 0017, 0031).
2
+
3
+ A row exists for each branch a `store.materialize` job turns into a full copy
4
+ (`lorebase_store.materialize.background`): from the base rows of an overlay (`base`) or from
5
+ the tree of its head (`tree`). It holds the status, the phase and cursor where the next
6
+ batch starts, the rows done out of the rows to read, the failed jobs and the schema changes
7
+ to reconcile.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from collections.abc import Collection
13
+ from dataclasses import dataclass
14
+ from datetime import datetime
15
+ from enum import StrEnum
16
+ from typing import Any
17
+
18
+ from sqlalchemy import Table, func, select, text, update
19
+ from sqlalchemy.dialects.postgresql import insert as pg_insert
20
+ from sqlalchemy.ext.asyncio import AsyncSession
21
+
22
+ from lorebase_store.models import metadata
23
+ from lorebase_store.repositories.refs import advisory_lock_key
24
+
25
+ MATERIALIZATIONS: Table = metadata.tables["branch_materializations"]
26
+
27
+ _FENCE = text("SELECT pg_advisory_xact_lock(:key)")
28
+ _FENCE_SHARED = text("SELECT pg_advisory_xact_lock_shared(:key)")
29
+
30
+
31
+ async def copy_fence(
32
+ session: AsyncSession, domain_id: str, branch: str, *, shared: bool = False
33
+ ) -> None:
34
+ """Take the transaction-scoped fence between the batches of a background copy of `branch`
35
+ (`shared`) and whatever removes or replaces its rows outside a commit (promotion, re-fork,
36
+ rebuild, the switch to a full copy; exclusive). A batch inserts with `ON CONFLICT DO
37
+ NOTHING`: one running while such a transaction deletes rows of the branch could insert a
38
+ row again after the delete. Commits do not take it."""
39
+ key = advisory_lock_key(domain_id, f"\x00copy-fence\x00{branch}")
40
+ await session.execute(_FENCE_SHARED if shared else _FENCE, {"key": key})
41
+
42
+
43
+ class MaterializationStatus(StrEnum):
44
+ """Where a background copy stands."""
45
+
46
+ PENDING = "pending"
47
+ """Queued, or waiting for a retry after a failed attempt."""
48
+ RUNNING = "running"
49
+ DONE = "done"
50
+ FAILED = "failed"
51
+ """Its job used every attempt; the branch stays a correct overlay."""
52
+
53
+
54
+ ACTIVE = (MaterializationStatus.PENDING, MaterializationStatus.RUNNING)
55
+
56
+
57
+ class MaterializationStrategy(StrEnum):
58
+ """Where a background copy reads the rows it writes."""
59
+
60
+ BASE = "base"
61
+ """The base rows an overlay reads, as of its fork point."""
62
+ TREE = "tree"
63
+ """The tree of the branch head (a `building` branch)."""
64
+
65
+
66
+ class MaterializationPhase(StrEnum):
67
+ """What the next batch does; `switch` makes the branch a full copy.
68
+
69
+ `base`: `records`, `links`, `history`, `switch`. `tree`: `clear` (rows left by an
70
+ earlier state), `tree_history`, `tree_rows`, `derived`, `paths`, `switch`. After the
71
+ switch, a copy that got schema changes reconciles: `prune`, `derived`, `paths`,
72
+ `search`.
73
+ """
74
+
75
+ RECORDS = "records"
76
+ LINKS = "links"
77
+ HISTORY = "history"
78
+ CLEAR = "clear"
79
+ TREE_HISTORY = "tree_history"
80
+ TREE_ROWS = "tree_rows"
81
+ SWITCH = "switch"
82
+ PRUNE = "prune"
83
+ DERIVED = "derived"
84
+ PATHS = "paths"
85
+ SEARCH = "search"
86
+ DONE = "done"
87
+
88
+
89
+ @dataclass(frozen=True, slots=True)
90
+ class MaterializationState:
91
+ branch: str
92
+ status: MaterializationStatus
93
+ phase: MaterializationPhase
94
+ base_branch: str
95
+ base_generation: int
96
+ cursor: list[Any] | None
97
+ rows_done: int
98
+ rows_total: int | None
99
+ error: str | None
100
+ created_at: datetime
101
+ updated_at: datetime
102
+ finished_at: datetime | None
103
+ strategy: MaterializationStrategy = MaterializationStrategy.BASE
104
+ failures: int = 0
105
+ reconcile: dict[str, Any] | None = None
106
+
107
+ @property
108
+ def active(self) -> bool:
109
+ return self.status in ACTIVE
110
+
111
+
112
+ def _state(row: Any) -> MaterializationState:
113
+ cursor = row["cursor"]
114
+ return MaterializationState(
115
+ branch=row["branch"],
116
+ status=MaterializationStatus(row["status"]),
117
+ phase=MaterializationPhase(row["phase"]),
118
+ base_branch=row["base_branch"],
119
+ base_generation=int(row["base_generation"]),
120
+ cursor=None if cursor is None else list(cursor["key"]),
121
+ rows_done=int(row["rows_done"]),
122
+ rows_total=None if row["rows_total"] is None else int(row["rows_total"]),
123
+ error=row["error"],
124
+ created_at=row["created_at"],
125
+ updated_at=row["updated_at"],
126
+ finished_at=row["finished_at"],
127
+ strategy=MaterializationStrategy(row["strategy"]),
128
+ failures=int(row["failures"]),
129
+ reconcile=row["reconcile"],
130
+ )
131
+
132
+
133
+ class BranchMaterializationRepository:
134
+ """Background copy progress of the branches of one Domain."""
135
+
136
+ def __init__(self, session: AsyncSession, domain_id: str) -> None:
137
+ self._session = session
138
+ self._domain = domain_id
139
+
140
+ async def get(self, branch: str, *, for_update: bool = False) -> MaterializationState | None:
141
+ query = select(MATERIALIZATIONS).where(
142
+ MATERIALIZATIONS.c.domain_id == self._domain, MATERIALIZATIONS.c.branch == branch
143
+ )
144
+ if for_update:
145
+ query = query.with_for_update()
146
+ found = (await self._session.execute(query)).mappings().one_or_none()
147
+ return None if found is None else _state(found)
148
+
149
+ async def of(self, branches: Collection[str]) -> dict[str, MaterializationState]:
150
+ """The rows of `branches` that have one, by branch."""
151
+ if not branches:
152
+ return {}
153
+ result = await self._session.execute(
154
+ select(MATERIALIZATIONS).where(
155
+ MATERIALIZATIONS.c.domain_id == self._domain,
156
+ MATERIALIZATIONS.c.branch.in_(list(branches)),
157
+ )
158
+ )
159
+ return {row["branch"]: _state(row) for row in result.mappings()}
160
+
161
+ async def active(self, branch: str) -> bool:
162
+ """Whether a background copy of `branch` is queued or running."""
163
+ found = await self._session.scalar(
164
+ select(func.count())
165
+ .select_from(MATERIALIZATIONS)
166
+ .where(
167
+ MATERIALIZATIONS.c.domain_id == self._domain,
168
+ MATERIALIZATIONS.c.branch == branch,
169
+ MATERIALIZATIONS.c.status.in_([s.value for s in ACTIVE]),
170
+ )
171
+ )
172
+ return bool(found)
173
+
174
+ async def count_active(self) -> int:
175
+ """Background copies queued or running in the Domain."""
176
+ found = await self._session.scalar(
177
+ select(func.count())
178
+ .select_from(MATERIALIZATIONS)
179
+ .where(
180
+ MATERIALIZATIONS.c.domain_id == self._domain,
181
+ MATERIALIZATIONS.c.status.in_([s.value for s in ACTIVE]),
182
+ )
183
+ )
184
+ return int(found or 0)
185
+
186
+ async def start(
187
+ self,
188
+ branch: str,
189
+ *,
190
+ base_branch: str,
191
+ base_generation: int,
192
+ status: MaterializationStatus,
193
+ at: datetime,
194
+ strategy: MaterializationStrategy = MaterializationStrategy.BASE,
195
+ failures: int = 0,
196
+ reconcile: dict[str, Any] | None = None,
197
+ ) -> MaterializationState:
198
+ """Start (or restart from the first row) the copy of `branch`: from its fork point
199
+ (`base`) or from the tree of its head (`tree`)."""
200
+ first = (
201
+ MaterializationPhase.RECORDS
202
+ if strategy is MaterializationStrategy.BASE
203
+ else MaterializationPhase.CLEAR
204
+ )
205
+ values: dict[str, Any] = {
206
+ "status": status.value,
207
+ "phase": first.value,
208
+ "strategy": strategy.value,
209
+ "base_branch": base_branch,
210
+ "base_generation": base_generation,
211
+ "cursor": None,
212
+ "rows_done": 0,
213
+ "rows_total": None,
214
+ "error": None,
215
+ "failures": failures,
216
+ "reconcile": reconcile,
217
+ "updated_at": at,
218
+ "finished_at": None,
219
+ }
220
+ statement = pg_insert(MATERIALIZATIONS).values(
221
+ domain_id=self._domain, branch=branch, created_at=at, **values
222
+ )
223
+ await self._session.execute(
224
+ statement.on_conflict_do_update(
225
+ index_elements=[MATERIALIZATIONS.c.domain_id, MATERIALIZATIONS.c.branch],
226
+ set_=values,
227
+ )
228
+ )
229
+ found = await self.get(branch)
230
+ assert found is not None
231
+ return found
232
+
233
+ async def update(self, branch: str, *, at: datetime, **values: Any) -> None:
234
+ """Set columns of the row of `branch` (`cursor` as a list of key values or None)."""
235
+ if "status" in values:
236
+ values["status"] = MaterializationStatus(values["status"]).value
237
+ if "phase" in values:
238
+ values["phase"] = MaterializationPhase(values["phase"]).value
239
+ if "strategy" in values:
240
+ values["strategy"] = MaterializationStrategy(values["strategy"]).value
241
+ if "cursor" in values and values["cursor"] is not None:
242
+ values["cursor"] = {"key": list(values["cursor"])}
243
+ await self._session.execute(
244
+ update(MATERIALIZATIONS)
245
+ .where(
246
+ MATERIALIZATIONS.c.domain_id == self._domain,
247
+ MATERIALIZATIONS.c.branch == branch,
248
+ )
249
+ .values(updated_at=at, **values)
250
+ )
251
+
252
+ async def finish(self, branch: str, *, at: datetime) -> None:
253
+ """Mark the copy of `branch` done (also when another path made it a full copy)."""
254
+ await self.update(
255
+ branch,
256
+ at=at,
257
+ status=MaterializationStatus.DONE,
258
+ phase=MaterializationPhase.DONE,
259
+ cursor=None,
260
+ error=None,
261
+ failures=0,
262
+ reconcile=None,
263
+ finished_at=at,
264
+ )
265
+
266
+ async def add_reconcile(
267
+ self, branch: str, *, refresh_kinds: Collection[str], at: datetime
268
+ ) -> None:
269
+ """Record a schema change made while the copy of `branch` runs: after its switch, the
270
+ job recomputes what a schema change recomputes on a full branch (`refresh_kinds`:
271
+ Kinds whose titles and search vectors change)."""
272
+ state = await self.get(branch, for_update=True)
273
+ if state is None:
274
+ return
275
+ kinds = set((state.reconcile or {}).get("refresh_kinds", ())) | set(refresh_kinds)
276
+ await self.update(branch, at=at, reconcile={"refresh_kinds": sorted(kinds)})