schwab_rb 0.2.0 → 0.3.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 +4 -4
- data/.claude/settings.local.json +9 -0
- data/.rspec_status +209 -180
- data/CLAUDE.md +137 -0
- data/README.md +3 -3
- data/examples/fetch_account_numbers.rb +12 -15
- data/examples/fetch_user_preferences.rb +16 -19
- data/lib/schwab_rb/account.rb +1 -1
- data/lib/schwab_rb/auth/auth_context.rb +1 -1
- data/lib/schwab_rb/auth/init_client_easy.rb +29 -31
- data/lib/schwab_rb/auth/init_client_login.rb +24 -21
- data/lib/schwab_rb/auth/login_flow_server.rb +2 -2
- data/lib/schwab_rb/auth/token.rb +1 -1
- data/lib/schwab_rb/auth/token_manager.rb +5 -7
- data/lib/schwab_rb/clients/async_client.rb +25 -27
- data/lib/schwab_rb/clients/base_client.rb +22 -16
- data/lib/schwab_rb/clients/client.rb +14 -14
- data/lib/schwab_rb/configuration.rb +4 -4
- data/lib/schwab_rb/data_objects/account.rb +2 -2
- data/lib/schwab_rb/data_objects/instrument.rb +1 -1
- data/lib/schwab_rb/data_objects/market_hours.rb +43 -33
- data/lib/schwab_rb/data_objects/market_movers.rb +98 -0
- data/lib/schwab_rb/data_objects/option.rb +2 -2
- data/lib/schwab_rb/data_objects/option_chain.rb +7 -7
- data/lib/schwab_rb/data_objects/option_expiration_chain.rb +26 -25
- data/lib/schwab_rb/data_objects/order.rb +7 -6
- data/lib/schwab_rb/data_objects/order_leg.rb +5 -5
- data/lib/schwab_rb/data_objects/order_preview.rb +13 -16
- data/lib/schwab_rb/data_objects/position.rb +4 -4
- data/lib/schwab_rb/data_objects/price_history.rb +27 -19
- data/lib/schwab_rb/data_objects/quote.rb +6 -6
- data/lib/schwab_rb/data_objects/transaction.rb +6 -6
- data/lib/schwab_rb/data_objects/user_preferences.rb +3 -3
- data/lib/schwab_rb/market_hours.rb +5 -5
- data/lib/schwab_rb/movers.rb +16 -16
- data/lib/schwab_rb/orders/builder.rb +5 -5
- data/lib/schwab_rb/orders/destination.rb +12 -12
- data/lib/schwab_rb/orders/duration.rb +7 -7
- data/lib/schwab_rb/orders/equity_instructions.rb +4 -4
- data/lib/schwab_rb/orders/instruments.rb +8 -8
- data/lib/schwab_rb/orders/price_link_basis.rb +9 -9
- data/lib/schwab_rb/orders/price_link_type.rb +3 -3
- data/lib/schwab_rb/orders/session.rb +4 -4
- data/lib/schwab_rb/orders/special_instruction.rb +3 -3
- data/lib/schwab_rb/orders/stop_price_link_basis.rb +9 -9
- data/lib/schwab_rb/orders/stop_price_link_type.rb +3 -3
- data/lib/schwab_rb/orders/stop_type.rb +5 -5
- data/lib/schwab_rb/orders/tax_lot_method.rb +7 -7
- data/lib/schwab_rb/price_history.rb +8 -8
- data/lib/schwab_rb/quote.rb +5 -5
- data/lib/schwab_rb/transaction.rb +15 -15
- data/lib/schwab_rb/utils/logger.rb +11 -15
- data/lib/schwab_rb/utils/redactor.rb +23 -25
- data/lib/schwab_rb/version.rb +1 -1
- data/lib/schwab_rb.rb +1 -0
- metadata +6 -2
data/CLAUDE.md
ADDED
@@ -0,0 +1,137 @@
|
|
1
|
+
# CLAUDE.md
|
2
|
+
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
4
|
+
|
5
|
+
## Development Commands
|
6
|
+
|
7
|
+
### Setup and Installation
|
8
|
+
- `bin/setup` - Install dependencies and set up the development environment
|
9
|
+
- `bundle install` - Install gem dependencies
|
10
|
+
- `bin/console` - Start an interactive Ruby console with the gem loaded
|
11
|
+
|
12
|
+
### Testing
|
13
|
+
- `bundle exec rake spec` - Run all RSpec tests
|
14
|
+
- `bundle exec rspec` - Run RSpec tests directly
|
15
|
+
- `bundle exec rspec spec/path/to/specific_spec.rb` - Run a specific test file
|
16
|
+
- `bundle exec rspec spec/path/to/specific_spec.rb:line_number` - Run a specific test
|
17
|
+
|
18
|
+
### Linting and Code Quality
|
19
|
+
- `bundle exec rubocop` - Run RuboCop linter
|
20
|
+
- `bundle exec rubocop -a` - Auto-correct RuboCop violations where possible
|
21
|
+
- `bundle exec rake` - Run both tests and linting (default task)
|
22
|
+
|
23
|
+
### Gem Management
|
24
|
+
- `bundle exec rake build` - Build the gem
|
25
|
+
- `bundle exec rake install` - Install the gem locally
|
26
|
+
- `bundle exec rake release` - Release a new version (creates git tag and pushes to RubyGems)
|
27
|
+
|
28
|
+
## Architecture Overview
|
29
|
+
|
30
|
+
### Core Structure
|
31
|
+
This is a Ruby gem (`schwab_rb`) that provides a client library for the Charles Schwab API. The architecture follows a modular design:
|
32
|
+
|
33
|
+
**Authentication Layer** (`lib/schwab_rb/auth/`):
|
34
|
+
- `init_client_easy.rb` - Recommended initialization method that handles token management automatically
|
35
|
+
- `init_client_login.rb` - Interactive browser-based authentication flow
|
36
|
+
- `init_client_token_file.rb` - Initialize from existing token file
|
37
|
+
- `token_manager.rb` - Handles token refresh and persistence
|
38
|
+
- `login_flow_server.rb` - Temporary server for OAuth callback handling
|
39
|
+
|
40
|
+
**Client Layer** (`lib/schwab_rb/clients/`):
|
41
|
+
- `client.rb` - Synchronous HTTP client for API calls
|
42
|
+
- `async_client.rb` - Asynchronous client using the `async` gem
|
43
|
+
- `base_client.rb` - Shared client functionality
|
44
|
+
|
45
|
+
**Data Objects** (`lib/schwab_rb/data_objects/`):
|
46
|
+
- Structured Ruby objects for API responses (Account, Quote, Order, etc.)
|
47
|
+
- Replaces raw JSON responses with typed objects for better developer experience
|
48
|
+
- All API methods support `return_data_objects: false` to get raw JSON if needed
|
49
|
+
|
50
|
+
**Order Management** (`lib/schwab_rb/orders/`):
|
51
|
+
- `builder.rb` - Fluent interface for constructing complex orders
|
52
|
+
- Various enum classes for order parameters (duration, session, instructions, etc.)
|
53
|
+
|
54
|
+
### Key Design Patterns
|
55
|
+
|
56
|
+
**Three-Tier Client Initialization**:
|
57
|
+
1. `init_client_easy()` - Handles everything automatically (recommended)
|
58
|
+
2. `init_client_token_file()` - For existing tokens
|
59
|
+
3. `init_client_login()` - For interactive authentication
|
60
|
+
|
61
|
+
**Data Object Strategy**:
|
62
|
+
- All API methods return structured Ruby objects by default
|
63
|
+
- Use `return_data_objects: false` for raw JSON responses
|
64
|
+
- Data objects are built from JSON using factory patterns
|
65
|
+
|
66
|
+
**Async Support**:
|
67
|
+
- Both sync and async clients available
|
68
|
+
- Async operations use the `async` gem with promises
|
69
|
+
- Set `asyncio: true` during client initialization
|
70
|
+
|
71
|
+
### Configuration
|
72
|
+
|
73
|
+
Environment variables (loaded via `dotenv`):
|
74
|
+
- `SCHWAB_API_KEY` - Your Schwab API key
|
75
|
+
- `SCHWAB_APP_SECRET` - Your Schwab application secret
|
76
|
+
- `APP_CALLBACK_URL` - OAuth callback URL
|
77
|
+
- `TOKEN_PATH` - Path for token storage
|
78
|
+
- `SCHWAB_ACCOUNT_NUMBER` - Your account number
|
79
|
+
- `SCHWAB_LOGFILE` - Log file path (optional, defaults to STDOUT)
|
80
|
+
- `SCHWAB_LOG_LEVEL` - Log level (DEBUG, INFO, WARN, ERROR, FATAL)
|
81
|
+
- `SCHWAB_SILENCE_OUTPUT` - Set to 'true' to disable logging
|
82
|
+
|
83
|
+
Programmatic configuration:
|
84
|
+
```ruby
|
85
|
+
SchwabRb.configure do |config|
|
86
|
+
config.logger = Logger.new(STDOUT)
|
87
|
+
config.log_level = 'INFO'
|
88
|
+
config.silence_output = false
|
89
|
+
end
|
90
|
+
```
|
91
|
+
|
92
|
+
### Testing Infrastructure
|
93
|
+
|
94
|
+
**Fixtures and Factories**:
|
95
|
+
- JSON fixtures in `spec/fixtures/` for realistic API responses
|
96
|
+
- Factory classes in `spec/factories/` for creating test objects
|
97
|
+
- Extensive mocking of HTTP responses for unit tests
|
98
|
+
|
99
|
+
**Test Organization**:
|
100
|
+
- Unit tests mirror the `lib/` structure
|
101
|
+
- Separate specs for data objects, clients, auth, and orders
|
102
|
+
- Uses `async-rspec` for testing async functionality
|
103
|
+
|
104
|
+
### Key Dependencies
|
105
|
+
- `oauth2` - OAuth2 authentication
|
106
|
+
- `async` + `async-http` - Asynchronous HTTP operations
|
107
|
+
- `sinatra` + `puma` - Temporary server for OAuth callbacks
|
108
|
+
- `dotenv` - Environment variable management
|
109
|
+
- `rspec` + `async-rspec` - Testing framework
|
110
|
+
- `rubocop` - Code linting
|
111
|
+
|
112
|
+
## Common Patterns
|
113
|
+
|
114
|
+
### Client Initialization
|
115
|
+
Always use `init_client_easy()` for new development:
|
116
|
+
```ruby
|
117
|
+
client = SchwabRb::Auth.init_client_easy(
|
118
|
+
ENV['SCHWAB_API_KEY'],
|
119
|
+
ENV['SCHWAB_APP_SECRET'],
|
120
|
+
ENV['APP_CALLBACK_URL'],
|
121
|
+
ENV['TOKEN_PATH']
|
122
|
+
)
|
123
|
+
```
|
124
|
+
|
125
|
+
### Order Building
|
126
|
+
Use the fluent builder pattern for complex orders:
|
127
|
+
```ruby
|
128
|
+
order = SchwabRb::Orders::Builder.new
|
129
|
+
.set_session(:normal)
|
130
|
+
.set_duration(:day)
|
131
|
+
.set_order_type(:market)
|
132
|
+
.add_equity_leg(:buy, 'AAPL', 100)
|
133
|
+
.build
|
134
|
+
```
|
135
|
+
|
136
|
+
### Error Handling
|
137
|
+
Token expiration is handled automatically by `init_client_easy()`. Custom error classes are defined in `lib/schwab_rb/orders/errors.rb`.
|
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
|
1
|
+
# schwab_rb: Schwab API Ruby Client
|
2
2
|
|
3
3
|
The `schwab_rb` gem is a Ruby client for interacting with the Schwab API. It provides a simple and flexible interface for accessing Schwab account data, placing orders, retrieving quotes, and more.
|
4
4
|
|
@@ -204,7 +204,7 @@ response = client.place_order('account_hash', order)
|
|
204
204
|
The gem supports multiple authentication approaches:
|
205
205
|
|
206
206
|
1. **Easy Initialization** (Recommended): Automatically handles token storage and refresh
|
207
|
-
2. **Token File**: Initialize from a saved token file
|
207
|
+
2. **Token File**: Initialize from a saved token file
|
208
208
|
3. **Login Flow**: Interactive browser-based authentication
|
209
209
|
|
210
210
|
### Easy Authentication Setup
|
@@ -212,7 +212,7 @@ The gem supports multiple authentication approaches:
|
|
212
212
|
```ruby
|
213
213
|
client = SchwabRb::Auth.init_client_easy(
|
214
214
|
ENV['SCHWAB_API_KEY'],
|
215
|
-
ENV['SCHWAB_APP_SECRET'],
|
215
|
+
ENV['SCHWAB_APP_SECRET'],
|
216
216
|
ENV['APP_CALLBACK_URL'],
|
217
217
|
ENV['TOKEN_PATH']
|
218
218
|
)
|
@@ -4,19 +4,19 @@
|
|
4
4
|
# Script to fetch account numbers data and save as fixture
|
5
5
|
# Usage: ruby examples/fetch_account_numbers.rb
|
6
6
|
|
7
|
-
require_relative
|
8
|
-
require
|
9
|
-
require
|
10
|
-
require
|
7
|
+
require_relative "../lib/schwab_rb"
|
8
|
+
require "dotenv"
|
9
|
+
require "json"
|
10
|
+
require "fileutils"
|
11
11
|
|
12
12
|
Dotenv.load
|
13
13
|
|
14
14
|
def create_client
|
15
|
-
token_path = ENV[
|
15
|
+
token_path = ENV["TOKEN_PATH"] || "schwab_token.json"
|
16
16
|
SchwabRb::Auth.init_client_easy(
|
17
|
-
ENV
|
18
|
-
ENV
|
19
|
-
ENV
|
17
|
+
ENV.fetch("SCHWAB_API_KEY", nil),
|
18
|
+
ENV.fetch("SCHWAB_APP_SECRET", nil),
|
19
|
+
ENV.fetch("APP_CALLBACK_URL", nil),
|
20
20
|
token_path
|
21
21
|
)
|
22
22
|
end
|
@@ -29,21 +29,18 @@ def fetch_account_numbers
|
|
29
29
|
parsed_data = JSON.parse(response.body, symbolize_names: true)
|
30
30
|
|
31
31
|
# Create fixtures directory if it doesn't exist
|
32
|
-
fixtures_dir = File.join(__dir__,
|
32
|
+
fixtures_dir = File.join(__dir__, "..", "spec", "fixtures")
|
33
33
|
FileUtils.mkdir_p(fixtures_dir)
|
34
34
|
|
35
35
|
# Save the raw response
|
36
|
-
fixture_file = File.join(fixtures_dir,
|
36
|
+
fixture_file = File.join(fixtures_dir, "account_numbers.json")
|
37
37
|
File.write(fixture_file, JSON.pretty_generate(parsed_data))
|
38
38
|
|
39
39
|
puts "Account numbers data saved to: #{fixture_file}"
|
40
40
|
puts "Sample data: #{parsed_data.first(2)}"
|
41
|
-
|
42
|
-
rescue => e
|
41
|
+
rescue StandardError => e
|
43
42
|
puts "Error fetching account numbers: #{e.message}"
|
44
43
|
puts e.backtrace.first(3)
|
45
44
|
end
|
46
45
|
|
47
|
-
if __FILE__ == $0
|
48
|
-
fetch_account_numbers
|
49
|
-
end
|
46
|
+
fetch_account_numbers if __FILE__ == $0
|
@@ -4,19 +4,19 @@
|
|
4
4
|
# Script to fetch user preferences data and save as fixture
|
5
5
|
# Usage: ruby examples/fetch_user_preferences.rb
|
6
6
|
|
7
|
-
require_relative
|
8
|
-
require
|
9
|
-
require
|
10
|
-
require
|
7
|
+
require_relative "../lib/schwab_rb"
|
8
|
+
require "dotenv"
|
9
|
+
require "json"
|
10
|
+
require "fileutils"
|
11
11
|
|
12
12
|
Dotenv.load
|
13
13
|
|
14
14
|
def create_client
|
15
|
-
token_path = ENV[
|
15
|
+
token_path = ENV["TOKEN_PATH"] || "schwab_token.json"
|
16
16
|
SchwabRb::Auth.init_client_easy(
|
17
|
-
ENV
|
18
|
-
ENV
|
19
|
-
ENV
|
17
|
+
ENV.fetch("SCHWAB_API_KEY", nil),
|
18
|
+
ENV.fetch("SCHWAB_APP_SECRET", nil),
|
19
|
+
ENV.fetch("APP_CALLBACK_URL", nil),
|
20
20
|
token_path
|
21
21
|
)
|
22
22
|
end
|
@@ -24,26 +24,23 @@ end
|
|
24
24
|
def fetch_user_preferences
|
25
25
|
client = create_client
|
26
26
|
puts "Fetching user preferences..."
|
27
|
-
|
27
|
+
|
28
28
|
response = client.get_user_preferences
|
29
29
|
parsed_data = JSON.parse(response.body, symbolize_names: true)
|
30
|
-
|
30
|
+
|
31
31
|
# Create fixtures directory if it doesn't exist
|
32
|
-
fixtures_dir = File.join(__dir__,
|
32
|
+
fixtures_dir = File.join(__dir__, "..", "spec", "fixtures")
|
33
33
|
FileUtils.mkdir_p(fixtures_dir)
|
34
|
-
|
34
|
+
|
35
35
|
# Save the raw response
|
36
|
-
fixture_file = File.join(fixtures_dir,
|
36
|
+
fixture_file = File.join(fixtures_dir, "user_preferences.json")
|
37
37
|
File.write(fixture_file, JSON.pretty_generate(parsed_data))
|
38
|
-
|
38
|
+
|
39
39
|
puts "User preferences data saved to: #{fixture_file}"
|
40
40
|
puts "Sample data keys: #{parsed_data.keys.first(5)}"
|
41
|
-
|
42
|
-
rescue => e
|
41
|
+
rescue StandardError => e
|
43
42
|
puts "Error fetching user preferences: #{e.message}"
|
44
43
|
puts e.backtrace.first(3)
|
45
44
|
end
|
46
45
|
|
47
|
-
if __FILE__ == $0
|
48
|
-
fetch_user_preferences
|
49
|
-
end
|
46
|
+
fetch_user_preferences if __FILE__ == $0
|
data/lib/schwab_rb/account.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
|
-
require
|
2
|
-
require_relative
|
3
|
-
require_relative
|
1
|
+
require "oauth2"
|
2
|
+
require_relative "init_client_token_file"
|
3
|
+
require_relative "init_client_login"
|
4
4
|
|
5
5
|
module SchwabRb::Auth
|
6
6
|
def self.init_client_easy(
|
@@ -12,34 +12,32 @@ module SchwabRb::Auth
|
|
12
12
|
enforce_enums: false,
|
13
13
|
callback_timeout: 300.0,
|
14
14
|
interactive: true,
|
15
|
-
requested_browser: nil
|
15
|
+
requested_browser: nil
|
16
|
+
)
|
16
17
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
requested_browser: requested_browser
|
42
|
-
)
|
43
|
-
end
|
18
|
+
raise OAuth2::Error.new("No token found") unless File.exist?(token_path)
|
19
|
+
|
20
|
+
client = SchwabRb::Auth.init_client_token_file(
|
21
|
+
api_key,
|
22
|
+
app_secret,
|
23
|
+
token_path,
|
24
|
+
enforce_enums: enforce_enums
|
25
|
+
)
|
26
|
+
client.refresh! if client.session.expired?
|
27
|
+
raise OAuth2::Error.new("Token expired") if client.session.expired?
|
28
|
+
|
29
|
+
client
|
30
|
+
rescue StandardError
|
31
|
+
SchwabRb::Auth.init_client_login(
|
32
|
+
api_key,
|
33
|
+
app_secret,
|
34
|
+
callback_url,
|
35
|
+
token_path,
|
36
|
+
asyncio: asyncio,
|
37
|
+
enforce_enums: enforce_enums,
|
38
|
+
callback_timeout: callback_timeout,
|
39
|
+
interactive: interactive,
|
40
|
+
requested_browser: requested_browser
|
41
|
+
)
|
44
42
|
end
|
45
43
|
end
|
@@ -1,17 +1,19 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
1
|
+
require "openssl"
|
2
|
+
require "uri"
|
3
|
+
require "net/http"
|
4
|
+
require "json"
|
5
|
+
require "oauth2"
|
6
6
|
# require 'logger'
|
7
7
|
|
8
8
|
module SchwabRb::Auth
|
9
9
|
class RedirectTimeoutError < StandardError
|
10
|
-
def initialize(msg="Timed out waiting for a callback")
|
11
|
-
super
|
10
|
+
def initialize(msg = "Timed out waiting for a callback")
|
11
|
+
super
|
12
12
|
end
|
13
13
|
end
|
14
|
+
|
14
15
|
class RedirectServerExitedError < StandardError; end
|
16
|
+
|
15
17
|
# class TokenExchangeError < StandardError
|
16
18
|
# def initialize(msg)
|
17
19
|
# super(msg)
|
@@ -33,15 +35,16 @@ module SchwabRb::Auth
|
|
33
35
|
enforce_enums: false,
|
34
36
|
callback_timeout: 300.0,
|
35
37
|
interactive: true,
|
36
|
-
requested_browser: nil
|
38
|
+
requested_browser: nil
|
39
|
+
)
|
37
40
|
|
38
|
-
callback_timeout = if
|
39
|
-
|
41
|
+
callback_timeout = if !callback_timeout
|
42
|
+
0
|
40
43
|
elsif callback_timeout < 0
|
41
44
|
raise ArgumentError, "callback_timeout must be non-negative"
|
42
45
|
else
|
43
46
|
callback_timeout
|
44
|
-
|
47
|
+
end
|
45
48
|
|
46
49
|
parsed = URI.parse(callback_url)
|
47
50
|
raise InvalidHostname.new(parsed.host) unless parsed.host == "127.0.0.1"
|
@@ -49,9 +52,9 @@ module SchwabRb::Auth
|
|
49
52
|
callback_port = parsed.port || 4567
|
50
53
|
callback_path = parsed.path.empty? ? "/" : parsed.path
|
51
54
|
|
52
|
-
cert_file, key_file =
|
55
|
+
cert_file, key_file = create_ssl_certificate
|
53
56
|
|
54
|
-
|
57
|
+
SchwabRb::Auth::LoginFlowServer.run_in_thread(
|
55
58
|
callback_port: callback_port,
|
56
59
|
callback_path: callback_path,
|
57
60
|
cert_file: cert_file,
|
@@ -80,13 +83,13 @@ module SchwabRb::Auth
|
|
80
83
|
raise RedirectServerExitedError if Time.now - start_time > 5
|
81
84
|
end
|
82
85
|
|
83
|
-
auth_context =
|
86
|
+
auth_context = build_auth_context(api_key, callback_url)
|
84
87
|
|
85
88
|
puts <<~MESSAGE
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
89
|
+
***********************************************************************
|
90
|
+
Open this URL in your browser to log in:
|
91
|
+
#{auth_context.authorization_url}
|
92
|
+
***********************************************************************
|
90
93
|
MESSAGE
|
91
94
|
|
92
95
|
if interactive
|
@@ -109,7 +112,7 @@ module SchwabRb::Auth
|
|
109
112
|
|
110
113
|
raise RedirectTimeoutError.new unless received_url
|
111
114
|
|
112
|
-
|
115
|
+
client_from_received_url(
|
113
116
|
api_key,
|
114
117
|
app_secret,
|
115
118
|
auth_context,
|
@@ -132,7 +135,7 @@ module SchwabRb::Auth
|
|
132
135
|
cert.not_after = Time.now + (60 * 60 * 24) # 1 day
|
133
136
|
cert.serial = 0x0
|
134
137
|
cert.version = 2
|
135
|
-
cert.sign(key, OpenSSL::Digest
|
138
|
+
cert.sign(key, OpenSSL::Digest.new("SHA256"))
|
136
139
|
|
137
140
|
cert_file = Tempfile.new("cert.pem")
|
138
141
|
cert_file.write(cert.to_pem)
|
@@ -142,7 +145,7 @@ module SchwabRb::Auth
|
|
142
145
|
key_file.write(key.to_pem)
|
143
146
|
key_file.close
|
144
147
|
|
145
|
-
|
148
|
+
[cert_file, key_file]
|
146
149
|
end
|
147
150
|
|
148
151
|
def self.build_auth_context(api_key, callback_url, state: nil)
|
@@ -10,7 +10,7 @@ module SchwabRb::Auth
|
|
10
10
|
|
11
11
|
self.queue = Queue.new
|
12
12
|
|
13
|
-
|
13
|
+
disable :logging
|
14
14
|
|
15
15
|
get "/status" do
|
16
16
|
"running"
|
@@ -34,7 +34,7 @@ module SchwabRb::Auth
|
|
34
34
|
ctx = Puma::MiniSSL::Context.new.tap do |ctx|
|
35
35
|
ctx.key = key_file.path
|
36
36
|
ctx.cert = cert_file.path
|
37
|
-
ctx.verify_mode=Puma::MiniSSL::VERIFY_NONE
|
37
|
+
ctx.verify_mode = Puma::MiniSSL::VERIFY_NONE
|
38
38
|
end
|
39
39
|
|
40
40
|
puts ctx.inspect
|
data/lib/schwab_rb/auth/token.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
|
-
require
|
2
|
-
require
|
1
|
+
require "oauth2"
|
2
|
+
require "json"
|
3
3
|
|
4
4
|
module SchwabRb::Auth
|
5
5
|
class TokenManager
|
@@ -74,9 +74,7 @@ module SchwabRb::Auth
|
|
74
74
|
end
|
75
75
|
|
76
76
|
def to_file
|
77
|
-
File.
|
78
|
-
f.write(to_json)
|
79
|
-
end
|
77
|
+
File.write(token_path, to_json)
|
80
78
|
end
|
81
79
|
|
82
80
|
def token_age
|
@@ -84,7 +82,7 @@ module SchwabRb::Auth
|
|
84
82
|
end
|
85
83
|
|
86
84
|
def to_h
|
87
|
-
|
85
|
+
{
|
88
86
|
timestamp: timestamp,
|
89
87
|
token: {
|
90
88
|
expires_in: token.expires_in,
|
@@ -98,7 +96,7 @@ module SchwabRb::Auth
|
|
98
96
|
}
|
99
97
|
end
|
100
98
|
|
101
|
-
def to_json
|
99
|
+
def to_json(*_args)
|
102
100
|
to_h.to_json
|
103
101
|
end
|
104
102
|
end
|
@@ -1,11 +1,11 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require_relative
|
6
|
-
require_relative
|
7
|
-
require_relative
|
8
|
-
require_relative
|
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
9
|
|
10
10
|
module SchwabRb
|
11
11
|
class AsyncClient < BaseClient
|
@@ -28,7 +28,7 @@ module SchwabRb
|
|
28
28
|
dest.query = URI.encode_www_form(params) if params.any?
|
29
29
|
|
30
30
|
req_num = req_num()
|
31
|
-
log_request(
|
31
|
+
log_request("GET", req_num, dest, params)
|
32
32
|
|
33
33
|
# Use path only since @endpoint already has the base URL
|
34
34
|
query_string = params.any? ? "?#{URI.encode_www_form(params)}" : ""
|
@@ -45,7 +45,7 @@ module SchwabRb
|
|
45
45
|
dest = URI(URI::DEFAULT_PARSER.escape("#{SchwabRb::Constants::SCHWAB_BASE_URL}#{path}"))
|
46
46
|
|
47
47
|
req_num = req_num()
|
48
|
-
log_request(
|
48
|
+
log_request("POST", req_num, dest, data)
|
49
49
|
|
50
50
|
response = @client.post(path, build_headers, JSON.dump(data))
|
51
51
|
|
@@ -60,7 +60,7 @@ module SchwabRb
|
|
60
60
|
dest = URI(URI::DEFAULT_PARSER.escape("#{SchwabRb::Constants::SCHWAB_BASE_URL}#{path}"))
|
61
61
|
|
62
62
|
req_num = req_num()
|
63
|
-
log_request(
|
63
|
+
log_request("PUT", req_num, dest, data)
|
64
64
|
|
65
65
|
response = @client.put(path, build_headers, JSON.dump(data))
|
66
66
|
|
@@ -75,7 +75,7 @@ module SchwabRb
|
|
75
75
|
dest = URI(URI::DEFAULT_PARSER.escape("#{SchwabRb::Constants::SCHWAB_BASE_URL}#{path}"))
|
76
76
|
|
77
77
|
req_num = req_num()
|
78
|
-
log_request(
|
78
|
+
log_request("DELETE", req_num, dest)
|
79
79
|
|
80
80
|
response = @client.delete(path, build_headers)
|
81
81
|
|
@@ -86,32 +86,30 @@ module SchwabRb
|
|
86
86
|
|
87
87
|
def build_headers
|
88
88
|
headers = { "Content-Type" => "application/json" }
|
89
|
-
|
89
|
+
|
90
90
|
# Add authorization header if token is available
|
91
|
-
if @token_manager&.access_token
|
92
|
-
|
93
|
-
end
|
94
|
-
|
91
|
+
headers["Authorization"] = "Bearer #{@token_manager.access_token}" if @token_manager&.access_token
|
92
|
+
|
95
93
|
headers
|
96
94
|
end
|
97
95
|
|
98
96
|
def log_request(method, req_num, dest, data = nil)
|
99
97
|
redacted_dest = SchwabRb::Redactor.redact_url(dest.to_s)
|
100
98
|
SchwabRb::Logger.logger.info("Req #{req_num}: #{method} to #{redacted_dest}")
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
99
|
+
|
100
|
+
return unless data
|
101
|
+
|
102
|
+
redacted_data = SchwabRb::Redactor.redact_data(data)
|
103
|
+
SchwabRb::Logger.logger.debug("Payload: #{JSON.pretty_generate(redacted_data)}")
|
106
104
|
end
|
107
105
|
|
108
106
|
def log_response(response, req_num)
|
109
107
|
SchwabRb::Logger.logger.info("Resp #{req_num}: Status #{response.status}")
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
108
|
+
|
109
|
+
return unless SchwabRb::Logger.logger.level == ::Logger::DEBUG
|
110
|
+
|
111
|
+
redacted_body = SchwabRb::Redactor.redact_response_body(response)
|
112
|
+
SchwabRb::Logger.logger.debug("Response body: #{JSON.pretty_generate(redacted_body)}") if redacted_body
|
115
113
|
end
|
116
114
|
|
117
115
|
def req_num
|