lorebase-keeper 0.2.3__tar.gz

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 (237) hide show
  1. lorebase_keeper-0.2.3/.gitignore +79 -0
  2. lorebase_keeper-0.2.3/PKG-INFO +21 -0
  3. lorebase_keeper-0.2.3/pyproject.toml +32 -0
  4. lorebase_keeper-0.2.3/src/lorebase_keeper/__init__.py +1 -0
  5. lorebase_keeper-0.2.3/src/lorebase_keeper/__main__.py +456 -0
  6. lorebase_keeper-0.2.3/src/lorebase_keeper/agents/__init__.py +1 -0
  7. lorebase_keeper-0.2.3/src/lorebase_keeper/agents/budget.py +247 -0
  8. lorebase_keeper-0.2.3/src/lorebase_keeper/agents/context.py +529 -0
  9. lorebase_keeper-0.2.3/src/lorebase_keeper/agents/explain.py +582 -0
  10. lorebase_keeper-0.2.3/src/lorebase_keeper/agents/feed.py +259 -0
  11. lorebase_keeper-0.2.3/src/lorebase_keeper/agents/markdown.py +219 -0
  12. lorebase_keeper-0.2.3/src/lorebase_keeper/agents/pack.py +403 -0
  13. lorebase_keeper-0.2.3/src/lorebase_keeper/agents/settings.py +111 -0
  14. lorebase_keeper-0.2.3/src/lorebase_keeper/app.py +373 -0
  15. lorebase_keeper-0.2.3/src/lorebase_keeper/auth/__init__.py +4 -0
  16. lorebase_keeper-0.2.3/src/lorebase_keeper/auth/api_keys.py +127 -0
  17. lorebase_keeper-0.2.3/src/lorebase_keeper/auth/audit.py +356 -0
  18. lorebase_keeper-0.2.3/src/lorebase_keeper/auth/cache.py +53 -0
  19. lorebase_keeper-0.2.3/src/lorebase_keeper/auth/config.py +618 -0
  20. lorebase_keeper-0.2.3/src/lorebase_keeper/auth/deps.py +183 -0
  21. lorebase_keeper-0.2.3/src/lorebase_keeper/auth/keyring.py +14 -0
  22. lorebase_keeper-0.2.3/src/lorebase_keeper/auth/oidc.py +242 -0
  23. lorebase_keeper-0.2.3/src/lorebase_keeper/auth/permissions.py +142 -0
  24. lorebase_keeper-0.2.3/src/lorebase_keeper/auth/ports.py +212 -0
  25. lorebase_keeper-0.2.3/src/lorebase_keeper/auth/principals.py +377 -0
  26. lorebase_keeper-0.2.3/src/lorebase_keeper/auth/provider.py +250 -0
  27. lorebase_keeper-0.2.3/src/lorebase_keeper/auth/rbac.py +247 -0
  28. lorebase_keeper-0.2.3/src/lorebase_keeper/auth/read_audit.py +357 -0
  29. lorebase_keeper-0.2.3/src/lorebase_keeper/auth/runtime.py +113 -0
  30. lorebase_keeper-0.2.3/src/lorebase_keeper/auth/sessions.py +452 -0
  31. lorebase_keeper-0.2.3/src/lorebase_keeper/bootstrap.py +151 -0
  32. lorebase_keeper-0.2.3/src/lorebase_keeper/client_address.py +129 -0
  33. lorebase_keeper-0.2.3/src/lorebase_keeper/context.py +226 -0
  34. lorebase_keeper-0.2.3/src/lorebase_keeper/db.py +93 -0
  35. lorebase_keeper-0.2.3/src/lorebase_keeper/deps.py +47 -0
  36. lorebase_keeper-0.2.3/src/lorebase_keeper/errors.py +257 -0
  37. lorebase_keeper-0.2.3/src/lorebase_keeper/health.py +61 -0
  38. lorebase_keeper-0.2.3/src/lorebase_keeper/kindhealth/__init__.py +6 -0
  39. lorebase_keeper-0.2.3/src/lorebase_keeper/kindhealth/config.py +51 -0
  40. lorebase_keeper-0.2.3/src/lorebase_keeper/kindhealth/markdown.py +163 -0
  41. lorebase_keeper-0.2.3/src/lorebase_keeper/kindhealth/models.py +179 -0
  42. lorebase_keeper-0.2.3/src/lorebase_keeper/kindhealth/report.py +154 -0
  43. lorebase_keeper-0.2.3/src/lorebase_keeper/kindhealth/scan.py +512 -0
  44. lorebase_keeper-0.2.3/src/lorebase_keeper/middleware.py +285 -0
  45. lorebase_keeper-0.2.3/src/lorebase_keeper/pagination.py +87 -0
  46. lorebase_keeper-0.2.3/src/lorebase_keeper/ratelimit/__init__.py +10 -0
  47. lorebase_keeper-0.2.3/src/lorebase_keeper/ratelimit/bucket.py +118 -0
  48. lorebase_keeper-0.2.3/src/lorebase_keeper/ratelimit/config.py +283 -0
  49. lorebase_keeper-0.2.3/src/lorebase_keeper/ratelimit/enforce.py +393 -0
  50. lorebase_keeper-0.2.3/src/lorebase_keeper/ratelimit/limiter.py +149 -0
  51. lorebase_keeper-0.2.3/src/lorebase_keeper/ratelimit/quotas.py +204 -0
  52. lorebase_keeper-0.2.3/src/lorebase_keeper/ratelimit/shared.py +242 -0
  53. lorebase_keeper-0.2.3/src/lorebase_keeper/readiness.py +257 -0
  54. lorebase_keeper-0.2.3/src/lorebase_keeper/reading/__init__.py +1 -0
  55. lorebase_keeper-0.2.3/src/lorebase_keeper/reading/caching.py +251 -0
  56. lorebase_keeper-0.2.3/src/lorebase_keeper/reading/compiler.py +678 -0
  57. lorebase_keeper-0.2.3/src/lorebase_keeper/reading/cursors.py +56 -0
  58. lorebase_keeper-0.2.3/src/lorebase_keeper/reading/diff.py +408 -0
  59. lorebase_keeper-0.2.3/src/lorebase_keeper/reading/graph.py +268 -0
  60. lorebase_keeper-0.2.3/src/lorebase_keeper/reading/history.py +152 -0
  61. lorebase_keeper-0.2.3/src/lorebase_keeper/reading/points.py +410 -0
  62. lorebase_keeper-0.2.3/src/lorebase_keeper/reading/problems.py +74 -0
  63. lorebase_keeper-0.2.3/src/lorebase_keeper/reading/record_history.py +722 -0
  64. lorebase_keeper-0.2.3/src/lorebase_keeper/reading/records.py +780 -0
  65. lorebase_keeper-0.2.3/src/lorebase_keeper/reading/requests.py +107 -0
  66. lorebase_keeper-0.2.3/src/lorebase_keeper/reading/runtime.py +145 -0
  67. lorebase_keeper-0.2.3/src/lorebase_keeper/reading/search.py +708 -0
  68. lorebase_keeper-0.2.3/src/lorebase_keeper/reading/sensitivity.py +882 -0
  69. lorebase_keeper-0.2.3/src/lorebase_keeper/reading/settings.py +339 -0
  70. lorebase_keeper-0.2.3/src/lorebase_keeper/reading/snapshots.py +148 -0
  71. lorebase_keeper-0.2.3/src/lorebase_keeper/reading/tree.py +98 -0
  72. lorebase_keeper-0.2.3/src/lorebase_keeper/reading/views.py +264 -0
  73. lorebase_keeper-0.2.3/src/lorebase_keeper/reading/visibility.py +93 -0
  74. lorebase_keeper-0.2.3/src/lorebase_keeper/routes/__init__.py +1 -0
  75. lorebase_keeper-0.2.3/src/lorebase_keeper/routes/admin.py +597 -0
  76. lorebase_keeper-0.2.3/src/lorebase_keeper/routes/agents.py +160 -0
  77. lorebase_keeper-0.2.3/src/lorebase_keeper/routes/branches.py +438 -0
  78. lorebase_keeper-0.2.3/src/lorebase_keeper/routes/derivations.py +215 -0
  79. lorebase_keeper-0.2.3/src/lorebase_keeper/routes/documents.py +497 -0
  80. lorebase_keeper-0.2.3/src/lorebase_keeper/routes/domain_api.py +59 -0
  81. lorebase_keeper-0.2.3/src/lorebase_keeper/routes/domains.py +220 -0
  82. lorebase_keeper-0.2.3/src/lorebase_keeper/routes/events.py +187 -0
  83. lorebase_keeper-0.2.3/src/lorebase_keeper/routes/kind_health.py +166 -0
  84. lorebase_keeper-0.2.3/src/lorebase_keeper/routes/operations.py +392 -0
  85. lorebase_keeper-0.2.3/src/lorebase_keeper/routes/proposal_items.py +563 -0
  86. lorebase_keeper-0.2.3/src/lorebase_keeper/routes/proposals.py +1107 -0
  87. lorebase_keeper-0.2.3/src/lorebase_keeper/routes/read.py +493 -0
  88. lorebase_keeper-0.2.3/src/lorebase_keeper/routes/revisions.py +523 -0
  89. lorebase_keeper-0.2.3/src/lorebase_keeper/routes/schema.py +682 -0
  90. lorebase_keeper-0.2.3/src/lorebase_keeper/routes/schema_inference.py +437 -0
  91. lorebase_keeper-0.2.3/src/lorebase_keeper/routes/schema_suggestions.py +328 -0
  92. lorebase_keeper-0.2.3/src/lorebase_keeper/routes/sessions.py +286 -0
  93. lorebase_keeper-0.2.3/src/lorebase_keeper/routes/source_preview.py +232 -0
  94. lorebase_keeper-0.2.3/src/lorebase_keeper/routes/sources.py +200 -0
  95. lorebase_keeper-0.2.3/src/lorebase_keeper/routes/trace.py +506 -0
  96. lorebase_keeper-0.2.3/src/lorebase_keeper/routes/vault.py +332 -0
  97. lorebase_keeper-0.2.3/src/lorebase_keeper/routes/webhooks.py +298 -0
  98. lorebase_keeper-0.2.3/src/lorebase_keeper/routes/whoami.py +88 -0
  99. lorebase_keeper-0.2.3/src/lorebase_keeper/services/__init__.py +1 -0
  100. lorebase_keeper-0.2.3/src/lorebase_keeper/services/agent_writes.py +1035 -0
  101. lorebase_keeper-0.2.3/src/lorebase_keeper/services/branches.py +337 -0
  102. lorebase_keeper-0.2.3/src/lorebase_keeper/services/config.py +431 -0
  103. lorebase_keeper-0.2.3/src/lorebase_keeper/services/derivation_preview.py +279 -0
  104. lorebase_keeper-0.2.3/src/lorebase_keeper/services/document_urls.py +114 -0
  105. lorebase_keeper-0.2.3/src/lorebase_keeper/services/domains.py +118 -0
  106. lorebase_keeper-0.2.3/src/lorebase_keeper/services/errors.py +196 -0
  107. lorebase_keeper-0.2.3/src/lorebase_keeper/services/operation_catalog.py +806 -0
  108. lorebase_keeper-0.2.3/src/lorebase_keeper/services/operation_events.py +199 -0
  109. lorebase_keeper-0.2.3/src/lorebase_keeper/services/operations.py +1229 -0
  110. lorebase_keeper-0.2.3/src/lorebase_keeper/services/pii_import.py +222 -0
  111. lorebase_keeper-0.2.3/src/lorebase_keeper/services/proposal_reads.py +158 -0
  112. lorebase_keeper-0.2.3/src/lorebase_keeper/services/proposals.py +850 -0
  113. lorebase_keeper-0.2.3/src/lorebase_keeper/services/reads.py +286 -0
  114. lorebase_keeper-0.2.3/src/lorebase_keeper/services/schema.py +601 -0
  115. lorebase_keeper-0.2.3/src/lorebase_keeper/services/schema_inference.py +390 -0
  116. lorebase_keeper-0.2.3/src/lorebase_keeper/services/schema_suggestions.py +418 -0
  117. lorebase_keeper-0.2.3/src/lorebase_keeper/services/source_preview.py +127 -0
  118. lorebase_keeper-0.2.3/src/lorebase_keeper/settings.py +313 -0
  119. lorebase_keeper-0.2.3/src/lorebase_keeper/tracing/__init__.py +1 -0
  120. lorebase_keeper-0.2.3/src/lorebase_keeper/tracing/capture.py +350 -0
  121. lorebase_keeper-0.2.3/src/lorebase_keeper/tracing/config.py +157 -0
  122. lorebase_keeper-0.2.3/src/lorebase_keeper/tracing/explain.py +97 -0
  123. lorebase_keeper-0.2.3/src/lorebase_keeper/tracing/middleware.py +359 -0
  124. lorebase_keeper-0.2.3/src/lorebase_keeper/tracing/notes.py +80 -0
  125. lorebase_keeper-0.2.3/src/lorebase_keeper/tracing/recorder.py +180 -0
  126. lorebase_keeper-0.2.3/tests/agents/conftest.py +131 -0
  127. lorebase_keeper-0.2.3/tests/agents/snapshots/context_pack.md +98 -0
  128. lorebase_keeper-0.2.3/tests/agents/snapshots/context_pack_trimmed.md +69 -0
  129. lorebase_keeper-0.2.3/tests/agents/snapshots/explain_part.md +16 -0
  130. lorebase_keeper-0.2.3/tests/agents/test_budget.py +157 -0
  131. lorebase_keeper-0.2.3/tests/agents/test_rendering.py +139 -0
  132. lorebase_keeper-0.2.3/tests/agents/test_scope.py +87 -0
  133. lorebase_keeper-0.2.3/tests/agents/test_settings.py +36 -0
  134. lorebase_keeper-0.2.3/tests/auth/conftest.py +616 -0
  135. lorebase_keeper-0.2.3/tests/auth/test_admin.py +636 -0
  136. lorebase_keeper-0.2.3/tests/auth/test_api_key_authentication.py +310 -0
  137. lorebase_keeper-0.2.3/tests/auth/test_api_keys.py +119 -0
  138. lorebase_keeper-0.2.3/tests/auth/test_audit.py +260 -0
  139. lorebase_keeper-0.2.3/tests/auth/test_auth_failure_limits.py +253 -0
  140. lorebase_keeper-0.2.3/tests/auth/test_bootstrap.py +126 -0
  141. lorebase_keeper-0.2.3/tests/auth/test_oidc.py +398 -0
  142. lorebase_keeper-0.2.3/tests/auth/test_proposal_read_authorization.py +276 -0
  143. lorebase_keeper-0.2.3/tests/auth/test_quotas.py +198 -0
  144. lorebase_keeper-0.2.3/tests/auth/test_rate_limits.py +253 -0
  145. lorebase_keeper-0.2.3/tests/auth/test_rbac.py +552 -0
  146. lorebase_keeper-0.2.3/tests/auth/test_read_audit.py +350 -0
  147. lorebase_keeper-0.2.3/tests/auth/test_read_authorization.py +356 -0
  148. lorebase_keeper-0.2.3/tests/auth/test_schema_inference_access.py +68 -0
  149. lorebase_keeper-0.2.3/tests/auth/test_sessions.py +732 -0
  150. lorebase_keeper-0.2.3/tests/conftest.py +62 -0
  151. lorebase_keeper-0.2.3/tests/integration/conftest.py +84 -0
  152. lorebase_keeper-0.2.3/tests/integration/read_fixtures.py +135 -0
  153. lorebase_keeper-0.2.3/tests/integration/snapshots/context_supplier_s1001.md +96 -0
  154. lorebase_keeper-0.2.3/tests/integration/snapshots/explain_supplier_s1001.md +26 -0
  155. lorebase_keeper-0.2.3/tests/integration/supply.py +184 -0
  156. lorebase_keeper-0.2.3/tests/integration/test_agent_trace_postgres.py +435 -0
  157. lorebase_keeper-0.2.3/tests/integration/test_agent_trace_rbac_postgres.py +189 -0
  158. lorebase_keeper-0.2.3/tests/integration/test_agents_api.py +245 -0
  159. lorebase_keeper-0.2.3/tests/integration/test_auth_postgres.py +196 -0
  160. lorebase_keeper-0.2.3/tests/integration/test_change_feed.py +352 -0
  161. lorebase_keeper-0.2.3/tests/integration/test_derivation_preview_api.py +417 -0
  162. lorebase_keeper-0.2.3/tests/integration/test_derivations_postgres.py +163 -0
  163. lorebase_keeper-0.2.3/tests/integration/test_derived_sensitivity_api.py +219 -0
  164. lorebase_keeper-0.2.3/tests/integration/test_documents_api.py +222 -0
  165. lorebase_keeper-0.2.3/tests/integration/test_expressions_postgres.py +176 -0
  166. lorebase_keeper-0.2.3/tests/integration/test_extraction_vault_edits.py +228 -0
  167. lorebase_keeper-0.2.3/tests/integration/test_health_postgres.py +491 -0
  168. lorebase_keeper-0.2.3/tests/integration/test_invisible_kinds.py +392 -0
  169. lorebase_keeper-0.2.3/tests/integration/test_limits_postgres.py +134 -0
  170. lorebase_keeper-0.2.3/tests/integration/test_operation_events_postgres.py +328 -0
  171. lorebase_keeper-0.2.3/tests/integration/test_overlay_reads_postgres.py +273 -0
  172. lorebase_keeper-0.2.3/tests/integration/test_path_derivations_postgres.py +906 -0
  173. lorebase_keeper-0.2.3/tests/integration/test_read_api.py +941 -0
  174. lorebase_keeper-0.2.3/tests/integration/test_read_authorization.py +471 -0
  175. lorebase_keeper-0.2.3/tests/integration/test_read_replicas_postgres.py +156 -0
  176. lorebase_keeper-0.2.3/tests/integration/test_read_timing.py +144 -0
  177. lorebase_keeper-0.2.3/tests/integration/test_readiness_postgres.py +62 -0
  178. lorebase_keeper-0.2.3/tests/integration/test_record_history_api.py +204 -0
  179. lorebase_keeper-0.2.3/tests/integration/test_record_lineage_api.py +368 -0
  180. lorebase_keeper-0.2.3/tests/integration/test_schema_suggestions_api.py +216 -0
  181. lorebase_keeper-0.2.3/tests/integration/test_search_bm25_postgres.py +124 -0
  182. lorebase_keeper-0.2.3/tests/integration/test_search_tuning_postgres.py +43 -0
  183. lorebase_keeper-0.2.3/tests/integration/test_sensitivity_api.py +671 -0
  184. lorebase_keeper-0.2.3/tests/integration/test_sessions_postgres.py +57 -0
  185. lorebase_keeper-0.2.3/tests/integration/test_source_preview_api.py +205 -0
  186. lorebase_keeper-0.2.3/tests/integration/test_sources_api.py +106 -0
  187. lorebase_keeper-0.2.3/tests/integration/test_time_travel_api.py +387 -0
  188. lorebase_keeper-0.2.3/tests/integration/writes/conftest.py +102 -0
  189. lorebase_keeper-0.2.3/tests/integration/writes/test_agent_writes_postgres.py +566 -0
  190. lorebase_keeper-0.2.3/tests/integration/writes/test_domains_branches_postgres.py +287 -0
  191. lorebase_keeper-0.2.3/tests/integration/writes/test_operations_postgres.py +486 -0
  192. lorebase_keeper-0.2.3/tests/integration/writes/test_pii_import_postgres.py +179 -0
  193. lorebase_keeper-0.2.3/tests/integration/writes/test_proposals_postgres.py +571 -0
  194. lorebase_keeper-0.2.3/tests/integration/writes/test_schema_postgres.py +149 -0
  195. lorebase_keeper-0.2.3/tests/integration/writes/test_sensitivity_writes_postgres.py +118 -0
  196. lorebase_keeper-0.2.3/tests/integration/writes/test_webhook_clearances_postgres.py +112 -0
  197. lorebase_keeper-0.2.3/tests/integration/writes/test_webhook_sealing_postgres.py +44 -0
  198. lorebase_keeper-0.2.3/tests/kindhealth/test_health_reports.py +312 -0
  199. lorebase_keeper-0.2.3/tests/ratelimit/test_rate_limit_buckets.py +252 -0
  200. lorebase_keeper-0.2.3/tests/ratelimit/test_shared_store.py +109 -0
  201. lorebase_keeper-0.2.3/tests/reading/snapshots/located_trait.sql +21 -0
  202. lorebase_keeper-0.2.3/tests/reading/snapshots/part_include.sql +17 -0
  203. lorebase_keeper-0.2.3/tests/reading/snapshots/part_relations.sql +31 -0
  204. lorebase_keeper-0.2.3/tests/reading/snapshots/supplier_filter_keyset.sql +24 -0
  205. lorebase_keeper-0.2.3/tests/reading/snapshots/supplier_foreign_key.sql +27 -0
  206. lorebase_keeper-0.2.3/tests/reading/test_caching.py +138 -0
  207. lorebase_keeper-0.2.3/tests/reading/test_compiler.py +252 -0
  208. lorebase_keeper-0.2.3/tests/reading/test_routes.py +42 -0
  209. lorebase_keeper-0.2.3/tests/reading/test_sensitivity.py +659 -0
  210. lorebase_keeper-0.2.3/tests/reading/test_settings.py +46 -0
  211. lorebase_keeper-0.2.3/tests/reading/test_visibility.py +83 -0
  212. lorebase_keeper-0.2.3/tests/test_agent_writes.py +349 -0
  213. lorebase_keeper-0.2.3/tests/test_client_address.py +135 -0
  214. lorebase_keeper-0.2.3/tests/test_conventions.py +79 -0
  215. lorebase_keeper-0.2.3/tests/test_cors.py +60 -0
  216. lorebase_keeper-0.2.3/tests/test_derivation_preview.py +158 -0
  217. lorebase_keeper-0.2.3/tests/test_document_urls.py +159 -0
  218. lorebase_keeper-0.2.3/tests/test_domain_routes.py +40 -0
  219. lorebase_keeper-0.2.3/tests/test_errors.py +264 -0
  220. lorebase_keeper-0.2.3/tests/test_health.py +162 -0
  221. lorebase_keeper-0.2.3/tests/test_main.py +274 -0
  222. lorebase_keeper-0.2.3/tests/test_operation_events.py +113 -0
  223. lorebase_keeper-0.2.3/tests/test_operations.py +618 -0
  224. lorebase_keeper-0.2.3/tests/test_pii_import.py +96 -0
  225. lorebase_keeper-0.2.3/tests/test_proposals.py +148 -0
  226. lorebase_keeper-0.2.3/tests/test_read_replicas.py +144 -0
  227. lorebase_keeper-0.2.3/tests/test_readiness_migrations.py +92 -0
  228. lorebase_keeper-0.2.3/tests/test_request_id.py +141 -0
  229. lorebase_keeper-0.2.3/tests/test_schema_changes.py +140 -0
  230. lorebase_keeper-0.2.3/tests/test_schema_inference.py +635 -0
  231. lorebase_keeper-0.2.3/tests/test_schema_search.py +48 -0
  232. lorebase_keeper-0.2.3/tests/test_security_headers.py +152 -0
  233. lorebase_keeper-0.2.3/tests/test_settings.py +106 -0
  234. lorebase_keeper-0.2.3/tests/test_source_preview.py +156 -0
  235. lorebase_keeper-0.2.3/tests/tracing/test_capture.py +245 -0
  236. lorebase_keeper-0.2.3/tests/tracing/test_trace_fields.py +136 -0
  237. lorebase_keeper-0.2.3/tests/tracing/test_trace_middleware.py +225 -0
