owenrusk.dev

casebench

test cases straight onto the board.

git clone https://owenrusk.dev/casebench.git

commit 71e65c89933d25d238a483807b7a3de0e6f55f13
author Owen Rusk <owen@papermothgames.com>
date   2025-03-10 14:12:37 -0500
first cut: put a case folder on a board

writes the case straight into the board tables. the runtime only sees what passes through it, so it never sees these.
.gitignore+3-0
README.md+7-0
casebench/__init__.py+0-0
casebench/__main__.py+5-0
casebench/casefile.py+34-0
casebench/cli.py+29-0
casebench/db.py+26-0
pyproject.toml+12-0
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..5e4567d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+__pycache__/
+*.egg-info/
+.venv/
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..29eb4dc
--- /dev/null
+++ b/README.md
@@ -0,0 +1,7 @@
+# casebench
+
+Test cases go straight to the board. The runtime never sees them.
+
+    casebench put path/to/case --player owen
+
+needs CASEBENCH_DSN, pointing at the hub's postgres.
diff --git a/casebench/__init__.py b/casebench/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/casebench/__main__.py b/casebench/__main__.py
new file mode 100644
index 0000000..dd8a8c9
--- /dev/null
+++ b/casebench/__main__.py
@@ -0,0 +1,5 @@
+import sys
+
+from .cli import main
+
+sys.exit(main())
diff --git a/casebench/casefile.py b/casebench/casefile.py
new file mode 100644
index 0000000..2beeafc
--- /dev/null
+++ b/casebench/casefile.py
@@ -0,0 +1,34 @@
+from dataclasses import dataclass
+from pathlib import Path
+
+import yaml
+
+
+class CaseError(ValueError):
+    pass
+
+
+@dataclass(frozen=True)
+class Item:
+    id: str
+    title: str
+    text: str
+
+
+@dataclass(frozen=True)
+class Case:
+    id: str
+    title: str
+    items: tuple[Item, ...]
+
+
+def load(folder: Path) -> Case:
+    path = Path(folder) / "case.yaml"
+    raw = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
+    items = tuple(
+        Item(entry["id"], entry["title"], (path.parent / entry["text"]).read_text(encoding="utf-8"))
+        for entry in raw.get("items") or []
+    )
+    if not items:
+        raise CaseError(f"{path}: no items")
+    return Case(raw["id"], raw["title"], items)
diff --git a/casebench/cli.py b/casebench/cli.py
new file mode 100644
index 0000000..8098615
--- /dev/null
+++ b/casebench/cli.py
@@ -0,0 +1,29 @@
+import argparse
+import os
+import sys
+from pathlib import Path
+
+from . import casefile, db
+
+
+def main(argv: list[str] | None = None) -> int:
+    parser = argparse.ArgumentParser(prog="casebench")
+    sub = parser.add_subparsers(dest="command", required=True)
+    put = sub.add_parser("put", help="put a case on a player's board")
+    put.add_argument("folder", type=Path)
+    put.add_argument("--player", required=True)
+    args = parser.parse_args(argv)
+
+    dsn = os.environ.get("CASEBENCH_DSN")
+    if not dsn:
+        print("casebench: set CASEBENCH_DSN", file=sys.stderr)
+        return 2
+    case = casefile.load(args.folder)
+    with db.connect(dsn) as conn:
+        player = db.player_id(conn, args.player)
+        if player is None:
+            print(f"casebench: no player {args.player}", file=sys.stderr)
+            return 1
+        db.put(conn, player, case)
+    print(f"{args.player}: put {case.id}")
+    return 0
diff --git a/casebench/db.py b/casebench/db.py
new file mode 100644
index 0000000..b75e378
--- /dev/null
+++ b/casebench/db.py
@@ -0,0 +1,26 @@
+import psycopg
+
+from .casefile import Case
+
+
+def connect(dsn: str) -> psycopg.Connection:
+    return psycopg.connect(dsn)
+
+
+def player_id(conn: psycopg.Connection, handle: str) -> str | None:
+    row = conn.execute("select id from players where handle = %s", (handle,)).fetchone()
+    return row[0] if row else None
+
+
+def put(conn: psycopg.Connection, player: str, case: Case) -> None:
+    with conn.transaction():
+        conn.execute(
+            "insert into board_cases (player_id, case_id, title, source) values (%s, %s, %s, 'bench')",
+            (player, case.id, case.title),
+        )
+        for position, item in enumerate(case.items):
+            conn.execute(
+                "insert into board_items (player_id, case_id, item_id, position, title, body)"
+                " values (%s, %s, %s, %s, %s, %s)",
+                (player, case.id, item.id, position, item.title, item.text),
+            )
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..9d717a4
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,12 @@
+[project]
+name = "casebench"
+version = "0.1.0"
+requires-python = ">=3.12"
+dependencies = ["pyyaml>=6", "psycopg>=3.1"]
+
+[project.scripts]
+casebench = "casebench.cli:main"
+
+[build-system]
+requires = ["setuptools>=69"]
+build-backend = "setuptools.build_meta"