dbtips-curate 0.2.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.
Files changed (35) hide show
  1. dbtips_curate/__init__.py +1 -0
  2. dbtips_curate/cli/__init__.py +0 -0
  3. dbtips_curate/cli/_common.py +67 -0
  4. dbtips_curate/cli/_output.py +51 -0
  5. dbtips_curate/cli/_widget.py +207 -0
  6. dbtips_curate/cli/dev.py +358 -0
  7. dbtips_curate/cli/entity.py +723 -0
  8. dbtips_curate/cli/history.py +213 -0
  9. dbtips_curate/cli/main.py +70 -0
  10. dbtips_curate/cli/op.py +399 -0
  11. dbtips_curate/core/__init__.py +0 -0
  12. dbtips_curate/core/config.py +181 -0
  13. dbtips_curate/core/fs.py +154 -0
  14. dbtips_curate/core/git.py +80 -0
  15. dbtips_curate/core/git_history.py +183 -0
  16. dbtips_curate/core/merge.py +314 -0
  17. dbtips_curate/core/ops.py +78 -0
  18. dbtips_curate/core/schema.py +161 -0
  19. dbtips_curate/core/worklist.py +166 -0
  20. dbtips_curate/mcp_server.py +114 -0
  21. dbtips_curate/skill/_deferred/targetability-design-note.md +170 -0
  22. dbtips_curate/skill/dbtips-curate/SKILL.md +400 -0
  23. dbtips_curate/skill/dbtips-curate-antibodies/SKILL.md +219 -0
  24. dbtips_curate/skill/dbtips-curate-assayability/SKILL.md +175 -0
  25. dbtips_curate/skill/dbtips-curate-gene-essentiality/SKILL.md +107 -0
  26. dbtips_curate/skill/dbtips-curate-ontology/SKILL.md +129 -0
  27. dbtips_curate/skill/dbtips-curate-orthologs/SKILL.md +123 -0
  28. dbtips_curate/skill/dbtips-curate-supplementary-materials/SKILL.md +147 -0
  29. dbtips_curate/skill/dbtips-curate-target-literature/SKILL.md +286 -0
  30. dbtips_curate/skill/dbtips-curate-target-pathway/SKILL.md +166 -0
  31. dbtips_curate/skill/dbtips-curate-target-pipeline/SKILL.md +291 -0
  32. dbtips_curate-0.2.0.dist-info/METADATA +278 -0
  33. dbtips_curate-0.2.0.dist-info/RECORD +35 -0
  34. dbtips_curate-0.2.0.dist-info/WHEEL +4 -0
  35. dbtips_curate-0.2.0.dist-info/entry_points.txt +3 -0