@@ -0,0 +1,79 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ build/
8
+ dist/
9
+ *.egg-info/
10
+ .eggs/
11
+ .venv/
12
+ venv/
13
+ .python-eggs/
14
+ pip-wheel-metadata/
15
+ .pytest_cache/
16
+ .mypy_cache/
17
+ .ruff_cache/
18
+ .pyre/
19
+ .pytype/
20
+ .hypothesis/
21
+ .coverage
22
+ .coverage.*
23
+ coverage.xml
24
+ htmlcov/
25
+ .tox/
26
+ .nox/
27
+ *.prof
28
+
29
+ # Node
30
+ node_modules/
31
+ .pnpm-store/
32
+ .npm/
33
+ .yarn/
34
+ !.yarn/releases
35
+ .pnp.*
36
+ npm-debug.log*
37
+ yarn-debug.log*
38
+ yarn-error.log*
39
+ pnpm-debug.log*
40
+ .vite/
41
+ *.tsbuildinfo
42
+ apps/web/dist/
43
+ apps/web/.vitest/
44
+
45
+ # Environment and secrets
46
+ .env
47
+ .env.*
48
+ !.env.example
49
+ *.pem
50
+ *.key
51
+ secrets/
52
+
53
+ # Docker
54
+ docker-compose.override.yaml
55
+ docker-compose.override.yml
56
+ .docker/
57
+
58
+ # Data and local artifacts
59
+ *.sqlite
60
+ *.db
61
+ /tmp/
62
+ /data/
63
+ tests/perf/results/
64
+
65
+ # Editors and OS
66
+ .idea/
67
+ .vscode/*
68
+ !.vscode/extensions.json
69
+ !.vscode/settings.example.json
70
+ *.swp
71
+ *.swo
72
+ *~
73
+ .DS_Store
74
+ Thumbs.db
75
+ *.code-workspace
76
+ .history/
77
+
78
+ # Session worktrees
79
+ .claude/
@@ -0,0 +1,21 @@
1
+ Metadata-Version: 2.5
2
+ Name: lorebase-keeper
3
+ Version: 0.2.3
4
+ Summary: Lorebase Keeper: REST API service
5
+ Requires-Python: >=3.12
6
+ Requires-Dist: asyncpg>=0.29
7
+ Requires-Dist: fastapi>=0.115
8
+ Requires-Dist: httpx>=0.27
9
+ Requires-Dist: lorebase-platform<0.3,>=0.2
10
+ Requires-Dist: lorebase-store<0.3,>=0.2
11
+ Requires-Dist: opentelemetry-api>=1.25
12
+ Requires-Dist: pydantic>=2.7
13
+ Requires-Dist: pyjwt[crypto]>=2.8
14
+ Requires-Dist: python-multipart>=0.0.9
15
+ Requires-Dist: uvicorn>=0.30
16
+ Provides-Extra: mssql
17
+ Requires-Dist: pymssql>=2.3; extra == 'mssql'
18
+ Provides-Extra: mysql
19
+ Requires-Dist: asyncmy>=0.2.10; extra == 'mysql'
20
+ Provides-Extra: parquet
21
+ Requires-Dist: pyarrow>=15; extra == 'parquet'
@@ -0,0 +1,32 @@
1
+ [project]
2
+ name = "lorebase-keeper"
3
+ version = "0.2.3"
4
+ description = "Lorebase Keeper: REST API service"
5
+ requires-python = ">=3.12"
6
+ dependencies = [
7
+ "lorebase-store>=0.2,<0.3",
8
+ "lorebase-platform>=0.2,<0.3",
9
+ "fastapi>=0.115",
10
+ "uvicorn>=0.30",
11
+ "pydantic>=2.7",
12
+ "opentelemetry-api>=1.25",
13
+ "asyncpg>=0.29",
14
+ "httpx>=0.27",
15
+ "PyJWT[crypto]>=2.8",
16
+ "python-multipart>=0.0.9", # form parser of Starlette: file uploads of POST schema:infer
17
+ ]
18
+
19
+ [project.optional-dependencies]
20
+ parquet = ["pyarrow>=15"] # Parquet uploads of POST schema:infer and schema:preview
21
+ mysql = ["asyncmy>=0.2.10"] # MySQL and MariaDB sources of POST schema:infer
22
+ mssql = ["pymssql>=2.3"] # SQL Server sources of POST schema:infer
23
+
24
+ [project.scripts]
25
+ lorebase-keeper = "lorebase_keeper.__main__:main"
26
+
27
+ [build-system]
28
+ requires = ["hatchling"]
29
+ build-backend = "hatchling.build"
30
+
31
+ [tool.hatch.build.targets.wheel]
32
+ packages = ["src/lorebase_keeper"]
@@ -0,0 +1 @@
1
+ __version__ = "0.2.3"
@@ -0,0 +1,456 @@
1
+ """Run Keeper, or prepare an installation, with the settings Keeper reads.
2
+
3
+ python -m lorebase_keeper [serve]
4
+ Serve the API; host and port come from the `keeper` settings.
5
+ python -m lorebase_keeper bootstrap [--principal ID] [--name NAME] [--force]
6
+ Create the first installation administrator key and print it once (see bootstrap.py).
7
+ python -m lorebase_keeper principal create ID [--name NAME]
8
+ Add a service principal with a chosen id; nothing happens when it exists.
9
+ python -m lorebase_keeper audit verify (DOMAIN | --installation | --all) [--after-seq N] [--json]
10
+ [--reads] [--checkpoints PATH ... --checkpoint-key FILE]
11
+ Recompute the hash chains of the audit log (the read stream with --reads) and report the
12
+ first broken entry; with --checkpoints, also compare the log with the signed checkpoints
13
+ Scribe published (files or directories of them) to detect removed or rewritten entries.
14
+ The exit status is 1 when an entry does not verify, a checkpointed entry is missing or
15
+ changed, or a checkpoint is invalid.
16
+ python -m lorebase_keeper sources vault [--json] [--check]
17
+ List the Sources of every Domain's default branch that map vault Attributes (upgrade
18
+ check for pii.import, design 13.5); with --check the exit status is 1 when any is listed.
19
+
20
+ The installation commands connect with `database.maintenance_dsn` when it is set, else with
21
+ `database.dsn`.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import argparse
27
+ import asyncio
28
+ import json
29
+ import sys
30
+ from collections.abc import Awaitable, Callable, Coroutine, Sequence
31
+ from dataclasses import dataclass, field
32
+ from datetime import UTC, datetime
33
+ from pathlib import Path
34
+ from typing import Any
35
+
36
+ import uvicorn
37
+ from sqlalchemy.ext.asyncio import async_sessionmaker
38
+
39
+ from lorebase_keeper.app import create_app
40
+ from lorebase_keeper.bootstrap import (
41
+ DEFAULT_ADMIN_NAME,
42
+ BootstrapError,
43
+ bootstrap,
44
+ create_service_principal,
45
+ )
46
+ from lorebase_keeper.services.pii_import import installation_vault_sources
47
+ from lorebase_keeper.settings import keeper_settings
48
+ from lorebase_platform.logging import configure_logging
49
+ from lorebase_platform.settings import ConfigError, Settings, load_settings
50
+ from lorebase_platform.telemetry import setup_telemetry
51
+ from lorebase_store.auth import AuditStream, Chains, ChainVerification, SqlAuthStore
52
+ from lorebase_store.auth.checkpoints import (
53
+ Checkpoint,
54
+ CheckpointError,
55
+ CheckpointFinding,
56
+ compare,
57
+ load_public_key,
58
+ parse_document,
59
+ )
60
+ from lorebase_store.db import create_engine
61
+ from lorebase_store.settings import DatabaseSettings, store_settings
62
+
63
+ SERVICE_NAME = "keeper"
64
+
65
+
66
+ def _parser() -> argparse.ArgumentParser:
67
+ parser = argparse.ArgumentParser(
68
+ prog="python -m lorebase_keeper",
69
+ description="Run Keeper or prepare an installation with Keeper's settings.",
70
+ )
71
+ commands = parser.add_subparsers(dest="command")
72
+ commands.add_parser("serve", help="serve the API (the default)")
73
+ boot = commands.add_parser(
74
+ "bootstrap",
75
+ help="create the first installation administrator key and print it once",
76
+ description="Create a key for an installation administrator (keeper.auth."
77
+ "installation_admins) and print it on standard output, once. Refused when an "
78
+ "installation administrator already holds a usable key, unless --force is given.",
79
+ )
80
+ boot.add_argument(
81
+ "--principal",
82
+ help="administrator principal id (default: the first of keeper.auth.installation_admins)",
83
+ )
84
+ boot.add_argument(
85
+ "--name",
86
+ default=DEFAULT_ADMIN_NAME,
87
+ help="display name when the principal is created",
88
+ )
89
+ boot.add_argument(
90
+ "--force",
91
+ action="store_true",
92
+ help="add a key even when an administrator already holds one (existing keys stay valid)",
93
+ )
94
+ principal = commands.add_parser("principal", help="manage service principals")
95
+ principal_commands = principal.add_subparsers(dest="principal_command", required=True)
96
+ create = principal_commands.add_parser(
97
+ "create",
98
+ help="add a service principal with a chosen id",
99
+ description="Add a service principal with a chosen id, for example one that a schema "
100
+ "names in Operation permissions. Nothing happens when it already exists.",
101
+ )
102
+ create.add_argument("principal_id", metavar="ID")
103
+ create.add_argument("--name", help="display name (default: the id)")
104
+ audit = commands.add_parser("audit", help="check the audit log")
105
+ audit_commands = audit.add_subparsers(dest="audit_command", required=True)
106
+ verify = audit_commands.add_parser(
107
+ "verify",
108
+ help="recompute the audit hash chains and report the first broken entry",
109
+ description="Recompute the hash chain of a Domain's audit log, of the installation-wide "
110
+ "chain or of every chain, reading entries in batches. Exits with status 1 when an entry "
111
+ "does not verify.",
112
+ )
113
+ chain = verify.add_mutually_exclusive_group(required=True)
114
+ chain.add_argument("domain", nargs="?", metavar="DOMAIN", help="Domain id")
115
+ chain.add_argument(
116
+ "--installation", action="store_true", help="the chain of entries without a Domain"
117
+ )
118
+ chain.add_argument("--all", action="store_true", help="every chain")
119
+ verify.add_argument(
120
+ "--after-seq",
121
+ type=int,
122
+ default=0,
123
+ metavar="N",
124
+ help="start after entry N, trusting it (the last_seq of an earlier run); default 0",
125
+ )
126
+ verify.add_argument("--json", action="store_true", help="print the result as JSON")
127
+ verify.add_argument(
128
+ "--reads", action="store_true", help="the read stream (audit_read_log) instead of actions"
129
+ )
130
+ verify.add_argument(
131
+ "--checkpoints",
132
+ action="append",
133
+ default=[],
134
+ type=Path,
135
+ metavar="PATH",
136
+ help="checkpoint file, or directory searched for *.json (repeatable)",
137
+ )
138
+ verify.add_argument(
139
+ "--checkpoint-key",
140
+ type=Path,
141
+ metavar="FILE",
142
+ help="PEM Ed25519 public key the checkpoints are signed with",
143
+ )
144
+ verify.add_argument(
145
+ "--max-age",
146
+ type=float,
147
+ metavar="SECONDS",
148
+ help="fail when the newest checkpoint is older (publication stopped)",
149
+ )
150
+ sources = commands.add_parser("sources", help="check Sources")
151
+ sources_commands = sources.add_subparsers(dest="sources_command", required=True)
152
+ vault = sources_commands.add_parser(
153
+ "vault",
154
+ help="list the Sources that map vault Attributes",
155
+ description="List the Sources of every Domain's default branch that map columns to "
156
+ "vault Attributes. Defining, changing or triggering such a Source needs pii.import; "
157
+ "Sources defined before that permission existed still sync on their schedule: confirm "
158
+ "each, or disable it (remove its schedule or deprecate it) with a schema change.",
159
+ )
160
+ vault.add_argument("--json", action="store_true", help="print the Sources as JSON")
161
+ vault.add_argument(
162
+ "--check", action="store_true", help="exit with status 1 when any Source is listed"
163
+ )
164
+ return parser
165
+
166
+
167
+ def main(argv: Sequence[str] | None = None) -> None:
168
+ """Dispatch to serving (no arguments) or to an installation command."""
169
+ args = _parser().parse_args(argv)
170
+ if args.command == "bootstrap":
171
+ _run_admin(lambda settings: _bootstrap(settings, args))
172
+ elif args.command == "principal":
173
+ _run_admin(lambda settings: _create_principal(settings, args))
174
+ elif args.command == "audit":
175
+ _run_admin(lambda settings: _verify_audit(settings, args))
176
+ elif args.command == "sources":
177
+ _run_admin(lambda settings: _vault_sources(settings, args))
178
+ else:
179
+ serve()
180
+
181
+
182
+ def serve() -> None:
183
+ """Load settings, configure logging and telemetry, and serve until stopped."""
184
+ try:
185
+ settings = load_settings()
186
+ keeper = keeper_settings(settings)
187
+ configure_logging(settings.logging, service=SERVICE_NAME)
188
+ telemetry = setup_telemetry(settings.telemetry)
189
+ app = create_app(settings, telemetry=telemetry)
190
+ except ConfigError as exc:
191
+ sys.exit(str(exc))
192
+ try:
193
+ uvicorn.run(
194
+ app,
195
+ host=keeper.host,
196
+ port=keeper.port,
197
+ log_config=None,
198
+ access_log=False,
199
+ server_header=False,
200
+ # The client address comes from `keeper.trusted_proxies` (client_address.py) only.
201
+ proxy_headers=False,
202
+ )
203
+ finally:
204
+ telemetry.shutdown()
205
+
206
+
207
+ def _run_admin(command: Callable[[Settings], Coroutine[Any, Any, None]]) -> None:
208
+ """Load settings and run an installation command; problems exit with a message."""
209
+ try:
210
+ settings = load_settings()
211
+ configure_logging(settings.logging, service=SERVICE_NAME)
212
+ asyncio.run(command(settings))
213
+ except (ConfigError, BootstrapError) as exc:
214
+ sys.exit(f"lorebase_keeper: {exc}")
215
+
216
+
217
+ def _store(settings: Settings) -> tuple[SqlAuthStore, Callable[[], Awaitable[None]]]:
218
+ """Auth store of the installation commands: the owner role of `database.maintenance_dsn`
219
+ when it is set (the services may connect as a role subject to row-level security), else
220
+ `database.dsn`."""
221
+ database = settings.section("database", DatabaseSettings)
222
+ owner = database.maintenance_dsn
223
+ engine = create_engine(
224
+ database, url=None if owner is None else owner.get_secret_value(), pooled=False
225
+ )
226
+ return SqlAuthStore(async_sessionmaker(engine, expire_on_commit=False)), engine.dispose
227
+
228
+
229
+ async def _bootstrap(settings: Settings, args: argparse.Namespace) -> None:
230
+ auth = keeper_settings(settings).auth
231
+ store, dispose = _store(settings)
232
+ try:
233
+ created = await bootstrap(
234
+ store,
235
+ auth,
236
+ now=datetime.now(UTC),
237
+ principal_id=args.principal,
238
+ display_name=args.name,
239
+ force=args.force,
240
+ )
241
+ finally:
242
+ await dispose()
243
+ expiry = created.expires_at.date().isoformat() if created.expires_at else "never"
244
+ action = "created" if created.principal_created else "existing"
245
+ print(
246
+ f"API key {created.prefix}... for {action} principal {created.principal_id} "
247
+ f"(expires {expiry}); it is printed once on standard output",
248
+ file=sys.stderr,
249
+ )
250
+ print(created.token)
251
+
252
+
253
+ async def _create_principal(settings: Settings, args: argparse.Namespace) -> None:
254
+ store, dispose = _store(settings)
255
+ try:
256
+ added = await create_service_principal(
257
+ store, args.principal_id, args.name or args.principal_id, now=datetime.now(UTC)
258
+ )
259
+ finally:
260
+ await dispose()
261
+ state = "created" if added else "already exists"
262
+ print(f"service principal {args.principal_id} {state}", file=sys.stderr)
263
+
264
+
265
+ def _chain_of(args: argparse.Namespace) -> str | Chains | None:
266
+ if args.all:
267
+ return Chains.ALL
268
+ return None if args.installation else str(args.domain)
269
+
270
+
271
+ def verification_report(
272
+ chain: str | Chains | None,
273
+ result: ChainVerification,
274
+ stream: AuditStream = AuditStream.ACTIONS,
275
+ anchored: Anchoring | None = None,
276
+ ) -> dict[str, Any]:
277
+ """JSON form of a verification, as `audit verify --json` prints it."""
278
+ broken = (
279
+ None
280
+ if result.ok
281
+ else {"seq": result.bad_seq, "domain_id": result.bad_domain_id, "reason": result.reason}
282
+ )
283
+ report: dict[str, Any] = {
284
+ "stream": stream.value,
285
+ "chain": chain.value if isinstance(chain, Chains) else chain,
286
+ "intact": result.ok and (anchored is None or anchored.ok),
287
+ "checked": result.checked,
288
+ "chains": result.chains,
289
+ "last_seq": result.last_seq,
290
+ "first_broken": broken,
291
+ }
292
+ if anchored is not None:
293
+ report["checkpoints"] = {
294
+ "checked": anchored.checked,
295
+ "newest_at": anchored.newest.isoformat() if anchored.newest else None,
296
+ "newest_age_s": anchored.age_s,
297
+ "problems": anchored.problems,
298
+ "invalid": anchored.invalid,
299
+ "findings": [
300
+ {
301
+ "checkpoint_at": f.checkpoint_at.isoformat(),
302
+ "domain_id": f.domain_id,
303
+ "seq": f.seq,
304
+ "problem": f.problem,
305
+ }
306
+ for f in anchored.findings
307
+ ],
308
+ }
309
+ return report
310
+
311
+
312
+ @dataclass(frozen=True)
313
+ class Anchoring:
314
+ """Comparison of the log with checkpoints: how many were compared, the files that are
315
+ not valid checkpoints, and the checkpointed entries the log no longer holds as they were."""
316
+
317
+ checked: int
318
+ invalid: list[str]
319
+ findings: list[CheckpointFinding]
320
+ newest: datetime | None = None
321
+ max_age_s: float | None = None
322
+ now: datetime = field(default_factory=lambda: datetime.now(UTC))
323
+
324
+ @property
325
+ def age_s(self) -> float | None:
326
+ """Age of the newest checkpoint compared."""
327
+ return None if self.newest is None else (self.now - self.newest).total_seconds()
328
+
329
+ @property
330
+ def problems(self) -> list[str]:
331
+ """Why the comparison does not vouch for the log, besides the findings."""
332
+ found = [f"invalid checkpoint {problem}" for problem in self.invalid]
333
+ if self.checked == 0:
334
+ found.append("no valid checkpoint of this stream was found")
335
+ age = self.age_s
336
+ if self.max_age_s is not None and age is not None and age > self.max_age_s:
337
+ found.append(f"the newest checkpoint is {age:.0f} s old (--max-age {self.max_age_s:g})")
338
+ return found
339
+
340
+ @property
341
+ def ok(self) -> bool:
342
+ return not self.problems and not self.findings
343
+
344
+
345
+ def _checkpoint_files(paths: Sequence[Path]) -> list[Path]:
346
+ files: list[Path] = []
347
+ for path in paths:
348
+ files.extend(sorted(path.rglob("*.json")) if path.is_dir() else [path])
349
+ return files
350
+
351
+
352
+ def _load_checkpoints(
353
+ paths: Sequence[Path], key_file: Path, stream: AuditStream
354
+ ) -> tuple[list[Checkpoint], list[str]]:
355
+ """Valid checkpoints of `stream` and the files that are not valid checkpoints."""
356
+ try:
357
+ key = load_public_key(key_file.read_text(encoding="utf-8"))
358
+ except (OSError, CheckpointError) as error:
359
+ raise BootstrapError(f"--checkpoint-key: {error}") from None
360
+ found: list[Checkpoint] = []
361
+ invalid: list[str] = []
362
+ for file in _checkpoint_files(paths):
363
+ try:
364
+ checkpoint = parse_document(file.read_bytes(), key)
365
+ except (OSError, CheckpointError) as error:
366
+ invalid.append(f"{file}: {error}")
367
+ continue
368
+ if checkpoint.stream is stream:
369
+ found.append(checkpoint)
370
+ return found, invalid
371
+
372
+
373
+ async def _verify_audit(settings: Settings, args: argparse.Namespace) -> None:
374
+ if args.after_seq < 0:
375
+ raise BootstrapError("--after-seq must not be negative")
376
+ if args.checkpoints and args.checkpoint_key is None:
377
+ raise BootstrapError("--checkpoints needs --checkpoint-key")
378
+ chain = _chain_of(args)
379
+ stream = AuditStream.READS if args.reads else AuditStream.ACTIONS
380
+ checkpoints, invalid = (
381
+ _load_checkpoints(args.checkpoints, args.checkpoint_key, stream)
382
+ if args.checkpoints
383
+ else ([], [])
384
+ )
385
+ anchored: Anchoring | None = None
386
+ store, dispose = _store(settings)
387
+ try:
388
+ async with store.unit() as unit:
389
+ repository = unit.read_audit if args.reads else unit.audit
390
+ result = await repository.verify(after_seq=args.after_seq, domain_id=chain)
391
+ if args.checkpoints:
392
+ domains = None if isinstance(chain, Chains) else [chain]
393
+ findings = await compare(repository, checkpoints, domain_ids=domains)
394
+ newest = max((c.at for c in checkpoints), default=None)
395
+ anchored = Anchoring(len(checkpoints), invalid, findings, newest, args.max_age)
396
+ finally:
397
+ await dispose()
398
+ if args.json:
399
+ print(json.dumps(verification_report(chain, result, stream, anchored)))
400
+ elif result.ok:
401
+ last = "-" if result.last_seq is None else result.last_seq
402
+ print(
403
+ f"audit log intact: {result.checked} entries in {result.chains} chain(s), "
404
+ f"last seq {last}"
405
+ )
406
+ else:
407
+ where = result.bad_domain_id or "installation"
408
+ print(
409
+ f"audit log broken at seq {result.bad_seq} (chain {where}): {result.reason}; "
410
+ f"{result.checked} entries verified before it"
411
+ )
412
+ if anchored is not None and not args.json:
413
+ age = "-" if anchored.age_s is None else f"{anchored.age_s:.0f} s"
414
+ print(f"checkpoints compared: {anchored.checked}, newest {age} old")
415
+ for problem in anchored.problems:
416
+ print(problem)
417
+ for finding in anchored.findings:
418
+ where = finding.domain_id or "installation"
419
+ print(
420
+ f"entry seq {finding.seq} (chain {where}) of the checkpoint of "
421
+ f"{finding.checkpoint_at.isoformat()} is {finding.problem}"
422
+ )
423
+ if not result.ok or (anchored is not None and not anchored.ok):
424
+ sys.exit(1)
425
+
426
+
427
+ async def _vault_sources(settings: Settings, args: argparse.Namespace) -> None:
428
+ database = settings.section("database", DatabaseSettings)
429
+ owner = database.maintenance_dsn
430
+ engine = create_engine(
431
+ database, url=None if owner is None else owner.get_secret_value(), pooled=False
432
+ )
433
+ try:
434
+ found = await installation_vault_sources(
435
+ async_sessionmaker(engine, expire_on_commit=False),
436
+ store_settings(settings).writes,
437
+ )
438
+ finally:
439
+ await engine.dispose()
440
+ if args.json:
441
+ print(json.dumps([source.document() for source in found]))
442
+ elif not found:
443
+ print("no Source maps vault Attributes")
444
+ else:
445
+ for source in found:
446
+ schedule = source.schedule or "manual"
447
+ print(
448
+ f"{source.domain} {source.api_name} ({source.source}): {source.kind}."
449
+ f"{', '.join(source.attributes)}; schedule {schedule}; {source.status}"
450
+ )
451
+ if args.check and found:
452
+ sys.exit(1)
453
+
454
+
455
+ if __name__ == "__main__":
456
+ main()
@@ -0,0 +1 @@
1
+ """Agent-facing reads: context packs, Record explanations and the change feed (design 11)."""