Compare commits
2 Commits
main
...
6d2a7818db
Author | SHA1 | Date | |
---|---|---|---|
6d2a7818db | |||
6da6b70537 |
19
README.md
19
README.md
@ -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`.
|
|
@ -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:
|
||||||
|
@ -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()
|
||||||
|
@ -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)]
|
||||||
|
@ -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')
|
||||||
|
@ -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()
|
||||||
|
@ -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):
|
||||||
|
@ -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))
|
||||||
|
@ -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
|
||||||
|
@ -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):
|
||||||
|
@ -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):
|
||||||
|
@ -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 } = {}) {
|
||||||
|
@ -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 {
|
||||||
|
Reference in New Issue
Block a user