ibauth 0.0.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ibauth-0.0.2/PKG-INFO +47 -0
- ibauth-0.0.2/README.md +30 -0
- ibauth-0.0.2/ibauth.egg-info/PKG-INFO +47 -0
- ibauth-0.0.2/ibauth.egg-info/SOURCES.txt +11 -0
- ibauth-0.0.2/ibauth.egg-info/dependency_links.txt +1 -0
- ibauth-0.0.2/ibauth.egg-info/requires.txt +10 -0
- ibauth-0.0.2/ibauth.egg-info/top_level.txt +1 -0
- ibauth-0.0.2/ibkr_oauth_flow/__init__.py +276 -0
- ibauth-0.0.2/ibkr_oauth_flow/const.py +14 -0
- ibauth-0.0.2/ibkr_oauth_flow/logger.py +3 -0
- ibauth-0.0.2/ibkr_oauth_flow/util.py +50 -0
- ibauth-0.0.2/pyproject.toml +34 -0
- ibauth-0.0.2/setup.cfg +4 -0
ibauth-0.0.2/PKG-INFO
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ibauth
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: Interactive Brokers OAuth
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: bump2version>=1.0.1
|
|
8
|
+
Requires-Dist: cryptography>=44.0.2
|
|
9
|
+
Requires-Dist: curlify>=2.2.1
|
|
10
|
+
Requires-Dist: mypy>=1.15.0
|
|
11
|
+
Requires-Dist: pre-commit>=4.1.0
|
|
12
|
+
Requires-Dist: pyjwt>=2.10.1
|
|
13
|
+
Requires-Dist: pyyaml>=6.0.2
|
|
14
|
+
Requires-Dist: requests>=2.32.3
|
|
15
|
+
Requires-Dist: ruff>=0.9.9
|
|
16
|
+
Requires-Dist: tenacity>=9.0.0
|
|
17
|
+
|
|
18
|
+
# IBKR Authentication Workflow
|
|
19
|
+
|
|
20
|
+
Documentation for the IBKR Web API is [here](https://www.interactivebrokers.com/campus/ibkr-api-page/webapi-ref/).
|
|
21
|
+
|
|
22
|
+
1. Pull the repository.
|
|
23
|
+
2. Create and activate a virtual environment.
|
|
24
|
+
3. Install dependencies from `requirements.txt`.
|
|
25
|
+
4. Create a YAML configuration file:
|
|
26
|
+
|
|
27
|
+
```yaml
|
|
28
|
+
client_id: ""
|
|
29
|
+
client_key_id: ""
|
|
30
|
+
credential: ""
|
|
31
|
+
private_key_file: ""
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The private key file will usually have a `.pem` extension.
|
|
35
|
+
5. Run the test script.
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
python auth.py
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
You can install from GitHub.
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip3 install git+https://github.com/datawookie/ibkr-oauth-flow
|
|
47
|
+
```
|
ibauth-0.0.2/README.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# IBKR Authentication Workflow
|
|
2
|
+
|
|
3
|
+
Documentation for the IBKR Web API is [here](https://www.interactivebrokers.com/campus/ibkr-api-page/webapi-ref/).
|
|
4
|
+
|
|
5
|
+
1. Pull the repository.
|
|
6
|
+
2. Create and activate a virtual environment.
|
|
7
|
+
3. Install dependencies from `requirements.txt`.
|
|
8
|
+
4. Create a YAML configuration file:
|
|
9
|
+
|
|
10
|
+
```yaml
|
|
11
|
+
client_id: ""
|
|
12
|
+
client_key_id: ""
|
|
13
|
+
credential: ""
|
|
14
|
+
private_key_file: ""
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The private key file will usually have a `.pem` extension.
|
|
18
|
+
5. Run the test script.
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
python auth.py
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
You can install from GitHub.
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip3 install git+https://github.com/datawookie/ibkr-oauth-flow
|
|
30
|
+
```
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ibauth
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: Interactive Brokers OAuth
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: bump2version>=1.0.1
|
|
8
|
+
Requires-Dist: cryptography>=44.0.2
|
|
9
|
+
Requires-Dist: curlify>=2.2.1
|
|
10
|
+
Requires-Dist: mypy>=1.15.0
|
|
11
|
+
Requires-Dist: pre-commit>=4.1.0
|
|
12
|
+
Requires-Dist: pyjwt>=2.10.1
|
|
13
|
+
Requires-Dist: pyyaml>=6.0.2
|
|
14
|
+
Requires-Dist: requests>=2.32.3
|
|
15
|
+
Requires-Dist: ruff>=0.9.9
|
|
16
|
+
Requires-Dist: tenacity>=9.0.0
|
|
17
|
+
|
|
18
|
+
# IBKR Authentication Workflow
|
|
19
|
+
|
|
20
|
+
Documentation for the IBKR Web API is [here](https://www.interactivebrokers.com/campus/ibkr-api-page/webapi-ref/).
|
|
21
|
+
|
|
22
|
+
1. Pull the repository.
|
|
23
|
+
2. Create and activate a virtual environment.
|
|
24
|
+
3. Install dependencies from `requirements.txt`.
|
|
25
|
+
4. Create a YAML configuration file:
|
|
26
|
+
|
|
27
|
+
```yaml
|
|
28
|
+
client_id: ""
|
|
29
|
+
client_key_id: ""
|
|
30
|
+
credential: ""
|
|
31
|
+
private_key_file: ""
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The private key file will usually have a `.pem` extension.
|
|
35
|
+
5. Run the test script.
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
python auth.py
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
You can install from GitHub.
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip3 install git+https://github.com/datawookie/ibkr-oauth-flow
|
|
47
|
+
```
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
ibauth.egg-info/PKG-INFO
|
|
4
|
+
ibauth.egg-info/SOURCES.txt
|
|
5
|
+
ibauth.egg-info/dependency_links.txt
|
|
6
|
+
ibauth.egg-info/requires.txt
|
|
7
|
+
ibauth.egg-info/top_level.txt
|
|
8
|
+
ibkr_oauth_flow/__init__.py
|
|
9
|
+
ibkr_oauth_flow/const.py
|
|
10
|
+
ibkr_oauth_flow/logger.py
|
|
11
|
+
ibkr_oauth_flow/util.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ibkr_oauth_flow
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import math
|
|
2
|
+
from typing import Any
|
|
3
|
+
import time
|
|
4
|
+
import yaml
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from cryptography.hazmat.primitives import serialization
|
|
8
|
+
from tenacity import retry, stop_after_attempt, wait_exponential
|
|
9
|
+
|
|
10
|
+
from .const import GRANT_TYPE, CLIENT_ASSERTION_TYPE, SCOPE, VALID_DOMAINS
|
|
11
|
+
from .util import make_jws, get, post, ReadTimeout, HTTPError
|
|
12
|
+
|
|
13
|
+
from .logger import logger
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class IBKROAuthFlow:
|
|
17
|
+
def __init__(
|
|
18
|
+
self, client_id: str, client_key_id: str, credential: str, private_key_file: str, domain: str = "api.ibkr.com"
|
|
19
|
+
):
|
|
20
|
+
if not client_id:
|
|
21
|
+
raise ValueError("Required parameter 'client_id' is missing.")
|
|
22
|
+
|
|
23
|
+
if not client_key_id:
|
|
24
|
+
raise ValueError("Required parameter 'client_key_id' is missing.")
|
|
25
|
+
|
|
26
|
+
if not credential:
|
|
27
|
+
raise ValueError("Required parameter 'credential' is missing.")
|
|
28
|
+
|
|
29
|
+
if not private_key_file:
|
|
30
|
+
raise ValueError("Required parameter 'private_key_file' is missing.")
|
|
31
|
+
|
|
32
|
+
if domain not in VALID_DOMAINS:
|
|
33
|
+
raise ValueError(f"Invalid domain: {domain}.")
|
|
34
|
+
else:
|
|
35
|
+
self.domain = domain
|
|
36
|
+
|
|
37
|
+
self.client_id = client_id
|
|
38
|
+
self.client_key_id = client_key_id
|
|
39
|
+
self.credential = credential
|
|
40
|
+
|
|
41
|
+
logger.info(f"Load private key from {private_key_file}.")
|
|
42
|
+
with open(private_key_file, "r") as file:
|
|
43
|
+
self.private_key = serialization.load_pem_private_key(
|
|
44
|
+
file.read().encode(),
|
|
45
|
+
password=None,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
self.access_token = None
|
|
49
|
+
self.bearer_token = None
|
|
50
|
+
|
|
51
|
+
# These fields are set in the tickle() method.
|
|
52
|
+
#
|
|
53
|
+
self.authenticated = None
|
|
54
|
+
self.connected = None
|
|
55
|
+
self.competing = None
|
|
56
|
+
|
|
57
|
+
self.IP = None
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def url_oauth2(self) -> str:
|
|
61
|
+
return f"https://{self.domain}/oauth2"
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def url_gateway(self) -> str:
|
|
65
|
+
return f"https://{self.domain}/gw"
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def url_client_portal(self) -> str:
|
|
69
|
+
return f"https://{self.domain}"
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def domain(self) -> str:
|
|
73
|
+
return self._domain
|
|
74
|
+
|
|
75
|
+
@domain.setter
|
|
76
|
+
def domain(self, value: str) -> None:
|
|
77
|
+
"""
|
|
78
|
+
Set and validate the domain.
|
|
79
|
+
"""
|
|
80
|
+
if value not in VALID_DOMAINS:
|
|
81
|
+
raise ValueError(f"Invalid domain: {value}. Must be one of {VALID_DOMAINS}.")
|
|
82
|
+
logger.info(f"Domain: {value}")
|
|
83
|
+
self._domain = value
|
|
84
|
+
|
|
85
|
+
def _check_ip(self) -> Any:
|
|
86
|
+
"""
|
|
87
|
+
Get public IP address.
|
|
88
|
+
"""
|
|
89
|
+
logger.debug("Check public IP.")
|
|
90
|
+
IP = get("https://api.ipify.org", timeout=10).content.decode("utf8")
|
|
91
|
+
|
|
92
|
+
logger.info(f"Public IP: {IP}.")
|
|
93
|
+
if self.IP and self.IP != IP:
|
|
94
|
+
logger.warning("🚨 Public IP has changed.")
|
|
95
|
+
|
|
96
|
+
self.IP = IP
|
|
97
|
+
return IP
|
|
98
|
+
|
|
99
|
+
def _compute_client_assertion(self, url: str) -> Any:
|
|
100
|
+
now = math.floor(time.time())
|
|
101
|
+
header = {"alg": "RS256", "typ": "JWT", "kid": f"{self.client_key_id}"}
|
|
102
|
+
|
|
103
|
+
if url == f"{self.url_oauth2}/api/v1/token":
|
|
104
|
+
claims = {
|
|
105
|
+
"iss": f"{self.client_id}",
|
|
106
|
+
"sub": f"{self.client_id}",
|
|
107
|
+
"aud": "/token",
|
|
108
|
+
"exp": now + 20,
|
|
109
|
+
"iat": now - 10,
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
elif url == f"{self.url_gateway}/api/v1/sso-sessions":
|
|
113
|
+
claims = {
|
|
114
|
+
"ip": self.IP,
|
|
115
|
+
"credential": f"{self.credential}",
|
|
116
|
+
"iss": f"{self.client_id}",
|
|
117
|
+
"exp": now + 86400,
|
|
118
|
+
"iat": now,
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
logger.debug(f"Header: {header}.")
|
|
122
|
+
logger.debug(f"Claims: {claims}.")
|
|
123
|
+
|
|
124
|
+
return make_jws(header, claims, self.private_key)
|
|
125
|
+
|
|
126
|
+
def get_access_token(self) -> None:
|
|
127
|
+
"""
|
|
128
|
+
Obtain an access token. This is the first step in the authentication
|
|
129
|
+
flow.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
str: The access token.
|
|
133
|
+
"""
|
|
134
|
+
url = f"{self.url_oauth2}/api/v1/token"
|
|
135
|
+
|
|
136
|
+
headers = {"Content-Type": "application/x-www-form-urlencoded"}
|
|
137
|
+
|
|
138
|
+
form_data = {
|
|
139
|
+
"grant_type": GRANT_TYPE,
|
|
140
|
+
"client_assertion": self._compute_client_assertion(url),
|
|
141
|
+
"client_assertion_type": CLIENT_ASSERTION_TYPE,
|
|
142
|
+
"scope": SCOPE,
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
logger.info("Request access token.")
|
|
146
|
+
response = post(url=url, headers=headers, data=form_data)
|
|
147
|
+
|
|
148
|
+
self.access_token = response.json()["access_token"]
|
|
149
|
+
|
|
150
|
+
def get_bearer_token(self) -> None:
|
|
151
|
+
url = f"{self.url_gateway}/api/v1/sso-sessions"
|
|
152
|
+
|
|
153
|
+
headers = {
|
|
154
|
+
"Authorization": "Bearer " + self.access_token, # type: ignore
|
|
155
|
+
"Content-Type": "application/jwt",
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
# Initialise IP (it's embedded in the bearer token).
|
|
159
|
+
self._check_ip()
|
|
160
|
+
|
|
161
|
+
logger.info("Request bearer token.")
|
|
162
|
+
response = post(url=url, headers=headers, data=self._compute_client_assertion(url))
|
|
163
|
+
|
|
164
|
+
self.bearer_token = response.json()["access_token"]
|
|
165
|
+
|
|
166
|
+
@retry(stop=stop_after_attempt(5), wait=wait_exponential(multiplier=5)) # type: ignore
|
|
167
|
+
def ssodh_init(self) -> None:
|
|
168
|
+
"""
|
|
169
|
+
Initialise a brokerage session.
|
|
170
|
+
"""
|
|
171
|
+
url = f"{self.url_client_portal}/v1/api/iserver/auth/ssodh/init"
|
|
172
|
+
|
|
173
|
+
headers = {
|
|
174
|
+
"Authorization": "Bearer " + self.bearer_token, # type: ignore
|
|
175
|
+
"User-Agent": "python/3.11",
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
logger.info("Initiate a brokerage session.")
|
|
179
|
+
try:
|
|
180
|
+
response = post(url=url, headers=headers, json={"publish": True, "compete": True})
|
|
181
|
+
except HTTPError:
|
|
182
|
+
logger.error("â›” Error initiating a brokerage session.")
|
|
183
|
+
raise
|
|
184
|
+
|
|
185
|
+
logger.debug(f"Response content: {response.json()}.")
|
|
186
|
+
|
|
187
|
+
def validate_sso(self) -> None:
|
|
188
|
+
url = f"{self.url_client_portal}/v1/api/sso/validate"
|
|
189
|
+
|
|
190
|
+
headers = {
|
|
191
|
+
"Authorization": "Bearer " + self.bearer_token, # type: ignore
|
|
192
|
+
"User-Agent": "python/3.11",
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
logger.info("Validate brokerage session.")
|
|
196
|
+
response = get(url=url, headers=headers)
|
|
197
|
+
|
|
198
|
+
logger.debug(f"Response content: {response.json()}.")
|
|
199
|
+
|
|
200
|
+
@retry(stop=stop_after_attempt(5), wait=wait_exponential(multiplier=5)) # type: ignore
|
|
201
|
+
def tickle(self) -> str:
|
|
202
|
+
"""
|
|
203
|
+
Keeps session alive.
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
Session ID.
|
|
207
|
+
"""
|
|
208
|
+
url = f"{self.url_client_portal}/v1/api/tickle"
|
|
209
|
+
|
|
210
|
+
headers = {
|
|
211
|
+
"Authorization": "Bearer " + self.bearer_token, # type: ignore
|
|
212
|
+
"User-Agent": "python/3.11",
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
logger.info("Send tickle.")
|
|
216
|
+
try:
|
|
217
|
+
response = get(url=url, headers=headers, timeout=10)
|
|
218
|
+
except (HTTPError, ReadTimeout):
|
|
219
|
+
logger.error("â›” Error connecting to session.")
|
|
220
|
+
self.get_bearer_token()
|
|
221
|
+
self.ssodh_init()
|
|
222
|
+
raise
|
|
223
|
+
|
|
224
|
+
self.session_id: str = response.json()["session"]
|
|
225
|
+
auth_status = response.json()["iserver"]["authStatus"]
|
|
226
|
+
self.authenticated = auth_status["authenticated"]
|
|
227
|
+
self.competing = auth_status["competing"]
|
|
228
|
+
self.connected = auth_status["connected"]
|
|
229
|
+
|
|
230
|
+
logger.debug(f"Session ID: {self.session_id}")
|
|
231
|
+
logger.debug(f"- authenticated: {self.authenticated}")
|
|
232
|
+
logger.debug(f"- competing: {self.competing}")
|
|
233
|
+
logger.debug(f"- connected: {self.connected}")
|
|
234
|
+
|
|
235
|
+
logger.debug(f"Response content: {response.json()}.")
|
|
236
|
+
|
|
237
|
+
return self.session_id
|
|
238
|
+
|
|
239
|
+
def logout(self) -> None:
|
|
240
|
+
url = f"{self.url_client_portal}/v1/api/logout"
|
|
241
|
+
|
|
242
|
+
if self.bearer_token is None:
|
|
243
|
+
logger.warning("🚨 Not terminating brokerage session (no bearer token found).")
|
|
244
|
+
return
|
|
245
|
+
|
|
246
|
+
headers = {
|
|
247
|
+
"Authorization": "Bearer " + self.bearer_token,
|
|
248
|
+
"User-Agent": "python/3.11",
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
logger.info("Terminate brokerage session.")
|
|
252
|
+
post(url=url, headers=headers)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def auth_from_yaml(path: str) -> IBKROAuthFlow:
|
|
256
|
+
"""
|
|
257
|
+
Create an IBKROAuthFlow instance from a YAML configuration file.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
path (str): The path to the YAML configuration file.
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
IBKROAuthFlow: An instance of IBKROAuthFlow.
|
|
264
|
+
"""
|
|
265
|
+
path_absolute = Path(path).resolve()
|
|
266
|
+
logger.info(f"Load configuration from {path_absolute}.")
|
|
267
|
+
with open(path_absolute, "r") as file:
|
|
268
|
+
config = yaml.safe_load(file)
|
|
269
|
+
|
|
270
|
+
return IBKROAuthFlow(
|
|
271
|
+
client_id=config["client_id"],
|
|
272
|
+
client_key_id=config["client_key_id"],
|
|
273
|
+
credential=config["credential"],
|
|
274
|
+
private_key_file=config["private_key_file"],
|
|
275
|
+
domain=config.get("domain", "api.ibkr.com"),
|
|
276
|
+
)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
CLIENT_ASSERTION_TYPE = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
|
|
2
|
+
SCOPE = "sso-sessions.write"
|
|
3
|
+
GRANT_TYPE = "client_credentials"
|
|
4
|
+
|
|
5
|
+
VALID_DOMAINS = [
|
|
6
|
+
"api.ibkr.com",
|
|
7
|
+
"1.api.ibkr.com",
|
|
8
|
+
"2.api.ibkr.com",
|
|
9
|
+
"3.api.ibkr.com",
|
|
10
|
+
"4.api.ibkr.com",
|
|
11
|
+
"5.api.ibkr.com",
|
|
12
|
+
"6.api.ibkr.com",
|
|
13
|
+
"7.api.ibkr.com",
|
|
14
|
+
]
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import jwt
|
|
3
|
+
import curlify
|
|
4
|
+
import requests
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from requests import Response
|
|
8
|
+
from requests.exceptions import HTTPError, ReadTimeout # noqa
|
|
9
|
+
|
|
10
|
+
from .logger import logger
|
|
11
|
+
|
|
12
|
+
__all__ = ["ReadTimeout", "HTTPError"]
|
|
13
|
+
|
|
14
|
+
from .const import *
|
|
15
|
+
|
|
16
|
+
RESP_HEADERS_TO_PRINT = ["Cookie", "Cache-Control", "Content-Type", "Host"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def log_response(response: Response) -> None:
|
|
20
|
+
logger.debug(f"Request: {curlify.to_curl(response.request)}")
|
|
21
|
+
logger.debug(f"Response: {response.status_code} {response.text}")
|
|
22
|
+
response.raise_for_status()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get(url: str, headers: dict[str, str] | None = None, timeout: float | None = None) -> requests.Response:
|
|
26
|
+
logger.debug(f"🔄 GET {url}")
|
|
27
|
+
response = requests.get(url, headers=headers, timeout=timeout)
|
|
28
|
+
log_response(response)
|
|
29
|
+
return response
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def post(
|
|
33
|
+
url: str,
|
|
34
|
+
data: dict[str, Any] | None = None,
|
|
35
|
+
json: dict[str, Any] | None = None,
|
|
36
|
+
headers: dict[str, str] | None = None,
|
|
37
|
+
timeout: float | None = None,
|
|
38
|
+
) -> requests.Response:
|
|
39
|
+
logger.debug(f"🔄 POST {url}")
|
|
40
|
+
response = requests.post(url, data=data, json=json, headers=headers, timeout=timeout)
|
|
41
|
+
log_response(response)
|
|
42
|
+
return response
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def make_jws(header: dict[str, Any], claims: dict[str, Any], clientPrivateKey: Any) -> Any:
|
|
46
|
+
# Set expiration time.
|
|
47
|
+
claims["exp"] = int(time.time()) + 600
|
|
48
|
+
claims["iat"] = int(time.time())
|
|
49
|
+
|
|
50
|
+
return jwt.encode(claims, clientPrivateKey, algorithm="RS256", headers=header)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "ibauth"
|
|
3
|
+
version = "0.0.2"
|
|
4
|
+
description = "Interactive Brokers OAuth"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"bump2version>=1.0.1",
|
|
9
|
+
"cryptography>=44.0.2",
|
|
10
|
+
"curlify>=2.2.1",
|
|
11
|
+
"mypy>=1.15.0",
|
|
12
|
+
"pre-commit>=4.1.0",
|
|
13
|
+
"pyjwt>=2.10.1",
|
|
14
|
+
"pyyaml>=6.0.2",
|
|
15
|
+
"requests>=2.32.3",
|
|
16
|
+
"ruff>=0.9.9",
|
|
17
|
+
"tenacity>=9.0.0",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[build-system]
|
|
21
|
+
requires = ["setuptools"]
|
|
22
|
+
|
|
23
|
+
[tool.mypy]
|
|
24
|
+
ignore_missing_imports = false
|
|
25
|
+
|
|
26
|
+
[[tool.mypy.overrides]]
|
|
27
|
+
module = ["requests", "requests.exceptions", "curlify", "yaml"]
|
|
28
|
+
ignore_missing_imports = true
|
|
29
|
+
|
|
30
|
+
[tool.ruff]
|
|
31
|
+
line-length = 120
|
|
32
|
+
|
|
33
|
+
[tool.ruff.lint]
|
|
34
|
+
select = ["I", "F"]
|
ibauth-0.0.2/setup.cfg
ADDED