schwab_rb 0.2.0
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.
- checksums.yaml +7 -0
- data/.copilotignore +4 -0
- data/.rspec +2 -0
- data/.rspec_status +292 -0
- data/.rubocop.yml +41 -0
- data/.rubocop_todo.yml +105 -0
- data/CHANGELOG.md +28 -0
- data/LICENSE.txt +23 -0
- data/README.md +271 -0
- data/Rakefile +12 -0
- data/doc/notes/data_objects_analysis.md +223 -0
- data/doc/notes/data_objects_refactoring_plan.md +82 -0
- data/examples/fetch_account_numbers.rb +49 -0
- data/examples/fetch_user_preferences.rb +49 -0
- data/lib/schwab_rb/account.rb +9 -0
- data/lib/schwab_rb/auth/auth_context.rb +23 -0
- data/lib/schwab_rb/auth/init_client_easy.rb +45 -0
- data/lib/schwab_rb/auth/init_client_login.rb +201 -0
- data/lib/schwab_rb/auth/init_client_token_file.rb +30 -0
- data/lib/schwab_rb/auth/login_flow_server.rb +55 -0
- data/lib/schwab_rb/auth/token.rb +24 -0
- data/lib/schwab_rb/auth/token_manager.rb +105 -0
- data/lib/schwab_rb/clients/async_client.rb +122 -0
- data/lib/schwab_rb/clients/base_client.rb +887 -0
- data/lib/schwab_rb/clients/client.rb +97 -0
- data/lib/schwab_rb/configuration.rb +39 -0
- data/lib/schwab_rb/constants.rb +7 -0
- data/lib/schwab_rb/data_objects/account.rb +281 -0
- data/lib/schwab_rb/data_objects/account_numbers.rb +68 -0
- data/lib/schwab_rb/data_objects/instrument.rb +156 -0
- data/lib/schwab_rb/data_objects/market_hours.rb +275 -0
- data/lib/schwab_rb/data_objects/option.rb +147 -0
- data/lib/schwab_rb/data_objects/option_chain.rb +95 -0
- data/lib/schwab_rb/data_objects/option_expiration_chain.rb +134 -0
- data/lib/schwab_rb/data_objects/order.rb +186 -0
- data/lib/schwab_rb/data_objects/order_leg.rb +68 -0
- data/lib/schwab_rb/data_objects/order_preview.rb +237 -0
- data/lib/schwab_rb/data_objects/position.rb +100 -0
- data/lib/schwab_rb/data_objects/price_history.rb +187 -0
- data/lib/schwab_rb/data_objects/quote.rb +276 -0
- data/lib/schwab_rb/data_objects/transaction.rb +132 -0
- data/lib/schwab_rb/data_objects/user_preferences.rb +129 -0
- data/lib/schwab_rb/market_hours.rb +13 -0
- data/lib/schwab_rb/movers.rb +35 -0
- data/lib/schwab_rb/option.rb +64 -0
- data/lib/schwab_rb/orders/builder.rb +202 -0
- data/lib/schwab_rb/orders/destination.rb +19 -0
- data/lib/schwab_rb/orders/duration.rb +9 -0
- data/lib/schwab_rb/orders/equity_instructions.rb +10 -0
- data/lib/schwab_rb/orders/errors.rb +5 -0
- data/lib/schwab_rb/orders/instruments.rb +35 -0
- data/lib/schwab_rb/orders/option_instructions.rb +10 -0
- data/lib/schwab_rb/orders/order.rb +77 -0
- data/lib/schwab_rb/orders/price_link_basis.rb +15 -0
- data/lib/schwab_rb/orders/price_link_type.rb +9 -0
- data/lib/schwab_rb/orders/session.rb +14 -0
- data/lib/schwab_rb/orders/special_instruction.rb +10 -0
- data/lib/schwab_rb/orders/stop_price_link_basis.rb +15 -0
- data/lib/schwab_rb/orders/stop_price_link_type.rb +9 -0
- data/lib/schwab_rb/orders/stop_type.rb +11 -0
- data/lib/schwab_rb/orders/tax_lot_method.rb +13 -0
- data/lib/schwab_rb/price_history.rb +55 -0
- data/lib/schwab_rb/quote.rb +13 -0
- data/lib/schwab_rb/transaction.rb +23 -0
- data/lib/schwab_rb/utils/enum_enforcer.rb +73 -0
- data/lib/schwab_rb/utils/logger.rb +70 -0
- data/lib/schwab_rb/utils/redactor.rb +104 -0
- data/lib/schwab_rb/version.rb +5 -0
- data/lib/schwab_rb.rb +48 -0
- data/sig/schwab_rb.rbs +4 -0
- metadata +289 -0
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'oauth2'
|
2
|
+
|
3
|
+
module SchwabRb::Auth
|
4
|
+
class AuthContext
|
5
|
+
class << self
|
6
|
+
def build(oauth_client, callback_url, authorization_url, state: nil)
|
7
|
+
auth_params = { redirect_uri: callback_url }
|
8
|
+
auth_params[:state] = state if state
|
9
|
+
authorization_url = oauth_client.auth_code.authorize_url(auth_params)
|
10
|
+
|
11
|
+
new(callback_url, authorization_url, state)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(callback_url, authorization_url, state)
|
16
|
+
@callback_url = callback_url
|
17
|
+
@authorization_url = authorization_url
|
18
|
+
@state = state
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_reader :callback_url, :authorization_url, :state
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'oauth2'
|
2
|
+
require_relative 'init_client_token_file'
|
3
|
+
require_relative 'init_client_login'
|
4
|
+
|
5
|
+
module SchwabRb::Auth
|
6
|
+
def self.init_client_easy(
|
7
|
+
api_key,
|
8
|
+
app_secret,
|
9
|
+
callback_url,
|
10
|
+
token_path,
|
11
|
+
asyncio: false,
|
12
|
+
enforce_enums: false,
|
13
|
+
callback_timeout: 300.0,
|
14
|
+
interactive: true,
|
15
|
+
requested_browser: nil)
|
16
|
+
|
17
|
+
begin
|
18
|
+
if File.exist?(token_path)
|
19
|
+
client = SchwabRb::Auth::init_client_token_file(
|
20
|
+
api_key,
|
21
|
+
app_secret,
|
22
|
+
token_path,
|
23
|
+
enforce_enums: enforce_enums
|
24
|
+
)
|
25
|
+
client.refresh! if client.session.expired?
|
26
|
+
raise OAuth2::Error.new("Token expired") if client.session.expired?
|
27
|
+
client
|
28
|
+
else
|
29
|
+
raise OAuth2::Error.new("No token found")
|
30
|
+
end
|
31
|
+
rescue
|
32
|
+
SchwabRb::Auth::init_client_login(
|
33
|
+
api_key,
|
34
|
+
app_secret,
|
35
|
+
callback_url,
|
36
|
+
token_path,
|
37
|
+
asyncio: asyncio,
|
38
|
+
enforce_enums: enforce_enums,
|
39
|
+
callback_timeout: callback_timeout,
|
40
|
+
interactive: interactive,
|
41
|
+
requested_browser: requested_browser
|
42
|
+
)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,201 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
require 'uri'
|
3
|
+
require 'net/http'
|
4
|
+
require 'json'
|
5
|
+
require 'oauth2'
|
6
|
+
# require 'logger'
|
7
|
+
|
8
|
+
module SchwabRb::Auth
|
9
|
+
class RedirectTimeoutError < StandardError
|
10
|
+
def initialize(msg="Timed out waiting for a callback")
|
11
|
+
super(msg)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
class RedirectServerExitedError < StandardError; end
|
15
|
+
# class TokenExchangeError < StandardError
|
16
|
+
# def initialize(msg)
|
17
|
+
# super(msg)
|
18
|
+
# end
|
19
|
+
# end
|
20
|
+
class InvalidHostname < ArgumentError
|
21
|
+
def initialize(hostname)
|
22
|
+
msg = "Disallowed hostname #{hostname}. init_client_login only allows callback URLs with hostname 127.0.0.1."
|
23
|
+
super(msg)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.init_client_login(
|
28
|
+
api_key,
|
29
|
+
app_secret,
|
30
|
+
callback_url,
|
31
|
+
token_path,
|
32
|
+
asyncio: false,
|
33
|
+
enforce_enums: false,
|
34
|
+
callback_timeout: 300.0,
|
35
|
+
interactive: true,
|
36
|
+
requested_browser: nil)
|
37
|
+
|
38
|
+
callback_timeout = if not callback_timeout
|
39
|
+
callback_timeout = 0
|
40
|
+
elsif callback_timeout < 0
|
41
|
+
raise ArgumentError, "callback_timeout must be non-negative"
|
42
|
+
else
|
43
|
+
callback_timeout
|
44
|
+
end
|
45
|
+
|
46
|
+
parsed = URI.parse(callback_url)
|
47
|
+
raise InvalidHostname.new(parsed.host) unless parsed.host == "127.0.0.1"
|
48
|
+
|
49
|
+
callback_port = parsed.port || 4567
|
50
|
+
callback_path = parsed.path.empty? ? "/" : parsed.path
|
51
|
+
|
52
|
+
cert_file, key_file = self.create_ssl_certificate
|
53
|
+
|
54
|
+
server_thread = SchwabRb::Auth::LoginFlowServer.run_in_thread(
|
55
|
+
callback_port: callback_port,
|
56
|
+
callback_path: callback_path,
|
57
|
+
cert_file: cert_file,
|
58
|
+
key_file: key_file
|
59
|
+
)
|
60
|
+
|
61
|
+
begin
|
62
|
+
# NOTE: wait for server to start
|
63
|
+
start_time = Time.now
|
64
|
+
while true
|
65
|
+
begin
|
66
|
+
uri = URI("https://127.0.0.1:#{callback_port}/status")
|
67
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
68
|
+
http.use_ssl = true
|
69
|
+
http.ca_file = cert_file.path
|
70
|
+
|
71
|
+
http.set_debug_output($stdout)
|
72
|
+
|
73
|
+
resp = http.get(uri.path)
|
74
|
+
|
75
|
+
break if resp.is_a?(Net::HTTPSuccess)
|
76
|
+
rescue Errno::ECONNREFUSED
|
77
|
+
sleep 0.1
|
78
|
+
end
|
79
|
+
|
80
|
+
raise RedirectServerExitedError if Time.now - start_time > 5
|
81
|
+
end
|
82
|
+
|
83
|
+
auth_context = self.build_auth_context(api_key, callback_url)
|
84
|
+
|
85
|
+
puts <<~MESSAGE
|
86
|
+
***********************************************************************
|
87
|
+
Open this URL in your browser to log in:
|
88
|
+
#{auth_context.authorization_url}
|
89
|
+
***********************************************************************
|
90
|
+
MESSAGE
|
91
|
+
|
92
|
+
if interactive
|
93
|
+
puts "Press ENTER to open the browser..."
|
94
|
+
gets
|
95
|
+
end
|
96
|
+
|
97
|
+
`open "#{auth_context.authorization_url}}"`
|
98
|
+
|
99
|
+
timeout_time = Time.now + callback_timeout
|
100
|
+
received_url = nil
|
101
|
+
|
102
|
+
while Time.now < timeout_time
|
103
|
+
unless LoginFlowServer.queue.empty?
|
104
|
+
received_url = LoginFlowServer.queue.pop
|
105
|
+
break
|
106
|
+
end
|
107
|
+
sleep 0.1
|
108
|
+
end
|
109
|
+
|
110
|
+
raise RedirectTimeoutError.new unless received_url
|
111
|
+
|
112
|
+
self.client_from_received_url(
|
113
|
+
api_key,
|
114
|
+
app_secret,
|
115
|
+
auth_context,
|
116
|
+
received_url,
|
117
|
+
token_path
|
118
|
+
)
|
119
|
+
ensure
|
120
|
+
LoginFlowServer.stop
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def self.create_ssl_certificate
|
125
|
+
key = OpenSSL::PKey::RSA.new(2048)
|
126
|
+
cert = OpenSSL::X509::Certificate.new
|
127
|
+
|
128
|
+
cert.subject = OpenSSL::X509::Name.parse("/CN=127.0.0.1")
|
129
|
+
cert.issuer = cert.subject
|
130
|
+
cert.public_key = key.public_key
|
131
|
+
cert.not_before = Time.now
|
132
|
+
cert.not_after = Time.now + (60 * 60 * 24) # 1 day
|
133
|
+
cert.serial = 0x0
|
134
|
+
cert.version = 2
|
135
|
+
cert.sign(key, OpenSSL::Digest::SHA256.new)
|
136
|
+
|
137
|
+
cert_file = Tempfile.new("cert.pem")
|
138
|
+
cert_file.write(cert.to_pem)
|
139
|
+
cert_file.close
|
140
|
+
|
141
|
+
key_file = Tempfile.new("key.pem")
|
142
|
+
key_file.write(key.to_pem)
|
143
|
+
key_file.close
|
144
|
+
|
145
|
+
return cert_file, key_file
|
146
|
+
end
|
147
|
+
|
148
|
+
def self.build_auth_context(api_key, callback_url, state: nil)
|
149
|
+
oauth = OAuth2::Client.new(
|
150
|
+
api_key,
|
151
|
+
nil,
|
152
|
+
site: SchwabRb::Constants::SCHWAB_BASE_URL,
|
153
|
+
authorize_url: "/v1/oauth/authorize",
|
154
|
+
connection_opts: { ssl: { verify: false } }
|
155
|
+
)
|
156
|
+
|
157
|
+
auth_params = { redirect_uri: callback_url }
|
158
|
+
auth_params[:state] = state if state
|
159
|
+
authorization_url = oauth.auth_code.authorize_url(auth_params)
|
160
|
+
|
161
|
+
AuthContext.new(callback_url, authorization_url, state)
|
162
|
+
end
|
163
|
+
|
164
|
+
def self.client_from_received_url(
|
165
|
+
api_key, app_secret, auth_context, received_url, token_path, enforce_enums: true
|
166
|
+
)
|
167
|
+
oauth = OAuth2::Client.new(
|
168
|
+
api_key,
|
169
|
+
app_secret,
|
170
|
+
site: SchwabRb::Constants::SCHWAB_BASE_URL,
|
171
|
+
token_url: "/v1/oauth/token"
|
172
|
+
)
|
173
|
+
uri = URI.parse(received_url)
|
174
|
+
params = URI.decode_www_form(uri.query).to_h
|
175
|
+
authorization_code = params["code"]
|
176
|
+
|
177
|
+
token = oauth.auth_code.get_token(authorization_code, redirect_uri: auth_context.callback_url)
|
178
|
+
|
179
|
+
metadata_manager = SchwabRb::Auth::TokenManager.from_oauth2_token(
|
180
|
+
token,
|
181
|
+
Time.now.to_i,
|
182
|
+
token_path: token_path
|
183
|
+
)
|
184
|
+
metadata_manager.to_file
|
185
|
+
|
186
|
+
session = OAuth2::AccessToken.new(
|
187
|
+
oauth,
|
188
|
+
token.token,
|
189
|
+
refresh_token: token.refresh_token,
|
190
|
+
expires_at: token.expires_at
|
191
|
+
)
|
192
|
+
|
193
|
+
SchwabRb::Client.new(
|
194
|
+
api_key,
|
195
|
+
app_secret,
|
196
|
+
session,
|
197
|
+
token_manager: metadata_manager,
|
198
|
+
enforce_enums: enforce_enums
|
199
|
+
)
|
200
|
+
end
|
201
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require "oauth2"
|
2
|
+
|
3
|
+
module SchwabRb::Auth
|
4
|
+
def self.init_client_token_file(api_key, app_secret, token_path, enforce_enums: true)
|
5
|
+
oauth = OAuth2::Client.new(
|
6
|
+
api_key,
|
7
|
+
app_secret,
|
8
|
+
site: SchwabRb::Constants::SCHWAB_BASE_URL,
|
9
|
+
token_url: "/v1/oauth/token"
|
10
|
+
)
|
11
|
+
|
12
|
+
metadata_manager = SchwabRb::Auth::TokenManager.from_file(token_path)
|
13
|
+
token = metadata_manager.token
|
14
|
+
|
15
|
+
session = OAuth2::AccessToken.new(
|
16
|
+
oauth,
|
17
|
+
token.token,
|
18
|
+
refresh_token: token.refresh_token,
|
19
|
+
expires_at: token.expires_at
|
20
|
+
)
|
21
|
+
|
22
|
+
SchwabRb::Client.new(
|
23
|
+
api_key,
|
24
|
+
app_secret,
|
25
|
+
session,
|
26
|
+
token_manager: metadata_manager,
|
27
|
+
enforce_enums: enforce_enums
|
28
|
+
)
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require "sinatra"
|
2
|
+
require "puma"
|
3
|
+
require "thread"
|
4
|
+
|
5
|
+
module SchwabRb::Auth
|
6
|
+
class LoginFlowServer < Sinatra::Base
|
7
|
+
class << self
|
8
|
+
attr_accessor :queue
|
9
|
+
end
|
10
|
+
|
11
|
+
self.queue = Queue.new
|
12
|
+
|
13
|
+
self.disable :logging
|
14
|
+
|
15
|
+
get "/status" do
|
16
|
+
"running"
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.create_routes(root_path)
|
20
|
+
get root_path do
|
21
|
+
self.class.queue.push(request.url)
|
22
|
+
"Callback received! You may now close this window/tab."
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.run_in_thread(callback_port: 4567, callback_path: "/", cert_file: nil, key_file: nil)
|
27
|
+
create_routes(callback_path)
|
28
|
+
|
29
|
+
thread = Thread.new do
|
30
|
+
set :server, "puma"
|
31
|
+
set :port, callback_port
|
32
|
+
set :bind, "127.0.0.1"
|
33
|
+
|
34
|
+
ctx = Puma::MiniSSL::Context.new.tap do |ctx|
|
35
|
+
ctx.key = key_file.path
|
36
|
+
ctx.cert = cert_file.path
|
37
|
+
ctx.verify_mode=Puma::MiniSSL::VERIFY_NONE
|
38
|
+
end
|
39
|
+
|
40
|
+
puts ctx.inspect
|
41
|
+
|
42
|
+
Puma::Server.new(self).tap do |server|
|
43
|
+
server.add_ssl_listener("127.0.0.1", callback_port, ctx)
|
44
|
+
server.run
|
45
|
+
end
|
46
|
+
end
|
47
|
+
sleep 0.5
|
48
|
+
thread
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.stop
|
52
|
+
Thread.list.each { |t| t.exit if t != Thread.main }
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module SchwabRb::Auth
|
2
|
+
class Token
|
3
|
+
def initialize(
|
4
|
+
token: nil,
|
5
|
+
expires_in: nil,
|
6
|
+
token_type: "Bearer",
|
7
|
+
scope: nil,
|
8
|
+
refresh_token: nil,
|
9
|
+
id_token: nil,
|
10
|
+
expires_at: nil
|
11
|
+
)
|
12
|
+
@token = token
|
13
|
+
@expires_in = expires_in
|
14
|
+
@token_type = token_type
|
15
|
+
@scope = scope
|
16
|
+
@refresh_token = refresh_token
|
17
|
+
@id_token = id_token
|
18
|
+
@expires_at = expires_at
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_reader :token, :expires_in, :token_type, :scope,
|
22
|
+
:refresh_token, :id_token, :expires_at
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
require 'oauth2'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module SchwabRb::Auth
|
5
|
+
class TokenManager
|
6
|
+
class << self
|
7
|
+
def from_file(token_path)
|
8
|
+
token_data = JSON.parse(File.read(token_path))
|
9
|
+
token = SchwabRb::Auth::Token.new(
|
10
|
+
token: token_data["token"]["access_token"],
|
11
|
+
expires_in: token_data["token"]["expires_in"],
|
12
|
+
token_type: token_data["token"]["token_type"],
|
13
|
+
scope: token_data["token"]["scope"],
|
14
|
+
refresh_token: token_data["token"]["refresh_token"],
|
15
|
+
id_token: token_data["token"]["id_token"],
|
16
|
+
expires_at: token_data["token"]["expires_at"]
|
17
|
+
)
|
18
|
+
|
19
|
+
TokenManager.new(token, token_data["timestamp"], token_path: token_path)
|
20
|
+
end
|
21
|
+
|
22
|
+
def from_oauth2_token(oauth2_token, timestamp, token_path: SchwabRb::Constants::DEFAULT_TOKEN_PATH)
|
23
|
+
token = SchwabRb::Auth::Token.new(
|
24
|
+
token: oauth2_token.token,
|
25
|
+
expires_in: oauth2_token.expires_in,
|
26
|
+
token_type: oauth2_token.params["token_type"] || "Bearer",
|
27
|
+
scope: oauth2_token.params["scope"],
|
28
|
+
refresh_token: oauth2_token.refresh_token,
|
29
|
+
id_token: oauth2_token.params["id_token"],
|
30
|
+
expires_at: oauth2_token.expires_at
|
31
|
+
)
|
32
|
+
|
33
|
+
TokenManager.new(token, timestamp, token_path: token_path)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def initialize(token, timestamp, token_path: SchwabRb::Constants::DEFAULT_TOKEN_PATH)
|
38
|
+
@token = token
|
39
|
+
@timestamp = timestamp
|
40
|
+
@token_path = token_path
|
41
|
+
end
|
42
|
+
|
43
|
+
attr_reader :token, :timestamp, :token_path
|
44
|
+
|
45
|
+
def refresh_token(client)
|
46
|
+
new_token = client.session.refresh!
|
47
|
+
|
48
|
+
@token = SchwabRb::Auth::Token.new(
|
49
|
+
token: new_token.token,
|
50
|
+
expires_in: new_token.expires_in,
|
51
|
+
token_type: new_token.params["token_type"] || "Bearer",
|
52
|
+
scope: new_token.params["scope"],
|
53
|
+
refresh_token: new_token.refresh_token,
|
54
|
+
id_token: new_token.params["id_token"],
|
55
|
+
expires_at: new_token.expires_at
|
56
|
+
)
|
57
|
+
@timestamp = Time.now.to_i
|
58
|
+
|
59
|
+
to_file
|
60
|
+
|
61
|
+
oauth = OAuth2::Client.new(
|
62
|
+
client.api_key,
|
63
|
+
client.app_secret,
|
64
|
+
site: SchwabRb::Constants::SCHWAB_BASE_URL,
|
65
|
+
token_url: "/v1/oauth/token"
|
66
|
+
)
|
67
|
+
|
68
|
+
OAuth2::AccessToken.new(
|
69
|
+
oauth,
|
70
|
+
token.token,
|
71
|
+
refresh_token: token.refresh_token,
|
72
|
+
expires_at: token.expires_at
|
73
|
+
)
|
74
|
+
end
|
75
|
+
|
76
|
+
def to_file
|
77
|
+
File.open(token_path, "w") do |f|
|
78
|
+
f.write(to_json)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def token_age
|
83
|
+
Time.now.to_i - timestamp
|
84
|
+
end
|
85
|
+
|
86
|
+
def to_h
|
87
|
+
token_data = {
|
88
|
+
timestamp: timestamp,
|
89
|
+
token: {
|
90
|
+
expires_in: token.expires_in,
|
91
|
+
token_type: token.token_type,
|
92
|
+
scope: token.scope,
|
93
|
+
refresh_token: token.refresh_token,
|
94
|
+
access_token: token.token,
|
95
|
+
id_token: token.id_token,
|
96
|
+
expires_at: token.expires_at
|
97
|
+
}
|
98
|
+
}
|
99
|
+
end
|
100
|
+
|
101
|
+
def to_json
|
102
|
+
to_h.to_json
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
require 'async'
|
2
|
+
require 'async/http'
|
3
|
+
require 'json'
|
4
|
+
require 'uri'
|
5
|
+
require_relative 'base_client'
|
6
|
+
require_relative '../utils/logger'
|
7
|
+
require_relative '../utils/redactor'
|
8
|
+
require_relative '../constants'
|
9
|
+
|
10
|
+
module SchwabRb
|
11
|
+
class AsyncClient < BaseClient
|
12
|
+
def initialize(api_key, app_secret, session, token_manager:, enforce_enums: true)
|
13
|
+
super
|
14
|
+
@endpoint = Async::HTTP::Endpoint.parse(SchwabRb::Constants::SCHWAB_BASE_URL)
|
15
|
+
@client = Async::HTTP::Client.new(@endpoint)
|
16
|
+
end
|
17
|
+
|
18
|
+
def close_async_session
|
19
|
+
@client.close
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def get(path, params = {})
|
25
|
+
Async do
|
26
|
+
refresh_token_if_needed
|
27
|
+
dest = URI(URI::DEFAULT_PARSER.escape("#{SchwabRb::Constants::SCHWAB_BASE_URL}#{path}"))
|
28
|
+
dest.query = URI.encode_www_form(params) if params.any?
|
29
|
+
|
30
|
+
req_num = req_num()
|
31
|
+
log_request('GET', req_num, dest, params)
|
32
|
+
|
33
|
+
# Use path only since @endpoint already has the base URL
|
34
|
+
query_string = params.any? ? "?#{URI.encode_www_form(params)}" : ""
|
35
|
+
response = @client.get("#{path}#{query_string}", build_headers)
|
36
|
+
|
37
|
+
log_response(response, req_num)
|
38
|
+
response
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def post(path, data = {})
|
43
|
+
Async do
|
44
|
+
refresh_token_if_needed
|
45
|
+
dest = URI(URI::DEFAULT_PARSER.escape("#{SchwabRb::Constants::SCHWAB_BASE_URL}#{path}"))
|
46
|
+
|
47
|
+
req_num = req_num()
|
48
|
+
log_request('POST', req_num, dest, data)
|
49
|
+
|
50
|
+
response = @client.post(path, build_headers, JSON.dump(data))
|
51
|
+
|
52
|
+
log_response(response, req_num)
|
53
|
+
response
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def put(path, data = {})
|
58
|
+
Async do
|
59
|
+
refresh_token_if_needed
|
60
|
+
dest = URI(URI::DEFAULT_PARSER.escape("#{SchwabRb::Constants::SCHWAB_BASE_URL}#{path}"))
|
61
|
+
|
62
|
+
req_num = req_num()
|
63
|
+
log_request('PUT', req_num, dest, data)
|
64
|
+
|
65
|
+
response = @client.put(path, build_headers, JSON.dump(data))
|
66
|
+
|
67
|
+
log_response(response, req_num)
|
68
|
+
response
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def delete(path)
|
73
|
+
Async do
|
74
|
+
refresh_token_if_needed
|
75
|
+
dest = URI(URI::DEFAULT_PARSER.escape("#{SchwabRb::Constants::SCHWAB_BASE_URL}#{path}"))
|
76
|
+
|
77
|
+
req_num = req_num()
|
78
|
+
log_request('DELETE', req_num, dest)
|
79
|
+
|
80
|
+
response = @client.delete(path, build_headers)
|
81
|
+
|
82
|
+
log_response(response, req_num)
|
83
|
+
response
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def build_headers
|
88
|
+
headers = { "Content-Type" => "application/json" }
|
89
|
+
|
90
|
+
# Add authorization header if token is available
|
91
|
+
if @token_manager&.access_token
|
92
|
+
headers["Authorization"] = "Bearer #{@token_manager.access_token}"
|
93
|
+
end
|
94
|
+
|
95
|
+
headers
|
96
|
+
end
|
97
|
+
|
98
|
+
def log_request(method, req_num, dest, data = nil)
|
99
|
+
redacted_dest = SchwabRb::Redactor.redact_url(dest.to_s)
|
100
|
+
SchwabRb::Logger.logger.info("Req #{req_num}: #{method} to #{redacted_dest}")
|
101
|
+
|
102
|
+
if data
|
103
|
+
redacted_data = SchwabRb::Redactor.redact_data(data)
|
104
|
+
SchwabRb::Logger.logger.debug("Payload: #{JSON.pretty_generate(redacted_data)}")
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def log_response(response, req_num)
|
109
|
+
SchwabRb::Logger.logger.info("Resp #{req_num}: Status #{response.status}")
|
110
|
+
|
111
|
+
if SchwabRb::Logger.logger.level == ::Logger::DEBUG
|
112
|
+
redacted_body = SchwabRb::Redactor.redact_response_body(response)
|
113
|
+
SchwabRb::Logger.logger.debug("Response body: #{JSON.pretty_generate(redacted_body)}") if redacted_body
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def req_num
|
118
|
+
@request_counter ||= 0
|
119
|
+
@request_counter += 1
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|