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,196 @@
|
|
|
1
|
+
"""Problem types of the write endpoints and the mapping of store failures to them.
|
|
2
|
+
|
|
3
|
+
Store errors become problem details with stable codes:
|
|
4
|
+
|
|
5
|
+
| Failure | Status | Code | Extra members |
|
|
6
|
+
|---------|--------|------|---------------|
|
|
7
|
+
| `StaleHeadError` | 409 | `stale-head` | `branch`, `expected_head`, `current_head` |
|
|
8
|
+
| `BranchLockTimeoutError` | 503 | `branch-busy` | `Retry-After` header |
|
|
9
|
+
| `ValidationFailedError` | 422 | `validation-failed` | `issues` |
|
|
10
|
+
| `SchemaChangeError` | 422 | `schema-change-invalid` | |
|
|
11
|
+
| `BranchNotFoundError` | 404 | `not-found` | |
|
|
12
|
+
| `BranchNotMaterializedError` | 409 | `branch-not-materialized` | |
|
|
13
|
+
| `BranchMaterializingError` | 409 | `branch-materializing` | `Retry-After` header |
|
|
14
|
+
|
|
15
|
+
A lock timeout is transient (another writer holds the branch), so it is a 503 with a retry
|
|
16
|
+
hint rather than 423, which clients rarely handle.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from collections.abc import Callable, Iterable, Sequence
|
|
22
|
+
from dataclasses import dataclass, field
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
from lorebase_core.canonical import hex
|
|
26
|
+
from lorebase_core.validate import Issue
|
|
27
|
+
from lorebase_keeper.errors import (
|
|
28
|
+
FORBIDDEN,
|
|
29
|
+
NOT_FOUND,
|
|
30
|
+
VALIDATION_FAILED,
|
|
31
|
+
ProblemError,
|
|
32
|
+
ProblemType,
|
|
33
|
+
)
|
|
34
|
+
from lorebase_store.commit import (
|
|
35
|
+
BranchLockTimeoutError,
|
|
36
|
+
BranchMaterializingError,
|
|
37
|
+
BranchNotFoundError,
|
|
38
|
+
BranchNotMaterializedError,
|
|
39
|
+
CommitError,
|
|
40
|
+
SchemaChangeError,
|
|
41
|
+
SensitivityLoweredError,
|
|
42
|
+
StaleHeadError,
|
|
43
|
+
ValidationFailedError,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
STALE_HEAD = ProblemType("stale-head", 409, "Branch head moved")
|
|
47
|
+
BRANCH_BUSY = ProblemType("branch-busy", 503, "Branch is busy")
|
|
48
|
+
BRANCH_NOT_MATERIALIZED = ProblemType("branch-not-materialized", 409, "Branch not materialized")
|
|
49
|
+
BRANCH_MATERIALIZING = ProblemType("branch-materializing", 409, "Branch still being built")
|
|
50
|
+
SCHEMA_CHANGE_INVALID = ProblemType("schema-change-invalid", 422, "Schema change not applicable")
|
|
51
|
+
PROTECTED_BRANCH = ProblemType("protected-branch", 409, "Branch is protected")
|
|
52
|
+
REVIEW_REQUIRED = ProblemType("review-required", 409, "Review required")
|
|
53
|
+
EXPRESSIONS_NOT_ENABLED = ProblemType("expressions-not-enabled", 422, "Expressions not enabled")
|
|
54
|
+
PRECONDITION_FAILED = ProblemType("precondition-failed", 422, "Precondition failed")
|
|
55
|
+
ALREADY_EXISTS = ProblemType("already-exists", 409, "Already exists")
|
|
56
|
+
IDEMPOTENCY_CONFLICT = ProblemType("idempotency-conflict", 409, "Idempotency key reused")
|
|
57
|
+
LIMIT_EXCEEDED = ProblemType("limit-exceeded", 409, "Limit exceeded")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True)
|
|
61
|
+
class Finding:
|
|
62
|
+
"""A validation finding as the API reports it: a core `Issue` or a document problem.
|
|
63
|
+
|
|
64
|
+
`path` locates it in the request (schema document paths use api names).
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
code: str
|
|
68
|
+
message: str
|
|
69
|
+
path: tuple[str | int, ...] = ()
|
|
70
|
+
severity: str = "error"
|
|
71
|
+
kind: str | None = None
|
|
72
|
+
relation: str | None = None
|
|
73
|
+
key: str | None = None
|
|
74
|
+
attribute: str | None = None
|
|
75
|
+
details: dict[str, Any] = field(default_factory=dict)
|
|
76
|
+
|
|
77
|
+
@classmethod
|
|
78
|
+
def of(cls, issue: Issue) -> Finding:
|
|
79
|
+
return cls(
|
|
80
|
+
code=issue.code.value,
|
|
81
|
+
message=issue.message,
|
|
82
|
+
path=tuple(issue.path),
|
|
83
|
+
severity=issue.severity.value,
|
|
84
|
+
kind=issue.kind,
|
|
85
|
+
relation=issue.relation,
|
|
86
|
+
key=issue.key,
|
|
87
|
+
attribute=issue.attribute,
|
|
88
|
+
details=dict(issue.details),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def document(self) -> dict[str, Any]:
|
|
92
|
+
"""JSON form; absent references are omitted."""
|
|
93
|
+
body: dict[str, Any] = {
|
|
94
|
+
"code": self.code,
|
|
95
|
+
"severity": self.severity,
|
|
96
|
+
"path": list(self.path),
|
|
97
|
+
"message": self.message,
|
|
98
|
+
}
|
|
99
|
+
for name in ("kind", "relation", "key", "attribute"):
|
|
100
|
+
value = getattr(self, name)
|
|
101
|
+
if value is not None:
|
|
102
|
+
body[name] = value
|
|
103
|
+
if self.details:
|
|
104
|
+
body["details"] = self.details
|
|
105
|
+
return body
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def findings_of(issues: Iterable[Issue]) -> list[Finding]:
|
|
109
|
+
return [Finding.of(issue) for issue in issues]
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def finding_documents(findings: Iterable[Finding], limit: int) -> list[dict[str, Any]]:
|
|
113
|
+
"""JSON form of the first `limit` findings."""
|
|
114
|
+
return [finding.document() for _, finding in zip(range(limit), findings, strict=False)]
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def violations_problem(
|
|
118
|
+
findings: Sequence[Finding], *, limit: int, extensions: dict[str, Any] | None = None
|
|
119
|
+
) -> ProblemError:
|
|
120
|
+
"""422 listing validation findings."""
|
|
121
|
+
return ProblemError(
|
|
122
|
+
VALIDATION_FAILED,
|
|
123
|
+
f"{len(findings)} validation issue(s); nothing was written.",
|
|
124
|
+
extensions={
|
|
125
|
+
"issues": finding_documents(findings, limit),
|
|
126
|
+
"issue_count": len(findings),
|
|
127
|
+
**(extensions or {}),
|
|
128
|
+
},
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class ProtectedBranchError(Exception):
|
|
133
|
+
"""A direct write to a protected branch; it must go through a Proposal."""
|
|
134
|
+
|
|
135
|
+
def __init__(self, branch: str) -> None:
|
|
136
|
+
super().__init__(f"branch {branch!r} is protected")
|
|
137
|
+
self.branch = branch
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def protected_branch_problem(domain_id: str, branch: str, proposals_path: str) -> ProblemError:
|
|
141
|
+
"""409 for direct writes to a protected branch."""
|
|
142
|
+
return ProblemError(
|
|
143
|
+
PROTECTED_BRANCH,
|
|
144
|
+
f"Branch {branch!r} is protected: changes reach it through a Proposal. Commit to "
|
|
145
|
+
"another branch and open a Proposal.",
|
|
146
|
+
extensions={"domain": domain_id, "branch": branch, "proposals": proposals_path},
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def commit_problem(
|
|
151
|
+
error: CommitError,
|
|
152
|
+
*,
|
|
153
|
+
max_issues: int,
|
|
154
|
+
retry_after_s: int,
|
|
155
|
+
locate: Callable[[Sequence[Issue]], Sequence[Finding]] = findings_of,
|
|
156
|
+
) -> ProblemError:
|
|
157
|
+
"""The problem detail answering a commit failure.
|
|
158
|
+
|
|
159
|
+
`locate`, when given, rewrites validation issues (for example into YAML paths).
|
|
160
|
+
"""
|
|
161
|
+
if isinstance(error, StaleHeadError):
|
|
162
|
+
return ProblemError(
|
|
163
|
+
STALE_HEAD,
|
|
164
|
+
f"Branch {error.branch!r} moved; read the current head and retry.",
|
|
165
|
+
extensions={
|
|
166
|
+
"branch": error.branch,
|
|
167
|
+
"expected_head": hex(error.expected),
|
|
168
|
+
"current_head": None if error.actual is None else hex(error.actual),
|
|
169
|
+
},
|
|
170
|
+
)
|
|
171
|
+
if isinstance(error, BranchLockTimeoutError):
|
|
172
|
+
return ProblemError(
|
|
173
|
+
BRANCH_BUSY,
|
|
174
|
+
"Another writer holds the branch; retry later.",
|
|
175
|
+
headers={"Retry-After": str(retry_after_s)},
|
|
176
|
+
)
|
|
177
|
+
if isinstance(error, ValidationFailedError):
|
|
178
|
+
return violations_problem(locate(error.issues), limit=max_issues)
|
|
179
|
+
if isinstance(error, SensitivityLoweredError):
|
|
180
|
+
return ProblemError(
|
|
181
|
+
FORBIDDEN,
|
|
182
|
+
f"The change removes the sensitivity tags {', '.join(error.tags)}; only a caller "
|
|
183
|
+
"cleared for them on the Domain may.",
|
|
184
|
+
extensions={"code": "sensitivity_lowered", "tags": error.tags},
|
|
185
|
+
)
|
|
186
|
+
if isinstance(error, SchemaChangeError):
|
|
187
|
+
return ProblemError(SCHEMA_CHANGE_INVALID, str(error))
|
|
188
|
+
if isinstance(error, BranchNotFoundError):
|
|
189
|
+
return ProblemError(NOT_FOUND, str(error))
|
|
190
|
+
if isinstance(error, BranchNotMaterializedError):
|
|
191
|
+
return ProblemError(BRANCH_NOT_MATERIALIZED, str(error))
|
|
192
|
+
if isinstance(error, BranchMaterializingError):
|
|
193
|
+
return ProblemError(
|
|
194
|
+
BRANCH_MATERIALIZING, str(error), headers={"Retry-After": str(retry_after_s)}
|
|
195
|
+
)
|
|
196
|
+
raise error
|