schwab_rb 0.9.3 → 1.0.1
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 +18 -1
- data/CHANGELOG.md +20 -4
- data/examples/place_oco_order.rb +1 -4
- data/examples/stream_level_one.rb +65 -0
- data/examples/stream_order_book.rb +57 -0
- data/lib/schwab_rb/auth/init_client_easy.rb +19 -18
- data/lib/schwab_rb/auth/{init_client_token_file.rb → init_client_from_database.rb} +5 -6
- data/lib/schwab_rb/auth/init_client_login.rb +7 -8
- data/lib/schwab_rb/auth/token_manager.rb +25 -15
- data/lib/schwab_rb/cli/app.rb +11 -23
- data/lib/schwab_rb/clients/base_client.rb +51 -75
- data/lib/schwab_rb/configuration.rb +3 -3
- data/lib/schwab_rb/constants.rb +0 -1
- data/lib/schwab_rb/path_support.rb +1 -2
- data/lib/schwab_rb/storage/database.rb +148 -0
- data/lib/schwab_rb/stream/async_client.rb +30 -0
- data/lib/schwab_rb/stream/base.rb +240 -0
- data/lib/schwab_rb/stream/client.rb +44 -0
- data/lib/schwab_rb/stream/fields.rb +339 -0
- data/lib/schwab_rb/stream/message_builder.rb +66 -0
- data/lib/schwab_rb/stream/services.rb +52 -0
- data/lib/schwab_rb/utils/logger.rb +5 -1
- data/lib/schwab_rb/version.rb +1 -1
- data/lib/schwab_rb.rb +8 -2
- metadata +40 -5
- data/doc/ACCOUNT_MANAGEMENT.md +0 -297
- data/lib/schwab_rb/account_hash_manager.rb +0 -119
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a575cc586047d31d897d0ec4ba6d35afa1c2c97938a8dbf3e82b292d3b5d74a2
|
|
4
|
+
data.tar.gz: 3c859570ce9831b5b30bc2798d9083d2110d7e9be09065b33a312292334d7963
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3c2a2709207b61685083ff764585ccafe8a722ae798faf4c469fe75617585d65ae89847aa63d4b17bd9315a504a234d212b63226c40acfb2324f4e28199e7fc6
|
|
7
|
+
data.tar.gz: 85799a5ec440620b5c3756b2d86ac007e10d5c5c08be8fd08dc793232c0fb2ad1f918787a8fede962a797cecd7f4bef3cc222a121c285faed8ac06e364551355
|
data/.claude/settings.local.json
CHANGED
|
@@ -1,7 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"permissions": {
|
|
3
3
|
"allow": [
|
|
4
|
-
"Bash(gh pr edit:*)"
|
|
4
|
+
"Bash(gh pr edit:*)",
|
|
5
|
+
"WebSearch",
|
|
6
|
+
"WebFetch(domain:github.com)",
|
|
7
|
+
"Bash(bundle exec:*)",
|
|
8
|
+
"Bash(bundle install:*)",
|
|
9
|
+
"Bash(git checkout:*)",
|
|
10
|
+
"Bash(git add:*)",
|
|
11
|
+
"Bash(git rm:*)",
|
|
12
|
+
"Bash(git commit:*)",
|
|
13
|
+
"Bash(git push:*)",
|
|
14
|
+
"Bash(gh pr:*)",
|
|
15
|
+
"Bash(git reset:*)",
|
|
16
|
+
"Bash(find:*)",
|
|
17
|
+
"Bash(git pull:*)",
|
|
18
|
+
"Bash(git fetch:*)",
|
|
19
|
+
"Bash(git merge:*)",
|
|
20
|
+
"Bash(git stash:*)",
|
|
21
|
+
"Bash(git cherry-pick:*)"
|
|
5
22
|
],
|
|
6
23
|
"deny": [],
|
|
7
24
|
"ask": []
|
data/CHANGELOG.md
CHANGED
|
@@ -1,16 +1,32 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [1.0.0] - 2026-09-05
|
|
4
|
+
|
|
5
|
+
### Breaking Changes
|
|
6
|
+
- **Token storage**: Tokens are now persisted in SQLite (`~/.schwab_rb/schwab.db`) instead of a JSON file. `init_client_easy` and `init_client_login` no longer accept a `token_path` argument. Use `init_client_from_database` instead of `init_client_token_file`.
|
|
7
|
+
- **Account management**: `AccountHashManager`, `account_names.json`, and `account_hashes.json` are removed. Set `SCHWAB_ACCOUNT_NUMBER` in your environment instead. Account hashes are fetched from the API automatically and cached in SQLite.
|
|
8
|
+
- **Client method signatures**: All account-specific methods (`get_account`, `place_order`, `get_order`, etc.) now use `account_hash:` as a keyword argument instead of a positional argument. The `account_name:` keyword argument is removed.
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **WebSocket streaming client**: Real-time market data streaming via `SchwabRb::Stream::Client`
|
|
12
|
+
- Event-based API: `stream.on(:nyse_book, symbols: ["AAPL"], fields: :all) { |event| ... }`
|
|
13
|
+
- Supported services: `NYSE_BOOK`, `NASDAQ_BOOK`, `OPTIONS_BOOK`, `LEVELONE_EQUITIES`, `LEVELONE_OPTIONS`, `LEVELONE_FUTURES`, `LEVELONE_FUTURES_OPTIONS`, `LEVELONE_FOREX`, `CHART_EQUITY`, `CHART_FUTURES`, `SCREENER_EQUITY`, `SCREENER_OPTION`, `ACCT_ACTIVITY`
|
|
14
|
+
- Ruby constant field definitions for all services (e.g. `Fields::Book::BIDS`, `Fields::LevelOneEquity::BID_PRICE`)
|
|
15
|
+
- Exponential backoff reconnection (2s initial, doubles, 120s cap)
|
|
16
|
+
- Sync (`Stream::Client`) and async (`Stream::AsyncClient`) wrappers
|
|
17
|
+
- **SQLite storage** (`SchwabRb::Storage::Database`): Single database at `~/.schwab_rb/schwab.db` (configurable via `SCHWAB_DATABASE_PATH`) storing tokens and account hashes
|
|
18
|
+
- **`SCHWAB_ACCOUNT_NUMBER` env var**: Configure your account number once; the gem auto-fetches and caches the account hash on first use
|
|
19
|
+
- **`init_client_from_database`**: New initializer that loads tokens from SQLite (replaces `init_client_token_file`)
|
|
20
|
+
|
|
3
21
|
### Fixed
|
|
4
22
|
- CLI `login` no longer reads from `ARGF` at the browser prompt, which prevented `schwab_rb login` from opening the browser when command arguments were present
|
|
5
23
|
- `OptionExpirationChain` now accepts both string-keyed and symbol-keyed hashes, matching `BaseClient#get_option_expiration_chain`
|
|
6
24
|
|
|
7
|
-
### Added
|
|
8
|
-
- CLI `sample` command for saving single-expiration option chain snapshots as CSV or JSON
|
|
9
|
-
- `--root` option for filtering sampled contracts by option root and using that root in output filenames
|
|
10
|
-
|
|
11
25
|
### Changed
|
|
12
26
|
- CLI history downloads now default to `~/.schwab_rb/data/history`
|
|
13
27
|
- CLI option samples now default to `~/.schwab_rb/data/options`
|
|
28
|
+
- CLI `login` success message updated to reflect database storage
|
|
29
|
+
- `PathSupport.expand_path` error message generalized from `"token_path is nil or empty"` to `"path is nil or empty"`
|
|
14
30
|
|
|
15
31
|
## [0.6.0] - 2025-12-11
|
|
16
32
|
|
data/examples/place_oco_order.rb
CHANGED
|
@@ -19,9 +19,6 @@ Dotenv.load
|
|
|
19
19
|
# config.log_level = "DEBUG"
|
|
20
20
|
# end
|
|
21
21
|
|
|
22
|
-
CURRENT_ACCT = "TRADING_BROKERAGE_ACCOUNT"
|
|
23
|
-
acct_manager = SchwabRb::AccountHashManager.new
|
|
24
|
-
|
|
25
22
|
client = SchwabRb::Auth.init_client_easy(
|
|
26
23
|
ENV['SCHWAB_API_KEY'],
|
|
27
24
|
ENV['SCHWAB_APP_SECRET'],
|
|
@@ -72,6 +69,6 @@ built_order = oco_order.build
|
|
|
72
69
|
|
|
73
70
|
binding.pry
|
|
74
71
|
|
|
75
|
-
# response = client.place_order(built_order
|
|
72
|
+
# response = client.place_order(built_order)
|
|
76
73
|
|
|
77
74
|
binding.pry
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Stream Level 1 equity quotes
|
|
5
|
+
#
|
|
6
|
+
# Usage: ruby examples/stream_level_one.rb
|
|
7
|
+
#
|
|
8
|
+
# Requires SCHWAB_API_KEY, SCHWAB_APP_SECRET, and APP_CALLBACK_URL env vars.
|
|
9
|
+
# Token must already exist in the database (run login first).
|
|
10
|
+
|
|
11
|
+
require "schwab_rb"
|
|
12
|
+
require "dotenv/load"
|
|
13
|
+
|
|
14
|
+
Fields = SchwabRb::Stream::Fields
|
|
15
|
+
|
|
16
|
+
client = SchwabRb::Auth.init_client_easy(
|
|
17
|
+
ENV.fetch("SCHWAB_API_KEY"),
|
|
18
|
+
ENV.fetch("SCHWAB_APP_SECRET"),
|
|
19
|
+
ENV.fetch("SCHWAB_APP_CALLBACK_URL")
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
stream = SchwabRb::Stream::Client.new(client)
|
|
23
|
+
|
|
24
|
+
stream.on(:level_one_equities,
|
|
25
|
+
symbols: ["AAPL", "MSFT", "GOOGL", "AMZN"],
|
|
26
|
+
fields: [
|
|
27
|
+
Fields::LevelOneEquity::SYMBOL,
|
|
28
|
+
Fields::LevelOneEquity::BID_PRICE,
|
|
29
|
+
Fields::LevelOneEquity::ASK_PRICE,
|
|
30
|
+
Fields::LevelOneEquity::LAST_PRICE,
|
|
31
|
+
Fields::LevelOneEquity::TOTAL_VOLUME,
|
|
32
|
+
Fields::LevelOneEquity::NET_CHANGE,
|
|
33
|
+
Fields::LevelOneEquity::NET_CHANGE_PERCENT
|
|
34
|
+
]
|
|
35
|
+
) do |event|
|
|
36
|
+
event["content"]&.each do |entry|
|
|
37
|
+
symbol = entry["key"]
|
|
38
|
+
bid = entry["1"]
|
|
39
|
+
ask = entry["2"]
|
|
40
|
+
last = entry["3"]
|
|
41
|
+
volume = entry["8"]
|
|
42
|
+
change = entry["18"]
|
|
43
|
+
change_pct = entry["42"]
|
|
44
|
+
|
|
45
|
+
parts = ["#{symbol}:"]
|
|
46
|
+
parts << "Last=#{last}" if last
|
|
47
|
+
parts << "Bid=#{bid}" if bid
|
|
48
|
+
parts << "Ask=#{ask}" if ask
|
|
49
|
+
parts << "Vol=#{volume}" if volume
|
|
50
|
+
parts << "Chg=#{change}" if change
|
|
51
|
+
parts << "Chg%=#{change_pct}" if change_pct
|
|
52
|
+
|
|
53
|
+
puts parts.join(" ")
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
puts "Starting Level 1 equity stream... Press Ctrl+C to stop."
|
|
58
|
+
|
|
59
|
+
trap("INT") do
|
|
60
|
+
puts "\nStopping stream..."
|
|
61
|
+
stream.stop
|
|
62
|
+
exit
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
stream.start
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Stream order book (Level 2) data for NYSE, NASDAQ, and Options
|
|
5
|
+
#
|
|
6
|
+
# Usage: ruby examples/stream_order_book.rb
|
|
7
|
+
#
|
|
8
|
+
# Requires SCHWAB_API_KEY, SCHWAB_APP_SECRET, and APP_CALLBACK_URL env vars.
|
|
9
|
+
# Token must already exist in the database (run login first).
|
|
10
|
+
|
|
11
|
+
require "schwab_rb"
|
|
12
|
+
require "dotenv/load"
|
|
13
|
+
|
|
14
|
+
Fields = SchwabRb::Stream::Fields
|
|
15
|
+
|
|
16
|
+
client = SchwabRb::Auth.init_client_easy(
|
|
17
|
+
ENV.fetch("SCHWAB_API_KEY"),
|
|
18
|
+
ENV.fetch("SCHWAB_APP_SECRET"),
|
|
19
|
+
ENV.fetch("SCHWAB_APP_CALLBACK_URL")
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
stream = SchwabRb::Stream::Client.new(client)
|
|
23
|
+
|
|
24
|
+
stream.on(:nyse_book, symbols: ["AAPL", "MSFT"], fields: Fields::Book::ALL) do |event|
|
|
25
|
+
puts "=== NYSE BOOK ==="
|
|
26
|
+
event["content"]&.each do |entry|
|
|
27
|
+
puts " Symbol: #{entry["key"]}"
|
|
28
|
+
puts " Bids: #{entry["2"]&.length || 0} levels"
|
|
29
|
+
puts " Asks: #{entry["3"]&.length || 0} levels"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
stream.on(:nasdaq_book, symbols: ["AMD", "NVDA"], fields: Fields::Book::ALL) do |event|
|
|
34
|
+
puts "=== NASDAQ BOOK ==="
|
|
35
|
+
event["content"]&.each do |entry|
|
|
36
|
+
puts " Symbol: #{entry["key"]}"
|
|
37
|
+
puts " Bids: #{entry["2"]&.length || 0} levels"
|
|
38
|
+
puts " Asks: #{entry["3"]&.length || 0} levels"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
stream.on(:options_book, symbols: ["AAPL 261218C00200000"], fields: Fields::Book::ALL) do |event|
|
|
43
|
+
puts "=== OPTIONS BOOK ==="
|
|
44
|
+
event["content"]&.each do |entry|
|
|
45
|
+
puts " Symbol: #{entry["key"]}"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
puts "Starting order book stream... Press Ctrl+C to stop."
|
|
50
|
+
|
|
51
|
+
trap("INT") do
|
|
52
|
+
puts "\nStopping stream..."
|
|
53
|
+
stream.stop
|
|
54
|
+
exit
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
stream.start
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "oauth2"
|
|
4
|
-
require_relative "
|
|
4
|
+
require_relative "init_client_from_database"
|
|
5
5
|
require_relative "init_client_login"
|
|
6
6
|
require_relative "../path_support"
|
|
7
7
|
|
|
@@ -11,38 +11,39 @@ module SchwabRb
|
|
|
11
11
|
api_key,
|
|
12
12
|
app_secret,
|
|
13
13
|
callback_url,
|
|
14
|
-
token_path,
|
|
15
14
|
asyncio: false,
|
|
16
15
|
enforce_enums: false,
|
|
17
16
|
callback_timeout: 300.0,
|
|
18
17
|
interactive: true,
|
|
19
|
-
requested_browser: nil
|
|
18
|
+
requested_browser: nil,
|
|
19
|
+
database: nil
|
|
20
20
|
)
|
|
21
|
-
|
|
21
|
+
client = begin
|
|
22
|
+
SchwabRb::Auth.init_client_from_database(
|
|
23
|
+
api_key,
|
|
24
|
+
app_secret,
|
|
25
|
+
enforce_enums: enforce_enums,
|
|
26
|
+
database: database
|
|
27
|
+
)
|
|
28
|
+
rescue StandardError
|
|
29
|
+
nil
|
|
30
|
+
end
|
|
22
31
|
|
|
23
|
-
|
|
32
|
+
if client
|
|
33
|
+
client.refresh! if client.session.expired?
|
|
34
|
+
return client unless client.session.expired?
|
|
35
|
+
end
|
|
24
36
|
|
|
25
|
-
client = SchwabRb::Auth.init_client_token_file(
|
|
26
|
-
api_key,
|
|
27
|
-
app_secret,
|
|
28
|
-
token_path,
|
|
29
|
-
enforce_enums: enforce_enums
|
|
30
|
-
)
|
|
31
|
-
client.refresh! if client.session.expired?
|
|
32
|
-
raise OAuth2::Error, "Token expired" if client.session.expired?
|
|
33
|
-
|
|
34
|
-
client
|
|
35
|
-
rescue StandardError
|
|
36
37
|
SchwabRb::Auth.init_client_login(
|
|
37
38
|
api_key,
|
|
38
39
|
app_secret,
|
|
39
40
|
callback_url,
|
|
40
|
-
token_path,
|
|
41
41
|
asyncio: asyncio,
|
|
42
42
|
enforce_enums: enforce_enums,
|
|
43
43
|
callback_timeout: callback_timeout,
|
|
44
44
|
interactive: interactive,
|
|
45
|
-
requested_browser: requested_browser
|
|
45
|
+
requested_browser: requested_browser,
|
|
46
|
+
database: database
|
|
46
47
|
)
|
|
47
48
|
end
|
|
48
49
|
end
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "oauth2"
|
|
4
|
-
require_relative "../path_support"
|
|
5
4
|
|
|
6
5
|
module SchwabRb
|
|
7
6
|
module Auth
|
|
8
|
-
def self.
|
|
9
|
-
|
|
7
|
+
def self.init_client_from_database(api_key, app_secret, enforce_enums: true, database: nil)
|
|
8
|
+
metadata_manager = SchwabRb::Auth::TokenManager.from_database(api_key, database: database)
|
|
9
|
+
return nil unless metadata_manager
|
|
10
|
+
|
|
11
|
+
token = metadata_manager.token
|
|
10
12
|
|
|
11
13
|
oauth = OAuth2::Client.new(
|
|
12
14
|
api_key,
|
|
@@ -15,9 +17,6 @@ module SchwabRb
|
|
|
15
17
|
token_url: "/v1/oauth/token"
|
|
16
18
|
)
|
|
17
19
|
|
|
18
|
-
metadata_manager = SchwabRb::Auth::TokenManager.from_file(token_path)
|
|
19
|
-
token = metadata_manager.token
|
|
20
|
-
|
|
21
20
|
session = OAuth2::AccessToken.new(
|
|
22
21
|
oauth,
|
|
23
22
|
token.token,
|
|
@@ -65,16 +65,14 @@ module SchwabRb
|
|
|
65
65
|
api_key,
|
|
66
66
|
app_secret,
|
|
67
67
|
callback_url,
|
|
68
|
-
token_path,
|
|
69
68
|
asyncio: false,
|
|
70
69
|
enforce_enums: false,
|
|
71
70
|
callback_timeout: 300.0,
|
|
72
71
|
interactive: true,
|
|
73
72
|
input: $stdin,
|
|
74
|
-
requested_browser: nil
|
|
73
|
+
requested_browser: nil,
|
|
74
|
+
database: nil
|
|
75
75
|
)
|
|
76
|
-
token_path = SchwabRb::PathSupport.expand_path(token_path)
|
|
77
|
-
|
|
78
76
|
callback_timeout = if !callback_timeout
|
|
79
77
|
0
|
|
80
78
|
elsif callback_timeout.negative?
|
|
@@ -154,7 +152,7 @@ module SchwabRb
|
|
|
154
152
|
app_secret,
|
|
155
153
|
auth_context,
|
|
156
154
|
received_url,
|
|
157
|
-
|
|
155
|
+
database: database
|
|
158
156
|
)
|
|
159
157
|
ensure
|
|
160
158
|
LoginFlowServer.stop
|
|
@@ -213,7 +211,7 @@ module SchwabRb
|
|
|
213
211
|
end
|
|
214
212
|
|
|
215
213
|
def self.client_from_received_url(
|
|
216
|
-
api_key, app_secret, auth_context, received_url,
|
|
214
|
+
api_key, app_secret, auth_context, received_url, enforce_enums: true, database: nil
|
|
217
215
|
)
|
|
218
216
|
oauth = OAuth2::Client.new(
|
|
219
217
|
api_key,
|
|
@@ -230,9 +228,10 @@ module SchwabRb
|
|
|
230
228
|
metadata_manager = SchwabRb::Auth::TokenManager.from_oauth2_token(
|
|
231
229
|
token,
|
|
232
230
|
Time.now.to_i,
|
|
233
|
-
|
|
231
|
+
api_key: api_key,
|
|
232
|
+
database: database
|
|
234
233
|
)
|
|
235
|
-
metadata_manager.
|
|
234
|
+
metadata_manager.save
|
|
236
235
|
|
|
237
236
|
session = OAuth2::AccessToken.new(
|
|
238
237
|
oauth,
|
|
@@ -9,9 +9,11 @@ module SchwabRb
|
|
|
9
9
|
module Auth
|
|
10
10
|
class TokenManager
|
|
11
11
|
class << self
|
|
12
|
-
def
|
|
13
|
-
|
|
14
|
-
token_data =
|
|
12
|
+
def from_database(api_key, database: nil)
|
|
13
|
+
database ||= default_database
|
|
14
|
+
token_data = database.load_token(api_key)
|
|
15
|
+
return nil unless token_data
|
|
16
|
+
|
|
15
17
|
token = SchwabRb::Auth::Token.new(
|
|
16
18
|
token: token_data["token"]["access_token"],
|
|
17
19
|
expires_in: token_data["token"]["expires_in"],
|
|
@@ -22,10 +24,10 @@ module SchwabRb
|
|
|
22
24
|
expires_at: token_data["token"]["expires_at"]
|
|
23
25
|
)
|
|
24
26
|
|
|
25
|
-
TokenManager.new(token, token_data["timestamp"],
|
|
27
|
+
TokenManager.new(token, token_data["timestamp"], api_key: api_key, database: database)
|
|
26
28
|
end
|
|
27
29
|
|
|
28
|
-
def from_oauth2_token(oauth2_token, timestamp,
|
|
30
|
+
def from_oauth2_token(oauth2_token, timestamp, api_key:, database: nil)
|
|
29
31
|
token = SchwabRb::Auth::Token.new(
|
|
30
32
|
token: oauth2_token.token,
|
|
31
33
|
expires_in: oauth2_token.expires_in,
|
|
@@ -36,17 +38,24 @@ module SchwabRb
|
|
|
36
38
|
expires_at: oauth2_token.expires_at
|
|
37
39
|
)
|
|
38
40
|
|
|
39
|
-
TokenManager.new(token, timestamp,
|
|
41
|
+
TokenManager.new(token, timestamp, api_key: api_key, database: database)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def default_database
|
|
47
|
+
SchwabRb::Storage::Database.new
|
|
40
48
|
end
|
|
41
49
|
end
|
|
42
50
|
|
|
43
|
-
def initialize(token, timestamp,
|
|
51
|
+
def initialize(token, timestamp, api_key: nil, database: nil)
|
|
44
52
|
@token = token
|
|
45
53
|
@timestamp = timestamp
|
|
46
|
-
@
|
|
54
|
+
@api_key = api_key
|
|
55
|
+
@database = database
|
|
47
56
|
end
|
|
48
57
|
|
|
49
|
-
attr_reader :token, :timestamp, :
|
|
58
|
+
attr_reader :token, :timestamp, :api_key, :database
|
|
50
59
|
|
|
51
60
|
def refresh_token(client)
|
|
52
61
|
new_token = client.session.refresh!
|
|
@@ -62,7 +71,7 @@ module SchwabRb
|
|
|
62
71
|
)
|
|
63
72
|
@timestamp = Time.now.to_i
|
|
64
73
|
|
|
65
|
-
|
|
74
|
+
save
|
|
66
75
|
|
|
67
76
|
oauth = OAuth2::Client.new(
|
|
68
77
|
client.api_key,
|
|
@@ -79,9 +88,8 @@ module SchwabRb
|
|
|
79
88
|
)
|
|
80
89
|
end
|
|
81
90
|
|
|
82
|
-
def
|
|
83
|
-
|
|
84
|
-
File.write(token_path, to_json)
|
|
91
|
+
def save
|
|
92
|
+
effective_database.save_token(api_key, to_h)
|
|
85
93
|
end
|
|
86
94
|
|
|
87
95
|
def token_age
|
|
@@ -103,8 +111,10 @@ module SchwabRb
|
|
|
103
111
|
}
|
|
104
112
|
end
|
|
105
113
|
|
|
106
|
-
|
|
107
|
-
|
|
114
|
+
private
|
|
115
|
+
|
|
116
|
+
def effective_database
|
|
117
|
+
@database || self.class.send(:default_database)
|
|
108
118
|
end
|
|
109
119
|
end
|
|
110
120
|
end
|
data/lib/schwab_rb/cli/app.rb
CHANGED
|
@@ -83,21 +83,15 @@ module SchwabRb
|
|
|
83
83
|
raise Error, "Unexpected arguments: #{argv.join(' ')}" if argv.any?
|
|
84
84
|
|
|
85
85
|
credentials = load_credentials(require_callback_url: true)
|
|
86
|
-
token_path = resolved_token_path
|
|
87
86
|
|
|
88
87
|
SchwabRb::Auth.init_client_login(
|
|
89
88
|
credentials.fetch(:api_key),
|
|
90
89
|
credentials.fetch(:app_secret),
|
|
91
|
-
credentials.fetch(:callback_url)
|
|
92
|
-
token_path
|
|
90
|
+
credentials.fetch(:callback_url)
|
|
93
91
|
)
|
|
94
92
|
|
|
95
|
-
stdout.puts("Authentication succeeded. Token saved to
|
|
93
|
+
stdout.puts("Authentication succeeded. Token saved to database.")
|
|
96
94
|
0
|
|
97
|
-
rescue JSON::ParserError
|
|
98
|
-
raise Error,
|
|
99
|
-
"The token file at #{resolved_token_path} is not valid JSON. " \
|
|
100
|
-
"Delete it or run `schwab_rb login` to recreate it."
|
|
101
95
|
rescue OAuth2::Error, SchwabRb::Auth::RedirectTimeoutError, SchwabRb::Auth::RedirectServerExitedError => e
|
|
102
96
|
raise Error, "Authentication failed: #{e.message}"
|
|
103
97
|
end
|
|
@@ -230,24 +224,23 @@ module SchwabRb
|
|
|
230
224
|
|
|
231
225
|
def build_non_interactive_client
|
|
232
226
|
credentials = load_credentials(require_callback_url: false)
|
|
233
|
-
token_path = resolved_token_path
|
|
234
227
|
|
|
235
|
-
client = SchwabRb::Auth.
|
|
228
|
+
client = SchwabRb::Auth.init_client_from_database(
|
|
236
229
|
credentials.fetch(:api_key),
|
|
237
|
-
credentials.fetch(:app_secret)
|
|
238
|
-
token_path
|
|
230
|
+
credentials.fetch(:app_secret)
|
|
239
231
|
)
|
|
232
|
+
|
|
233
|
+
raise Error, "No valid API token found. Run `schwab_rb login` to authenticate." unless client
|
|
234
|
+
|
|
240
235
|
client.refresh!
|
|
241
236
|
|
|
242
237
|
return client unless client.session.expired?
|
|
243
238
|
|
|
244
|
-
raise Error, "
|
|
239
|
+
raise Error, "Token expired. Run `schwab_rb login` to authenticate."
|
|
245
240
|
rescue Errno::ENOENT
|
|
246
|
-
raise Error, "No valid API token found
|
|
247
|
-
rescue JSON::ParserError
|
|
248
|
-
raise Error, "The token file at #{token_path} is not valid JSON. Run `schwab_rb login` to recreate it."
|
|
241
|
+
raise Error, "No valid API token found. Run `schwab_rb login` to authenticate."
|
|
249
242
|
rescue OAuth2::Error => e
|
|
250
|
-
raise Error, "Unable to use
|
|
243
|
+
raise Error, "Unable to use stored token: #{e.message}. Run `schwab_rb login` to refresh it."
|
|
251
244
|
end
|
|
252
245
|
|
|
253
246
|
# rubocop:disable Metrics/AbcSize
|
|
@@ -280,11 +273,6 @@ module SchwabRb
|
|
|
280
273
|
raise Error, "The `--expiration-date` option is required." unless options[:expiration_date]
|
|
281
274
|
end
|
|
282
275
|
|
|
283
|
-
def resolved_token_path
|
|
284
|
-
token_path = env["SCHWAB_TOKEN_PATH"] || env["TOKEN_PATH"] || SchwabRb::Constants::DEFAULT_TOKEN_PATH
|
|
285
|
-
SchwabRb::PathSupport.expand_path(token_path)
|
|
286
|
-
end
|
|
287
|
-
|
|
288
276
|
def default_history_dir
|
|
289
277
|
SchwabRb::PathSupport.expand_path(DEFAULT_HISTORY_DIR)
|
|
290
278
|
end
|
|
@@ -359,7 +347,7 @@ module SchwabRb
|
|
|
359
347
|
<<~HELP
|
|
360
348
|
Usage: schwab_rb login
|
|
361
349
|
|
|
362
|
-
Authenticates with Schwab in a browser and stores the token
|
|
350
|
+
Authenticates with Schwab in a browser and stores the token in the database.
|
|
363
351
|
Required environment variables: SCHWAB_API_KEY, SCHWAB_APP_SECRET, SCHWAB_APP_CALLBACK_URL
|
|
364
352
|
HELP
|
|
365
353
|
end
|