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,268 @@
1
+ """Branch creation (design 6.5, 6.11 modes `full` and `overlay`).
2
+
3
+ Creating the ref is O(1). The source branch is locked meanwhile, so no commit moves it.
4
+
5
+ - `overlay`: the branch stores no rows yet and reads the source's rows as of its head
6
+ (`materialize.overlay`); only the compiled schema row is copied. Available when the
7
+ branch starts at the head of a fully materialized source that has fewer than
8
+ `store.materialize.overlay_max_per_branch` overlays; otherwise the branch is a copy.
9
+ - `copy`: the materialized head is copied from the source branch with `INSERT ... SELECT`
10
+ when the branch starts at the source head (an overlay source gives its resolved rows);
11
+ otherwise it is rebuilt from the tree of the start Revision. The branch then gets its
12
+ per-Attribute indexes. When an overlay is possible and the source holds more than
13
+ `store.materialize.async_copy_min_rows` rows, the branch is created as an overlay and a
14
+ `store.materialize` job copies its rows in batches (`materialize.background`):
15
+ `BranchCreation.pending` is then true. A copy of that size that cannot start as an
16
+ overlay (an overlay source, another start point, the overlay cap reached) is created
17
+ `building`: the same job writes its rows from the tree of its start Revision in batches
18
+ (`pending` too); meanwhile reads of its head are served from the tree and commits wait.
19
+
20
+ Without an explicit mode, `store.materialize.branch_mode` decides, except for names starting
21
+ with one of `overlay_branch_prefixes`, which default to overlays.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ from collections.abc import Callable
27
+ from dataclasses import dataclass
28
+ from datetime import UTC, datetime
29
+ from enum import StrEnum
30
+
31
+ from sqlalchemy.ext.asyncio import AsyncSession
32
+
33
+ from lorebase_core.tree import PARTITION_SCHEMA
34
+ from lorebase_store.commit.schema import load_compiled
35
+ from lorebase_store.indexes import BranchIndexes
36
+ from lorebase_store.materialize.background import queue_build, queue_materialization
37
+ from lorebase_store.materialize.overlay import wants_overlay
38
+ from lorebase_store.materialize.rebuild import materialize_revision
39
+ from lorebase_store.materialize.stale import queue_if_stale
40
+ from lorebase_store.models.enums import Materialization, RefKind
41
+ from lorebase_store.nodestore import NodeRepository
42
+ from lorebase_store.repositories.domains import DomainRepository
43
+ from lorebase_store.repositories.materialized import MaterializedRepository
44
+ from lorebase_store.repositories.refs import RefRepository, RefRow
45
+ from lorebase_store.repositories.revisions import RevisionRepository, RevisionRow
46
+ from lorebase_store.repositories.scope import scope_to_domain
47
+ from lorebase_store.runtime import StoreRuntime, default_runtime
48
+ from lorebase_store.storage import constraints_only
49
+
50
+ BRANCH_CREATE_REASON = "branch.create"
51
+
52
+
53
+ class MaterializationMode(StrEnum):
54
+ """How a new branch got its materialized head."""
55
+
56
+ COPY = "copy"
57
+ REBUILD = "rebuild"
58
+ OVERLAY = "overlay"
59
+
60
+
61
+ class BranchMode(StrEnum):
62
+ """The materialization a caller asks for when creating a branch."""
63
+
64
+ COPY = "copy"
65
+ OVERLAY = "overlay"
66
+
67
+
68
+ class BranchExistsError(Exception):
69
+ """A ref with that name already exists."""
70
+
71
+
72
+ class BranchDeletingError(Exception):
73
+ """A branch with that name is being deleted in the background; the name is free again
74
+ once its rows are gone."""
75
+
76
+
77
+ @dataclass(frozen=True, slots=True)
78
+ class BranchCreation:
79
+ ref: RefRow
80
+ mode: MaterializationMode
81
+ pending: bool = False
82
+ """A copy whose rows a background job is still copying (the ref is an overlay)."""
83
+
84
+
85
+ async def create_branch(
86
+ session: AsyncSession,
87
+ domain_id: str,
88
+ name: str,
89
+ *,
90
+ created_by: str,
91
+ source: str | None = None,
92
+ start: bytes | None = None,
93
+ mode: BranchMode | str | None = None,
94
+ runtime: StoreRuntime | None = None,
95
+ clock: Callable[[], datetime] | None = None,
96
+ ) -> BranchCreation:
97
+ """Create branch `name` at `start` (default: the head of `source`, itself defaulting to
98
+ the Domain's default branch), as an overlay or a full copy (`mode`, see module doc).
99
+
100
+ Runs in the caller's transaction when one is open, otherwise in its own.
101
+ """
102
+ runtime = runtime or default_runtime()
103
+ now = clock or (lambda: datetime.now(UTC))
104
+ if mode is not None:
105
+ wanted = BranchMode(mode)
106
+ elif wants_overlay(name, runtime.settings.materialize):
107
+ wanted = BranchMode.OVERLAY
108
+ else:
109
+ wanted = BranchMode.COPY
110
+ args = (session, domain_id, name, created_by, source, start, wanted, runtime, now)
111
+ if session.in_transaction():
112
+ return await _create(*args)
113
+ async with session.begin():
114
+ return await _create(*args)
115
+
116
+
117
+ async def _create(
118
+ session: AsyncSession,
119
+ domain_id: str,
120
+ name: str,
121
+ created_by: str,
122
+ source: str | None,
123
+ start: bytes | None,
124
+ wanted: BranchMode,
125
+ runtime: StoreRuntime,
126
+ now: Callable[[], datetime],
127
+ ) -> BranchCreation:
128
+ await scope_to_domain(session, domain_id)
129
+ refs = RefRepository(session, domain_id)
130
+ if await refs.get(name) is not None:
131
+ raise BranchExistsError(f"ref {name!r} already exists in Domain {domain_id!r}")
132
+ if await refs.get_deleting(name) is not None:
133
+ raise BranchDeletingError(
134
+ f"branch {name!r} of Domain {domain_id!r} is being deleted; retry later"
135
+ )
136
+ source_name = source or (await DomainRepository(session).require(domain_id)).default_branch
137
+ await refs.lock(source_name, timeout_ms=runtime.settings.commit.lock_timeout_ms)
138
+ source_ref = await refs.require(source_name)
139
+ target = start or source_ref.head
140
+ head = await RevisionRepository(session, domain_id).require(target)
141
+ source_rows = MaterializedRepository(
142
+ session, domain_id, source_name, writes=runtime.settings.writes
143
+ )
144
+ # A source still being built has no complete rows to copy: its tree is read instead.
145
+ building = source_ref.materialization is Materialization.BUILDING
146
+ source_schema = None if building else await source_rows.schema()
147
+ at_head = source_ref.head == head.id and source_schema is not None
148
+ settings = runtime.settings.materialize
149
+ overlay_possible = (
150
+ at_head
151
+ and source_ref.kind is RefKind.BRANCH
152
+ and source_ref.materialization is Materialization.FULL
153
+ and len(await refs.overlays_of(source_name)) < settings.overlay_max_per_branch
154
+ )
155
+ background = (
156
+ wanted is BranchMode.COPY
157
+ and overlay_possible
158
+ and await source_rows.own_rows(limit=settings.async_copy_min_rows)
159
+ > settings.async_copy_min_rows
160
+ )
161
+ if (wanted is BranchMode.OVERLAY and overlay_possible) or background:
162
+ assert source_schema is not None
163
+ ref = await refs.create(
164
+ name,
165
+ head.id,
166
+ created_at=now(),
167
+ created_by=created_by,
168
+ kind=RefKind.BRANCH,
169
+ materialization=Materialization.OVERLAY,
170
+ reason=BRANCH_CREATE_REASON,
171
+ base_branch=source_name,
172
+ base_revision=head.id,
173
+ base_generation=head.generation,
174
+ )
175
+ await MaterializedRepository(
176
+ session, domain_id, name, writes=runtime.settings.writes, layout=ref.layout
177
+ ).set_schema(source_schema)
178
+ # Queued: the overlay half of its reads uses them once the job worker built them.
179
+ await BranchIndexes(
180
+ session,
181
+ domain_id,
182
+ name,
183
+ settings=runtime.settings.indexes,
184
+ constraints_only=constraints_only(runtime.settings),
185
+ maintenance=runtime.index_maintenance,
186
+ ).sync(load_compiled(source_schema.compiled))
187
+ # The overlay reads the source's rows: stale ones are the source's to rewrite (its
188
+ # job then queues the overlays based on it).
189
+ await queue_if_stale(
190
+ session, domain_id, source_name, load_compiled(source_schema.compiled), runtime=runtime
191
+ )
192
+ if background:
193
+ await queue_materialization(session, domain_id, ref, at=now())
194
+ return BranchCreation(ref, MaterializationMode.COPY, pending=True)
195
+ return BranchCreation(ref, MaterializationMode.OVERLAY)
196
+ if await _large_copy(session, domain_id, source_rows, head, at_head=at_head, runtime=runtime):
197
+ ref = await refs.create(
198
+ name,
199
+ head.id,
200
+ created_at=now(),
201
+ created_by=created_by,
202
+ kind=RefKind.BRANCH,
203
+ materialization=Materialization.BUILDING,
204
+ reason=BRANCH_CREATE_REASON,
205
+ )
206
+ await queue_build(session, domain_id, name, source=source_name, head=head, at=now())
207
+ mode = MaterializationMode.COPY if at_head else MaterializationMode.REBUILD
208
+ return BranchCreation(ref, mode, pending=True)
209
+ ref = await refs.create(
210
+ name,
211
+ head.id,
212
+ created_at=now(),
213
+ created_by=created_by,
214
+ kind=RefKind.BRANCH,
215
+ materialization=Materialization.FULL,
216
+ reason=BRANCH_CREATE_REASON,
217
+ )
218
+ repository = MaterializedRepository(
219
+ session, domain_id, name, writes=runtime.settings.writes, layout=ref.layout
220
+ )
221
+ if at_head:
222
+ assert source_schema is not None
223
+ await repository.copy_from(source_name)
224
+ copied = load_compiled(source_schema.compiled)
225
+ await BranchIndexes(
226
+ session,
227
+ domain_id,
228
+ name,
229
+ settings=runtime.settings.indexes,
230
+ constraints_only=constraints_only(runtime.settings),
231
+ maintenance=runtime.index_maintenance,
232
+ ).sync(copied)
233
+ # Stale rows of the source are copied as they are.
234
+ await queue_if_stale(session, domain_id, name, copied, runtime=runtime)
235
+ return BranchCreation(ref, MaterializationMode.COPY)
236
+ await materialize_revision(session, domain_id, name, head, runtime=runtime)
237
+ return BranchCreation(ref, MaterializationMode.REBUILD)
238
+
239
+
240
+ async def _large_copy(
241
+ session: AsyncSession,
242
+ domain_id: str,
243
+ source_rows: MaterializedRepository,
244
+ head: RevisionRow,
245
+ *,
246
+ at_head: bool,
247
+ runtime: StoreRuntime,
248
+ ) -> bool:
249
+ """Whether a copy that cannot start as an overlay holds more than
250
+ `async_copy_min_rows` rows: the rows the source reads at its head (an overlay's own rows
251
+ and its base's, an upper bound), or the entries of the start Revision's tree."""
252
+ limit = runtime.settings.materialize.async_copy_min_rows
253
+ if at_head:
254
+ layout = await source_rows.layout()
255
+ total = await source_rows.own_rows(limit=limit)
256
+ if layout.base is not None and total <= limit:
257
+ base_rows = MaterializedRepository(
258
+ session, domain_id, layout.base, writes=runtime.settings.writes
259
+ )
260
+ total += await base_rows.own_rows(limit=limit)
261
+ return total > limit
262
+ settings = runtime.settings
263
+ nodes = NodeRepository(
264
+ session, domain_id, writes=settings.writes, batch=settings.nodes.prefetch_batch
265
+ )
266
+ tree = await nodes.load_tree(head.root)
267
+ total = sum(ref.count for name, ref in tree.partitions.items() if name != PARTITION_SCHEMA)
268
+ return total > limit