Skip to content

Commit 31fb283

Browse files
authored
feat(request): request retry config option (#77)
1 parent bee2790 commit 31fb283

4 files changed

Lines changed: 97 additions & 44 deletions

File tree

.github/workflows/ci.yml

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ name: CI
22

33
on:
44
pull_request:
5-
branches: [main]
5+
branches: [ main ]
66
push:
7-
branches: [main]
7+
branches: [ main ]
88

99
jobs:
1010
build:
@@ -18,7 +18,7 @@ jobs:
1818
- name: Setup Python
1919
uses: actions/setup-python@v5
2020
with:
21-
python-version: "3.9"
21+
python-version: "3.11"
2222

2323
- name: Install Poetry
2424
uses: snok/install-poetry@v1
@@ -43,9 +43,10 @@ jobs:
4343
name: Test
4444
runs-on: ubuntu-20.04
4545
strategy:
46+
fail-fast: false
4647
matrix:
47-
python-version: ["3.8", "3.9", "3.10", "3.11"]
48-
poetry-version: ["1.6.1"]
48+
python-version: [ "3.8", "3.9", "3.10", "3.11", "3.12" ]
49+
poetry-version: [ "1.8.2" ]
4950
include:
5051
- python-version: "3.7"
5152
poetry-version: "1.5.1"

README.md

Lines changed: 62 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,20 @@
55
[![PyPi](https://img.shields.io/pypi/v/fds.sdk.utils)](https://pypi.org/project/fds.sdk.utils/)
66
[![Apache-2 license](https://img.shields.io/badge/license-Apache2-brightgreen.svg)](https://www.apache.org/licenses/LICENSE-2.0)
77

8-
This repository contains a collection of utilities that supports FactSet's SDK in Python and facilitate usage of FactSet APIs.
8+
This repository contains a collection of utilities that supports FactSet's SDK in Python and facilitate usage of FactSet
9+
APIs.
910

1011
## Installation
1112

1213
### Poetry
1314

14-
```python
15+
```sh
1516
poetry add fds.sdk.utils
1617
```
1718

1819
### pip
1920

20-
```python
21+
```sh
2122
pip install fds.sdk.utils
2223
```
2324

@@ -29,7 +30,8 @@ This library contains multiple modules, sample usage of each module is below.
2930

3031
First, you need to create the OAuth 2.0 client configuration that will be used to authenticate against FactSet's APIs:
3132

32-
1. [Create a new application](https://developer.factset.com/learn/authentication-oauth2#creating-an-application) on FactSet's Developer Portal.
33+
1. [Create a new application](https://developer.factset.com/learn/authentication-oauth2#creating-an-application) on
34+
FactSet's Developer Portal.
3335
2. When prompted, download the configuration file and move it to your development environment.
3436

3537
```python
@@ -61,7 +63,7 @@ from fds.sdk.utils.authentication import ConfidentialClient
6163

6264
client = ConfidentialClient(
6365
config_path='/path/to/config.json',
64-
proxy= "http://secret:password@localhost:5050",
66+
proxy="http://secret:password@localhost:5050",
6567
proxy_headers={
6668
"Custom-Proxy-Header": "Custom-Proxy-Header-Value"
6769
}
@@ -79,7 +81,6 @@ With `verify_ssl` it is possible to disable the verifications of certificates.
7981
Disabling the verification is not recommended, but it might be useful during
8082
local development or testing
8183

82-
8384
```python
8485
from fds.sdk.utils.authentication import ConfidentialClient
8586

@@ -90,55 +91,86 @@ client = ConfidentialClient(
9091
)
9192
```
9293

94+
### Request Retries
95+
96+
In case the request retry behaviour should be customized, it is possible to pass a `urllib3.Retry` object to
97+
the `ConfidentialClient`.
98+
99+
```python
100+
from urllib3 import Retry
101+
from fds.sdk.utils.authentication import ConfidentialClient
102+
103+
client = ConfidentialClient(
104+
config_path='/path/to/config.json',
105+
retry=Retry(
106+
total=5,
107+
backoff_factor=0.1,
108+
status_forcelist=[500, 502, 503, 504]
109+
)
110+
)
111+
```
112+
93113
## Modules
94114

95115
Information about the various utility modules contained in this library can be found below.
96116

97117
### Authentication
98118

99-
The [authentication module](src/fds/sdk/utils/authentication) provides helper classes that facilitate [OAuth 2.0](https://developer.factset.com/learn/authentication-oauth2) authentication and authorization with FactSet's APIs. Currently the module has support for the [client credentials flow](https://github.com/factset/oauth2-guidelines#client-credentials-flow-1).
119+
The [authentication module](src/fds/sdk/utils/authentication) provides helper classes that
120+
facilitate [OAuth 2.0](https://developer.factset.com/learn/authentication-oauth2) authentication and authorization with
121+
FactSet's APIs. Currently the module has support for
122+
the [client credentials flow](https://github.com/factset/oauth2-guidelines#client-credentials-flow-1).
100123

101124
Each helper class in the module has the following features:
102125

103-
* Accepts a configuration file or `dict` that contains information about the OAuth 2.0 client, including the client ID and private key.
126+
* Accepts a configuration file or `dict` that contains information about the OAuth 2.0 client, including the client ID
127+
and private key.
104128
* Performs authentication with FactSet's OAuth 2.0 authorization server and retrieves an access token.
105129
* Caches the access token for reuse and requests a new access token as needed when one expires.
106130
* In order for this to work correctly, the helper class instance should be reused in production environments.
107131

108132
#### Configuration
109133

110-
Classes in the authentication module require OAuth 2.0 client configuration information to be passed to constructors through a JSON-formatted file or a `dict`. In either case the format is the same:
134+
Classes in the authentication module require OAuth 2.0 client configuration information to be passed to constructors
135+
through a JSON-formatted file or a `dict`. In either case the format is the same:
111136

112137
```json
113138
{
114-
"name": "Application name registered with FactSet's Developer Portal",
115-
"clientId": "OAuth 2.0 Client ID registered with FactSet's Developer Portal",
116-
"clientAuthType": "Confidential",
117-
"owners": ["USERNAME-SERIAL"],
118-
"jwk": {
119-
"kty": "RSA",
120-
"use": "sig",
121-
"alg": "RS256",
122-
"kid": "Key ID",
123-
"d": "ECC Private Key",
124-
"n": "Modulus",
125-
"e": "Exponent",
126-
"p": "First Prime Factor",
127-
"q": "Second Prime Factor",
128-
"dp": "First Factor CRT Exponent",
129-
"dq": "Second Factor CRT Exponent",
130-
"qi": "First CRT Coefficient",
131-
}
139+
"name": "Application name registered with FactSet's Developer Portal",
140+
"clientId": "OAuth 2.0 Client ID registered with FactSet's Developer Portal",
141+
"clientAuthType": "Confidential",
142+
"owners": [
143+
"USERNAME-SERIAL"
144+
],
145+
"jwk": {
146+
"kty": "RSA",
147+
"use": "sig",
148+
"alg": "RS256",
149+
"kid": "Key ID",
150+
"d": "ECC Private Key",
151+
"n": "Modulus",
152+
"e": "Exponent",
153+
"p": "First Prime Factor",
154+
"q": "Second Prime Factor",
155+
"dp": "First Factor CRT Exponent",
156+
"dq": "Second Factor CRT Exponent",
157+
"qi": "First CRT Coefficient"
158+
}
132159
}
133160
```
134161

135-
If you're just starting out, you can visit FactSet's Developer Portal to [create a new application](https://developer.factset.com/applications) and download a configuration file in this format.
162+
If you're just starting out, you can visit FactSet's Developer Portal
163+
to [create a new application](https://developer.factset.com/applications) and download a configuration file in this
164+
format.
136165

137-
If you're creating and managing your signing key pair yourself, see the required [JWK parameters](https://github.com/factset/oauth2-guidelines#jwk-parameters) for public-private key pairs.
166+
If you're creating and managing your signing key pair yourself, see the
167+
required [JWK parameters](https://github.com/factset/oauth2-guidelines#jwk-parameters) for public-private key pairs.
138168

139169
## Debugging
140170

141-
This library uses the [logging module](https://docs.python.org/3/howto/logging.html) to log various messages that will help you understand what it's doing. You can increase the log level to see additional debug information using standard conventions. For example:
171+
This library uses the [logging module](https://docs.python.org/3/howto/logging.html) to log various messages that will
172+
help you understand what it's doing. You can increase the log level to see additional debug information using standard
173+
conventions. For example:
142174

143175
```python
144176
logging.getLogger('fds.sdk.utils').setLevel(logging.DEBUG)

src/fds/sdk/utils/authentication/confidential.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66
import requests
77
from jose import JWSError, jws
88
from oauthlib.oauth2 import BackendApplicationClient
9+
from requests import Session
10+
from requests.adapters import HTTPAdapter
911
from requests_oauthlib import OAuth2Session
12+
from urllib3 import Retry
1013

1114
from .constants import CONSTS
1215
from .oauth2client import OAuth2Client
@@ -41,6 +44,7 @@ def __init__(
4144
proxy_headers: dict = None,
4245
verify_ssl: bool = True,
4346
ssl_ca_cert: str = None,
47+
retry: Retry = None,
4448
) -> None:
4549
"""
4650
Creates a new ConfidentialClient.
@@ -95,6 +99,8 @@ def __init__(
9599
`ssl_ca_cert` (str): Set this to customize the certificate file to verify the peer. If ``ssl_ca_cert`` is
96100
set, the ca_cert will be verified whether ``verify_ssl`` is enabled
97101
102+
`retry` (Retry): Set this to custommize the retry policy for the requests. If not set, the default is used.
103+
98104
99105
Raises:
100106
AuthServerMetadataError: Raised if there's an issue retrieving the authorization server metadata
@@ -131,10 +137,21 @@ def __init__(
131137
self._proxy_headers = proxy_headers
132138
self._ssl_ca_cert = ssl_ca_cert
133139

140+
if retry is not None:
141+
self._retry = retry
142+
else:
143+
self._retry = Retry(
144+
total=3,
145+
backoff_factor=1,
146+
status_forcelist=[413, 429, 500, 502, 503, 504],
147+
allowed_methods={"DELETE", "GET", "HEAD", "OPTIONS", "POST", "PUT", "TRACE"},
148+
)
149+
134150
try:
135151
self._oauth_session = OAuth2Session(
136152
client=BackendApplicationClient(client_id=self._config[CONSTS.CONFIG_CLIENT_ID])
137153
)
154+
self._oauth_session.mount("https://", HTTPAdapter(max_retries=self._retry))
138155
except Exception as e:
139156
raise ConfidentialClientError(
140157
f"Error instantiating OAuth2 session with {CONSTS.CONFIG_CLIENT_ID}:{self._config[CONSTS.CONFIG_CLIENT_ID]}"
@@ -152,6 +169,9 @@ def __init__(
152169

153170
log.debug("Credentials are complete and formatted correctly")
154171

172+
self._requests_session = Session()
173+
self._requests_session.mount("https://", HTTPAdapter(max_retries=self._retry))
174+
155175
self._init_auth_server_metadata()
156176

157177
self._cached_token = {}
@@ -167,7 +187,7 @@ def _init_auth_server_metadata(self) -> None:
167187
if self._ssl_ca_cert:
168188
verify = self._ssl_ca_cert
169189

170-
res = requests.get(
190+
res = self._requests_session.get(
171191
url=self._config[CONSTS.CONFIG_WELL_KNOWN_URI],
172192
proxies=self._proxy,
173193
verify=verify,

tests/fds/sdk/utils/authentication/test_confidential.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ def client(mocker, example_config):
4646
"fds.sdk.utils.authentication.confidential.OAuth2Session.fetch_token",
4747
return_value={"access_token": "test-token", "expires_at": 10},
4848
)
49-
mock_get = mocker.patch("fds.sdk.utils.authentication.confidential.requests.get")
49+
mock_get = mocker.patch("fds.sdk.utils.authentication.confidential.requests.Session.get")
5050
mock_get.return_value.json.return_value = {
5151
"issuer": "test-issuer",
5252
"token_endpoint": "https://test.token.endpoint",
@@ -75,7 +75,7 @@ def json(self):
7575
return {"issuer": "test", "token_endpoint": "http://test.test"}
7676

7777
caplog.set_level(logging.DEBUG)
78-
mocker.patch("requests.get", return_value=AuthServerMetadataRes())
78+
mocker.patch("requests.Session.get", return_value=AuthServerMetadataRes())
7979

8080
client = ConfidentialClient(config=example_config)
8181

@@ -104,7 +104,7 @@ def json(self):
104104
return {"issuer": "test", "token_endpoint": "http://test.test"}
105105

106106
caplog.set_level(logging.DEBUG)
107-
mocker.patch("requests.get", return_value=AuthServerMetadataRes())
107+
mocker.patch("requests.Session.get", return_value=AuthServerMetadataRes())
108108
mocker.patch("json.load", return_value=example_config)
109109
fake_file_path = "/my/fake/path/creds.json"
110110

@@ -189,7 +189,7 @@ class AuthServerMetadataRes:
189189
def json(self):
190190
return {"issuer": "test", "token_endpoint": "http://test.test"}
191191

192-
get_mock = mocker.patch("requests.get", return_value=AuthServerMetadataRes())
192+
get_mock = mocker.patch("requests.Session.get", return_value=AuthServerMetadataRes())
193193

194194
ConfidentialClient(config=example_config, **additional_parameters)
195195

@@ -218,7 +218,7 @@ class AuthServerMetadataRes:
218218
def json(self):
219219
return {"issuer": "test", "token_endpoint": "http://test.test"}
220220

221-
get_mock = mocker.patch("requests.get", return_value=AuthServerMetadataRes())
221+
get_mock = mocker.patch("requests.Session.get", return_value=AuthServerMetadataRes())
222222
auth_test = "https://auth.test"
223223

224224
example_config["wellKnownUri"] = auth_test
@@ -237,7 +237,7 @@ def test_constructor_metadata_error(mocker, example_config):
237237
"fds.sdk.utils.authentication.confidential.OAuth2Session.fetch_token",
238238
return_value={"access_token": "test", "expires_at": 10},
239239
)
240-
mocker.patch("requests.get", side_effect=Exception("error"))
240+
mocker.patch("requests.Session.get", side_effect=Exception("error"))
241241
with pytest.raises(AuthServerMetadataError):
242242
ConfidentialClient(config=example_config)
243243

@@ -256,7 +256,7 @@ class AuthServerMetadataRes:
256256
def json():
257257
return {}
258258

259-
mocker.patch("requests.get", return_value=AuthServerMetadataRes)
259+
mocker.patch("requests.Session.get", return_value=AuthServerMetadataRes)
260260

261261
with pytest.raises(AuthServerMetadataContentError):
262262
ConfidentialClient(config=example_config)
@@ -392,7 +392,7 @@ def test_get_access_token_fetch_error(client, mocker, caplog):
392392

393393
def test_get_access_token_cached(example_config, mocker, caplog):
394394
caplog.set_level(logging.DEBUG)
395-
mock_get = mocker.patch("fds.sdk.utils.authentication.confidential.requests.get")
395+
mock_get = mocker.patch("fds.sdk.utils.authentication.confidential.requests.Session.get")
396396
mock_get.return_value.json.return_value = {
397397
"issuer": "test-issuer",
398398
"token_endpoint": "https://test.token.endpoint",

0 commit comments

Comments
 (0)