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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: schwabdev
3
- Version: 2.2.2
3
+ Version: 2.2.3
4
4
  Summary: An easy and lightweight wrapper for using the Charles Schwab API.
5
5
  Author: Tyler Bowers
6
6
  Author-email: tylerebowers@gmail.com
@@ -0,0 +1,3 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
@@ -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(60)
94
+ time.sleep(30)
95
95
  threading.Thread(target=checker, daemon=True).start()
96
- elif not self.verbose:
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.quote(symbol_id)}/quotes',
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 has closed.")
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
- if self.verbose: print(f"Stream closed (likely no subscriptions): {e}")
93
+ print(f"[Schwabdev] Stream connection closed (likely no subscriptions): {e}")
94
94
  break
95
- elif (datetime.now() - start_time).seconds <= 90:
96
- if self.verbose: print("Stream has crashed within 90 seconds, likely no subscriptions or invalid login.")
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
- if self.verbose: print(f"{e}")
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 start_automatic(self, receiver=print, after_hours=False, pre_hours=False):
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(9, 29, 0) # market opens at 9:30
129
- end = time(16, 0, 0) # market closes at 4:00
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(7, 59, 0)
130
+ start = datetime.time(11, 59, 0, tzinfo=datetime.timezone.utc)
132
131
  if after_hours:
133
- end = time(20, 0, 0)
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
- if self._streamer_info is not None:
252
- request = {"service": service.upper(),
253
- "command": command.upper(),
254
- "requestid": self._request_id,
255
- "SchwabClientCustomerId": self._streamer_info.get("schwabClientCustomerId"),
256
- "SchwabClientCorrelId": self._streamer_info.get("schwabClientCorrelId")}
257
- if parameters is not None and len(parameters) > 0: request["parameters"] = parameters
258
- self._request_id += 1
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):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: schwabdev
3
- Version: 2.2.2
3
+ Version: 2.2.3
4
4
  Summary: An easy and lightweight wrapper for using the Charles Schwab API.
5
5
  Author: Tyler Bowers
6
6
  Author-email: tylerebowers@gmail.com
@@ -1,5 +1,6 @@
1
1
  LICENSE.txt
2
2
  README.md
3
+ pyproject.toml
3
4
  setup.py
4
5
  schwabdev/__init__.py
5
6
  schwabdev/api.py
@@ -1,6 +1,6 @@
1
1
  from setuptools import setup, find_packages
2
2
 
3
- VERSION = '2.2.2'
3
+ VERSION = '2.2.3'
4
4
  DESCRIPTION = 'An easy and lightweight wrapper for using the Charles Schwab API.'
5
5
  with open('README.md', 'r') as f:
6
6
  LONG_DESCRIPTION = f.read()
File without changes
File without changes
File without changes