HengJay commited on
Commit
a24feb6
·
1 Parent(s): 4d8662e

Add Cerner login feature.

Browse files
Files changed (3) hide show
  1. logo.jpg +0 -0
  2. pages/Cerner_LogIn.py +371 -0
  3. pages/logo.jpg +0 -0
logo.jpg ADDED
pages/Cerner_LogIn.py ADDED
@@ -0,0 +1,371 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import requests
3
+ import base64
4
+ import uuid
5
+ import json
6
+ import os
7
+ from datetime import datetime
8
+ from dotenv import load_dotenv
9
+
10
+ load_dotenv()
11
+
12
+ CERNER_CLIENT_ID = st.secrets["CERNER_CLIENT_ID"]
13
+ CERNER_CLIENT_SECRET = st.secrets["CERNER_CLIENT_SECRET"]
14
+ CERNER_TENANT_ID = st.secrets["CERNER_TENANT_ID"]
15
+ CERNER_AUTH_SERVER_URL = f"https://authorization.cerner.com/tenants/{CERNER_TENANT_ID}/protocols/oauth2/profiles/smart-v1/personas/provider/authorize"
16
+ CERNER_TOKEN_ENDPOINT = f"https://authorization.cerner.com/tenants/{CERNER_TENANT_ID}/protocols/oauth2/profiles/smart-v1/token"
17
+ CERNER_REDIRECT_URI = "https://huggingface.co/spaces/HengJay/snomed-ct-assistant" #"http://localhost:8501" #"https://huggingface.co/spaces/HengJay/snomed-ct-assistant"
18
+ CERNER_AUDIENCE_URL = f"https://fhir-ehr.cerner.com/r4/{CERNER_TENANT_ID}"
19
+
20
+ def get_fhir_url():
21
+ params = {
22
+ "response_type": "code",
23
+ "client_id": CERNER_CLIENT_ID,
24
+ "redirect_uri": CERNER_REDIRECT_URI,
25
+ "scope": "user/Practitioner.read user/Patient.read user/Observation.read openid profile",
26
+ "state": str(uuid.uuid4()),
27
+ "aud": CERNER_AUDIENCE_URL
28
+ }
29
+ oauth2_url = requests.Request('GET', CERNER_AUTH_SERVER_URL, params=params).prepare().url
30
+ return oauth2_url
31
+
32
+ def get_fhir_url_launch(launch):
33
+ params = {
34
+ "response_type": "code",
35
+ "client_id": CERNER_CLIENT_ID,
36
+ "redirect_uri": CERNER_REDIRECT_URI,
37
+ "scope": "user/Practitioner.read user/Patient.read user/Observation.read launch openid profile",
38
+ "state": str(uuid.uuid4()),
39
+ "aud": CERNER_AUDIENCE_URL,
40
+ "launch": launch
41
+ }
42
+ oauth2_url = requests.Request('GET', CERNER_AUTH_SERVER_URL, params=params).prepare().url
43
+ return oauth2_url
44
+
45
+ def get_fhir_token(auth_code):
46
+ auth_string = f"{CERNER_CLIENT_ID}:{CERNER_CLIENT_SECRET}".encode("ascii")
47
+ base64_bytes = base64.b64encode(auth_string)
48
+ base64_string = base64_bytes.decode("ascii")
49
+
50
+ auth_headers = {
51
+ "Authorization": f"Basic {base64_string}",
52
+ }
53
+
54
+ data = {
55
+ "grant_type": "authorization_code",
56
+ "code": auth_code,
57
+ "redirect_uri": CERNER_REDIRECT_URI,
58
+ "client_id": CERNER_CLIENT_ID,
59
+ "client_secret": CERNER_CLIENT_SECRET
60
+ }
61
+
62
+ response = requests.post(CERNER_TOKEN_ENDPOINT, headers=auth_headers, data=data)
63
+ return response.json()
64
+
65
+ def standalone_launch(url: str, text: str= None, color="#aa8ccc"):
66
+ st.markdown(
67
+ f"""
68
+ <a href="{url}" target="_self">
69
+ <div style="
70
+ display: inline-block;
71
+ padding: 0.5em 1em;
72
+ color: #FFFFFF;
73
+ background-color: {color};
74
+ border-radius: 3px;
75
+ text-decoration: none;">
76
+ {text}
77
+ </div>
78
+ </a>
79
+ """,
80
+ unsafe_allow_html=True
81
+ )
82
+
83
+ def get_practitioner(user_profile_url, access_token):
84
+ headers = {
85
+ "Accept": "application/fhir+json",
86
+ "Authorization": f"Bearer {access_token}"
87
+ }
88
+
89
+ response = requests.get(user_profile_url, headers=headers)
90
+ return response.json()
91
+
92
+ def get_patient(access_token, person_id):
93
+ base_url = f"https://fhir-ehr.cerner.com/r4/{CERNER_TENANT_ID}/Patient"
94
+ headers = {
95
+ "Accept": "application/fhir+json",
96
+ "Authorization": f"Bearer {access_token}"
97
+ }
98
+ query_params = {
99
+ "_id": person_id
100
+ }
101
+
102
+ response = requests.get(base_url, headers=headers, params=query_params)
103
+ return response.json()
104
+
105
+ def get_observation(access_token, person_id):
106
+ base_url = f"https://fhir-ehr.cerner.com/r4/{CERNER_TENANT_ID}/Observation"
107
+ headers = {
108
+ "Accept": "application/fhir+json",
109
+ "Authorization": f"Bearer {access_token}"
110
+ }
111
+ query_params = {
112
+ "patient": person_id
113
+ }
114
+
115
+ response = requests.get(base_url, headers=headers, params=query_params)
116
+ return response.json()
117
+
118
+ def fhir_params():
119
+ query_params = st.experimental_get_query_params()
120
+ auth_code = query_params.get("code")
121
+ iss_param = query_params.get("iss")
122
+ launch_param = query_params.get("launch")
123
+ print(f"fhir_params: {auth_code}, {iss_param}, {launch_param}")
124
+ return auth_code, iss_param, launch_param
125
+
126
+ def main():
127
+ logo, title = st.columns([1,1])
128
+ logo_image = 'logo.jpg'
129
+
130
+ with logo:
131
+ st.image(logo_image)
132
+
133
+ auth_code, iss_param, launch_param = fhir_params()
134
+
135
+ if auth_code is None and iss_param is None:
136
+ fhir_login_url = get_fhir_url()
137
+ st.subheader("Our App's Launch Capabilities")
138
+ st.markdown("""
139
+ 1. EHR Launch: Seamlessly integrates with your EHR system.
140
+ 2. Standalone Launch: No need for an EHR to start up. This app can independently access FHIR data as long as it's authorized and provided the relevant iss URL.
141
+ """)
142
+ st.subheader("How It Works")
143
+ st.markdown("""
144
+ * When the app gets a launch request, it seeks permission to access FHIR data.
145
+ * It does this by directing the browser to the EHR's authorization point.
146
+ * Depending on certain rules and potential user approval, the EHR authorization system either approves or denies the request.
147
+ * If approved, an authorization code is sent to the app, which is then swapped for an access token.
148
+ * This access token is your key to the EHR's resource data.
149
+ * Should a refresh token be provided with the access token, the app can utilize it to obtain a fresh access token once the original expires.
150
+ """)
151
+
152
+ standalone_launch(fhir_login_url, "Login")
153
+
154
+ if iss_param is not None:
155
+ fhir_login_url = get_fhir_url_launch(launch_param[0])
156
+ st.write(f"""
157
+ <meta http-equiv="refresh" content="0; URL={fhir_login_url}">
158
+ """, unsafe_allow_html=True)
159
+
160
+ if auth_code:
161
+ if 'token' in st.session_state and 'access_token' in st.session_state.token:
162
+ token = st.session_state.token
163
+ else:
164
+ token = get_fhir_token(auth_code[0])
165
+ if token.get('error') == 'invalid_grant':
166
+ fhir_login_url = get_fhir_url()
167
+ st.markdown("""
168
+ Session Expired
169
+ """)
170
+ standalone_launch(fhir_login_url, "Login")
171
+ return
172
+ else:
173
+ st.session_state.token = token
174
+
175
+ access_token = token.get('access_token')
176
+ st.session_state.access_token = access_token
177
+
178
+ header, payload, signature = token.get('id_token').split('.')
179
+ decoded_payload = base64.urlsafe_b64decode(payload + '==').decode('utf-8')
180
+
181
+ person_id = None
182
+
183
+ if 'person_id' not in st.session_state:
184
+ st.session_state.person_id = token.get('patient', "12724065") # If no person_id is provided, app defaults to WILMA SMART
185
+
186
+ person_id = st.session_state.person_id
187
+
188
+ if 'profile_data' not in st.session_state:
189
+ st.session_state.profile_data = json.loads(decoded_payload)
190
+ if 'practitioner_data' not in st.session_state:
191
+ st.session_state.practitioner_data = get_practitioner(st.session_state.profile_data.get('profile'), access_token)
192
+ if 'patient_data' not in st.session_state:
193
+ patient_data = get_patient(access_token, person_id)
194
+ if 'entry' in patient_data:
195
+ st.session_state.patient_data = patient_data
196
+ else:
197
+ st.session_state.patient_data = None
198
+ if 'observation_data' not in st.session_state:
199
+ observation_data = get_observation(access_token, person_id)
200
+ if 'entry' in observation_data:
201
+ st.session_state.observation_data = observation_data
202
+ else:
203
+ st.session_state.observation_data = None
204
+
205
+ resource_list = ['Profile', 'Practitioner', 'Patient', 'Observation']
206
+ resource = st.sidebar.selectbox("Resource:", resource_list, index=0)
207
+
208
+ if resource == 'Profile':
209
+ with title:
210
+ st.markdown('')
211
+ st.subheader('Cerner Profile')
212
+ profile_data = st.session_state.profile_data
213
+ profile_username = profile_data["sub"]
214
+ profile_full_name = profile_data["name"]
215
+ profile_token_iat = profile_data["iat"]
216
+ profile_token_exp = profile_data["exp"]
217
+ profile_token_iat = datetime.utcfromtimestamp(profile_token_iat).strftime('%Y-%m-%d %H:%M:%S UTC')
218
+ profile_token_exp = datetime.utcfromtimestamp(profile_token_exp).strftime('%Y-%m-%d %H:%M:%S UTC')
219
+ st.markdown("*This data shows profile details of the user currently signed in*")
220
+ st.markdown(f"""
221
+ * **Username:** {profile_username}
222
+ * **Name:** {profile_full_name}
223
+ * **Token Issued:** {profile_token_iat}
224
+ * **Token Expiration:** {profile_token_exp}
225
+ """)
226
+ if st.checkbox('Show JSON Response'):
227
+ st.json(st.session_state.profile_data)
228
+ if resource == 'Practitioner':
229
+ with title:
230
+ st.markdown('')
231
+ st.subheader('Practitioner Resource')
232
+ practitioner_data = st.session_state.practitioner_data
233
+ practitioner_name = practitioner_data["name"][0]["text"]
234
+ practitioner_npi = next(identifier["value"] for identifier in practitioner_data["identifier"] if identifier["type"]["coding"][0]["code"] == "NPI")
235
+ practitioner_email = next(telecom["value"] for telecom in practitioner_data["telecom"] if telecom["system"] == "email")
236
+ st.markdown("*This data shows details in the practitioner endpoint of the user currently signed in*")
237
+ st.markdown(f"""
238
+ * **Practitioner Name:** {practitioner_name}
239
+ * **NPI:** {practitioner_npi}
240
+ * **Email:** {practitioner_email}
241
+ """)
242
+ if st.checkbox('Show JSON Response'):
243
+ st.json(st.session_state.practitioner_data)
244
+ if resource == 'Patient':
245
+ with title:
246
+ st.markdown('')
247
+ st.subheader('Patient Resource')
248
+ if st.session_state.patient_data is None:
249
+ st.markdown("No patient data available.")
250
+ else:
251
+ patient_data = st.session_state.patient_data
252
+ patient_name = patient_data['entry'][0]['resource']['name'][0]['text']
253
+ patient_status = patient_data['entry'][0]['resource']['active']
254
+ if patient_status is True:
255
+ patient_status = 'active'
256
+ else:
257
+ patient_status = 'inactive'
258
+ patient_phone = next((telecom['value'] for telecom in patient_data['entry'][0]['resource']['telecom'] if telecom['system'] == 'phone'), None)
259
+ patient_address = patient_data['entry'][0]['resource']['address'][0]
260
+ patient_address = f"{patient_address['line'][0]}, {patient_address['city']}, {patient_address['state']} {patient_address['postalCode']}, {patient_address['country']}"
261
+ patient_email = next((telecom['value'] for telecom in patient_data['entry'][0]['resource']['telecom'] if telecom['system'] == 'email'), None)
262
+ patient_dob = patient_data['entry'][0]['resource']['birthDate']
263
+ patient_gender = patient_data['entry'][0]['resource']['gender']
264
+ patient_pref_lang = patient_data['entry'][0]['resource']['communication'][0]['language']['text']
265
+ patient_marital_status = patient_data['entry'][0]['resource']['maritalStatus']['text']
266
+ contact_person = patient_data['entry'][0]['resource']['contact'][0]
267
+ contact_person_name = contact_person['name']['text']
268
+ contact_person_phone = contact_person['telecom'][0]['value']
269
+ contact_person_relationship = contact_person['relationship'][0]['text']
270
+ st.markdown(f"*This data shows details in the patient endpoint of patient ID: {person_id}*")
271
+ st.markdown(f"""
272
+ * **Name:** {patient_name}
273
+ * **Status:** {patient_status}
274
+ * **Phone:** {patient_phone}
275
+ * **Address:** {patient_address}
276
+ * **Email:** {patient_email}
277
+ * **DOB:** {patient_dob}
278
+ * **Gender:** {patient_gender}
279
+ * **Preferred Language:** {patient_pref_lang}
280
+ * **Marital Status:** {patient_marital_status}
281
+ * **Contact Person Name:** {contact_person_name}
282
+ * **Contact Person Phone:** {contact_person_phone}
283
+ * **Contact Person Relationship:** {contact_person_relationship}
284
+ """)
285
+ if st.checkbox('Show JSON Response'):
286
+ st.json(st.session_state.patient_data)
287
+ if resource == 'Observation':
288
+ with title:
289
+ st.markdown('')
290
+ st.subheader('Observation Resource')
291
+ if st.session_state.observation_data is None:
292
+ st.markdown("No patient data available.")
293
+ else:
294
+ observation_data = st.session_state.observation_data
295
+ weight_with_date = []
296
+ bp_with_date = []
297
+ for i in observation_data.get('entry', []):
298
+ resource = i.get('resource', {})
299
+ status = resource.get('status', '')
300
+ if status == 'final':
301
+ category_list = resource.get('category', [])
302
+ for category in category_list:
303
+ category_coding = category.get('coding', [])
304
+ for coding in category_coding:
305
+ if coding.get('code', '') == 'vital-signs':
306
+ code_info = resource.get('code', {})
307
+ code_coding = code_info.get('coding', [])
308
+ for code in code_coding:
309
+ display = code.get('display', '')
310
+ if display.lower() in ['weight measured']:
311
+ value_quantity = resource.get('valueQuantity', {})
312
+ value = value_quantity.get('value', 'N/A')
313
+ unit = value_quantity.get('unit', '')
314
+ effective_date = resource.get('effectiveDateTime', 'N/A')
315
+ weight_with_date.append({
316
+ 'Value': value,
317
+ 'Unit': unit,
318
+ 'Date': effective_date
319
+ })
320
+ code_data = resource.get('code', {})
321
+ if any(coding.get('code', '') == '85354-9' for coding in code_data.get('coding', [])):
322
+ effective_date_time = resource.get('effectiveDateTime', 'N/A')
323
+
324
+ systolic_pressure = None
325
+ diastolic_pressure = None
326
+
327
+ for component in resource.get('component', []):
328
+ code_comp_data = component.get('code', {})
329
+
330
+ # Systolic blood pressure
331
+ if any(coding.get('code', '') in ['8460-8', '8480-6'] for coding in code_comp_data.get('coding', [])):
332
+ value_quantity = component.get('valueQuantity', {})
333
+ systolic_pressure = {
334
+ 'value': value_quantity.get('value', 'N/A'),
335
+ 'unit': value_quantity.get('unit', '')
336
+ }
337
+
338
+ # Diastolic blood pressure
339
+ if any(coding.get('code', '') in ['8454-1', '8462-4'] for coding in code_comp_data.get('coding', [])):
340
+ value_quantity = component.get('valueQuantity', {})
341
+ diastolic_pressure = {
342
+ 'value': value_quantity.get('value', 'N/A'),
343
+ 'unit': value_quantity.get('unit', '')
344
+ }
345
+
346
+ if systolic_pressure and diastolic_pressure:
347
+ bp_with_date.append({
348
+ 'Date': effective_date_time,
349
+ 'Systolic': systolic_pressure,
350
+ 'Diastolic': diastolic_pressure
351
+ })
352
+ st.markdown(f"*This data shows details in the observation endpoint of patient ID: {person_id}*")
353
+ if weight_with_date:
354
+ st.subheader("Weight")
355
+ for i in weight_with_date:
356
+ st.markdown(f"""
357
+ **Date:** {i['Date']}
358
+ * **Weight:** {i['Value']}{i['Unit']}
359
+ """)
360
+ if bp_with_date:
361
+ st.subheader("Blood Pressure")
362
+ for j in bp_with_date:
363
+ st.markdown(f"""
364
+ **Date:** {j['Date']}
365
+ * **Systolic:** {j['Systolic']['value']}{j['Systolic']['unit']}, **Diastolic:** {j['Diastolic']['value']}{j['Diastolic']['unit']}
366
+ """)
367
+ if st.checkbox('Show JSON Response'):
368
+ st.json(st.session_state.observation_data)
369
+
370
+ if __name__ == "__main__":
371
+ main()
pages/logo.jpg ADDED