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,268 @@
|
|
|
1
|
+
"""Traversal and neighborhoods (design 8.3).
|
|
2
|
+
|
|
3
|
+
Traversal runs one set-based query per hop, capped per hop. A neighborhood is one statement
|
|
4
|
+
of the store primitive `lorebase_store.traverse.neighborhood` (a recursive walk with fan-out,
|
|
5
|
+
Link and node caps; hop by hop when it follows a path Derivation composed at read time). Both
|
|
6
|
+
read the materialized head, or at another Revision a snapshot of it (`reading.snapshots`).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from collections.abc import Mapping
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
15
|
+
|
|
16
|
+
from lorebase_core.model import Kind, LinkDirection
|
|
17
|
+
from lorebase_core.query import (
|
|
18
|
+
Condition,
|
|
19
|
+
Hop,
|
|
20
|
+
QueryError,
|
|
21
|
+
Target,
|
|
22
|
+
condition_kinds,
|
|
23
|
+
resolve_hop,
|
|
24
|
+
resolve_where,
|
|
25
|
+
)
|
|
26
|
+
from lorebase_keeper.reading.points import ReadPoint, not_found
|
|
27
|
+
from lorebase_keeper.reading.problems import invalid, query_problem
|
|
28
|
+
from lorebase_keeper.reading.records import compiler_for, get_record, require_kind
|
|
29
|
+
from lorebase_keeper.reading.requests import NeighborhoodRequest, TraverseRequest
|
|
30
|
+
from lorebase_keeper.reading.runtime import ReadRuntime
|
|
31
|
+
from lorebase_keeper.reading.sensitivity import condition_fields, require_comparable
|
|
32
|
+
from lorebase_keeper.reading.snapshots import with_rows
|
|
33
|
+
from lorebase_keeper.reading.views import (
|
|
34
|
+
Edge,
|
|
35
|
+
EdgeRef,
|
|
36
|
+
NeighborhoodResponse,
|
|
37
|
+
RecordOut,
|
|
38
|
+
RecordRef,
|
|
39
|
+
TraverseHopOut,
|
|
40
|
+
TraverseResponse,
|
|
41
|
+
link_values,
|
|
42
|
+
record_out,
|
|
43
|
+
record_ref,
|
|
44
|
+
served,
|
|
45
|
+
)
|
|
46
|
+
from lorebase_store.reads import Direction, hydrate_records
|
|
47
|
+
from lorebase_store.repositories.materialized import RecordRow
|
|
48
|
+
from lorebase_store.traverse import neighborhood as walk_neighborhood
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _edge(point: ReadPoint, hop: Hop, row: Mapping[Any, Any]) -> Edge:
|
|
52
|
+
return Edge(
|
|
53
|
+
relation=hop.name,
|
|
54
|
+
source=record_ref(point.schema, hop.near_kind, row["near_key"]),
|
|
55
|
+
to=record_ref(
|
|
56
|
+
point.schema, hop.far.kind_ids[0], row["far_key"], row["title"], point.masking
|
|
57
|
+
),
|
|
58
|
+
values=link_values(point.schema, hop.relation, row["link_values"], point.masking),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
async def traverse(
|
|
63
|
+
session: AsyncSession, runtime: ReadRuntime, point: ReadPoint, request: TraverseRequest
|
|
64
|
+
) -> TraverseResponse:
|
|
65
|
+
"""Follow `request.path` from the start Record; each hop keeps at most its limit. The
|
|
66
|
+
whole path is validated and authorized before any row is read."""
|
|
67
|
+
caps = runtime.read.traverse
|
|
68
|
+
if len(request.path) > caps.max_hops:
|
|
69
|
+
raise invalid("/body/path", f"a path has at most {caps.max_hops} steps", "limit_exceeded")
|
|
70
|
+
kind = require_kind(point, request.start.kind)
|
|
71
|
+
current = Target(kind.api_name, (kind,))
|
|
72
|
+
steps: list[tuple[Hop, int, Condition | None]] = []
|
|
73
|
+
for index, step in enumerate(request.path):
|
|
74
|
+
pointer = f"/body/path/{index}"
|
|
75
|
+
hop = resolve_hop(point.schema, current, step.relation)
|
|
76
|
+
if isinstance(hop, str):
|
|
77
|
+
raise invalid(f"{pointer}/relation", hop, "unknown_relation")
|
|
78
|
+
point.scope.require(hop.kinds_read)
|
|
79
|
+
limit = step.limit or caps.max_nodes_per_hop
|
|
80
|
+
if limit > caps.max_nodes_per_hop:
|
|
81
|
+
raise invalid(
|
|
82
|
+
f"{pointer}/limit", f"must not exceed {caps.max_nodes_per_hop}", "limit_exceeded"
|
|
83
|
+
)
|
|
84
|
+
try:
|
|
85
|
+
where = (
|
|
86
|
+
None
|
|
87
|
+
if step.where is None
|
|
88
|
+
else resolve_where(
|
|
89
|
+
step.where, hop.far, point.schema, runtime.read.query_limits(), "/where"
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
except QueryError as error:
|
|
93
|
+
raise query_problem(error, pointer) from None
|
|
94
|
+
point.scope.require(condition_kinds(where))
|
|
95
|
+
require_comparable(
|
|
96
|
+
point.schema, point.masking, condition_fields(where, hop.far.kinds), f"{pointer}/where"
|
|
97
|
+
)
|
|
98
|
+
steps.append((hop, limit, where))
|
|
99
|
+
current = hop.far
|
|
100
|
+
point = await with_rows(session, runtime, point, "Traversal")
|
|
101
|
+
start, _ = await get_record(
|
|
102
|
+
session, runtime, point, kind, request.start.key, links=False, lineage=False
|
|
103
|
+
)
|
|
104
|
+
compiler = compiler_for(point, runtime)
|
|
105
|
+
frontier = [request.start.key]
|
|
106
|
+
hops: list[TraverseHopOut] = []
|
|
107
|
+
for hop, limit, where in steps:
|
|
108
|
+
rows: list[Mapping[Any, Any]] = []
|
|
109
|
+
if frontier:
|
|
110
|
+
query = compiler.neighbors(hop, frontier, where=where, limit=limit + 1)
|
|
111
|
+
rows = list((await session.execute(query)).mappings())
|
|
112
|
+
truncated = len(rows) > limit
|
|
113
|
+
rows = rows[:limit]
|
|
114
|
+
reached: dict[str, RecordOut] = {}
|
|
115
|
+
for row in rows:
|
|
116
|
+
reached.setdefault(row["far_key"], record_out(point.schema, row, masking=point.masking))
|
|
117
|
+
hops.append(
|
|
118
|
+
TraverseHopOut(
|
|
119
|
+
relation=hop.name,
|
|
120
|
+
records=list(reached.values()),
|
|
121
|
+
edges=[_edge(point, hop, row) for row in rows],
|
|
122
|
+
truncated=truncated,
|
|
123
|
+
)
|
|
124
|
+
)
|
|
125
|
+
frontier = list(reached)
|
|
126
|
+
return TraverseResponse(**served(point), start=start, hops=hops)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
async def neighborhood(
|
|
130
|
+
session: AsyncSession, runtime: ReadRuntime, point: ReadPoint, request: NeighborhoodRequest
|
|
131
|
+
) -> NeighborhoodResponse:
|
|
132
|
+
"""Records and Links around the seeds, breadth first, within the depth and the caps.
|
|
133
|
+
|
|
134
|
+
The store primitive `lorebase_store.traverse.neighborhood`; caps are the smaller of
|
|
135
|
+
`read.traverse` and `store.traverse`. Nodes come nearest first; each Link is reported
|
|
136
|
+
once, by its forward name, from its `from` Record to its `to` Record. Path Derivations are
|
|
137
|
+
followed when named in `relations`. With `values: false` nodes are Record headers and
|
|
138
|
+
edges Link endpoints: the walk is the same, the response carries no values.
|
|
139
|
+
"""
|
|
140
|
+
caps, store = runtime.read.traverse, runtime.store_settings.traverse
|
|
141
|
+
max_seeds = min(caps.max_seeds, store.max_seeds)
|
|
142
|
+
if len(request.seeds) > max_seeds:
|
|
143
|
+
raise invalid("/body/seeds", f"at most {max_seeds} seeds", "limit_exceeded")
|
|
144
|
+
max_depth = min(caps.max_depth, store.max_depth)
|
|
145
|
+
if request.depth > max_depth:
|
|
146
|
+
raise invalid("/body/depth", f"must not exceed {max_depth}", "limit_exceeded")
|
|
147
|
+
nodes_cap = min(caps.max_nodes, store.max_nodes)
|
|
148
|
+
edges_cap = min(caps.max_edges, store.max_links)
|
|
149
|
+
max_nodes = request.max_nodes or nodes_cap
|
|
150
|
+
max_edges = request.max_edges or edges_cap
|
|
151
|
+
if max_nodes > nodes_cap:
|
|
152
|
+
raise invalid("/body/max_nodes", f"must not exceed {nodes_cap}", "limit_exceeded")
|
|
153
|
+
if max_edges > edges_cap:
|
|
154
|
+
raise invalid("/body/max_edges", f"must not exceed {edges_cap}", "limit_exceeded")
|
|
155
|
+
follow = _follow(point, _followed(point, request.relations))
|
|
156
|
+
seeds = [(require_kind(point, seed.kind).id, seed.key) for seed in request.seeds]
|
|
157
|
+
point = await with_rows(session, runtime, point, "Neighborhoods")
|
|
158
|
+
found = await hydrate_records(
|
|
159
|
+
session, point.domain.id, point.layout, seeds, runtime=runtime.store
|
|
160
|
+
)
|
|
161
|
+
for kind_id, key in seeds:
|
|
162
|
+
if (kind_id, key) not in found:
|
|
163
|
+
raise not_found(f"Record {point.schema.kinds[kind_id].api_name}/{key}")
|
|
164
|
+
|
|
165
|
+
walked = await walk_neighborhood(
|
|
166
|
+
session,
|
|
167
|
+
point.domain.id,
|
|
168
|
+
point.layout,
|
|
169
|
+
point.schema,
|
|
170
|
+
seeds,
|
|
171
|
+
depth=request.depth,
|
|
172
|
+
follow=follow,
|
|
173
|
+
max_nodes=max_nodes,
|
|
174
|
+
max_links=max_edges,
|
|
175
|
+
runtime=runtime.store,
|
|
176
|
+
)
|
|
177
|
+
titles = {(n.record.kind_id, n.record.key): n.record.title for n in walked.records}
|
|
178
|
+
nodes: list[RecordOut | RecordRef] = [
|
|
179
|
+
record_out(point.schema, _row(n.record), masking=point.masking)
|
|
180
|
+
if request.values
|
|
181
|
+
else record_ref(point.schema, n.record.kind_id, n.record.key, n.record.title, point.masking)
|
|
182
|
+
for n in walked.records
|
|
183
|
+
]
|
|
184
|
+
edges: list[Edge | EdgeRef] = []
|
|
185
|
+
for link in walked.links:
|
|
186
|
+
relation = point.schema.link_relation(link.relation_id)
|
|
187
|
+
assert relation is not None # the walk follows only Relations of the schema
|
|
188
|
+
source = record_ref(point.schema, relation.from_kind, link.from_key)
|
|
189
|
+
to = record_ref(
|
|
190
|
+
point.schema,
|
|
191
|
+
relation.to_kind,
|
|
192
|
+
link.to_key,
|
|
193
|
+
titles.get((relation.to_kind, link.to_key)),
|
|
194
|
+
point.masking,
|
|
195
|
+
)
|
|
196
|
+
if not request.values:
|
|
197
|
+
edges.append(EdgeRef(relation=relation.api_name, source=source, to=to))
|
|
198
|
+
continue
|
|
199
|
+
edges.append(
|
|
200
|
+
Edge(
|
|
201
|
+
relation=relation.api_name,
|
|
202
|
+
source=source,
|
|
203
|
+
to=to,
|
|
204
|
+
values=link_values(point.schema, relation, link.values, point.masking),
|
|
205
|
+
)
|
|
206
|
+
)
|
|
207
|
+
return NeighborhoodResponse(
|
|
208
|
+
**served(point), nodes=nodes, edges=edges, truncated=walked.truncated
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _row(record: RecordRow) -> dict[str, Any]:
|
|
213
|
+
"""A store Record row as the mapping `record_out` reads."""
|
|
214
|
+
return {
|
|
215
|
+
"kind_id": record.kind_id,
|
|
216
|
+
"key": record.key,
|
|
217
|
+
"title": record.title,
|
|
218
|
+
"values": record.values,
|
|
219
|
+
"updated_revision": record.updated_revision,
|
|
220
|
+
"updated_at": record.updated_at,
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _followed(point: ReadPoint, names: list[str] | None) -> set[tuple[str, LinkDirection]] | None:
|
|
225
|
+
if names is None:
|
|
226
|
+
return None
|
|
227
|
+
wanted: set[tuple[str, LinkDirection]] = set()
|
|
228
|
+
for index, name in enumerate(names):
|
|
229
|
+
found = point.schema.link_by_api_name(name)
|
|
230
|
+
if found is None:
|
|
231
|
+
raise invalid(f"/body/relations/{index}", f"unknown Relation {name!r}", "unknown")
|
|
232
|
+
relation, direction = found
|
|
233
|
+
wanted.add((relation.id, direction))
|
|
234
|
+
return wanted
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _follow(
|
|
238
|
+
point: ReadPoint, wanted: set[tuple[str, LinkDirection]] | None
|
|
239
|
+
) -> dict[str, Direction]:
|
|
240
|
+
"""Relations to walk (id to direction): those requested (every Relation by default, path
|
|
241
|
+
Derivations only when named) in the directions whose far Kind the caller may read; Kinds
|
|
242
|
+
the caller may not read are left out, and so are paths crossing one."""
|
|
243
|
+
schema = point.schema
|
|
244
|
+
paths = schema.path_relations()
|
|
245
|
+
relations = [
|
|
246
|
+
*schema.relations.values(),
|
|
247
|
+
*(path.relation for path in paths.values() if wanted is not None),
|
|
248
|
+
]
|
|
249
|
+
follow: dict[str, Direction] = {}
|
|
250
|
+
for relation in sorted(relations, key=lambda r: r.id):
|
|
251
|
+
path = paths.get(relation.id)
|
|
252
|
+
inner = () if path is None else path.inner_kinds
|
|
253
|
+
ways: list[Direction] = []
|
|
254
|
+
for direction in LinkDirection:
|
|
255
|
+
if wanted is not None and (relation.id, direction) not in wanted:
|
|
256
|
+
continue
|
|
257
|
+
far: Kind | None = schema.kinds.get(relation.far_endpoint(direction))
|
|
258
|
+
if far is None or not point.scope.allows(far.api_name):
|
|
259
|
+
continue
|
|
260
|
+
if not all(
|
|
261
|
+
(k := schema.kinds.get(kind_id)) is not None and point.scope.allows(k.api_name)
|
|
262
|
+
for kind_id in inner
|
|
263
|
+
):
|
|
264
|
+
continue
|
|
265
|
+
ways.append(Direction.OUT if direction is LinkDirection.FORWARD else Direction.IN)
|
|
266
|
+
if ways:
|
|
267
|
+
follow[relation.id] = Direction.BOTH if len(ways) == 2 else ways[0]
|
|
268
|
+
return follow
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Revisions, the log of a branch and per-partition counts (design 6.3, 6.4, 11.1).
|
|
2
|
+
|
|
3
|
+
The log walks the first-parent chain of the branch head (`all=true`: every ancestor) with a
|
|
4
|
+
recursive query, newest first by generation; pages continue with a `(generation, id)` keyset.
|
|
5
|
+
The walk covers the whole history on every page, which is fine for M1 history sizes.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections import defaultdict
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from pydantic import BaseModel
|
|
15
|
+
from sqlalchemy import LargeBinary, Table, cast, literal, or_, select, tuple_
|
|
16
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
17
|
+
|
|
18
|
+
from lorebase_core.model import Schema
|
|
19
|
+
from lorebase_keeper.reading.compiler import json_text
|
|
20
|
+
from lorebase_keeper.reading.diff import partition_name
|
|
21
|
+
from lorebase_keeper.reading.runtime import ReadRuntime
|
|
22
|
+
from lorebase_store.materialize.rows import PartitionType
|
|
23
|
+
from lorebase_store.models import metadata
|
|
24
|
+
from lorebase_store.nodestore import NodeRepository
|
|
25
|
+
from lorebase_store.repositories import RevisionRow
|
|
26
|
+
|
|
27
|
+
REVISIONS: Table = metadata.tables["revisions"]
|
|
28
|
+
REVISION_PARENTS: Table = metadata.tables["revision_parents"]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class RevisionOut(BaseModel):
|
|
32
|
+
id: str
|
|
33
|
+
parents: list[str]
|
|
34
|
+
root: str
|
|
35
|
+
generation: int
|
|
36
|
+
author: str
|
|
37
|
+
on_behalf_of: str | None = None
|
|
38
|
+
message: str
|
|
39
|
+
origin: dict[str, Any]
|
|
40
|
+
stats: dict[str, Any]
|
|
41
|
+
created_at: datetime
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class PartitionCount(BaseModel):
|
|
45
|
+
partition: str
|
|
46
|
+
element: PartitionType
|
|
47
|
+
name: str
|
|
48
|
+
count: int
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def revision_out(row: RevisionRow) -> RevisionOut:
|
|
52
|
+
return RevisionOut(
|
|
53
|
+
id=row.id.hex(),
|
|
54
|
+
parents=[p.hex() for p in row.parents],
|
|
55
|
+
root=row.root.hex(),
|
|
56
|
+
generation=row.generation,
|
|
57
|
+
author=row.author,
|
|
58
|
+
on_behalf_of=row.on_behalf_of,
|
|
59
|
+
message=row.message,
|
|
60
|
+
origin=row.origin,
|
|
61
|
+
stats=row.stats,
|
|
62
|
+
created_at=row.created_at,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
async def log(
|
|
67
|
+
session: AsyncSession,
|
|
68
|
+
domain_id: str,
|
|
69
|
+
head: bytes,
|
|
70
|
+
*,
|
|
71
|
+
first_parent: bool,
|
|
72
|
+
origin_type: str | None,
|
|
73
|
+
author: str | None,
|
|
74
|
+
limit: int,
|
|
75
|
+
after: tuple[int, bytes] | None,
|
|
76
|
+
) -> tuple[list[RevisionOut], bool]:
|
|
77
|
+
"""A page of the history of `head`, newest first; the flag tells whether more follow."""
|
|
78
|
+
rp = REVISION_PARENTS
|
|
79
|
+
chain = select(cast(literal(head, LargeBinary), LargeBinary).label("id")).cte(
|
|
80
|
+
"chain", recursive=True
|
|
81
|
+
)
|
|
82
|
+
step = select(rp.c.parent_id).where(rp.c.domain_id == domain_id, rp.c.revision_id == chain.c.id)
|
|
83
|
+
if first_parent:
|
|
84
|
+
step = step.where(rp.c.position == 0)
|
|
85
|
+
chain = chain.union(step)
|
|
86
|
+
r = REVISIONS
|
|
87
|
+
query = (
|
|
88
|
+
select(
|
|
89
|
+
r.c.id,
|
|
90
|
+
r.c.root,
|
|
91
|
+
r.c.generation,
|
|
92
|
+
r.c.author,
|
|
93
|
+
r.c.on_behalf_of,
|
|
94
|
+
r.c.message,
|
|
95
|
+
r.c.origin,
|
|
96
|
+
r.c.stats,
|
|
97
|
+
r.c.created_at,
|
|
98
|
+
)
|
|
99
|
+
.join(chain, r.c.id == chain.c.id)
|
|
100
|
+
.where(r.c.domain_id == domain_id)
|
|
101
|
+
.order_by(r.c.generation.desc(), r.c.id.desc())
|
|
102
|
+
.limit(limit + 1)
|
|
103
|
+
)
|
|
104
|
+
if origin_type is not None:
|
|
105
|
+
query = query.where(json_text(r.c.origin, "type") == origin_type)
|
|
106
|
+
if author is not None:
|
|
107
|
+
query = query.where(or_(r.c.author == author, r.c.on_behalf_of == author))
|
|
108
|
+
if after is not None:
|
|
109
|
+
generation, last = after
|
|
110
|
+
query = query.where(
|
|
111
|
+
tuple_(r.c.generation, r.c.id) < tuple_(literal(generation), literal(last, LargeBinary))
|
|
112
|
+
)
|
|
113
|
+
rows = list((await session.execute(query)).mappings())
|
|
114
|
+
more = len(rows) > limit
|
|
115
|
+
rows = rows[:limit]
|
|
116
|
+
parents: dict[bytes, list[bytes]] = defaultdict(list)
|
|
117
|
+
if rows:
|
|
118
|
+
result = await session.execute(
|
|
119
|
+
select(rp.c.revision_id, rp.c.parent_id)
|
|
120
|
+
.where(rp.c.domain_id == domain_id, rp.c.revision_id.in_([row["id"] for row in rows]))
|
|
121
|
+
.order_by(rp.c.revision_id, rp.c.position)
|
|
122
|
+
)
|
|
123
|
+
for revision, parent in result:
|
|
124
|
+
parents[revision].append(parent)
|
|
125
|
+
items = [revision_out(RevisionRow(parents=tuple(parents[row["id"]]), **row)) for row in rows]
|
|
126
|
+
return items, more
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
async def partition_counts(
|
|
130
|
+
session: AsyncSession,
|
|
131
|
+
runtime: ReadRuntime,
|
|
132
|
+
domain_id: str,
|
|
133
|
+
revision: RevisionRow,
|
|
134
|
+
schema: Schema,
|
|
135
|
+
) -> list[PartitionCount]:
|
|
136
|
+
"""Entries per partition at a Revision, read from the tree root in O(1)."""
|
|
137
|
+
settings = runtime.store_settings
|
|
138
|
+
nodes = NodeRepository(
|
|
139
|
+
session,
|
|
140
|
+
domain_id,
|
|
141
|
+
writes=settings.writes,
|
|
142
|
+
cache=runtime.store.node_cache,
|
|
143
|
+
batch=settings.nodes.prefetch_batch,
|
|
144
|
+
)
|
|
145
|
+
tree = await nodes.load_tree(revision.root)
|
|
146
|
+
counts = []
|
|
147
|
+
for partition, ref in sorted(tree.partitions.items()):
|
|
148
|
+
element, name = partition_name(partition, [schema])
|
|
149
|
+
counts.append(
|
|
150
|
+
PartitionCount(partition=partition, element=element, name=name, count=ref.count)
|
|
151
|
+
)
|
|
152
|
+
return counts
|