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,142 @@
1
+ """OpenTelemetry instruments of the store. They are no-ops until the process installs
2
+ providers (`lorebase_platform.telemetry.setup_telemetry`, `telemetry.otlp_endpoint` set); the
3
+ process running the store code (Keeper or Scribe) exports them under its own service name."""
4
+
5
+ from __future__ import annotations
6
+
7
+ from collections.abc import Iterable
8
+ from threading import Lock
9
+
10
+ from opentelemetry import metrics
11
+ from opentelemetry.metrics import CallbackOptions, Observation
12
+
13
+ from lorebase_platform.telemetry import LATENCY_BUCKETS_S
14
+
15
+ INSTRUMENTATION = "lorebase.store"
16
+
17
+ meter = metrics.get_meter(INSTRUMENTATION)
18
+
19
+ COMMIT_DURATION = meter.create_histogram(
20
+ "lorebase.store.commit.duration",
21
+ unit="s",
22
+ description="Duration of a successful commit through the commit pipeline, by origin.",
23
+ explicit_bucket_boundaries_advisory=LATENCY_BUCKETS_S,
24
+ )
25
+ MERGE_ATTEMPTS = meter.create_counter(
26
+ "lorebase.store.merge.attempts",
27
+ unit="{attempt}",
28
+ description="Merges of a Proposal, by result (merged, fast_forward, up_to_date, conflicts).",
29
+ )
30
+ DEFINER_READS = meter.create_counter(
31
+ "lorebase.store.reads.definer",
32
+ unit="{call}",
33
+ description="Calls of the security definer read functions, by function.",
34
+ )
35
+ JOBS_CLAIMED = meter.create_counter(
36
+ "lorebase.jobs.claimed", unit="{job}", description="Jobs claimed from the queue, by type."
37
+ )
38
+ JOBS_RECLAIMED = meter.create_counter(
39
+ "lorebase.jobs.reclaimed",
40
+ unit="{job}",
41
+ description="Running jobs whose worker stopped sending heartbeats, taken back from it, by "
42
+ "type and outcome (requeued, failed).",
43
+ )
44
+ JOBS_RELEASED = meter.create_counter(
45
+ "lorebase.jobs.released",
46
+ unit="{job}",
47
+ description="Running jobs a stopping worker put back in the queue, by type.",
48
+ )
49
+ WEBHOOK_OUTCOMES = meter.create_counter(
50
+ "lorebase.webhooks.deliveries",
51
+ unit="{delivery}",
52
+ description="Webhook delivery attempts, by outcome (delivered, retrying, dead).",
53
+ )
54
+ WEBHOOK_TIMEOUTS = meter.create_counter(
55
+ "lorebase.webhooks.timeouts",
56
+ unit="{attempt}",
57
+ description="Webhook delivery attempts cut off by webhooks.timeout_s.",
58
+ )
59
+ OUTBOX_HANDOFFS = meter.create_counter(
60
+ "lorebase.outbox.handoffs",
61
+ unit="{event}",
62
+ description="Outbox events handed off by a consumer, by consumer.",
63
+ )
64
+ OUTBOX_PRUNED = meter.create_counter(
65
+ "lorebase.outbox.pruned",
66
+ unit="{event}",
67
+ description="Outbox events deleted, by reason (retention: handed off by every consumer; "
68
+ "expired: older than store.outbox.max_age_s while still pending).",
69
+ )
70
+
71
+
72
+ class PruneBacklog:
73
+ """Outbox events still eligible for pruning after the latest pruning pass of this process
74
+ (capped, see `OutboxRepository.prunable`); nothing is reported before the first pass."""
75
+
76
+ def __init__(self) -> None:
77
+ self._lock = Lock()
78
+ self._value: int | None = None
79
+
80
+ def set(self, value: int) -> None:
81
+ with self._lock:
82
+ self._value = value
83
+
84
+ def observe(self, _options: CallbackOptions | None = None) -> Iterable[Observation]:
85
+ with self._lock:
86
+ value = self._value
87
+ return [] if value is None else [Observation(value)]
88
+
89
+
90
+ OUTBOX_PRUNE_BACKLOG = PruneBacklog()
91
+ meter.create_observable_gauge(
92
+ "lorebase.outbox.prune.backlog",
93
+ callbacks=[OUTBOX_PRUNE_BACKLOG.observe],
94
+ unit="{event}",
95
+ description="Outbox events eligible for pruning left after the latest pruning pass "
96
+ "(counted up to a cap).",
97
+ )
98
+ OPERATION_EVENTS = meter.create_counter(
99
+ "lorebase.operations.events",
100
+ unit="{event}",
101
+ description="Operation side effect events written to the outbox with a committed execution.",
102
+ )
103
+ GC_RUNS = meter.create_counter(
104
+ "lorebase.gc.runs", unit="{run}", description="Completed garbage collections of a Domain."
105
+ )
106
+ GC_FREED_BYTES = meter.create_counter(
107
+ "lorebase.gc.freed_bytes",
108
+ unit="By",
109
+ description="Bytes of tree nodes and objects deleted by garbage collection.",
110
+ )
111
+
112
+ BRANCH_JOB_ROWS = meter.create_counter(
113
+ "lorebase.store.branch_jobs.rows",
114
+ unit="{row}",
115
+ description="Rows read by background branch jobs, by job (materialize: base rows copied "
116
+ "to a branch, delete: rows of a deleted branch removed, sensitivity_refresh: Records whose "
117
+ "title and search vector were checked).",
118
+ )
119
+ BRANCH_JOB_RUNS = meter.create_counter(
120
+ "lorebase.store.branch_jobs.runs",
121
+ unit="{run}",
122
+ description="Runs of background branch jobs, by job and outcome (done, deferred, waiting, "
123
+ "skipped, cancelled, failed; for copies also retried: failed for good, queued again "
124
+ "with backoff; fallback: the overlay was detached and is rebuilt from its tree; "
125
+ "given_up: no attempt left).",
126
+ )
127
+ BRANCH_JOB_BATCH_ROWS = meter.create_histogram(
128
+ "lorebase.store.branch_jobs.batch.rows",
129
+ unit="{row}",
130
+ description="Rows a batch of a background branch job was sized for, by job: batches "
131
+ "shrink while commits wait for the branch lock and grow back when none does.",
132
+ )
133
+ BRANCH_JOB_BATCH_DURATION = meter.create_histogram(
134
+ "lorebase.store.branch_jobs.batch.duration",
135
+ unit="s",
136
+ description="Duration of one batch transaction of a background branch job, by job.",
137
+ explicit_bucket_boundaries_advisory=LATENCY_BUCKETS_S,
138
+ )
139
+
140
+
141
+ def record_definer_call(function: str) -> None:
142
+ DEFINER_READS.add(1, {"function": function})
@@ -0,0 +1,108 @@
1
+ """Postgres for tests: an existing server or a disposable container.
2
+
3
+ `postgres_server()` yields an asyncpg URL of a server where the role may create databases and
4
+ roles. It uses `LOREBASE_TEST_DATABASE_URL` when set; otherwise it starts a Postgres 16 +
5
+ pgvector container with testcontainers (image overridable with `LOREBASE_TEST_POSTGRES_IMAGE`).
6
+ `scratch_database()` creates an empty database on that server and drops it afterwards.
7
+ `nested_plans()` collects the plans of statements run inside functions (`auto_explain`).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import asyncio
13
+ import json
14
+ import os
15
+ import uuid
16
+ from collections.abc import AsyncIterator, Iterator
17
+ from contextlib import asynccontextmanager, contextmanager
18
+ from typing import Any
19
+
20
+ from sqlalchemy import URL, text
21
+ from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
22
+ from sqlalchemy.pool import NullPool
23
+
24
+ from lorebase_store.db import async_url
25
+
26
+ DATABASE_URL_ENV = "LOREBASE_TEST_DATABASE_URL"
27
+ IMAGE_ENV = "LOREBASE_TEST_POSTGRES_IMAGE"
28
+ DEFAULT_IMAGE = "pgvector/pgvector:pg16"
29
+ SCRATCH_PREFIX = "lorebase_test_"
30
+
31
+
32
+ @contextmanager
33
+ def postgres_server() -> Iterator[str]:
34
+ """URL of a Postgres server for integration tests."""
35
+ configured = os.environ.get(DATABASE_URL_ENV)
36
+ if configured:
37
+ yield async_url(configured).render_as_string(hide_password=False)
38
+ return
39
+ try:
40
+ from testcontainers.community.postgres import PostgresContainer
41
+ except ImportError as exc:
42
+ raise RuntimeError(
43
+ f"set {DATABASE_URL_ENV} or install the store dev dependencies "
44
+ "(uv sync --all-packages) to start a Postgres container"
45
+ ) from exc
46
+ with PostgresContainer(os.environ.get(IMAGE_ENV, DEFAULT_IMAGE), driver="asyncpg") as pg:
47
+ yield pg.get_connection_url()
48
+
49
+
50
+ async def _execute_autocommit(url: URL, statement: str) -> None:
51
+ engine = create_async_engine(url, poolclass=NullPool, isolation_level="AUTOCOMMIT")
52
+ try:
53
+ async with engine.connect() as connection:
54
+ await connection.execute(text(statement))
55
+ finally:
56
+ await engine.dispose()
57
+
58
+
59
+ _AUTO_EXPLAIN = (
60
+ "LOAD 'auto_explain'",
61
+ "SET LOCAL auto_explain.log_min_duration = 0",
62
+ "SET LOCAL auto_explain.log_nested_statements = on",
63
+ "SET LOCAL auto_explain.log_format = json",
64
+ "SET LOCAL client_min_messages = log",
65
+ )
66
+
67
+
68
+ @asynccontextmanager
69
+ async def nested_plans(session: AsyncSession) -> AsyncIterator[list[dict[str, Any]]]:
70
+ """Plans (EXPLAIN JSON `Plan` nodes) of the statements the session's transaction runs
71
+ meanwhile, including those run inside functions, as `auto_explain` reports them.
72
+
73
+ Loading the module needs a superuser: enter before `SET LOCAL ROLE`. Raises
74
+ `sqlalchemy.exc.DBAPIError` where the module cannot be loaded.
75
+ """
76
+ connection = await session.connection()
77
+ for statement in _AUTO_EXPLAIN:
78
+ await connection.exec_driver_sql(statement)
79
+ driver = (await connection.get_raw_connection()).driver_connection
80
+ assert driver is not None
81
+ plans: list[dict[str, Any]] = []
82
+
83
+ def listen(_connection: Any, message: Any) -> None:
84
+ _, marker, body = str(message.message).partition("plan:\n")
85
+ if marker:
86
+ plans.append(json.loads(body)["Plan"])
87
+
88
+ driver.add_log_listener(listen)
89
+ try:
90
+ yield plans
91
+ finally:
92
+ # A top-level statement's plan is logged when its portal closes, at the next statement.
93
+ await connection.exec_driver_sql("SELECT 1")
94
+ await asyncio.sleep(0) # log messages are dispatched through the event loop
95
+ driver.remove_log_listener(listen)
96
+ await connection.exec_driver_sql("SET LOCAL client_min_messages = notice")
97
+
98
+
99
+ @contextmanager
100
+ def scratch_database(server_url: str) -> Iterator[str]:
101
+ """Create a uniquely named empty database on `server_url`, yield its URL, then drop it."""
102
+ server = async_url(server_url)
103
+ name = f"{SCRATCH_PREFIX}{uuid.uuid4().hex[:12]}"
104
+ asyncio.run(_execute_autocommit(server, f'CREATE DATABASE "{name}"'))
105
+ try:
106
+ yield server.set(database=name).render_as_string(hide_password=False)
107
+ finally:
108
+ asyncio.run(_execute_autocommit(server, f'DROP DATABASE IF EXISTS "{name}" WITH (FORCE)'))