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,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."""