Skip to content

Commit 31c0cfb

Browse files
author
James Fuqian
authored
Merge pull request #15 from CMSgov/jfuqian/BB2-1366-Python-SDK-pagination-support
[BB2-1366] Python SDK Pagination Support
2 parents 2276bd4 + 3fe551c commit 31c0cfb

13 files changed

Lines changed: 72972 additions & 30 deletions

README.md

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,18 +37,27 @@ The configuration parameters are:
3737
- The version number of the API
3838
- The app's environment (the BB2.0 web location where the app is registered)
3939
- The FHIR call retry settings
40+
- Enable / Disable Token refresh if FHIR request processing detected the access token expired
4041

41-
| Parameter | Value | Comments |
42-
| ------------ | ----------------------- | ------------------------------- |
43-
| environment | "SANDBOX" or "PRODUCTION" | BB2 API environment (default="SANDBOX")
44-
| version | 1 or 2 | BB2 API version (default=2) |
45-
| client_id | "foo" | oauth2 client id of the app |
46-
| client_secret | "bar" | oauth2 client secret of the app |
47-
| callback_url | "https://www.fake.com/callback" | oauth2 callback URL of the app |
42+
| Parameter | Value | Comments |
43+
| ------------- | ------------------------------- | --------------------------------------- |
44+
| environment | "SANDBOX" or "PRODUCTION" | BB2 API environment (default="SANDBOX") |
45+
| version | 1 or 2 | BB2 API version (default=2) |
46+
| client_id | "foo" | oauth2 client id of the app |
47+
| client_secret | "bar" | oauth2 client secret of the app |
48+
| callback_url | "https://www.fake.com/callback" | oauth2 callback URL of the app |
4849

