lorebase-keeper 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_keeper/__init__.py +1 -0
- lorebase_keeper/__main__.py +456 -0
- lorebase_keeper/agents/__init__.py +1 -0
- lorebase_keeper/agents/budget.py +247 -0
- lorebase_keeper/agents/context.py +529 -0
- lorebase_keeper/agents/explain.py +582 -0
- lorebase_keeper/agents/feed.py +259 -0
- lorebase_keeper/agents/markdown.py +219 -0
- lorebase_keeper/agents/pack.py +403 -0
- lorebase_keeper/agents/settings.py +111 -0
- lorebase_keeper/app.py +373 -0
- lorebase_keeper/auth/__init__.py +4 -0
- lorebase_keeper/auth/api_keys.py +127 -0
- lorebase_keeper/auth/audit.py +356 -0
- lorebase_keeper/auth/cache.py +53 -0
- lorebase_keeper/auth/config.py +618 -0
- lorebase_keeper/auth/deps.py +183 -0
- lorebase_keeper/auth/keyring.py +14 -0
- lorebase_keeper/auth/oidc.py +242 -0
- lorebase_keeper/auth/permissions.py +142 -0
- lorebase_keeper/auth/ports.py +212 -0
- lorebase_keeper/auth/principals.py +377 -0
- lorebase_keeper/auth/provider.py +250 -0
- lorebase_keeper/auth/rbac.py +247 -0
- lorebase_keeper/auth/read_audit.py +357 -0
- lorebase_keeper/auth/runtime.py +113 -0
- lorebase_keeper/auth/sessions.py +452 -0
- lorebase_keeper/bootstrap.py +151 -0
- lorebase_keeper/client_address.py +129 -0
- lorebase_keeper/context.py +226 -0
- lorebase_keeper/db.py +93 -0
- lorebase_keeper/deps.py +47 -0
- lorebase_keeper/errors.py +257 -0
- lorebase_keeper/health.py +61 -0
- lorebase_keeper/kindhealth/__init__.py +6 -0
- lorebase_keeper/kindhealth/config.py +51 -0
- lorebase_keeper/kindhealth/markdown.py +163 -0
- lorebase_keeper/kindhealth/models.py +179 -0
- lorebase_keeper/kindhealth/report.py +154 -0
- lorebase_keeper/kindhealth/scan.py +512 -0
- lorebase_keeper/middleware.py +285 -0
- lorebase_keeper/pagination.py +87 -0
- lorebase_keeper/ratelimit/__init__.py +10 -0
- lorebase_keeper/ratelimit/bucket.py +118 -0
- lorebase_keeper/ratelimit/config.py +283 -0
- lorebase_keeper/ratelimit/enforce.py +393 -0
- lorebase_keeper/ratelimit/limiter.py +149 -0
- lorebase_keeper/ratelimit/quotas.py +204 -0
- lorebase_keeper/ratelimit/shared.py +242 -0
- lorebase_keeper/readiness.py +257 -0
- lorebase_keeper/reading/__init__.py +1 -0
- lorebase_keeper/reading/caching.py +251 -0
- lorebase_keeper/reading/compiler.py +678 -0
- lorebase_keeper/reading/cursors.py +56 -0
- lorebase_keeper/reading/diff.py +408 -0
- lorebase_keeper/reading/graph.py +268 -0
- lorebase_keeper/reading/history.py +152 -0
- lorebase_keeper/reading/points.py +410 -0
- lorebase_keeper/reading/problems.py +74 -0
- lorebase_keeper/reading/record_history.py +722 -0
- lorebase_keeper/reading/records.py +780 -0
- lorebase_keeper/reading/requests.py +107 -0
- lorebase_keeper/reading/runtime.py +145 -0
- lorebase_keeper/reading/search.py +708 -0
- lorebase_keeper/reading/sensitivity.py +882 -0
- lorebase_keeper/reading/settings.py +339 -0
- lorebase_keeper/reading/snapshots.py +148 -0
- lorebase_keeper/reading/tree.py +98 -0
- lorebase_keeper/reading/views.py +264 -0
- lorebase_keeper/reading/visibility.py +93 -0
- lorebase_keeper/routes/__init__.py +1 -0
- lorebase_keeper/routes/admin.py +597 -0
- lorebase_keeper/routes/agents.py +160 -0
- lorebase_keeper/routes/branches.py +438 -0
- lorebase_keeper/routes/derivations.py +215 -0
- lorebase_keeper/routes/documents.py +497 -0
- lorebase_keeper/routes/domain_api.py +59 -0
- lorebase_keeper/routes/domains.py +220 -0
- lorebase_keeper/routes/events.py +187 -0
- lorebase_keeper/routes/kind_health.py +166 -0
- lorebase_keeper/routes/operations.py +392 -0
- lorebase_keeper/routes/proposal_items.py +563 -0
- lorebase_keeper/routes/proposals.py +1107 -0
- lorebase_keeper/routes/read.py +493 -0
- lorebase_keeper/routes/revisions.py +523 -0
- lorebase_keeper/routes/schema.py +682 -0
- lorebase_keeper/routes/schema_inference.py +437 -0
- lorebase_keeper/routes/schema_suggestions.py +328 -0
- lorebase_keeper/routes/sessions.py +286 -0
- lorebase_keeper/routes/source_preview.py +232 -0
- lorebase_keeper/routes/sources.py +200 -0
- lorebase_keeper/routes/trace.py +506 -0
- lorebase_keeper/routes/vault.py +332 -0
- lorebase_keeper/routes/webhooks.py +298 -0
- lorebase_keeper/routes/whoami.py +88 -0
- lorebase_keeper/services/__init__.py +1 -0
- lorebase_keeper/services/agent_writes.py +1035 -0
- lorebase_keeper/services/branches.py +337 -0
- lorebase_keeper/services/config.py +431 -0
- lorebase_keeper/services/derivation_preview.py +279 -0
- lorebase_keeper/services/document_urls.py +114 -0
- lorebase_keeper/services/domains.py +118 -0
- lorebase_keeper/services/errors.py +196 -0
- lorebase_keeper/services/operation_catalog.py +806 -0
- lorebase_keeper/services/operation_events.py +199 -0
- lorebase_keeper/services/operations.py +1229 -0
- lorebase_keeper/services/pii_import.py +222 -0
- lorebase_keeper/services/proposal_reads.py +158 -0
- lorebase_keeper/services/proposals.py +850 -0
- lorebase_keeper/services/reads.py +286 -0
- lorebase_keeper/services/schema.py +601 -0
- lorebase_keeper/services/schema_inference.py +390 -0
- lorebase_keeper/services/schema_suggestions.py +418 -0
- lorebase_keeper/services/source_preview.py +127 -0
- lorebase_keeper/settings.py +313 -0
- lorebase_keeper/tracing/__init__.py +1 -0
- lorebase_keeper/tracing/capture.py +350 -0
- lorebase_keeper/tracing/config.py +157 -0
- lorebase_keeper/tracing/explain.py +97 -0
- lorebase_keeper/tracing/middleware.py +359 -0
- lorebase_keeper/tracing/notes.py +80 -0
- lorebase_keeper/tracing/recorder.py +180 -0
- lorebase_keeper-0.2.3.dist-info/METADATA +21 -0
- lorebase_keeper-0.2.3.dist-info/RECORD +126 -0
- lorebase_keeper-0.2.3.dist-info/WHEEL +4 -0
- lorebase_keeper-0.2.3.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Request bodies of the read API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
8
|
+
|
|
9
|
+
from lorebase_core.query import Predicate, Query
|
|
10
|
+
|
|
11
|
+
AT = (
|
|
12
|
+
"Revision id or tag to read at (time travel): key-ordered listings read the tree, other "
|
|
13
|
+
"reads a snapshot of the Revision."
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class _Body(BaseModel):
|
|
18
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class QueryRequest(Query):
|
|
22
|
+
"""A DSL query (design 8.2) and where to read it."""
|
|
23
|
+
|
|
24
|
+
branch: str | None = Field(default=None, description="Default: the Domain's default branch.")
|
|
25
|
+
at: str | None = Field(default=None, description=AT)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class RecordAddress(_Body):
|
|
29
|
+
kind: str = Field(min_length=1)
|
|
30
|
+
key: str = Field(min_length=1)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class TraverseStep(_Body):
|
|
34
|
+
relation: str = Field(min_length=1, description="Forward or inverse Relation api name.")
|
|
35
|
+
where: Predicate | None = Field(default=None, description="Filter on the Records reached.")
|
|
36
|
+
limit: int | None = Field(default=None, ge=1, description="Records reached by this step.")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class TraverseRequest(_Body):
|
|
40
|
+
"""Follow a path of Relations from one Record (design 8.3)."""
|
|
41
|
+
|
|
42
|
+
branch: str | None = None
|
|
43
|
+
at: str | None = Field(default=None, description=AT)
|
|
44
|
+
start: RecordAddress
|
|
45
|
+
path: list[TraverseStep] = Field(min_length=1)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class NeighborhoodRequest(_Body):
|
|
49
|
+
"""Breadth-first expansion from seed Records, bounded by depth and caps (design 8.3)."""
|
|
50
|
+
|
|
51
|
+
branch: str | None = None
|
|
52
|
+
at: str | None = Field(default=None, description=AT)
|
|
53
|
+
seeds: list[RecordAddress] = Field(min_length=1)
|
|
54
|
+
depth: int = Field(default=1, ge=1)
|
|
55
|
+
relations: list[str] | None = Field(
|
|
56
|
+
default=None,
|
|
57
|
+
description="Relation api names to follow; a forward name follows Links forward, an "
|
|
58
|
+
"inverse name backward. Default: every Relation in both directions.",
|
|
59
|
+
)
|
|
60
|
+
max_nodes: int | None = Field(default=None, ge=1)
|
|
61
|
+
max_edges: int | None = Field(default=None, ge=1)
|
|
62
|
+
values: bool = Field(
|
|
63
|
+
default=True,
|
|
64
|
+
description="false: nodes are Record headers (kind, key, title) and edges Link "
|
|
65
|
+
"endpoints (relation, from, to), without values; for callers that need the structure "
|
|
66
|
+
"only.",
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
SearchMode = Literal["lexical", "semantic", "hybrid"]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class SearchTuning(_Body):
|
|
74
|
+
"""Ranking knobs of one search in place of the `search` settings (to compare parameter
|
|
75
|
+
sets). Unset knobs keep their configured value; `candidates` and `typo_candidates` may only
|
|
76
|
+
be lowered and `trigram_threshold` only raised."""
|
|
77
|
+
|
|
78
|
+
lexical_weight: float | None = Field(default=None, ge=0, le=100)
|
|
79
|
+
semantic_weight: float | None = Field(default=None, ge=0, le=100)
|
|
80
|
+
rrf_k: int | None = Field(default=None, ge=1, le=10_000)
|
|
81
|
+
trigram_threshold: float | None = Field(default=None, ge=0.1, le=1)
|
|
82
|
+
trigram_weight: float | None = Field(default=None, ge=0, le=100)
|
|
83
|
+
typo_candidates: int | None = Field(default=None, ge=0)
|
|
84
|
+
candidates: int | None = Field(default=None, ge=1)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class SearchRequest(_Body):
|
|
88
|
+
"""Full-text, vector or hybrid search (design 8.4). Vectors come from the caller."""
|
|
89
|
+
|
|
90
|
+
branch: str | None = None
|
|
91
|
+
at: str | None = Field(default=None, description=AT)
|
|
92
|
+
q: str | None = Field(default=None, min_length=1, description="Text query (web search syntax).")
|
|
93
|
+
kinds: list[str] | None = Field(default=None, description="Kind api names; default: all.")
|
|
94
|
+
where: Predicate | None = Field(
|
|
95
|
+
default=None, description="DSL filter; needs exactly one Kind in `kinds`."
|
|
96
|
+
)
|
|
97
|
+
mode: SearchMode | None = Field(
|
|
98
|
+
default=None,
|
|
99
|
+
description="Default: hybrid with both `q` and `query_vector`, otherwise the one given.",
|
|
100
|
+
)
|
|
101
|
+
query_vector: list[float] | None = Field(
|
|
102
|
+
default=None, description="Query embedding of the configured model and dimension."
|
|
103
|
+
)
|
|
104
|
+
limit: int | None = Field(default=None, ge=1)
|
|
105
|
+
tuning: SearchTuning | None = Field(
|
|
106
|
+
default=None, description="Ranking knobs in place of the `search` settings."
|
|
107
|
+
)
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Per-application state of the read path and the read transaction dependency.
|
|
2
|
+
|
|
3
|
+
Reads run in a read-only transaction with the `read.statement_timeout_ms` statement timeout.
|
|
4
|
+
The Domain is added to the row-level security scope when the read point is resolved
|
|
5
|
+
(`reading.points`), so every query of a request stays inside its Domain.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
from collections import OrderedDict
|
|
12
|
+
from collections.abc import AsyncIterator
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from threading import Lock
|
|
15
|
+
from typing import Annotated, cast
|
|
16
|
+
|
|
17
|
+
from fastapi import Depends, Request
|
|
18
|
+
from sqlalchemy import func, select, text
|
|
19
|
+
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
|
20
|
+
|
|
21
|
+
from lorebase_core.model import Schema
|
|
22
|
+
from lorebase_keeper.errors import UNAVAILABLE, ProblemError
|
|
23
|
+
from lorebase_keeper.ratelimit.bucket import InMemoryRateStore
|
|
24
|
+
from lorebase_keeper.reading.caching import ResponseCache, build_cache
|
|
25
|
+
from lorebase_keeper.reading.settings import (
|
|
26
|
+
ReadSettings,
|
|
27
|
+
SearchSettings,
|
|
28
|
+
read_settings,
|
|
29
|
+
search_settings,
|
|
30
|
+
)
|
|
31
|
+
from lorebase_keeper.settings import SensitivitySettings, keeper_settings
|
|
32
|
+
from lorebase_platform.settings import Settings
|
|
33
|
+
from lorebase_store.runtime import StoreRuntime
|
|
34
|
+
from lorebase_store.settings import StoreSettings
|
|
35
|
+
from lorebase_store.vault import VaultKeys
|
|
36
|
+
|
|
37
|
+
_STATE_KEY = "read_runtime"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class SchemaCache:
|
|
41
|
+
"""Thread-safe LRU of compiled schemas keyed by `(domain_id, schema partition hash)`."""
|
|
42
|
+
|
|
43
|
+
def __init__(self, max_entries: int) -> None:
|
|
44
|
+
self._max = max_entries
|
|
45
|
+
self._entries: OrderedDict[tuple[str, bytes], Schema] = OrderedDict()
|
|
46
|
+
self._lock = Lock()
|
|
47
|
+
|
|
48
|
+
def get(self, domain_id: str, schema_hash: bytes) -> Schema | None:
|
|
49
|
+
key = (domain_id, schema_hash)
|
|
50
|
+
with self._lock:
|
|
51
|
+
found = self._entries.get(key)
|
|
52
|
+
if found is not None:
|
|
53
|
+
self._entries.move_to_end(key)
|
|
54
|
+
return found
|
|
55
|
+
|
|
56
|
+
def put(self, domain_id: str, schema_hash: bytes, schema: Schema) -> None:
|
|
57
|
+
with self._lock:
|
|
58
|
+
self._entries[(domain_id, schema_hash)] = schema
|
|
59
|
+
self._entries.move_to_end((domain_id, schema_hash))
|
|
60
|
+
while len(self._entries) > self._max:
|
|
61
|
+
self._entries.popitem(last=False)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
SNAPSHOT_RATE_BUCKETS = 10_000
|
|
65
|
+
"""Principals whose snapshot build buckets are kept (the least recently used are dropped)."""
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass
|
|
69
|
+
class ReadRuntime:
|
|
70
|
+
"""Settings, sessions and caches shared by the reads of one application."""
|
|
71
|
+
|
|
72
|
+
sessions: async_sessionmaker[AsyncSession]
|
|
73
|
+
read: ReadSettings
|
|
74
|
+
search: SearchSettings
|
|
75
|
+
store: StoreRuntime
|
|
76
|
+
cache: ResponseCache | None = None
|
|
77
|
+
sensitivity: SensitivitySettings = field(default_factory=SensitivitySettings)
|
|
78
|
+
schemas: SchemaCache = field(init=False)
|
|
79
|
+
snapshot_builds: asyncio.Semaphore = field(init=False)
|
|
80
|
+
"""Snapshot builds running in this process (`read.time_travel`)."""
|
|
81
|
+
snapshot_rates: InMemoryRateStore = field(init=False)
|
|
82
|
+
"""Snapshot builds started per principal in this process."""
|
|
83
|
+
|
|
84
|
+
def __post_init__(self) -> None:
|
|
85
|
+
self.schemas = SchemaCache(self.read.schema_cache_entries)
|
|
86
|
+
self.snapshot_builds = asyncio.Semaphore(
|
|
87
|
+
self.read.time_travel.snapshot_max_concurrent_builds
|
|
88
|
+
)
|
|
89
|
+
self.snapshot_rates = InMemoryRateStore(SNAPSHOT_RATE_BUCKETS)
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def store_settings(self) -> StoreSettings:
|
|
93
|
+
return self.store.settings
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def vault(self) -> VaultKeys | None:
|
|
97
|
+
"""Keys of the personal data vault; None when it is not configured."""
|
|
98
|
+
return self.store.vault
|
|
99
|
+
|
|
100
|
+
@classmethod
|
|
101
|
+
def build(cls, settings: Settings, sessions: async_sessionmaker[AsyncSession]) -> ReadRuntime:
|
|
102
|
+
reads = keeper_settings(settings).reads
|
|
103
|
+
return cls(
|
|
104
|
+
sessions=sessions,
|
|
105
|
+
read=read_settings(settings),
|
|
106
|
+
search=search_settings(settings),
|
|
107
|
+
store=StoreRuntime.from_loaded(settings),
|
|
108
|
+
cache=build_cache(reads.cache),
|
|
109
|
+
sensitivity=reads.sensitivity,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def get_read_runtime(request: Request) -> ReadRuntime:
|
|
114
|
+
"""The read runtime of the application, built on first use; 503 without a database."""
|
|
115
|
+
state = request.app.state
|
|
116
|
+
runtime = getattr(state, _STATE_KEY, None)
|
|
117
|
+
if runtime is not None:
|
|
118
|
+
return cast(ReadRuntime, runtime)
|
|
119
|
+
database = getattr(state, "database", None)
|
|
120
|
+
if database is None:
|
|
121
|
+
raise ProblemError(UNAVAILABLE, "Reads need a configured database.")
|
|
122
|
+
runtime = ReadRuntime.build(cast(Settings, state.settings), database.sessions)
|
|
123
|
+
setattr(state, _STATE_KEY, runtime)
|
|
124
|
+
return runtime
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
ReadRuntimeDep = Annotated[ReadRuntime, Depends(get_read_runtime)]
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
async def begin_read(session: AsyncSession, runtime: ReadRuntime) -> None:
|
|
131
|
+
"""Make the session's (new) transaction read-only with the read statement timeout."""
|
|
132
|
+
await session.execute(text("SET TRANSACTION READ ONLY"))
|
|
133
|
+
await session.execute(
|
|
134
|
+
select(func.set_config("statement_timeout", str(runtime.read.statement_timeout_ms), True))
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
async def read_session(runtime: ReadRuntimeDep) -> AsyncIterator[AsyncSession]:
|
|
139
|
+
"""A session in a read-only transaction, closed after the response."""
|
|
140
|
+
async with runtime.sessions() as session, session.begin():
|
|
141
|
+
await begin_read(session, runtime)
|
|
142
|
+
yield session
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
ReadSessionDep = Annotated[AsyncSession, Depends(read_session)]
|