Compare commits

..

2 Commits

13 changed files with 15 additions and 99 deletions

View File

@ -1,20 +1,3 @@
# vistassh-py # vistassh-py
Python-based web interface for VistA roll-and-scroll terminal Python-based web interface for VistA roll-and-scroll terminal
## Getting started
You will need:
- Python 3.10+ & pip
- Node.js & npm
To install the dependencies and build the frontend, perform the following steps:
```bash
pip install -r requirements.txt
cd frontend
npm install
npm run build
```
To run, invoke `main.py`.

View File

@ -9,7 +9,7 @@ import contextlib
import logging import logging
from collections import namedtuple from collections import namedtuple
from typing import Any, Optional, Union, Sequence, NamedTuple, Callable, AsyncGenerator from typing import Optional, Union, Sequence, NamedTuple, Callable
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -28,7 +28,7 @@ class ExpectQ(object):
"""Clear or restore buffer""" """Clear or restore buffer"""
self.buffer = buffer self.buffer = buffer
clear = reset clear = reset
async def prompts(self, endl: str='\r\n', timeout_settle: Optional[float]=None, throw: bool=False) -> AsyncGenerator[tuple[Optional[str], Optional[int]], None]: async def prompts(self, endl: str='\r\n', timeout_settle: Optional[float]=None, throw: bool=False):
len_endl = len(endl) len_endl = len(endl)
while True: while True:
if (pos := self.buffer.rfind(endl)) >= 0: if (pos := self.buffer.rfind(endl)) >= 0:
@ -43,7 +43,7 @@ class ExpectQ(object):
if throw: if throw:
raise raise
yield None, None yield None, None
async def promptmatches(self, *mappings: Union[str, re.Pattern, tuple, list], endl: str='\r\n', timeout_settle: Optional[float]=None, throw: bool=False) -> AsyncGenerator[tuple[Optional[ExpectMatch], Any], Optional[bool]]: async def promptmatches(self, *mappings: Union[str, re.Pattern, tuple, list], endl: str='\r\n', timeout_settle: Optional[float]=None, throw: bool=False):
for i, mapping in enumerate(mappings): for i, mapping in enumerate(mappings):
try: try:
match mapping: match mapping:

View File

@ -26,11 +26,5 @@ async def cmd_listclinics(proc):
proc.sendline('^') proc.sendline('^')
break break
proc.sendline('^Patient information AND OE/RR') proc.sendline('^Patient information AND OE/RR')
async for prompt, response in expect.promptmatches(( assert await expect.endswith('\r\nSelect Patient Information and OE/RR Option: ', '\r\nSelect Patient Information and OE/RR <TEST ACCOUNT> Option: ')
(re.compile(r' Press \'RETURN\' to continue, \'\^\' to stop: $'), None),
('Select Patient Information and OE/RR Option: ', None, True),
('Select Patient Information and OE/RR <TEST ACCOUNT> Option: ', None, True),
), throw=True):
if prompt.index == 0:
proc.sendline(response)
expect.clear() expect.clear()

View File

@ -74,13 +74,7 @@ async def cmd_reports(proc, mrn, alpha, omega):
else: else:
print(repr(before)) print(repr(before))
assert False assert False
async for prompt, response in expect.promptmatches(( assert await expect.endswith('\r\nSelect Patient Information and OE/RR Option: ', '\r\nSelect Patient Information and OE/RR <TEST ACCOUNT> Option: ')
(re.compile(r' Press \'RETURN\' to continue, \'\^\' to stop: $'), None),
('Select Patient Information and OE/RR Option: ', None, True),
('Select Patient Information and OE/RR <TEST ACCOUNT> Option: ', None, True),
), throw=True):
if prompt.index == 0:
proc.sendline(response)
expect.clear() expect.clear()
text = re.sub(r'\r\n\s+>> CONTINUATION OF .+? <<(?:(?:\r\n)|(?:\s+page \d+))', '', '\r\n'.join(pages)) text = re.sub(r'\r\n\s+>> CONTINUATION OF .+? <<(?:(?:\r\n)|(?:\s+page \d+))', '', '\r\n'.join(pages))
positions = [m.start() for m in re.finditer(r'(?:(?:[ ]+----MICROBIOLOGY----[ ]+page \d+\r\n\r\n)|(?:[ ]+))Reporting Lab:', text)] positions = [m.start() for m in re.finditer(r'(?:(?:[ ]+----MICROBIOLOGY----[ ]+page \d+\r\n\r\n)|(?:[ ]+))Reporting Lab:', text)]

View File

