Snapshot of the working project as of 2026-10-23 (local 2d4d4fc), one commit with no earlier history: the tracked files were de-identified first (synthetic claim ids and patient data only; see Context.md section 7). Credentials, run logs, screenshots, claim reports and client workbooks are gitignored and not included. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
122 lines
4.7 KiB
Python
122 lines
4.7 KiB
Python
"""
|
|
Read-only access to the results table (`dbo.CV_ChargeSummary`, written by
|
|
src/ecw_bot/core/results_db.py). SELECT statements only, always
|
|
parameterized; one short connection per call.
|
|
"""
|
|
|
|
from datetime import date, datetime, timedelta
|
|
|
|
from ecw_bot.core import results_db
|
|
|
|
# started_by since 2026-10-22 (EEWA-33): a table without it is reported as
|
|
# "needs tools/sql/create_tables.sql" (web/app.py, SQLSTATE 42S22).
|
|
_SELECT_COLUMNS = ", ".join(["id", "run_at", "run_mode", "started_by"]
|
|
+ [c for _, c in results_db.COLUMNS]
|
|
+ ["details_json"])
|
|
|
|
|
|
_RUN_COUNTS = (
|
|
"run_at, MAX(run_mode) AS run_mode, COUNT(*) AS claims, "
|
|
"SUM(CASE WHEN category = 'Pass' THEN 1 ELSE 0 END) AS pass_count, "
|
|
"SUM(CASE WHEN category = 'Fail' THEN 1 ELSE 0 END) AS fail_count, "
|
|
"SUM(CASE WHEN category = 'Review' THEN 1 ELSE 0 END) AS review_count")
|
|
|
|
# The same counts per day a run STARTED: every row of a run shares its
|
|
# run_at, so a run never splits over two days (portal, 2026-10-19).
|
|
_DAY = "CAST(run_at AS DATE)"
|
|
_DAY_COUNTS = (
|
|
f"{_DAY} AS day, COUNT(*) AS claims, COUNT(DISTINCT run_at) AS runs, "
|
|
"SUM(CASE WHEN category = 'Pass' THEN 1 ELSE 0 END) AS pass_count, "
|
|
"SUM(CASE WHEN category = 'Fail' THEN 1 ELSE 0 END) AS fail_count, "
|
|
"SUM(CASE WHEN category = 'Review' THEN 1 ELSE 0 END) AS review_count")
|
|
|
|
|
|
def _coerce(row: dict) -> dict:
|
|
"""
|
|
run_at as a datetime whatever the driver: ODBC Driver 17/18 return
|
|
DATETIME2 as datetime, the legacy {SQL Server} driver as text
|
|
("2026-10-08 15:10:00.0000000"), which the page cannot compare or
|
|
format (review, 2026-10-12).
|
|
"""
|
|
value = row.get("run_at")
|
|
if isinstance(value, str):
|
|
try:
|
|
row["run_at"] = datetime.fromisoformat(value.strip()[:26])
|
|
except ValueError:
|
|
pass
|
|
# The same for a day (DATE), which that driver returns as "2026-10-16".
|
|
value = row.get("day")
|
|
if isinstance(value, str):
|
|
try:
|
|
row["day"] = date.fromisoformat(value.strip()[:10])
|
|
except ValueError:
|
|
pass
|
|
elif isinstance(value, datetime):
|
|
row["day"] = value.date()
|
|
return row
|
|
|
|
|
|
class SqlResultsRepo:
|
|
def __init__(self, conn_str: str, connect=None):
|
|
self.conn_str = conn_str
|
|
self._connect = connect
|
|
|
|
def _query(self, sql: str, params=()) -> list:
|
|
connect = self._connect
|
|
if connect is None:
|
|
import pyodbc
|
|
connect = pyodbc.connect
|
|
conn = connect(self.conn_str, timeout=10)
|
|
try:
|
|
cur = conn.cursor().execute(sql, list(params))
|
|
cols = [d[0] for d in cur.description]
|
|
return [_coerce(dict(zip(cols, row))) for row in cur.fetchall()]
|
|
finally:
|
|
conn.close()
|
|
|
|
def runs(self) -> list:
|
|
"""Every run, newest first, with its claim count per Category."""
|
|
return self._query(
|
|
f"SELECT TOP 200 {_RUN_COUNTS} FROM {results_db.TABLE} "
|
|
f"GROUP BY run_at ORDER BY run_at DESC")
|
|
|
|
def results(self, run_at, category=None, claim_no=None) -> list:
|
|
"""One run's rows, in S.No order, optionally filtered."""
|
|
sql = (f"SELECT {_SELECT_COLUMNS} FROM {results_db.TABLE} "
|
|
f"WHERE run_at = ?")
|
|
params = [run_at]
|
|
if category:
|
|
sql += " AND category = ?"
|
|
params.append(category)
|
|
if claim_no:
|
|
sql += " AND claim_no LIKE ?"
|
|
params.append(f"%{claim_no}%")
|
|
sql += " ORDER BY s_no, id"
|
|
return self._query(sql, params)
|
|
|
|
def days(self, limit: int = 90) -> list:
|
|
"""The last `limit` days with a run, newest first: claims checked
|
|
(every row — a claim checked twice counts twice), runs, and the
|
|
count per Category."""
|
|
return self._query(
|
|
f"SELECT TOP {int(limit)} {_DAY_COUNTS} FROM {results_db.TABLE} "
|
|
f"GROUP BY {_DAY} ORDER BY {_DAY} DESC")
|
|
|
|
def day_results(self, day) -> list:
|
|
"""Every row of every run that started on `day`, in run order then
|
|
S.No. The page counts and filters them itself: its tiles need the
|
|
whole day whatever filter is on."""
|
|
start = datetime(day.year, day.month, day.day)
|
|
return self._query(
|
|
f"SELECT {_SELECT_COLUMNS} FROM {results_db.TABLE} "
|
|
f"WHERE run_at >= ? AND run_at < ? ORDER BY run_at, s_no, id",
|
|
[start, start + timedelta(days=1)])
|
|
|
|
def claim_history(self, claim_no) -> list:
|
|
"""Every saved check of the claims matching `claim_no`, newest
|
|
first (at most 200)."""
|
|
return self._query(
|
|
f"SELECT TOP 200 {_SELECT_COLUMNS} FROM {results_db.TABLE} "
|
|
f"WHERE claim_no LIKE ? ORDER BY run_at DESC, s_no, id",
|
|
[f"%{claim_no}%"])
|