Spaces:
Sleeping
Sleeping
Remove Cerner login page.
Browse files- pages/Cerner_LogIn.py +0 -371
pages/Cerner_LogIn.py
DELETED
@@ -1,371 +0,0 @@
|
|
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()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|