@ -39,13 +39,7 @@ async def cmd_entries(proc, mrn, alpha, omega):
proc.sendline(response) proc.sendline(response)
if prompt.index == 0 or prompt.index == 1: if prompt.index == 0 or prompt.index == 1:
pages.append(re.sub(r'^\x1b\[H\x1b\[J\x1b\[2J\x1b\[H\r\n[^\r\n]+? Cumulative Vitals\/Measurements Report[ ]+Page \d+\r\n\r\n-{10,}\r\n(?:\d{2}\/\d{2}\/\d{2} \(continued\)\r\n\r\n)?|\r\n\r\n\*\*\*[^\r\n]+\r\n\r\n[^\r\n]+?VAF 10-7987j\r\nUnit:[^\r\n]+\r\nDivision:[^\r\n]+(?:\r\n)?$', '', prompt.before)) pages.append(re.sub(r'^\x1b\[H\x1b\[J\x1b\[2J\x1b\[H\r\n[^\r\n]+? Cumulative Vitals\/Measurements Report[ ]+Page \d+\r\n\r\n-{10,}\r\n(?:\d{2}\/\d{2}\/\d{2} \(continued\)\r\n\r\n)?|\r\n\r\n\*\*\*[^\r\n]+\r\n\r\n[^\r\n]+?VAF 10-7987j\r\nUnit:[^\r\n]+\r\nDivision:[^\r\n]+(?:\r\n)?$', '', prompt.before))
async for prompt, response in expect.promptmatches(( assert await expect.endswith('\r\nSelect Patient Information and OE/RR Option: ', '\r\nSelect Patient Information and OE/RR <TEST ACCOUNT> Option: ')
(re.compile(r' Press \'RETURN\' to continue, \'\^\' to stop: $'), None),
('Select Patient Information and OE/RR Option: ', None, True),
('Select Patient Information and OE/RR <TEST ACCOUNT> Option: ', None, True),
), throw=True):
if prompt.index == 0:
proc.sendline(response)
expect.clear() expect.clear()
for m_date in re.finditer(r'^(?P<date>\d{2}\/\d{2}\/\d{2})\r\n(?P<body>.*?\r\n)(?:(?=\d{2}\/)|\r\n|$)', '\r\n'.join(pages), re.DOTALL|re.MULTILINE): for m_date in re.finditer(r'^(?P<date>\d{2}\/\d{2}\/\d{2})\r\n(?P<body>.*?\r\n)(?:(?=\d{2}\/)|\r\n|$)', '\r\n'.join(pages), re.DOTALL|re.MULTILINE):
g_date = m_date.group('date') g_date = m_date.group('date')

View File

@ -68,11 +68,5 @@ async def cmd_reports(proc, mrn, alpha, omega):
case _: assert False case _: assert False
proc.sendline('^') proc.sendline('^')
proc.sendline('^Patient information AND OE/RR') proc.sendline('^Patient information AND OE/RR')
async for prompt, response in expect.promptmatches(( assert await expect.endswith('\r\nSelect Patient Information and OE/RR Option: ', '\r\nSelect Patient Information and OE/RR <TEST ACCOUNT> Option: ')
(re.compile(r' Press \'RETURN\' to continue, \'\^\' to stop: $'), None),
('Select Patient Information and OE/RR Option: ', None, True),
('Select Patient Information and OE/RR <TEST ACCOUNT> Option: ', None, True),
), throw=True):
if prompt.index == 0:
proc.sendline(response)
expect.clear() expect.clear()

View File

@ -77,13 +77,7 @@ async def cmd_entries(proc, mrn, alpha, omega):
break break
case _: assert False case _: assert False
proc.sendline('^Patient information AND OE/RR') proc.sendline('^Patient information AND OE/RR')
async for prompt, response in expect.promptmatches(( assert await expect.endswith('\r\nSelect Patient Information and OE/RR Option: ', '\r\nSelect Patient Information and OE/RR <TEST ACCOUNT> Option: ')
(re.compile(r' Press \'RETURN\' to continue, \'\^\' to stop: $'), None),
('Select Patient Information and OE/RR Option: ', None, True),
('Select Patient Information and OE/RR <TEST ACCOUNT> Option: ', None, True),
), throw=True):
if prompt.index == 0:
proc.sendline(response)
expect.clear() expect.clear()
prev = None prev = None
for m in re.finditer(r'\b\d{2}/\d{2}/\d{2}.*?\r\n\r\n', '\r\n'.join(pages).replace('\x1b[1m', '').replace('\x1b[m', ''), re.DOTALL): for m in re.finditer(r'\b\d{2}/\d{2}/\d{2}.*?\r\n\r\n', '\r\n'.join(pages).replace('\x1b[1m', '').replace('\x1b[m', ''), re.DOTALL):

View File

