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,264 @@
|
|
|
1
|
+
"""Response shapes of the read API and the mapping from stored rows to them.
|
|
2
|
+
|
|
3
|
+
Responses use api names (design 8.1); values stay in canonical form (design 6.1): integers and
|
|
4
|
+
decimals are JSON strings, timestamps RFC 3339 UTC with six fractional digits. Values of
|
|
5
|
+
Attributes the schema no longer knows are left out; values of sensitive Attributes are masked
|
|
6
|
+
for callers without the clearance (`reading.sensitivity`).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from collections.abc import Mapping, Sequence
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from typing import Any, Literal
|
|
14
|
+
|
|
15
|
+
from pydantic import BaseModel, Field, JsonValue
|
|
16
|
+
|
|
17
|
+
from lorebase_core.model import LinkDirection, Relation, Schema
|
|
18
|
+
from lorebase_core.query import FieldRef, FieldSource
|
|
19
|
+
from lorebase_keeper.reading.points import ReadPoint, ServedFrom
|
|
20
|
+
from lorebase_keeper.reading.sensitivity import Masking
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Served(BaseModel):
|
|
24
|
+
"""Where a response was read: Revision id and materialized head or tree."""
|
|
25
|
+
|
|
26
|
+
revision: str = Field(description="Revision the response was served from (hex).")
|
|
27
|
+
served_from: ServedFrom
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class LinkCountOut(BaseModel):
|
|
31
|
+
relation: str = Field(description="Relation api name in the direction seen from the Record.")
|
|
32
|
+
direction: LinkDirection
|
|
33
|
+
count: int
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class LineageOut(BaseModel):
|
|
37
|
+
provenance_id: str
|
|
38
|
+
origin_type: str | None = None
|
|
39
|
+
actor: str | None = None
|
|
40
|
+
at: datetime | None = None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class RecordOut(BaseModel):
|
|
44
|
+
"""A Record with values keyed by Attribute api name."""
|
|
45
|
+
|
|
46
|
+
kind: str
|
|
47
|
+
key: str
|
|
48
|
+
title: str | None = None
|
|
49
|
+
values: dict[str, JsonValue]
|
|
50
|
+
updated_revision: str | None = None
|
|
51
|
+
updated_at: datetime | None = None
|
|
52
|
+
links: list[LinkCountOut] | None = None
|
|
53
|
+
lineage: dict[str, LineageOut] | None = None
|
|
54
|
+
included: dict[str, list[RecordOut]] | None = None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class RecordResponse(Served):
|
|
58
|
+
record: RecordOut
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class RecordPage(Served):
|
|
62
|
+
items: list[RecordOut]
|
|
63
|
+
next_cursor: str | None = None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class AggregateCount(BaseModel):
|
|
67
|
+
kind: str
|
|
68
|
+
value: JsonValue = Field(default=None, description="Group value when `group_by` is given.")
|
|
69
|
+
count: int
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class AggregateOut(BaseModel):
|
|
73
|
+
counts: list[AggregateCount]
|
|
74
|
+
truncated: bool = False
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class QueryResponse(RecordPage):
|
|
78
|
+
aggregate: AggregateOut | None = None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class LinkOut(BaseModel):
|
|
82
|
+
"""A Link seen from one side: `relation` is the api name in that direction."""
|
|
83
|
+
|
|
84
|
+
relation: str
|
|
85
|
+
direction: LinkDirection
|
|
86
|
+
from_key: str
|
|
87
|
+
to_key: str
|
|
88
|
+
values: dict[str, JsonValue]
|
|
89
|
+
target: RecordRef | None = Field(
|
|
90
|
+
default=None, description="The Record at the other end, as seen from the source."
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class RecordRef(BaseModel):
|
|
95
|
+
kind: str
|
|
96
|
+
key: str
|
|
97
|
+
title: str | None = None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class LinkPage(Served):
|
|
101
|
+
items: list[LinkOut]
|
|
102
|
+
next_cursor: str | None = None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class Edge(BaseModel):
|
|
106
|
+
relation: str
|
|
107
|
+
source: RecordRef = Field(alias="from")
|
|
108
|
+
to: RecordRef
|
|
109
|
+
values: dict[str, JsonValue]
|
|
110
|
+
|
|
111
|
+
model_config = {"populate_by_name": True, "serialize_by_alias": True}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class EdgeRef(BaseModel):
|
|
115
|
+
"""A Link by its endpoints only (`values: false`)."""
|
|
116
|
+
|
|
117
|
+
relation: str
|
|
118
|
+
source: RecordRef = Field(alias="from")
|
|
119
|
+
to: RecordRef
|
|
120
|
+
|
|
121
|
+
model_config = {"populate_by_name": True, "serialize_by_alias": True}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class TraverseHopOut(BaseModel):
|
|
125
|
+
relation: str
|
|
126
|
+
records: list[RecordOut]
|
|
127
|
+
edges: list[Edge]
|
|
128
|
+
truncated: bool = False
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class TraverseResponse(Served):
|
|
132
|
+
start: RecordOut
|
|
133
|
+
hops: list[TraverseHopOut]
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class NeighborhoodResponse(Served):
|
|
137
|
+
nodes: list[RecordOut | RecordRef] = Field(
|
|
138
|
+
description="Records reached, nearest first: headers only (`RecordRef`) when the "
|
|
139
|
+
"request has `values: false`."
|
|
140
|
+
)
|
|
141
|
+
edges: list[Edge | EdgeRef] = Field(
|
|
142
|
+
description="Links between the nodes: endpoints only (`EdgeRef`) when the request has "
|
|
143
|
+
"`values: false`."
|
|
144
|
+
)
|
|
145
|
+
truncated: bool = False
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class SearchHit(BaseModel):
|
|
149
|
+
kind: str
|
|
150
|
+
key: str
|
|
151
|
+
title: str | None = None
|
|
152
|
+
snippet: str | None = None
|
|
153
|
+
score: float
|
|
154
|
+
match: Literal["all", "partial"] | None = Field(
|
|
155
|
+
default=None,
|
|
156
|
+
description="How the hit matched q: every term (`all`), or only some of them "
|
|
157
|
+
"(`partial`, ranked after every `all` hit); absent for hits found by vector only.",
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class SearchCorrection(BaseModel):
|
|
162
|
+
"""A query word the branch does not hold and the words searched in its place as well.
|
|
163
|
+
|
|
164
|
+
Words are lexemes of the text search configuration (lowercased, stemmed by its
|
|
165
|
+
dictionaries), as full-text search compares them.
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
word: str
|
|
169
|
+
replacements: list[str]
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class SearchResponse(Served):
|
|
173
|
+
mode: str
|
|
174
|
+
items: list[SearchHit]
|
|
175
|
+
corrections: list[SearchCorrection] = Field(
|
|
176
|
+
default_factory=list,
|
|
177
|
+
description="Typo corrections applied to q; hits found only through them come after "
|
|
178
|
+
"the others.",
|
|
179
|
+
)
|
|
180
|
+
truncated_candidates: bool = Field(
|
|
181
|
+
default=False,
|
|
182
|
+
description="Only a sample of the matches of q (the first ones in index order) was "
|
|
183
|
+
"ranked, as happens when every term of q is very common: better matches may exist. "
|
|
184
|
+
"Add a rarer term, a Kind or a filter to narrow q.",
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# Rendering ----------------------------------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def kind_name(schema: Schema, kind_id: str) -> str:
|
|
192
|
+
kind = schema.kinds.get(kind_id)
|
|
193
|
+
return kind.api_name if kind is not None else kind_id
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def relation_name(relation: Relation, direction: LinkDirection) -> str:
|
|
197
|
+
return relation.api_name if direction is LinkDirection.FORWARD else relation.inverse_api_name
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def record_out(
|
|
201
|
+
schema: Schema,
|
|
202
|
+
row: Mapping[Any, Any],
|
|
203
|
+
*,
|
|
204
|
+
masking: Masking,
|
|
205
|
+
select: Sequence[FieldRef] | None = None,
|
|
206
|
+
) -> RecordOut:
|
|
207
|
+
"""API form of a materialized row (`record_columns`) or a tree Record, with the values the
|
|
208
|
+
caller is not cleared for masked."""
|
|
209
|
+
kind_id = row["kind_id"]
|
|
210
|
+
kind = schema.kinds.get(kind_id)
|
|
211
|
+
values: Mapping[str, Any] = row.get("values") or {}
|
|
212
|
+
derived: Mapping[str, Any] = row.get("derived") or {}
|
|
213
|
+
rendered = {} if kind is None else masking.kind_values(kind, values, derived=derived)
|
|
214
|
+
if select is not None:
|
|
215
|
+
wanted = _selected(select, kind_id, kind.attributes if kind is not None else {})
|
|
216
|
+
rendered = {name: value for name, value in rendered.items() if name in wanted}
|
|
217
|
+
updated = row.get("updated_revision")
|
|
218
|
+
return RecordOut(
|
|
219
|
+
kind=kind.api_name if kind is not None else kind_id,
|
|
220
|
+
key=row["key"],
|
|
221
|
+
title=masking.title(schema, kind_id, row.get("title")),
|
|
222
|
+
values=rendered,
|
|
223
|
+
updated_revision=None if updated is None else bytes(updated).hex(),
|
|
224
|
+
updated_at=row.get("updated_at"),
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _selected(select: Sequence[FieldRef], kind_id: str, attributes: Mapping[str, Any]) -> set[str]:
|
|
229
|
+
wanted: set[str] = set()
|
|
230
|
+
for ref in select:
|
|
231
|
+
if ref.source in (FieldSource.KEY, FieldSource.TITLE):
|
|
232
|
+
continue
|
|
233
|
+
attribute_id = ref.attributes.get(kind_id)
|
|
234
|
+
attribute = attributes.get(attribute_id) if attribute_id is not None else None
|
|
235
|
+
if attribute is not None:
|
|
236
|
+
wanted.add(attribute.api_name)
|
|
237
|
+
return wanted
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def record_ref(
|
|
241
|
+
schema: Schema,
|
|
242
|
+
kind_id: str,
|
|
243
|
+
key: str,
|
|
244
|
+
title: str | None = None,
|
|
245
|
+
masking: Masking | None = None,
|
|
246
|
+
) -> RecordRef:
|
|
247
|
+
"""A Record reference; a title needs `masking` to be shown."""
|
|
248
|
+
shown = None if masking is None else masking.title(schema, kind_id, title)
|
|
249
|
+
return RecordRef(kind=kind_name(schema, kind_id), key=key, title=shown)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def link_values(
|
|
253
|
+
schema: Schema, relation: Relation, values: Mapping[str, Any] | None, masking: Masking
|
|
254
|
+
) -> dict[str, Any]:
|
|
255
|
+
"""Link Attribute values keyed by api name, masked for the caller."""
|
|
256
|
+
return masking.link_values(schema, relation, values)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def served(point: ReadPoint) -> dict[str, Any]:
|
|
260
|
+
return {"revision": point.revision_hex, "served_from": point.served_from}
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
RecordOut.model_rebuild()
|
|
264
|
+
LinkOut.model_rebuild()
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Kind-scoped invisibility (design 13.3): the schema a caller limited to some Kinds reads.
|
|
2
|
+
|
|
3
|
+
A caller whose grants are narrowed to Kinds reads the projection of the schema to those Kinds
|
|
4
|
+
(`lorebase_core.expr.visibility.visible_schema`): names are resolved against it, so a Kind
|
|
5
|
+
the caller may not read, and everything that would tell about it, fails like a name that
|
|
6
|
+
does not exist (`keeper.auth.authz.invisible_kinds`).
|
|
7
|
+
|
|
8
|
+
Projections are remembered per schema object and Kinds (compiled schemas are cached, so the
|
|
9
|
+
same object comes back for the same schema), like the effective sensitivity tags.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import re
|
|
16
|
+
from collections import OrderedDict
|
|
17
|
+
from collections.abc import Collection
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from threading import Lock
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
from lorebase_core.expr.visibility import element_ids, visible_schema
|
|
23
|
+
from lorebase_core.model import Schema
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True, slots=True)
|
|
27
|
+
class Projection:
|
|
28
|
+
"""A schema as a reader of some Kinds sees it, the ids it leaves out and the names of the
|
|
29
|
+
elements it leaves out (Kinds, Relations both ways, Traits, Operations, Derivations,
|
|
30
|
+
Sources)."""
|
|
31
|
+
|
|
32
|
+
schema: Schema
|
|
33
|
+
hidden: frozenset[str]
|
|
34
|
+
names: frozenset[str] = frozenset()
|
|
35
|
+
|
|
36
|
+
def tells(self, value: Any) -> bool:
|
|
37
|
+
"""Whether `value` (any JSON-like value: a Revision message, an origin document)
|
|
38
|
+
holds an id or, as a word, a name left out."""
|
|
39
|
+
return tells(value, self.hidden, self.names)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
_WORD = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def tells(value: Any, hidden: Collection[str], names: Collection[str]) -> bool:
|
|
46
|
+
"""Whether `value` holds one of the ids `hidden` or, as a word, one of `names`."""
|
|
47
|
+
if not hidden or value is None:
|
|
48
|
+
return False
|
|
49
|
+
text = value if isinstance(value, str) else json.dumps(value, default=str)
|
|
50
|
+
return any(i in text for i in hidden) or not set(_WORD.findall(text)).isdisjoint(names)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _names(schema: Schema, hidden: frozenset[str]) -> frozenset[str]:
|
|
54
|
+
names: set[str] = set()
|
|
55
|
+
for element in schema.elements():
|
|
56
|
+
if element.id not in hidden:
|
|
57
|
+
continue
|
|
58
|
+
names.add(element.api_name)
|
|
59
|
+
inverse = getattr(element, "inverse_api_name", None)
|
|
60
|
+
definition = getattr(element, "definition", None)
|
|
61
|
+
inverse = inverse or getattr(definition, "inverse_api_name", None)
|
|
62
|
+
if isinstance(inverse, str) and inverse:
|
|
63
|
+
names.add(inverse)
|
|
64
|
+
return frozenset(n for n in names if n)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
_Key = tuple[int, frozenset[str]]
|
|
68
|
+
_VISIBLE: OrderedDict[_Key, tuple[Schema, Projection]] = OrderedDict()
|
|
69
|
+
_VISIBLE_ENTRIES = 256
|
|
70
|
+
_VISIBLE_LOCK = Lock()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def projection(schema: Schema, kinds: frozenset[str]) -> Projection:
|
|
74
|
+
"""`schema` as a reader of the Kinds `kinds` (api names) sees it."""
|
|
75
|
+
key = (id(schema), kinds)
|
|
76
|
+
with _VISIBLE_LOCK:
|
|
77
|
+
found = _VISIBLE.get(key)
|
|
78
|
+
if found is not None and found[0] is schema:
|
|
79
|
+
_VISIBLE.move_to_end(key)
|
|
80
|
+
return found[1]
|
|
81
|
+
seen = visible_schema(schema, kinds)
|
|
82
|
+
hidden = frozenset() if seen is schema else frozenset(element_ids(schema) - element_ids(seen))
|
|
83
|
+
projected = Projection(seen, hidden, _names(schema, hidden))
|
|
84
|
+
with _VISIBLE_LOCK:
|
|
85
|
+
_VISIBLE[key] = (schema, projected)
|
|
86
|
+
while len(_VISIBLE) > _VISIBLE_ENTRIES:
|
|
87
|
+
_VISIBLE.popitem(last=False)
|
|
88
|
+
return projected
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def visible(schema: Schema, kinds: frozenset[str]) -> Schema:
|
|
92
|
+
"""`schema` as a reader of the Kinds `kinds` sees it (`projection`)."""
|
|
93
|
+
return projection(schema, kinds).schema
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""HTTP routes."""
|