Evening.
[Apologies re non linking links - original reply bounced because newbies can’t post a reply with more than 2 links. Also seem to have turned code comments into banner headlines - oh well]
Not on Github but potentially could be if I ever get it far enough to be (a) clean and (b) useful. Have included enough before to kickstart anyone interested in the same things. Agree that the lack of an “open” open api is frustrating but with a bit of stubbornness and few simple tools necessity is most definitely the mother of invention.
The tricky bit is the API key - can’t share working code even though 5 minutes with a debugger or similar (I like WinAPIOverride : Free Advanced API Monitor, spy or override API or exe internal functions) will find you one I would imagine. The key bit would be the InitialiseWithId call as below while an API app runs. Find that and you find the key… allegedly.
The basic idea is that em_pacc.dll contains COM objects (which oleview.exe from within even the free version of visual studio) will happily show. In Python, if you install the win32 extensions (pywin32 · PyPI) you get COM support and Quick Start to Client side COM and Python talks through how that works.
What I have working is - connect, get current patient id or list of changed records since a given date, retrieve record as XML file (be very, very, very careful with your data security if you do any of this), parse the XML (the lxml objectify layer is handy here) then wrap each Medication XML entry (or BP entry etc) in a data class which extracts useful info… which can finally be inserted into a sqlite database or reported in some other fashion. As an aside, the TRUD dm+d datasets are also XML and lxml does a lovely job mapping AMP through VMP to VTM such that one can say Zestoretic is lisinopril+hydrochlorothiazide (which is basically all you need to say “what is it and what monitoring does it need?”).
I’d hoped that the diary entries would change meaningfully on completion (but they don’t)… hence the query about retrieving searches from Emis. It’s odd… completing a diary entry seems to change a pathology result when you compare pre and post change XML… all very curious.
Some code for the curious below.
Iain
# COM will only work on windows
if sys.platform.startswith('win32'):
import win32com.client
from win32com.client import gencache
import pythoncom
# Define a namespace for the XML we'll be parsing
BNS = 'http://www.e-mis.com/emisopen/MedicalRecord'
#-------------------------------------------------------------------------------
@dataclass
class EMW():
""" This is Emis Web itself """
_emw: object = field(init=False, default=None, repr=False)
_SID: str = field(init=False, default=None, repr=False)
_ChangedMR: list = field(init=False,default=None,repr=False)
def __exit__(self, exc_type, exc_value, exc_traceback):
""" Tidy up gracefully """
if sys.platform.startswith('win32'): pythoncom.CoUninitialize()
# print ('EMW cleanup finished')
def __enter__(self):
""" Connect to Emis Web """
# COM will only work on windows
if sys.platform.startswith('win32'):
# This is EM_PACC.dll from EmisWeb
gencache.EnsureModule('{99847EE4-4F79-4EDD-A420-79487CD94EEF}', 0, 1, 0)
# As per http://www.icodeguru.com/WebServer/Python-Programming-on-Win32/appd.htm
sys.coinit_flags = 0 # pythoncom.COINIT_MULTITHREADED == 0
self._emw=win32com.client.Dispatch('EM_PACC.PatientAccess_20100519')
# Using the API key
print ('Initialising connection...', end='\r', flush=True)
(rv, cdb, product, version, loginID, error, outcome, self._SID)= self._emw.InitializeWithID(0, "[webinterop.cymru.nhs.uk](http://webinterop.cymru.nhs.uk/)", "", "",
"4 digit site code here", 'API key here in 8-4-4-12 hex digit GUID format')
if outcome != '4': raise AssertionError(f'InitializeWithID - Outcome code {outcome}')
print ('Initialising connection... done', flush=True)
#stream = pythoncom.CoMarshalInterThreadInterfaceInStream(pythoncom.IID_IDispatch, self._emw._oleobj_)
else:
print ('Not on windows - EMW.Connect run')
return self
- after that you’ve got things like
# -----------------------------------------------------------------------------
def ChangedMR_Patients(self, when='1860/01/01') -> list:
# Keep everyone informed
print ('Requesting list of patients with updated medical records...', flush=True)
# Request the list
(rv, pts, error, outcome)= self._emw.GetChangedPatientsMR(self._SID, when)
# print (f'Outcome: {outcome}, Error: {error}, RV: {rv}')
# Outcome = 3 when future date given
if outcome != '1': raise AssertionError(f'GetChangedPatientsMR - {error}')
# Save off the XML received
with bz2.open('GetChangedPatientsMR_XML.bz2', 'wt', encoding='utf-8') as f: f.write(pts)
# Parse the XML
d = etree.fromstring(pts)
# Build a patient list - ugly though namespaces are... it's faster to work with than to remove them
return [e for e in etree.XPath("""//x:PatientMatches/x:PatientList/x:Patient/x:DBID/text()""",namespaces={'x': BNS})(d)]
to get a list of EMIS patient IDs. Don’t seem to be able to specify a granularity of less than a day but 2021/06/14 would mean anything from 00:00 this morning onwards and
# -----------------------------------------------------------------------------
def Get_Medical_Record(self, pt_list):
# Patient list is a deck (deque) - basically a FIFO queue
try:
# COM will only work on windows
if sys.platform.startswith('win32'):
pythoncom.CoInitializeEx(pythoncom.COINIT_MULTITHREADED)
emw2 = win32com.client.Dispatch(self._emw)
while pt_list:
pid = pt_list.popleft():
start = time.perf_counter()
# print (f'Processing {pid}...')
# Get medical record
(rv,rec,error,outcome) = emw2.GetMedicalRecord(self._SID, pid)
#print (f'{rv},{error},{outcome}')
# Outcome 1 = medical record found
if outcome == '3':
raise ValueError(f'Patient {pid} not found')
else:
# Store the XML
with bz2.open(os.path.join('Data','MR',f'{pid}.xz2'), 'wt', encoding='utf-8') as f: f.write(rec)
print (f'Patient {pid} retrieved in {time.perf_counter() - start:.2f} seconds.')
finally:
# Release the COM object
pythoncom.CoUninitialize()
with a bit of COM glue thrown in because this runs asynchronously in a set of threads all working through the same list (can take 4-16 seconds to retrieve a record so why not grab 10 in parallel). I’m a bit wooly on COM so some of this may be overkill.
Once you’ve got an XML file (the medical record) then the python bindings for the gloriously fast lxml gives you
with bz2.open(f'Data/MR/{pid}.xz2','r') as f:
# Parse the XML
MR = objectify.parse(f)
xpe = etree.XPathEvaluator(MR,namespaces={'x': BNS})
on a bzip2 compressed XML file and then
# Update Medications table - delete old entries then (re)populate
db.execute('delete from Medications where pid = ?', (pid,))
db.executemany('INSERT INTO Medications VALUES (?,?,?,?)',
((pid,m.last_issued,m.code,m.term) for m in (Medication(e) for e in xpe('/x:MedicalRecord/x:MedicationList/x:Medication'))))
which uses generator comprehensions (sort of nested for loops). The Medication bit starts:
@dataclass
class Medication:
_e : objectify.ObjectifiedElement = field(init=False, repr=True)
when: date = field(init=False, repr=False)
def __init__(self,e): self._e = e
@property
def when(self) -> date:
try:
w = str(self._e.AssignedDate)
return date(int(w[-4:]),int(w[3:5]),int(w[:2]))
except AttributeError: return None