@ -25,13 +25,7 @@ async def cmd_lookup_patient(proc, query):
res.append(prompt.before[:-24] if prompt.index == 0 and prompt.before.endswith('\r\nENTER \'^\' TO STOP, OR \r\n') else prompt.before) res.append(prompt.before[:-24] if prompt.index == 0 and prompt.before.endswith('\r\nENTER \'^\' TO STOP, OR \r\n') else prompt.before)
if 0 < prompt.index < 4: if 0 < prompt.index < 4:
single = True single = True
async for prompt, response in expect.promptmatches(( assert await expect.endswith('\r\nSelect Patient Information and OE/RR Option: ', '\r\nSelect Patient Information and OE/RR <TEST ACCOUNT> Option: ')
(re.compile(r' Press \'RETURN\' to continue, \'\^\' to stop: $'), None),
('Select Patient Information and OE/RR Option: ', None, True),
('Select Patient Information and OE/RR <TEST ACCOUNT> Option: ', None, True),
), throw=True):
if prompt.index == 0:
proc.sendline(response)
expect.clear() expect.clear()
if single: if single:
return [re.search(r'[ ]{2}(?P<name>.+?)[ ]{2}(?:\((?P<alias>[^\)]*?)\))?[ ]{6}(?P<dob>\S+)[ ]{4}(?P<ssn>\S+(?:P \*\*Pseudo SSN\*\*)?)[ ]{5}(?P<yesno>\S+)[ ]{5}(?P<type>.+?)[ ]{6}(?P<no>[^\r\n]*)', res[0].replace('\r\n', '', 1)).groupdict()] return [re.search(r'[ ]{2}(?P<name>.+?)[ ]{2}(?:\((?P<alias>[^\)]*?)\))?[ ]{6}(?P<dob>\S+)[ ]{4}(?P<ssn>\S+(?:P \*\*Pseudo SSN\*\*)?)[ ]{5}(?P<yesno>\S+)[ ]{5}(?P<type>.+?)[ ]{6}(?P<no>[^\r\n]*)', res[0].replace('\r\n', '', 1)).groupdict()]
@ -71,12 +65,6 @@ async def cmd_lookup_patient_ordinal(proc, query, ordinal, force=False):
proc.sendline(response) proc.sendline(response)
case autoproc.ExpectMatch(index=5): case autoproc.ExpectMatch(index=5):
proc.sendline() proc.sendline()
async for prompt, response in expect.promptmatches(( assert await expect.endswith('\r\nSelect Patient Information and OE/RR Option: ', '\r\nSelect Patient Information and OE/RR <TEST ACCOUNT> Option: ')
(re.compile(r' Press \'RETURN\' to continue, \'\^\' to stop: $'), None),
('Select Patient Information and OE/RR Option: ', None, True),
('Select Patient Information and OE/RR <TEST ACCOUNT> Option: ', None, True),
), throw=True):
if prompt.index == 0:
proc.sendline(response)
expect.clear() expect.clear()
return re.sub(r'\r\n\r\n(?:[^\r\n;]+);(?:\([^\)]*?\))? (?:\d+ )?(?:\d{3}-\d{2}-\d{4}P?) (?:[^\r\n]+?)[ ]*?(\r\n={10,}\r\n)\r\n', r'\1', '\r\n'.join(res)) return re.sub(r'\r\n\r\n(?:[^\r\n;]+);(?:\([^\)]*?\))? (?:\d+ )?(?:\d{3}-\d{2}-\d{4}P?) (?:[^\r\n]+?)[ ]*?(\r\n={10,}\r\n)\r\n', r'\1', '\r\n'.join(res))

View File

@ -58,13 +58,7 @@ async def cmd_patients(proc, alpha, omega):
summary.extend({k.strip(): v.strip() for k, v in row.groupdict().items()} for row in re.finditer(r'(?P<last5>[A-Z]\d{4}) (?P<name>[^\r\n]{30}) (?P<uid>[^ \r\n]+) (?P<primarySite>[^ \r\n]+) (?P<dateOfDiagnosis>\d{2}/\d{2}/\d{4}) (?P<dateCaseLastChanged>\d{2}/\d{2}/\d{4})', prompt.before)) summary.extend({k.strip(): v.strip() for k, v in row.groupdict().items()} for row in re.finditer(r'(?P<last5>[A-Z]\d{4}) (?P<name>[^\r\n]{30}) (?P<uid>[^ \r\n]+) (?P<primarySite>[^ \r\n]+) (?P<dateOfDiagnosis>\d{2}/\d{2}/\d{4}) (?P<dateCaseLastChanged>\d{2}/\d{2}/\d{4})', prompt.before))
elif prompt.index == 4: elif prompt.index == 4:
break break
async for prompt, response in expect.promptmatches(( assert await expect.endswith('\r\nSelect Patient Information and OE/RR Option: ', '\r\nSelect Patient Information and OE/RR <TEST ACCOUNT> Option: ')
(re.compile(r' Press \'RETURN\' to continue, \'\^\' to stop: $'), None),
('Select Patient Information and OE/RR Option: ', None, True),
('Select Patient Information and OE/RR <TEST ACCOUNT> Option: ', None, True),
), throw=True):
if prompt.index == 0:
proc.sendline(response)
expect.clear() expect.clear()
for item in parse_xml_rcrs(doc_rcrs, summary): for item in parse_xml_rcrs(doc_rcrs, summary):
yield item yield item

