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.
Files changed (71) hide show
  1. checksums.yaml +7 -0
  2. data/.copilotignore +4 -0
  3. data/.rspec +2 -0
  4. data/.rspec_status +292 -0
  5. data/.rubocop.yml +41 -0
  6. data/.rubocop_todo.yml +105 -0
  7. data/CHANGELOG.md +28 -0
  8. data/LICENSE.txt +23 -0
  9. data/README.md +271 -0
  10. data/Rakefile +12 -0
  11. data/doc/notes/data_objects_analysis.md +223 -0
  12. data/doc/notes/data_objects_refactoring_plan.md +82 -0
  13. data/examples/fetch_account_numbers.rb +49 -0
  14. data/examples/fetch_user_preferences.rb +49 -0
  15. data/lib/schwab_rb/account.rb +9 -0
  16. data/lib/schwab_rb/auth/auth_context.rb +23 -0
  17. data/lib/schwab_rb/auth/init_client_easy.rb +45 -0
  18. data/lib/schwab_rb/auth/init_client_login.rb +201 -0
  19. data/lib/schwab_rb/auth/init_client_token_file.rb +30 -0
  20. data/lib/schwab_rb/auth/login_flow_server.rb +55 -0
  21. data/lib/schwab_rb/auth/token.rb +24 -0
  22. data/lib/schwab_rb/auth/token_manager.rb +105 -0
  23. data/lib/schwab_rb/clients/async_client.rb +122 -0
  24. data/lib/schwab_rb/clients/base_client.rb +887 -0
  25. data/lib/schwab_rb/clients/client.rb +97 -0
  26. data/lib/schwab_rb/configuration.rb +39 -0
  27. data/lib/schwab_rb/constants.rb +7 -0
  28. data/lib/schwab_rb/data_objects/account.rb +281 -0
  29. data/lib/schwab_rb/data_objects/account_numbers.rb +68 -0
  30. data/lib/schwab_rb/data_objects/instrument.rb +156 -0
  31. data/lib/schwab_rb/data_objects/market_hours.rb +275 -0
  32. data/lib/schwab_rb/data_objects/option.rb +147 -0
  33. data/lib/schwab_rb/data_objects/option_chain.rb +95 -0
  34. data/lib/schwab_rb/data_objects/option_expiration_chain.rb +134 -0
  35. data/lib/schwab_rb/data_objects/order.rb +186 -0
  36. data/lib/schwab_rb/data_objects/order_leg.rb +68 -0
  37. data/lib/schwab_rb/data_objects/order_preview.rb +237 -0
  38. data/lib/schwab_rb/data_objects/position.rb +100 -0
  39. data/lib/schwab_rb/data_objects/price_history.rb +187 -0
  40. data/lib/schwab_rb/data_objects/quote.rb +276 -0
  41. data/lib/schwab_rb/data_objects/transaction.rb +132 -0
  42. data/lib/schwab_rb/data_objects/user_preferences.rb +129 -0
  43. data/lib/schwab_rb/market_hours.rb +13 -0
  44. data/lib/schwab_rb/movers.rb +35 -0
  45. data/lib/schwab_rb/option.rb +64 -0
  46. data/lib/schwab_rb/orders/builder.rb +202 -0
  47. data/lib/schwab_rb/orders/destination.rb +19 -0
  48. data/lib/schwab_rb/orders/duration.rb +9 -0
  49. data/lib/schwab_rb/orders/equity_instructions.rb +10 -0
  50. data/lib/schwab_rb/orders/errors.rb +5 -0
  51. data/lib/schwab_rb/orders/instruments.rb +35 -0
  52. data/lib/schwab_rb/orders/option_instructions.rb +10 -0
  53. data/lib/schwab_rb/orders/order.rb +77 -0
  54. data/lib/schwab_rb/orders/price_link_basis.rb +15 -0
  55. data/lib/schwab_rb/orders/price_link_type.rb +9 -0
  56. data/lib/schwab_rb/orders/session.rb +14 -0
  57. data/lib/schwab_rb/orders/special_instruction.rb +10 -0
  58. data/lib/schwab_rb/orders/stop_price_link_basis.rb +15 -0
  59. data/lib/schwab_rb/orders/stop_price_link_type.rb +9 -0
  60. data/lib/schwab_rb/orders/stop_type.rb +11 -0
  61. data/lib/schwab_rb/orders/tax_lot_method.rb +13 -0
  62. data/lib/schwab_rb/price_history.rb +55 -0
  63. data/lib/schwab_rb/quote.rb +13 -0
  64. data/lib/schwab_rb/transaction.rb +23 -0
  65. data/lib/schwab_rb/utils/enum_enforcer.rb +73 -0
  66. data/lib/schwab_rb/utils/logger.rb +70 -0
  67. data/lib/schwab_rb/utils/redactor.rb +104 -0
  68. data/lib/schwab_rb/version.rb +5 -0
  69. data/lib/schwab_rb.rb +48 -0
  70. data/sig/schwab_rb.rbs +4 -0
  71. metadata +289 -0
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SchwabRb
4
+ class Account
5
+ module Statuses
6
+ POSITIONS = 'positions'
7
+ end
8
+ end
9
+ end
@@ -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