4950
For application registration and client id and client secret, please refer to:
5051
[Blue Button 2.0 API Docs - Try the API](https://bluebutton.cms.gov/developers/#try-the-api)
5152

53+
Auth Token Refresh on Expire:
54+
55+
SDK FHIR requests will check if the access token is expired before the data end point call, if the access token is expired, then a token refresh is performed with the refresh token in the current auth token object, this behavior can be disabled by setting configuration parameter as below:
56+
57+
"token_refresh_on_expire": false
58+
59+
By default, token_refresh_on_expire is true.
60+
5261
FHIR requests retry:
5362

5463
Retry is enabled by default for FHIR requests, retry_settings: parameters for exponential back off retry algorithm
@@ -178,7 +187,7 @@ def authorization_callback():
178187
Check the scope
179188
of the current access token as shown below:
180189
"""
181-
scopes = auth_token.scope;
190+
scopes = auth_token.scope
182191

183192
# iterate scope entries here or check if a permission is in the scope
184193
if "patient/Patient.read" in scopes:
@@ -213,6 +222,22 @@ def authorization_callback():
213222
try:
214223
eob_data = bb.get_explaination_of_benefit_data(config)
215224
result['eob_data'] = eob_data['response'].json()
225+
eob_data = eob_data['response'].json()
226+
result['eob_data'] = eob_data
227+
228+
# fhir search response can contain large number of resources,
229+
# e.g. it is not unusual an EOB search of a beneficiary would result
230+
# in hundreds of EOB resources, by default they are chunked into pages
231+
# of 10 resources each, e.g. the above call bb.get_explaination_of_benefit_data(config)
232+
# return the 1st page of EOBs, in the format of a FHIR bundle resource
233+
# with a link section where page navigation urls with the link name as:
234+
# 'first', 'last', 'self', 'next', 'previous', which indicating the
235+
# pagination relation relative to the current page.
236+
237+
# Use bb.get_pages(data, config) to get all the pages
238+
eob_pages = bb.get_pages(eob_data, config)
239+
result['eob_pages'] = eob_pages['pages']
240+
auth_token = eob_pages['auth_token']
216241

217242
pt_data = bb.get_patient_data(config)
218243
result['patient_data'] = pt_data['response'].json()

cms_bluebutton/cms_bluebutton.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ def __init__(self, config=DEFAULT_CONFIG_FILE_LOCATION):
2828
self.client_secret = None
2929
self.callback_url = None
3030
self.version = 2 # Default to BB2 version 2
31+
self.token_refresh_on_expire = True
3132
# initilized with default
3233
self.retry_config = {"total": 3,
3334
"backoff_factor": 5,
@@ -92,6 +93,7 @@ def set_configuration(self, config):
9293
self.version = config_dict.get("version", 2)
9394
self.auth_base_url = "{}/v{}/o/authorize".format(self.base_url, self.version)
9495
self.auth_token_url = "{}/v{}/o/token/".format(self.base_url, self.version)
96+
self.token_refresh_on_expire = config_dict.get("token_refresh_on_expire", True)
9597
retrycfg = config_dict.get("retry_settings")
9698
if retrycfg:
9799
# override default with normalization
@@ -115,6 +117,30 @@ def get_profile_data(self, config):
115117
config["url"] = FHIR_RESOURCE_TYPE["Profile"]
116118
return fhir_request(self, config)
117119

120+
def extract_page_nav_url(self, data, relation):
121+
if data and data['resourceType'] == "Bundle" and data['type'] == "searchset" and data['link']:
122+
for lnk in data['link']:
123+
if lnk['relation'] == relation:
124+
return lnk['url']
125+
return None
126+
127+
def extract_next_page_url(self, data):
128+
return self.extract_page_nav_url(data, 'next')
129+
130+
def get_pages(self, data, config):
131+
bundle = data
132+
pages = [bundle]
133+
page_url = self.extract_next_page_url(bundle)
134+
auth_token = config["auth_token"]
135+
while page_url:
136+
config["url"] = page_url
137+
next_page = fhir_request(self, config)
138+
bundle = next_page['response'].json()
139+
auth_token = next_page["auth_token"]
140+
pages.append(bundle)
141+
page_url = self.extract_next_page_url(bundle)
142+
return {"auth_token": auth_token, "pages": pages}
143+
118144
def get_custom_data(self, config):
119145
return fhir_request(self, config)
120146

cms_bluebutton/fhir_request.py

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,30 +6,37 @@
66

77
def fhir_request(bb, config):
88
auth_token = config["auth_token"]
9-
new_auth_token = handle_expired(bb, auth_token)
9+
if bb.token_refresh_on_expire:
10+
auth_token = handle_expired(bb, auth_token)
1011

11-
if new_auth_token is not None:
12-
auth_token = new_auth_token
12+
url_param = config["url"]
13+
full_url = None
14+
15+
if url_param.startswith(bb.base_url):
16+
# allow full url passed in from config as long as it roots from base url
17+
full_url = url_param
18+
else:
19+
full_url = "{}/v{}/{}".format(bb.base_url, bb.version, config["url"])
1320

14-
retry_config = Retry(
15-
total=bb.retry_config.get("total"),
16-
backoff_factor=bb.retry_config.get("backoff_factor"),
17-
status_forcelist=bb.retry_config.get("status_forcelist")
18-
)
19-
full_url = "{}/v{}/{}".format(bb.base_url, bb.version, config["url"])
2021
headers = SDK_HEADERS
2122
headers["Authorization"] = "Bearer " + auth_token.access_token
22-
adapter = HTTPAdapter(max_retries=retry_config)
23+
24+
adapter = HTTPAdapter()
25+
26+
if bb.retry_config.get("total") > 0:
27+
adapter = HTTPAdapter(max_retries=Retry(
28+
total=bb.retry_config.get("total"),
29+
backoff_factor=bb.retry_config.get("backoff_factor"),
30+
status_forcelist=bb.retry_config.get("status_forcelist")
31+
))
32+
2333
sesh = requests.Session()
2434
sesh.mount("https://", adapter)
2535
sesh.mount("http://", adapter)
2636
response = sesh.get(url=full_url, params=config["params"], headers=headers)
2737

28-
return {"auth_token": new_auth_token, "response": response}
38+
return {"auth_token": auth_token, "response": response}
2939

3040

3141
def handle_expired(bb, auth_token):
32-
if auth_token.access_token_expired():
33-
return refresh_auth_token(bb, auth_token)
34-
else:
35-
return None
42+
return refresh_auth_token(bb, auth_token) if auth_token.access_token_expired() else auth_token

0 commit comments

Comments
 (0)