schwabdev 2.2.2__tar.gz → 2.2.3__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.
- {schwabdev-2.2.2 → schwabdev-2.2.3}/PKG-INFO +1 -1
- schwabdev-2.2.3/pyproject.toml +3 -0
- {schwabdev-2.2.2 → schwabdev-2.2.3}/schwabdev/api.py +17 -17
- {schwabdev-2.2.2 → schwabdev-2.2.3}/schwabdev/stream.py +57 -36
- {schwabdev-2.2.2 → schwabdev-2.2.3}/schwabdev.egg-info/PKG-INFO +1 -1
- {schwabdev-2.2.2 → schwabdev-2.2.3}/schwabdev.egg-info/SOURCES.txt +1 -0
- {schwabdev-2.2.2 → schwabdev-2.2.3}/setup.py +1 -1
- {schwabdev-2.2.2 → schwabdev-2.2.3}/LICENSE.txt +0 -0
- {schwabdev-2.2.2 → schwabdev-2.2.3}/README.md +0 -0
- {schwabdev-2.2.2 → schwabdev-2.2.3}/schwabdev/__init__.py +0 -0
- {schwabdev-2.2.2 → schwabdev-2.2.3}/schwabdev.egg-info/dependency_links.txt +0 -0
- {schwabdev-2.2.2 → schwabdev-2.2.3}/schwabdev.egg-info/requires.txt +0 -0
- {schwabdev-2.2.2 → schwabdev-2.2.3}/schwabdev.egg-info/top_level.txt +0 -0
- {schwabdev-2.2.2 → schwabdev-2.2.3}/setup.cfg +0 -0
|
@@ -74,14 +74,14 @@ class Client:
|
|
|
74
74
|
self._access_token_issued = at_issued
|
|
75
75
|
self._refresh_token_issued = rt_issued
|
|
76
76
|
if self.verbose:
|
|
77
|
-
print(self._access_token_issued.strftime("Access token last updated: %Y-%m-%d %H:%M:%S") + f" (expires in {self._access_token_timeout - (datetime.datetime.now(datetime.timezone.utc) - self._access_token_issued).seconds} seconds)")
|
|
78
|
-
print(self._refresh_token_issued.strftime("Refresh token last updated: %Y-%m-%d %H:%M:%S") + f" (expires in {self._refresh_token_timeout - (datetime.datetime.now(datetime.timezone.utc) - self._refresh_token_issued).days} days)")
|
|
77
|
+
print(self._access_token_issued.strftime("[Schwabdev] Access token last updated: %Y-%m-%d %H:%M:%S") + f" (expires in {self._access_token_timeout - (datetime.datetime.now(datetime.timezone.utc) - self._access_token_issued).seconds} seconds)")
|
|
78
|
+
print(self._refresh_token_issued.strftime("[Schwabdev] Refresh token last updated: %Y-%m-%d %H:%M:%S") + f" (expires in {self._refresh_token_timeout - (datetime.datetime.now(datetime.timezone.utc) - self._refresh_token_issued).days} days)")
|
|
79
79
|
# check if tokens need to be updated and update if needed
|
|
80
80
|
self.update_tokens()
|
|
81
81
|
else:
|
|
82
82
|
# The tokens file doesn't exist, so create it.
|
|
83
83
|
if self.verbose:
|
|
84
|
-
print(f"Token file does not exist or invalid formatting, creating \"{str(tokens_file)}\"")
|
|
84
|
+
print(f"[Schwabdev] Token file does not exist or invalid formatting, creating \"{str(tokens_file)}\"")
|
|
85
85
|
open(self._tokens_file, 'w').close()
|
|
86
86
|
# Tokens must be updated.
|
|
87
87
|
self._update_refresh_token()
|
|
@@ -91,13 +91,13 @@ class Client:
|
|
|
91
91
|
def checker():
|
|
92
92
|
while True:
|
|
93
93
|
self.update_tokens()
|
|
94
|
-
time.sleep(
|
|
94
|
+
time.sleep(30)
|
|
95
95
|
threading.Thread(target=checker, daemon=True).start()
|
|
96
|
-
elif
|
|
97
|
-
print("Warning: Tokens will not be updated automatically.")
|
|
96
|
+
elif self.verbose:
|
|
97
|
+
print("[Schwabdev] Warning: Tokens will not be updated automatically.")
|
|
98
98
|
|
|
99
99
|
if self.verbose:
|
|
100
|
-
print("Schwabdev Client Initialization Complete")
|
|
100
|
+
print("[Schwabdev] Client Initialization Complete")
|
|
101
101
|
|
|
102
102
|
def update_tokens(self, force=False):
|
|
103
103
|
"""
|
|
@@ -106,16 +106,16 @@ class Client:
|
|
|
106
106
|
:type force: bool
|
|
107
107
|
"""
|
|
108
108
|
if (datetime.datetime.now(datetime.timezone.utc) - self._refresh_token_issued).days >= (self._refresh_token_timeout - 1) or force: # check if we need to update refresh (and access) token
|
|
109
|
-
print("The refresh token has expired, please update!")
|
|
109
|
+
print("[Schwabdev] The refresh token has expired, please update!")
|
|
110
110
|
self._update_refresh_token()
|
|
111
111
|
elif ((datetime.datetime.now(datetime.timezone.utc) - self._access_token_issued).days >= 1) or (
|
|
112
112
|
(datetime.datetime.now(datetime.timezone.utc) - self._access_token_issued).seconds > (self._access_token_timeout - 61)): # check if we need to update access token
|
|
113
|
-
if self.verbose: print("The access token has expired, updating automatically.")
|
|
113
|
+
if self.verbose: print("[Schwabdev] The access token has expired, updating automatically.")
|
|
114
114
|
self._update_access_token()
|
|
115
115
|
|
|
116
116
|
def update_tokens_auto(self):
|
|
117
117
|
import warnings
|
|
118
|
-
warnings.warn("update_tokens_auto() is deprecated and is now started when the client is created (if update_tokens_auto=True (default)).", DeprecationWarning, stacklevel=2)
|
|
118
|
+
warnings.warn("update_tokens_auto() is deprecated and is now started by default when the client is created (if update_tokens_auto=True (default)).", DeprecationWarning, stacklevel=2)
|
|
119
119
|
|
|
120
120
|
def _update_access_token(self):
|
|
121
121
|
"""
|
|
@@ -136,11 +136,11 @@ class Client:
|
|
|
136
136
|
self.id_token = new_td.get("id_token")
|
|
137
137
|
self._write_tokens_file(self._access_token_issued, refresh_token_issued, new_td)
|
|
138
138
|
if self.verbose: # show user that we have updated the access token
|
|
139
|
-
print(f"Access token updated: {self._access_token_issued}")
|
|
139
|
+
print(f"[Schwabdev] Access token updated: {self._access_token_issued}")
|
|
140
140
|
break
|
|
141
141
|
else:
|
|
142
142
|
print(response.text)
|
|
143
|
-
print(f"Could not get new access token ({i+1} of 3).")
|
|
143
|
+
print(f"[Schwabdev] Could not get new access token ({i+1} of 3).")
|
|
144
144
|
time.sleep(10)
|
|
145
145
|
|
|
146
146
|
def _update_refresh_token(self):
|
|
@@ -149,9 +149,9 @@ class Client:
|
|
|
149
149
|
"""
|
|
150
150
|
self.awaiting_input = True # set flag since we are waiting for user input
|
|
151
151
|
# get authorization code (requires user to authorize)
|
|
152
|
-
#print("Please authorize this program to access your schwab account.")
|
|
152
|
+
#print("[Schwabdev] Please authorize this program to access your schwab account.")
|
|
153
153
|
auth_url = f'https://api.schwabapi.com/v1/oauth/authorize?client_id={self._app_key}&redirect_uri={self._callback_url}'
|
|
154
|
-
print(f"Open to authenticate: {auth_url}")
|
|
154
|
+
print(f"[Schwabdev] Open to authenticate: {auth_url}")
|
|
155
155
|
webbrowser.open(auth_url)
|
|
156
156
|
response_url = input("After authorizing, paste the address bar url here: ")
|
|
157
157
|
code = f"{response_url[response_url.index('code=') + 5:response_url.index('%40')]}@" # session = responseURL[responseURL.index("session=")+8:]
|
|
@@ -166,10 +166,10 @@ class Client:
|
|
|
166
166
|
self.awaiting_input = False # reset flag since tokens have been updated
|
|
167
167
|
self.id_token = new_td.get("id_token")
|
|
168
168
|
self._write_tokens_file(self._access_token_issued, self._refresh_token_issued, new_td)
|
|
169
|
-
if self.verbose: print("Refresh and Access tokens updated")
|
|
169
|
+
if self.verbose: print("[Schwabdev] Refresh and Access tokens updated")
|
|
170
170
|
else:
|
|
171
171
|
print(response.text)
|
|
172
|
-
print("Could not get new refresh and access tokens, check these:\n 1. App status is "
|
|
172
|
+
print("[Schwabdev] Could not get new refresh and access tokens, check these:\n 1. App status is "
|
|
173
173
|
"\"Ready For Use\".\n 2. App key and app secret are valid.\n 3. You pasted the "
|
|
174
174
|
"whole url within 30 seconds. (it has a quick expiration)")
|
|
175
175
|
|
|
@@ -516,7 +516,7 @@ class Client:
|
|
|
516
516
|
:return: quote for a single symbol
|
|
517
517
|
:rtype: request.Response
|
|
518
518
|
"""
|
|
519
|
-
return requests.get(f'{self._base_api_url}/marketdata/v1/{urllib.parse.
|
|
519
|
+
return requests.get(f'{self._base_api_url}/marketdata/v1/{urllib.parse.quote_plus(symbol_id)}/quotes',
|
|
520
520
|
headers={'Authorization': f'Bearer {self.access_token}'},
|
|
521
521
|
params=self._params_parser({'fields': fields}),
|
|
522
522
|
timeout=self.timeout)
|
|
@@ -7,11 +7,11 @@ Github: https://github.com/tylerebowers/Schwab-API-Python
|
|
|
7
7
|
import json
|
|
8
8
|
import atexit
|
|
9
9
|
import asyncio
|
|
10
|
+
import datetime
|
|
10
11
|
import threading
|
|
11
12
|
import websockets
|
|
12
13
|
from time import sleep
|
|
13
14
|
import websockets.exceptions
|
|
14
|
-
from datetime import datetime, time
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
class Stream:
|
|
@@ -28,7 +28,6 @@ class Stream:
|
|
|
28
28
|
self.active = False # whether the stream is active
|
|
29
29
|
self._thread = None # the thread that runs the stream
|
|
30
30
|
self._client = client # so we can get streamer info
|
|
31
|
-
self.verbose = client.verbose # inherit the client's verbose setting
|
|
32
31
|
self.subscriptions = {} # a dictionary of subscriptions
|
|
33
32
|
|
|
34
33
|
# register atexit to stop the stream (if active)
|
|
@@ -49,15 +48,16 @@ class Stream:
|
|
|
49
48
|
if response.ok:
|
|
50
49
|
self._streamer_info = response.json().get('streamerInfo', None)[0]
|
|
51
50
|
else:
|
|
52
|
-
print("Could not get streamerInfo")
|
|
51
|
+
print("[Schwabdev] Could not get streamerInfo")
|
|
53
52
|
|
|
54
53
|
# start the stream
|
|
55
|
-
start_time = datetime.now()
|
|
54
|
+
start_time = datetime.datetime.now(datetime.timezone.utc)
|
|
56
55
|
while True:
|
|
57
56
|
try:
|
|
58
|
-
start_time = datetime.now()
|
|
59
|
-
if self.verbose: print("Connecting to streaming server...")
|
|
57
|
+
start_time = datetime.datetime.now(datetime.timezone.utc)
|
|
58
|
+
if self._client.verbose: print("[Schwabdev] Connecting to streaming server...")
|
|
60
59
|
async with websockets.connect(self._streamer_info.get('streamerSocketUrl'), ping_interval=None) as self._websocket:
|
|
60
|
+
if self._client.verbose: print("[Schwabdev] Connected to streaming server.")
|
|
61
61
|
# send login payload
|
|
62
62
|
login_payload = self.basic_request(service="ADMIN",
|
|
63
63
|
command="LOGIN",
|
|
@@ -87,17 +87,16 @@ class Stream:
|
|
|
87
87
|
except Exception as e:
|
|
88
88
|
self.active = False
|
|
89
89
|
if e is websockets.exceptions.ConnectionClosedOK or str(e) == "received 1000 (OK); then sent 1000 (OK)": # catch logout request
|
|
90
|
-
if self.verbose: print("Stream
|
|
90
|
+
if self._client.verbose: print("[Schwabdev] Stream connection closed.")
|
|
91
91
|
break
|
|
92
92
|
elif e is websockets.exceptions.ConnectionClosedError or str(e) == "no close frame received or sent": # catch no subscriptions kick
|
|
93
|
-
|
|
93
|
+
print(f"[Schwabdev] Stream connection closed (likely no subscriptions): {e}")
|
|
94
94
|
break
|
|
95
|
-
elif (datetime.now() - start_time).seconds <= 90:
|
|
96
|
-
|
|
95
|
+
elif (datetime.datetime.now(datetime.timezone.utc) - start_time).seconds <= 90:
|
|
96
|
+
print(f"[Schwabdev] Stream has crashed within 90 seconds ({e}), likely no subscriptions, invalid login, or lost connection (not restarting).")
|
|
97
97
|
break
|
|
98
98
|
else: # stream has quit unexpectedly, try to reconnect
|
|
99
|
-
|
|
100
|
-
if self.verbose: print("Connection lost to server, reconnecting...")
|
|
99
|
+
print(f"[Schwabdev] Stream connection lost to server ({e}), reconnecting...")
|
|
101
100
|
|
|
102
101
|
def start(self, receiver=print, *args, **kwargs):
|
|
103
102
|
"""
|
|
@@ -113,9 +112,9 @@ class Stream:
|
|
|
113
112
|
self._thread.start()
|
|
114
113
|
# if the thread does not start in time then the main program may close before the streamer starts
|
|
115
114
|
else:
|
|
116
|
-
print("Stream already active.")
|
|
115
|
+
if self._client.verbose: print("[Schwabdev] Stream already active.")
|
|
117
116
|
|
|
118
|
-
def
|
|
117
|
+
def start_auto(self, receiver=print, after_hours=False, pre_hours=False):
|
|
119
118
|
"""
|
|
120
119
|
Start the stream automatically at market open and close, will NOT erase subscriptions
|
|
121
120
|
:param receiver: function to call when data is received
|
|
@@ -125,30 +124,30 @@ class Stream:
|
|
|
125
124
|
:param pre_hours: include pre hours trading
|
|
126
125
|
:type pre_hours: bool
|
|
127
126
|
"""
|
|
128
|
-
start = time(
|
|
129
|
-
end = time(
|
|
127
|
+
start = datetime.time(13, 29, 0, tzinfo=datetime.timezone.utc) # market opens at 9:30 ET
|
|
128
|
+
end = datetime.time(20, 0, 0, tzinfo=datetime.timezone.utc) # market closes at 4:00 ET
|
|
130
129
|
if pre_hours:
|
|
131
|
-
start = time(
|
|
130
|
+
start = datetime.time(11, 59, 0, tzinfo=datetime.timezone.utc)
|
|
132
131
|
if after_hours:
|
|
133
|
-
end = time(
|
|
132
|
+
end = datetime.time(24, 0, 0, tzinfo=datetime.timezone.utc)
|
|
134
133
|
|
|
135
134
|
def checker():
|
|
136
135
|
|
|
137
136
|
while True:
|
|
138
|
-
in_hours = (start <= datetime.now().time() <= end) and (0 <= datetime.now().weekday() <= 4)
|
|
137
|
+
in_hours = (start <= datetime.datetime.now(datetime.timezone.utc).time() <= end) and (0 <= datetime.datetime.now(datetime.timezone.utc).weekday() <= 4)
|
|
139
138
|
if in_hours and not self.active:
|
|
140
139
|
if len(self.subscriptions) == 0:
|
|
141
|
-
if self.verbose: print("No subscriptions, starting stream anyways.")
|
|
140
|
+
if self._client.verbose: print("[Schwabdev] No subscriptions, starting stream anyways.")
|
|
142
141
|
self.start(receiver=receiver)
|
|
143
142
|
elif not in_hours and self.active:
|
|
144
|
-
if self.verbose: print("Stopping Stream.")
|
|
143
|
+
if self._client.verbose: print("[Schwabdev] Stopping Stream.")
|
|
145
144
|
self.stop(clear_subscriptions=False)
|
|
146
145
|
sleep(60)
|
|
147
146
|
|
|
148
147
|
threading.Thread(target=checker).start()
|
|
149
148
|
|
|
150
|
-
if not start <= datetime.now().time() <= end:
|
|
151
|
-
print("Stream was started outside of active hours and will launch when in hours.")
|
|
149
|
+
if not start <= datetime.datetime.now(datetime.timezone.utc).time() <= end:
|
|
150
|
+
print("[Schwabdev] Stream was started outside of active hours and will launch when in hours.")
|
|
152
151
|
|
|
153
152
|
def _record_request(self, request):
|
|
154
153
|
"""
|
|
@@ -211,7 +210,30 @@ class Stream:
|
|
|
211
210
|
to_send = json.dumps({"requests": requests})
|
|
212
211
|
asyncio.run(_send(to_send))
|
|
213
212
|
else:
|
|
214
|
-
if self.verbose: print("Stream is not active, request queued.")
|
|
213
|
+
if self._client.verbose: print("[Schwabdev] Stream is not active, request queued.")
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
async def send_async(self, requests):
|
|
217
|
+
"""
|
|
218
|
+
Send an async (must be awaited) request to the stream (functionally equivalent to send)
|
|
219
|
+
:param requests: list of requests or a single request
|
|
220
|
+
:type requests: list | dict
|
|
221
|
+
"""
|
|
222
|
+
|
|
223
|
+
# make sure requests is a list
|
|
224
|
+
if type(requests) is not list:
|
|
225
|
+
requests = [requests]
|
|
226
|
+
|
|
227
|
+
# add requests to list of subscriptions
|
|
228
|
+
for request in requests:
|
|
229
|
+
self._record_request(request)
|
|
230
|
+
|
|
231
|
+
# send the request if the stream is active, queue otherwise
|
|
232
|
+
if self.active:
|
|
233
|
+
to_send = json.dumps({"requests": requests})
|
|
234
|
+
await self._websocket.send(to_send)
|
|
235
|
+
else:
|
|
236
|
+
if self._client.verbose: print("[Schwabdev] Stream is not active, request queued.")
|
|
215
237
|
|
|
216
238
|
|
|
217
239
|
def stop(self, clear_subscriptions=True):
|
|
@@ -242,24 +264,23 @@ class Stream:
|
|
|
242
264
|
response = self._client.preferences()
|
|
243
265
|
if response.ok:
|
|
244
266
|
self._streamer_info = response.json().get('streamerInfo', None)[0]
|
|
267
|
+
else:
|
|
268
|
+
print("[Schwabdev] Could not use/get streamerInfo")
|
|
269
|
+
return {}
|
|
245
270
|
|
|
246
271
|
# remove None parameters
|
|
247
272
|
if parameters is not None:
|
|
248
273
|
for key in parameters.keys():
|
|
249
274
|
if parameters[key] is None: del parameters[key]
|
|
250
275
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
return request
|
|
260
|
-
else:
|
|
261
|
-
print("basic_request(): Could not use/get streamerInfo")
|
|
262
|
-
return None
|
|
276
|
+
self._request_id += 1
|
|
277
|
+
request = {"service": service.upper(),
|
|
278
|
+
"command": command.upper(),
|
|
279
|
+
"requestid": self._request_id,
|
|
280
|
+
"SchwabClientCustomerId": self._streamer_info.get("schwabClientCustomerId"),
|
|
281
|
+
"SchwabClientCorrelId": self._streamer_info.get("schwabClientCorrelId")}
|
|
282
|
+
if parameters is not None and len(parameters) > 0: request["parameters"] = parameters
|
|
283
|
+
return request
|
|
263
284
|
|
|
264
285
|
@staticmethod
|
|
265
286
|
def _list_to_string(ls):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|