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.
@@ -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))