@@ -0,0 +1 @@
1
+ __version__ = "0.2.0"
File without changes
@@ -0,0 +1,67 @@
1
+ """Helpers shared by command handlers — config + entity resolution."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ import typer
9
+
10
+ from dbtips_curate.cli._output import emit_error
11
+ from dbtips_curate.core.config import Config, load_config
12
+ from dbtips_curate.core.fs import EntityPaths
13
+ from dbtips_curate.core.schema import Section
14
+
15
+ # Sentinel: when a section-aware command's --type option receives this,
16
+ # resolve() derives entity_type from the section's entity_scope. Explicit
17
+ # --type values on the CLI still win — this only fires when the user doesn't
18
+ # pass --type. Used by every command that also takes --section (delete, add,
19
+ # patch, entity status/track/snapshot/records/diff/ingest/compare/finalize).
20
+ AUTO_ENTITY_TYPE = "__auto__"
21
+
22
+ # Back-compat default for commands that DON'T take --section (list, undo).
23
+ # They can't consult the section registry to auto-derive the type.
24
+ DEFAULT_ENTITY_TYPE = "target_disease"
25
+ DEFAULT_SECTION = "/evidence/target-literature/"
26
+
27
+
28
+ def get_config(ctx: typer.Context) -> Config:
29
+ config = ctx.obj.get("config") if ctx.obj else None
30
+ if config is None:
31
+ try:
32
+ config = load_config()
33
+ except FileNotFoundError as e:
34
+ emit_error("NO_CONFIG", str(e), json_mode=ctx.obj.get("json", False),
35
+ hint="Run from inside a directory under your platform repo "
36
+ "with a curation/.dbtips-curate.toml")
37
+ raise typer.Exit(code=2)
38
+ ctx.ensure_object(dict)
39
+ ctx.obj["config"] = config
40
+ return config
41
+
42
+
43
+ def resolve(ctx: typer.Context, entity_type: str, entity_id: str, endpoint: str) -> tuple[EntityPaths, Section]:
44
+ config = get_config(ctx)
45
+ try:
46
+ section = config.registry.get(endpoint)
47
+ except KeyError:
48
+ emit_error("UNKNOWN_SECTION", f"No schema registered for section {endpoint!r}",
49
+ json_mode=ctx.obj.get("json", False),
50
+ hint="Add an entry under [sections.\"%s\"] in .dbtips-curate.toml" % endpoint)
51
+ raise typer.Exit(code=2)
52
+ # Derive --type from the section's entity_scope when the user didn't pass
53
+ # --type explicitly. Explicit --type still wins (needed for tests and for
54
+ # sections that are legitimately curated in both scopes).
55
+ if entity_type == AUTO_ENTITY_TYPE:
56
+ entity_type = section.entity_scope
57
+ paths = EntityPaths(config, entity_type, entity_id)
58
+ return paths, section
59
+
60
+
61
+ def require_served_file(ctx: typer.Context, paths: EntityPaths) -> None:
62
+ if not paths.served_path.exists():
63
+ emit_error("NO_SERVED_FILE",
64
+ f"Served file not found: {paths.served_path}",
65
+ json_mode=ctx.obj.get("json", False),
66
+ hint="Has the pipeline generated this entity yet?")
67
+ raise typer.Exit(code=1)
@@ -0,0 +1,51 @@
1
+ """Shared output helpers — keeps human/JSON formatting out of command handlers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import sys
8
+ from typing import Any
9
+
10
+ from rich.console import Console
11
+
12
+
13
+ def _stdout() -> Console:
14
+ return Console(file=sys.stdout, force_terminal=False, no_color=_no_color())
15
+
16
+
17
+ def _stderr() -> Console:
18
+ return Console(file=sys.stderr, force_terminal=False, no_color=_no_color())
19
+
20
+
21
+ def _no_color() -> bool:
22
+ return bool(os.environ.get("NO_COLOR")) or not sys.stdout.isatty()
23
+
24
+
25
+ def emit_ok(data: Any, *, json_mode: bool, human: str | None = None) -> None:
26
+ """Result -> stdout. JSON or human."""
27
+ if json_mode:
28
+ print(json.dumps({"ok": True, "data": data}, default=str))
29
+ else:
30
+ if human is not None:
31
+ print(human)
32
+ else:
33
+ print(json.dumps(data, indent=2, default=str))
34
+
35
+
36
+ def emit_error(code: str, message: str, *, json_mode: bool, hint: str | None = None) -> None:
37
+ """Errors -> stderr; do not pollute stdout."""
38
+ if json_mode:
39
+ payload = {"ok": False, "error": {"code": code, "message": message}}
40
+ if hint:
41
+ payload["error"]["hint"] = hint
42
+ print(json.dumps(payload), file=sys.stderr)
43
+ else:
44
+ print(f"Error: {message}", file=sys.stderr)
45
+ if hint:
46
+ print(f" {hint}", file=sys.stderr)
47
+
48
+
49
+ def emit_progress(message: str) -> None:
50
+ """Progress -> stderr in both modes."""
51
+ print(message, file=sys.stderr)
@@ -0,0 +1,207 @@
1
+ """Self-contained HTML artifact generator for the diff preview.
2
+
3
+ The CLI reads the cache directly, so it can inline *full* abstracts for every
4
+ record with no token budget — unlike a chat agent that has to transcribe the
5
+ widget code through its own context. `render_diff_widget` returns a widget
6
+ fragment (no <html>/<head>/<body>) suitable for inline artifact rendering in
7
+ the Claude Code desktop app: CSS variables for theming, a shared <dialog>
8
+ abstract modal, pagination, checkbox selection, and `sendPrompt` action
9
+ buttons that the dbtips-curate skill's batch-execution rule picks up.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ from typing import Any
16
+
17
+
18
+ def _rows_from_queue(entries: list[dict], identity_key: str) -> list[dict]:
19
+ """Flatten a diff queue into rows the widget understands."""
20
+ rows = []
21
+ for e in entries:
22
+ preview = e.get("_preview", {}) or {}
23
+ identity = {k: v for k, v in e.items() if k != "_preview"}
24
+ pmid = identity.get(identity_key)
25
+ if pmid is None and identity:
26
+ pmid = next(iter(identity.values()))
27
+ # Display title falls back across sections: literature Title, reagent
28
+ # product_name, pathway figtitle, assay Description. The modal body uses
29
+ # the richest text available (Abstract / description).
30
+ title = (preview.get("Title") or preview.get("product_name")
31
+ or preview.get("figtitle") or preview.get("Description") or "")
32
+ provider = preview.get("Provider")
33
+ if provider and not preview.get("Title"):
34
+ title = f"{title} — {provider}"
35
+ rows.append({
36
+ "pmid": str(pmid) if pmid is not None else "",
37
+ "year": preview.get("Year"),
38
+ "cite": preview.get("citedby"),
39
+ "title": title,
40
+ "abstract": preview.get("Abstract") or preview.get("description") or "",
41
+ })
42
+ return rows
43
+
44
+
45
+ def render_diff_widget(payload: dict[str, Any], *, identity_key: str = "PMID") -> str:
46
+ """Build the inline diff artifact for `entity diff --emit-widget`."""
47
+ entity_id = payload["entity"]
48
+ queue = payload.get("queue", {})
49
+ totals = payload.get("totals", {})
50
+
51
+ data = {
52
+ "entity": entity_id,
53
+ "base_raw": payload.get("base_raw"),
54
+ "section": payload.get("section"),
55
+ "totals": totals,
56
+ "new": _rows_from_queue(queue.get("new", []), identity_key),
57
+ "removed": _rows_from_queue(queue.get("removed_by_pipeline", []), identity_key),
58
+ "resurrected": _rows_from_queue(queue.get("resurrected", []), identity_key),
59
+ "conflicts": queue.get("conflicts", []),
60
+ }
61
+ data_json = json.dumps(data, ensure_ascii=False, default=str)
62
+ # Close any stray </script> inside abstracts so the embed can't break out.
63
+ data_json = data_json.replace("</", "<\\/")
64
+ return _TEMPLATE.replace("/*__DATA__*/", data_json)
65
+
66
+
67
+ _TEMPLATE = r"""<div style="padding:0.5rem 0;">
68
+ <h2 class="sr-only">Diff preview artifact: new, removed-by-pipeline, resurrected, and conflict records with full-abstract modal, pagination, and batch actions.</h2>
69
+ <div id="summary" style="font-size:13px; color:var(--color-text-secondary); margin-bottom:14px;"></div>
70
+
71
+ <div style="display:flex; gap:6px; margin-bottom:12px; flex-wrap:wrap;">
72
+ <button class="tab" data-tab="new">New from pipeline</button>
73
+ <button class="tab" data-tab="removed">Removed by pipeline</button>
74
+ <button class="tab" data-tab="resurrected">Resurrected</button>
75
+ <button class="tab" data-tab="conflicts">Conflicts</button>
76
+ </div>
77
+
78
+ <div id="toolbar" style="display:flex; gap:8px; flex-wrap:wrap; align-items:center; margin-bottom:10px;">
79
+ <span id="secnote" style="font-size:12px; color:var(--color-text-tertiary); flex:1;"></span>
80
+ <button id="checkall" style="font-size:13px;">Select all on page</button>
81
+ <button id="clearall" style="font-size:13px;">Clear</button>
82
+ </div>
83
+
84
+ <table style="width:100%; border-collapse:collapse; font-size:13px; table-layout:fixed;">
85
+ <colgroup><col style="width:30px"><col style="width:26px"><col style="width:78px"><col style="width:52px"><col style="width:42px"><col><col style="width:54px"></colgroup>
86
+ <thead><tr id="thead" style="text-align:left; color:var(--color-text-secondary); border-bottom:0.5px solid var(--color-border-secondary);"></tr></thead>
87
+ <tbody id="rows"></tbody>
88
+ </table>
89
+
90
+ <div style="display:flex; gap:8px; align-items:center; justify-content:center; margin-top:14px;">
91
+ <button id="prev" style="font-size:13px;"><i class="ti ti-chevron-left" aria-hidden="true"></i> Prev</button>
92
+ <span id="pageinfo" style="font-size:13px; color:var(--color-text-secondary); min-width:130px; text-align:center;"></span>
93
+ <button id="next" style="font-size:13px;">Next <i class="ti ti-chevron-right" aria-hidden="true"></i></button>
94
+ </div>
95
+
96
+ <div style="border-top:0.5px solid var(--color-border-tertiary); margin-top:16px; padding-top:14px; display:flex; align-items:center; gap:12px;">
97
+ <span id="selsum" style="font-size:13px; color:var(--color-text-secondary); flex:1;"></span>
98
+ <button id="apply" style="font-size:14px; padding:7px 18px; color:var(--color-text-danger); border-color:var(--color-border-danger);"><i class="ti ti-trash" aria-hidden="true"></i> Apply <span id="applyverb">deletions</span> &#8599;</button>
99
+ </div>
100
+
101
+ <dialog id="amodal" style="max-width:620px; padding:18px; border:none; border-radius:var(--border-radius-lg); background:var(--color-background-primary); color:var(--color-text-primary);">
102
+ <header style="display:flex; align-items:center; gap:8px; margin-bottom:10px;">
103
+ <strong id="m-pmid" style="font-family:var(--font-mono); font-size:13px;"></strong>
104
+ <span style="flex:1"></span>
105
+ <button onclick="document.getElementById('amodal').close()" style="font-size:13px;">Close</button>
106
+ </header>
107
+ <p id="m-title" style="font-size:14px; font-weight:500; margin:0 0 10px; line-height:1.4;"></p>
108
+ <p id="m-abs" style="white-space:pre-wrap; line-height:1.55; font-size:13px; color:var(--color-text-secondary); max-height:55vh; overflow:auto; margin:0;"></p>
109
+ </dialog>
110
+ </div>
111
+ <style>
112
+ .tab{font-size:13px;}
113
+ .tab[aria-selected="true"]{background:var(--color-background-secondary); border-color:var(--color-border-primary);}
114
+ </style>
115
+ <script>
116
+ const DB=/*__DATA__*/;
117
+ const PS=20;
118
+ const VERB={new:"deletions", removed:"drops"};
119
+ const ACTION={
120
+ new:(ids)=>`Delete these ${ids.length} new-from-pipeline records for ${DB.entity} (run op delete on each, no further confirmation needed): ${ids.join(", ")}`,
121
+ removed:(ids)=>`Delete these ${ids.length} removed-by-pipeline records for ${DB.entity} (run op delete on each, no further confirmation needed): ${ids.join(", ")}`
122
+ };
123
+ let tab="new", page=0;
124
+ const sel={new:new Set(), removed:new Set()};
125
+ function esc(s){return String(s==null?"":s).replace(/&/g,"&amp;").replace(/</g,"&lt;");}
126
+ function fmt(v){return v==null?"":v;}
127
+ function curRows(){
128
+ if(tab==="new")return DB.new;
129
+ if(tab==="removed")return DB.removed;
130
+ if(tab==="resurrected")return DB.resurrected;
131
+ return DB.conflicts;
132
+ }
133
+ function summary(){
134
+ const t=DB.totals||{};
135
+ document.getElementById("summary").innerHTML=
136
+ `<strong>${esc(DB.entity)}</strong> &middot; base=${esc(DB.base_raw||"—")} vs current served &middot; `+
137
+ `merged ${fmt(t.merged)} &middot; new ${fmt(t.new_from_pipeline)} &middot; removed ${fmt(t.removed_by_pipeline)} &middot; `+
138
+ `resurrected ${fmt(t.resurrected_tombstones)} &middot; conflicts ${fmt(t.conflicts)}`;
139
+ const selectable=(tab==="new"||tab==="removed");
140
+ document.getElementById("apply").style.display=selectable?"":"none";
141
+ document.getElementById("checkall").style.display=selectable?"":"none";
142
+ document.getElementById("clearall").style.display=selectable?"":"none";
143
+ document.getElementById("applyverb").textContent=VERB[tab]||"";
144
+ const n=selectable?sel[tab].size:0;
145
+ document.getElementById("selsum").textContent=n?`${n} selected`:"No records selected";
146
+ document.getElementById("apply").disabled=n===0;
147
+ const notes={new:"Records the refresh newly emitted — triage and delete noise.",
148
+ removed:"In the previous file but dropped by the refresh — retained on ingest unless you drop them.",
149
+ resurrected:"You deleted these before; the pipeline re-emitted them. They stay deleted on ingest.",
150
+ conflicts:"Field-level disagreements between your patch and the pipeline."};
151
+ document.getElementById("secnote").textContent=notes[tab]||"";
152
+ }
153
+ function openModal(r){
154
+ document.getElementById("m-pmid").textContent="PMID "+r.pmid+(r.year?" · "+r.year:"")+(r.cite!=null?" · cited "+r.cite:"");
155
+ document.getElementById("m-title").textContent=r.title||"";
156
+ document.getElementById("m-abs").textContent=r.abstract||"(no abstract)";
157
+ document.getElementById("amodal").showModal();
158
+ }
159
+ function render(){
160
+ document.querySelectorAll(".tab").forEach(b=>b.setAttribute("aria-selected", b.dataset.tab===tab?"true":"false"));
161
+ const rows=curRows();
162
+ const TP=Math.max(1, Math.ceil(rows.length/PS));
163
+ if(page>=TP)page=0;
164
+ const thead=document.getElementById("thead");
165
+ const tb=document.getElementById("rows"); tb.innerHTML="";
166
+ if(tab==="conflicts"){
167
+ thead.innerHTML=`<th style="padding:8px 4px;">key</th><th>field</th><th>ours</th><th>theirs</th><th>base</th>`;
168
+ rows.slice(page*PS,page*PS+PS).forEach(c=>{
169
+ const tr=document.createElement("tr"); tr.style.borderTop="0.5px solid var(--color-border-tertiary)";
170
+ tr.innerHTML=`<td style="padding:8px 4px; font-family:var(--font-mono); font-size:12px;">${esc(JSON.stringify(c.key))}</td>`+
171
+ `<td>${esc(c.field)}</td><td>${esc(c.ours)}</td><td>${esc(c.theirs)}</td><td>${esc(c.base)}</td>`;
172
+ tb.appendChild(tr);
173
+ });
174
+ } else {
175
+ const selectable=(tab==="new"||tab==="removed");
176
+ thead.innerHTML=`<th style="padding:8px 4px;">${selectable?'<i class="ti ti-trash" aria-hidden="true"></i>':''}</th><th>#</th><th>PMID</th><th>Year</th><th>Cite</th><th>Title</th><th>Abs</th>`;
177
+ const start=page*PS, end=Math.min(start+PS, rows.length);
178
+ for(let i=start;i<end;i++){
179
+ const r=rows[i];
180
+ const tr=document.createElement("tr"); tr.style.borderTop="0.5px solid var(--color-border-tertiary)";
181
+ const box=selectable?`<input type="checkbox" data-pmid="${esc(r.pmid)}" ${sel[tab].has(r.pmid)?"checked":""} style="width:16px;height:16px;cursor:pointer;">`:"";
182
+ tr.innerHTML=`<td style="padding:9px 4px; vertical-align:top;">${box}</td>`+
183
+ `<td style="vertical-align:top; color:var(--color-text-tertiary);">${i+1}</td>`+
184
+ `<td style="vertical-align:top; font-family:var(--font-mono); font-size:12px;">${esc(r.pmid)}</td>`+
185
+ `<td style="vertical-align:top;">${esc(fmt(r.year))}</td>`+
186
+ `<td style="vertical-align:top;">${esc(fmt(r.cite))}</td>`+
187
+ `<td style="vertical-align:top; white-space:normal; word-break:break-word; line-height:1.45;">${esc(r.title)}</td>`+
188
+ `<td style="vertical-align:top;"><button data-i="${i}" class="absbtn" style="font-size:11px; padding:3px 8px;"><i class="ti ti-file-text" aria-hidden="true"></i></button></td>`;
189
+ tb.appendChild(tr);
190
+ }
191
+ tb.querySelectorAll("input[type=checkbox]").forEach(cb=>cb.onchange=()=>{cb.checked?sel[tab].add(cb.dataset.pmid):sel[tab].delete(cb.dataset.pmid); summary();});
192
+ tb.querySelectorAll("button.absbtn").forEach(b=>b.onclick=()=>openModal(rows[+b.dataset.i]));
193
+ }
194
+ document.getElementById("pageinfo").textContent=`Page ${page+1}/${TP} · ${rows.length} rows`;
195
+ document.getElementById("prev").disabled=page===0;
196
+ document.getElementById("next").disabled=page>=TP-1;
197
+ summary();
198
+ }
199
+ document.querySelectorAll(".tab").forEach(b=>b.onclick=()=>{tab=b.dataset.tab; page=0; render();});
200
+ document.getElementById("prev").onclick=()=>{if(page>0){page--;render();}};
201
+ document.getElementById("next").onclick=()=>{page++;render();};
202
+ document.getElementById("checkall").onclick=()=>{const rows=curRows(); rows.slice(page*PS,page*PS+PS).forEach(r=>sel[tab].add(r.pmid)); render();};
203
+ document.getElementById("clearall").onclick=()=>{sel[tab].clear(); render();};
204
+ document.getElementById("apply").onclick=()=>{const ids=[...sel[tab]]; if(!ids.length)return; sendPrompt(ACTION[tab](ids));};
205
+ document.getElementById("amodal").addEventListener("click",e=>{if(e.target.id==="amodal")e.target.close();});
206
+ render();
207
+ </script>"""