lorebase-cli 0.2.3__py3-none-any.whl → 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
lorebase_cli/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """lorebase CLI: command line client of a Lorebase Keeper."""
2
2
 
3
- __version__ = "0.2.3"
3
+ __version__ = "0.3.0"
@@ -57,6 +57,20 @@ def context_command(
57
57
  list[str] | None,
58
58
  typer.Option("--relation", help="Follow only this Relation. Repeatable."),
59
59
  ] = None,
60
+ trait: Annotated[
61
+ list[str] | None,
62
+ typer.Option(
63
+ "--trait",
64
+ help="Limit --search and --query seeds and neighbors to the Kinds implementing "
65
+ "this Trait. Repeatable.",
66
+ ),
67
+ ] = None,
68
+ lens: Annotated[
69
+ str | None,
70
+ typer.Option(
71
+ "--lens", help="Apply this lens: its Relations, depth, budget and Kinds are defaults."
72
+ ),
73
+ ] = None,
60
74
  domain: DomainOpt = None,
61
75
  branch: BranchOpt = None,
62
76
  at: AtOpt = None,
@@ -80,6 +94,8 @@ def context_command(
80
94
  budget_tokens=budget,
81
95
  depth=depth,
82
96
  relations=relation or None,
97
+ traits=trait or None,
98
+ lens=lens,
83
99
  )
84
100
  agents = app.client().agents
85
101
  name = app.domain(domain)
