Initial import: eCW Charge Validation bot and results Portal

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>
This commit is contained in:
ManikandanandClaude Opus 5.5 committed 2026-10-23 18:38:17 +05:30
commit ba15a574ab
205 files changed
+56772

No files matched your search

+229
View File
@@ -0,0 +1,229 @@
r"""
The run record — `Logs\bot_run.json`, the latest bot run (spec
docs/superpowers/specs/2026-10-14-start-bot-from-web-page-design.md).
Written by the bot itself at the start of `main()` and finished when
`main()` returns, so the results page sees a run started in the terminal
as well as one it started. While a run is live a second `main()` refuses to
start: two runs would share one eCW login guard and one Charge Summary.
{"pid", "started_at", "log_path", "source": "web" | "terminal",
"ended_at", "exit_code", "stopped_by_operator"}
Live = no `ended_at`, the pid is a running python process, and that process
was created no later than the record's `started_at` (plus a few seconds) —
so a record left unended by a killed run, whose pid Windows later hands to
another python process, does not read as live. A killed run cannot finish
its own record; its pid is gone or reused, so it reads as ended.
"""
import os
import time
import json
import ctypes
from ctypes import wintypes
from datetime import datetime, timedelta
from .. import settings as cfg
FILE_NAME = "bot_run.json"
_PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
_STILL_ACTIVE = 259
# started_at is written after the process exists, truncated to the second.
_CREATED_SLACK = timedelta(seconds=5)
_FILETIME_UNIX_EPOCH = 116444736000000000 # 1601-01-01 -> 1970-01-01, 100 ns
class Busy(Exception):
"""Another bot run is live."""
def path():
"""Read at call time: tests point cfg.LOGS elsewhere."""
return cfg.LOGS / FILE_NAME
def _now() -> str:
return datetime.now().isoformat(timespec="seconds")
def pid_alive(pid) -> bool:
"""
A running process with this pid whose program is python.
Never `os.kill(pid, 0)`: on Windows that TERMINATES the process.
The program-name check keeps a stale record whose pid Windows gave to
some other program from reading as a live bot run.
"""
try:
pid = int(pid)
except (TypeError, ValueError):
return False
if pid <= 0:
return False
if os.name != "nt":
try:
os.kill(pid, 0)
return True
except OSError:
return False
k32 = ctypes.WinDLL("kernel32", use_last_error=True)
k32.OpenProcess.restype = wintypes.HANDLE
k32.OpenProcess.argtypes = [wintypes.DWORD, wintypes.BOOL, wintypes.DWORD]
k32.GetExitCodeProcess.argtypes = [wintypes.HANDLE,
ctypes.POINTER(wintypes.DWORD)]
k32.QueryFullProcessImageNameW.argtypes = [
wintypes.HANDLE, wintypes.DWORD, wintypes.LPWSTR,
ctypes.POINTER(wintypes.DWORD)]
k32.CloseHandle.argtypes = [wintypes.HANDLE]
handle = k32.OpenProcess(_PROCESS_QUERY_LIMITED_INFORMATION, False, pid)
if not handle:
return False
try:
code = wintypes.DWORD()
if not k32.GetExitCodeProcess(handle, ctypes.byref(code)):
return False
if code.value != _STILL_ACTIVE:
return False
buf = ctypes.create_unicode_buffer(1024)
size = wintypes.DWORD(len(buf))
if not k32.QueryFullProcessImageNameW(handle, 0, buf,
ctypes.byref(size)):
return True
return "python" in os.path.basename(buf.value).lower()
finally:
k32.CloseHandle(handle)
def process_created(pid):
"""
When process `pid` was created, as a local naive datetime (the same
clock as the record's `started_at`); None when it cannot be read or
the process is no longer running.
"""
if os.name != "nt":
return None
try:
pid = int(pid)
except (TypeError, ValueError):
return None
if pid <= 0:
return None
k32 = ctypes.WinDLL("kernel32", use_last_error=True)
k32.OpenProcess.restype = wintypes.HANDLE
k32.OpenProcess.argtypes = [wintypes.DWORD, wintypes.BOOL, wintypes.DWORD]
k32.GetExitCodeProcess.argtypes = [wintypes.HANDLE,
ctypes.POINTER(wintypes.DWORD)]
k32.GetProcessTimes.argtypes = (
[wintypes.HANDLE] + [ctypes.POINTER(wintypes.FILETIME)] * 4)
k32.CloseHandle.argtypes = [wintypes.HANDLE]
handle = k32.OpenProcess(_PROCESS_QUERY_LIMITED_INFORMATION, False, pid)
if not handle:
return None
try:
# An ended process can still be opened while some handle to it is
# held (its parent's, say) and still has times: not a running one.
code = wintypes.DWORD()
if (not k32.GetExitCodeProcess(handle, ctypes.byref(code))
or code.value != _STILL_ACTIVE):
return None
created, exited, kernel, user = (wintypes.FILETIME() for _ in range(4))
if not k32.GetProcessTimes(handle, ctypes.byref(created),
ctypes.byref(exited), ctypes.byref(kernel),
ctypes.byref(user)):
return None
ticks = (created.dwHighDateTime << 32) | created.dwLowDateTime
if ticks <= _FILETIME_UNIX_EPOCH:
return None
return datetime.fromtimestamp(
(ticks - _FILETIME_UNIX_EPOCH) / 10_000_000)
except (OSError, OverflowError, ValueError):
return None
finally:
k32.CloseHandle(handle)
def read():
"""The record, or None when there is none or it cannot be read."""
try:
data = json.loads(path().read_text(encoding="utf-8"))
except (OSError, ValueError):
return None
return data if isinstance(data, dict) else None
def is_live(rec) -> bool:
if not rec or rec.get("ended_at") or not pid_alive(rec.get("pid")):
return False
# Same pid, but a younger process: the pid was reused after the run
# died without finishing its record. If either time cannot be read,
# trust the pid.
try:
started = datetime.fromisoformat(rec["started_at"])
except (KeyError, TypeError, ValueError):
return True
created = process_created(rec.get("pid"))
try:
if created is not None and created > started + _CREATED_SLACK:
return False
except TypeError:
pass # started_at carries a UTC offset: keep the pid verdict
return True
def _write(rec: dict) -> None:
target = path()
target.parent.mkdir(parents=True, exist_ok=True)
tmp = target.with_suffix(".tmp")
tmp.write_text(json.dumps(rec, indent=2), encoding="utf-8")
# On Windows the replace is refused for a moment while another process
# (the page polling, a scanner) holds the file open.
for attempt in range(10):
try:
os.replace(tmp, target)
return
except PermissionError:
if attempt == 9:
raise
time.sleep(0.05)
def acquire(log_path, source=None) -> dict:
"""This process's record, written; raises Busy over a live one."""
rec = read()
if is_live(rec):
raise Busy(f"A bot run is already going (pid {rec.get('pid')}, "
f"started {rec.get('started_at') or '?'}) — not starting "
f"a second one")
rec = {"pid": os.getpid(), "started_at": _now(),
"log_path": str(log_path),
"source": source or os.environ.get("ECW_RUN_SOURCE") or "terminal",
# The Portal User for a Portal run (EEWA-32), else Windows.
"started_by": (os.environ.get("ECW_STARTED_BY")
or os.environ.get("USERNAME") or ""),
"ended_at": None, "exit_code": None,
"stopped_by_operator": False}
_write(rec)
return rec
def finish(exit_code) -> None:
"""End this process's own record. Never raises."""
try:
rec = read()
if not rec or rec.get("pid") != os.getpid():
return
rec.update(ended_at=_now(), exit_code=exit_code)
_write(rec)
except Exception:
pass
def mark_stopped(pid) -> None:
"""The page killed run `pid`: say so on its record."""
rec = read()
if not rec or rec.get("pid") != pid or rec.get("ended_at"):
return
rec.update(ended_at=_now(), exit_code=None, stopped_by_operator=True)
_write(rec)