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.
Files changed (126) hide show
  1. lorebase_keeper/__init__.py +1 -0
  2. lorebase_keeper/__main__.py +456 -0
  3. lorebase_keeper/agents/__init__.py +1 -0
  4. lorebase_keeper/agents/budget.py +247 -0
  5. lorebase_keeper/agents/context.py +529 -0
  6. lorebase_keeper/agents/explain.py +582 -0
  7. lorebase_keeper/agents/feed.py +259 -0
  8. lorebase_keeper/agents/markdown.py +219 -0
  9. lorebase_keeper/agents/pack.py +403 -0
  10. lorebase_keeper/agents/settings.py +111 -0
  11. lorebase_keeper/app.py +373 -0
  12. lorebase_keeper/auth/__init__.py +4 -0
  13. lorebase_keeper/auth/api_keys.py +127 -0
  14. lorebase_keeper/auth/audit.py +356 -0
  15. lorebase_keeper/auth/cache.py +53 -0
  16. lorebase_keeper/auth/config.py +618 -0
  17. lorebase_keeper/auth/deps.py +183 -0
  18. lorebase_keeper/auth/keyring.py +14 -0
  19. lorebase_keeper/auth/oidc.py +242 -0
  20. lorebase_keeper/auth/permissions.py +142 -0
  21. lorebase_keeper/auth/ports.py +212 -0
  22. lorebase_keeper/auth/principals.py +377 -0
  23. lorebase_keeper/auth/provider.py +250 -0
  24. lorebase_keeper/auth/rbac.py +247 -0
  25. lorebase_keeper/auth/read_audit.py +357 -0
  26. lorebase_keeper/auth/runtime.py +113 -0
  27. lorebase_keeper/auth/sessions.py +452 -0
  28. lorebase_keeper/bootstrap.py +151 -0
  29. lorebase_keeper/client_address.py +129 -0
  30. lorebase_keeper/context.py +226 -0
  31. lorebase_keeper/db.py +93 -0
  32. lorebase_keeper/deps.py +47 -0
  33. lorebase_keeper/errors.py +257 -0
  34. lorebase_keeper/health.py +61 -0
  35. lorebase_keeper/kindhealth/__init__.py +6 -0
  36. lorebase_keeper/kindhealth/config.py +51 -0
  37. lorebase_keeper/kindhealth/markdown.py +163 -0
  38. lorebase_keeper/kindhealth/models.py +179 -0
  39. lorebase_keeper/kindhealth/report.py +154 -0
  40. lorebase_keeper/kindhealth/scan.py +512 -0
  41. lorebase_keeper/middleware.py +285 -0
  42. lorebase_keeper/pagination.py +87 -0
  43. lorebase_keeper/ratelimit/__init__.py +10 -0
  44. lorebase_keeper/ratelimit/bucket.py +118 -0
  45. lorebase_keeper/ratelimit/config.py +283 -0
  46. lorebase_keeper/ratelimit/enforce.py +393 -0
  47. lorebase_keeper/ratelimit/limiter.py +149 -0
  48. lorebase_keeper/ratelimit/quotas.py +204 -0
  49. lorebase_keeper/ratelimit/shared.py +242 -0
  50. lorebase_keeper/readiness.py +257 -0
  51. lorebase_keeper/reading/__init__.py +1 -0
  52. lorebase_keeper/reading/caching.py +251 -0
  53. lorebase_keeper/reading/compiler.py +678 -0
  54. lorebase_keeper/reading/cursors.py +56 -0
  55. lorebase_keeper/reading/diff.py +408 -0
  56. lorebase_keeper/reading/graph.py +268 -0
  57. lorebase_keeper/reading/history.py +152 -0
  58. lorebase_keeper/reading/points.py +410 -0
  59. lorebase_keeper/reading/problems.py +74 -0
  60. lorebase_keeper/reading/record_history.py +722 -0
  61. lorebase_keeper/reading/records.py +780 -0
  62. lorebase_keeper/reading/requests.py +107 -0
  63. lorebase_keeper/reading/runtime.py +145 -0
  64. lorebase_keeper/reading/search.py +708 -0
  65. lorebase_keeper/reading/sensitivity.py +882 -0
  66. lorebase_keeper/reading/settings.py +339 -0
  67. lorebase_keeper/reading/snapshots.py +148 -0
  68. lorebase_keeper/reading/tree.py +98 -0
  69. lorebase_keeper/reading/views.py +264 -0
  70. lorebase_keeper/reading/visibility.py +93 -0
  71. lorebase_keeper/routes/__init__.py +1 -0
  72. lorebase_keeper/routes/admin.py +597 -0
  73. lorebase_keeper/routes/agents.py +160 -0
  74. lorebase_keeper/routes/branches.py +438 -0
  75. lorebase_keeper/routes/derivations.py +215 -0
  76. lorebase_keeper/routes/documents.py +497 -0
  77. lorebase_keeper/routes/domain_api.py +59 -0
  78. lorebase_keeper/routes/domains.py +220 -0
  79. lorebase_keeper/routes/events.py +187 -0
  80. lorebase_keeper/routes/kind_health.py +166 -0
  81. lorebase_keeper/routes/operations.py +392 -0
  82. lorebase_keeper/routes/proposal_items.py +563 -0
  83. lorebase_keeper/routes/proposals.py +1107 -0
  84. lorebase_keeper/routes/read.py +493 -0
  85. lorebase_keeper/routes/revisions.py +523 -0
  86. lorebase_keeper/routes/schema.py +682 -0
  87. lorebase_keeper/routes/schema_inference.py +437 -0
  88. lorebase_keeper/routes/schema_suggestions.py +328 -0
  89. lorebase_keeper/routes/sessions.py +286 -0
  90. lorebase_keeper/routes/source_preview.py +232 -0
  91. lorebase_keeper/routes/sources.py +200 -0
  92. lorebase_keeper/routes/trace.py +506 -0
  93. lorebase_keeper/routes/vault.py +332 -0
  94. lorebase_keeper/routes/webhooks.py +298 -0
  95. lorebase_keeper/routes/whoami.py +88 -0
  96. lorebase_keeper/services/__init__.py +1 -0
  97. lorebase_keeper/services/agent_writes.py +1035 -0
  98. lorebase_keeper/services/branches.py +337 -0
  99. lorebase_keeper/services/config.py +431 -0
  100. lorebase_keeper/services/derivation_preview.py +279 -0
  101. lorebase_keeper/services/document_urls.py +114 -0
  102. lorebase_keeper/services/domains.py +118 -0
  103. lorebase_keeper/services/errors.py +196 -0
  104. lorebase_keeper/services/operation_catalog.py +806 -0
  105. lorebase_keeper/services/operation_events.py +199 -0
  106. lorebase_keeper/services/operations.py +1229 -0
  107. lorebase_keeper/services/pii_import.py +222 -0
  108. lorebase_keeper/services/proposal_reads.py +158 -0
  109. lorebase_keeper/services/proposals.py +850 -0
  110. lorebase_keeper/services/reads.py +286 -0
  111. lorebase_keeper/services/schema.py +601 -0
  112. lorebase_keeper/services/schema_inference.py +390 -0
  113. lorebase_keeper/services/schema_suggestions.py +418 -0
  114. lorebase_keeper/services/source_preview.py +127 -0
  115. lorebase_keeper/settings.py +313 -0
  116. lorebase_keeper/tracing/__init__.py +1 -0
  117. lorebase_keeper/tracing/capture.py +350 -0
  118. lorebase_keeper/tracing/config.py +157 -0
  119. lorebase_keeper/tracing/explain.py +97 -0
  120. lorebase_keeper/tracing/middleware.py +359 -0
  121. lorebase_keeper/tracing/notes.py +80 -0
  122. lorebase_keeper/tracing/recorder.py +180 -0
  123. lorebase_keeper-0.2.3.dist-info/METADATA +21 -0
  124. lorebase_keeper-0.2.3.dist-info/RECORD +126 -0
  125. lorebase_keeper-0.2.3.dist-info/WHEEL +4 -0
  126. 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
+ )