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