lorebase-scribe 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.
- lorebase_scribe/__init__.py +3 -0
- lorebase_scribe/__main__.py +156 -0
- lorebase_scribe/checkpoints.py +324 -0
- lorebase_scribe/cron.py +117 -0
- lorebase_scribe/embeddings.py +570 -0
- lorebase_scribe/extraction/__init__.py +17 -0
- lorebase_scribe/extraction/assemble.py +339 -0
- lorebase_scribe/extraction/candidates.py +136 -0
- lorebase_scribe/extraction/discovery.py +567 -0
- lorebase_scribe/extraction/llm_discovery.py +271 -0
- lorebase_scribe/extraction/matching.py +285 -0
- lorebase_scribe/extraction/ontology.py +432 -0
- lorebase_scribe/extraction/parsing.py +114 -0
- lorebase_scribe/extraction/pipeline.py +1091 -0
- lorebase_scribe/extraction/relations.py +194 -0
- lorebase_scribe/extraction/sandbox.py +269 -0
- lorebase_scribe/extraction/settings.py +293 -0
- lorebase_scribe/extraction/text.py +179 -0
- lorebase_scribe/extraction/tier2.py +573 -0
- lorebase_scribe/extraction/values.py +282 -0
- lorebase_scribe/jobs.py +418 -0
- lorebase_scribe/scheduler.py +318 -0
- lorebase_scribe/service.py +146 -0
- lorebase_scribe/settings.py +521 -0
- lorebase_scribe/sources/__init__.py +60 -0
- lorebase_scribe/sources/access.py +241 -0
- lorebase_scribe/sources/connectors.py +320 -0
- lorebase_scribe/sources/delta.py +175 -0
- lorebase_scribe/sources/http_json.py +283 -0
- lorebase_scribe/sources/mapping.py +429 -0
- lorebase_scribe/sources/profile.py +36 -0
- lorebase_scribe/sources/records.py +298 -0
- lorebase_scribe/sources/sql.py +332 -0
- lorebase_scribe/sources/sync.py +730 -0
- lorebase_scribe/telemetry.py +113 -0
- lorebase_scribe/webhooks.py +99 -0
- lorebase_scribe/worker.py +254 -0
- lorebase_scribe-0.2.3.dist-info/METADATA +23 -0
- lorebase_scribe-0.2.3.dist-info/RECORD +41 -0
- lorebase_scribe-0.2.3.dist-info/WHEEL +4 -0
- lorebase_scribe-0.2.3.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
"""The `scribe` settings section: worker loop, scheduler and Source synchronization.
|
|
2
|
+
|
|
3
|
+
Every value has a documented default (see `config/lorebase.example.yaml`); deployments override
|
|
4
|
+
them in the YAML file or with `LOREBASE__SCRIBE__...` variables. Job retries and stale locks
|
|
5
|
+
come from `store.jobs`, webhook delivery from `webhooks`, garbage collection from `store.gc`.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Literal, Self
|
|
11
|
+
|
|
12
|
+
from pydantic import Field, field_validator, model_validator
|
|
13
|
+
|
|
14
|
+
from lorebase_platform.secrets import SecretValue
|
|
15
|
+
from lorebase_platform.settings import SectionModel, Settings, register_section
|
|
16
|
+
from lorebase_scribe.cron import CronError, parse_cron
|
|
17
|
+
from lorebase_scribe.extraction.settings import ExtractionSettings
|
|
18
|
+
from lorebase_store.auth.checkpoints import CheckpointError, load_private_key
|
|
19
|
+
|
|
20
|
+
SECTION = "scribe"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class WorkerSettings(SectionModel):
|
|
24
|
+
"""The job loop of one Scribe process."""
|
|
25
|
+
|
|
26
|
+
concurrency: int = Field(
|
|
27
|
+
default=2, ge=1, le=64, description="Jobs run at the same time by one process."
|
|
28
|
+
)
|
|
29
|
+
poll_interval_s: float = Field(
|
|
30
|
+
default=2.0, gt=0, description="Pause of an idle slot before it looks for jobs again."
|
|
31
|
+
)
|
|
32
|
+
heartbeat_interval_s: float = Field(
|
|
33
|
+
default=30.0,
|
|
34
|
+
gt=0,
|
|
35
|
+
description="Running jobs get their lock time refreshed this often; at most half of "
|
|
36
|
+
"store.jobs.stale_after_seconds, after which a job without heartbeat is reclaimed.",
|
|
37
|
+
)
|
|
38
|
+
shutdown_grace_s: float = Field(
|
|
39
|
+
default=30.0,
|
|
40
|
+
ge=0,
|
|
41
|
+
description="On shutdown, running jobs get this long to finish; unfinished ones are "
|
|
42
|
+
"cancelled and go back to the queue without counting the attempt.",
|
|
43
|
+
)
|
|
44
|
+
shutdown_cancel_s: float = Field(
|
|
45
|
+
default=5.0,
|
|
46
|
+
ge=0,
|
|
47
|
+
description="After shutdown_grace_s, cancelled jobs get this long to stop before they are "
|
|
48
|
+
"put back in the queue anyway. Keep shutdown_grace_s plus this below the time the "
|
|
49
|
+
"platform waits before killing the process (compose stop_grace_period, Kubernetes "
|
|
50
|
+
"terminationGracePeriodSeconds).",
|
|
51
|
+
)
|
|
52
|
+
statement_timeout_ms: int = Field(
|
|
53
|
+
default=0,
|
|
54
|
+
ge=0,
|
|
55
|
+
description="statement_timeout of Scribe's own database connections; 0 disables it "
|
|
56
|
+
"(collections and index builds run long). Postgres Sources use "
|
|
57
|
+
"scribe.sources.postgres.statement_timeout_ms.",
|
|
58
|
+
)
|
|
59
|
+
principal: str = Field(
|
|
60
|
+
default="svc_scribe",
|
|
61
|
+
min_length=1,
|
|
62
|
+
description="Principal id recorded as the author of Scribe's commits and Proposals.",
|
|
63
|
+
)
|
|
64
|
+
types: tuple[str, ...] = Field(
|
|
65
|
+
default=(),
|
|
66
|
+
description="Job types this process runs; empty runs every type Scribe knows.",
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class SchedulerSettings(SectionModel):
|
|
71
|
+
"""Periodic jobs. One Scribe process at a time is the leader (Postgres advisory lock)."""
|
|
72
|
+
|
|
73
|
+
enabled: bool = Field(default=True, description="Take part in the scheduler election.")
|
|
74
|
+
tick_s: float = Field(default=15.0, gt=0, description="Interval between scheduling passes.")
|
|
75
|
+
lock_name: str = Field(
|
|
76
|
+
default="lorebase.scribe.scheduler",
|
|
77
|
+
min_length=1,
|
|
78
|
+
description="Name hashed into the advisory lock key of the leader election.",
|
|
79
|
+
)
|
|
80
|
+
webhook_interval_s: float = Field(
|
|
81
|
+
default=10.0,
|
|
82
|
+
ge=0,
|
|
83
|
+
description="Queue a webhook delivery pass this often (retries included); 0 disables.",
|
|
84
|
+
)
|
|
85
|
+
gc_schedule: str | None = Field(
|
|
86
|
+
default="0 3 * * *",
|
|
87
|
+
description="Cron expression (UTC) of the garbage collection of every Domain; null "
|
|
88
|
+
"disables it.",
|
|
89
|
+
)
|
|
90
|
+
gc_dry_run: bool = Field(default=False, description="Scheduled collections only report.")
|
|
91
|
+
source_schedules: bool = Field(
|
|
92
|
+
default=True,
|
|
93
|
+
description="Queue Source synchronizations according to each Source's `schedule`.",
|
|
94
|
+
)
|
|
95
|
+
embeddings_interval_s: float = Field(
|
|
96
|
+
default=60.0,
|
|
97
|
+
ge=0,
|
|
98
|
+
description="Queue an embedding pass of every Domain this often; on a branch whose "
|
|
99
|
+
"head did not move a pass reads its schema and one key. 0 disables it.",
|
|
100
|
+
)
|
|
101
|
+
audit_verify_interval_s: float = Field(
|
|
102
|
+
default=86400.0,
|
|
103
|
+
ge=0,
|
|
104
|
+
description="Queue a verification of every audit hash chain this often; a broken chain "
|
|
105
|
+
"counts in `lorebase.audit.verifications` with outcome `broken`. The pass reads the "
|
|
106
|
+
"whole audit log in batches. 0 disables it.",
|
|
107
|
+
)
|
|
108
|
+
domain_batch: int = Field(default=100, ge=1, description="Domains read per query.")
|
|
109
|
+
|
|
110
|
+
@field_validator("gc_schedule")
|
|
111
|
+
@classmethod
|
|
112
|
+
def _cron(cls, value: str | None) -> str | None:
|
|
113
|
+
if value is not None:
|
|
114
|
+
try:
|
|
115
|
+
parse_cron(value)
|
|
116
|
+
except CronError as error:
|
|
117
|
+
raise ValueError(str(error)) from None
|
|
118
|
+
return value
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class CsvSourceSettings(SectionModel):
|
|
122
|
+
"""Reading CSV Sources from local files or HTTP(S) URLs."""
|
|
123
|
+
|
|
124
|
+
allowed_roots: tuple[str, ...] = Field(
|
|
125
|
+
default=(),
|
|
126
|
+
description="Directories local CSV files may be read from; empty refuses local files.",
|
|
127
|
+
)
|
|
128
|
+
allowed_hosts: tuple[str, ...] = Field(
|
|
129
|
+
default=(),
|
|
130
|
+
description="Host names CSV URLs may point at; empty refuses URLs.",
|
|
131
|
+
)
|
|
132
|
+
allowed_schemes: tuple[str, ...] = Field(
|
|
133
|
+
default=("https",), min_length=1, description="URL schemes CSV URLs may use."
|
|
134
|
+
)
|
|
135
|
+
allow_private_targets: bool = Field(
|
|
136
|
+
default=False,
|
|
137
|
+
description="Allow CSV URL hosts that resolve to loopback, private or link-local "
|
|
138
|
+
"addresses. Off by default: every resolved address must be global unicast.",
|
|
139
|
+
)
|
|
140
|
+
max_bytes: int = Field(
|
|
141
|
+
default=100 * 1024 * 1024,
|
|
142
|
+
ge=1,
|
|
143
|
+
description="Larger files or downloads are refused; reads stop after this many bytes.",
|
|
144
|
+
)
|
|
145
|
+
timeout_s: float = Field(
|
|
146
|
+
default=60.0, gt=0, description="Time budget of one download, from lookup to last byte."
|
|
147
|
+
)
|
|
148
|
+
encodings: tuple[str, ...] = Field(
|
|
149
|
+
default=("utf-8-sig", "cp1252"),
|
|
150
|
+
min_length=1,
|
|
151
|
+
description="Encodings tried in order; the first that decodes the file wins.",
|
|
152
|
+
)
|
|
153
|
+
delimiters: str = Field(
|
|
154
|
+
default=",;\t|", min_length=1, description="Delimiters the dialect sniffer may pick."
|
|
155
|
+
)
|
|
156
|
+
sniff_bytes: int = Field(
|
|
157
|
+
default=65536, ge=1024, description="Leading bytes used to detect the delimiter."
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class PostgresSourceSettings(SectionModel):
|
|
162
|
+
"""Reading Postgres Sources: read-only transaction, bounded statements."""
|
|
163
|
+
|
|
164
|
+
statement_timeout_ms: int = Field(
|
|
165
|
+
default=60000, ge=1, description="statement_timeout of the read transaction."
|
|
166
|
+
)
|
|
167
|
+
connect_timeout_s: float = Field(default=10.0, gt=0, description="Connection timeout.")
|
|
168
|
+
fetch_rows: int = Field(default=5000, ge=1, description="Rows fetched per round trip.")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class MysqlSourceSettings(SectionModel):
|
|
172
|
+
"""Reading MySQL and MariaDB Sources (needs the `mysql` extra: asyncmy): read-only
|
|
173
|
+
session, bounded statements."""
|
|
174
|
+
|
|
175
|
+
statement_timeout_ms: int = Field(
|
|
176
|
+
default=60000,
|
|
177
|
+
ge=1,
|
|
178
|
+
description="Longest execution of the read: MAX_EXECUTION_TIME on MySQL, "
|
|
179
|
+
"max_statement_time on MariaDB (whole seconds, rounded up).",
|
|
180
|
+
)
|
|
181
|
+
connect_timeout_s: float = Field(default=10.0, gt=0, description="Connection timeout.")
|
|
182
|
+
fetch_rows: int = Field(default=5000, ge=1, description="Rows fetched per round trip.")
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class MssqlSourceSettings(SectionModel):
|
|
186
|
+
"""Reading SQL Server Sources (needs the `mssql` extra: pymssql, run in worker threads).
|
|
187
|
+
|
|
188
|
+
SQL Server has no read-only transaction: connect with a login that can only read."""
|
|
189
|
+
|
|
190
|
+
statement_timeout_ms: int = Field(
|
|
191
|
+
default=60000,
|
|
192
|
+
ge=1,
|
|
193
|
+
description="Longest wait for a result (driver query timeout, whole seconds rounded "
|
|
194
|
+
"up) and for a lock (LOCK_TIMEOUT).",
|
|
195
|
+
)
|
|
196
|
+
connect_timeout_s: float = Field(default=10.0, gt=0, description="Login timeout.")
|
|
197
|
+
fetch_rows: int = Field(default=5000, ge=1, description="Rows fetched per round trip.")
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class JsonSourceSettings(SectionModel):
|
|
201
|
+
"""Reading `json` documents and `jsonl` (JSON Lines) files from local paths."""
|
|
202
|
+
|
|
203
|
+
allowed_roots: tuple[str, ...] = Field(
|
|
204
|
+
default=(),
|
|
205
|
+
description="Directories JSON and JSON Lines files may be read from; empty refuses "
|
|
206
|
+
"every file.",
|
|
207
|
+
)
|
|
208
|
+
max_bytes: int = Field(
|
|
209
|
+
default=100 * 1024 * 1024,
|
|
210
|
+
ge=1,
|
|
211
|
+
description="Largest `json` document; it is parsed whole, so larger files are refused.",
|
|
212
|
+
)
|
|
213
|
+
max_line_bytes: int = Field(
|
|
214
|
+
default=16 * 1024 * 1024,
|
|
215
|
+
ge=1,
|
|
216
|
+
description="Longest line of a `jsonl` file; the file itself is streamed.",
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class ParquetSourceSettings(SectionModel):
|
|
221
|
+
"""Reading `parquet` files from local paths (needs the `parquet` extra: pyarrow)."""
|
|
222
|
+
|
|
223
|
+
allowed_roots: tuple[str, ...] = Field(
|
|
224
|
+
default=(),
|
|
225
|
+
description="Directories Parquet files may be read from; empty refuses every file.",
|
|
226
|
+
)
|
|
227
|
+
batch_rows: int = Field(
|
|
228
|
+
default=10000,
|
|
229
|
+
ge=1,
|
|
230
|
+
description="Rows decoded per batch; a file is streamed batch by batch, never whole.",
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class HttpJsonSourceSettings(SectionModel):
|
|
235
|
+
"""Reading `http_json` Sources: GET requests to JSON APIs, page by page."""
|
|
236
|
+
|
|
237
|
+
allowed_hosts: tuple[str, ...] = Field(
|
|
238
|
+
default=(),
|
|
239
|
+
description="Host names the URLs may point at, next pages included; empty refuses "
|
|
240
|
+
"every URL.",
|
|
241
|
+
)
|
|
242
|
+
allowed_schemes: tuple[str, ...] = Field(
|
|
243
|
+
default=("https",), min_length=1, description="URL schemes the URLs may use."
|
|
244
|
+
)
|
|
245
|
+
allow_private_targets: bool = Field(
|
|
246
|
+
default=False,
|
|
247
|
+
description="Allow hosts that resolve to loopback, private or link-local addresses. "
|
|
248
|
+
"Off by default: every resolved address must be global unicast.",
|
|
249
|
+
)
|
|
250
|
+
timeout_s: float = Field(
|
|
251
|
+
default=30.0, gt=0, description="Time budget of one request, from lookup to last byte."
|
|
252
|
+
)
|
|
253
|
+
max_bytes: int = Field(
|
|
254
|
+
default=50 * 1024 * 1024,
|
|
255
|
+
ge=1,
|
|
256
|
+
description="Larger responses are refused; reads stop after this many bytes.",
|
|
257
|
+
)
|
|
258
|
+
max_pages: int = Field(
|
|
259
|
+
default=1000,
|
|
260
|
+
ge=1,
|
|
261
|
+
description="Pages one run may read; a Source with more fails instead of committing "
|
|
262
|
+
"part of it. A Source's `http.max_pages` can only lower it.",
|
|
263
|
+
)
|
|
264
|
+
max_attempts: int = Field(
|
|
265
|
+
default=4, ge=1, description="Attempts per request, the first one included."
|
|
266
|
+
)
|
|
267
|
+
retry_statuses: tuple[int, ...] = Field(
|
|
268
|
+
default=(429, 500, 502, 503, 504),
|
|
269
|
+
description="Response statuses that are retried; network errors and timeouts are too.",
|
|
270
|
+
)
|
|
271
|
+
backoff_base_s: float = Field(default=1.0, ge=0, description="Pause before the first retry.")
|
|
272
|
+
backoff_factor: float = Field(
|
|
273
|
+
default=2.0, ge=1, description="Each further retry waits this many times longer."
|
|
274
|
+
)
|
|
275
|
+
backoff_max_s: float = Field(
|
|
276
|
+
default=60.0,
|
|
277
|
+
ge=0,
|
|
278
|
+
description="Longest pause between attempts, a server's Retry-After included.",
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
class SourceSettings(SectionModel):
|
|
283
|
+
"""Source synchronization (design 9.1)."""
|
|
284
|
+
|
|
285
|
+
allowed_connections: tuple[str, ...] = Field(
|
|
286
|
+
default=(),
|
|
287
|
+
description="Secret references (secret://env/NAME, secret://file/PATH) a Source "
|
|
288
|
+
"`connection` or an `http_json` header value may name. Sources are defined by schema "
|
|
289
|
+
"authors, so any other reference is refused rather than resolved on this host. Empty: "
|
|
290
|
+
"no Source can be synchronized.",
|
|
291
|
+
)
|
|
292
|
+
commit_batch: int = Field(
|
|
293
|
+
default=5000,
|
|
294
|
+
ge=1,
|
|
295
|
+
description="Changes per Revision on the Source branch; the changes of one row are "
|
|
296
|
+
"never split across Revisions.",
|
|
297
|
+
)
|
|
298
|
+
max_row_errors: int = Field(
|
|
299
|
+
default=100,
|
|
300
|
+
ge=0,
|
|
301
|
+
description="Rows that fail mapping are skipped and reported; a run with more failed "
|
|
302
|
+
"rows than this commits nothing.",
|
|
303
|
+
)
|
|
304
|
+
error_samples: int = Field(default=20, ge=1, description="Row errors kept in the run record.")
|
|
305
|
+
auto_merge_protected: bool = Field(
|
|
306
|
+
default=True,
|
|
307
|
+
description="Sources with an automatic merge policy merge into protected target "
|
|
308
|
+
"branches too; false leaves those Proposals open for review.",
|
|
309
|
+
)
|
|
310
|
+
csv: CsvSourceSettings = Field(default_factory=CsvSourceSettings)
|
|
311
|
+
json_files: JsonSourceSettings = Field(default_factory=JsonSourceSettings)
|
|
312
|
+
parquet: ParquetSourceSettings = Field(default_factory=ParquetSourceSettings)
|
|
313
|
+
http_json: HttpJsonSourceSettings = Field(default_factory=HttpJsonSourceSettings)
|
|
314
|
+
postgres: PostgresSourceSettings = Field(default_factory=PostgresSourceSettings)
|
|
315
|
+
mysql: MysqlSourceSettings = Field(default_factory=MysqlSourceSettings)
|
|
316
|
+
mssql: MssqlSourceSettings = Field(default_factory=MssqlSourceSettings)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
class EmbeddingRetrySettings(SectionModel):
|
|
320
|
+
"""Retries of a failed model call inside a job, after the provider's own retries."""
|
|
321
|
+
|
|
322
|
+
attempts: int = Field(default=2, ge=1, description="Attempts per call, the first included.")
|
|
323
|
+
backoff_s: float = Field(default=2.0, ge=0, description="Pause before the first retry.")
|
|
324
|
+
backoff_max_s: float = Field(
|
|
325
|
+
default=60.0, ge=0, description="Longest pause; each retry waits twice as long."
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
class EmbeddingJobSettings(SectionModel):
|
|
330
|
+
"""Embedding jobs (design 9.3). The model is `inference.embeddings`; Kinds opt in with
|
|
331
|
+
`search.embedding_template` in the schema."""
|
|
332
|
+
|
|
333
|
+
page_rows: int = Field(
|
|
334
|
+
default=500, ge=1, description="Record rows read and rendered per query."
|
|
335
|
+
)
|
|
336
|
+
batch_texts: int = Field(
|
|
337
|
+
default=64,
|
|
338
|
+
ge=1,
|
|
339
|
+
description="Texts per call of the embedding model (the provider may split a call "
|
|
340
|
+
"further by inference.embeddings.batch_size).",
|
|
341
|
+
)
|
|
342
|
+
max_records_per_job: int = Field(
|
|
343
|
+
default=20000,
|
|
344
|
+
ge=1,
|
|
345
|
+
description="Rows one job reads; a pass that needs more queues a continuation, so a "
|
|
346
|
+
"large backfill never holds a worker slot for long.",
|
|
347
|
+
)
|
|
348
|
+
max_calls_per_second: float = Field(
|
|
349
|
+
default=0.0,
|
|
350
|
+
ge=0,
|
|
351
|
+
description="Model calls per second of one job (a paid endpoint's rate limit); 0 "
|
|
352
|
+
"does not limit them.",
|
|
353
|
+
)
|
|
354
|
+
max_text_chars: int = Field(
|
|
355
|
+
default=8000,
|
|
356
|
+
ge=1,
|
|
357
|
+
description="Rendered texts are cut to this many characters before they are hashed and "
|
|
358
|
+
"embedded.",
|
|
359
|
+
)
|
|
360
|
+
template_cache_size: int = Field(
|
|
361
|
+
default=64,
|
|
362
|
+
ge=1,
|
|
363
|
+
description="Schemas whose resolved embedding templates a Scribe process keeps, by "
|
|
364
|
+
"schema hash: branches sharing a schema, and passes over an unchanged one, do not read "
|
|
365
|
+
"and parse it again.",
|
|
366
|
+
)
|
|
367
|
+
retry: EmbeddingRetrySettings = Field(default_factory=EmbeddingRetrySettings)
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
class CheckpointHttpSettings(SectionModel):
|
|
371
|
+
"""An HTTP endpoint receiving each checkpoint (a webhook, or an object store accepting a
|
|
372
|
+
PUT with a bearer token)."""
|
|
373
|
+
|
|
374
|
+
url: str | None = Field(
|
|
375
|
+
default=None,
|
|
376
|
+
description="Target URL; `{name}` is replaced by the checkpoint's object name "
|
|
377
|
+
"(prefix, stream, time), e.g. https://audit.example.com/lorebase/{name}.",
|
|
378
|
+
)
|
|
379
|
+
method: Literal["PUT", "POST"] = Field(default="PUT", description="Request method.")
|
|
380
|
+
headers: dict[str, SecretValue] = Field(
|
|
381
|
+
default_factory=dict,
|
|
382
|
+
description="Extra request headers; values may be secret references, e.g. "
|
|
383
|
+
"Authorization: secret://env/LOREBASE_CHECKPOINT_TOKEN.",
|
|
384
|
+
)
|
|
385
|
+
allowed_schemes: tuple[str, ...] = Field(
|
|
386
|
+
default=("https",), min_length=1, description="URL schemes the URL may use."
|
|
387
|
+
)
|
|
388
|
+
allow_private_targets: bool = Field(
|
|
389
|
+
default=False,
|
|
390
|
+
description="Allow a host resolving to loopback, private or link-local addresses (an "
|
|
391
|
+
"in-cluster store). Off by default: every resolved address must be global unicast and "
|
|
392
|
+
"the connection is pinned to the checked addresses; redirects are never followed.",
|
|
393
|
+
)
|
|
394
|
+
timeout_s: float = Field(default=10.0, gt=0, description="Time budget of one upload.")
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
class CheckpointS3Settings(SectionModel):
|
|
398
|
+
"""An S3-compatible object store (AWS S3, MinIO, Ceph...), written with a PUT signed with
|
|
399
|
+
Signature Version 4. Turn on versioning or object lock on the bucket and give these
|
|
400
|
+
credentials `PutObject` only, so that checkpoints cannot be replaced or deleted."""
|
|
401
|
+
|
|
402
|
+
endpoint: str | None = Field(
|
|
403
|
+
default=None, description="Endpoint URL, e.g. https://s3.eu-west-1.amazonaws.com."
|
|
404
|
+
)
|
|
405
|
+
bucket: str | None = Field(default=None, description="Bucket name.")
|
|
406
|
+
region: str = Field(default="us-east-1", min_length=1, description="Signing region.")
|
|
407
|
+
path_style: bool = Field(
|
|
408
|
+
default=True,
|
|
409
|
+
description="Address the bucket in the path (endpoint/bucket/key) rather than in the "
|
|
410
|
+
"host name (bucket.endpoint/key).",
|
|
411
|
+
)
|
|
412
|
+
access_key_id: SecretValue | None = Field(default=None, description="Access key id.")
|
|
413
|
+
secret_access_key: SecretValue | None = Field(default=None, description="Secret key.")
|
|
414
|
+
session_token: SecretValue | None = Field(
|
|
415
|
+
default=None, description="Session token of temporary credentials."
|
|
416
|
+
)
|
|
417
|
+
allowed_schemes: tuple[str, ...] = Field(
|
|
418
|
+
default=("https",), min_length=1, description="URL schemes the endpoint may use."
|
|
419
|
+
)
|
|
420
|
+
allow_private_targets: bool = Field(
|
|
421
|
+
default=False,
|
|
422
|
+
description="Allow an endpoint resolving to private addresses (as for `http`).",
|
|
423
|
+
)
|
|
424
|
+
timeout_s: float = Field(default=10.0, gt=0, description="Time budget of one upload.")
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
class AuditCheckpointSettings(SectionModel):
|
|
428
|
+
"""Signed checkpoints of the audit chains published outside the database
|
|
429
|
+
(`audit_checkpoint` jobs), against which `python -m lorebase_keeper audit verify
|
|
430
|
+
--checkpoints` detects entries removed from the end of a chain, or rewritten."""
|
|
431
|
+
|
|
432
|
+
enabled: bool = Field(default=False, description="Publish checkpoints.")
|
|
433
|
+
interval_s: float = Field(
|
|
434
|
+
default=3600.0,
|
|
435
|
+
gt=0,
|
|
436
|
+
description="Queue a checkpoint this often. Entries newer than the latest published "
|
|
437
|
+
"checkpoint are not covered.",
|
|
438
|
+
)
|
|
439
|
+
streams: tuple[Literal["actions", "reads"], ...] = Field(
|
|
440
|
+
default=("actions", "reads"),
|
|
441
|
+
min_length=1,
|
|
442
|
+
description="Audit streams checkpointed: actions (audit_log), reads (audit_read_log).",
|
|
443
|
+
)
|
|
444
|
+
signing_key: SecretValue | None = Field(
|
|
445
|
+
default=None,
|
|
446
|
+
description="Ed25519 private key in PEM (openssl genpkey -algorithm ed25519), usually "
|
|
447
|
+
"secret://file//run/secrets/checkpoint.pem. Verifiers need only its public key.",
|
|
448
|
+
)
|
|
449
|
+
key_id: str = Field(
|
|
450
|
+
default="main", min_length=1, description="Name of the key, recorded in checkpoints."
|
|
451
|
+
)
|
|
452
|
+
prefix: str = Field(
|
|
453
|
+
default="lorebase-audit/",
|
|
454
|
+
description="Prefix of the object names: {prefix}{stream}/{UTC time}.json.",
|
|
455
|
+
)
|
|
456
|
+
sink: Literal["file", "http", "s3"] = Field(
|
|
457
|
+
default="file", description="Where checkpoints go: file, http or s3."
|
|
458
|
+
)
|
|
459
|
+
directory: str | None = Field(
|
|
460
|
+
default=None,
|
|
461
|
+
description="Directory of the file sink, on storage the database host cannot write "
|
|
462
|
+
"(a separate volume, or a mounted object store).",
|
|
463
|
+
)
|
|
464
|
+
http: CheckpointHttpSettings = Field(default_factory=CheckpointHttpSettings)
|
|
465
|
+
s3: CheckpointS3Settings = Field(default_factory=CheckpointS3Settings)
|
|
466
|
+
|
|
467
|
+
@field_validator("prefix")
|
|
468
|
+
@classmethod
|
|
469
|
+
def _relative(cls, value: str) -> str:
|
|
470
|
+
if value.startswith("/") or "\\" in value or ".." in value.split("/"):
|
|
471
|
+
raise ValueError("prefix must be a relative name without '..'")
|
|
472
|
+
return value
|
|
473
|
+
|
|
474
|
+
@model_validator(mode="after")
|
|
475
|
+
def _complete(self) -> Self:
|
|
476
|
+
if not self.enabled:
|
|
477
|
+
return self
|
|
478
|
+
missing = []
|
|
479
|
+
if self.signing_key is None:
|
|
480
|
+
missing.append("signing_key")
|
|
481
|
+
if self.sink == "file" and not self.directory:
|
|
482
|
+
missing.append("directory")
|
|
483
|
+
if self.sink == "http" and not self.http.url:
|
|
484
|
+
missing.append("http.url")
|
|
485
|
+
if self.sink == "s3":
|
|
486
|
+
for name in ("endpoint", "bucket", "access_key_id", "secret_access_key"):
|
|
487
|
+
if getattr(self.s3, name) is None:
|
|
488
|
+
missing.append(f"s3.{name}")
|
|
489
|
+
if missing:
|
|
490
|
+
raise ValueError(f"enabled checkpoints need {', '.join(missing)}")
|
|
491
|
+
if self.signing_key is not None:
|
|
492
|
+
try:
|
|
493
|
+
load_private_key(self.signing_key.get_secret_value())
|
|
494
|
+
except CheckpointError as error:
|
|
495
|
+
raise ValueError(f"signing_key: {error}") from None
|
|
496
|
+
return self
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
class ScribeSettings(SectionModel):
|
|
500
|
+
"""Scribe, the ingestion and maintenance worker."""
|
|
501
|
+
|
|
502
|
+
worker: WorkerSettings = Field(default_factory=WorkerSettings)
|
|
503
|
+
scheduler: SchedulerSettings = Field(default_factory=SchedulerSettings)
|
|
504
|
+
sources: SourceSettings = Field(default_factory=SourceSettings)
|
|
505
|
+
extraction: ExtractionSettings = Field(default_factory=ExtractionSettings)
|
|
506
|
+
embeddings: EmbeddingJobSettings = Field(default_factory=EmbeddingJobSettings)
|
|
507
|
+
audit_checkpoints: AuditCheckpointSettings = Field(default_factory=AuditCheckpointSettings)
|
|
508
|
+
|
|
509
|
+
@model_validator(mode="after")
|
|
510
|
+
def _consistent(self) -> Self:
|
|
511
|
+
if self.worker.heartbeat_interval_s <= self.worker.poll_interval_s:
|
|
512
|
+
raise ValueError("worker.heartbeat_interval_s must exceed worker.poll_interval_s")
|
|
513
|
+
return self
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
register_section(SECTION, ScribeSettings)
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def scribe_settings(settings: Settings) -> ScribeSettings:
|
|
520
|
+
"""The validated `scribe` section."""
|
|
521
|
+
return settings.section(SECTION, ScribeSettings)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Structured Sources (design 9.1): connectors, row mapping, delta and the synchronization run."""
|
|
2
|
+
|
|
3
|
+
from lorebase_scribe.sources.connectors import (
|
|
4
|
+
Connector,
|
|
5
|
+
ConnectorFactory,
|
|
6
|
+
CsvConnector,
|
|
7
|
+
HttpJsonConnector,
|
|
8
|
+
JsonConnector,
|
|
9
|
+
JsonLinesConnector,
|
|
10
|
+
MssqlConnector,
|
|
11
|
+
MysqlConnector,
|
|
12
|
+
ParquetConnector,
|
|
13
|
+
PostgresConnector,
|
|
14
|
+
SourceReadError,
|
|
15
|
+
default_connector,
|
|
16
|
+
)
|
|
17
|
+
from lorebase_scribe.sources.delta import CurrentState, Delta, StoredRecord, compute_delta
|
|
18
|
+
from lorebase_scribe.sources.mapping import (
|
|
19
|
+
MappedRow,
|
|
20
|
+
RowError,
|
|
21
|
+
RowMapper,
|
|
22
|
+
SourceDefinitionError,
|
|
23
|
+
)
|
|
24
|
+
from lorebase_scribe.sources.profile import profile_source
|
|
25
|
+
from lorebase_scribe.sources.sync import (
|
|
26
|
+
PERMANENT_ERRORS,
|
|
27
|
+
SourceNotFoundError,
|
|
28
|
+
SourceSync,
|
|
29
|
+
SourceSyncError,
|
|
30
|
+
SyncReport,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"PERMANENT_ERRORS",
|
|
35
|
+
"Connector",
|
|
36
|
+
"ConnectorFactory",
|
|
37
|
+
"CsvConnector",
|
|
38
|
+
"CurrentState",
|
|
39
|
+
"Delta",
|
|
40
|
+
"HttpJsonConnector",
|
|
41
|
+
"JsonConnector",
|
|
42
|
+
"JsonLinesConnector",
|
|
43
|
+
"MappedRow",
|
|
44
|
+
"MssqlConnector",
|
|
45
|
+
"MysqlConnector",
|
|
46
|
+
"ParquetConnector",
|
|
47
|
+
"PostgresConnector",
|
|
48
|
+
"RowError",
|
|
49
|
+
"RowMapper",
|
|
50
|
+
"SourceDefinitionError",
|
|
51
|
+
"SourceNotFoundError",
|
|
52
|
+
"SourceReadError",
|
|
53
|
+
"SourceSync",
|
|
54
|
+
"SourceSyncError",
|
|
55
|
+
"StoredRecord",
|
|
56
|
+
"SyncReport",
|
|
57
|
+
"compute_delta",
|
|
58
|
+
"default_connector",
|
|
59
|
+
"profile_source",
|
|
60
|
+
]
|