@@ -1,9 +1,10 @@
1
- """`import`, `record`, `query` and `search`: loading and reading Records and Links."""
1
+ """`import`, `record`, `query`, `search` and `resolve`: loading and reading Records and Links."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
5
  import json
6
6
  import sys
7
+ from collections.abc import Iterator
7
8
  from pathlib import Path
8
9
  from typing import Annotated, Any
9
10
 
@@ -638,15 +639,39 @@ def query_command(
638
639
  body["at"] = at
639
640
  client = app.client()
640
641
  domain_id = app.domain(domain)
642
+ first = client.records.query(domain_id, body)
641
643
  if body.get("aggregate"):
642
- result = client.records.query(domain_id, body)
643
- app.out.emit(result, lambda: _print_aggregate(app, result))
644
+ app.out.emit(first, lambda: _print_aggregate(app, first))
645
+ _print_coverage(app, first.coverage)
644
646
  return
645
- items, more = app.take(client.records.iterate_query(domain_id, body), limit, everything)
647
+
648
+ def pages() -> Iterator[m.RecordItem]:
649
+ page = first
650
+ while True:
651
+ yield from page.items
652
+ if page.next_cursor is None:
653
+ return
654
+ # Only the request's own members: a Keeper older than the coverage block refuses
655
+ # unknown ones, so later pages repeat the first one's body with the cursor.
656
+ following = {**body, "cursor": page.next_cursor}
657
+ page = client.records.query(domain_id, following)
658
+
659
+ items, more = app.take(pages(), limit, everything)
646
660
  selected = [str(s) for s in body.get("select") or [] if s not in ("key", "title")]
647
661
  _print_records(app, items, selected)
648
662
  if more:
649
663
  app.note_truncated(len(items))
664
+ _print_coverage(app, first.coverage)
665
+
666
+
667
+ def _print_coverage(app: AppContext, coverage: m.Coverage | None) -> None:
668
+ """The coverage notes (design 8.11), one line each, besides the results."""
669
+ if coverage is None:
670
+ return
671
+ for note in coverage.notes:
672
+ app.out.info(note)
673
+ if coverage.omitted:
674
+ app.out.info(f"coverage: {coverage.omitted}")
650
675
 
651
676
 
652
677
  def _query_body(text: str) -> dict[str, Any]:
@@ -691,6 +716,18 @@ def search_command(
691
716
  kind: Annotated[
692
717
  list[str] | None, typer.Option("--kind", help="Only this Kind. Repeatable.")
693
718
  ] = None,
719
+ trait: Annotated[
720
+ list[str] | None,
721
+ typer.Option(
722
+ "--trait", help="Only the Kinds that implement this Trait. Repeatable; adds to --kind."
723
+ ),
724
+ ] = None,
725
+ lens: Annotated[
726
+ str | None,
727
+ typer.Option(
728
+ "--lens", help="Search the Kinds and Traits of this lens unless --kind or --trait."
729
+ ),
730
+ ] = None,
694
731
  mode: Annotated[
695
732
  str | None,
696
733
  typer.Option(
@@ -719,6 +756,8 @@ def search_command(
719
756
  {
720
757
  "q": text,
721
758
  "kinds": kind,
759
+ "traits": trait,
760
+ "lens": lens,
722
761
  "mode": mode,
723
762
  "query_vector": vector,
724
763
  "branch": app.branch(branch),
@@ -740,3 +779,78 @@ def search_command(
740
779
  ],
741
780
  ),
742
781
  )
782
+ if result.semantic_truncated:
783
+ app.out.info("the vector list is short: the index scan stopped at its bound; narrow --kind")
784
+
785
+
786
+ def resolve_command(
787
+ ctx: typer.Context,
788
+ text: Annotated[str, typer.Argument(help="Words to ground, e.g. a question.", metavar="TEXT")],
789
+ kind: Annotated[
790
+ list[str] | None, typer.Option("--kind", help="Only look in this Kind. Repeatable.")
791
+ ] = None,
792
+ level: Annotated[
793
+ list[str] | None,
794
+ typer.Option(
795
+ "--level",
796
+ help="exact, synonym, trigram or embedding (default: those the server enables). "
797
+ "Repeatable.",
798
+ ),
799
+ ] = None,
800
+ vector_file: Annotated[
801
+ str | None,
802
+ typer.Option(
803
+ "--vector-file",
804
+ help="Embedding of TEXT, enabling the embedding level when nothing else matched: a "
805
+ "JSON array, or the output of `lorebase-inference embed`; `-` reads standard input.",
806
+ ),
807
+ ] = None,
808
+ domain: DomainOpt = None,
809
+ branch: BranchOpt = None,
810
+ at: AtOpt = None,
811
+ limit: LimitOpt = None,
812
+ json_out: JsonOpt = False,
813
+ ) -> None:
814
+ """Ground the words of a text in Kinds, Attributes and stored values (with `where` fragments
815
+ in --json); it never runs a query."""
816
+ app = app_of(ctx, json_out)
817
+ vector = None if vector_file is None else query_vector(vector_file)
818
+ request = m.ResolveRequest.model_validate(
819
+ {
820
+ "text": text,
821
+ "kinds": kind,
822
+ "levels": level,
823
+ "query_vector": vector,
824
+ "branch": app.branch(branch),
825
+ "at": at,
826
+ "limit": limit or app.settings.list_limit,
827
+ }
828
+ )
829
+ result = app.client().records.resolve(app.domain(domain), request)
830
+
831
+ def rows() -> list[dict[str, Any]]:
832
+ return [
833
+ {**c.model_dump(), "ref": ".".join(filter(None, [c.kind, c.attribute]))}
834
+ for c in result.candidates
835
+ ]
836
+
837
+ app.out.emit(
838
+ result,
839
+ lambda: app.out.table(
840
+ rows(),
841
+ [
842
+ Column("target", "target"),
843
+ Column("kind.attribute", "ref"),
844
+ Column("values", "values"),
845
+ Column("level", "level"),
846
+ Column("score", "score", lambda v: f"{v:.3f}"),
847
+ Column("records", "records"),
848
+ Column("matched", "matched"),
849
+ ],
850
+ ),
851
+ )
852
+ for skipped in result.skipped:
853
+ app.out.info(f"{skipped.level} skipped: {skipped.reason}")
854
+ if result.unmatched:
855
+ app.out.info("unmatched: " + ", ".join(result.unmatched))
856
+ _print_coverage(app, result.coverage)
@@ -1,4 +1,6 @@
1
- """`documents upload|list|get|text`: documents stored for extraction by Scribe."""
1
+ """`documents upload|list|versions|get|text|retire|purge`: documents stored for extraction by
2
+ Scribe. Uploads with `--key` are versions of one source; retiring stops further extraction and
3
+ purging irreversibly erases the content."""
2
4
 
3
5
  from __future__ import annotations
4
6
 
@@ -10,7 +12,7 @@ import typer
10
12
  from lorebase_cli.context import AppContext, app_of
11
13
  from lorebase_cli.failures import CliError
12
14
  from lorebase_cli.options import AllOpt, DomainOpt, JsonOpt, LimitOpt
13
- from lorebase_cli.output import Column, short_id
15
+ from lorebase_cli.output import Column
14
16
  from lorebase_client import models as m
15
17
 
16
18
  documents_app = typer.Typer(
@@ -22,11 +24,20 @@ DOCUMENT_COLUMNS = [
22
24
  Column("filename", "filename"),
23
25
  Column("type", "media_type"),
24
26
  Column("size", "size"),
25
- Column("sha256", "sha256", short_id),
27
+ Column("key", "source_key"),
28
+ Column("v", "version"),
29
+ Column("status", "status"),
26
30
  Column("created", "created_at"),
27
31
  ]
28
32
 
29
33
 
34
+ KeyOpt = Annotated[
35
+ str | None,
36
+ typer.Option("--key", help="Source key (path, URL or external id): the document's identity."),
37
+ ]
38
+ ReasonOpt = Annotated[str, typer.Option("--reason", help="Why, recorded in the audit.")]
39
+
40
+
30
41
  def _tiers(text: str | None) -> list[int] | None:
31
42
  """Extraction tiers from a comma separated list such as `0,1`."""
32
43
  if text is None:
@@ -44,9 +55,23 @@ def _document_pairs(found: m.Document) -> list[tuple[str, Any]]:
44
55
  ("media type", found.media_type),
45
56
  ("size", found.size),
46
57
  ("sha256", found.sha256),
58
+ ("source key", found.source_key),
59
+ ("version", found.version),
60
+ ("replaces", found.replaces),
61
+ ("status", found.status),
47
62
  ("text parsed", found.text_sha256 is not None),
48
63
  ("created", found.created_at),
49
64
  ("created by", found.created_by),
65
+ *(
66
+ [("retired", f"{found.retired_at} by {found.retired_by}")]
67
+ if found.retired_at is not None
68
+ else []
69
+ ),
70
+ *(
71
+ [("purged", f"{found.purged_at} by {found.purged_by}")]
72
+ if found.purged_at is not None
73
+ else []
74
+ ),
50
75
  ]
51
76
 
52
77
 
@@ -67,10 +92,14 @@ def documents_upload(
67
92
  tiers: Annotated[
68
93
  str | None, typer.Option("--tiers", help="Extraction tiers, comma separated (e.g. 0,1).")
69
94
  ] = None,
95
+ key: KeyOpt = None,
70
96
  domain: DomainOpt = None,
71
97
  json_out: JsonOpt = False,
72
98
  ) -> None:
73
- """Upload a document (PATH, or --url) and queue its extraction."""
99
+ """Upload a document (PATH, or --url) and queue its extraction.
100
+
101
+ With --key, new bytes become a new version of the key's document.
102
+ """
74
103
  app = app_of(ctx, json_out)
75
104
  name = app.domain(domain)
76
105
  extract = False if no_extract else None
@@ -78,11 +107,11 @@ def documents_upload(
78
107
  documents = app.client().documents
79
108
  if path is not None and url is None:
80
109
  try:
81
- uploaded = documents.upload(name, path, extract=extract, tiers=wanted)
110
+ uploaded = documents.upload(name, path, extract=extract, tiers=wanted, source_key=key)
82
111
  except OSError as error:
83
112
  raise CliError(f"cannot read {path}: {error.strerror or error}") from error
84
113
  elif url is not None and path is None:
85
- uploaded = documents.upload_url(name, url, extract=extract, tiers=wanted)
114
+ uploaded = documents.upload_url(name, url, extract=extract, tiers=wanted, source_key=key)
86
115
  else:
87
116
  raise CliError("give PATH or --url")
88
117
  app.out.emit(
@@ -92,6 +121,7 @@ def documents_upload(
92
121
  *_document_pairs(uploaded.document),
93
122
  ("duplicate", uploaded.duplicate),
94
123
  ("job", uploaded.job_id),
124
+ ("check job", uploaded.check_job_id),
95
125
  ]
96
126
  ),
97
127
  )
@@ -100,6 +130,9 @@ def documents_upload(
100
130
  @documents_app.command("list")
101
131
  def documents_list(
102
132
  ctx: typer.Context,
133
+ key: Annotated[
134
+ str | None, typer.Option("--key", help="Only the versions of this source key.")
135
+ ] = None,
103
136
  domain: DomainOpt = None,
104
137
  limit: LimitOpt = None,
105
138
  everything: AllOpt = False,
@@ -107,7 +140,26 @@ def documents_list(
107
140
  ) -> None:
108
141
  """List the documents of a Domain, newest first."""
109
142
  app = app_of(ctx, json_out)
110
- items, more = app.take(app.client().documents.iterate(app.domain(domain)), limit, everything)
143
+ stream = app.client().documents.iterate(app.domain(domain), source_key=key)
144
+ items, more = app.take(stream, limit, everything)
145
+ app.out.emit(items, lambda: app.out.table([d.model_dump() for d in items], DOCUMENT_COLUMNS))
146
+ if more:
147
+ app.note_truncated(len(items))
148
+
149
+
150
+ @documents_app.command("versions")
151
+ def documents_versions(
152
+ ctx: typer.Context,
153
+ document_id: Annotated[str, typer.Argument(help="Document id.", metavar="ID")],
154
+ domain: DomainOpt = None,
155
+ limit: LimitOpt = None,
156
+ everything: AllOpt = False,
157
+ json_out: JsonOpt = False,
158
+ ) -> None:
159
+ """List the versions of a document's source key, newest first."""
160
+ app = app_of(ctx, json_out)
161
+ stream = app.client().documents.iterate_versions(app.domain(domain), document_id)
162
+ items, more = app.take(stream, limit, everything)
111
163
  app.out.emit(items, lambda: app.out.table([d.model_dump() for d in items], DOCUMENT_COLUMNS))