View File

@ -63,13 +63,7 @@ async def cmd_appointments(proc, clinics='NPT-HEM/ONC ATTENDING', date='T', stor
item['comment'] = '\r\n'.join(m.group(1) for m in re.finditer(r'^\s{15}(\w.*?)$', detail, re.MULTILINE)) item['comment'] = '\r\n'.join(m.group(1) for m in re.finditer(r'^\s{15}(\w.*?)$', detail, re.MULTILINE))
yield item yield item
proc.sendline('^Patient information AND OE/RR') proc.sendline('^Patient information AND OE/RR')
async for prompt, response in expect.promptmatches(( assert await expect.endswith('\r\nSelect Patient Information and OE/RR Option: ', '\r\nSelect Patient Information and OE/RR <TEST ACCOUNT> Option: ')
(re.compile(r' Press \'RETURN\' to continue, \'\^\' to stop: $'), None),
('Select Patient Information and OE/RR Option: ', None, True),
('Select Patient Information and OE/RR <TEST ACCOUNT> Option: ', None, True),
), throw=True):
if prompt.index == 0:
proc.sendline(response)
expect.clear() expect.clear()
async def vista_appointment_clinics(proc, expect): async def vista_appointment_clinics(proc, expect):

View File

@ -23,13 +23,6 @@ async def task_smartcard(proc, config: Optional[configparser.ConfigParser]=None)
if certificate: if certificate:
config.set('auth', 'certificate', certificate) config.set('auth', 'certificate', certificate)
proc.create_task(task_keepalive(proc, True), name='@task:keepalive') proc.create_task(task_keepalive(proc, True), name='@task:keepalive')
async for prompt, response in expect.promptmatches((
(re.compile(r' Press \'RETURN\' to continue, \'\^\' to stop: $'), None),
('Select Patient Information and OE/RR Option: ', None, True),
('Select Patient Information and OE/RR <TEST ACCOUNT> Option: ', None, True),
), throw=True):
if prompt.index == 0:
proc.sendline(response)
return True return True
async def task_keepalive(proc, suppress=False): async def task_keepalive(proc, suppress=False):

View File

@ -1,8 +1,7 @@
export async function get_api_appointments({ fetch, clinics = [], date = 'T' } = {}) { export async function get_api_appointments({ fetch, clinics = [], date = 'T' } = {}) {
if(clinics.constructor === Array) clinics = clinics.map(x => x.replace(/^\s+|\s+$/g, '').replace(/\s+/, ' ')).filter(x => x).join('^').replace(/\//g, '|'); if(clinics.constructor === Array) clinics = clinics.map(x => x.replace(/^\s+|\s+$/g, '').replace(/\s+/, ' ')).filter(x => x).join('^').replace(/\//g, '|');
else clinics = clinics.replace(/^\s+|\s+$/g, '').replace(/\s+/, ' ').replace(/\//g, '|'); else clinics = clinics.replace(/^\s+|\s+$/g, '').replace(/\s+/, ' ').replace(/\//g, '|');
if(clinics) return await (await (fetch || window.fetch)('/api/appointments/' + clinics + '/' + date)).json(); return await (await (fetch || window.fetch)('/api/appointments/' + clinics + '/' + date)).json();
else return [];
} }
export async function get_api_lookup({ fetch, query, ordinal, force = false } = {}) { export async function get_api_lookup({ fetch, query, ordinal, force = false } = {}) {

View File

@ -2,12 +2,7 @@ import { get_api_appointments } from '$lib/backend.js';
/** @type {import('./$types').PageLoad} */ /** @type {import('./$types').PageLoad} */
export async function load({ params, fetch }) { export async function load({ params, fetch }) {
let clinics = []; let clinics = await (await fetch('/api/config/user/clinics')).json();
try {
clinics = await (await fetch('/api/config/user/clinics')).json();
} catch(ex) {
console.error(ex, ex.stack);
}
let appointments = await get_api_appointments({ fetch, clinics, date: 'T' }); let appointments = await get_api_appointments({ fetch, clinics, date: 'T' });
appointments.sort((a, b) => a.time_scheduled < b.time_scheduled ? -1 : a.time_scheduled > b.time_scheduled ? 1 : 0); appointments.sort((a, b) => a.time_scheduled < b.time_scheduled ? -1 : a.time_scheduled > b.time_scheduled ? 1 : 0);
return { return {