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,107 @@
1
+ """Request bodies of the read API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Literal
6
+
7
+ from pydantic import BaseModel, ConfigDict, Field
8
+
9
+ from lorebase_core.query import Predicate, Query
10
+
11
+ AT = (
12
+ "Revision id or tag to read at (time travel): key-ordered listings read the tree, other "
13
+ "reads a snapshot of the Revision."
14
+ )
15
+
16
+
17
+ class _Body(BaseModel):
18
+ model_config = ConfigDict(extra="forbid", frozen=True)
19
+
20
+
21
+ class QueryRequest(Query):
22
+ """A DSL query (design 8.2) and where to read it."""
23
+
24
+ branch: str | None = Field(default=None, description="Default: the Domain's default branch.")
25
+ at: str | None = Field(default=None, description=AT)
26
+
27
+
28
+ class RecordAddress(_Body):
29
+ kind: str = Field(min_length=1)
30
+ key: str = Field(min_length=1)
31
+
32
+
33
+ class TraverseStep(_Body):
34
+ relation: str = Field(min_length=1, description="Forward or inverse Relation api name.")
35
+ where: Predicate | None = Field(default=None, description="Filter on the Records reached.")
36
+ limit: int | None = Field(default=None, ge=1, description="Records reached by this step.")
37
+
38
+
39
+ class TraverseRequest(_Body):
40
+ """Follow a path of Relations from one Record (design 8.3)."""
41
+
42
+ branch: str | None = None
43
+ at: str | None = Field(default=None, description=AT)
44
+ start: RecordAddress
45
+ path: list[TraverseStep] = Field(min_length=1)
46
+
47
+
48
+ class NeighborhoodRequest(_Body):
49
+ """Breadth-first expansion from seed Records, bounded by depth and caps (design 8.3)."""
50
+
51
+ branch: str | None = None
52
+ at: str | None = Field(default=None, description=AT)
53
+ seeds: list[RecordAddress] = Field(min_length=1)
54
+ depth: int = Field(default=1, ge=1)
55
+ relations: list[str] | None = Field(
56
+ default=None,
57
+ description="Relation api names to follow; a forward name follows Links forward, an "
58
+ "inverse name backward. Default: every Relation in both directions.",
59
+ )
60
+ max_nodes: int | None = Field(default=None, ge=1)
61
+ max_edges: int | None = Field(default=None, ge=1)
62
+ values: bool = Field(
63
+ default=True,
64
+ description="false: nodes are Record headers (kind, key, title) and edges Link "
65
+ "endpoints (relation, from, to), without values; for callers that need the structure "
66
+ "only.",
67
+ )
68
+
69
+
70
+ SearchMode = Literal["lexical", "semantic", "hybrid"]
71
+
72
+
73
+ class SearchTuning(_Body):
74
+ """Ranking knobs of one search in place of the `search` settings (to compare parameter
75
+ sets). Unset knobs keep their configured value; `candidates` and `typo_candidates` may only
76
+ be lowered and `trigram_threshold` only raised."""
77
+
78
+ lexical_weight: float | None = Field(default=None, ge=0, le=100)
79
+ semantic_weight: float | None = Field(default=None, ge=0, le=100)
80
+ rrf_k: int | None = Field(default=None, ge=1, le=10_000)
81
+ trigram_threshold: float | None = Field(default=None, ge=0.1, le=1)
82
+ trigram_weight: float | None = Field(default=None, ge=0, le=100)
83
+ typo_candidates: int | None = Field(default=None, ge=0)
84
+ candidates: int | None = Field(default=None, ge=1)
85
+
86
+
87
+ class SearchRequest(_Body):
88
+ """Full-text, vector or hybrid search (design 8.4). Vectors come from the caller."""
89
+
90
+ branch: str | None = None
91
+ at: str | None = Field(default=None, description=AT)
92
+ q: str | None = Field(default=None, min_length=1, description="Text query (web search syntax).")
93
+ kinds: list[str] | None = Field(default=None, description="Kind api names; default: all.")
94
+ where: Predicate | None = Field(
95
+ default=None, description="DSL filter; needs exactly one Kind in `kinds`."
96
+ )
97
+ mode: SearchMode | None = Field(
98
+ default=None,
99
+ description="Default: hybrid with both `q` and `query_vector`, otherwise the one given.",
100
+ )
101
+ query_vector: list[float] | None = Field(
102
+ default=None, description="Query embedding of the configured model and dimension."
103
+ )
104
+ limit: int | None = Field(default=None, ge=1)
105
+ tuning: SearchTuning | None = Field(
106
+ default=None, description="Ranking knobs in place of the `search` settings."
107
+ )
@@ -0,0 +1,145 @@
1
+ """Per-application state of the read path and the read transaction dependency.
2
+
3
+ Reads run in a read-only transaction with the `read.statement_timeout_ms` statement timeout.
4
+ The Domain is added to the row-level security scope when the read point is resolved
5
+ (`reading.points`), so every query of a request stays inside its Domain.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+ from collections import OrderedDict
12
+ from collections.abc import AsyncIterator
13
+ from dataclasses import dataclass, field
14
+ from threading import Lock
15
+ from typing import Annotated, cast
16
+
17
+ from fastapi import Depends, Request
18
+ from sqlalchemy import func, select, text
19
+ from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
20
+
21
+ from lorebase_core.model import Schema
22
+ from lorebase_keeper.errors import UNAVAILABLE, ProblemError
23
+ from lorebase_keeper.ratelimit.bucket import InMemoryRateStore
24
+ from lorebase_keeper.reading.caching import ResponseCache, build_cache
25
+ from lorebase_keeper.reading.settings import (
26
+ ReadSettings,
27
+ SearchSettings,
28
+ read_settings,
29
+ search_settings,
30
+ )
31
+ from lorebase_keeper.settings import SensitivitySettings, keeper_settings
32
+ from lorebase_platform.settings import Settings
33
+ from lorebase_store.runtime import StoreRuntime
34
+ from lorebase_store.settings import StoreSettings
35
+ from lorebase_store.vault import VaultKeys
36
+
37
+ _STATE_KEY = "read_runtime"
38
+
39
+
40
+ class SchemaCache:
41
+ """Thread-safe LRU of compiled schemas keyed by `(domain_id, schema partition hash)`."""
42
+
43
+ def __init__(self, max_entries: int) -> None:
44
+ self._max = max_entries
45
+ self._entries: OrderedDict[tuple[str, bytes], Schema] = OrderedDict()
46
+ self._lock = Lock()
47
+
48
+ def get(self, domain_id: str, schema_hash: bytes) -> Schema | None:
49
+ key = (domain_id, schema_hash)
50
+ with self._lock:
51
+ found = self._entries.get(key)
52
+ if found is not None:
53
+ self._entries.move_to_end(key)
54
+ return found
55
+
56
+ def put(self, domain_id: str, schema_hash: bytes, schema: Schema) -> None:
57
+ with self._lock:
58
+ self._entries[(domain_id, schema_hash)] = schema
59
+ self._entries.move_to_end((domain_id, schema_hash))
60
+ while len(self._entries) > self._max:
61
+ self._entries.popitem(last=False)
62
+
63
+
64
+ SNAPSHOT_RATE_BUCKETS = 10_000
65
+ """Principals whose snapshot build buckets are kept (the least recently used are dropped)."""
66
+
67
+
68
+ @dataclass
69
+ class ReadRuntime:
70
+ """Settings, sessions and caches shared by the reads of one application."""
71
+
72
+ sessions: async_sessionmaker[AsyncSession]
73
+ read: ReadSettings
74
+ search: SearchSettings
75
+ store: StoreRuntime
76
+ cache: ResponseCache | None = None
77
+ sensitivity: SensitivitySettings = field(default_factory=SensitivitySettings)
78
+ schemas: SchemaCache = field(init=False)
79
+ snapshot_builds: asyncio.Semaphore = field(init=False)
80
+ """Snapshot builds running in this process (`read.time_travel`)."""
81
+ snapshot_rates: InMemoryRateStore = field(init=False)
82
+ """Snapshot builds started per principal in this process."""
83
+
84
+ def __post_init__(self) -> None:
85
+ self.schemas = SchemaCache(self.read.schema_cache_entries)
86
+ self.snapshot_builds = asyncio.Semaphore(
87
+ self.read.time_travel.snapshot_max_concurrent_builds
88
+ )
89
+ self.snapshot_rates = InMemoryRateStore(SNAPSHOT_RATE_BUCKETS)
90
+
91
+ @property
92
+ def store_settings(self) -> StoreSettings:
93
+ return self.store.settings
94
+
95
+ @property
96
+ def vault(self) -> VaultKeys | None:
97
+ """Keys of the personal data vault; None when it is not configured."""
98
+ return self.store.vault
99
+
100
+ @classmethod
101
+ def build(cls, settings: Settings, sessions: async_sessionmaker[AsyncSession]) -> ReadRuntime:
102
+ reads = keeper_settings(settings).reads
103
+ return cls(
104
+ sessions=sessions,
105
+ read=read_settings(settings),
106
+ search=search_settings(settings),
107
+ store=StoreRuntime.from_loaded(settings),
108
+ cache=build_cache(reads.cache),
109
+ sensitivity=reads.sensitivity,
110
+ )
111
+
112
+
113
+ def get_read_runtime(request: Request) -> ReadRuntime:
114
+ """The read runtime of the application, built on first use; 503 without a database."""
115
+ state = request.app.state
116
+ runtime = getattr(state, _STATE_KEY, None)
117
+ if runtime is not None:
118
+ return cast(ReadRuntime, runtime)
119
+ database = getattr(state, "database", None)
120
+ if database is None:
121
+ raise ProblemError(UNAVAILABLE, "Reads need a configured database.")
122
+ runtime = ReadRuntime.build(cast(Settings, state.settings), database.sessions)
123
+ setattr(state, _STATE_KEY, runtime)
124
+ return runtime
125
+
126
+
127
+ ReadRuntimeDep = Annotated[ReadRuntime, Depends(get_read_runtime)]
128
+
129
+
130
+ async def begin_read(session: AsyncSession, runtime: ReadRuntime) -> None:
131
+ """Make the session's (new) transaction read-only with the read statement timeout."""
132
+ await session.execute(text("SET TRANSACTION READ ONLY"))
133
+ await session.execute(
134
+ select(func.set_config("statement_timeout", str(runtime.read.statement_timeout_ms), True))
135
+ )
136
+
137
+
138
+ async def read_session(runtime: ReadRuntimeDep) -> AsyncIterator[AsyncSession]:
139
+ """A session in a read-only transaction, closed after the response."""
140
+ async with runtime.sessions() as session, session.begin():
141
+ await begin_read(session, runtime)
142
+ yield session
143
+
144
+
145
+ ReadSessionDep = Annotated[AsyncSession, Depends(read_session)]