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