stonk 1.0.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 9c8aaeb619986b3fff1bfa2de91c70796287bc96335c650a16b0ab98e9d9cbb9
4
+ data.tar.gz: 757458f818e7a2bb9b4b6465a36b3b96299bb8d502ebb9883d63372e7bc7efc3
5
+ SHA512:
6
+ metadata.gz: 10d76d734b74e04aa4e75d49a6ec9f64760ba70a15b494ddd26df0a9ada7707d4d2048d130bbb710154455ef70c5fb6ce72e58411d629c6db9fc350afd668251
7
+ data.tar.gz: a6866306f9d9c118c42ae331f2f74360751f56687c4508a0d411e2e575b6af9df6eb18e291754ed55a16c661ff24ebe9197f47e17b1cc0fc612428963800a8c0
checksums.yaml.gz.sig ADDED
Binary file
data/.rubocop.yml ADDED
@@ -0,0 +1,8 @@
1
+ inherit_gem:
2
+ rubocop-shopify: rubocop.yml
3
+
4
+ plugins:
5
+ - rubocop-minitest
6
+
7
+ AllCops:
8
+ TargetRubyVersion: 3.1
data/CHANGELOG.md ADDED
@@ -0,0 +1,41 @@
1
+ ## [1.0.0] - 2025-07-22
2
+
3
+ ### Core Features
4
+ - **Fallback strategy** - automatically tries multiple adapters if one fails
5
+ - **Built-in caching** with file-based cache adapter to reduce API calls
6
+ - **Rate limiting support** with configurable retry logic
7
+ - **Comprehensive error handling** for various failure scenarios
8
+
9
+ ### Data Sources
10
+ - **Alpha Vantage adapter** - Premium stock data via RapidAPI
11
+ - Configurable retry logic for rate limits
12
+ - API key-based authentication
13
+ - JSON response parsing
14
+
15
+ ### Caching System
16
+ - **File cache adapter** for local storage of stock prices
17
+ - JSON-based cache file format
18
+ - Automatic cache file creation and management
19
+ - Cache clearing functionality
20
+
21
+ ### Core Classes
22
+ - **Stonk::Service** - Main service class for orchestrating adapters
23
+ - **Stonk::Money** - BigDecimal-based money class for precise price handling
24
+ - **Stonk::Adapter** - Base adapter module with common error classes
25
+ - StockNotFound, RateLimitExceeded, ServerError exceptions
26
+
27
+ ### Developer Experience
28
+ - **Comprehensive test suite** with Minitest
29
+ - Unit tests for all adapters and core classes
30
+ - VCR cassettes for API response testing
31
+ - Fixtures for reliable test data
32
+ - **Logging support** with configurable log levels via STONK_LOG_LEVEL
33
+ - **Ruby 3.1+ compatibility** with modern Ruby features
34
+ - **MIT license** for open source use
35
+ - **Gem signing** with 1Password integration
36
+
37
+ ### Error Handling
38
+ - Graceful handling of network failures
39
+ - Rate limit detection and retry mechanisms
40
+ - Stock symbol validation and not-found scenarios
41
+ - Server error classification and reporting
@@ -0,0 +1,132 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our
6
+ community a harassment-free experience for everyone, regardless of age, body
7
+ size, visible or invisible disability, ethnicity, sex characteristics, gender
8
+ identity and expression, level of experience, education, socio-economic status,
9
+ nationality, personal appearance, race, caste, color, religion, or sexual
10
+ identity and orientation.
11
+
12
+ We pledge to act and interact in ways that contribute to an open, welcoming,
13
+ diverse, inclusive, and healthy community.
14
+
15
+ ## Our Standards
16
+
17
+ Examples of behavior that contributes to a positive environment for our
18
+ community include:
19
+
20
+ * Demonstrating empathy and kindness toward other people
21
+ * Being respectful of differing opinions, viewpoints, and experiences
22
+ * Giving and gracefully accepting constructive feedback
23
+ * Accepting responsibility and apologizing to those affected by our mistakes,
24
+ and learning from the experience
25
+ * Focusing on what is best not just for us as individuals, but for the overall
26
+ community
27
+
28
+ Examples of unacceptable behavior include:
29
+
30
+ * The use of sexualized language or imagery, and sexual attention or advances of
31
+ any kind
32
+ * Trolling, insulting or derogatory comments, and personal or political attacks
33
+ * Public or private harassment
34
+ * Publishing others' private information, such as a physical or email address,
35
+ without their explicit permission
36
+ * Other conduct which could reasonably be considered inappropriate in a
37
+ professional setting
38
+
39
+ ## Enforcement Responsibilities
40
+
41
+ Community leaders are responsible for clarifying and enforcing our standards of
42
+ acceptable behavior and will take appropriate and fair corrective action in
43
+ response to any behavior that they deem inappropriate, threatening, offensive,
44
+ or harmful.
45
+
46
+ Community leaders have the right and responsibility to remove, edit, or reject
47
+ comments, commits, code, wiki edits, issues, and other contributions that are
48
+ not aligned to this Code of Conduct, and will communicate reasons for moderation
49
+ decisions when appropriate.
50
+
51
+ ## Scope
52
+
53
+ This Code of Conduct applies within all community spaces, and also applies when
54
+ an individual is officially representing the community in public spaces.
55
+ Examples of representing our community include using an official email address,
56
+ posting via an official social media account, or acting as an appointed
57
+ representative at an online or offline event.
58
+
59
+ ## Enforcement
60
+
61
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
62
+ reported to the community leaders responsible for enforcement at
63
+ [INSERT CONTACT METHOD].
64
+ All complaints will be reviewed and investigated promptly and fairly.
65
+
66
+ All community leaders are obligated to respect the privacy and security of the
67
+ reporter of any incident.
68
+
69
+ ## Enforcement Guidelines
70
+
71
+ Community leaders will follow these Community Impact Guidelines in determining
72
+ the consequences for any action they deem in violation of this Code of Conduct:
73
+
74
+ ### 1. Correction
75
+
76
+ **Community Impact**: Use of inappropriate language or other behavior deemed
77
+ unprofessional or unwelcome in the community.
78
+
79
+ **Consequence**: A private, written warning from community leaders, providing
80
+ clarity around the nature of the violation and an explanation of why the
81
+ behavior was inappropriate. A public apology may be requested.
82
+
83
+ ### 2. Warning
84
+
85
+ **Community Impact**: A violation through a single incident or series of
86
+ actions.
87
+
88
+ **Consequence**: A warning with consequences for continued behavior. No
89
+ interaction with the people involved, including unsolicited interaction with
90
+ those enforcing the Code of Conduct, for a specified period of time. This
91
+ includes avoiding interactions in community spaces as well as external channels
92
+ like social media. Violating these terms may lead to a temporary or permanent
93
+ ban.
94
+
95
+ ### 3. Temporary Ban
96
+
97
+ **Community Impact**: A serious violation of community standards, including
98
+ sustained inappropriate behavior.
99
+
100
+ **Consequence**: A temporary ban from any sort of interaction or public
101
+ communication with the community for a specified period of time. No public or
102
+ private interaction with the people involved, including unsolicited interaction
103
+ with those enforcing the Code of Conduct, is allowed during this period.
104
+ Violating these terms may lead to a permanent ban.
105
+
106
+ ### 4. Permanent Ban
107
+
108
+ **Community Impact**: Demonstrating a pattern of violation of community
109
+ standards, including sustained inappropriate behavior, harassment of an
110
+ individual, or aggression toward or disparagement of classes of individuals.
111
+
112
+ **Consequence**: A permanent ban from any sort of public interaction within the
113
+ community.
114
+
115
+ ## Attribution
116
+
117
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118
+ version 2.1, available at
119
+ [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
120
+
121
+ Community Impact Guidelines were inspired by
122
+ [Mozilla's code of conduct enforcement ladder][Mozilla CoC].
123
+
124
+ For answers to common questions about this code of conduct, see the FAQ at
125
+ [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
126
+ [https://www.contributor-covenant.org/translations][translations].
127
+
128
+ [homepage]: https://www.contributor-covenant.org
129
+ [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
130
+ [Mozilla CoC]: https://github.com/mozilla/diversity
131
+ [FAQ]: https://www.contributor-covenant.org/faq
132
+ [translations]: https://www.contributor-covenant.org/translations
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Nick Jones
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,208 @@
1
+ # Stonk
2
+
3
+ A Ruby gem for fetching real-time stock prices from multiple data sources with a unified interface. Stonk provides adapters for popular financial APIs and includes caching capabilities for improved performance.
4
+
5
+ ## Features
6
+
7
+ - **Caching**: Built-in file-based caching to reduce API calls
8
+ - **Fallback Strategy**: Automatically tries multiple adapters if one fails
9
+ - **Rate Limiting**: Built-in rate limit handling with retry logic
10
+ - **Error Handling**: Comprehensive error handling for various failure scenarios
11
+
12
+ ## Installation
13
+
14
+ Add this line to your application's Gemfile:
15
+
16
+ ```ruby
17
+ gem 'stonk'
18
+ ```
19
+
20
+ And then execute:
21
+
22
+ ```bash
23
+ bundle install
24
+ ```
25
+
26
+ Or install it yourself as:
27
+
28
+ ```bash
29
+ gem install stonk
30
+ ```
31
+
32
+ ## Usage
33
+
34
+ ### Basic Usage
35
+
36
+ ```ruby
37
+ require 'stonk'
38
+
39
+ # Create a service with lookup adapters and optional cache adapter
40
+ cache_adapter = Stonk::Adapter::FileCacheAdapter.new('/tmp/stock_cache.json')
41
+ service = Stonk::Service.new(
42
+ lookup_adapters: [
43
+ Stonk::Adapter::AlphaVantageAdapter.new(ENV['ALPHA_VANTAGE_API_KEY'])
44
+ ],
45
+ cache_adapter:
46
+ )
47
+
48
+ # Get stock price (automatically cached if cache_adapter is provided)
49
+ price = service.get_stock_price('AAPL')
50
+ puts "Apple stock price: $#{price}" if price
51
+ ```
52
+
53
+ ### Using Individual Adapters
54
+
55
+ #### Alpha Vantage Adapter
56
+
57
+ The Alpha Vantage adapter requires an API key from [Alpha Vantage](https://www.alphavantage.co/):
58
+
59
+ ```ruby
60
+ require 'stonk'
61
+
62
+ # Create Alpha Vantage adapter with API key
63
+ alpha_adapter = Stonk::Adapter::AlphaVantageAdapter.new(
64
+ ENV['ALPHA_VANTAGE_API_KEY'],
65
+ retry_on_rate_limit: true,
66
+ rate_limit_sleep_seconds: 5
67
+ )
68
+
69
+ # Get stock price
70
+ begin
71
+ price = alpha_adapter.get_stock_price('AAPL')
72
+ puts "Apple stock price: $#{price}"
73
+ rescue Stonk::Adapter::StockNotFound
74
+ puts "Stock not found"
75
+ rescue Stonk::Adapter::RateLimitExceeded
76
+ puts "Rate limit exceeded"
77
+ rescue Stonk::Adapter::ServerError => e
78
+ puts "Server error: #{e.message}"
79
+ end
80
+ ```
81
+
82
+ #### File Cache Adapter
83
+
84
+ The file cache adapter stores stock prices locally to reduce API calls:
85
+
86
+ ```ruby
87
+ require 'stonk'
88
+
89
+ # Create file cache adapter
90
+ cache_adapter = Stonk::Adapter::FileCacheAdapter.new('/tmp/stock_cache.json')
91
+
92
+ # Set a stock price in cache
93
+ cache_adapter.set_stock_price('AAPL', Stonk::Money.new('150.25'))
94
+
95
+ # Get stock price from cache
96
+ begin
97
+ price = cache_adapter.get_stock_price('AAPL')
98
+ puts "Cached Apple stock price: $#{price}"
99
+ rescue Stonk::Adapter::StockNotFound
100
+ puts "Stock not found in cache"
101
+ end
102
+
103
+ # Clear the cache
104
+ cache_adapter.clear_cache
105
+ ```
106
+
107
+ ### Advanced Usage with Fallback Strategy
108
+
109
+ ```ruby
110
+ require 'stonk'
111
+
112
+ # Create adapters in order of preference
113
+ cache_adapter = Stonk::Adapter::FileCacheAdapter.new('/tmp/stock_cache.json')
114
+ alpha_adapter = Stonk::Adapter::AlphaVantageAdapter.new(ENV['ALPHA_VANTAGE_API_KEY'])
115
+ alpha_adapter_with_retry = Stonk::Adapter::AlphaVantageAdapter.new(
116
+ ENV['ALPHA_VANTAGE_API_KEY'],
117
+ retry_on_rate_limit: true
118
+ )
119
+
120
+ # Service will try lookup adapters in order until one succeeds, and cache results
121
+ service = Stonk::Service.new(
122
+ lookup_adapters: [
123
+ cache_adapter, # Try looknig up in the cache first
124
+ alpha_adapter # Then Alpha Vantage
125
+ alpha_adapter_with_retry # Then back to Alpha Vantage with a retry.
126
+ ],
127
+ cache_adapter: # Cache successful results
128
+ )
129
+
130
+ # Get stock price (will try each lookup adapter until one returns a price, then cache the result)
131
+ price = service.get_stock_price('AAPL')
132
+ puts "Apple stock price: $#{price}" if price
133
+ ```
134
+
135
+ ### Working with Money Objects
136
+
137
+ Stock prices are returned as `Stonk::Money` objects, which are based on `BigDecimal` for precision:
138
+
139
+ ```ruby
140
+ require 'stonk'
141
+
142
+ yahoo_adapter = Stonk::Adapter::YahooFinanceAdapter.new
143
+ price = yahoo_adapter.get_stock_price('AAPL')
144
+
145
+ # Money objects support arithmetic operations
146
+ puts "Price: $#{price}"
147
+ puts "Price as float: #{price.to_f}"
148
+ puts "Price as string: #{price.to_s}"
149
+
150
+ # You can perform calculations
151
+ total_value = price * 100
152
+ puts "Value of 100 shares: $#{total_value}"
153
+ ```
154
+
155
+ ### Error Handling
156
+
157
+ ```ruby
158
+ require 'stonk'
159
+
160
+ service = Stonk::Service.new(
161
+ lookup_adapters: [
162
+ Stonk::Adapter::YahooFinanceAdapter.new
163
+ ]
164
+ )
165
+
166
+ begin
167
+ price = service.get_stock_price('INVALID_SYMBOL')
168
+ puts "Price: $#{price}" if price
169
+ rescue Stonk::Adapter::StockNotFound
170
+ puts "Stock symbol not found"
171
+ rescue Stonk::Adapter::RateLimitExceeded
172
+ puts "Rate limit exceeded, try again later"
173
+ rescue Stonk::Adapter::ServerError => e
174
+ puts "Server error: #{e.message}"
175
+ rescue => e
176
+ puts "Unexpected error: #{e.message}"
177
+ end
178
+ ```
179
+
180
+ ### Logging
181
+
182
+ Stonk includes built-in logging that can be configured via environment variables:
183
+
184
+ ```ruby
185
+ # Set log level via environment variable
186
+ ENV['STONK_LOG_LEVEL'] = 'DEBUG'
187
+
188
+ # Or access the logger directly
189
+ Stonk.logger.level = Logger::DEBUG
190
+ ```
191
+
192
+ ## Development
193
+
194
+ After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
195
+
196
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
197
+
198
+ ## Contributing
199
+
200
+ Bug reports and pull requests are welcome on GitHub at [https://github.com/punkstar/stonk.rb](https://github.com/punkstar/stonk.rb). This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/punkstar/stonk.rb/blob/main/CODE_OF_CONDUCT.md).
201
+
202
+ ## License
203
+
204
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
205
+
206
+ ## Code of Conduct
207
+
208
+ Everyone interacting in the Stonk project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/punkstar/stonk.rb/blob/main/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rubocop/rake_task"
5
+ require "minitest/test_task"
6
+
7
+ RuboCop::RakeTask.new
8
+
9
+ Minitest::TestTask.create do |t|
10
+ t.warning = false
11
+ end
12
+
13
+ task default: :test
data/bin/stonk ADDED
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "stonk"
6
+ require "optparse"
7
+
8
+ use_cache = true
9
+
10
+ def setup_service(use_cache:)
11
+ cache_adapter = Stonk::Adapter::FileCacheAdapter.new("/tmp/stock_cache.json")
12
+ alpha_adapter = Stonk::Adapter::AlphaVantageAdapter.new(ENV["ALPHA_VANTAGE_API_KEY"])
13
+ alpha_adapter_with_retry = Stonk::Adapter::AlphaVantageAdapter.new(
14
+ ENV["ALPHA_VANTAGE_API_KEY"],
15
+ retry_on_rate_limit: true,
16
+ )
17
+
18
+ lookup_adapters = [alpha_adapter, alpha_adapter_with_retry]
19
+
20
+ lookup_adapters.unshift(cache_adapter) if use_cache
21
+
22
+ Stonk::Service.new(
23
+ lookup_adapters:,
24
+ cache_adapter: use_cache ? cache_adapter : nil,
25
+ )
26
+ end
27
+
28
+ def lookup_stock(service, symbol)
29
+ puts "Looking up #{symbol.upcase}..."
30
+
31
+ begin
32
+ price = service.get_stock_price(symbol.upcase)
33
+
34
+ if price
35
+ puts "#{symbol.upcase}: $#{price.to_f}"
36
+ else
37
+ puts "Could not find price for #{symbol.upcase}"
38
+ exit(1)
39
+ end
40
+ rescue Stonk::Adapter::StockNotFound
41
+ puts "Stock not found: #{symbol.upcase}"
42
+ exit(1)
43
+ rescue Stonk::Adapter::RateLimitExceeded
44
+ puts "Rate limit exceeded. Please try again later."
45
+ exit(1)
46
+ rescue Stonk::Adapter::ServerError => e
47
+ puts "Server error occurred: #{e.message}"
48
+ exit(1)
49
+ rescue => e
50
+ puts "Error: #{e.message}"
51
+ exit(1)
52
+ end
53
+ end
54
+
55
+ def show_help
56
+ puts <<~HELP
57
+ Usage: stonk [OPTIONS] SYMBOL
58
+
59
+ Look up stock prices using multiple data sources.
60
+
61
+ Arguments:
62
+ SYMBOL Stock symbol to look up (e.g., AAPL, MSFT)
63
+
64
+ Options:
65
+ -h, --help Show this help message
66
+
67
+ Environment Variables:
68
+ ALPHA_VANTAGE_API_KEY API key for Alpha Vantage (optional)
69
+
70
+ Examples:
71
+ stonk AAPL Look up Apple stock price
72
+ stonk MSFT Look up Microsoft stock price
73
+
74
+ The tool will try multiple data sources in order:
75
+ 1. Local cache
76
+ 2. Alpha Vantage (if API key provided)
77
+ 4. Alpha Vantage with retry (if API key provided)
78
+
79
+ Successful lookups are cached for future use.
80
+ HELP
81
+ end
82
+
83
+ OptionParser.new do |opts|
84
+ opts.banner = "Usage: stonk [OPTIONS] SYMBOL"
85
+
86
+ opts.on("-h", "--help", "Show this help message") do
87
+ show_help
88
+ exit
89
+ end
90
+
91
+ opts.on("--verbose", "Show verbose output") do
92
+ Stonk.logger.level = Logger::DEBUG
93
+ end
94
+
95
+ opts.on("--no-cache", "Do not use the cache") do
96
+ use_cache = false
97
+ end
98
+ end.parse!
99
+
100
+ if ARGV.empty?
101
+ puts "Error: Stock symbol is required"
102
+ puts "Use 'stonk --help' for usage information"
103
+ exit 1
104
+ end
105
+
106
+ symbol = ARGV.first
107
+
108
+ service = setup_service(use_cache:)
109
+ lookup_stock(service, symbol)
data/etc/punkstar.pem ADDED
@@ -0,0 +1,26 @@
1
+ -----BEGIN CERTIFICATE-----
2
+ MIIEdDCCAtygAwIBAgIBATANBgkqhkiG9w0BAQsFADBAMQ0wCwYDVQQDDARuaWNr
3
+ MRkwFwYKCZImiZPyLGQBGRYJbmlja2pvbmVzMRQwEgYKCZImiZPyLGQBGRYEdGVj
4
+ aDAeFw0yNTA3MDYxOTM3MzNaFw0yNjA3MDYxOTM3MzNaMEAxDTALBgNVBAMMBG5p
5
+ Y2sxGTAXBgoJkiaJk/IsZAEZFgluaWNram9uZXMxFDASBgoJkiaJk/IsZAEZFgR0
6
+ ZWNoMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAtVeWYtCOx6NWkD1H
7
+ M01ivSrilcoVoJ10AtlLgH23yN3pP4iMg+/c4ZfFqsrSDFbigbOmimQx74NlMdFG
8
+ dhhrAfPvoHmzUYvPWnCXkkqWRnXdK7+0m1tG4HxD1JJhgPtQKDSK9xx4seLZt01b
9
+ kL4ndBDXd7YXCzQhoozwXBVbVuip3bE2OYnUsEV7wXMZJYHK5Tj68QH5oppHTRV5
10
+ 2Ws/W5CoHeZ7oExGYkYpF1U2/XG+722m6zHeUX9opLcXfvP+fpomDbFA7W1MFxTN
11
+ C+lxXhZYPt1+Pr9YeUnsc9uiX/NTgCvOCczR4eH5EeEchOZKFypiERajFXV9mDTG
12
+ f+fEBktE7zRdqTZD1KO/0peN8fvBlXWgA0fLD50UFfBR8s7pvYGzhs1uHiS8AFC/
13
+ BTWm7ttFetzKVZURP4ym3xxzpYuOTWDqkqvfd4ew6BDXBuZJsqXWE8X4qzDsNMCx
14
+ SGhyP3NbbRuY46T+T5K+k3zNtDRB7ZVjJFeum21mBkN6SVm7AgMBAAGjeTB3MAkG
15
+ A1UdEwQCMAAwCwYDVR0PBAQDAgSwMB0GA1UdDgQWBBTnEkevlJfGftCtQSV5MwW5
16
+ DDEKzDAeBgNVHREEFzAVgRNuaWNrQG5pY2tqb25lcy50ZWNoMB4GA1UdEgQXMBWB
17
+ E25pY2tAbmlja2pvbmVzLnRlY2gwDQYJKoZIhvcNAQELBQADggGBADCu8hK3XBEm
18
+ aVNShTjB1JR6Hy7+wCbUevLqRyTaf2aA2wpn7gJnlENMo4UKIXAzBSKMzTFTH2ZJ
19
+ 1WKrO045godBAqF005q3WHDNpUTCM1IALxxpUSqKJqRcz5zrymorX8sffqcP1dFY
20
+ Gp5Qtx/1YRD0cQ4iAHMCz9pPxVkOM4pvz/7M3JjoU3loP8kVIKMD3E4xVgccBayP
21
+ /9bTPaOM7sgtoPDzX/LsxICCBgxeW87wA7mwhQVgUu2BV0lVNKc5ZBXtoMBKHVEZ
22
+ gU7bzkS10OiaB8dP1C54NpOquXDXJ1szS/qzry/dKS3OULsSw731cmAYF1KXLN01
23
+ SNU7lyEKF7DglZ7e+v4q18c3QWttmkhJgNVku+scAf6Fg9qTB/Tp9znLSaC8JiEo
24
+ az50nHustlmuPWriALbXeG493US/lYAIesFK9oZfFC3L/K9ni6LZPvGNQVymEAAD
25
+ kOJf02iMdjt0UrCnKde4RZDPnifzBnMfEawwdHekT7T4UNWbzaaFgQ==
26
+ -----END CERTIFICATE-----
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "json"
5
+
6
+ module Stonk
7
+ module Adapter
8
+ class AlphaVantageAdapter
9
+ def initialize(api_key, retry_on_rate_limit: false, rate_limit_sleep_seconds: 3, rate_limit_retry_count: 10)
10
+ @api_key = api_key
11
+ @base_url = URI("https://alpha-vantage.p.rapidapi.com")
12
+ @retry_on_rate_limit = retry_on_rate_limit
13
+ @rate_limit_sleep_seconds = rate_limit_sleep_seconds
14
+ @rate_limit_retry_count = rate_limit_retry_count
15
+ @retry_attempts = 0
16
+
17
+ raise ConfigurationError, "API key is required" if @api_key.nil?
18
+ raise ConfigurationError, "Rate limit sleep seconds must be greater than 0" if @rate_limit_sleep_seconds < 0
19
+ end
20
+
21
+ def get_stock_price(stock_symbol)
22
+ make_stock_price_request(stock_symbol).dig("Global Quote", "05. price").then do |price|
23
+ raise Stonk::Adapter::StockNotFound, "Stock price not found for #{stock_symbol}" if price.nil?
24
+
25
+ reset_retry_attempts
26
+ Stonk::Money.new(price)
27
+ end
28
+ rescue Stonk::Adapter::RateLimitExceeded => e
29
+ raise e unless @retry_on_rate_limit
30
+
31
+ if @retry_attempts >= @rate_limit_retry_count
32
+ raise Stonk::Adapter::RateLimitRetryExceeded, "Rate limit retry count exceeded"
33
+ else
34
+ Stonk.logger.warn("[AlphaVantage] Too many requests for #{stock_symbol}, sleeping for #{@rate_limit_sleep_seconds} seconds")
35
+ sleep(@rate_limit_sleep_seconds) if @rate_limit_sleep_seconds > 0
36
+ register_retry_attempt
37
+ retry
38
+ end
39
+ rescue Net::HTTPError => e
40
+ raise Stonk::Adapter::ServerError, "Failed to fetch stock price for #{stock_symbol}: #{e.message}"
41
+ end
42
+
43
+ private
44
+
45
+ def make_stock_price_request(stock_symbol)
46
+ params = {
47
+ "function" => "GLOBAL_QUOTE",
48
+ "symbol" => stock_symbol,
49
+ "datatype" => "json",
50
+ }
51
+
52
+ make_request(params)
53
+ end
54
+
55
+ def make_request(params)
56
+ uri = @base_url.dup
57
+ uri.path = "/query"
58
+ uri.query = URI.encode_www_form(params)
59
+
60
+ http = Net::HTTP.new(uri.host, uri.port)
61
+ http.use_ssl = true
62
+
63
+ request = Net::HTTP::Get.new(uri)
64
+ request["x-rapidapi-key"] = @api_key
65
+ request["x-rapidapi-host"] = @base_url.host
66
+
67
+ Stonk.logger.debug("[AlphaVantage] Making request to #{uri}")
68
+
69
+ response = http.request(request)
70
+
71
+ Stonk.logger.debug("[AlphaVantage] #{response.code}, #{response.body.gsub("\n", " ")}, #{response.to_hash}")
72
+
73
+ if response.is_a?(Net::HTTPSuccess)
74
+ JSON.parse(response.body)
75
+ elsif response.is_a?(Net::HTTPTooManyRequests)
76
+ raise Stonk::Adapter::RateLimitExceeded, "Too many requests"
77
+ else
78
+ raise Stonk::Adapter::ServerError, "Failed to fetch stock price for #{params}: #{response.body}"
79
+ end
80
+ end
81
+
82
+ def register_retry_attempt
83
+ @retry_attempts += 1
84
+ end
85
+
86
+ def reset_retry_attempts
87
+ @retry_attempts = 0
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "json"
5
+
6
+ module Stonk
7
+ module Adapter
8
+ class FileCacheAdapter
9
+ def initialize(cache_file_path, default_ttl: 60 * 60)
10
+ @cache_file_path = cache_file_path
11
+ @default_ttl = default_ttl
12
+ @data = nil
13
+ end
14
+
15
+ def get_stock_price(stock_symbol)
16
+ load_data
17
+
18
+ raise Stonk::Adapter::StockNotFound, "Stock not found: #{stock_symbol}" unless @data.key?(stock_symbol)
19
+
20
+ entry = @data[stock_symbol]
21
+
22
+ if Time.now.to_i >= entry["expires_at"]
23
+ @data.delete(stock_symbol)
24
+ save_data
25
+ raise Stonk::Adapter::StockNotFound, "Stock not found: #{stock_symbol}"
26
+ end
27
+
28
+ entry["price"]
29
+ end
30
+
31
+ def set_stock_price(stock_symbol, price, ttl: nil)
32
+ return if price.nil?
33
+
34
+ load_data
35
+
36
+ ttl_seconds = ttl || @default_ttl
37
+ expires_at = Time.now.to_i + ttl_seconds
38
+
39
+ @data[stock_symbol] = {
40
+ "price" => price,
41
+ "expires_at" => expires_at,
42
+ }
43
+
44
+ save_data
45
+ end
46
+
47
+ def clear_cache
48
+ FileUtils.rm_f(cache_path)
49
+ end
50
+
51
+ def clear_expired_entries
52
+ load_data
53
+ original_size = @data.size
54
+
55
+ @data.delete_if do |_symbol, entry|
56
+ Time.now.to_i >= entry["expires_at"]
57
+ end
58
+
59
+ if @data.size < original_size
60
+ save_data
61
+ Stonk.logger.debug("[FileCacheAdapter] Cleared #{original_size - @data.size} expired entries")
62
+ end
63
+ end
64
+
65
+ private
66
+
67
+ def load_data
68
+ @data = JSON.parse(File.read(cache_path))
69
+ rescue JSON::ParserError
70
+ @data = {}
71
+ end
72
+
73
+ def save_data
74
+ Stonk.logger.debug("[FileCacheAdapter] Saving #{@data.to_json} to #{cache_path}")
75
+ File.write(cache_path, @data.to_json)
76
+ end
77
+
78
+ def cache_path
79
+ FileUtils.touch(@cache_file_path) unless File.exist?(@cache_file_path)
80
+
81
+ @cache_file_path
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stonk
4
+ module Adapter
5
+ class AdapterError < Error; end
6
+
7
+ class ConfigurationError < AdapterError; end
8
+ class StockNotFound < AdapterError; end
9
+ class RateLimitExceeded < AdapterError; end
10
+ class RateLimitRetryExceeded < AdapterError; end
11
+ class ServerError < AdapterError; end
12
+
13
+ autoload :AlphaVantageAdapter, "stonk/adapter/alpha_vantage_adapter"
14
+ autoload :FileCacheAdapter, "stonk/adapter/file_cache_adapter"
15
+ autoload :YahooFinanceAdapter, "stonk/adapter/yahoo_finance_adapter"
16
+ end
17
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bigdecimal"
4
+ require "delegate"
5
+
6
+ module Stonk
7
+ class Money < SimpleDelegator
8
+ def initialize(amount)
9
+ super(BigDecimal(amount))
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stonk
4
+ class Service
5
+ def initialize(lookup_adapters: [], cache_adapter: nil)
6
+ @lookup_adapters = lookup_adapters
7
+ @cache_adapter = cache_adapter
8
+ end
9
+
10
+ def get_stock_price(stock_symbol)
11
+ get_stock_price_from_adapters(stock_symbol).tap do |price|
12
+ @cache_adapter.set_stock_price(stock_symbol, price) if has_cache_adapter?
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def get_stock_price_from_adapters(stock_symbol)
19
+ @lookup_adapters.each do |adapter|
20
+ Stonk.logger.debug("Looking up #{stock_symbol} from #{adapter.class.name}")
21
+ return adapter.get_stock_price(stock_symbol)
22
+ rescue Stonk::Adapter::AdapterError => e
23
+ Stonk.logger.warn("Error looking up #{stock_symbol} from #{adapter.class.name}: #{e.message}")
24
+ next
25
+ end
26
+
27
+ nil
28
+ end
29
+
30
+ def has_cache_adapter?
31
+ !@cache_adapter.nil?
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stonk
4
+ Stock = Data.define(:symbol, :price)
5
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stonk
4
+ VERSION = "1.0.0"
5
+ end
data/lib/stonk.rb ADDED
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "stonk/version"
4
+ require "logger"
5
+ module Stonk
6
+ class Error < StandardError; end
7
+
8
+ autoload :Adapter, "stonk/adapter"
9
+ autoload :Service, "stonk/service"
10
+ autoload :Stock, "stonk/stock"
11
+ autoload :Money, "stonk/money"
12
+
13
+ class << self
14
+ def logger
15
+ @logger ||= Logger.new($stdout).tap do |logger|
16
+ logger.level = ENV["STONK_LOG_LEVEL"] || Logger::ERROR
17
+ end
18
+ end
19
+ end
20
+ end
data/sig/stonk.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Stonk
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
data.tar.gz.sig ADDED
Binary file
metadata ADDED
@@ -0,0 +1,100 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: stonk
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Nick Jones
8
+ bindir: bin
9
+ cert_chain:
10
+ - |
11
+ -----BEGIN CERTIFICATE-----
12
+ MIIEdDCCAtygAwIBAgIBATANBgkqhkiG9w0BAQsFADBAMQ0wCwYDVQQDDARuaWNr
13
+ MRkwFwYKCZImiZPyLGQBGRYJbmlja2pvbmVzMRQwEgYKCZImiZPyLGQBGRYEdGVj
14
+ aDAeFw0yNTA3MDYxOTM3MzNaFw0yNjA3MDYxOTM3MzNaMEAxDTALBgNVBAMMBG5p
15
+ Y2sxGTAXBgoJkiaJk/IsZAEZFgluaWNram9uZXMxFDASBgoJkiaJk/IsZAEZFgR0
16
+ ZWNoMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAtVeWYtCOx6NWkD1H
17
+ M01ivSrilcoVoJ10AtlLgH23yN3pP4iMg+/c4ZfFqsrSDFbigbOmimQx74NlMdFG
18
+ dhhrAfPvoHmzUYvPWnCXkkqWRnXdK7+0m1tG4HxD1JJhgPtQKDSK9xx4seLZt01b
19
+ kL4ndBDXd7YXCzQhoozwXBVbVuip3bE2OYnUsEV7wXMZJYHK5Tj68QH5oppHTRV5
20
+ 2Ws/W5CoHeZ7oExGYkYpF1U2/XG+722m6zHeUX9opLcXfvP+fpomDbFA7W1MFxTN
21
+ C+lxXhZYPt1+Pr9YeUnsc9uiX/NTgCvOCczR4eH5EeEchOZKFypiERajFXV9mDTG
22
+ f+fEBktE7zRdqTZD1KO/0peN8fvBlXWgA0fLD50UFfBR8s7pvYGzhs1uHiS8AFC/
23
+ BTWm7ttFetzKVZURP4ym3xxzpYuOTWDqkqvfd4ew6BDXBuZJsqXWE8X4qzDsNMCx
24
+ SGhyP3NbbRuY46T+T5K+k3zNtDRB7ZVjJFeum21mBkN6SVm7AgMBAAGjeTB3MAkG
25
+ A1UdEwQCMAAwCwYDVR0PBAQDAgSwMB0GA1UdDgQWBBTnEkevlJfGftCtQSV5MwW5
26
+ DDEKzDAeBgNVHREEFzAVgRNuaWNrQG5pY2tqb25lcy50ZWNoMB4GA1UdEgQXMBWB
27
+ E25pY2tAbmlja2pvbmVzLnRlY2gwDQYJKoZIhvcNAQELBQADggGBADCu8hK3XBEm
28
+ aVNShTjB1JR6Hy7+wCbUevLqRyTaf2aA2wpn7gJnlENMo4UKIXAzBSKMzTFTH2ZJ
29
+ 1WKrO045godBAqF005q3WHDNpUTCM1IALxxpUSqKJqRcz5zrymorX8sffqcP1dFY
30
+ Gp5Qtx/1YRD0cQ4iAHMCz9pPxVkOM4pvz/7M3JjoU3loP8kVIKMD3E4xVgccBayP
31
+ /9bTPaOM7sgtoPDzX/LsxICCBgxeW87wA7mwhQVgUu2BV0lVNKc5ZBXtoMBKHVEZ
32
+ gU7bzkS10OiaB8dP1C54NpOquXDXJ1szS/qzry/dKS3OULsSw731cmAYF1KXLN01
33
+ SNU7lyEKF7DglZ7e+v4q18c3QWttmkhJgNVku+scAf6Fg9qTB/Tp9znLSaC8JiEo
34
+ az50nHustlmuPWriALbXeG493US/lYAIesFK9oZfFC3L/K9ni6LZPvGNQVymEAAD
35
+ kOJf02iMdjt0UrCnKde4RZDPnifzBnMfEawwdHekT7T4UNWbzaaFgQ==
36
+ -----END CERTIFICATE-----
37
+ date: 1980-01-02 00:00:00.000000000 Z
38
+ dependencies:
39
+ - !ruby/object:Gem::Dependency
40
+ name: logger
41
+ requirement: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - "~>"
44
+ - !ruby/object:Gem::Version
45
+ version: '1.7'
46
+ type: :runtime
47
+ prerelease: false
48
+ version_requirements: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - "~>"
51
+ - !ruby/object:Gem::Version
52
+ version: '1.7'
53
+ email:
54
+ - nick@nickjones.tech
55
+ executables:
56
+ - stonk
57
+ extensions: []
58
+ extra_rdoc_files: []
59
+ files:
60
+ - ".rubocop.yml"
61
+ - CHANGELOG.md
62
+ - CODE_OF_CONDUCT.md
63
+ - LICENSE
64
+ - README.md
65
+ - Rakefile
66
+ - bin/stonk
67
+ - etc/punkstar.pem
68
+ - lib/stonk.rb
69
+ - lib/stonk/adapter.rb
70
+ - lib/stonk/adapter/alpha_vantage_adapter.rb
71
+ - lib/stonk/adapter/file_cache_adapter.rb
72
+ - lib/stonk/money.rb
73
+ - lib/stonk/service.rb
74
+ - lib/stonk/stock.rb
75
+ - lib/stonk/version.rb
76
+ - sig/stonk.rbs
77
+ homepage: https://github.com/punkstar/stonk.rb
78
+ licenses:
79
+ - MIT
80
+ metadata:
81
+ source_code_uri: https://github.com/punkstar/stonk.rb
82
+ changelog_uri: https://github.com/punkstar/stonk.rb/blob/main/CHANGELOG.md
83
+ rdoc_options: []
84
+ require_paths:
85
+ - lib
86
+ required_ruby_version: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: 3.2.0
91
+ required_rubygems_version: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ requirements: []
97
+ rubygems_version: 3.6.7
98
+ specification_version: 4
99
+ summary: A Ruby gem that fetches real-time stock prices from multiple sources.
100
+ test_files: []
metadata.gz.sig ADDED
Binary file