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,1292 @@
1
+ """One builder per API endpoint, returning a `Call` that describes the request.
2
+
3
+ The sync and async clients expose these through resource objects; nothing here does I/O.
4
+ Paths are relative to the API prefix. List builders take `limit` and `cursor`; GET reads that
5
+ carry an ETag are marked `cacheable`.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import os
12
+ from collections.abc import Mapping, Sequence
13
+ from pathlib import Path
14
+ from typing import Any
15
+ from urllib.parse import quote
16
+
17
+ import httpx
18
+
19
+ from lorebase_client import models as m
20
+ from lorebase_client.core import (
21
+ Call,
22
+ parse_json,
23
+ parse_model,
24
+ parse_none,
25
+ parse_text,
26
+ )
27
+
28
+ _ACCEPTED = 202
29
+
30
+
31
+ _DOT_SEGMENTS = frozenset({"", ".", ".."})
32
+
33
+
34
+ def _seg(value: str) -> str:
35
+ """One path segment, percent-encoded. Empty and dot segments are refused: an HTTP client
36
+ or proxy would resolve them and send the request to another route."""
37
+ if value in _DOT_SEGMENTS:
38
+ raise ValueError(f"invalid path segment {value!r}")
39
+ return quote(value, safe="")
40
+
41
+
42
+ def _branch(name: str) -> str:
43
+ """Branch names are hierarchical: slashes stay in the path, but no part may be empty or a
44
+ dot segment."""
45
+ if any(part in _DOT_SEGMENTS for part in name.split("/")):
46
+ raise ValueError(f"invalid branch name {name!r}")
47
+ return quote(name, safe="/")
48
+
49
+
50
+ def _domain(domain: str) -> str:
51
+ return f"/domains/{_seg(domain)}"
52
+
53
+
54
+ def _csv(values: Sequence[str] | None) -> str | None:
55
+ return None if not values else ",".join(values)
56
+
57
+
58
+ # Identity ---------------------------------------------------------------------------------
59
+
60
+
61
+ def whoami() -> Call[m.WhoAmI]:
62
+ return Call("GET", "/auth/whoami", parse_model(m.WhoAmI), safe=True)
63
+
64
+
65
+ # Domains ----------------------------------------------------------------------------------
66
+
67
+
68
+ def create_domain(body: m.DomainCreate, *, idempotency_key: str | None = None) -> Call[m.Domain]:
69
+ return Call(
70
+ "POST",
71
+ "/domains",
72
+ parse_model(m.Domain),
73
+ body=body.payload(),
74
+ accepts_key=True,
75
+ idempotency_key=idempotency_key,
76
+ )
77
+
78
+
79
+ def list_domains(*, limit: int | None = None, cursor: str | None = None) -> Call[m.Page[m.Domain]]:
80
+ return Call(
81
+ "GET",
82
+ "/domains",
83
+ parse_model(m.Page[m.Domain]),
84
+ params={"limit": limit, "cursor": cursor},
85
+ safe=True,
86
+ )
87
+
88
+
89
+ def get_domain(domain: str) -> Call[m.Domain]:
90
+ return Call("GET", _domain(domain), parse_model(m.Domain), safe=True)
91
+
92
+
93
+ def update_domain(domain: str, body: m.DomainUpdate) -> Call[m.Domain]:
94
+ return Call("PATCH", _domain(domain), parse_model(m.Domain), body=body.payload())
95
+
96
+
97
+ def delete_domain(domain: str, *, confirm: str) -> Call[None]:
98
+ return Call("DELETE", _domain(domain), parse_none, params={"confirm": confirm}, safe=True)
99
+
100
+
101
+ # Branches and tags ------------------------------------------------------------------------
102
+
103
+
104
+ def list_branches(
105
+ domain: str, *, limit: int | None = None, cursor: str | None = None
106
+ ) -> Call[m.Page[m.Branch]]:
107
+ return Call(
108
+ "GET",
109
+ f"{_domain(domain)}/branches",
110
+ parse_model(m.Page[m.Branch]),
111
+ params={"limit": limit, "cursor": cursor},
112
+ safe=True,
113
+ )
114
+
115
+
116
+ def create_branch(
117
+ domain: str, body: m.BranchCreate, *, idempotency_key: str | None = None
118
+ ) -> Call[m.Branch]:
119
+ return Call(
120
+ "POST",
121
+ f"{_domain(domain)}/branches",
122
+ parse_model(m.Branch),
123
+ body=body.payload(),
124
+ accepts_key=True,
125
+ idempotency_key=idempotency_key,
126
+ )
127
+
128
+
129
+ def get_branch(domain: str, branch: str) -> Call[m.BranchDetail]:
130
+ return Call(
131
+ "GET",
132
+ f"{_domain(domain)}/branches/{_branch(branch)}",
133
+ parse_model(m.BranchDetail),
134
+ safe=True,
135
+ )
136
+
137
+
138
+ def protect_branch(domain: str, branch: str, *, protected: bool) -> Call[m.Branch]:
139
+ return Call(
140
+ "PATCH",
141
+ f"{_domain(domain)}/branches/{_branch(branch)}",
142
+ parse_model(m.Branch),
143
+ body=m.BranchUpdate(protected=protected).payload(),
144
+ safe=True,
145
+ )
146
+
147
+
148
+ def delete_branch(domain: str, branch: str) -> Call[None]:
149
+ return Call("DELETE", f"{_domain(domain)}/branches/{_branch(branch)}", parse_none, safe=True)
150
+
151
+
152
+ def rebase_branch_tags(
153
+ domain: str, branch: str, body: m.RebaseTagsRequest
154
+ ) -> Call[m.RebaseTagsResult]:
155
+ """Give a branch the sensitivity tags of the default branch (or, with `rows_only`, queue
156
+ the rewrite of its stale titles and search vectors)."""
157
+ return Call(
158
+ "POST",
159
+ f"{_domain(domain)}/branches/{_branch(branch)}/rebase-tags",
160
+ parse_model(m.RebaseTagsResult),
161
+ body=body.payload(),
162
+ )
163
+
164
+
165
+ def list_tags(
166
+ domain: str, *, limit: int | None = None, cursor: str | None = None
167
+ ) -> Call[m.Page[m.Tag]]:
168
+ return Call(
169
+ "GET",
170
+ f"{_domain(domain)}/tags",
171
+ parse_model(m.Page[m.Tag]),
172
+ params={"limit": limit, "cursor": cursor},
173
+ safe=True,
174
+ )
175
+
176
+
177
+ def create_tag(
178
+ domain: str, body: m.TagCreate, *, idempotency_key: str | None = None
179
+ ) -> Call[m.Tag]:
180
+ return Call(
181
+ "POST",
182
+ f"{_domain(domain)}/tags",
183
+ parse_model(m.Tag),
184
+ body=body.payload(),
185
+ accepts_key=True,
186
+ idempotency_key=idempotency_key,
187
+ )
188
+
189
+
190
+ # Schema -----------------------------------------------------------------------------------
191
+
192
+
193
+ def get_schema(
194
+ domain: str, *, branch: str | None = None, at: str | None = None
195
+ ) -> Call[m.SchemaDocument]:
196
+ return Call(
197
+ "GET",
198
+ f"{_domain(domain)}/schema",
199
+ parse_model(m.SchemaDocument),
200
+ params={"branch": branch, "at": at, "format": "json"},
201
+ safe=True,
202
+ cacheable=True,
203
+ )
204
+
205
+
206
+ def get_schema_yaml(domain: str, *, branch: str | None = None, at: str | None = None) -> Call[str]:
207
+ return Call(
208
+ "GET",
209
+ f"{_domain(domain)}/schema",
210
+ parse_text,
211
+ params={"branch": branch, "at": at, "format": "yaml"},
212
+ accept="text/yaml",
213
+ safe=True,
214
+ cacheable=True,
215
+ )
216
+
217
+
218
+ def get_schema_element(
219
+ domain: str,
220
+ section: m.SchemaSection,
221
+ name: str,
222
+ *,
223
+ branch: str | None = None,
224
+ at: str | None = None,
225
+ ) -> Call[dict[str, Any]]:
226
+ return Call(
227
+ "GET",
228
+ f"{_domain(domain)}/schema/{section}/{_seg(name)}",
229
+ parse_json,
230
+ params={"branch": branch, "at": at},
231
+ safe=True,
232
+ )
233
+
234
+
235
+ def validate_schema(domain: str, body: m.SchemaValidateRequest) -> Call[m.SchemaValidation]:
236
+ return Call(
237
+ "POST",
238
+ f"{_domain(domain)}/schema:validate",
239
+ parse_model(m.SchemaValidation),
240
+ body=body.payload(),
241
+ safe=True,
242
+ )
243
+
244
+
245
+ def apply_schema(
246
+ domain: str, branch: str, body: m.SchemaChangeRequest
247
+ ) -> Call[m.SchemaChangeResult]:
248
+ return Call(
249
+ "POST",
250
+ f"{_domain(domain)}/branches/{_branch(branch)}/schema/changes",
251
+ parse_model(m.SchemaChangeResult),
252
+ body=body.payload(),
253
+ )
254
+
255
+
256
+ def infer_schema_postgres(domain: str, body: Mapping[str, Any]) -> Call[dict[str, Any]]:
257
+ """Propose a schema from Postgres tables: `body` is `{"postgres": {...}, "options": {...}}`.
258
+
259
+ The answer carries the proposal as YAML in `yaml`; nothing is written.
260
+ """
261
+ return Call(
262
+ "POST",
263
+ f"{_domain(domain)}/schema:infer",
264
+ parse_json,
265
+ params={"format": "json"},
266
+ body=dict(body),
267
+ safe=True,
268
+ )
269
+
270
+
271
+ def infer_schema_files(
272
+ domain: str,
273
+ files: Mapping[str, bytes],
274
+ *,
275
+ options: Mapping[str, Any] | None = None,
276
+ records_path: str | None = None,
277
+ ) -> Call[dict[str, Any]]:
278
+ """Propose a schema from data files (file name to content); the name is the table.
279
+ `records_path` (dotted keys) locates the records array of JSON documents that hold it under
280
+ a key."""
281
+ form = {} if not options else {"options": json.dumps(options, separators=(",", ":"))}
282
+ if records_path is not None:
283
+ form["records_path"] = records_path
284
+ return Call(
285
+ "POST",
286
+ f"{_domain(domain)}/schema:infer",
287
+ parse_json,
288
+ params={"format": "json"},
289
+ files=tuple(("files", name, data) for name, data in files.items()),
290
+ form=form,
291
+ safe=True,
292
+ )
293
+
294
+
295
+ def preview_files(
296
+ domain: str,
297
+ files: Mapping[str, bytes],
298
+ *,
299
+ rows: int | None = None,
300
+ records_path: str | None = None,
301
+ ) -> Call[dict[str, Any]]:
302
+ """The first rows of each data file (file name to content) as Keeper reads them; nothing
303
+ is stored. `rows` per file is capped by Keeper's settings."""
304
+ form = {} if records_path is None else {"records_path": records_path}
305
+ return Call(
306
+ "POST",
307
+ f"{_domain(domain)}/schema:preview",
308
+ parse_json,
309
+ params={"rows": rows},
310
+ files=tuple(("files", name, data) for name, data in files.items()),
311
+ form=form,
312
+ safe=True,
313
+ )
314
+
315
+
316
+ def preview_derivation(domain: str, body: Mapping[str, Any]) -> Call[dict[str, Any]]:
317
+ """Evaluate a candidate Derivation on sample Records without saving it: `body` holds the
318
+ change set (`yaml`, or `patch`/`remove`), `derivation` and optionally `branch`, `limit`,
319
+ `keys`. Record-read permissions and masking apply; nothing is written."""
320
+ return Call(
321
+ "POST", f"{_domain(domain)}/derivations:preview", parse_json, body=dict(body), safe=True
322
+ )
323
+
324
+
325
+ def preview_source(domain: str, body: Mapping[str, Any]) -> Call[dict[str, Any]]:
326
+ """Evaluate the expression mappings of a candidate Source on the given `rows`: `body` holds
327
+ the change set, `source` and `rows`. Nothing is read from the Source or written."""
328
+ return Call(
329
+ "POST", f"{_domain(domain)}/sources:preview", parse_json, body=dict(body), safe=True
330
+ )
331
+
332
+
333
+ def list_schema_suggestions(
334
+ domain: str,
335
+ *,
336
+ status: Sequence[m.ItemDecisionValue] | None = None,
337
+ type: Sequence[m.SuggestionType] | None = None,
338
+ all: bool | None = None,
339
+ limit: int | None = None,
340
+ cursor: str | None = None,
341
+ ) -> Call[m.Page[m.SchemaSuggestion]]:
342
+ """Schema suggestions from extraction, the most supported first; `all` lists pending ones
343
+ below the configured support too."""
344
+ return Call(
345
+ "GET",
346
+ f"{_domain(domain)}/schema/suggestions",
347
+ parse_model(m.Page[m.SchemaSuggestion]),
348
+ params={
349
+ "status": list(status or ()),
350
+ "type": list(type or ()),
351
+ "all": all,
352
+ "limit": limit,
353
+ "cursor": cursor,
354
+ },
355
+ safe=True,
356
+ )
357
+
358
+
359
+ def decide_schema_suggestions(domain: str, body: m.DecideSuggestions) -> Call[m.SuggestionsDecided]:
360
+ """Accepting opens a Proposal on a new branch: not safe to repeat blindly."""
361
+ return Call(
362
+ "POST",
363
+ f"{_domain(domain)}/schema/suggestions:decide",
364
+ parse_model(m.SuggestionsDecided),
365
+ body=body.payload(),
366
+ )
367
+
368
+
369
+ def merge_schema_suggestions(domain: str) -> Call[m.SuggestionsMerged]:
370
+ """Fold stored variants of Attribute suggestions; repeating it changes nothing."""
371
+ return Call(
372
+ "POST",
373
+ f"{_domain(domain)}/schema/suggestions:merge",
374
+ parse_model(m.SuggestionsMerged),
375
+ safe=True,
376
+ )
377
+
378
+
379
+ # Operations -------------------------------------------------------------------------------
380
+
381
+
382
+ def execute_operation(
383
+ domain: str,
384
+ operation: str,
385
+ body: m.ExecuteRequest,
386
+ *,
387
+ idempotency_key: str | None = None,
388
+ ) -> Call[m.Execution]:
389
+ return Call(
390
+ "POST",
391
+ f"{_domain(domain)}/operations/{_seg(operation)}/execute",
392
+ parse_model(m.Execution),
393
+ body=body.payload(),
394
+ accepts_key=True,
395
+ idempotency_key=idempotency_key,
396
+ )
397
+
398
+
399
+ def _execution_or_proposal(response: httpx.Response) -> m.Execution | m.AgentWrite:
400
+ """202 answers an Operation that needed review: the Proposal it opened."""
401
+ if response.status_code == _ACCEPTED:
402
+ return m.AgentWrite.model_validate_json(response.content)
403
+ return m.Execution.model_validate_json(response.content)
404
+
405
+
406
+ def execute_operation_as_agent(
407
+ domain: str,
408
+ operation: str,
409
+ body: m.ExecuteRequest,
410
+ *,
411
+ idempotency_key: str | None = None,
412
+ ) -> Call[m.Execution | m.AgentWrite]:
413
+ """Like `execute_operation`, for requests that may come back as a Proposal."""
414
+ return Call(
415
+ "POST",
416
+ f"{_domain(domain)}/operations/{_seg(operation)}/execute",
417
+ _execution_or_proposal,
418
+ body=body.payload(),
419
+ accepts_key=True,
420
+ idempotency_key=idempotency_key,
421
+ )
422
+
423
+
424
+ # Reads ------------------------------------------------------------------------------------
425
+
426
+
427
+ def _order(order_by: Sequence[str] | None) -> str | None:
428
+ return _csv(order_by)
429
+
430
+
431
+ def list_records(
432
+ domain: str,
433
+ kind: str,
434
+ *,
435
+ branch: str | None = None,
436
+ at: str | None = None,
437
+ where: Mapping[str, Any] | None = None,
438
+ order_by: Sequence[str] | None = None,
439
+ select: Sequence[str] | None = None,
440
+ limit: int | None = None,
441
+ cursor: str | None = None,
442
+ ) -> Call[m.RecordPage]:
443
+ return Call(
444
+ "GET",
445
+ f"{_domain(domain)}/kinds/{_seg(kind)}/records",
446
+ parse_model(m.RecordPage),
447
+ params={
448
+ "branch": branch,
449
+ "at": at,
450
+ "where": None if not where else json.dumps(where, separators=(",", ":")),
451
+ "order_by": _order(order_by),
452
+ "select": _csv(select),
453
+ "limit": limit,
454
+ "cursor": cursor,
455
+ },
456
+ safe=True,
457
+ cacheable=True,
458
+ )
459
+
460
+
461
+ def get_record(
462
+ domain: str,
463
+ kind: str,
464
+ key: str,
465
+ *,
466
+ branch: str | None = None,
467
+ at: str | None = None,
468
+ links: bool = False,
469
+ lineage: bool = False,
470
+ ) -> Call[m.RecordResponse]:
471
+ return Call(
472
+ "GET",
473
+ f"{_domain(domain)}/kinds/{_seg(kind)}/records/{_seg(key)}",
474
+ parse_model(m.RecordResponse),
475
+ params={
476
+ "branch": branch,
477
+ "at": at,
478
+ "links": links or None,
479
+ "lineage": lineage or None,
480
+ },
481
+ safe=True,
482
+ cacheable=True,
483
+ )
484
+
485
+
486
+ def record_history(
487
+ domain: str,
488
+ kind: str,
489
+ key: str,
490
+ *,
491
+ branch: str | None = None,
492
+ at: str | None = None,
493
+ values: bool = False,
494
+ limit: int | None = None,
495
+ cursor: str | None = None,
496
+ ) -> Call[m.RecordHistory]:
497
+ return Call(
498
+ "GET",
499
+ f"{_domain(domain)}/kinds/{_seg(kind)}/records/{_seg(key)}/history",
500
+ parse_model(m.RecordHistory),
501
+ params={
502
+ "branch": branch,
503
+ "at": at,
504
+ "values": values or None,
505
+ "limit": limit,
506
+ "cursor": cursor,
507
+ },
508
+ safe=True,
509
+ cacheable=True,
510
+ )
511
+
512
+
513
+ def record_lineage(
514
+ domain: str, kind: str, key: str, *, branch: str | None = None, at: str | None = None
515
+ ) -> Call[m.RecordLineage]:
516
+ return Call(
517
+ "GET",
518
+ f"{_domain(domain)}/kinds/{_seg(kind)}/records/{_seg(key)}/lineage",
519
+ parse_model(m.RecordLineage),
520
+ params={"branch": branch, "at": at},
521
+ safe=True,
522
+ cacheable=True,
523
+ )
524
+
525
+
526
+ def list_record_links(
527
+ domain: str,
528
+ kind: str,
529
+ key: str,
530
+ relation: str,
531
+ *,
532
+ branch: str | None = None,
533
+ at: str | None = None,
534
+ limit: int | None = None,
535
+ cursor: str | None = None,
536
+ ) -> Call[m.LinkPage]:
537
+ return Call(
538
+ "GET",
539
+ f"{_domain(domain)}/kinds/{_seg(kind)}/records/{_seg(key)}/links/{_seg(relation)}",
540
+ parse_model(m.LinkPage),
541
+ params={"branch": branch, "at": at, "limit": limit, "cursor": cursor},
542
+ safe=True,
543
+ cacheable=True,
544
+ )
545
+
546
+
547
+ def list_relation_links(
548
+ domain: str,
549
+ relation: str,
550
+ *,
551
+ branch: str | None = None,
552
+ at: str | None = None,
553
+ from_key: str | None = None,
554
+ to_key: str | None = None,
555
+ limit: int | None = None,
556
+ cursor: str | None = None,
557
+ ) -> Call[m.LinkPage]:
558
+ return Call(
559
+ "GET",
560
+ f"{_domain(domain)}/relations/{_seg(relation)}/links",
561
+ parse_model(m.LinkPage),
562
+ params={
563
+ "branch": branch,
564
+ "at": at,
565
+ "from_key": from_key,
566
+ "to_key": to_key,
567
+ "limit": limit,
568
+ "cursor": cursor,
569
+ },
570
+ safe=True,
571
+ cacheable=True,
572
+ )
573
+
574
+
575
+ def query(domain: str, body: m.QueryRequest) -> Call[m.QueryResult]:
576
+ return Call(
577
+ "POST",
578
+ f"{_domain(domain)}/query",
579
+ parse_model(m.QueryResult),
580
+ body=body.payload(),
581
+ safe=True,
582
+ )
583
+
584
+
585
+ def traverse(domain: str, body: m.TraverseRequest) -> Call[m.TraverseResult]:
586
+ return Call(
587
+ "POST",
588
+ f"{_domain(domain)}/traverse",
589
+ parse_model(m.TraverseResult),
590
+ body=body.payload(),
591
+ safe=True,
592
+ )
593
+
594
+
595
+ def neighborhood(domain: str, body: m.NeighborhoodRequest) -> Call[m.NeighborhoodResult]:
596
+ return Call(
597
+ "POST",
598
+ f"{_domain(domain)}/neighborhood",
599
+ parse_model(m.NeighborhoodResult),
600
+ body=body.payload(),
601
+ safe=True,
602
+ )
603
+
604
+
605
+ def search(domain: str, body: m.SearchRequest) -> Call[m.SearchResult]:
606
+ return Call(
607
+ "POST",
608
+ f"{_domain(domain)}/search",
609
+ parse_model(m.SearchResult),
610
+ body=body.payload(),
611
+ safe=True,
612
+ )
613
+
614
+
615
+ # Revisions, log and diff ------------------------------------------------------------------
616
+
617
+
618
+ def branch_log(
619
+ domain: str,
620
+ branch: str,
621
+ *,
622
+ all_parents: bool = False,
623
+ origin_type: str | None = None,
624
+ author: str | None = None,
625
+ limit: int | None = None,
626
+ cursor: str | None = None,
627
+ ) -> Call[m.LogPage]:
628
+ return Call(
629
+ "GET",
630
+ f"{_domain(domain)}/branches/{_branch(branch)}/log",
631
+ parse_model(m.LogPage),
632
+ params={
633
+ "all": all_parents or None,
634
+ "origin_type": origin_type,
635
+ "author": author,
636
+ "limit": limit,
637
+ "cursor": cursor,
638
+ },
639
+ safe=True,
640
+ cacheable=True,
641
+ )
642
+
643
+
644
+ def get_revision(domain: str, revision: str) -> Call[m.Revision]:
645
+ return Call(
646
+ "GET",
647
+ f"{_domain(domain)}/revisions/{_seg(revision)}",
648
+ parse_model(m.Revision),
649
+ safe=True,
650
+ cacheable=True,
651
+ )
652
+
653
+
654
+ def revision_counts(domain: str, revision: str) -> Call[m.RevisionCounts]:
655
+ return Call(
656
+ "GET",
657
+ f"{_domain(domain)}/revisions/{_seg(revision)}/counts",
658
+ parse_model(m.RevisionCounts),
659
+ safe=True,
660
+ cacheable=True,
661
+ )
662
+
663
+
664
+ def revision_changeset(
665
+ domain: str, revision: str, *, limit: int | None = None, cursor: str | None = None
666
+ ) -> Call[m.ChangesPage]:
667
+ return Call(
668
+ "GET",
669
+ f"{_domain(domain)}/revisions/{_seg(revision)}/changeset",
670
+ parse_model(m.ChangesPage),
671
+ params={"limit": limit, "cursor": cursor},
672
+ safe=True,
673
+ cacheable=True,
674
+ )
675
+
676
+
677
+ def diff(
678
+ domain: str,
679
+ from_: str,
680
+ to: str,
681
+ *,
682
+ kinds: Sequence[str] | None = None,
683
+ relations: Sequence[str] | None = None,
684
+ schema: bool | None = None,
685
+ limit: int | None = None,
686
+ cursor: str | None = None,
687
+ ) -> Call[m.ChangesPage]:
688
+ return Call(
689
+ "GET",
690
+ f"{_domain(domain)}/diff",
691
+ parse_model(m.ChangesPage),
692
+ params={
693
+ "from": from_,
694
+ "to": to,
695
+ "kinds": list(kinds) if kinds else None,
696
+ "relations": list(relations) if relations else None,
697
+ "schema": schema,
698
+ "limit": limit,
699
+ "cursor": cursor,
700
+ },
701
+ safe=True,
702
+ cacheable=True,
703
+ )
704
+
705
+
706
+ # Administration ---------------------------------------------------------------------------
707
+
708
+
709
+ def erase_vault(domain: str, body: m.EraseRequest) -> Call[m.EraseResult]:
710
+ return Call(
711
+ "POST", f"{_domain(domain)}/vault/erase", parse_model(m.EraseResult), body=body.payload()
712
+ )
713
+
714
+
715
+ def vault_keys() -> Call[m.VaultKeys]:
716
+ return Call("GET", "/vault/keys", parse_model(m.VaultKeys), safe=True)
717
+
718
+
719
+ def rotate_vault() -> Call[m.VaultRotation]:
720
+ return Call("POST", "/vault/rotate", parse_model(m.VaultRotation))
721
+
722
+
723
+ def create_api_key(domain: str, body: m.CreateApiKey) -> Call[m.ApiKeyCreated]:
724
+ return Call(
725
+ "POST", f"{_domain(domain)}/api-keys", parse_model(m.ApiKeyCreated), body=body.payload()
726
+ )
727
+
728
+
729
+ def list_api_keys(
730
+ domain: str, *, limit: int | None = None, cursor: str | None = None
731
+ ) -> Call[m.Page[m.ApiKey]]:
732
+ return Call(
733
+ "GET",
734
+ f"{_domain(domain)}/api-keys",
735
+ parse_model(m.Page[m.ApiKey]),
736
+ params={"limit": limit, "cursor": cursor},
737
+ safe=True,
738
+ )
739
+
740
+
741
+ def revoke_api_key(domain: str, key_id: str) -> Call[None]:
742
+ return Call("DELETE", f"{_domain(domain)}/api-keys/{_seg(key_id)}", parse_none, safe=True)
743
+
744
+
745
+ def list_principals(
746
+ *, type_: str | None = None, limit: int | None = None, cursor: str | None = None
747
+ ) -> Call[m.Page[m.Principal]]:
748
+ return Call(
749
+ "GET",
750
+ "/principals",
751
+ parse_model(m.Page[m.Principal]),
752
+ params={"type": type_, "limit": limit, "cursor": cursor},
753
+ safe=True,
754
+ )
755
+
756
+
757
+ def create_grant(domain: str, body: m.CreateGrant) -> Call[m.Grant]:
758
+ return Call("POST", f"{_domain(domain)}/grants", parse_model(m.Grant), body=body.payload())
759
+
760
+
761
+ def list_grants(
762
+ domain: str, *, limit: int | None = None, cursor: str | None = None
763
+ ) -> Call[m.Page[m.Grant]]:
764
+ return Call(
765
+ "GET",
766
+ f"{_domain(domain)}/grants",
767
+ parse_model(m.Page[m.Grant]),
768
+ params={"limit": limit, "cursor": cursor},
769
+ safe=True,
770
+ )
771
+
772
+
773
+ def revoke_grant(domain: str, grant_id: str) -> Call[None]:
774
+ return Call("DELETE", f"{_domain(domain)}/grants/{_seg(grant_id)}", parse_none, safe=True)
775
+
776
+
777
+ def list_audit(
778
+ domain: str, *, limit: int | None = None, cursor: str | None = None
779
+ ) -> Call[m.Page[m.AuditEntry]]:
780
+ return Call(
781
+ "GET",
782
+ f"{_domain(domain)}/audit",
783
+ parse_model(m.Page[m.AuditEntry]),
784
+ params={"limit": limit, "cursor": cursor},
785
+ safe=True,
786
+ )
787
+
788
+
789
+ def verify_audit(domain: str, *, after_seq: int | None = None) -> Call[m.AuditVerification]:
790
+ return Call(
791
+ "GET",
792
+ f"{_domain(domain)}/audit/verify",
793
+ parse_model(m.AuditVerification),
794
+ params={"after_seq": after_seq},
795
+ safe=True,
796
+ )
797
+
798
+
799
+ # Proposals --------------------------------------------------------------------------------
800
+
801
+
802
+ def create_proposal(
803
+ domain: str, body: m.ProposalCreate, *, idempotency_key: str | None = None
804
+ ) -> Call[m.ProposalDetail]:
805
+ return Call(
806
+ "POST",
807
+ f"{_domain(domain)}/proposals",
808
+ parse_model(m.ProposalDetail),
809
+ body=body.payload(),
810
+ accepts_key=True,
811
+ idempotency_key=idempotency_key,
812
+ )
813
+
814
+
815
+ def list_proposals(
816
+ domain: str,
817
+ *,
818
+ status: m.ProposalStatus | None = None,
819
+ origin: m.ProposalOrigin | None = None,
820
+ source: str | None = None,
821
+ target: str | None = None,
822
+ author: str | None = None,
823
+ limit: int | None = None,
824
+ cursor: str | None = None,
825
+ ) -> Call[m.Page[m.Proposal]]:
826
+ return Call(
827
+ "GET",
828
+ f"{_domain(domain)}/proposals",
829
+ parse_model(m.Page[m.Proposal]),
830
+ params={
831
+ "status": status,
832
+ "origin": origin,
833
+ "source": source,
834
+ "target": target,
835
+ "author": author,
836
+ "limit": limit,
837
+ "cursor": cursor,
838
+ },
839
+ safe=True,
840
+ )
841
+
842
+
843
+ def get_proposal(domain: str, proposal_id: str) -> Call[m.ProposalDetail]:
844
+ return Call(
845
+ "GET",
846
+ f"{_domain(domain)}/proposals/{_seg(proposal_id)}",
847
+ parse_model(m.ProposalDetail),
848
+ safe=True,
849
+ )
850
+
851
+
852
+ def update_proposal(
853
+ domain: str, proposal_id: str, body: m.ProposalUpdate
854
+ ) -> Call[m.ProposalDetail]:
855
+ return Call(
856
+ "PATCH",
857
+ f"{_domain(domain)}/proposals/{_seg(proposal_id)}",
858
+ parse_model(m.ProposalDetail),
859
+ body=body.payload(),
860
+ safe=True,
861
+ )
862
+
863
+
864
+ def close_proposal(domain: str, proposal_id: str) -> Call[m.ProposalDetail]:
865
+ return Call(
866
+ "POST",
867
+ f"{_domain(domain)}/proposals/{_seg(proposal_id)}/close",
868
+ parse_model(m.ProposalDetail),
869
+ )
870
+
871
+
872
+ def review_proposal(domain: str, proposal_id: str, body: m.ReviewIn) -> Call[m.ReviewResult]:
873
+ return Call(
874
+ "POST",
875
+ f"{_domain(domain)}/proposals/{_seg(proposal_id)}/reviews",
876
+ parse_model(m.ReviewResult),
877
+ body=body.payload(),
878
+ )
879
+
880
+
881
+ def merge_proposal(
882
+ domain: str,
883
+ proposal_id: str,
884
+ body: m.MergeIn | None = None,
885
+ *,
886
+ idempotency_key: str | None = None,
887
+ ) -> Call[m.MergeResult]:
888
+ return Call(
889
+ "POST",
890
+ f"{_domain(domain)}/proposals/{_seg(proposal_id)}/merge",
891
+ parse_model(m.MergeResult),
892
+ body=None if body is None else body.payload(),
893
+ accepts_key=True,
894
+ idempotency_key=idempotency_key,
895
+ )
896
+
897
+
898
+ def list_conflicts(
899
+ domain: str,
900
+ proposal_id: str,
901
+ *,
902
+ kind: str | None = None,
903
+ status: m.ConflictStatus | None = None,
904
+ limit: int | None = None,
905
+ cursor: str | None = None,
906
+ ) -> Call[m.Conflicts]:
907
+ return Call(
908
+ "GET",
909
+ f"{_domain(domain)}/proposals/{_seg(proposal_id)}/conflicts",
910
+ parse_model(m.Conflicts),
911
+ params={"kind": kind, "status": status, "limit": limit, "cursor": cursor},
912
+ safe=True,
913
+ )
914
+
915
+
916
+ def resolve_conflict(
917
+ domain: str, proposal_id: str, conflict_id: str, body: m.ResolutionIn
918
+ ) -> Call[m.Resolution]:
919
+ return Call(
920
+ "POST",
921
+ f"{_domain(domain)}/proposals/{_seg(proposal_id)}/conflicts/{_seg(conflict_id)}/resolution",
922
+ parse_model(m.Resolution),
923
+ body=body.payload(),
924
+ safe=True,
925
+ )
926
+
927
+
928
+ def resolve_conflicts(domain: str, proposal_id: str, body: m.ResolveManyIn) -> Call[m.Conflicts]:
929
+ return Call(
930
+ "POST",
931
+ f"{_domain(domain)}/proposals/{_seg(proposal_id)}/conflicts:resolve",
932
+ parse_model(m.Conflicts),
933
+ body=body.payload(),
934
+ safe=True,
935
+ )
936
+
937
+
938
+ def list_proposal_items(
939
+ domain: str,
940
+ proposal_id: str,
941
+ *,
942
+ status: Sequence[m.ItemDecisionValue] | None = None,
943
+ kind: Sequence[str] | None = None,
944
+ type: Sequence[m.ItemType] | None = None,
945
+ min_confidence: float | None = None,
946
+ max_confidence: float | None = None,
947
+ limit: int | None = None,
948
+ cursor: str | None = None,
949
+ ) -> Call[m.Page[m.ProposalItem]]:
950
+ """Items of an extraction Proposal; a filter given several values matches any of them."""
951
+ return Call(
952
+ "GET",
953
+ f"{_domain(domain)}/proposals/{_seg(proposal_id)}/items",
954
+ parse_model(m.Page[m.ProposalItem]),
955
+ params={
956
+ "status": list(status or ()),
957
+ "kind": list(kind or ()),
958
+ "type": list(type or ()),
959
+ "min_confidence": min_confidence,
960
+ "max_confidence": max_confidence,
961
+ "limit": limit,
962
+ "cursor": cursor,
963
+ },
964
+ safe=True,
965
+ )
966
+
967
+
968
+ def decide_proposal_items(
969
+ domain: str, proposal_id: str, body: m.DecideItems
970
+ ) -> Call[m.ItemsDecided]:
971
+ """Safe to repeat: the same decisions leave the same state."""
972
+ return Call(
973
+ "POST",
974
+ f"{_domain(domain)}/proposals/{_seg(proposal_id)}/items:decide",
975
+ parse_model(m.ItemsDecided),
976
+ body=body.payload(),
977
+ safe=True,
978
+ )
979
+
980
+
981
+ # Agents -----------------------------------------------------------------------------------
982
+
983
+
984
+ def context_pack(domain: str, body: m.ContextRequest) -> Call[m.ContextPack]:
985
+ request = body.model_copy(update={"format": "json"})
986
+ return Call(
987
+ "POST",
988
+ f"{_domain(domain)}/context",
989
+ parse_model(m.ContextPack),
990
+ body=request.payload(),
991
+ safe=True,
992
+ )
993
+
994
+
995
+ def context_pack_markdown(domain: str, body: m.ContextRequest) -> Call[str]:
996
+ request = body.model_copy(update={"format": "markdown"})
997
+ return Call(
998
+ "POST",
999
+ f"{_domain(domain)}/context",
1000
+ parse_text,
1001
+ body=request.payload(),
1002
+ accept="text/markdown",
1003
+ safe=True,
1004
+ )
1005
+
1006
+
1007
+ def explain_record(
1008
+ domain: str, kind: str, key: str, *, branch: str | None = None, at: str | None = None
1009
+ ) -> Call[m.Explain]:
1010
+ return Call(
1011
+ "GET",
1012
+ f"{_domain(domain)}/kinds/{_seg(kind)}/records/{_seg(key)}/explain",
1013
+ parse_model(m.Explain),
1014
+ params={"branch": branch, "at": at, "format": "json"},
1015
+ safe=True,
1016
+ cacheable=True,
1017
+ )
1018
+
1019
+
1020
+ def explain_record_markdown(
1021
+ domain: str, kind: str, key: str, *, branch: str | None = None, at: str | None = None
1022
+ ) -> Call[str]:
1023
+ return Call(
1024
+ "GET",
1025
+ f"{_domain(domain)}/kinds/{_seg(kind)}/records/{_seg(key)}/explain",
1026
+ parse_text,
1027
+ params={"branch": branch, "at": at, "format": "markdown"},
1028
+ accept="text/markdown",
1029
+ safe=True,
1030
+ cacheable=True,
1031
+ )
1032
+
1033
+
1034
+ def domain_health(
1035
+ domain: str, *, branch: str | None = None, at: str | None = None
1036
+ ) -> Call[m.DomainHealth]:
1037
+ return Call(
1038
+ "GET",
1039
+ f"{_domain(domain)}/health",
1040
+ parse_model(m.DomainHealth),
1041
+ params={"branch": branch, "at": at, "format": "json"},
1042
+ safe=True,
1043
+ cacheable=True,
1044
+ )
1045
+
1046
+
1047
+ def domain_health_markdown(
1048
+ domain: str, *, branch: str | None = None, at: str | None = None
1049
+ ) -> Call[str]:
1050
+ return Call(
1051
+ "GET",
1052
+ f"{_domain(domain)}/health",
1053
+ parse_text,
1054
+ params={"branch": branch, "at": at, "format": "markdown"},
1055
+ accept="text/markdown",
1056
+ safe=True,
1057
+ cacheable=True,
1058
+ )
1059
+
1060
+
1061
+ def kind_health(
1062
+ domain: str, kind: str, *, branch: str | None = None, at: str | None = None
1063
+ ) -> Call[m.KindHealthReport]:
1064
+ return Call(
1065
+ "GET",
1066
+ f"{_domain(domain)}/kinds/{_seg(kind)}/health",
1067
+ parse_model(m.KindHealthReport),
1068
+ params={"branch": branch, "at": at, "format": "json"},
1069
+ safe=True,
1070
+ cacheable=True,
1071
+ )
1072
+
1073
+
1074
+ def kind_health_markdown(
1075
+ domain: str, kind: str, *, branch: str | None = None, at: str | None = None
1076
+ ) -> Call[str]:
1077
+ return Call(
1078
+ "GET",
1079
+ f"{_domain(domain)}/kinds/{_seg(kind)}/health",
1080
+ parse_text,
1081
+ params={"branch": branch, "at": at, "format": "markdown"},
1082
+ accept="text/markdown",
1083
+ safe=True,
1084
+ cacheable=True,
1085
+ )
1086
+
1087
+
1088
+ def branch_changes(
1089
+ domain: str,
1090
+ branch: str,
1091
+ *,
1092
+ since: str | None = None,
1093
+ kinds: Sequence[str] | None = None,
1094
+ limit: int | None = None,
1095
+ ) -> Call[m.ChangesFeed]:
1096
+ return Call(
1097
+ "GET",
1098
+ f"{_domain(domain)}/branches/{_branch(branch)}/changes",
1099
+ parse_model(m.ChangesFeed),
1100
+ params={"since": since, "kinds": list(kinds) if kinds else None, "limit": limit},
1101
+ safe=True,
1102
+ cacheable=True,
1103
+ )
1104
+
1105
+
1106
+ def branch_events(
1107
+ domain: str,
1108
+ branch: str,
1109
+ *,
1110
+ kinds: Sequence[str] | None = None,
1111
+ last_event_id: str | None = None,
1112
+ ) -> Call[str]:
1113
+ """The Server-Sent Events stream of a branch, as text.
1114
+
1115
+ The answer is read until the server closes the stream, so the call returns only then (or
1116
+ fails at the client timeout); use `last_event_id` to resume after the last event seen.
1117
+ """
1118
+ return Call(
1119
+ "GET",
1120
+ f"{_domain(domain)}/branches/{_branch(branch)}/events",
1121
+ parse_text,
1122
+ params={"kinds": list(kinds) if kinds else None, "last_event_id": last_event_id},
1123
+ accept="text/event-stream",
1124
+ safe=True,
1125
+ )
1126
+
1127
+
1128
+ # Webhooks ---------------------------------------------------------------------------------
1129
+
1130
+
1131
+ def create_webhook(domain: str, body: m.CreateWebhook) -> Call[m.WebhookCreated]:
1132
+ return Call(
1133
+ "POST", f"{_domain(domain)}/webhooks", parse_model(m.WebhookCreated), body=body.payload()
1134
+ )
1135
+
1136
+
1137
+ def list_webhooks(domain: str) -> Call[m.WebhookList]:
1138
+ return Call("GET", f"{_domain(domain)}/webhooks", parse_model(m.WebhookList), safe=True)
1139
+
1140
+
1141
+ def update_webhook(domain: str, webhook_id: str, body: m.UpdateWebhook) -> Call[m.Webhook]:
1142
+ """Idempotent: the body is the full new set of clearances."""
1143
+ return Call(
1144
+ "PATCH",
1145
+ f"{_domain(domain)}/webhooks/{_seg(webhook_id)}",
1146
+ parse_model(m.Webhook),
1147
+ body=body.payload(),
1148
+ safe=True,
1149
+ )
1150
+
1151
+
1152
+ def delete_webhook(domain: str, webhook_id: str) -> Call[None]:
1153
+ return Call("DELETE", f"{_domain(domain)}/webhooks/{_seg(webhook_id)}", parse_none, safe=True)
1154
+
1155
+
1156
+ # Sources ----------------------------------------------------------------------------------
1157
+
1158
+
1159
+ def sync_source(domain: str, source: str, body: m.SyncRequest) -> Call[m.SyncQueued]:
1160
+ """Not marked safe: a retry may queue a second job once the first one has finished."""
1161
+ return Call(
1162
+ "POST",
1163
+ f"{_domain(domain)}/sources/{_seg(source)}/sync",
1164
+ parse_model(m.SyncQueued),
1165
+ body=body.payload(),
1166
+ )
1167
+
1168
+
1169
+ def list_source_runs(
1170
+ domain: str, source: str, *, limit: int | None = None, cursor: str | None = None
1171
+ ) -> Call[m.Page[m.SourceRun]]:
1172
+ return Call(
1173
+ "GET",
1174
+ f"{_domain(domain)}/sources/{_seg(source)}/runs",
1175
+ parse_model(m.Page[m.SourceRun]),
1176
+ params={"limit": limit, "cursor": cursor},
1177
+ safe=True,
1178
+ )
1179
+
1180
+
1181
+ # Documents --------------------------------------------------------------------------------
1182
+
1183
+
1184
+ def upload_document(
1185
+ domain: str,
1186
+ file: str | os.PathLike[str] | bytes,
1187
+ *,
1188
+ filename: str | None = None,
1189
+ extract: bool | None = None,
1190
+ tiers: Sequence[int] | None = None,
1191
+ ) -> Call[m.DocumentUploaded]:
1192
+ """Upload a document: a path (read here, named after the file) or bytes with a `filename`.
1193
+
1194
+ Not marked safe: a retry may queue a second extraction job.
1195
+ """
1196
+ if isinstance(file, bytes):
1197
+ if not filename:
1198
+ raise ValueError("a filename is required to upload bytes")
1199
+ name, content = filename, file
1200
+ else:
1201
+ path = Path(file)
1202
+ name, content = filename or path.name, path.read_bytes()
1203
+ form: dict[str, str] = {}
1204
+ if extract is not None:
1205
+ form["extract"] = "true" if extract else "false"
1206
+ if tiers is not None:
1207
+ form["tiers"] = json.dumps(list(tiers), separators=(",", ":"))
1208
+ return Call(
1209
+ "POST",
1210
+ f"{_domain(domain)}/documents",
1211
+ parse_model(m.DocumentUploaded),
1212
+ files=(("file", name, content),),
1213
+ form=form,
1214
+ )
1215
+
1216
+
1217
+ def upload_document_url(domain: str, body: m.DocumentFromUrl) -> Call[m.DocumentUploaded]:
1218
+ """Have the server fetch the document from an allowlisted URL. Not marked safe."""
1219
+ return Call(
1220
+ "POST",
1221
+ f"{_domain(domain)}/documents",
1222
+ parse_model(m.DocumentUploaded),
1223
+ body=body.payload(),
1224
+ )
1225
+
1226
+
1227
+ def list_documents(
1228
+ domain: str, *, limit: int | None = None, cursor: str | None = None
1229
+ ) -> Call[m.Page[m.Document]]:
1230
+ return Call(
1231
+ "GET",
1232
+ f"{_domain(domain)}/documents",
1233
+ parse_model(m.Page[m.Document]),
1234
+ params={"limit": limit, "cursor": cursor},
1235
+ safe=True,
1236
+ )
1237
+
1238
+
1239
+ def get_document(domain: str, document_id: str) -> Call[m.DocumentDetail]:
1240
+ return Call(
1241
+ "GET",
1242
+ f"{_domain(domain)}/documents/{_seg(document_id)}",
1243
+ parse_model(m.DocumentDetail),
1244
+ safe=True,
1245
+ )
1246
+
1247
+
1248
+ def get_document_text(domain: str, document_id: str) -> Call[m.DocumentText]:
1249
+ return Call(
1250
+ "GET",
1251
+ f"{_domain(domain)}/documents/{_seg(document_id)}/text",
1252
+ parse_model(m.DocumentText),
1253
+ safe=True,
1254
+ )
1255
+
1256
+
1257
+ # Agent trace ------------------------------------------------------------------------------
1258
+
1259
+
1260
+ def list_trace_sessions(
1261
+ *,
1262
+ actor: str | None = None,
1263
+ domain_id: str | None = None,
1264
+ limit: int | None = None,
1265
+ cursor: str | None = None,
1266
+ ) -> Call[m.TraceSessionPage]:
1267
+ return Call(
1268
+ "GET",
1269
+ "/trace/sessions",
1270
+ parse_model(m.TraceSessionPage),
1271
+ params={"actor": actor, "domain_id": domain_id, "limit": limit, "cursor": cursor},
1272
+ safe=True,
1273
+ )
1274
+
1275
+
1276
+ def get_trace_session(session_id: str, *, actor: str | None = None) -> Call[m.TraceTimeline]:
1277
+ return Call(
1278
+ "GET",
1279
+ f"/trace/sessions/{_seg(session_id)}",
1280
+ parse_model(m.TraceTimeline),
1281
+ params={"actor": actor},
1282
+ safe=True,
1283
+ )
1284
+
1285
+
1286
+ def add_trace_note(session_id: str, body: m.NoteRequest) -> Call[m.NoteAccepted]:
1287
+ return Call(
1288
+ "POST",
1289
+ f"/trace/sessions/{_seg(session_id)}/notes",
1290
+ parse_model(m.NoteAccepted),
1291
+ body=body.payload(),
1292
+ )