lorebase-client 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_client/__init__.py +86 -0
- lorebase_client/aclient.py +1585 -0
- lorebase_client/client.py +1587 -0
- lorebase_client/config.py +160 -0
- lorebase_client/core.py +330 -0
- lorebase_client/endpoints.py +1292 -0
- lorebase_client/errors.py +242 -0
- lorebase_client/models.py +1662 -0
- lorebase_client/py.typed +0 -0
- lorebase_client-0.2.3.dist-info/METADATA +7 -0
- lorebase_client-0.2.3.dist-info/RECORD +12 -0
- lorebase_client-0.2.3.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,1585 @@
|
|
|
1
|
+
"""Asynchronous client: `AsyncLorebaseClient`.
|
|
2
|
+
|
|
3
|
+
The same API as `LorebaseClient` (`lorebase_client.client`) with coroutines. List endpoints
|
|
4
|
+
have `list` (one page, awaited) and `iterate` (an async iterator over all items).
|
|
5
|
+
|
|
6
|
+
async with AsyncLorebaseClient("http://localhost:8080", api_key="lbk_...") as client:
|
|
7
|
+
async for record in client.records.iterate("supply", "Supplier", branch="work"):
|
|
8
|
+
print(record.key, record.title)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import asyncio
|
|
14
|
+
import copy
|
|
15
|
+
import os
|
|
16
|
+
from collections.abc import AsyncIterator, Awaitable, Callable, Mapping, Sequence
|
|
17
|
+
from dataclasses import replace
|
|
18
|
+
from types import TracebackType
|
|
19
|
+
from typing import Any, Self
|
|
20
|
+
|
|
21
|
+
import httpx
|
|
22
|
+
from pydantic import JsonValue
|
|
23
|
+
|
|
24
|
+
from lorebase_client import endpoints as ep
|
|
25
|
+
from lorebase_client import models as m
|
|
26
|
+
from lorebase_client.config import (
|
|
27
|
+
ClientConfig,
|
|
28
|
+
RetryPolicy,
|
|
29
|
+
TokenSource,
|
|
30
|
+
TraceMode,
|
|
31
|
+
check_session_id,
|
|
32
|
+
resolve_config,
|
|
33
|
+
)
|
|
34
|
+
from lorebase_client.core import Call, Done, Exchange, RequestCore, Retry, new_idempotency_key
|
|
35
|
+
|
|
36
|
+
JsonObject = Mapping[str, Any]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class AsyncLorebaseClient:
|
|
40
|
+
"""Non-blocking client over `httpx.AsyncClient`; arguments as for `LorebaseClient`.
|
|
41
|
+
|
|
42
|
+
`transport` may be an `httpx.ASGITransport` to call an in-process application. `event_hooks`
|
|
43
|
+
holds coroutine functions (`request` / `response`) run on every HTTP request sent.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
base_url: str | None = None,
|
|
49
|
+
*,
|
|
50
|
+
api_key: str | None = None,
|
|
51
|
+
token: TokenSource | None = None,
|
|
52
|
+
timeout: float | None = None,
|
|
53
|
+
api_prefix: str | None = None,
|
|
54
|
+
retry: RetryPolicy | None = None,
|
|
55
|
+
auto_idempotency_keys: bool = False,
|
|
56
|
+
cache: bool | int = False,
|
|
57
|
+
headers: Mapping[str, str] | None = None,
|
|
58
|
+
session_id: str | None = None,
|
|
59
|
+
tool: str | None = None,
|
|
60
|
+
trace: TraceMode | None = None,
|
|
61
|
+
transport: httpx.AsyncBaseTransport | None = None,
|
|
62
|
+
trust_env: bool = True,
|
|
63
|
+
event_hooks: Mapping[str, Sequence[Callable[..., Any]]] | None = None,
|
|
64
|
+
env: Mapping[str, str] | None = None,
|
|
65
|
+
sleep: Callable[[float], Awaitable[None]] = asyncio.sleep,
|
|
66
|
+
) -> None:
|
|
67
|
+
self.config: ClientConfig = resolve_config(
|
|
68
|
+
base_url,
|
|
69
|
+
api_key=api_key,
|
|
70
|
+
token=token,
|
|
71
|
+
timeout=timeout,
|
|
72
|
+
api_prefix=api_prefix,
|
|
73
|
+
retry=retry,
|
|
74
|
+
auto_idempotency_keys=auto_idempotency_keys,
|
|
75
|
+
cache=cache,
|
|
76
|
+
headers=headers,
|
|
77
|
+
session_id=session_id,
|
|
78
|
+
tool=tool,
|
|
79
|
+
trace=trace,
|
|
80
|
+
env=env,
|
|
81
|
+
)
|
|
82
|
+
self._core = RequestCore(self.config)
|
|
83
|
+
self._sleep = sleep
|
|
84
|
+
self._http = httpx.AsyncClient(
|
|
85
|
+
timeout=self.config.timeout_s,
|
|
86
|
+
transport=transport,
|
|
87
|
+
trust_env=trust_env,
|
|
88
|
+
event_hooks={k: list(v) for k, v in (event_hooks or {}).items()},
|
|
89
|
+
)
|
|
90
|
+
self._mount()
|
|
91
|
+
|
|
92
|
+
def _mount(self) -> None:
|
|
93
|
+
self.domains = Domains(self)
|
|
94
|
+
self.branches = Branches(self)
|
|
95
|
+
self.tags = Tags(self)
|
|
96
|
+
self.schema = SchemaApi(self)
|
|
97
|
+
self.operations = Operations(self)
|
|
98
|
+
self.records = Records(self)
|
|
99
|
+
self.revisions = Revisions(self)
|
|
100
|
+
self.api_keys = ApiKeys(self)
|
|
101
|
+
self.vault = Vault(self)
|
|
102
|
+
self.grants = Grants(self)
|
|
103
|
+
self.principals = Principals(self)
|
|
104
|
+
self.audit = Audit(self)
|
|
105
|
+
self.trace = Trace(self)
|
|
106
|
+
self.proposals = Proposals(self)
|
|
107
|
+
self.agents = Agents(self)
|
|
108
|
+
self.webhooks = Webhooks(self)
|
|
109
|
+
self.sources = Sources(self)
|
|
110
|
+
self.documents = Documents(self)
|
|
111
|
+
|
|
112
|
+
def with_session(
|
|
113
|
+
self, session_id: str, *, tool: str | None = None, trace: TraceMode | None = None
|
|
114
|
+
) -> Self:
|
|
115
|
+
"""A client for another agent trace session over the same connection and cache.
|
|
116
|
+
|
|
117
|
+
It sends `session_id` (and `tool`, `trace` when given; otherwise this client's) with
|
|
118
|
+
every request. Closing either client closes the shared connection.
|
|
119
|
+
"""
|
|
120
|
+
derived = copy.copy(self)
|
|
121
|
+
derived.config = replace(
|
|
122
|
+
self.config,
|
|
123
|
+
session_id=check_session_id(session_id),
|
|
124
|
+
tool=tool or self.config.tool,
|
|
125
|
+
trace=trace or self.config.trace,
|
|
126
|
+
)
|
|
127
|
+
derived._core = RequestCore(derived.config)
|
|
128
|
+
derived._core.cache = self._core.cache
|
|
129
|
+
derived._mount()
|
|
130
|
+
return derived
|
|
131
|
+
|
|
132
|
+
async def aclose(self) -> None:
|
|
133
|
+
await self._http.aclose()
|
|
134
|
+
|
|
135
|
+
async def __aenter__(self) -> Self:
|
|
136
|
+
return self
|
|
137
|
+
|
|
138
|
+
async def __aexit__(
|
|
139
|
+
self,
|
|
140
|
+
exc_type: type[BaseException] | None,
|
|
141
|
+
exc: BaseException | None,
|
|
142
|
+
tb: TracebackType | None,
|
|
143
|
+
) -> None:
|
|
144
|
+
await self.aclose()
|
|
145
|
+
|
|
146
|
+
# Request layer ---------------------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
async def send[T](self, call: Call[T]) -> T:
|
|
149
|
+
"""Run a call, retrying as the retry policy says; returns the parsed answer."""
|
|
150
|
+
exchange = Exchange(self._core, call)
|
|
151
|
+
step: Done[T] | Retry
|
|
152
|
+
while True:
|
|
153
|
+
request = self._http.build_request(**exchange.request_args())
|
|
154
|
+
try:
|
|
155
|
+
response = await self._http.send(request)
|
|
156
|
+
except httpx.TransportError as error:
|
|
157
|
+
step = exchange.on_error(error)
|
|
158
|
+
else:
|
|
159
|
+
step = exchange.on_response(response)
|
|
160
|
+
if isinstance(step, Done):
|
|
161
|
+
return step.value
|
|
162
|
+
await self._sleep(step.delay)
|
|
163
|
+
|
|
164
|
+
async def pages[P: m.Paged[Any]](self, call: Call[P]) -> AsyncIterator[P]:
|
|
165
|
+
"""Every page of a list call, following `next_cursor`."""
|
|
166
|
+
cursor = call.params.get("cursor")
|
|
167
|
+
while True:
|
|
168
|
+
page = await self.send(call.with_params(cursor=cursor))
|
|
169
|
+
yield page
|
|
170
|
+
cursor = page.next_cursor
|
|
171
|
+
if cursor is None:
|
|
172
|
+
return
|
|
173
|
+
|
|
174
|
+
async def iterate[T](self, call: Call[m.Paged[T]]) -> AsyncIterator[T]:
|
|
175
|
+
"""Every item of a list call."""
|
|
176
|
+
async for page in self.pages(call):
|
|
177
|
+
for item in page.items:
|
|
178
|
+
yield item
|
|
179
|
+
|
|
180
|
+
async def whoami(self) -> m.WhoAmI:
|
|
181
|
+
"""The authenticated caller."""
|
|
182
|
+
return await self.send(ep.whoami())
|
|
183
|
+
|
|
184
|
+
def idempotency_key(self, prefix: str | None = None) -> str:
|
|
185
|
+
"""A fresh key to pass as `idempotency_key=` to a write."""
|
|
186
|
+
return new_idempotency_key(prefix)
|
|
187
|
+
|
|
188
|
+
def cache_clear(self) -> None:
|
|
189
|
+
"""Forget cached answers (no effect when the cache is off)."""
|
|
190
|
+
if self._core.cache is not None:
|
|
191
|
+
self._core.cache.clear()
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class _Resource:
|
|
195
|
+
def __init__(self, client: AsyncLorebaseClient) -> None:
|
|
196
|
+
self._c = client
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class Domains(_Resource):
|
|
200
|
+
async def create(
|
|
201
|
+
self,
|
|
202
|
+
id: str,
|
|
203
|
+
*,
|
|
204
|
+
display_name: str = "",
|
|
205
|
+
description: str = "",
|
|
206
|
+
default_branch: str | None = None,
|
|
207
|
+
settings: JsonObject | None = None,
|
|
208
|
+
idempotency_key: str | None = None,
|
|
209
|
+
) -> m.Domain:
|
|
210
|
+
body = m.DomainCreate(
|
|
211
|
+
id=id,
|
|
212
|
+
display_name=display_name,
|
|
213
|
+
description=description,
|
|
214
|
+
default_branch=default_branch,
|
|
215
|
+
settings=dict(settings or {}),
|
|
216
|
+
)
|
|
217
|
+
return await self._c.send(ep.create_domain(body, idempotency_key=idempotency_key))
|
|
218
|
+
|
|
219
|
+
async def list(
|
|
220
|
+
self, *, limit: int | None = None, cursor: str | None = None
|
|
221
|
+
) -> m.Page[m.Domain]:
|
|
222
|
+
return await self._c.send(ep.list_domains(limit=limit, cursor=cursor))
|
|
223
|
+
|
|
224
|
+
def iterate(self, *, page_size: int | None = None) -> AsyncIterator[m.Domain]:
|
|
225
|
+
return self._c.iterate(ep.list_domains(limit=page_size))
|
|
226
|
+
|
|
227
|
+
async def get(self, id: str) -> m.Domain:
|
|
228
|
+
return await self._c.send(ep.get_domain(id))
|
|
229
|
+
|
|
230
|
+
async def update(
|
|
231
|
+
self, id: str, *, display_name: str | None = None, description: str | None = None
|
|
232
|
+
) -> m.Domain:
|
|
233
|
+
body = m.DomainUpdate(display_name=display_name, description=description)
|
|
234
|
+
return await self._c.send(ep.update_domain(id, body))
|
|
235
|
+
|
|
236
|
+
async def delete(self, id: str, *, confirm: str) -> None:
|
|
237
|
+
"""Delete the Domain with everything in it; `confirm` repeats its id."""
|
|
238
|
+
await self._c.send(ep.delete_domain(id, confirm=confirm))
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
class Branches(_Resource):
|
|
242
|
+
async def list(
|
|
243
|
+
self, domain: str, *, limit: int | None = None, cursor: str | None = None
|
|
244
|
+
) -> m.Page[m.Branch]:
|
|
245
|
+
return await self._c.send(ep.list_branches(domain, limit=limit, cursor=cursor))
|
|
246
|
+
|
|
247
|
+
def iterate(self, domain: str, *, page_size: int | None = None) -> AsyncIterator[m.Branch]:
|
|
248
|
+
return self._c.iterate(ep.list_branches(domain, limit=page_size))
|
|
249
|
+
|
|
250
|
+
async def create(
|
|
251
|
+
self,
|
|
252
|
+
domain: str,
|
|
253
|
+
name: str,
|
|
254
|
+
*,
|
|
255
|
+
source: str | None = None,
|
|
256
|
+
revision: str | None = None,
|
|
257
|
+
mode: m.BranchMode | None = None,
|
|
258
|
+
idempotency_key: str | None = None,
|
|
259
|
+
) -> m.Branch:
|
|
260
|
+
body = m.BranchCreate(name=name, source=source, revision=revision, mode=mode)
|
|
261
|
+
return await self._c.send(ep.create_branch(domain, body, idempotency_key=idempotency_key))
|
|
262
|
+
|
|
263
|
+
async def get(self, domain: str, name: str) -> m.BranchDetail:
|
|
264
|
+
return await self._c.send(ep.get_branch(domain, name))
|
|
265
|
+
|
|
266
|
+
async def set_protected(self, domain: str, name: str, protected: bool) -> m.Branch:
|
|
267
|
+
return await self._c.send(ep.protect_branch(domain, name, protected=protected))
|
|
268
|
+
|
|
269
|
+
async def delete(self, domain: str, name: str) -> None:
|
|
270
|
+
await self._c.send(ep.delete_branch(domain, name))
|
|
271
|
+
|
|
272
|
+
async def rebase_tags(
|
|
273
|
+
self,
|
|
274
|
+
domain: str,
|
|
275
|
+
name: str,
|
|
276
|
+
*,
|
|
277
|
+
message: str | None = None,
|
|
278
|
+
expected_head: str | None = None,
|
|
279
|
+
dry_run: bool = False,
|
|
280
|
+
rows_only: bool = False,
|
|
281
|
+
) -> m.RebaseTagsResult:
|
|
282
|
+
"""Commit the default branch's sensitivity tags to the branch's schema (or, with
|
|
283
|
+
`rows_only`, queue the rewrite of its stale titles and search vectors)."""
|
|
284
|
+
body = m.RebaseTagsRequest(
|
|
285
|
+
message=message, expected_head=expected_head, dry_run=dry_run, rows_only=rows_only
|
|
286
|
+
)
|
|
287
|
+
return await self._c.send(ep.rebase_branch_tags(domain, name, body))
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
class Tags(_Resource):
|
|
291
|
+
async def list(
|
|
292
|
+
self, domain: str, *, limit: int | None = None, cursor: str | None = None
|
|
293
|
+
) -> m.Page[m.Tag]:
|
|
294
|
+
return await self._c.send(ep.list_tags(domain, limit=limit, cursor=cursor))
|
|
295
|
+
|
|
296
|
+
def iterate(self, domain: str, *, page_size: int | None = None) -> AsyncIterator[m.Tag]:
|
|
297
|
+
return self._c.iterate(ep.list_tags(domain, limit=page_size))
|
|
298
|
+
|
|
299
|
+
async def create(
|
|
300
|
+
self,
|
|
301
|
+
domain: str,
|
|
302
|
+
name: str,
|
|
303
|
+
*,
|
|
304
|
+
source: str | None = None,
|
|
305
|
+
revision: str | None = None,
|
|
306
|
+
idempotency_key: str | None = None,
|
|
307
|
+
) -> m.Tag:
|
|
308
|
+
body = m.TagCreate(name=name, source=source, revision=revision)
|
|
309
|
+
return await self._c.send(ep.create_tag(domain, body, idempotency_key=idempotency_key))
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
class SchemaApi(_Resource):
|
|
313
|
+
async def get(
|
|
314
|
+
self, domain: str, *, branch: str | None = None, at: str | None = None
|
|
315
|
+
) -> m.SchemaDocument:
|
|
316
|
+
return await self._c.send(ep.get_schema(domain, branch=branch, at=at))
|
|
317
|
+
|
|
318
|
+
async def get_yaml(
|
|
319
|
+
self, domain: str, *, branch: str | None = None, at: str | None = None
|
|
320
|
+
) -> str:
|
|
321
|
+
return await self._c.send(ep.get_schema_yaml(domain, branch=branch, at=at))
|
|
322
|
+
|
|
323
|
+
async def element(
|
|
324
|
+
self,
|
|
325
|
+
domain: str,
|
|
326
|
+
section: m.SchemaSection,
|
|
327
|
+
name: str,
|
|
328
|
+
*,
|
|
329
|
+
branch: str | None = None,
|
|
330
|
+
at: str | None = None,
|
|
331
|
+
) -> dict[str, Any]:
|
|
332
|
+
return await self._c.send(
|
|
333
|
+
ep.get_schema_element(domain, section, name, branch=branch, at=at)
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
async def validate(
|
|
337
|
+
self,
|
|
338
|
+
domain: str,
|
|
339
|
+
*,
|
|
340
|
+
yaml: str | None = None,
|
|
341
|
+
patch: JsonObject | None = None,
|
|
342
|
+
remove: Sequence[m.ElementRemoval] | None = None,
|
|
343
|
+
branch: str | None = None,
|
|
344
|
+
at: str | None = None,
|
|
345
|
+
) -> m.SchemaValidation:
|
|
346
|
+
body = m.SchemaValidateRequest(
|
|
347
|
+
yaml=yaml,
|
|
348
|
+
patch=None if patch is None else dict(patch),
|
|
349
|
+
remove=list(remove or []),
|
|
350
|
+
branch=branch,
|
|
351
|
+
at=at,
|
|
352
|
+
)
|
|
353
|
+
return await self._c.send(ep.validate_schema(domain, body))
|
|
354
|
+
|
|
355
|
+
async def apply(
|
|
356
|
+
self,
|
|
357
|
+
domain: str,
|
|
358
|
+
branch: str,
|
|
359
|
+
*,
|
|
360
|
+
yaml: str | None = None,
|
|
361
|
+
patch: JsonObject | None = None,
|
|
362
|
+
remove: Sequence[m.ElementRemoval] | None = None,
|
|
363
|
+
message: str | None = None,
|
|
364
|
+
expected_head: str | None = None,
|
|
365
|
+
dry_run: bool = False,
|
|
366
|
+
cascade: bool = False,
|
|
367
|
+
) -> m.SchemaChangeResult:
|
|
368
|
+
body = m.SchemaChangeRequest(
|
|
369
|
+
yaml=yaml,
|
|
370
|
+
patch=None if patch is None else dict(patch),
|
|
371
|
+
remove=list(remove or []),
|
|
372
|
+
message=message,
|
|
373
|
+
expected_head=expected_head,
|
|
374
|
+
dry_run=dry_run,
|
|
375
|
+
cascade=cascade,
|
|
376
|
+
)
|
|
377
|
+
return await self._c.send(ep.apply_schema(domain, branch, body))
|
|
378
|
+
|
|
379
|
+
async def infer_postgres(
|
|
380
|
+
self,
|
|
381
|
+
domain: str,
|
|
382
|
+
dsn: str,
|
|
383
|
+
*,
|
|
384
|
+
schema: str | None = None,
|
|
385
|
+
tables: Sequence[str] | None = None,
|
|
386
|
+
options: JsonObject | None = None,
|
|
387
|
+
) -> dict[str, Any]:
|
|
388
|
+
"""Propose a schema from Postgres tables (`dsn` may be a `secret://` reference)."""
|
|
389
|
+
return await self._c.send(
|
|
390
|
+
ep.infer_schema_postgres(domain, _infer_body(dsn, schema, tables, options))
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
async def infer_files(
|
|
394
|
+
self,
|
|
395
|
+
domain: str,
|
|
396
|
+
files: Mapping[str, bytes],
|
|
397
|
+
*,
|
|
398
|
+
options: JsonObject | None = None,
|
|
399
|
+
records_path: str | None = None,
|
|
400
|
+
) -> dict[str, Any]:
|
|
401
|
+
"""Propose a schema from data files (file name to content); `records_path` (dotted keys)
|
|
402
|
+
locates the records array of JSON documents that hold it under a key."""
|
|
403
|
+
return await self._c.send(
|
|
404
|
+
ep.infer_schema_files(domain, files, options=options, records_path=records_path)
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
async def preview_files(
|
|
408
|
+
self,
|
|
409
|
+
domain: str,
|
|
410
|
+
files: Mapping[str, bytes],
|
|
411
|
+
*,
|
|
412
|
+
rows: int | None = None,
|
|
413
|
+
records_path: str | None = None,
|
|
414
|
+
) -> dict[str, Any]:
|
|
415
|
+
"""The first rows of each data file as Keeper reads them; nothing is stored."""
|
|
416
|
+
return await self._c.send(
|
|
417
|
+
ep.preview_files(domain, files, rows=rows, records_path=records_path)
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
async def preview_derivation(self, domain: str, body: JsonObject) -> dict[str, Any]:
|
|
421
|
+
"""Evaluate a candidate Derivation on sample Records without saving it."""
|
|
422
|
+
return await self._c.send(ep.preview_derivation(domain, body))
|
|
423
|
+
|
|
424
|
+
async def preview_source(self, domain: str, body: JsonObject) -> dict[str, Any]:
|
|
425
|
+
"""Evaluate the expression mappings of a candidate Source on sample rows."""
|
|
426
|
+
return await self._c.send(ep.preview_source(domain, body))
|
|
427
|
+
|
|
428
|
+
async def suggestions(
|
|
429
|
+
self,
|
|
430
|
+
domain: str,
|
|
431
|
+
*,
|
|
432
|
+
status: Sequence[m.ItemDecisionValue] | None = None,
|
|
433
|
+
type: Sequence[m.SuggestionType] | None = None,
|
|
434
|
+
all: bool | None = None,
|
|
435
|
+
limit: int | None = None,
|
|
436
|
+
cursor: str | None = None,
|
|
437
|
+
) -> m.Page[m.SchemaSuggestion]:
|
|
438
|
+
"""One page of schema suggestions from extraction, the most supported first."""
|
|
439
|
+
return await self._c.send(
|
|
440
|
+
ep.list_schema_suggestions(
|
|
441
|
+
domain, status=status, type=type, all=all, limit=limit, cursor=cursor
|
|
442
|
+
)
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
def iterate_suggestions(
|
|
446
|
+
self,
|
|
447
|
+
domain: str,
|
|
448
|
+
*,
|
|
449
|
+
status: Sequence[m.ItemDecisionValue] | None = None,
|
|
450
|
+
type: Sequence[m.SuggestionType] | None = None,
|
|
451
|
+
all: bool | None = None,
|
|
452
|
+
page_size: int | None = None,
|
|
453
|
+
) -> AsyncIterator[m.SchemaSuggestion]:
|
|
454
|
+
call = ep.list_schema_suggestions(
|
|
455
|
+
domain, status=status, type=type, all=all, limit=page_size
|
|
456
|
+
)
|
|
457
|
+
return self._c.iterate(call)
|
|
458
|
+
|
|
459
|
+
async def decide_suggestions(
|
|
460
|
+
self, domain: str, decisions: Sequence[m.SuggestionDecision | JsonObject]
|
|
461
|
+
) -> m.SuggestionsDecided:
|
|
462
|
+
"""Reject, reset or accept suggestions; the accepted ones become one schema change
|
|
463
|
+
set on a new branch, proposed into the default branch (`proposal`)."""
|
|
464
|
+
body = m.DecideSuggestions(
|
|
465
|
+
decisions=[
|
|
466
|
+
d if isinstance(d, m.SuggestionDecision) else m.SuggestionDecision(**d)
|
|
467
|
+
for d in decisions
|
|
468
|
+
]
|
|
469
|
+
)
|
|
470
|
+
return await self._c.send(ep.decide_schema_suggestions(domain, body))
|
|
471
|
+
|
|
472
|
+
async def merge_suggestions(self, domain: str) -> m.SuggestionsMerged:
|
|
473
|
+
"""Fold the stored Attribute suggestions that are variants of one another."""
|
|
474
|
+
return await self._c.send(ep.merge_schema_suggestions(domain))
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
class Operations(_Resource):
|
|
478
|
+
async def execute(
|
|
479
|
+
self,
|
|
480
|
+
domain: str,
|
|
481
|
+
operation: str,
|
|
482
|
+
parameters: JsonObject | None = None,
|
|
483
|
+
*,
|
|
484
|
+
branch: str | None = None,
|
|
485
|
+
dry_run: bool = False,
|
|
486
|
+
expected_head: str | None = None,
|
|
487
|
+
message: str | None = None,
|
|
488
|
+
idempotency_key: str | None = None,
|
|
489
|
+
) -> m.Execution:
|
|
490
|
+
body = m.ExecuteRequest(
|
|
491
|
+
parameters=dict(parameters or {}),
|
|
492
|
+
branch=branch,
|
|
493
|
+
dry_run=dry_run,
|
|
494
|
+
expected_head=expected_head,
|
|
495
|
+
message=message,
|
|
496
|
+
)
|
|
497
|
+
return await self._c.send(
|
|
498
|
+
ep.execute_operation(domain, operation, body, idempotency_key=idempotency_key)
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
async def execute_as_agent(
|
|
502
|
+
self,
|
|
503
|
+
domain: str,
|
|
504
|
+
operation: str,
|
|
505
|
+
parameters: JsonObject | None = None,
|
|
506
|
+
*,
|
|
507
|
+
task: str | None = None,
|
|
508
|
+
intent: str | None = None,
|
|
509
|
+
auto_propose: bool | None = None,
|
|
510
|
+
auto_merge: bool | None = None,
|
|
511
|
+
branch: str | None = None,
|
|
512
|
+
dry_run: bool = False,
|
|
513
|
+
expected_head: str | None = None,
|
|
514
|
+
message: str | None = None,
|
|
515
|
+
idempotency_key: str | None = None,
|
|
516
|
+
) -> m.Execution | m.AgentWrite:
|
|
517
|
+
"""Execute an Operation that may need review. When it does (protected branch, review
|
|
518
|
+
policy) it runs on the branch `agent/{principal}/{task}` and the answer is an
|
|
519
|
+
`AgentWrite` naming the Proposal; otherwise it is the usual `Execution`. Without a `task`
|
|
520
|
+
Keeper refuses `auto_propose` and answers 409 `review-required` when review is needed."""
|
|
521
|
+
body = m.ExecuteRequest(
|
|
522
|
+
parameters=dict(parameters or {}),
|
|
523
|
+
branch=branch,
|
|
524
|
+
dry_run=dry_run,
|
|
525
|
+
expected_head=expected_head,
|
|
526
|
+
message=message,
|
|
527
|
+
task=task,
|
|
528
|
+
intent=intent,
|
|
529
|
+
auto_propose=auto_propose,
|
|
530
|
+
auto_merge=auto_merge,
|
|
531
|
+
)
|
|
532
|
+
return await self._c.send(
|
|
533
|
+
ep.execute_operation_as_agent(domain, operation, body, idempotency_key=idempotency_key)
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
async def import_records(
|
|
537
|
+
self,
|
|
538
|
+
domain: str,
|
|
539
|
+
kind: str,
|
|
540
|
+
rows: Sequence[JsonObject],
|
|
541
|
+
*,
|
|
542
|
+
branch: str | None = None,
|
|
543
|
+
dry_run: bool = False,
|
|
544
|
+
expected_head: str | None = None,
|
|
545
|
+
message: str | None = None,
|
|
546
|
+
idempotency_key: str | None = None,
|
|
547
|
+
) -> m.Execution:
|
|
548
|
+
"""Upsert Records of `kind`; rows are objects keyed by Attribute api name."""
|
|
549
|
+
return await self.execute(
|
|
550
|
+
domain,
|
|
551
|
+
"import_records",
|
|
552
|
+
{"kind": kind, "rows": list(rows)},
|
|
553
|
+
branch=branch,
|
|
554
|
+
dry_run=dry_run,
|
|
555
|
+
expected_head=expected_head,
|
|
556
|
+
message=message,
|
|
557
|
+
idempotency_key=idempotency_key,
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
async def import_links(
|
|
561
|
+
self,
|
|
562
|
+
domain: str,
|
|
563
|
+
relation: str,
|
|
564
|
+
rows: Sequence[JsonObject],
|
|
565
|
+
*,
|
|
566
|
+
branch: str | None = None,
|
|
567
|
+
dry_run: bool = False,
|
|
568
|
+
expected_head: str | None = None,
|
|
569
|
+
message: str | None = None,
|
|
570
|
+
idempotency_key: str | None = None,
|
|
571
|
+
) -> m.Execution:
|
|
572
|
+
"""Upsert Links of a forward Relation; rows have `from`, `to` and Link Attributes."""
|
|
573
|
+
return await self.execute(
|
|
574
|
+
domain,
|
|
575
|
+
"import_links",
|
|
576
|
+
{"relation": relation, "rows": list(rows)},
|
|
577
|
+
branch=branch,
|
|
578
|
+
dry_run=dry_run,
|
|
579
|
+
expected_head=expected_head,
|
|
580
|
+
message=message,
|
|
581
|
+
idempotency_key=idempotency_key,
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
async def import_batch(
|
|
585
|
+
self,
|
|
586
|
+
domain: str,
|
|
587
|
+
records: Sequence[JsonObject] = (),
|
|
588
|
+
links: Sequence[JsonObject] = (),
|
|
589
|
+
*,
|
|
590
|
+
branch: str | None = None,
|
|
591
|
+
dry_run: bool = False,
|
|
592
|
+
expected_head: str | None = None,
|
|
593
|
+
message: str | None = None,
|
|
594
|
+
idempotency_key: str | None = None,
|
|
595
|
+
) -> m.Execution:
|
|
596
|
+
"""Upsert Records (rows with `kind`) and Links (rows with `relation`, `from`, `to`) in
|
|
597
|
+
one call; a Link goes in the same Revision as the Record at one of its ends."""
|
|
598
|
+
return await self.execute(
|
|
599
|
+
domain,
|
|
600
|
+
"import_batch",
|
|
601
|
+
{"records": list(records), "links": list(links)},
|
|
602
|
+
branch=branch,
|
|
603
|
+
dry_run=dry_run,
|
|
604
|
+
expected_head=expected_head,
|
|
605
|
+
message=message,
|
|
606
|
+
idempotency_key=idempotency_key,
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
async def delete_records(
|
|
610
|
+
self,
|
|
611
|
+
domain: str,
|
|
612
|
+
kind: str,
|
|
613
|
+
keys: Sequence[str],
|
|
614
|
+
*,
|
|
615
|
+
skip_missing: bool = False,
|
|
616
|
+
branch: str | None = None,
|
|
617
|
+
dry_run: bool = False,
|
|
618
|
+
expected_head: str | None = None,
|
|
619
|
+
message: str | None = None,
|
|
620
|
+
idempotency_key: str | None = None,
|
|
621
|
+
) -> m.Execution:
|
|
622
|
+
"""Delete Records of `kind` by key in one Revision; Links follow each Relation's
|
|
623
|
+
`on_delete`. Unknown keys fail the call unless `skip_missing` (then warnings)."""
|
|
624
|
+
parameters: dict[str, Any] = {"kind": kind, "keys": list(keys)}
|
|
625
|
+
if skip_missing:
|
|
626
|
+
parameters["skip_missing"] = True
|
|
627
|
+
return await self.execute(
|
|
628
|
+
domain,
|
|
629
|
+
"delete_records",
|
|
630
|
+
parameters,
|
|
631
|
+
branch=branch,
|
|
632
|
+
dry_run=dry_run,
|
|
633
|
+
expected_head=expected_head,
|
|
634
|
+
message=message,
|
|
635
|
+
idempotency_key=idempotency_key,
|
|
636
|
+
)
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
class Records(_Resource):
|
|
640
|
+
async def list(
|
|
641
|
+
self,
|
|
642
|
+
domain: str,
|
|
643
|
+
kind: str,
|
|
644
|
+
*,
|
|
645
|
+
branch: str | None = None,
|
|
646
|
+
at: str | None = None,
|
|
647
|
+
where: JsonObject | None = None,
|
|
648
|
+
order_by: Sequence[str] | None = None,
|
|
649
|
+
select: Sequence[str] | None = None,
|
|
650
|
+
limit: int | None = None,
|
|
651
|
+
cursor: str | None = None,
|
|
652
|
+
) -> m.RecordPage:
|
|
653
|
+
"""One page of Records; `order_by` terms are `name` or `name:desc`."""
|
|
654
|
+
return await self._c.send(
|
|
655
|
+
ep.list_records(
|
|
656
|
+
domain,
|
|
657
|
+
kind,
|
|
658
|
+
branch=branch,
|
|
659
|
+
at=at,
|
|
660
|
+
where=where,
|
|
661
|
+
order_by=order_by,
|
|
662
|
+
select=select,
|
|
663
|
+
limit=limit,
|
|
664
|
+
cursor=cursor,
|
|
665
|
+
)
|
|
666
|
+
)
|
|
667
|
+
|
|
668
|
+
def iterate(
|
|
669
|
+
self,
|
|
670
|
+
domain: str,
|
|
671
|
+
kind: str,
|
|
672
|
+
*,
|
|
673
|
+
branch: str | None = None,
|
|
674
|
+
at: str | None = None,
|
|
675
|
+
where: JsonObject | None = None,
|
|
676
|
+
order_by: Sequence[str] | None = None,
|
|
677
|
+
select: Sequence[str] | None = None,
|
|
678
|
+
page_size: int | None = None,
|
|
679
|
+
) -> AsyncIterator[m.RecordItem]:
|
|
680
|
+
call = ep.list_records(
|
|
681
|
+
domain,
|
|
682
|
+
kind,
|
|
683
|
+
branch=branch,
|
|
684
|
+
at=at,
|
|
685
|
+
where=where,
|
|
686
|
+
order_by=order_by,
|
|
687
|
+
select=select,
|
|
688
|
+
limit=page_size,
|
|
689
|
+
)
|
|
690
|
+
return self._c.iterate(call)
|
|
691
|
+
|
|
692
|
+
async def get(
|
|
693
|
+
self,
|
|
694
|
+
domain: str,
|
|
695
|
+
kind: str,
|
|
696
|
+
key: str,
|
|
697
|
+
*,
|
|
698
|
+
branch: str | None = None,
|
|
699
|
+
at: str | None = None,
|
|
700
|
+
links: bool = False,
|
|
701
|
+
lineage: bool = False,
|
|
702
|
+
) -> m.RecordResponse:
|
|
703
|
+
return await self._c.send(
|
|
704
|
+
ep.get_record(domain, kind, key, branch=branch, at=at, links=links, lineage=lineage)
|
|
705
|
+
)
|
|
706
|
+
|
|
707
|
+
async def history(
|
|
708
|
+
self,
|
|
709
|
+
domain: str,
|
|
710
|
+
kind: str,
|
|
711
|
+
key: str,
|
|
712
|
+
*,
|
|
713
|
+
branch: str | None = None,
|
|
714
|
+
at: str | None = None,
|
|
715
|
+
values: bool = False,
|
|
716
|
+
limit: int | None = None,
|
|
717
|
+
cursor: str | None = None,
|
|
718
|
+
) -> m.RecordHistory:
|
|
719
|
+
"""One page of the changes of a Record, newest first."""
|
|
720
|
+
return await self._c.send(
|
|
721
|
+
ep.record_history(
|
|
722
|
+
domain, kind, key, branch=branch, at=at, values=values, limit=limit, cursor=cursor
|
|
723
|
+
)
|
|
724
|
+
)
|
|
725
|
+
|
|
726
|
+
def iterate_history(
|
|
727
|
+
self,
|
|
728
|
+
domain: str,
|
|
729
|
+
kind: str,
|
|
730
|
+
key: str,
|
|
731
|
+
*,
|
|
732
|
+
branch: str | None = None,
|
|
733
|
+
at: str | None = None,
|
|
734
|
+
values: bool = False,
|
|
735
|
+
page_size: int | None = None,
|
|
736
|
+
) -> AsyncIterator[m.RecordChange]:
|
|
737
|
+
call = ep.record_history(
|
|
738
|
+
domain, kind, key, branch=branch, at=at, values=values, limit=page_size
|
|
739
|
+
)
|
|
740
|
+
return self._c.iterate(call)
|
|
741
|
+
|
|
742
|
+
async def lineage(
|
|
743
|
+
self,
|
|
744
|
+
domain: str,
|
|
745
|
+
kind: str,
|
|
746
|
+
key: str,
|
|
747
|
+
*,
|
|
748
|
+
branch: str | None = None,
|
|
749
|
+
at: str | None = None,
|
|
750
|
+
) -> m.RecordLineage:
|
|
751
|
+
"""Where the values of a Record came from, at the branch head or at `at`."""
|
|
752
|
+
return await self._c.send(ep.record_lineage(domain, kind, key, branch=branch, at=at))
|
|
753
|
+
|
|
754
|
+
async def links(
|
|
755
|
+
self,
|
|
756
|
+
domain: str,
|
|
757
|
+
kind: str,
|
|
758
|
+
key: str,
|
|
759
|
+
relation: str,
|
|
760
|
+
*,
|
|
761
|
+
branch: str | None = None,
|
|
762
|
+
at: str | None = None,
|
|
763
|
+
limit: int | None = None,
|
|
764
|
+
cursor: str | None = None,
|
|
765
|
+
) -> m.LinkPage:
|
|
766
|
+
"""Links of one Record along a Relation (forward or inverse api name)."""
|
|
767
|
+
return await self._c.send(
|
|
768
|
+
ep.list_record_links(
|
|
769
|
+
domain, kind, key, relation, branch=branch, at=at, limit=limit, cursor=cursor
|
|
770
|
+
)
|
|
771
|
+
)
|
|
772
|
+
|
|
773
|
+
async def relation_links(
|
|
774
|
+
self,
|
|
775
|
+
domain: str,
|
|
776
|
+
relation: str,
|
|
777
|
+
*,
|
|
778
|
+
branch: str | None = None,
|
|
779
|
+
at: str | None = None,
|
|
780
|
+
from_key: str | None = None,
|
|
781
|
+
to_key: str | None = None,
|
|
782
|
+
limit: int | None = None,
|
|
783
|
+
cursor: str | None = None,
|
|
784
|
+
) -> m.LinkPage:
|
|
785
|
+
return await self._c.send(
|
|
786
|
+
ep.list_relation_links(
|
|
787
|
+
domain,
|
|
788
|
+
relation,
|
|
789
|
+
branch=branch,
|
|
790
|
+
at=at,
|
|
791
|
+
from_key=from_key,
|
|
792
|
+
to_key=to_key,
|
|
793
|
+
limit=limit,
|
|
794
|
+
cursor=cursor,
|
|
795
|
+
)
|
|
796
|
+
)
|
|
797
|
+
|
|
798
|
+
def iterate_relation_links(
|
|
799
|
+
self,
|
|
800
|
+
domain: str,
|
|
801
|
+
relation: str,
|
|
802
|
+
*,
|
|
803
|
+
branch: str | None = None,
|
|
804
|
+
at: str | None = None,
|
|
805
|
+
from_key: str | None = None,
|
|
806
|
+
to_key: str | None = None,
|
|
807
|
+
page_size: int | None = None,
|
|
808
|
+
) -> AsyncIterator[m.LinkItem]:
|
|
809
|
+
call = ep.list_relation_links(
|
|
810
|
+
domain,
|
|
811
|
+
relation,
|
|
812
|
+
branch=branch,
|
|
813
|
+
at=at,
|
|
814
|
+
from_key=from_key,
|
|
815
|
+
to_key=to_key,
|
|
816
|
+
limit=page_size,
|
|
817
|
+
)
|
|
818
|
+
return self._c.iterate(call)
|
|
819
|
+
|
|
820
|
+
async def query(self, domain: str, query: m.QueryRequest | JsonObject) -> m.QueryResult:
|
|
821
|
+
"""Run a DSL query (a `QueryRequest` or the JSON object of the DSL)."""
|
|
822
|
+
return await self._c.send(ep.query(domain, _query_request(query)))
|
|
823
|
+
|
|
824
|
+
async def iterate_query(
|
|
825
|
+
self, domain: str, query: m.QueryRequest | JsonObject
|
|
826
|
+
) -> AsyncIterator[m.RecordItem]:
|
|
827
|
+
"""All Records a query matches, page after page."""
|
|
828
|
+
request = _query_request(query)
|
|
829
|
+
while True:
|
|
830
|
+
page = await self._c.send(ep.query(domain, request))
|
|
831
|
+
for item in page.items:
|
|
832
|
+
yield item
|
|
833
|
+
if page.next_cursor is None:
|
|
834
|
+
return
|
|
835
|
+
request = request.model_copy(update={"cursor": page.next_cursor})
|
|
836
|
+
|
|
837
|
+
async def traverse(
|
|
838
|
+
self, domain: str, request: m.TraverseRequest | JsonObject
|
|
839
|
+
) -> m.TraverseResult:
|
|
840
|
+
body = request if isinstance(request, m.TraverseRequest) else m.TraverseRequest(**request)
|
|
841
|
+
return await self._c.send(ep.traverse(domain, body))
|
|
842
|
+
|
|
843
|
+
async def neighborhood(
|
|
844
|
+
self, domain: str, request: m.NeighborhoodRequest | JsonObject
|
|
845
|
+
) -> m.NeighborhoodResult:
|
|
846
|
+
body = (
|
|
847
|
+
request
|
|
848
|
+
if isinstance(request, m.NeighborhoodRequest)
|
|
849
|
+
else m.NeighborhoodRequest(**request)
|
|
850
|
+
)
|
|
851
|
+
return await self._c.send(ep.neighborhood(domain, body))
|
|
852
|
+
|
|
853
|
+
async def search(self, domain: str, request: m.SearchRequest | JsonObject) -> m.SearchResult:
|
|
854
|
+
body = request if isinstance(request, m.SearchRequest) else m.SearchRequest(**request)
|
|
855
|
+
return await self._c.send(ep.search(domain, body))
|
|
856
|
+
|
|
857
|
+
|
|
858
|
+
def _query_request(query: m.QueryRequest | JsonObject) -> m.QueryRequest:
|
|
859
|
+
return query if isinstance(query, m.QueryRequest) else m.QueryRequest(**query)
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
def _context_request(request: m.ContextRequest | JsonObject) -> m.ContextRequest:
|
|
863
|
+
return request if isinstance(request, m.ContextRequest) else m.ContextRequest(**request)
|
|
864
|
+
|
|
865
|
+
|
|
866
|
+
def _infer_body(
|
|
867
|
+
dsn: str, schema: str | None, tables: Sequence[str] | None, options: JsonObject | None
|
|
868
|
+
) -> dict[str, Any]:
|
|
869
|
+
postgres: dict[str, Any] = {"dsn": dsn}
|
|
870
|
+
if schema is not None:
|
|
871
|
+
postgres["schema"] = schema
|
|
872
|
+
if tables is not None:
|
|
873
|
+
postgres["tables"] = list(tables)
|
|
874
|
+
body: dict[str, Any] = {"postgres": postgres}
|
|
875
|
+
if options:
|
|
876
|
+
body["options"] = dict(options)
|
|
877
|
+
return body
|
|
878
|
+
|
|
879
|
+
|
|
880
|
+
class Revisions(_Resource):
|
|
881
|
+
async def log(
|
|
882
|
+
self,
|
|
883
|
+
domain: str,
|
|
884
|
+
branch: str,
|
|
885
|
+
*,
|
|
886
|
+
all_parents: bool = False,
|
|
887
|
+
origin_type: str | None = None,
|
|
888
|
+
author: str | None = None,
|
|
889
|
+
limit: int | None = None,
|
|
890
|
+
cursor: str | None = None,
|
|
891
|
+
) -> m.LogPage:
|
|
892
|
+
return await self._c.send(
|
|
893
|
+
ep.branch_log(
|
|
894
|
+
domain,
|
|
895
|
+
branch,
|
|
896
|
+
all_parents=all_parents,
|
|
897
|
+
origin_type=origin_type,
|
|
898
|
+
author=author,
|
|
899
|
+
limit=limit,
|
|
900
|
+
cursor=cursor,
|
|
901
|
+
)
|
|
902
|
+
)
|
|
903
|
+
|
|
904
|
+
def iterate_log(
|
|
905
|
+
self,
|
|
906
|
+
domain: str,
|
|
907
|
+
branch: str,
|
|
908
|
+
*,
|
|
909
|
+
all_parents: bool = False,
|
|
910
|
+
origin_type: str | None = None,
|
|
911
|
+
author: str | None = None,
|
|
912
|
+
page_size: int | None = None,
|
|
913
|
+
) -> AsyncIterator[m.Revision]:
|
|
914
|
+
call = ep.branch_log(
|
|
915
|
+
domain,
|
|
916
|
+
branch,
|
|
917
|
+
all_parents=all_parents,
|
|
918
|
+
origin_type=origin_type,
|
|
919
|
+
author=author,
|
|
920
|
+
limit=page_size,
|
|
921
|
+
)
|
|
922
|
+
return self._c.iterate(call)
|
|
923
|
+
|
|
924
|
+
async def get(self, domain: str, revision: str) -> m.Revision:
|
|
925
|
+
"""A Revision by id, tag or branch name."""
|
|
926
|
+
return await self._c.send(ep.get_revision(domain, revision))
|
|
927
|
+
|
|
928
|
+
async def counts(self, domain: str, revision: str) -> m.RevisionCounts:
|
|
929
|
+
return await self._c.send(ep.revision_counts(domain, revision))
|
|
930
|
+
|
|
931
|
+
async def changeset(
|
|
932
|
+
self,
|
|
933
|
+
domain: str,
|
|
934
|
+
revision: str,
|
|
935
|
+
*,
|
|
936
|
+
limit: int | None = None,
|
|
937
|
+
cursor: str | None = None,
|
|
938
|
+
) -> m.ChangesPage:
|
|
939
|
+
return await self._c.send(
|
|
940
|
+
ep.revision_changeset(domain, revision, limit=limit, cursor=cursor)
|
|
941
|
+
)
|
|
942
|
+
|
|
943
|
+
def iterate_changeset(
|
|
944
|
+
self, domain: str, revision: str, *, page_size: int | None = None
|
|
945
|
+
) -> AsyncIterator[m.Change]:
|
|
946
|
+
return self._c.iterate(ep.revision_changeset(domain, revision, limit=page_size))
|
|
947
|
+
|
|
948
|
+
async def diff(
|
|
949
|
+
self,
|
|
950
|
+
domain: str,
|
|
951
|
+
from_: str,
|
|
952
|
+
to: str,
|
|
953
|
+
*,
|
|
954
|
+
kinds: Sequence[str] | None = None,
|
|
955
|
+
relations: Sequence[str] | None = None,
|
|
956
|
+
schema: bool | None = None,
|
|
957
|
+
limit: int | None = None,
|
|
958
|
+
cursor: str | None = None,
|
|
959
|
+
) -> m.ChangesPage:
|
|
960
|
+
return await self._c.send(
|
|
961
|
+
ep.diff(
|
|
962
|
+
domain,
|
|
963
|
+
from_,
|
|
964
|
+
to,
|
|
965
|
+
kinds=kinds,
|
|
966
|
+
relations=relations,
|
|
967
|
+
schema=schema,
|
|
968
|
+
limit=limit,
|
|
969
|
+
cursor=cursor,
|
|
970
|
+
)
|
|
971
|
+
)
|
|
972
|
+
|
|
973
|
+
def iterate_diff(
|
|
974
|
+
self,
|
|
975
|
+
domain: str,
|
|
976
|
+
from_: str,
|
|
977
|
+
to: str,
|
|
978
|
+
*,
|
|
979
|
+
kinds: Sequence[str] | None = None,
|
|
980
|
+
relations: Sequence[str] | None = None,
|
|
981
|
+
schema: bool | None = None,
|
|
982
|
+
page_size: int | None = None,
|
|
983
|
+
) -> AsyncIterator[m.Change]:
|
|
984
|
+
call = ep.diff(
|
|
985
|
+
domain, from_, to, kinds=kinds, relations=relations, schema=schema, limit=page_size
|
|
986
|
+
)
|
|
987
|
+
return self._c.iterate(call)
|
|
988
|
+
|
|
989
|
+
|
|
990
|
+
class Vault(_Resource):
|
|
991
|
+
"""Erasure of personal data held in the vault (`pii.erase`) and key rotation
|
|
992
|
+
(`vault.manage`, installation administrators)."""
|
|
993
|
+
|
|
994
|
+
async def keys(self) -> m.VaultKeys:
|
|
995
|
+
"""Rows by encryption key and fingerprint key, and the earlier keys no row uses."""
|
|
996
|
+
return await self._c.send(ep.vault_keys())
|
|
997
|
+
|
|
998
|
+
async def rotate(self) -> m.VaultRotation:
|
|
999
|
+
"""Queue the job that makes rows stored under earlier keys again."""
|
|
1000
|
+
return await self._c.send(ep.rotate_vault())
|
|
1001
|
+
|
|
1002
|
+
async def erase_record(
|
|
1003
|
+
self,
|
|
1004
|
+
domain: str,
|
|
1005
|
+
kind: str,
|
|
1006
|
+
key: str,
|
|
1007
|
+
*,
|
|
1008
|
+
attributes: Sequence[str] | None = None,
|
|
1009
|
+
branch: str | None = None,
|
|
1010
|
+
) -> m.EraseResult:
|
|
1011
|
+
"""Erase every value the Record ever held in its vault Attributes (or `attributes`)."""
|
|
1012
|
+
body = m.EraseRequest(
|
|
1013
|
+
kind=kind,
|
|
1014
|
+
key=key,
|
|
1015
|
+
attributes=list(attributes) if attributes else None,
|
|
1016
|
+
branch=branch,
|
|
1017
|
+
)
|
|
1018
|
+
return await self._c.send(ep.erase_vault(domain, body))
|
|
1019
|
+
|
|
1020
|
+
async def erase_value(
|
|
1021
|
+
self,
|
|
1022
|
+
domain: str,
|
|
1023
|
+
kind: str,
|
|
1024
|
+
attribute: str,
|
|
1025
|
+
value: JsonValue,
|
|
1026
|
+
*,
|
|
1027
|
+
branch: str | None = None,
|
|
1028
|
+
) -> m.EraseResult:
|
|
1029
|
+
"""Erase `value` of a vault Attribute in every Record of the Kind."""
|
|
1030
|
+
body = m.EraseRequest(kind=kind, attribute=attribute, value=value, branch=branch)
|
|
1031
|
+
return await self._c.send(ep.erase_vault(domain, body))
|
|
1032
|
+
|
|
1033
|
+
|
|
1034
|
+
class ApiKeys(_Resource):
|
|
1035
|
+
async def create(
|
|
1036
|
+
self,
|
|
1037
|
+
domain: str,
|
|
1038
|
+
name: str,
|
|
1039
|
+
*,
|
|
1040
|
+
role: str | None = None,
|
|
1041
|
+
ttl_days: int | None = None,
|
|
1042
|
+
principal_id: str | None = None,
|
|
1043
|
+
allowed_networks: Sequence[str] | None = None,
|
|
1044
|
+
) -> m.ApiKeyCreated:
|
|
1045
|
+
"""Create a key; the secret is in `key` of the answer and is not shown again.
|
|
1046
|
+
|
|
1047
|
+
`allowed_networks` (CIDR) limits the client addresses the key works from.
|
|
1048
|
+
"""
|
|
1049
|
+
body = m.CreateApiKey(
|
|
1050
|
+
name=name,
|
|
1051
|
+
role=role,
|
|
1052
|
+
ttl_days=ttl_days,
|
|
1053
|
+
principal_id=principal_id,
|
|
1054
|
+
allowed_networks=list(allowed_networks) if allowed_networks else None,
|
|
1055
|
+
)
|
|
1056
|
+
return await self._c.send(ep.create_api_key(domain, body))
|
|
1057
|
+
|
|
1058
|
+
async def list(
|
|
1059
|
+
self, domain: str, *, limit: int | None = None, cursor: str | None = None
|
|
1060
|
+
) -> m.Page[m.ApiKey]:
|
|
1061
|
+
return await self._c.send(ep.list_api_keys(domain, limit=limit, cursor=cursor))
|
|
1062
|
+
|
|
1063
|
+
def iterate(self, domain: str, *, page_size: int | None = None) -> AsyncIterator[m.ApiKey]:
|
|
1064
|
+
return self._c.iterate(ep.list_api_keys(domain, limit=page_size))
|
|
1065
|
+
|
|
1066
|
+
async def revoke(self, domain: str, key_id: str) -> None:
|
|
1067
|
+
await self._c.send(ep.revoke_api_key(domain, key_id))
|
|
1068
|
+
|
|
1069
|
+
|
|
1070
|
+
class Grants(_Resource):
|
|
1071
|
+
async def create(
|
|
1072
|
+
self,
|
|
1073
|
+
domain: str,
|
|
1074
|
+
subject: str,
|
|
1075
|
+
role: str,
|
|
1076
|
+
*,
|
|
1077
|
+
scope_type: str = "domain",
|
|
1078
|
+
scope_ref: str | None = None,
|
|
1079
|
+
) -> m.Grant:
|
|
1080
|
+
body = m.CreateGrant.model_validate(
|
|
1081
|
+
{"subject": subject, "role": role, "scope_type": scope_type, "scope_ref": scope_ref}
|
|
1082
|
+
)
|
|
1083
|
+
return await self._c.send(ep.create_grant(domain, body))
|
|
1084
|
+
|
|
1085
|
+
async def list(
|
|
1086
|
+
self, domain: str, *, limit: int | None = None, cursor: str | None = None
|
|
1087
|
+
) -> m.Page[m.Grant]:
|
|
1088
|
+
return await self._c.send(ep.list_grants(domain, limit=limit, cursor=cursor))
|
|
1089
|
+
|
|
1090
|
+
def iterate(self, domain: str, *, page_size: int | None = None) -> AsyncIterator[m.Grant]:
|
|
1091
|
+
return self._c.iterate(ep.list_grants(domain, limit=page_size))
|
|
1092
|
+
|
|
1093
|
+
async def revoke(self, domain: str, grant_id: str) -> None:
|
|
1094
|
+
await self._c.send(ep.revoke_grant(domain, grant_id))
|
|
1095
|
+
|
|
1096
|
+
|
|
1097
|
+
class Principals(_Resource):
|
|
1098
|
+
async def list(
|
|
1099
|
+
self, *, type_: str | None = None, limit: int | None = None, cursor: str | None = None
|
|
1100
|
+
) -> m.Page[m.Principal]:
|
|
1101
|
+
return await self._c.send(ep.list_principals(type_=type_, limit=limit, cursor=cursor))
|
|
1102
|
+
|
|
1103
|
+
def iterate(
|
|
1104
|
+
self, *, type_: str | None = None, page_size: int | None = None
|
|
1105
|
+
) -> AsyncIterator[m.Principal]:
|
|
1106
|
+
return self._c.iterate(ep.list_principals(type_=type_, limit=page_size))
|
|
1107
|
+
|
|
1108
|
+
|
|
1109
|
+
class Audit(_Resource):
|
|
1110
|
+
async def list(
|
|
1111
|
+
self, domain: str, *, limit: int | None = None, cursor: str | None = None
|
|
1112
|
+
) -> m.Page[m.AuditEntry]:
|
|
1113
|
+
return await self._c.send(ep.list_audit(domain, limit=limit, cursor=cursor))
|
|
1114
|
+
|
|
1115
|
+
def iterate(self, domain: str, *, page_size: int | None = None) -> AsyncIterator[m.AuditEntry]:
|
|
1116
|
+
return self._c.iterate(ep.list_audit(domain, limit=page_size))
|
|
1117
|
+
|
|
1118
|
+
async def verify(self, domain: str, *, after_seq: int | None = None) -> m.AuditVerification:
|
|
1119
|
+
"""Recompute the Domain's audit hash chain; `intact` and `first_broken` tell the result."""
|
|
1120
|
+
return await self._c.send(ep.verify_audit(domain, after_seq=after_seq))
|
|
1121
|
+
|
|
1122
|
+
|
|
1123
|
+
class Trace(_Resource):
|
|
1124
|
+
"""Agent trace: sessions the caller may see, their timelines and notes."""
|
|
1125
|
+
|
|
1126
|
+
async def sessions(
|
|
1127
|
+
self,
|
|
1128
|
+
*,
|
|
1129
|
+
actor: str | None = None,
|
|
1130
|
+
domain_id: str | None = None,
|
|
1131
|
+
limit: int | None = None,
|
|
1132
|
+
cursor: str | None = None,
|
|
1133
|
+
) -> m.TraceSessionPage:
|
|
1134
|
+
"""One page of sessions, most recent first; `enabled` tells whether Keeper traces."""
|
|
1135
|
+
call = ep.list_trace_sessions(actor=actor, domain_id=domain_id, limit=limit, cursor=cursor)
|
|
1136
|
+
return await self._c.send(call)
|
|
1137
|
+
|
|
1138
|
+
def iterate(
|
|
1139
|
+
self,
|
|
1140
|
+
*,
|
|
1141
|
+
actor: str | None = None,
|
|
1142
|
+
domain_id: str | None = None,
|
|
1143
|
+
page_size: int | None = None,
|
|
1144
|
+
) -> AsyncIterator[m.TraceSession]:
|
|
1145
|
+
return self._c.iterate(
|
|
1146
|
+
ep.list_trace_sessions(actor=actor, domain_id=domain_id, limit=page_size)
|
|
1147
|
+
)
|
|
1148
|
+
|
|
1149
|
+
async def session(self, session_id: str, *, actor: str | None = None) -> m.TraceTimeline:
|
|
1150
|
+
"""The calls and notes of a session in order."""
|
|
1151
|
+
return await self._c.send(ep.get_trace_session(session_id, actor=actor))
|
|
1152
|
+
|
|
1153
|
+
async def add_note(
|
|
1154
|
+
self,
|
|
1155
|
+
session_id: str,
|
|
1156
|
+
text: str,
|
|
1157
|
+
*,
|
|
1158
|
+
domain_id: str | None = None,
|
|
1159
|
+
step: int | None = None,
|
|
1160
|
+
) -> m.NoteAccepted:
|
|
1161
|
+
"""Attach a note to the caller's session; it is written off the request path."""
|
|
1162
|
+
body = m.NoteRequest(text=text, domain_id=domain_id, step=step)
|
|
1163
|
+
return await self._c.send(ep.add_trace_note(session_id, body))
|
|
1164
|
+
|
|
1165
|
+
|
|
1166
|
+
class Proposals(_Resource):
|
|
1167
|
+
async def create(
|
|
1168
|
+
self,
|
|
1169
|
+
domain: str,
|
|
1170
|
+
source_ref: str,
|
|
1171
|
+
target_ref: str,
|
|
1172
|
+
*,
|
|
1173
|
+
title: str,
|
|
1174
|
+
description: str | None = None,
|
|
1175
|
+
idempotency_key: str | None = None,
|
|
1176
|
+
) -> m.ProposalDetail:
|
|
1177
|
+
"""Propose merging `source_ref` into `target_ref`."""
|
|
1178
|
+
body = m.ProposalCreate(
|
|
1179
|
+
source_ref=source_ref, target_ref=target_ref, title=title, description=description
|
|
1180
|
+
)
|
|
1181
|
+
return await self._c.send(ep.create_proposal(domain, body, idempotency_key=idempotency_key))
|
|
1182
|
+
|
|
1183
|
+
async def list(
|
|
1184
|
+
self,
|
|
1185
|
+
domain: str,
|
|
1186
|
+
*,
|
|
1187
|
+
status: m.ProposalStatus | None = None,
|
|
1188
|
+
origin: m.ProposalOrigin | None = None,
|
|
1189
|
+
source: str | None = None,
|
|
1190
|
+
target: str | None = None,
|
|
1191
|
+
author: str | None = None,
|
|
1192
|
+
limit: int | None = None,
|
|
1193
|
+
cursor: str | None = None,
|
|
1194
|
+
) -> m.Page[m.Proposal]:
|
|
1195
|
+
return await self._c.send(
|
|
1196
|
+
ep.list_proposals(
|
|
1197
|
+
domain,
|
|
1198
|
+
status=status,
|
|
1199
|
+
origin=origin,
|
|
1200
|
+
source=source,
|
|
1201
|
+
target=target,
|
|
1202
|
+
author=author,
|
|
1203
|
+
limit=limit,
|
|
1204
|
+
cursor=cursor,
|
|
1205
|
+
)
|
|
1206
|
+
)
|
|
1207
|
+
|
|
1208
|
+
def iterate(
|
|
1209
|
+
self,
|
|
1210
|
+
domain: str,
|
|
1211
|
+
*,
|
|
1212
|
+
status: m.ProposalStatus | None = None,
|
|
1213
|
+
origin: m.ProposalOrigin | None = None,
|
|
1214
|
+
source: str | None = None,
|
|
1215
|
+
target: str | None = None,
|
|
1216
|
+
author: str | None = None,
|
|
1217
|
+
page_size: int | None = None,
|
|
1218
|
+
) -> AsyncIterator[m.Proposal]:
|
|
1219
|
+
call = ep.list_proposals(
|
|
1220
|
+
domain,
|
|
1221
|
+
status=status,
|
|
1222
|
+
origin=origin,
|
|
1223
|
+
source=source,
|
|
1224
|
+
target=target,
|
|
1225
|
+
author=author,
|
|
1226
|
+
limit=page_size,
|
|
1227
|
+
)
|
|
1228
|
+
return self._c.iterate(call)
|
|
1229
|
+
|
|
1230
|
+
async def get(self, domain: str, proposal_id: str) -> m.ProposalDetail:
|
|
1231
|
+
return await self._c.send(ep.get_proposal(domain, proposal_id))
|
|
1232
|
+
|
|
1233
|
+
async def update(
|
|
1234
|
+
self,
|
|
1235
|
+
domain: str,
|
|
1236
|
+
proposal_id: str,
|
|
1237
|
+
*,
|
|
1238
|
+
title: str | None = None,
|
|
1239
|
+
description: str | None = None,
|
|
1240
|
+
) -> m.ProposalDetail:
|
|
1241
|
+
body = m.ProposalUpdate(title=title, description=description)
|
|
1242
|
+
return await self._c.send(ep.update_proposal(domain, proposal_id, body))
|
|
1243
|
+
|
|
1244
|
+
async def close(self, domain: str, proposal_id: str) -> m.ProposalDetail:
|
|
1245
|
+
return await self._c.send(ep.close_proposal(domain, proposal_id))
|
|
1246
|
+
|
|
1247
|
+
async def review(
|
|
1248
|
+
self,
|
|
1249
|
+
domain: str,
|
|
1250
|
+
proposal_id: str,
|
|
1251
|
+
verdict: m.ReviewVerdict,
|
|
1252
|
+
*,
|
|
1253
|
+
comment: str | None = None,
|
|
1254
|
+
) -> m.ReviewResult:
|
|
1255
|
+
body = m.ReviewIn(verdict=verdict, comment=comment)
|
|
1256
|
+
return await self._c.send(ep.review_proposal(domain, proposal_id, body))
|
|
1257
|
+
|
|
1258
|
+
async def merge(
|
|
1259
|
+
self,
|
|
1260
|
+
domain: str,
|
|
1261
|
+
proposal_id: str,
|
|
1262
|
+
*,
|
|
1263
|
+
message: str | None = None,
|
|
1264
|
+
idempotency_key: str | None = None,
|
|
1265
|
+
) -> m.MergeResult:
|
|
1266
|
+
body = m.MergeIn(message=message)
|
|
1267
|
+
return await self._c.send(
|
|
1268
|
+
ep.merge_proposal(domain, proposal_id, body, idempotency_key=idempotency_key)
|
|
1269
|
+
)
|
|
1270
|
+
|
|
1271
|
+
async def conflicts(
|
|
1272
|
+
self,
|
|
1273
|
+
domain: str,
|
|
1274
|
+
proposal_id: str,
|
|
1275
|
+
*,
|
|
1276
|
+
kind: str | None = None,
|
|
1277
|
+
status: m.ConflictStatus | None = None,
|
|
1278
|
+
limit: int | None = None,
|
|
1279
|
+
cursor: str | None = None,
|
|
1280
|
+
) -> m.Conflicts:
|
|
1281
|
+
"""One page of the Conflicts of the latest merge attempt; `kind` is a code list."""
|
|
1282
|
+
return await self._c.send(
|
|
1283
|
+
ep.list_conflicts(
|
|
1284
|
+
domain, proposal_id, kind=kind, status=status, limit=limit, cursor=cursor
|
|
1285
|
+
)
|
|
1286
|
+
)
|
|
1287
|
+
|
|
1288
|
+
def iterate_conflicts(
|
|
1289
|
+
self,
|
|
1290
|
+
domain: str,
|
|
1291
|
+
proposal_id: str,
|
|
1292
|
+
*,
|
|
1293
|
+
kind: str | None = None,
|
|
1294
|
+
status: m.ConflictStatus | None = None,
|
|
1295
|
+
page_size: int | None = None,
|
|
1296
|
+
) -> AsyncIterator[m.ConflictItem]:
|
|
1297
|
+
call = ep.list_conflicts(domain, proposal_id, kind=kind, status=status, limit=page_size)
|
|
1298
|
+
return self._c.iterate(call)
|
|
1299
|
+
|
|
1300
|
+
async def resolve(
|
|
1301
|
+
self,
|
|
1302
|
+
domain: str,
|
|
1303
|
+
proposal_id: str,
|
|
1304
|
+
conflict_id: str,
|
|
1305
|
+
choice: str,
|
|
1306
|
+
*,
|
|
1307
|
+
value: JsonValue = None,
|
|
1308
|
+
values: JsonObject | None = None,
|
|
1309
|
+
) -> m.Resolution:
|
|
1310
|
+
"""Decide one Conflict; `value` or `values` go with the `custom` choice."""
|
|
1311
|
+
body = m.ResolutionIn(
|
|
1312
|
+
choice=choice, value=value, values=None if values is None else dict(values)
|
|
1313
|
+
)
|
|
1314
|
+
return await self._c.send(ep.resolve_conflict(domain, proposal_id, conflict_id, body))
|
|
1315
|
+
|
|
1316
|
+
async def resolve_many(
|
|
1317
|
+
self,
|
|
1318
|
+
domain: str,
|
|
1319
|
+
proposal_id: str,
|
|
1320
|
+
resolutions: Sequence[m.FingerprintedResolutionIn | JsonObject],
|
|
1321
|
+
) -> m.Conflicts:
|
|
1322
|
+
"""Decide several Conflicts, each named by its fingerprint."""
|
|
1323
|
+
body = m.ResolveManyIn(
|
|
1324
|
+
resolutions=[
|
|
1325
|
+
r
|
|
1326
|
+
if isinstance(r, m.FingerprintedResolutionIn)
|
|
1327
|
+
else m.FingerprintedResolutionIn(**r)
|
|
1328
|
+
for r in resolutions
|
|
1329
|
+
]
|
|
1330
|
+
)
|
|
1331
|
+
return await self._c.send(ep.resolve_conflicts(domain, proposal_id, body))
|
|
1332
|
+
|
|
1333
|
+
async def items(
|
|
1334
|
+
self,
|
|
1335
|
+
domain: str,
|
|
1336
|
+
proposal_id: str,
|
|
1337
|
+
*,
|
|
1338
|
+
status: Sequence[m.ItemDecisionValue] | None = None,
|
|
1339
|
+
kind: Sequence[str] | None = None,
|
|
1340
|
+
type: Sequence[m.ItemType] | None = None,
|
|
1341
|
+
min_confidence: float | None = None,
|
|
1342
|
+
max_confidence: float | None = None,
|
|
1343
|
+
limit: int | None = None,
|
|
1344
|
+
cursor: str | None = None,
|
|
1345
|
+
) -> m.Page[m.ProposalItem]:
|
|
1346
|
+
"""One page of the items of an extraction Proposal; a filter given several values
|
|
1347
|
+
matches any of them."""
|
|
1348
|
+
return await self._c.send(
|
|
1349
|
+
ep.list_proposal_items(
|
|
1350
|
+
domain,
|
|
1351
|
+
proposal_id,
|
|
1352
|
+
status=status,
|
|
1353
|
+
kind=kind,
|
|
1354
|
+
type=type,
|
|
1355
|
+
min_confidence=min_confidence,
|
|
1356
|
+
max_confidence=max_confidence,
|
|
1357
|
+
limit=limit,
|
|
1358
|
+
cursor=cursor,
|
|
1359
|
+
)
|
|
1360
|
+
)
|
|
1361
|
+
|
|
1362
|
+
def iterate_items(
|
|
1363
|
+
self,
|
|
1364
|
+
domain: str,
|
|
1365
|
+
proposal_id: str,
|
|
1366
|
+
*,
|
|
1367
|
+
status: Sequence[m.ItemDecisionValue] | None = None,
|
|
1368
|
+
kind: Sequence[str] | None = None,
|
|
1369
|
+
type: Sequence[m.ItemType] | None = None,
|
|
1370
|
+
min_confidence: float | None = None,
|
|
1371
|
+
max_confidence: float | None = None,
|
|
1372
|
+
page_size: int | None = None,
|
|
1373
|
+
) -> AsyncIterator[m.ProposalItem]:
|
|
1374
|
+
call = ep.list_proposal_items(
|
|
1375
|
+
domain,
|
|
1376
|
+
proposal_id,
|
|
1377
|
+
status=status,
|
|
1378
|
+
kind=kind,
|
|
1379
|
+
type=type,
|
|
1380
|
+
min_confidence=min_confidence,
|
|
1381
|
+
max_confidence=max_confidence,
|
|
1382
|
+
limit=page_size,
|
|
1383
|
+
)
|
|
1384
|
+
return self._c.iterate(call)
|
|
1385
|
+
|
|
1386
|
+
async def decide_items(
|
|
1387
|
+
self,
|
|
1388
|
+
domain: str,
|
|
1389
|
+
proposal_id: str,
|
|
1390
|
+
decisions: Sequence[m.ItemDecision | JsonObject] = (),
|
|
1391
|
+
*,
|
|
1392
|
+
where: m.ItemSelection | JsonObject | None = None,
|
|
1393
|
+
decision: m.ItemDecisionValue | None = None,
|
|
1394
|
+
) -> m.ItemsDecided:
|
|
1395
|
+
"""Record decisions by item, and or one `decision` for every item matching `where`;
|
|
1396
|
+
the extraction branch and the Proposal are refreshed."""
|
|
1397
|
+
body = m.DecideItems(
|
|
1398
|
+
decisions=[
|
|
1399
|
+
d if isinstance(d, m.ItemDecision) else m.ItemDecision(**d) for d in decisions
|
|
1400
|
+
],
|
|
1401
|
+
where=where
|
|
1402
|
+
if where is None or isinstance(where, m.ItemSelection)
|
|
1403
|
+
else m.ItemSelection(**where),
|
|
1404
|
+
decision=decision,
|
|
1405
|
+
)
|
|
1406
|
+
return await self._c.send(ep.decide_proposal_items(domain, proposal_id, body))
|
|
1407
|
+
|
|
1408
|
+
|
|
1409
|
+
class Agents(_Resource):
|
|
1410
|
+
async def context(self, domain: str, request: m.ContextRequest | JsonObject) -> m.ContextPack:
|
|
1411
|
+
"""A context pack as JSON (a `ContextRequest` or its JSON object)."""
|
|
1412
|
+
return await self._c.send(ep.context_pack(domain, _context_request(request)))
|
|
1413
|
+
|
|
1414
|
+
async def context_markdown(self, domain: str, request: m.ContextRequest | JsonObject) -> str:
|
|
1415
|
+
"""The same context pack rendered as Markdown."""
|
|
1416
|
+
return await self._c.send(ep.context_pack_markdown(domain, _context_request(request)))
|
|
1417
|
+
|
|
1418
|
+
async def explain(
|
|
1419
|
+
self, domain: str, kind: str, key: str, *, branch: str | None = None, at: str | None = None
|
|
1420
|
+
) -> m.Explain:
|
|
1421
|
+
"""Where the values and Links of a Record came from."""
|
|
1422
|
+
return await self._c.send(ep.explain_record(domain, kind, key, branch=branch, at=at))
|
|
1423
|
+
|
|
1424
|
+
async def explain_markdown(
|
|
1425
|
+
self, domain: str, kind: str, key: str, *, branch: str | None = None, at: str | None = None
|
|
1426
|
+
) -> str:
|
|
1427
|
+
return await self._c.send(
|
|
1428
|
+
ep.explain_record_markdown(domain, kind, key, branch=branch, at=at)
|
|
1429
|
+
)
|
|
1430
|
+
|
|
1431
|
+
async def health(
|
|
1432
|
+
self, domain: str, *, branch: str | None = None, at: str | None = None
|
|
1433
|
+
) -> m.DomainHealth:
|
|
1434
|
+
"""Violations, orphan Links, gaps and duplicates per Kind and Relation of a Domain."""
|
|
1435
|
+
return await self._c.send(ep.domain_health(domain, branch=branch, at=at))
|
|
1436
|
+
|
|
1437
|
+
async def health_markdown(
|
|
1438
|
+
self, domain: str, *, branch: str | None = None, at: str | None = None
|
|
1439
|
+
) -> str:
|
|
1440
|
+
"""The Domain health report rendered as Markdown."""
|
|
1441
|
+
return await self._c.send(ep.domain_health_markdown(domain, branch=branch, at=at))
|
|
1442
|
+
|
|
1443
|
+
async def kind_health(
|
|
1444
|
+
self, domain: str, kind: str, *, branch: str | None = None, at: str | None = None
|
|
1445
|
+
) -> m.KindHealthReport:
|
|
1446
|
+
"""The health of one Kind with the Relations it takes part in."""
|
|
1447
|
+
return await self._c.send(ep.kind_health(domain, kind, branch=branch, at=at))
|
|
1448
|
+
|
|
1449
|
+
async def kind_health_markdown(
|
|
1450
|
+
self, domain: str, kind: str, *, branch: str | None = None, at: str | None = None
|
|
1451
|
+
) -> str:
|
|
1452
|
+
return await self._c.send(ep.kind_health_markdown(domain, kind, branch=branch, at=at))
|
|
1453
|
+
|
|
1454
|
+
async def changes(
|
|
1455
|
+
self,
|
|
1456
|
+
domain: str,
|
|
1457
|
+
branch: str,
|
|
1458
|
+
*,
|
|
1459
|
+
since: str | None = None,
|
|
1460
|
+
kinds: Sequence[str] | None = None,
|
|
1461
|
+
limit: int | None = None,
|
|
1462
|
+
) -> m.ChangesFeed:
|
|
1463
|
+
"""Revision events of a branch after `since` (a Revision id or tag)."""
|
|
1464
|
+
return await self._c.send(
|
|
1465
|
+
ep.branch_changes(domain, branch, since=since, kinds=kinds, limit=limit)
|
|
1466
|
+
)
|
|
1467
|
+
|
|
1468
|
+
async def events(
|
|
1469
|
+
self,
|
|
1470
|
+
domain: str,
|
|
1471
|
+
branch: str,
|
|
1472
|
+
*,
|
|
1473
|
+
kinds: Sequence[str] | None = None,
|
|
1474
|
+
last_event_id: str | None = None,
|
|
1475
|
+
) -> str:
|
|
1476
|
+
"""The event stream of a branch as text, read until the server closes it."""
|
|
1477
|
+
return await self._c.send(
|
|
1478
|
+
ep.branch_events(domain, branch, kinds=kinds, last_event_id=last_event_id)
|
|
1479
|
+
)
|
|
1480
|
+
|
|
1481
|
+
|
|
1482
|
+
class Webhooks(_Resource):
|
|
1483
|
+
async def create(
|
|
1484
|
+
self,
|
|
1485
|
+
domain: str,
|
|
1486
|
+
url: str,
|
|
1487
|
+
*,
|
|
1488
|
+
events: Sequence[str] | None = None,
|
|
1489
|
+
branches: Sequence[str] | None = None,
|
|
1490
|
+
description: str | None = None,
|
|
1491
|
+
clearances: Sequence[str] | None = None,
|
|
1492
|
+
) -> m.WebhookCreated:
|
|
1493
|
+
"""Register a webhook; the signing secret is in `secret` and is not shown again."""
|
|
1494
|
+
body = m.CreateWebhook(
|
|
1495
|
+
url=url,
|
|
1496
|
+
events=None if events is None else list(events),
|
|
1497
|
+
branches=None if branches is None else list(branches),
|
|
1498
|
+
description=description,
|
|
1499
|
+
clearances=None if clearances is None else list(clearances),
|
|
1500
|
+
)
|
|
1501
|
+
return await self._c.send(ep.create_webhook(domain, body))
|
|
1502
|
+
|
|
1503
|
+
async def list(self, domain: str) -> m.WebhookList:
|
|
1504
|
+
return await self._c.send(ep.list_webhooks(domain))
|
|
1505
|
+
|
|
1506
|
+
async def set_clearances(
|
|
1507
|
+
self, domain: str, webhook_id: str, clearances: Sequence[str]
|
|
1508
|
+
) -> m.Webhook:
|
|
1509
|
+
"""Replace the clearances of a webhook; adding one needs its clearance permission on
|
|
1510
|
+
the Domain."""
|
|
1511
|
+
body = m.UpdateWebhook(clearances=list(clearances))
|
|
1512
|
+
return await self._c.send(ep.update_webhook(domain, webhook_id, body))
|
|
1513
|
+
|
|
1514
|
+
async def delete(self, domain: str, webhook_id: str) -> None:
|
|
1515
|
+
await self._c.send(ep.delete_webhook(domain, webhook_id))
|
|
1516
|
+
|
|
1517
|
+
|
|
1518
|
+
class Sources(_Resource):
|
|
1519
|
+
async def sync(self, domain: str, source: str, *, dry_run: bool = False) -> m.SyncQueued:
|
|
1520
|
+
"""Queue a synchronization of the Source (id or api name) for Scribe."""
|
|
1521
|
+
return await self._c.send(ep.sync_source(domain, source, m.SyncRequest(dry_run=dry_run)))
|
|
1522
|
+
|
|
1523
|
+
async def runs(
|
|
1524
|
+
self, domain: str, source: str, *, limit: int | None = None, cursor: str | None = None
|
|
1525
|
+
) -> m.Page[m.SourceRun]:
|
|
1526
|
+
"""Runs of the Source, newest first (needs a Domain-wide `data.read` grant)."""
|
|
1527
|
+
return await self._c.send(ep.list_source_runs(domain, source, limit=limit, cursor=cursor))
|
|
1528
|
+
|
|
1529
|
+
def iterate_runs(
|
|
1530
|
+
self, domain: str, source: str, *, page_size: int | None = None
|
|
1531
|
+
) -> AsyncIterator[m.SourceRun]:
|
|
1532
|
+
return self._c.iterate(ep.list_source_runs(domain, source, limit=page_size))
|
|
1533
|
+
|
|
1534
|
+
|
|
1535
|
+
class Documents(_Resource):
|
|
1536
|
+
async def upload(
|
|
1537
|
+
self,
|
|
1538
|
+
domain: str,
|
|
1539
|
+
file: str | os.PathLike[str] | bytes,
|
|
1540
|
+
*,
|
|
1541
|
+
filename: str | None = None,
|
|
1542
|
+
extract: bool | None = None,
|
|
1543
|
+
tiers: Sequence[int] | None = None,
|
|
1544
|
+
) -> m.DocumentUploaded:
|
|
1545
|
+
"""Upload a document: a path (named after the file) or bytes with a `filename`.
|
|
1546
|
+
|
|
1547
|
+
`extract` (server default: true) queues an extraction job on the given `tiers`.
|
|
1548
|
+
"""
|
|
1549
|
+
call = ep.upload_document(domain, file, filename=filename, extract=extract, tiers=tiers)
|
|
1550
|
+
return await self._c.send(call)
|
|
1551
|
+
|
|
1552
|
+
async def upload_url(
|
|
1553
|
+
self,
|
|
1554
|
+
domain: str,
|
|
1555
|
+
url: str,
|
|
1556
|
+
*,
|
|
1557
|
+
filename: str | None = None,
|
|
1558
|
+
extract: bool | None = None,
|
|
1559
|
+
tiers: Sequence[int] | None = None,
|
|
1560
|
+
) -> m.DocumentUploaded:
|
|
1561
|
+
"""Have the server fetch the document from an allowlisted URL."""
|
|
1562
|
+
body = m.DocumentFromUrl(
|
|
1563
|
+
url=url,
|
|
1564
|
+
filename=filename,
|
|
1565
|
+
extract=extract,
|
|
1566
|
+
tiers=None if tiers is None else list(tiers),
|
|
1567
|
+
)
|
|
1568
|
+
return await self._c.send(ep.upload_document_url(domain, body))
|
|
1569
|
+
|
|
1570
|
+
async def list(
|
|
1571
|
+
self, domain: str, *, limit: int | None = None, cursor: str | None = None
|
|
1572
|
+
) -> m.Page[m.Document]:
|
|
1573
|
+
"""Documents of the Domain, newest first."""
|
|
1574
|
+
return await self._c.send(ep.list_documents(domain, limit=limit, cursor=cursor))
|
|
1575
|
+
|
|
1576
|
+
def iterate(self, domain: str, *, page_size: int | None = None) -> AsyncIterator[m.Document]:
|
|
1577
|
+
return self._c.iterate(ep.list_documents(domain, limit=page_size))
|
|
1578
|
+
|
|
1579
|
+
async def get(self, domain: str, document_id: str) -> m.DocumentDetail:
|
|
1580
|
+
"""A document with its extraction jobs."""
|
|
1581
|
+
return await self._c.send(ep.get_document(domain, document_id))
|
|
1582
|
+
|
|
1583
|
+
async def text(self, domain: str, document_id: str) -> m.DocumentText:
|
|
1584
|
+
"""The parsed text of a document (404 until it is parsed)."""
|
|
1585
|
+
return await self._c.send(ep.get_document_text(domain, document_id))
|