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