Some checks failed
CI / lint (push) Successful in 45s
CI / notebooks-smoke (push) Successful in 1m40s
CI / lint (pull_request) Successful in 28s
CI / test (push) Failing after 3m39s
CI / notebooks-smoke (pull_request) Successful in 1m35s
CI / test (pull_request) Failing after 3m22s
Infra CI / notebooks (push) Successful in 1m0s
Infra CI / zotero (push) Successful in 17s
Infra CI / docs (push) Successful in 20s
Infra CI / api (push) Successful in 1m11s
Infra CI / bcda (push) Successful in 1m31s
Infra CI / mc (push) Successful in 16s
Infra CI / llm (push) Successful in 52s
Infra CI / zotero (pull_request) Successful in 23s
Infra CI / docs (pull_request) Successful in 16s
Infra CI / notebooks (pull_request) Successful in 58s
Infra CI / api (pull_request) Successful in 17s
Infra CI / bcda (pull_request) Successful in 16s
Infra CI / llm (pull_request) Successful in 20s
Infra CI / mc (pull_request) Successful in 17s
Local files land in the DuckDB mirror with the source headers kept: group labels, stacked IRS names, headerless code maps, and the HCPCS action key. The read-only explorer and the review page are wired at lead.fhirworx.io. OEWS now includes 2025.
155 lines
4.7 KiB
Python
155 lines
4.7 KiB
Python
"""Append-only gold labels. The latest row for a key is the current one."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sqlite3
|
|
from dataclasses import dataclass
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
|
|
from lead.review.codes import DECISIONS, PASSES, PROJECTS, STAGES, reason_codes
|
|
|
|
_DDL = """
|
|
CREATE TABLE IF NOT EXISTS label (
|
|
id INTEGER PRIMARY KEY,
|
|
zotero_key TEXT NOT NULL,
|
|
project TEXT NOT NULL,
|
|
stage TEXT NOT NULL,
|
|
pass INTEGER NOT NULL,
|
|
decision TEXT NOT NULL,
|
|
reason TEXT NOT NULL,
|
|
rationale TEXT NOT NULL,
|
|
actor TEXT NOT NULL,
|
|
created_at TEXT NOT NULL
|
|
);
|
|
"""
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Label:
|
|
zotero_key: str
|
|
project: str
|
|
stage: str
|
|
pass_n: int
|
|
decision: str
|
|
reason: str
|
|
rationale: str
|
|
actor: str
|
|
created_at: str
|
|
|
|
|
|
class Store:
|
|
"""One sqlite file of human labels. Opens per call and commits."""
|
|
|
|
def __init__(self, path: str | Path) -> None:
|
|
self.path = Path(path)
|
|
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
con = self._open()
|
|
try:
|
|
con.execute(_DDL)
|
|
con.commit()
|
|
finally:
|
|
con.close()
|
|
|
|
def _open(self) -> sqlite3.Connection:
|
|
con = sqlite3.connect(self.path)
|
|
con.row_factory = sqlite3.Row
|
|
return con
|
|
|
|
def record(
|
|
self,
|
|
*,
|
|
zotero_key: str,
|
|
project: str,
|
|
stage: str,
|
|
pass_n: int,
|
|
decision: str,
|
|
reason: str = "",
|
|
rationale: str,
|
|
actor: str,
|
|
) -> None:
|
|
"""Append one label. A second label for the same key supersedes it."""
|
|
key = zotero_key.strip()
|
|
if not key:
|
|
raise ValueError("zotero key is empty")
|
|
if project not in PROJECTS:
|
|
raise ValueError(project)
|
|
if stage not in STAGES:
|
|
raise ValueError(stage)
|
|
if pass_n not in PASSES:
|
|
raise ValueError(pass_n)
|
|
if decision not in DECISIONS:
|
|
raise ValueError(decision)
|
|
why = reason.strip()
|
|
if decision == "exclude":
|
|
if why not in reason_codes(project):
|
|
raise ValueError(f"{why or '(blank)'} is not in the {project} codebook")
|
|
elif why:
|
|
raise ValueError("a reason is recorded only on exclude")
|
|
text = rationale.strip()
|
|
if not text:
|
|
raise ValueError("rationale is empty")
|
|
who = actor.strip() or "reviewer"
|
|
stamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
con = self._open()
|
|
try:
|
|
con.execute(
|
|
"""INSERT INTO label (
|
|
zotero_key, project, stage, pass, decision, reason,
|
|
rationale, actor, created_at
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
(key, project, stage, pass_n, decision, why, text, who, stamp),
|
|
)
|
|
con.commit()
|
|
finally:
|
|
con.close()
|
|
|
|
def latest(self, project: str, stage: str, pass_n: int) -> dict[str, str]:
|
|
"""Current decision per zotero key. A later row replaces an earlier one."""
|
|
con = self._open()
|
|
try:
|
|
rows = con.execute(
|
|
"""SELECT zotero_key, decision FROM label
|
|
WHERE project = ? AND stage = ? AND pass = ?
|
|
ORDER BY id""",
|
|
(project, stage, pass_n),
|
|
).fetchall()
|
|
finally:
|
|
con.close()
|
|
out: dict[str, str] = {}
|
|
for row in rows:
|
|
out[row["zotero_key"]] = row["decision"]
|
|
return out
|
|
|
|
def count(self, project: str, stage: str, pass_n: int) -> int:
|
|
return len(self.latest(project, stage, pass_n))
|
|
|
|
def history(self, project: str, stage: str, pass_n: int) -> tuple[Label, ...]:
|
|
"""Every row, oldest first. The page shows the tail."""
|
|
con = self._open()
|
|
try:
|
|
rows = con.execute(
|
|
"""SELECT zotero_key, project, stage, pass, decision, reason,
|
|
rationale, actor, created_at
|
|
FROM label
|
|
WHERE project = ? AND stage = ? AND pass = ?
|
|
ORDER BY id""",
|
|
(project, stage, pass_n),
|
|
).fetchall()
|
|
finally:
|
|
con.close()
|
|
return tuple(
|
|
Label(
|
|
zotero_key=row["zotero_key"],
|
|
project=row["project"],
|
|
stage=row["stage"],
|
|
pass_n=row["pass"],
|
|
decision=row["decision"],
|
|
reason=row["reason"],
|
|
rationale=row["rationale"],
|
|
actor=row["actor"],
|
|
created_at=row["created_at"],
|
|
)
|
|
for row in rows
|
|
)
|