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,335 @@
1
+ """Data migrations implied by a schema change (design 6.6 step 2).
2
+
3
+ For every Kind or Relation whose definition changed, existing values are carried to the new
4
+ definition in the same Revision:
5
+
6
+ - values of removed Attributes are dropped;
7
+ - values of retyped Attributes are cast with the core cast rules; a value that cannot be cast
8
+ is an `XS3`-class violation naming the Record;
9
+ - Attributes that are new, or became required, receive their default when they have none;
10
+ - unchanged Records are re-validated (tightened constraints, new required Attributes) and
11
+ values of Attributes that became unique are checked for duplicates (`XS8` class).
12
+
13
+ Records and Links of removed Kinds and Relations are removed. Elements the commit itself
14
+ changes are left to the commit's own changes.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import json
20
+ from collections import defaultdict
21
+ from collections.abc import Callable, Iterable, Mapping
22
+ from dataclasses import dataclass, field
23
+ from datetime import datetime
24
+ from typing import Any
25
+
26
+ from pydantic import JsonValue
27
+
28
+ from lorebase_core.model import (
29
+ Attribute,
30
+ IdPrefix,
31
+ Link,
32
+ LinkIdentity,
33
+ ProvenanceEntry,
34
+ Record,
35
+ RecordIdentity,
36
+ Schema,
37
+ SchemaMigrationOrigin,
38
+ new_id,
39
+ )
40
+ from lorebase_core.types import CastError, TypeSpec, cast, format_timestamp
41
+ from lorebase_core.validate import (
42
+ DataChange,
43
+ Issue,
44
+ IssueCode,
45
+ LinkDelete,
46
+ LinkUpsert,
47
+ RecordDelete,
48
+ RecordUpsert,
49
+ validate_link,
50
+ validate_record,
51
+ )
52
+ from lorebase_store.commit.changes import Change
53
+
54
+ TYPE_COERCION_CLASS = "XS3"
55
+ UNIQUENESS_CLASS = "XS8"
56
+ _SAMPLE_SIZE = 10
57
+
58
+
59
+ @dataclass
60
+ class MigrationPlan:
61
+ """Changes implied by a schema change."""
62
+
63
+ #: Value migrations; validated with the commit's own changes.
64
+ changes: list[Change] = field(default_factory=list)
65
+ #: Records and Links of removed Kinds and Relations; not validated (their type is gone).
66
+ removals: list[DataChange] = field(default_factory=list)
67
+ provenance: list[ProvenanceEntry] = field(default_factory=list)
68
+ issues: list[Issue] = field(default_factory=list)
69
+ #: Kinds whose definition changed: titles and search vectors are recomputed.
70
+ refresh_kinds: list[str] = field(default_factory=list)
71
+
72
+
73
+ @dataclass
74
+ class _Attribution:
75
+ """One provenance entry per migrated Attribute and type transition."""
76
+
77
+ actor: str
78
+ at: datetime
79
+ entries: dict[tuple[str, str | None, str | None], ProvenanceEntry] = field(default_factory=dict)
80
+
81
+ def entry(self, attribute_id: str, from_type: TypeSpec | None, to_type: TypeSpec | None) -> str:
82
+ memo = (
83
+ attribute_id,
84
+ None if from_type is None else from_type.describe(),
85
+ None if to_type is None else to_type.describe(),
86
+ )
87
+ found = self.entries.get(memo)
88
+ if found is None:
89
+ found = ProvenanceEntry(
90
+ id=new_id(IdPrefix.PROVENANCE),
91
+ origin=SchemaMigrationOrigin(
92
+ attribute_id=attribute_id, from_type=from_type, to_type=to_type
93
+ ),
94
+ actor=self.actor,
95
+ at=format_timestamp(self.at),
96
+ )
97
+ self.entries[memo] = found
98
+ return found.id
99
+
100
+
101
+ def plan_migrations(
102
+ old: Schema,
103
+ new: Schema,
104
+ *,
105
+ records_of: Callable[[str], Iterable[Record]],
106
+ links_of: Callable[[str], Iterable[Link]],
107
+ skip: Iterable[RecordIdentity | LinkIdentity] = (),
108
+ actor: str,
109
+ at: datetime,
110
+ sensitive: Callable[[Attribute], bool] = lambda a: a.is_sensitive,
111
+ ) -> MigrationPlan:
112
+ """Data changes that carry existing values from `old` to `new`. Issues never quote
113
+ values of `sensitive` Attributes (by default, those with tags in the schemas)."""
114
+ plan = MigrationPlan()
115
+ attribution = _Attribution(actor, at)
116
+ skipped = set(skip)
117
+ for kind_id in sorted(old.kinds.keys() - new.kinds.keys()):
118
+ plan.removals.extend(
119
+ RecordDelete(kind=kind_id, key=record.key) for record in records_of(kind_id)
120
+ )
121
+ for relation_id in sorted(old.relations.keys() - new.relations.keys()):
122
+ plan.removals.extend(
123
+ LinkDelete(relation=relation_id, from_key=link.from_key, to_key=link.to_key)
124
+ for link in links_of(relation_id)
125
+ )
126
+ for kind_id in sorted(old.kinds.keys() & new.kinds.keys()):
127
+ old_kind, new_kind = old.kinds[kind_id], new.kinds[kind_id]
128
+ if old_kind == new_kind:
129
+ continue
130
+ plan.refresh_kinds.append(kind_id)
131
+ plan.issues.extend(_vault_changes(kind_id, old_kind.attributes, new_kind.attributes))
132
+ unique = [
133
+ a
134
+ for a in new_kind.attributes.values()
135
+ if a.unique and not (a.id in old_kind.attributes and old_kind.attributes[a.id].unique)
136
+ ]
137
+ holders: dict[tuple[str, str], list[str]] = defaultdict(list)
138
+ for record in records_of(kind_id):
139
+ if record.identity in skipped:
140
+ continue
141
+ values, provenance, issues = _migrate_values(
142
+ old_kind.attributes, new_kind.attributes, record.values, attribution, sensitive
143
+ )
144
+ for issue in issues:
145
+ plan.issues.append(
146
+ issue.model_copy(
147
+ update={
148
+ "kind": kind_id,
149
+ "key": record.key,
150
+ "path": ("migrations", kind_id, record.key, *issue.path),
151
+ }
152
+ )
153
+ )
154
+ migrated = Record(kind=kind_id, key=record.key, values=values)
155
+ # A Record with a failed cast is already reported; validating it would repeat it.
156
+ if provenance and not issues:
157
+ plan.changes.append(Change(RecordUpsert(record=migrated), provenance))
158
+ elif not issues:
159
+ plan.issues.extend(
160
+ validate_record(new, migrated, path=("migrations", kind_id, record.key))
161
+ )
162
+ for attribute in unique:
163
+ value = migrated.values.get(attribute.id)
164
+ if value is not None:
165
+ holders[(attribute.id, _value_text(value))].append(record.key)
166
+ plan.issues.extend(_duplicates(kind_id, unique, holders, sensitive))
167
+ for relation_id in sorted(old.relations.keys() & new.relations.keys()):
168
+ old_relation, new_relation = old.relations[relation_id], new.relations[relation_id]
169
+ if old_relation.attributes == new_relation.attributes:
170
+ continue
171
+ for link in links_of(relation_id):
172
+ if link.identity in skipped:
173
+ continue
174
+ values, provenance, issues = _migrate_values(
175
+ old_relation.attributes,
176
+ new_relation.attributes,
177
+ link.values,
178
+ attribution,
179
+ sensitive,
180
+ )
181
+ plan.issues.extend(
182
+ issue.model_copy(
183
+ update={
184
+ "relation": relation_id,
185
+ "path": (
186
+ "migrations",
187
+ relation_id,
188
+ link.from_key,
189
+ link.to_key,
190
+ *issue.path,
191
+ ),
192
+ }
193
+ )
194
+ for issue in issues
195
+ )
196
+ migrated_link = link.model_copy(update={"values": values})
197
+ if issues:
198
+ continue
199
+ if provenance:
200
+ plan.changes.append(Change(LinkUpsert(link=migrated_link), provenance))
201
+ else:
202
+ plan.issues.extend(
203
+ validate_link(new, migrated_link, path=("migrations", relation_id))
204
+ )
205
+ plan.provenance = list(attribution.entries.values())
206
+ return plan
207
+
208
+
209
+ def _migrate_values(
210
+ old_attributes: Mapping[str, Attribute],
211
+ new_attributes: Mapping[str, Attribute],
212
+ values: Mapping[str, JsonValue],
213
+ attribution: _Attribution,
214
+ is_sensitive: Callable[[Attribute], bool],
215
+ ) -> tuple[dict[str, JsonValue], dict[str, str], list[Issue]]:
216
+ migrated: dict[str, JsonValue] = {}
217
+ provenance: dict[str, str] = {}
218
+ issues: list[Issue] = []
219
+ for attribute_id, value in values.items():
220
+ attribute = new_attributes.get(attribute_id)
221
+ before = old_attributes.get(attribute_id)
222
+ if attribute is None:
223
+ provenance[attribute_id] = attribution.entry(
224
+ attribute_id, None if before is None else before.type, None
225
+ )
226
+ continue
227
+ if before is None or before.type == attribute.type:
228
+ migrated[attribute_id] = value
229
+ continue
230
+ try:
231
+ converted: Any = cast(before.type, attribute.type, value)
232
+ except CastError as error:
233
+ # Sensitive values are not quoted: the caller may lack the clearance to read them.
234
+ sensitive = is_sensitive(before) or is_sensitive(attribute)
235
+ details: dict[str, JsonValue] = {
236
+ "class": TYPE_COERCION_CLASS,
237
+ "from_type": before.type.describe(),
238
+ "to_type": attribute.type.describe(),
239
+ }
240
+ if not sensitive:
241
+ details["value"] = value
242
+ shown = "a value" if sensitive else f"value {value!r}"
243
+ issues.append(
244
+ Issue(
245
+ code=IssueCode.VALUE_TYPE,
246
+ path=("values", attribute_id),
247
+ message=(
248
+ f"{attribute.api_name!r} {shown} cannot be cast from "
249
+ f"{before.type.describe()} to {attribute.type.describe()}"
250
+ + ("" if sensitive else f": {error.message}")
251
+ ),
252
+ attribute=attribute_id,
253
+ details=details,
254
+ )
255
+ )
256
+ migrated[attribute_id] = value
257
+ continue
258
+ if converted is not None:
259
+ migrated[attribute_id] = converted
260
+ if converted != value:
261
+ provenance[attribute_id] = attribution.entry(attribute_id, before.type, attribute.type)
262
+ for attribute in new_attributes.values():
263
+ if attribute.id in migrated or attribute.default is None:
264
+ continue
265
+ before = old_attributes.get(attribute.id)
266
+ if before is None or (attribute.required and not before.required):
267
+ migrated[attribute.id] = attribute.default
268
+ provenance[attribute.id] = attribution.entry(attribute.id, None, attribute.type)
269
+ return migrated, provenance, issues
270
+
271
+
272
+ def _value_text(value: JsonValue) -> str:
273
+ return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
274
+
275
+
276
+ def _vault_changes(
277
+ kind_id: str, old: Mapping[str, Attribute], new: Mapping[str, Attribute]
278
+ ) -> list[Issue]:
279
+ """Vault storage is chosen when an Attribute is created: turning it on or off, or
280
+ retyping a vault Attribute, would leave Revisions holding values the other way."""
281
+ issues: list[Issue] = []
282
+ for attribute_id, attribute in new.items():
283
+ before = old.get(attribute_id)
284
+ if before is None:
285
+ continue
286
+ if before.in_vault != attribute.in_vault:
287
+ message = "vault storage cannot be turned on or off; add a new attribute instead"
288
+ elif attribute.in_vault and before.type != attribute.type:
289
+ message = "the type of a vault attribute cannot change; add a new attribute instead"
290
+ else:
291
+ continue
292
+ issues.append(
293
+ Issue(
294
+ code=IssueCode.SCHEMA_VAULT_INVALID,
295
+ path=("kinds", kind_id, "attributes", attribute_id, "vault"),
296
+ message=f"{attribute.api_name!r}: {message}",
297
+ kind=kind_id,
298
+ attribute=attribute_id,
299
+ )
300
+ )
301
+ return issues
302
+
303
+
304
+ def _duplicates(
305
+ kind_id: str,
306
+ unique: list[Attribute],
307
+ holders: Mapping[tuple[str, str], list[str]],
308
+ is_sensitive: Callable[[Attribute], bool],
309
+ ) -> list[Issue]:
310
+ names = {attribute.id: attribute.api_name for attribute in unique}
311
+ sensitive = {attribute.id for attribute in unique if is_sensitive(attribute)}
312
+ issues: list[Issue] = []
313
+ for (attribute_id, value), keys in sorted(holders.items()):
314
+ if len(keys) < 2:
315
+ continue
316
+ ordered = sorted(keys)
317
+ issues.append(
318
+ Issue(
319
+ code=IssueCode.VALUE_UNIQUE,
320
+ path=("migrations", kind_id, ordered[0], "values", attribute_id),
321
+ message=(
322
+ f"{names[attribute_id]!r} became unique but {len(keys)} Records share "
323
+ + ("a value" if attribute_id in sensitive else f"the value {value}")
324
+ ),
325
+ kind=kind_id,
326
+ key=ordered[0],
327
+ attribute=attribute_id,
328
+ details={
329
+ "class": UNIQUENESS_CLASS,
330
+ "count": len(keys),
331
+ "keys": list(ordered[:_SAMPLE_SIZE]),
332
+ },
333
+ )
334
+ )
335
+ return issues