Skip to content

Commit 2418aba

Browse files
author
JAMES FUQIAN
committed
python sdk expose retry parameters as configurable.
1 parent d79967c commit 2418aba

8 files changed

Lines changed: 118 additions & 24 deletions

File tree

README.md

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ The configuration parameters are:
3636
- The app's callback url
3737
- The version number of the API
3838
- The app's environment (the BB2.0 web location where the app is registered)
39+
- The FHIR call retry settings
3940

4041
| Parameter | Value | Comments |
4142
| ------------ | ----------------------- | ------------------------------- |
@@ -48,6 +49,23 @@ The configuration parameters are:
4849
For application registration and client id and client secret, please refer to:
4950
[Blue Button 2.0 API Docs - Try the API](https://bluebutton.cms.gov/developers/#try-the-api)
5051

52+
FHIR requests retry:
53+
54+
Retry is enabled by default for FHIR requests, retry_settings: parameters for exponential back off retry algorithm
55+
56+
| retry parameter | value (default) | Comments |
57+
| ----------------- | -------------------- | -------------------------------- |
58+
| backoff_factor | 5 | back off factor in seconds |
59+
| total | 3 | max retries |
60+
| status_forcelist | [500, 502, 503, 504] | error response codes to retry on |
61+
62+
the exponential back off factor (in seconds) is used to calculate interval between retries by below formular, where i starts from 0:
63+
64+
{backoff factor} * (2 ** ({i} - 1))
65+
66+
e.g. for backoff_factor is 5 seconds, it will generate wait intervals: 2.5, 5, 20, ...
67+
68+
to disable the retry: set total = 0
5169

5270
There are three ways to configure the SDK when instantiating a `BlueButton` class instance:
5371

@@ -62,6 +80,11 @@ There are three ways to configure the SDK when instantiating a `BlueButton` clas
6280
"client_secret": "bar",
6381
"callback_url": "https://www.fake.com/callback",
6482
"version": 2,
83+
"retry_settings": {
84+
"total": 3,
85+
"backoff_factor": 5,
86+
"status_forcelist": [500, 502, 503, 504]
87+
}
6588
}
6689
```
6790
* JSON config file:
@@ -79,7 +102,12 @@ There are three ways to configure the SDK when instantiating a `BlueButton` clas
79102
"client_id": "foo",
80103
"client_secret": "bar",
81104
"callback_url": "https://www.fake.com/callback",
82-
"version": 2
105+
"version": 2,
106+
"retry_settings": {
107+
"total": 3,
108+
"backoff_factor": 5,
109+
"status_forcelist": [500, 502, 503, 504]
110+
}
83111
}
84112
```
85113

cms_bluebutton/auth.py

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,7 @@ def set_dict(self, auth_token_dict):
4242
auth_token_dict.get("expires_at")
4343
).astimezone(datetime.timezone.utc)
4444
else:
45-
self.expires_at = datetime.datetime.now(datetime.timezone.utc)
46-
+datetime.timedelta(seconds=self.expires_in)
45+
self.expires_at = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(seconds=self.expires_in)
4746

4847
self.patient = auth_token_dict.get("patient")
4948
self.refresh_token = auth_token_dict.get("refresh_token")
@@ -57,16 +56,7 @@ def refresh_auth_token(bb, auth_token):
5756
"grant_type": "refresh_token",
5857
"refresh_token": auth_token.refresh_token,
5958
}
60-
61-
headers = SDK_HEADERS
62-
63-
token_response = requests.post(
64-
url=bb.auth_token_url,
65-
data=data,
66-
headers=headers,
67-
auth=(bb.client_id, bb.client_secret),
68-
)
69-
59+
token_response = _do_post(data, bb, (bb.client_id, bb.client_secret))
7060
token_response.raise_for_status()
7161
return AuthorizationToken(token_response.json())
7262

@@ -129,14 +119,7 @@ def get_access_token_from_code(bb, auth_data, callback_code):
129119
"code_challenge": auth_data["code_challenge"],
130120
}
131121

