tradier-api-client 0.1.1__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.
- tradier_api_client-0.1.1/MANIFEST.in +7 -0
- tradier_api_client-0.1.1/PKG-INFO +203 -0
- tradier_api_client-0.1.1/README.md +191 -0
- tradier_api_client-0.1.1/pyproject.toml +27 -0
- tradier_api_client-0.1.1/setup.cfg +4 -0
- tradier_api_client-0.1.1/tradier_api_client/__init__.py +12 -0
- tradier_api_client-0.1.1/tradier_api_client/decorators.py +21 -0
- tradier_api_client-0.1.1/tradier_api_client/enc_dec.py +103 -0
- tradier_api_client-0.1.1/tradier_api_client/helper_functions.py +41 -0
- tradier_api_client-0.1.1/tradier_api_client/rest/__init__.py +6 -0
- tradier_api_client-0.1.1/tradier_api_client/rest/extensions/__init__.py +4 -0
- tradier_api_client-0.1.1/tradier_api_client/rest/extensions/options.py +454 -0
- tradier_api_client-0.1.1/tradier_api_client/rest/extensions/orders.py +475 -0
- tradier_api_client-0.1.1/tradier_api_client/rest/models/__init__.py +3 -0
- tradier_api_client-0.1.1/tradier_api_client/rest/models/orders.py +106 -0
- tradier_api_client-0.1.1/tradier_api_client/rest/models/orders_fixed.py +241 -0
- tradier_api_client-0.1.1/tradier_api_client/rest/rest_client.py +740 -0
- tradier_api_client-0.1.1/tradier_api_client/streaming/__init__.py +6 -0
- tradier_api_client-0.1.1/tradier_api_client/streaming/streaming_client.py +530 -0
- tradier_api_client-0.1.1/tradier_api_client/streaming/websocket_stream.py +136 -0
- tradier_api_client-0.1.1/tradier_api_client.egg-info/PKG-INFO +203 -0
- tradier_api_client-0.1.1/tradier_api_client.egg-info/SOURCES.txt +23 -0
- tradier_api_client-0.1.1/tradier_api_client.egg-info/dependency_links.txt +1 -0
- tradier_api_client-0.1.1/tradier_api_client.egg-info/requires.txt +3 -0
- tradier_api_client-0.1.1/tradier_api_client.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tradier-api-client
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Tradier REST + Streaming API client
|
|
5
|
+
Author: Your Name
|
|
6
|
+
License: Proprietary
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: requests>=2.25.1
|
|
10
|
+
Requires-Dist: cryptography>=41
|
|
11
|
+
Requires-Dist: websocket-client>=1.5
|
|
12
|
+
|
|
13
|
+
# tradier_api_client
|
|
14
|
+
|
|
15
|
+
Python client for the Tradier Brokerage API (REST) plus optional streaming helpers.
|
|
16
|
+
|
|
17
|
+
This repo is intended to give you a lightweight, script-friendly way to call Tradier endpoints (quotes, options, orders, account data) using a single `RestClient`.
|
|
18
|
+
|
|
19
|
+
> Note: Some examples below are adapted from `tests/real_data.py` which is intended for *local* use with a real API key. Don’t commit keys.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
From source (editable):
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
python -m pip install -U pip
|
|
29
|
+
python -m pip install -e .
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Regular install (from the cloned repo):
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
python -m pip install .
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Authentication
|
|
41
|
+
|
|
42
|
+
Set your Tradier API key in an environment variable.
|
|
43
|
+
|
|
44
|
+
### Windows (cmd.exe)
|
|
45
|
+
|
|
46
|
+
```bat
|
|
47
|
+
set API_KEY=your_key_here
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### macOS/Linux
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
export API_KEY=your_key_here
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Quick start
|
|
59
|
+
|
|
60
|
+
### Create a `RestClient`
|
|
61
|
+
|
|
62
|
+
The tests create a client like this:
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
import os
|
|
66
|
+
from tradier_api_client import RestClient
|
|
67
|
+
|
|
68
|
+
client = RestClient(
|
|
69
|
+
base_url="https://sandbox.tradier.com/v1", # use https://api.tradier.com/v1 for production
|
|
70
|
+
api_key=os.environ.get("API_KEY"),
|
|
71
|
+
verbose=True,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
print("Account:", client.account_number)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Common REST examples
|
|
80
|
+
|
|
81
|
+
### 1) Quotes
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
quotes = client.get_quotes(["AAPL", "MSFT"])
|
|
85
|
+
print(quotes)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### 2) Historical quotes
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
history = client.get_historical_quotes("AAPL", start="1979-01-01")
|
|
92
|
+
print(history)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### 3) Option expirations + option chains
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
expirations_resp = client.get_option_expirations("AAPL")
|
|
99
|
+
expirations = expirations_resp.get("expirations", {}).get("date", [])
|
|
100
|
+
print(expirations)
|
|
101
|
+
|
|
102
|
+
if expirations:
|
|
103
|
+
chain = client.get_option_chains("AAPL", expirations[0])
|
|
104
|
+
print(chain)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### 4) Options helper workflow (OCC symbols)
|
|
108
|
+
|
|
109
|
+
The repo includes an `OptionsWrapper` and helper utilities used by `tests/real_data.py`.
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
from tradier_api_client.rest.extensions.options import (
|
|
113
|
+
OptionsWrapper,
|
|
114
|
+
parse_occ,
|
|
115
|
+
filter_options_occ,
|
|
116
|
+
filter_for_tradable_options_strike_plus_bid,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
ow = OptionsWrapper(client)
|
|
120
|
+
occ_symbols = ow.get_call_options_occ_symbols_list("AAPL")
|
|
121
|
+
|
|
122
|
+
# Parse OCC symbols into structured info
|
|
123
|
+
parsed = parse_occ(occ_symbols)
|
|
124
|
+
|
|
125
|
+
# Filter by expiration window and strike constraints (example)
|
|
126
|
+
filtered_occ = filter_options_occ(options=occ_symbols, exp_end="2027-08-30", exp_start="2027-01-01")
|
|
127
|
+
|
|
128
|
+
# Quote options and filter for tradable candidates
|
|
129
|
+
quotes = client.get_quotes(filtered_occ)
|
|
130
|
+
tradable = filter_for_tradable_options_strike_plus_bid(
|
|
131
|
+
quotes=quotes,
|
|
132
|
+
occ_symbols=filtered_occ,
|
|
133
|
+
target_price=5.5,
|
|
134
|
+
)
|
|
135
|
+
print(tradable)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### 5) Account endpoints (example: gain/loss)
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
gain_loss = client.get_gain_loss(client.account_number)
|
|
142
|
+
print(gain_loss)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Orders
|
|
148
|
+
|
|
149
|
+
The order helpers live under `tradier_api_client.rest.extensions.orders`.
|
|
150
|
+
|
|
151
|
+
### Equity limit buy (example)
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
from tradier_api_client.rest.extensions.orders import equity_limit
|
|
155
|
+
|
|
156
|
+
order_request = equity_limit(
|
|
157
|
+
symbol="AAPL",
|
|
158
|
+
side="buy",
|
|
159
|
+
quantity=1,
|
|
160
|
+
price=150,
|
|
161
|
+
duration="gtc",
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
resp = client.place_order(client.account_number, order_request)
|
|
165
|
+
print(resp)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Read / cancel orders
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
orders = client.get_orders(client.account_number)
|
|
172
|
+
print(orders)
|
|
173
|
+
|
|
174
|
+
# If you have an order id:
|
|
175
|
+
# order = client.get_order(client.account_number, "12345678")
|
|
176
|
+
# print(order)
|
|
177
|
+
|
|
178
|
+
# cancel_resp = client.cancel_order(client.account_number, "12345678")
|
|
179
|
+
# print(cancel_resp)
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## Running tests (local repo)
|
|
185
|
+
|
|
186
|
+
This project includes tests/integration scripts under `tests/`.
|
|
187
|
+
|
|
188
|
+
- Some scripts (like `tests/real_data.py`) may call live/sandbox endpoints and require `API_KEY`.
|
|
189
|
+
- The `tests/` folder is **not** intended to be installed with the client distribution.
|
|
190
|
+
|
|
191
|
+
To run tests locally:
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
python -m pip install -U pytest
|
|
195
|
+
pytest -q
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## Notes / troubleshooting
|
|
201
|
+
|
|
202
|
+
- **Sandbox vs production**: use `https://sandbox.tradier.com/v1` for paper testing and `https://api.tradier.com/v1` for production.
|
|
203
|
+
- If you see authentication errors, confirm `API_KEY` is set in the environment used by your Python interpreter.
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# tradier_api_client
|
|
2
|
+
|
|
3
|
+
Python client for the Tradier Brokerage API (REST) plus optional streaming helpers.
|
|
4
|
+
|
|
5
|
+
This repo is intended to give you a lightweight, script-friendly way to call Tradier endpoints (quotes, options, orders, account data) using a single `RestClient`.
|
|
6
|
+
|
|
7
|
+
> Note: Some examples below are adapted from `tests/real_data.py` which is intended for *local* use with a real API key. Don’t commit keys.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
From source (editable):
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
python -m pip install -U pip
|
|
17
|
+
python -m pip install -e .
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Regular install (from the cloned repo):
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
python -m pip install .
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Authentication
|
|
29
|
+
|
|
30
|
+
Set your Tradier API key in an environment variable.
|
|
31
|
+
|
|
32
|
+
### Windows (cmd.exe)
|
|
33
|
+
|
|
34
|
+
```bat
|
|
35
|
+
set API_KEY=your_key_here
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### macOS/Linux
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
export API_KEY=your_key_here
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Quick start
|
|
47
|
+
|
|
48
|
+
### Create a `RestClient`
|
|
49
|
+
|
|
50
|
+
The tests create a client like this:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
import os
|
|
54
|
+
from tradier_api_client import RestClient
|
|
55
|
+
|
|
56
|
+
client = RestClient(
|
|
57
|
+
base_url="https://sandbox.tradier.com/v1", # use https://api.tradier.com/v1 for production
|
|
58
|
+
api_key=os.environ.get("API_KEY"),
|
|
59
|
+
verbose=True,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
print("Account:", client.account_number)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Common REST examples
|
|
68
|
+
|
|
69
|
+
### 1) Quotes
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
quotes = client.get_quotes(["AAPL", "MSFT"])
|
|
73
|
+
print(quotes)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### 2) Historical quotes
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
history = client.get_historical_quotes("AAPL", start="1979-01-01")
|
|
80
|
+
print(history)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### 3) Option expirations + option chains
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
expirations_resp = client.get_option_expirations("AAPL")
|
|
87
|
+
expirations = expirations_resp.get("expirations", {}).get("date", [])
|
|
88
|
+
print(expirations)
|
|
89
|
+
|
|
90
|
+
if expirations:
|
|
91
|
+
chain = client.get_option_chains("AAPL", expirations[0])
|
|
92
|
+
print(chain)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### 4) Options helper workflow (OCC symbols)
|
|
96
|
+
|
|
97
|
+
The repo includes an `OptionsWrapper` and helper utilities used by `tests/real_data.py`.
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
from tradier_api_client.rest.extensions.options import (
|
|
101
|
+
OptionsWrapper,
|
|
102
|
+
parse_occ,
|
|
103
|
+
filter_options_occ,
|
|
104
|
+
filter_for_tradable_options_strike_plus_bid,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
ow = OptionsWrapper(client)
|
|
108
|
+
occ_symbols = ow.get_call_options_occ_symbols_list("AAPL")
|
|
109
|
+
|
|
110
|
+
# Parse OCC symbols into structured info
|
|
111
|
+
parsed = parse_occ(occ_symbols)
|
|
112
|
+
|
|
113
|
+
# Filter by expiration window and strike constraints (example)
|
|
114
|
+
filtered_occ = filter_options_occ(options=occ_symbols, exp_end="2027-08-30", exp_start="2027-01-01")
|
|
115
|
+
|
|
116
|
+
# Quote options and filter for tradable candidates
|
|
117
|
+
quotes = client.get_quotes(filtered_occ)
|
|
118
|
+
tradable = filter_for_tradable_options_strike_plus_bid(
|
|
119
|
+
quotes=quotes,
|
|
120
|
+
occ_symbols=filtered_occ,
|
|
121
|
+
target_price=5.5,
|
|
122
|
+
)
|
|
123
|
+
print(tradable)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### 5) Account endpoints (example: gain/loss)
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
gain_loss = client.get_gain_loss(client.account_number)
|
|
130
|
+
print(gain_loss)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Orders
|
|
136
|
+
|
|
137
|
+
The order helpers live under `tradier_api_client.rest.extensions.orders`.
|
|
138
|
+
|
|
139
|
+
### Equity limit buy (example)
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
from tradier_api_client.rest.extensions.orders import equity_limit
|
|
143
|
+
|
|
144
|
+
order_request = equity_limit(
|
|
145
|
+
symbol="AAPL",
|
|
146
|
+
side="buy",
|
|
147
|
+
quantity=1,
|
|
148
|
+
price=150,
|
|
149
|
+
duration="gtc",
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
resp = client.place_order(client.account_number, order_request)
|
|
153
|
+
print(resp)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Read / cancel orders
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
orders = client.get_orders(client.account_number)
|
|
160
|
+
print(orders)
|
|
161
|
+
|
|
162
|
+
# If you have an order id:
|
|
163
|
+
# order = client.get_order(client.account_number, "12345678")
|
|
164
|
+
# print(order)
|
|
165
|
+
|
|
166
|
+
# cancel_resp = client.cancel_order(client.account_number, "12345678")
|
|
167
|
+
# print(cancel_resp)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Running tests (local repo)
|
|
173
|
+
|
|
174
|
+
This project includes tests/integration scripts under `tests/`.
|
|
175
|
+
|
|
176
|
+
- Some scripts (like `tests/real_data.py`) may call live/sandbox endpoints and require `API_KEY`.
|
|
177
|
+
- The `tests/` folder is **not** intended to be installed with the client distribution.
|
|
178
|
+
|
|
179
|
+
To run tests locally:
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
python -m pip install -U pytest
|
|
183
|
+
pytest -q
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## Notes / troubleshooting
|
|
189
|
+
|
|
190
|
+
- **Sandbox vs production**: use `https://sandbox.tradier.com/v1` for paper testing and `https://api.tradier.com/v1` for production.
|
|
191
|
+
- If you see authentication errors, confirm `API_KEY` is set in the environment used by your Python interpreter.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "tradier-api-client"
|
|
7
|
+
dynamic=["version"]
|
|
8
|
+
description = "Tradier REST + Streaming API client"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "Proprietary" } # or: { file = "LICENSE" }
|
|
12
|
+
authors = [{ name = "Your Name" }]
|
|
13
|
+
|
|
14
|
+
# This replaces install_requires from setup.py
|
|
15
|
+
dependencies = [
|
|
16
|
+
"requests>=2.25.1",
|
|
17
|
+
"cryptography>=41",
|
|
18
|
+
"websocket-client>=1.5"
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[tool.setuptools.packages.find]
|
|
22
|
+
where = ["."]
|
|
23
|
+
include = ["tradier_api_client*"]
|
|
24
|
+
exclude = ["tests", "build", "dist"]
|
|
25
|
+
|
|
26
|
+
[tool.setuptools.dynamic]
|
|
27
|
+
version = {attr = "tradier_api_client.__version__"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tradier REST and Streaming client
|
|
3
|
+
"""
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
from tradier_api_client.rest import RestClient
|
|
7
|
+
from tradier_api_client.streaming.streaming_client import StreamingClient
|
|
8
|
+
|
|
9
|
+
logging.getLogger(__name__).addHandler(logging.NullHandler())
|
|
10
|
+
|
|
11
|
+
__version__ = "0.1.1"
|
|
12
|
+
__all__ = ["StreamingClient", "RestClient"]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Common decorators
|
|
3
|
+
"""
|
|
4
|
+
import functools
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def is_authenticated():
|
|
8
|
+
"""
|
|
9
|
+
Checks whether the api_key is set on the object
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def decorator(method):
|
|
13
|
+
@functools.wraps(method)
|
|
14
|
+
def wrapper(self, *args, **kwargs):
|
|
15
|
+
if not getattr(self, "authenticated", None):
|
|
16
|
+
raise Exception("API key is not set, API call will fail")
|
|
17
|
+
return method(self, *args, **kwargs)
|
|
18
|
+
|
|
19
|
+
return wrapper
|
|
20
|
+
|
|
21
|
+
return decorator
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""
|
|
2
|
+
EndDec util
|
|
3
|
+
"""
|
|
4
|
+
import os
|
|
5
|
+
import string
|
|
6
|
+
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
7
|
+
from cryptography.hazmat.primitives import hashes
|
|
8
|
+
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class EncDec:
|
|
12
|
+
"""
|
|
13
|
+
EndDec util
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self):
|
|
17
|
+
# Base62 alphabet: only alphanumeric characters.
|
|
18
|
+
self.BASE62_ALPHABET = string.digits + string.ascii_uppercase + string.ascii_lowercase # 0-9, A-Z, a-z
|
|
19
|
+
|
|
20
|
+
def base62_encode(self, data: bytes) -> str:
|
|
21
|
+
"""
|
|
22
|
+
EndDec util
|
|
23
|
+
"""
|
|
24
|
+
num = int.from_bytes(data, 'big')
|
|
25
|
+
if num == 0:
|
|
26
|
+
return self.BASE62_ALPHABET[0]
|
|
27
|
+
base = len(self.BASE62_ALPHABET)
|
|
28
|
+
encoded = []
|
|
29
|
+
while num:
|
|
30
|
+
num, rem = divmod(num, base)
|
|
31
|
+
encoded.append(self.BASE62_ALPHABET[rem])
|
|
32
|
+
encoded.reverse()
|
|
33
|
+
return ''.join(encoded)
|
|
34
|
+
|
|
35
|
+
def base62_decode(self, s: str) -> bytes:
|
|
36
|
+
"""
|
|
37
|
+
EndDec util
|
|
38
|
+
"""
|
|
39
|
+
base = len(self.BASE62_ALPHABET)
|
|
40
|
+
num = 0
|
|
41
|
+
for char in s:
|
|
42
|
+
num = num * base + self.BASE62_ALPHABET.index(char)
|
|
43
|
+
# Calculate the number of bytes needed.
|
|
44
|
+
byte_length = (num.bit_length() + 7) // 8
|
|
45
|
+
return num.to_bytes(byte_length, 'big')
|
|
46
|
+
|
|
47
|
+
def derive_key(self, secret: str, salt: bytes) -> bytes:
|
|
48
|
+
"""
|
|
49
|
+
EndDec util
|
|
50
|
+
"""
|
|
51
|
+
# Derive a 32-byte key using PBKDF2HMAC (AES-256 needs 32 bytes).
|
|
52
|
+
kdf = PBKDF2HMAC(
|
|
53
|
+
algorithm=hashes.SHA256(),
|
|
54
|
+
length=32,
|
|
55
|
+
salt=salt,
|
|
56
|
+
iterations=100000,
|
|
57
|
+
)
|
|
58
|
+
return kdf.derive(secret.encode())
|
|
59
|
+
|
|
60
|
+
def encrypt_base62(self, plaintext: str, secret: str) -> str:
|
|
61
|
+
"""
|
|
62
|
+
EndDec util
|
|
63
|
+
"""
|
|
64
|
+
# Generate a random 16-byte salt.
|
|
65
|
+
salt = os.urandom(16)
|
|
66
|
+
key = self.derive_key(secret, salt)
|
|
67
|
+
aesgcm = AESGCM(key)
|
|
68
|
+
# Generate a random 12-byte nonce (recommended for AESGCM).
|
|
69
|
+
nonce = os.urandom(12)
|
|
70
|
+
ciphertext = aesgcm.encrypt(nonce, plaintext.encode(), None)
|
|
71
|
+
|
|
72
|
+
# Combine salt + nonce + ciphertext.
|
|
73
|
+
encrypted_bytes = salt + nonce + ciphertext
|
|
74
|
+
|
|
75
|
+
# Encode using Base62.
|
|
76
|
+
encrypted_b62 = self.base62_encode(encrypted_bytes)
|
|
77
|
+
|
|
78
|
+
# Enforce the length constraint.
|
|
79
|
+
if len(encrypted_b62) >= 255:
|
|
80
|
+
raise ValueError("Encrypted string exceeds the length limit of 255 characters.")
|
|
81
|
+
|
|
82
|
+
return encrypted_b62
|
|
83
|
+
|
|
84
|
+
def decrypt_base62(self, encrypted_b62: str, secret: str) -> str:
|
|
85
|
+
"""
|
|
86
|
+
EndDec util
|
|
87
|
+
"""
|
|
88
|
+
encrypted_bytes = self.base62_decode(encrypted_b62)
|
|
89
|
+
|
|
90
|
+
# Extract salt (first 16 bytes), nonce (next 12 bytes), and the ciphertext.
|
|
91
|
+
salt = encrypted_bytes[:16]
|
|
92
|
+
nonce = encrypted_bytes[16:28]
|
|
93
|
+
ciphertext = encrypted_bytes[28:]
|
|
94
|
+
key = self.derive_key(secret, salt)
|
|
95
|
+
aesgcm = AESGCM(key)
|
|
96
|
+
plaintext = aesgcm.decrypt(nonce, ciphertext, None)
|
|
97
|
+
return plaintext.decode('utf-8')
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
if __name__ == '__main__':
|
|
101
|
+
encrypted = EncDec().encrypt_base62("yunind7.tradier", "Xjyrmnfg@321")
|
|
102
|
+
print(encrypted)
|
|
103
|
+
print(EncDec().decrypt_base62(encrypted, "Xjyrmnfg@321"))
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""
|
|
2
|
+
General utility functions for the Tradier API client.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import Any, Optional
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def log_for_level(
|
|
11
|
+
logger,
|
|
12
|
+
level: int,
|
|
13
|
+
message: str,
|
|
14
|
+
*,
|
|
15
|
+
exc: Optional[BaseException] = None,
|
|
16
|
+
exc_info: Any = None,
|
|
17
|
+
stack_info: bool = False,
|
|
18
|
+
extra: Optional[dict] = None,
|
|
19
|
+
):
|
|
20
|
+
"""Log *message* if *logger* is enabled for *level*.
|
|
21
|
+
|
|
22
|
+
This is a tiny helper to avoid building/logging messages when the level is disabled.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
logger: A ``logging.Logger`` instance.
|
|
26
|
+
level: A stdlib logging level (e.g. ``logging.INFO``).
|
|
27
|
+
message: The message string to log.
|
|
28
|
+
exc: Optional exception instance to attach (equivalent to ``exc_info=exc``).
|
|
29
|
+
exc_info: Passed through to ``logger.log(..., exc_info=...)``. If both ``exc`` and
|
|
30
|
+
``exc_info`` are provided, ``exc_info`` wins.
|
|
31
|
+
stack_info: Passed through to the logger.
|
|
32
|
+
extra: Passed through to the logger.
|
|
33
|
+
"""
|
|
34
|
+
if not logger or not logger.isEnabledFor(level):
|
|
35
|
+
return
|
|
36
|
+
|
|
37
|
+
# Support passing an exception instance directly.
|
|
38
|
+
if exc_info is None and exc is not None:
|
|
39
|
+
exc_info = exc
|
|
40
|
+
|
|
41
|
+
logger.log(level, message, exc_info=exc_info, stack_info=stack_info, extra=extra)
|