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,215 @@
|
|
|
1
|
+
"""Preview of a candidate Derivation (design 5.8, 11.1).
|
|
2
|
+
|
|
3
|
+
| Route | Permission |
|
|
4
|
+
|-------|------------|
|
|
5
|
+
| `POST /domains/{d}/derivations:preview` | `data.read` and `schema.read` on the branch |
|
|
6
|
+
|
|
7
|
+
The body is a schema change set, as for `POST /schema:validate` (`yaml`, or `patch` and
|
|
8
|
+
`remove`; an empty `patch` previews the schema as saved), and `derivation`, the api name of the
|
|
9
|
+
`expression` or `aggregate` Derivation to evaluate in the schema it produces. The schema is
|
|
10
|
+
validated like a schema change (`schema.expression_limits`, the rules of `schema.*`), then the
|
|
11
|
+
Derivation is evaluated on `limit` Records of its Kind (or the `keys` named) at the head of the
|
|
12
|
+
branch, reading the stored and derived values of the branch. Nothing is written.
|
|
13
|
+
|
|
14
|
+
Reads follow Record reads: the caller needs `data.read` for every Kind the Derivation touches
|
|
15
|
+
(403 otherwise), and a value of an Attribute that is sensitive, or computed from sensitive
|
|
16
|
+
Attributes, is masked unless the caller is cleared for it on the Derivation's Kind and on every
|
|
17
|
+
Kind the sensitive values come from; a masked item reports a failure without its code or
|
|
18
|
+
message. At a Revision other than the branch head the preview is refused. The sample size
|
|
19
|
+
defaults to and is capped by `read.derivation_preview`, the length of each of `keys` by
|
|
20
|
+
`read.derivation_preview_max_key_length`.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from collections.abc import Awaitable, Callable
|
|
26
|
+
from typing import Annotated, Any
|
|
27
|
+
|
|
28
|
+
from fastapi import APIRouter, Body, Depends, Request
|
|
29
|
+
from pydantic import BaseModel, Field, JsonValue
|
|
30
|
+
|
|
31
|
+
from lorebase_keeper.auth.audit import annotate
|
|
32
|
+
from lorebase_keeper.auth.deps import AuthDep, PrincipalDep
|
|
33
|
+
from lorebase_keeper.auth.permissions import Permission
|
|
34
|
+
from lorebase_keeper.deps import KeeperSettingsDep
|
|
35
|
+
from lorebase_keeper.errors import Problem, ValidationProblem
|
|
36
|
+
from lorebase_keeper.reading.points import resolve_point
|
|
37
|
+
from lorebase_keeper.reading.problems import invalid
|
|
38
|
+
from lorebase_keeper.reading.runtime import ReadRuntimeDep, ReadSessionDep
|
|
39
|
+
from lorebase_keeper.routes.schema import ChangeSetBody
|
|
40
|
+
from lorebase_keeper.services import derivation_preview as service
|
|
41
|
+
from lorebase_keeper.services.errors import violations_problem
|
|
42
|
+
from lorebase_keeper.services.reads import ReadGuard
|
|
43
|
+
|
|
44
|
+
router = APIRouter(tags=["derivations"])
|
|
45
|
+
|
|
46
|
+
_ERRORS: dict[int | str, dict[str, Any]] = {
|
|
47
|
+
403: {
|
|
48
|
+
"model": Problem,
|
|
49
|
+
"description": "The caller lacks data.read for a Kind the Derivation touches, or "
|
|
50
|
+
"schema.read.",
|
|
51
|
+
},
|
|
52
|
+
404: {"model": Problem, "description": "Domain, branch or Revision not found."},
|
|
53
|
+
422: {
|
|
54
|
+
"model": ValidationProblem,
|
|
55
|
+
"description": "Invalid request, or a candidate schema that fails validation (`issues` "
|
|
56
|
+
"locate why).",
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def preview_guard() -> Callable[..., Awaitable[ReadGuard]]:
|
|
62
|
+
"""Dependency: the authenticated reader of the Domain named by the path; `data.read` and
|
|
63
|
+
`schema.read` are checked once the read point is resolved."""
|
|
64
|
+
|
|
65
|
+
async def dependency(
|
|
66
|
+
request: Request, principal: PrincipalDep, auth: AuthDep, keeper: KeeperSettingsDep
|
|
67
|
+
) -> ReadGuard:
|
|
68
|
+
domain_id = str(request.path_params["domain_id"])
|
|
69
|
+
annotate(request, domain_id=domain_id, permission=Permission.DATA_READ.value)
|
|
70
|
+
return ReadGuard(
|
|
71
|
+
authorizer=auth.authorizer,
|
|
72
|
+
principal=principal,
|
|
73
|
+
permissions=(Permission.DATA_READ, Permission.SCHEMA_READ),
|
|
74
|
+
domain_id=domain_id,
|
|
75
|
+
max_depth=keeper.reads.reachability_max_depth,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
return dependency
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
PreviewReader = Annotated[ReadGuard, Depends(preview_guard())]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class DerivationPreviewRequest(ChangeSetBody):
|
|
85
|
+
"""A candidate schema change set and the Derivation to evaluate in it."""
|
|
86
|
+
|
|
87
|
+
derivation: str = Field(
|
|
88
|
+
min_length=1,
|
|
89
|
+
description="Api name of the `expression` or `aggregate` Derivation to evaluate in the "
|
|
90
|
+
"schema the change set produces.",
|
|
91
|
+
)
|
|
92
|
+
branch: str | None = Field(default=None, description="Default: the default branch.")
|
|
93
|
+
at: str | None = Field(
|
|
94
|
+
default=None, description="Revision id, tag or branch name; only the branch head."
|
|
95
|
+
)
|
|
96
|
+
limit: int | None = Field(
|
|
97
|
+
default=None,
|
|
98
|
+
ge=1,
|
|
99
|
+
description="Records to evaluate; default and maximum: `read.derivation_preview`.",
|
|
100
|
+
)
|
|
101
|
+
keys: list[str] | None = Field(
|
|
102
|
+
default=None,
|
|
103
|
+
min_length=1,
|
|
104
|
+
description="Keys of the Records to evaluate instead of the first ones by key; at most "
|
|
105
|
+
"`read.derivation_preview.max_limit`, each at most "
|
|
106
|
+
"`read.derivation_preview_max_key_length` characters.",
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class PreviewErrorOut(BaseModel):
|
|
111
|
+
"""Why a value could not be computed for a Record."""
|
|
112
|
+
|
|
113
|
+
code: str | None = Field(
|
|
114
|
+
description="Null when the value is masked: only the failure itself is reported."
|
|
115
|
+
)
|
|
116
|
+
message: str | None = Field(description="Null when the value is masked: messages quote values.")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class PreviewItemOut(BaseModel):
|
|
120
|
+
"""The computed value for one sample Record."""
|
|
121
|
+
|
|
122
|
+
key: str
|
|
123
|
+
title: str | None
|
|
124
|
+
value: JsonValue = Field(
|
|
125
|
+
description="The canonical value; null when there is none, the masked value (or null "
|
|
126
|
+
"with `keeper.reads.sensitivity.omit`) when `masked`."
|
|
127
|
+
)
|
|
128
|
+
masked: bool
|
|
129
|
+
error: PreviewErrorOut | None = None
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class DerivationPreviewOut(BaseModel):
|
|
133
|
+
"""Values a candidate Derivation computes on sample Records."""
|
|
134
|
+
|
|
135
|
+
domain: str
|
|
136
|
+
branch: str
|
|
137
|
+
revision: str = Field(description="Revision whose Records were evaluated.")
|
|
138
|
+
served_from: str
|
|
139
|
+
derivation: str
|
|
140
|
+
kind: str
|
|
141
|
+
attribute: str
|
|
142
|
+
items: list[PreviewItemOut]
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _bounded(count: int | None, default: int, maximum: int, pointer: str) -> int:
|
|
146
|
+
effective = default if count is None else count
|
|
147
|
+
if effective > maximum:
|
|
148
|
+
raise invalid(pointer, f"must not exceed {maximum}", "limit_exceeded")
|
|
149
|
+
return effective
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@router.post(
|
|
153
|
+
"/domains/{domain_id}/derivations:preview",
|
|
154
|
+
summary="Preview a candidate Derivation",
|
|
155
|
+
response_model=DerivationPreviewOut,
|
|
156
|
+
responses=_ERRORS,
|
|
157
|
+
)
|
|
158
|
+
async def preview_derivation(
|
|
159
|
+
domain_id: str,
|
|
160
|
+
payload: Annotated[DerivationPreviewRequest, Body()],
|
|
161
|
+
access: PreviewReader,
|
|
162
|
+
runtime: ReadRuntimeDep,
|
|
163
|
+
session: ReadSessionDep,
|
|
164
|
+
keeper: KeeperSettingsDep,
|
|
165
|
+
) -> DerivationPreviewOut:
|
|
166
|
+
"""Evaluate a Derivation, as edited and not yet saved, on sample Records of its Kind.
|
|
167
|
+
|
|
168
|
+
The candidate schema is checked like a schema change; reading follows Record reads
|
|
169
|
+
(permissions per Kind, masking of sensitive values). Nothing is written.
|
|
170
|
+
"""
|
|
171
|
+
bounds = runtime.read.derivation_preview
|
|
172
|
+
limit = _bounded(payload.limit, bounds.default_limit, bounds.max_limit, "/limit")
|
|
173
|
+
if payload.keys is not None:
|
|
174
|
+
_bounded(len(payload.keys), bounds.default_limit, bounds.max_limit, "/keys")
|
|
175
|
+
longest = runtime.read.derivation_preview_max_key_length
|
|
176
|
+
for index, key in enumerate(payload.keys):
|
|
177
|
+
if len(key) > longest:
|
|
178
|
+
raise invalid(
|
|
179
|
+
f"/keys/{index}", f"must not exceed {longest} characters", "limit_exceeded"
|
|
180
|
+
)
|
|
181
|
+
point = await resolve_point(session, runtime, access, branch=payload.branch, at=payload.at)
|
|
182
|
+
if not point.materialized:
|
|
183
|
+
raise invalid("/at", "A Derivation is previewed on the head of a branch.", "time_travel")
|
|
184
|
+
try:
|
|
185
|
+
candidate = service.candidate_of(
|
|
186
|
+
point.schema, payload.to_input(), payload.derivation, runtime.store.schema.rules()
|
|
187
|
+
)
|
|
188
|
+
service.require_new_ids(candidate, point.hidden)
|
|
189
|
+
except service.CandidateInvalidError as error:
|
|
190
|
+
raise violations_problem(error.issues, limit=keeper.schema_changes.max_issues) from None
|
|
191
|
+
point.scope.require(service.kind_names(candidate.schema, candidate.touched))
|
|
192
|
+
items = await service.preview(
|
|
193
|
+
session, runtime, point, candidate, keys=payload.keys, limit=limit
|
|
194
|
+
)
|
|
195
|
+
return DerivationPreviewOut(
|
|
196
|
+
domain=domain_id,
|
|
197
|
+
branch=point.branch,
|
|
198
|
+
revision=point.revision_hex,
|
|
199
|
+
served_from=point.served_from.value,
|
|
200
|
+
derivation=payload.derivation,
|
|
201
|
+
kind=candidate.kind.api_name,
|
|
202
|
+
attribute=candidate.attribute.api_name,
|
|
203
|
+
items=[
|
|
204
|
+
PreviewItemOut(
|
|
205
|
+
key=item.key,
|
|
206
|
+
title=item.title,
|
|
207
|
+
value=item.value,
|
|
208
|
+
masked=item.masked,
|
|
209
|
+
error=PreviewErrorOut(code=item.error_code, message=item.error_message)
|
|
210
|
+
if item.failed
|
|
211
|
+
else None,
|
|
212
|
+
)
|
|
213
|
+
for item in items
|
|
214
|
+
],
|
|
215
|
+
)
|