132-
mp_encoder = MultipartEncoder(data)
133-
headers = SDK_HEADERS
134-
headers["content-type"] = mp_encoder.content_type
135-
token_response = requests.post(
136-
url=bb.auth_token_url,
137-
data=mp_encoder,
138-
headers=headers
139-
)
122+
token_response = _do_post(data, bb, None)
140123
token_response.raise_for_status()
141124
token_dict = token_response.json()
142125
token_dict["expires_at"] = datetime.datetime.now(
@@ -157,3 +140,19 @@ def get_authorization_token(bb, auth_data, callback_code, callback_state):
157140
raise ValueError("Provided callback state does not match.")
158141

159142
return AuthorizationToken(get_access_token_from_code(bb, auth_data, callback_code))
143+
144+
145+
def _do_post(data, bb, auth):
146+
mp_encoder = MultipartEncoder(data)
147+
headers = SDK_HEADERS
148+
headers["content-type"] = mp_encoder.content_type
149+
return requests.post(
150+
url=bb.auth_token_url,
151+
data=mp_encoder,
152+
headers=headers
153+
) if not auth else requests.post(
154+
url=bb.auth_token_url,
155+
data=mp_encoder,
156+
headers=headers,
157+
auth=auth
158+
)

cms_bluebutton/cms_bluebutton.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ 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+
# initilized with default
32+
self.retry_config = {"total": 3,
33+
"backoff_factor": 5,
34+
"status_forcelist": [500, 502, 503, 504]}
3135

3236
self.base_url = None
3337

@@ -88,6 +92,12 @@ def set_configuration(self, config):
8892
self.version = config_dict.get("version", 2)
8993
self.auth_base_url = "{}/v{}/o/authorize".format(self.base_url, self.version)
9094
self.auth_token_url = "{}/v{}/o/token/".format(self.base_url, self.version)
95+
retrycfg = config_dict.get("retry_settings")
96+
if retrycfg:
97+
# override default with normalization
98+
self.retry_config["total"] = retrycfg.get("total", 3)
99+
self.retry_config["backoff_factor"] = retrycfg.get("backoff_factor", 5)
100+
self.retry_config["status_forcelist"] = retrycfg.get("status_forcelist", [500, 502, 503, 504])
91101

92102
def get_patient_data(self, config):
93103
config["url"] = FHIR_RESOURCE_TYPE["Patient"]

cms_bluebutton/fhir_request.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import requests
22
from requests.adapters import HTTPAdapter, Retry
3-
3+
from .auth import refresh_auth_token
44
from .constants import SDK_HEADERS
55

66

@@ -12,7 +12,9 @@ def fhir_request(bb, config):
1212
auth_token = new_auth_token
1313

1414
retry_config = Retry(
15-
total=3, backoff_factor=5, status_forcelist=[500, 502, 503, 504]
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")
1618
)
1719
full_url = "{}/v{}/{}".format(bb.base_url, bb.version, config["url"])
1820
headers = SDK_HEADERS
@@ -28,6 +30,6 @@ def fhir_request(bb, config):
2830

2931
def handle_expired(bb, auth_token):
3032
if auth_token.access_token_expired():
31-
return bb.refresh_auth_token(auth_token)
33+
return refresh_auth_token(bb, auth_token)
3234
else:
3335
return None

cms_bluebutton/tests/test_configs.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,17 @@ def test_valid_config():
5959
assert bb.client_secret == "<your BB2 client_secret here>"
6060
assert bb.callback_url == "https://www.fake-prod.com/your/callback/here"
6161
assert bb.version == 1
62+
assert bb.retry_config.get("total") == 3
63+
assert bb.retry_config.get("backoff_factor") == 5
64+
assert bb.retry_config.get("status_forcelist") == [500, 502, 503, 504]
65+
66+
67+
def test_valid_config_w_retry():
68+
# valid config sbx
69+
bb = BlueButton(config=CONFIGS_DIR + "json/bluebutton-sample-config-valid-w-retry.json")
70+
assert bb.retry_config.get("total") == 7
71+
assert bb.retry_config.get("backoff_factor") == 10
72+
assert bb.retry_config.get("status_forcelist") == [500, 502]
6273

6374

6475
def test_config_setting_environment():
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"environment": "PRODUCTION",
3+
"client_id": "<your BB2 client_id here>",
4+
"client_secret": "<your BB2 client_secret here>",
5+
"callback_url": "https://www.fake-prod.com/your/callback/here",
6+
"version": 1,
7+
"retry_settings": {
8+
"total": 7,
9+
"backoff_factor": 10,
10+
"status_forcelist": [500, 502]
11+
}
12+
}

requirements/req.dev.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
pyyaml==5.4.1
12
requests==2.27.1
23
requests-mock==1.9.3
34
requests-toolbelt==0.9.1

requirements/req.dev.txt

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,37 @@ pytest==7.0.1 \
103103
--hash=sha256:9ce3ff477af913ecf6321fe337b93a2c0dcf2a0a1439c43f5452112c1e4280db \
104104
--hash=sha256:e30905a0c131d3d94b89624a1cc5afec3e0ba2fbdb151867d8e0ebd49850f171
105105
# via -r requirements/req.dev.in
106+
pyyaml==5.4.1 \
107+
--hash=sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf \
108+
--hash=sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696 \
109+
--hash=sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393 \
110+
--hash=sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77 \
111+
--hash=sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922 \
112+
--hash=sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5 \
113+
--hash=sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8 \
114+
--hash=sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10 \
115+
--hash=sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc \
116+
--hash=sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018 \
117+
--hash=sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e \
118+
--hash=sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253 \
119+
--hash=sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347 \
120+
--hash=sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183 \
121+
--hash=sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541 \
122+
--hash=sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb \
123+
--hash=sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185 \
124+
--hash=sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc \
125+
--hash=sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db \
126+
--hash=sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa \
127+
--hash=sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46 \
128+
--hash=sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122 \
129+
--hash=sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b \
130+
--hash=sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63 \
131+
--hash=sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df \
132+
--hash=sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc \
133+
--hash=sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247 \
134+
--hash=sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6 \
135+
--hash=sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0
136+
# via -r requirements/req.dev.in
106137
requests==2.27.1 \
107138
--hash=sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61 \
108139
--hash=sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d

0 commit comments

Comments
 (0)