112
164
  if more:
113
165
  app.note_truncated(len(items))
@@ -159,3 +211,56 @@ def documents_text(
159
211
  app.out.info(f"text cut at {len(found.text)} of {found.length} characters")
160
212
 
161
213
  app.out.emit(found, human)
214
+
215
+
216
+ @documents_app.command("retire")
217
+ def documents_retire(
218
+ ctx: typer.Context,
219
+ document_id: Annotated[str, typer.Argument(help="Document id.", metavar="ID")],
220
+ reason: ReasonOpt,
221
+ domain: DomainOpt = None,
222
+ json_out: JsonOpt = False,
223
+ ) -> None:
224
+ """Retire a document: it is never extracted again and its evidence is re-checked."""
225
+ app = app_of(ctx, json_out)
226
+ done = app.client().documents.retire(app.domain(domain), document_id, reason=reason)
227
+ app.out.emit(
228
+ done,
229
+ lambda: app.out.pairs([*_document_pairs(done.document), ("check job", done.job_id)]),
230
+ )
231
+
232
+
233
+ @documents_app.command("purge")
234
+ def documents_purge(
235
+ ctx: typer.Context,
236
+ document_id: Annotated[str, typer.Argument(help="Document id.", metavar="ID")],
237
+ reason: ReasonOpt,
238
+ confirm: Annotated[
239
+ str, typer.Option("--confirm", help="The document id again: the purge is irreversible.")
240
+ ],
241
+ domain: DomainOpt = None,
242
+ json_out: JsonOpt = False,
243
+ ) -> None:
244
+ """Irreversibly erase a document's bytes, text, chunks and evidence quotes."""
245
+ if confirm != document_id:
246
+ raise CliError(f"--confirm must repeat the document id {document_id!r}")
247
+ app = app_of(ctx, json_out)
248
+ done = app.client().documents.purge(
249
+ app.domain(domain), document_id, reason=reason, confirm=confirm
250
+ )
251
+ app.out.emit(
252
+ done,
253
+ lambda: app.out.pairs(
254
+ [
255
+ *_document_pairs(done.document),
256
+ ("check job", done.job_id),
257
+ ("blob deleted", done.blob_deleted),
258
+ ("text deleted", done.text_deleted),
259
+ ("chunks", done.chunks),
260
+ ("cache entries", done.cache_entries),
261
+ ("items erased", done.items),
262
+ ("candidates erased", done.candidates),
263
+ ("suggestions erased", done.suggestions),
264
+ ]
265
+ ),
266
+ )
@@ -9,7 +9,7 @@ import typer
9
9
  from lorebase_cli.commands.data import json_argument
10
10
  from lorebase_cli.context import AppContext, app_of
11
11
  from lorebase_cli.failures import CliError
12
- from lorebase_cli.options import AllOpt, DomainOpt, JsonOpt, LimitOpt
12
+ from lorebase_cli.options import AllOpt, DomainOpt, JsonOpt, LimitOpt, MineOpt
13
13
  from lorebase_cli.output import Column, short_id
14
14
  from lorebase_client import models as m
15
15
 
@@ -26,6 +26,8 @@ PROPOSAL_COLUMNS = [
26
26
  Column("target", "target_ref"),
27
27
  Column("status", "status"),
28
28
  Column("author", "author"),
29
+ Column("stewards", "steward_roles"),
30
+ Column("assignees", "assignees"),
29
31
  Column("created", "created_at"),
30
32
  ]
31
33
  CONFLICT_COLUMNS = [
@@ -111,6 +113,7 @@ def proposal_list(
111
113
  author: Annotated[
112
114
  str | None, typer.Option("--author", help="Only Proposals by this principal.")
113
115
  ] = None,
116
+ mine: MineOpt = False,
114
117
  domain: DomainOpt = None,
115
118
  limit: LimitOpt = None,
116
119
  everything: AllOpt = False,
@@ -125,6 +128,7 @@ def proposal_list(
125
128
  source=source,
126
129
  target=target,
127
130
  author=author,
131
+ assigned="me" if mine else None,
128
132
  ),
129
133
  limit,
130
134
  everything,
@@ -0,0 +1,233 @@
1
+ """`reconcile list|show|resolve|dismiss|reopen`: the reconciliation queue."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Annotated, Any, cast, get_args
6
+
7
+ import typer
8
+
9
+ from lorebase_cli.commands.data import json_argument
10
+ from lorebase_cli.context import AppContext, app_of
11
+ from lorebase_cli.failures import CliError
12
+ from lorebase_cli.options import AllOpt, BranchOpt, DomainOpt, JsonOpt, LimitOpt, MineOpt
13
+ from lorebase_cli.output import Column
14
+ from lorebase_client import models as m
15
+
16
+ reconcile_app = typer.Typer(
17
+ help="Review contradictions and duplicates between Sources.", no_args_is_help=True
18
+ )
19
+
20
+ CandidateArg = Annotated[str, typer.Argument(help="Candidate id.", metavar="ID")]
21
+ ReasonOpt = Annotated[str, typer.Option("--reason", "-r", help="Why; kept in the audit log.")]
22
+
23
+ CANDIDATE_COLUMNS = [
24
+ Column("id", "id"),
25
+ Column("type", "type"),
26
+ Column("status", "status"),
27
+ Column("kind", "kind"),
28
+ Column("keys", "keys", lambda keys: ", ".join(keys)),
29
+ Column("attribute", "attribute"),
30
+ Column("claims", "claim_count"),
31
+ Column("stewards", "steward_roles"),
32
+ Column("assignees", "assignees"),
33
+ Column("created", "created_at"),
34
+ ]
35
+
36
+
37
+ def _show_candidate(app: AppContext, found: m.ReconciliationCandidate) -> None:
38
+ pairs: list[tuple[str, Any]] = [
39
+ ("candidate", found.id),
40
+ ("branch", found.branch),
41
+ ("type", found.type),
42
+ ("status", found.status),
43
+ ("kind", found.kind),
44
+ ("keys", found.keys),
45
+ ("attribute", found.attribute),
46
+ (
47
+ "claims",
48
+ [
49
+ f"{c.claim_id}: {c.value!r}" + (f" ({c.source})" if c.source else "")
50
+ for c in found.claims
51
+ ],
52
+ ),
53
+ ("claim count", found.claim_count),
54
+ ("proposal", found.proposal_id),
55
+ ("decided by", found.decided_by),
56
+ ("decided at", found.decided_at),
57
+ ("created", found.created_at),
58
+ ("updated", found.updated_at),
59
+ ]
60
+ app.out.pairs(pairs)
61
+
62
+
63
+ def _show_decision(app: AppContext, decided: m.ReconciliationDecision) -> None:
64
+ app.out.pairs(
65
+ [
66
+ ("candidate", decided.candidate.id),
67
+ ("status", decided.status),
68
+ ("claim", decided.claim_id),
69
+ ("branch", decided.branch),
70
+ ("revision", decided.revision),
71
+ ("proposal", decided.proposal),
72
+ ("detail", decided.detail),
73
+ ]
74
+ )
75
+
76
+
77
+ def _checked(value: str | None, allowed: Any, option: str) -> str | None:
78
+ """`value` checked against the members of a Literal type."""
79
+ if value is not None and value not in get_args(allowed):
80
+ raise CliError(f"{option}: unknown {value!r}, use {', '.join(get_args(allowed))}")
81
+ return value
82
+
83
+
84
+ @reconcile_app.command("list")
85
+ def reconcile_list(
86
+ ctx: typer.Context,
87
+ candidate_type: Annotated[
88
+ str | None,
89
+ typer.Option("--type", help="duplicate, contradiction, superseded or stale."),
90
+ ] = None,
91
+ status: Annotated[
92
+ str | None,
93
+ typer.Option("--status", help="open, resolved, dismissed or obsolete (default: all)."),
94
+ ] = None,
95
+ kind: Annotated[str | None, typer.Option("--kind", help="Only this Kind (api name).")] = None,
96
+ attribute: Annotated[
97
+ str | None,
98
+ typer.Option("--attribute", help="Only this Attribute (api name; needs --kind)."),
99
+ ] = None,
100
+ source: Annotated[
101
+ str | None,
102
+ typer.Option("--source", help="Only candidates with a claim of this Source identity."),
103
+ ] = None,
104
+ mine: MineOpt = False,
105
+ domain: DomainOpt = None,
106
+ branch: BranchOpt = None,
107
+ limit: LimitOpt = None,
108
+ everything: AllOpt = False,
109
+ json_out: JsonOpt = False,
110
+ ) -> None:
111
+ """List the candidates of a branch, newest first."""
112
+ chosen_type = _checked(candidate_type, m.CandidateType, "--type")
113
+ chosen_status = _checked(status, m.CandidateStatus, "--status")
114
+ app = app_of(ctx, json_out)
115
+ items, more = app.take(
116
+ app.client().reconciliation.iterate(
117
+ app.domain(domain),
118
+ branch=app.branch(branch),
119
+ type=cast("m.CandidateType | None", chosen_type),
120
+ status=cast("m.CandidateStatus | None", chosen_status),
121
+ kind=kind,
122
+ attribute=attribute,
123
+ source=source,
124
+ assigned="me" if mine else None,
125
+ ),
126
+ limit,
127
+ everything,
128
+ )
129
+ app.out.emit(items, lambda: app.out.table([c.model_dump() for c in items], CANDIDATE_COLUMNS))
130
+ if more:
131
+ app.note_truncated(len(items))
132
+
133
+
134
+ @reconcile_app.command("show")
135
+ def reconcile_show(
136
+ ctx: typer.Context,
137
+ candidate_id: CandidateArg,
138
+ domain: DomainOpt = None,
139
+ json_out: JsonOpt = False,
140
+ ) -> None:
141
+ """Show a candidate with its claims and last decision."""
142
+ app = app_of(ctx, json_out)
143
+ found = app.client().reconciliation.get(app.domain(domain), candidate_id)
144
+ app.out.emit(found, lambda: _show_candidate(app, found))
145
+
146
+
147
+ @reconcile_app.command("resolve")
148
+ def reconcile_resolve(
149
+ ctx: typer.Context,
150
+ candidate_id: CandidateArg,
151
+ choice: Annotated[
152
+ str, typer.Option("--choice", help="claim, keep_current, custom, policy or merge.")
153
+ ],
154
+ reason: ReasonOpt,
155
+ claim: Annotated[
156
+ str | None, typer.Option("--claim", help="Claim id to take (choice `claim`).")
157
+ ] = None,
158
+ value: Annotated[
159
+ str | None,
160
+ typer.Option("--value", help="Value of a `custom` choice: inline JSON, @file or -."),
161
+ ] = None,
162
+ policy: Annotated[
163
+ str | None,
164
+ typer.Option(
165
+ "--policy",
166
+ help="authority, newest_wins, prefer_current or prefer_incoming (choice `policy`).",
167
+ ),
168
+ ] = None,
169
+ survivor: Annotated[
170
+ str | None, typer.Option("--survivor", help="Key of the Record that stays (`merge`).")
171
+ ] = None,
172
+ values: Annotated[
173
+ str | None,
174
+ typer.Option(
175
+ "--values",
176
+ help="Values to set on the survivor, by Attribute api name (`merge`): "
177
+ "a JSON object, inline, @file or -.",
178
+ ),
179
+ ] = None,
180
+ dry_run: Annotated[
181
+ bool, typer.Option("--dry-run", help="Report the outcome without writing.")
182
+ ] = False,
183
+ domain: DomainOpt = None,
184
+ json_out: JsonOpt = False,
185
+ ) -> None:
186
+ """Decide a candidate; a write that needs review opens a Proposal."""
187
+ chosen = _checked(choice, m.ReconcileChoice, "--choice")
188
+ chosen_policy = _checked(policy, m.ReconcilePolicy, "--policy")
189
+ parsed = None if values is None else json_argument(values)
190
+ if parsed is not None and not isinstance(parsed, dict):
191
+ raise CliError("--values is a JSON object")
192
+ app = app_of(ctx, json_out)
193
+ decided = app.client().reconciliation.resolve(
194
+ app.domain(domain),
195
+ candidate_id,
196
+ cast("m.ReconcileChoice", chosen),
197
+ reason=reason,
198
+ claim_id=claim,
199
+ value=None if value is None else json_argument(value),
200
+ policy=cast("m.ReconcilePolicy | None", chosen_policy),
201
+ survivor=survivor,
202
+ values=parsed,
203
+ dry_run=dry_run,
204
+ )
205
+ app.out.emit(decided, lambda: _show_decision(app, decided))
206
+
207
+
208
+ @reconcile_app.command("dismiss")
209
+ def reconcile_dismiss(
210
+ ctx: typer.Context,
211
+ candidate_id: CandidateArg,
212
+ reason: ReasonOpt,
213
+ domain: DomainOpt = None,
214
+ json_out: JsonOpt = False,
215
+ ) -> None:
216
+ """Dismiss a candidate: the difference is accepted as it is."""
217
+ app = app_of(ctx, json_out)
218
+ decided = app.client().reconciliation.dismiss(app.domain(domain), candidate_id, reason=reason)
219
+ app.out.emit(decided, lambda: _show_decision(app, decided))
220
+
221
+
222
+ @reconcile_app.command("reopen")
223
+ def reconcile_reopen(
224
+ ctx: typer.Context,
225
+ candidate_id: CandidateArg,
226
+ reason: ReasonOpt,
227
+ domain: DomainOpt = None,
228
+ json_out: JsonOpt = False,
229
+ ) -> None:
230
+ """Reopen a dismissed candidate."""
231
+ app = app_of(ctx, json_out)
232
+ decided = app.client().reconciliation.reopen(app.domain(domain), candidate_id, reason=reason)
233
+ app.out.emit(decided, lambda: _show_decision(app, decided))
lorebase_cli/main.py CHANGED
@@ -20,6 +20,7 @@ from lorebase_cli.commands import (
20
20
  domains,
21
21
  history,
22
22
  proposals,
23
+ reconcile,
23
24
  schema,
24
25
  sources,
25
26
  trace,
@@ -139,11 +140,13 @@ app.add_typer(admin.admin_app, name="admin")
139
140
  app.add_typer(audit.audit_app, name="audit")
140
141
  app.add_typer(trace.trace_app, name="trace")
141
142
  app.add_typer(proposals.proposal_app, name="proposal")
143
+ app.add_typer(reconcile.reconcile_app, name="reconcile")
142
144
  app.add_typer(sources.sources_app, name="sources")
143
145
  app.add_typer(documents.documents_app, name="documents")
144
146
  app.command("whoami")(domains.whoami)
145
147
  app.command("query")(data.query_command)
146
148
  app.command("search")(data.search_command)
149
+ app.command("resolve")(data.resolve_command)
147
150
  app.command("log")(history.log_command)
148
151
  app.command("diff")(history.diff_command)
149
152
  app.command("context")(agents.context_command)
lorebase_cli/options.py CHANGED
@@ -54,3 +54,6 @@ LimitOpt = Annotated[
54
54
  typer.Option("--limit", "-n", min=1, help="Most items to print (default: list_limit setting)."),
55
55
  ]
56
56
  AllOpt = Annotated[bool, typer.Option("--all", help="Print every item, however many.")]
57
+ MineOpt = Annotated[
58
+ bool, typer.Option("--mine", help="Only items assigned to you through a steward role.")
59
+ ]
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: lorebase-cli
3
- Version: 0.2.3
3
+ Version: 0.3.0
4
4
  Summary: Lorebase CLI
5
5
  Requires-Python: >=3.12
6
- Requires-Dist: lorebase-client<0.3,>=0.2
6
+ Requires-Dist: lorebase-client<0.4,>=0.3
7
7
  Requires-Dist: typer>=0.27
8
8
  Provides-Extra: linkml
9
9
  Requires-Dist: pyyaml>=6.0; extra == 'linkml'
@@ -1,4 +1,4 @@
1
- lorebase_cli/__init__.py,sha256=WaRCGYJ-OeKg1iQYpvl-vnJ48G7NPGnsdDwWeaBfl6o,85
1
+ lorebase_cli/__init__.py,sha256=BKq-kdMuKMMA9_NewylTghGDRfmJZEsJuH5EOcupPG0,85
2
2
  lorebase_cli/__main__.py,sha256=mrbDZBnLym1bpDIpgTL1L1_iytsbMRrnBrIJZsRuoTc,76
3
3
  lorebase_cli/batch_import.py,sha256=HOaK0IBF5gKKnnurc34BcBh2kb4eHr3LBzgl1ZiB2wY,12650
4
4
  lorebase_cli/coercion.py,sha256=m4zHFHvGpUehbGaLksBau5gnc7DiQ9Dj7UaPGH2ugTw,6413
@@ -6,25 +6,26 @@ lorebase_cli/context.py,sha256=Aqu3eL9Nvq77Hau9O8EHsjAXJ3yY4NIesC78POLct4M,3381
6
6
  lorebase_cli/failures.py,sha256=UShNuEh6aQZ6IyMQmq12zDkx6OZcpg1WtRS69iPCtPU,6791
7
7
  lorebase_cli/importing.py,sha256=ScU7RreZ_awd8o5foPGQr1270mSu8r1JIJyEk6n72Hw,9261
8
8
  lorebase_cli/linkml.py,sha256=jhbRvDg7rGMCtlld76L-gl-DqF5FY0w2pmubz2IvzgI,69622
9
- lorebase_cli/main.py,sha256=2Voa3pNGpXVPE3J2VZ0BidqJhvW7GaQwTfP0Ld8Yn5s,5533
10
- lorebase_cli/options.py,sha256=Z5GCA7lOFIgmM8RDsTqso1U6h136Heu5zQCmPHo3ffc,1698
9
+ lorebase_cli/main.py,sha256=Zi2dcWh6GrJEGeLqii-fnLAkZAKxcKKEbuII707CRqg,5650
10
+ lorebase_cli/options.py,sha256=bcDV3CYPbJPS_RgaiX6RnSgulkMaGIGMYn1ea_k_Wh4,1813
11
11
  lorebase_cli/output.py,sha256=Lr0n03SKomt3kOTu8-NLgsewvHBp-2UDJZ2_8zDv2p4,5610
12
12
  lorebase_cli/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
13
  lorebase_cli/settings.py,sha256=wcD1WHMy2pVqDXKJduux_i2RtwW9rZYMJyE9MAC9Fq8,6159
14
14
  lorebase_cli/sources.py,sha256=Am6alJN_SL6j06PeCKp6IYfboRIFyieA6cXhWPBWD8A,4024
15
15
  lorebase_cli/commands/__init__.py,sha256=h3N31KSCa8vuU6OoxKib8pHICh1xk7OAqmkxN1k-fb4,42
16
16
  lorebase_cli/commands/admin.py,sha256=Dv4W-d0SBzEfCkYB0VMk257RHP2h3HZJYIWaOc3UYLU,9300
17
- lorebase_cli/commands/agents.py,sha256=8Himq5Rf72X_aIpcjY1oib9mT-5ljfMB6jRBWIk25Vk,8146
17
+ lorebase_cli/commands/agents.py,sha256=DiJk3d8vI3hFghH-6jHfhesNtMZNqqU4l7lJWRpW-4c,8632
18
18
  lorebase_cli/commands/audit.py,sha256=Su7jPzqSzQbKd7YkRSISHQVLupPD1ZRR1fLCq_cc8dA,1601
19
- lorebase_cli/commands/data.py,sha256=zVgmKgY2b-6ZLee4IVLZLwnvuy-BJgiOdFK1_xT3fQo,25010
20
- lorebase_cli/commands/documents.py,sha256=krcGi-UdWeMh4HIrLeoItYXybJgQVTyDGtuK_DpOdeY,5288
19
+ lorebase_cli/commands/data.py,sha256=N7uvbbw5vEtnY4ZGl6eEekzKE49hDc6bKEejUn6hluM,28994
20
+ lorebase_cli/commands/documents.py,sha256=Lfd4SfKyIvp76EJLFUobpylQfoAT-gw9sJl0XZUSfb4,9101
21
21
  lorebase_cli/commands/domains.py,sha256=vX73JcSz-th-bjGsZo-P17yoB79bwPdkOO3zNq8k020,14601
22
22
  lorebase_cli/commands/history.py,sha256=wkU02Rb30WQczYWr1rQU1VAdse6M1iQwAMd298cQ6Ss,7079
23
- lorebase_cli/commands/proposals.py,sha256=1pFVDvn4OTBRT5RHXr0B6VPKC-c6I7ivJSrsy0wDAZo,16474
23
+ lorebase_cli/commands/proposals.py,sha256=Y-cKsklox55fdKeZ4EpWZ1SNst7xVbfVNVJxqZfgmW4,16634
24
+ lorebase_cli/commands/reconcile.py,sha256=lf308KVp3zJCCuZOANwyPPtXvNB2N1AJFZmQSiz_OuE,8068
24
25
  lorebase_cli/commands/schema.py,sha256=ymkUGMfE3LfFVXFyN5L_cJIlm3kEL3D5Zj1Lp-3ohSE,17364
25
26
  lorebase_cli/commands/sources.py,sha256=YZNT20M28_xdWBQQ_TKmMScZkb0A-BaXJr4Dv4dXwoo,2079
26
27
  lorebase_cli/commands/trace.py,sha256=0aaYL3f3K29NmRcSzaV2LlugmoM7yMhE87AoMRb_YfA,4597
27
- lorebase_cli-0.2.3.dist-info/METADATA,sha256=Sp1HeHdQZm96qSJWmdlfUFnGWlneD6A9qeiqlYMnA2o,310
28
- lorebase_cli-0.2.3.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
29
- lorebase_cli-0.2.3.dist-info/entry_points.txt,sha256=RZqd8q81-mrfZq6cDj3CitYvTmLhADDCTkw9rvSMJ30,52
30
- lorebase_cli-0.2.3.dist-info/RECORD,,
28
+ lorebase_cli-0.3.0.dist-info/METADATA,sha256=0VaW5N1iXLstuA6LiBPbq4-ldL7Sspmv5Ld1LbUpEvI,310
29
+ lorebase_cli-0.3.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
30
+ lorebase_cli-0.3.0.dist-info/entry_points.txt,sha256=RZqd8q81-mrfZq6cDj3CitYvTmLhADDCTkw9rvSMJ30,52
31
+ lorebase_cli-0.3.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.32.4
2
+ Generator: hatchling 1.33.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any