eodhd.rb 0.15.0 → 0.16.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 75d710273541a726ea8483974f5ac0f0a78ddee69fcb5024fe5a003ecfff3f35
4
- data.tar.gz: d156d5857f9cc1892d23f54c491a7a5cdefcea49135c50e16f1ab70ea402a0c9
3
+ metadata.gz: 1400c119e7a404b5100e55d51234c2bde712e9227454799556191bcb96bc2923
4
+ data.tar.gz: ed91c5bc5a273e50f5462780c55529dd26bb5a98fa318747ccbb390b5d4b2228
5
5
  SHA512:
6
- metadata.gz: 6c09445a3ef620a5ee45be611bf41d4d70c9edc2aef5a52ab691849eb4dea1c06381decb9534faa2b6ff680d11a3e44c47133b4a997125697fa84209add6c49a
7
- data.tar.gz: 30f076f3f55ff6c11fbd8551627b01b90791ca467834b6154e3be2f64f66c3edb7695a2dc6ed04be9464f3befd653ea854108833c8313a2939fe0fead7b2c478
6
+ metadata.gz: 4e13b25610267151935b2e57d2367ed3274ba327d3edcee569b16ad5ac2a956671cebbf81e4146ff727a1b36522d53c6005e1c14c4ffbf4ce49342bf11f70878
7
+ data.tar.gz: 03e6404cc5fc0e7e58758dbb4086deb5039fd9807f3c26e350c4b7f33cbb4cf9e363e123b94f37ea394a5ee973e35616b83c1bbd426403e3e8de1fc9cee64d5f
data/eodhd.rb.gemspec CHANGED
@@ -4,7 +4,7 @@ Gem::Specification.new do |spec|
4
4
  spec.name = 'eodhd.rb'
5
5
 
6
6
  spec.version = Eodhd::VERSION
7
- spec.date = '2025-09-16'
7
+ spec.date = '2025-10-05'
8
8
 
9
9
  spec.summary = "Access the eodhd.com API with Ruby."
10
10
  spec.description = "Access the eodhd.com API with Ruby."
@@ -17,6 +17,15 @@ Gem::Specification.new do |spec|
17
17
  spec.required_ruby_version = '>= 2.5'
18
18
 
19
19
  spec.add_dependency('http.rb')
20
+ spec.add_dependency('iodine')
21
+
22
+ spec.add_development_dependency('rake')
23
+ spec.add_development_dependency('minitest')
24
+ spec.add_development_dependency('minitest-spec-context')
25
+ spec.add_development_dependency('webmock')
26
+ spec.add_development_dependency('vcr')
27
+ spec.add_development_dependency('simplecov')
28
+
20
29
  spec.files = [
21
30
  'eodhd.rb.gemspec',
22
31
  'Gemfile',
data/lib/Eodhd/Client.rb CHANGED
@@ -3,13 +3,13 @@
3
3
 
4
4
  gem 'http.rb'
5
5
 
6
- require 'fileutils'
7
6
  require 'http.rb'
8
7
  require 'json'
9
- require 'logger'
10
8
 
9
+ require_relative './DefaultLogger'
11
10
  require_relative './Error'
12
11
  require_relative '../Hash/x_www_form_urlencode'
12
+ require_relative './Validations'
13
13
 
14
14
  class Eodhd
15
15
  class Client
@@ -17,30 +17,13 @@ class Eodhd
17
17
  API_HOST = 'eodhd.com'
18
18
 
19
19
  class << self
20
- attr_writer :log_file_path
21
-
22
20
  def path_prefix
23
21
  '/api'
24
22
  end
25
-
26
- def default_log_file_path
27
- File.join(%w{~ log eodhd log.txt})
28
- end
29
-
30
- def log_file_path
31
- File.expand_path(@log_file_path || default_log_file_path)
32
- end
33
-
34
- def log_file
35
- FileUtils.mkdir_p(File.dirname(log_file_path))
36
- File.open(log_file_path, File::WRONLY | File::APPEND | File::CREAT)
37
- end
38
-
39
- def logger
40
- @logger ||= Logger.new(log_file, 'daily')
41
- end
42
23
  end # class << self
43
24
 
25
+ include Validations
26
+
44
27
  # This endpoint always returns json regardless of what fmt is specified.
45
28
  def exchanges_list
46
29
  response = get(path: '/exchanges-list')
@@ -75,20 +58,15 @@ class Eodhd
75
58
  handle_response(response)
76
59
  end
77
60
 
61
+ attr_accessor\
62
+ :api_token,
63
+ :logger
64
+
78
65
  private
79
66
 
80
- def initialize(api_token:)
67
+ def initialize(api_token: nil, logger: nil, use_default_logger: false)
81
68
  @api_token = api_token
82
- end
83
-
84
- def validate_arguments(exchange_code: nil, exchange_id: nil, symbol: nil, period: nil, from: nil, to: nil, date: nil)
85
- exchange_code ||= exchange_id
86
- validate_exchange_code!(exchange_code) if exchange_code
87
- validate_symbol!(symbol) if symbol
88
- validate_period!(period) if period
89
- validate_date!(from, 'from') if from
90
- validate_date!(to, 'to') if to
91
- validate_date_range!(from, to) if from && to
69
+ @logger = use_default_logger ? DefaultLogger.logger : logger
92
70
  end
93
71
 
94
72
  def request_string(path)
@@ -104,24 +82,24 @@ class Eodhd
104
82
  if log_args?(args)
105
83
  log_string << "?#{args.x_www_form_urlencode}"
106
84
  end
107
- self.class.logger.info(log_string)
85
+ @logger.info(log_string)
108
86
  end
109
87
 
110
88
  def log_response(code:, message:, body:)
111
89
  log_string = "#{code}\n#{message}\n#{body}"
112
- self.class.logger.info(log_string)
90
+ @logger.info(log_string)
113
91
  end
114
92
 
115
93
  def log_error(code:, message:, body:)
116
94
  log_string = "#{code}\n#{message}\n#{body}"
117
- self.class.logger.error(log_string)
95
+ @logger.error(log_string)
118
96
  end
119
97
 
120
98
  def do_request(verb:, path:, args: {})
121
99
  api_token = args[:api_token] || @api_token
122
100
  fmt = args[:fmt] || 'json'
123
101
  args.merge!(api_token: api_token, fmt: fmt)
124
- log_request(verb: verb, request_string: request_string(path), args: args)
102
+ log_request(verb: verb, request_string: request_string(path), args: args) if use_logging?
125
103
  HTTP.send(verb.to_s.downcase, request_string(path), args)
126
104
  end
127
105
 
@@ -135,9 +113,13 @@ class Eodhd
135
113
  log_response(code: response.code, message: response.message, body: response.body) if use_logging?
136
114
  parsed_body
137
115
  else
138
- log_error(code: response.code, message: response.message, body: response.body)
116
+ log_error(code: response.code, message: response.message, body: response.body) if use_logging?
139
117
  raise Eodhd::Error.new(code: response.code, message: response.message, body: response.body)
140
118
  end
141
119
  end
120
+
121
+ def use_logging?
122
+ !@logger.nil?
123
+ end
142
124
  end
143
125
  end
@@ -0,0 +1,30 @@
1
+ # Eodhd/DefaultLogger.rb
2
+ # Eodhd::DefaultLogger
3
+
4
+ require 'fileutils'
5
+ require 'logger'
6
+
7
+ class Eodhd
8
+ class DefaultLogger
9
+ class << self
10
+ attr_writer :log_filepath
11
+
12
+ def default_log_filepath
13
+ File.join(%w{~ log eodhd log.txt})
14
+ end
15
+
16
+ def log_filepath
17
+ File.expand_path(@log_filepath || default_log_filepath)
18
+ end
19
+
20
+ def log_file
21
+ FileUtils.mkdir_p(File.dirname(log_filepath))
22
+ File.open(log_filepath, File::WRONLY | File::APPEND | File::CREAT)
23
+ end
24
+
25
+ def logger
26
+ @logger ||= Logger.new(log_file, 'daily')
27
+ end
28
+ end
29
+ end
30
+ end
data/lib/Eodhd/VERSION.rb CHANGED
@@ -1,3 +1,3 @@
1
- module Eodhd
2
- VERSION = '0.15.0'
1
+ class Eodhd
2
+ VERSION = '0.16.0'
3
3
  end
@@ -1,16 +1,26 @@
1
1
  # Eodhd/Validations.rb
2
2
  # Eodhd::Validations
3
3
 
4
- module Eodhd
4
+ class Eodhd
5
5
  module Validations
6
- def validate_exchange_code!(exchange_code)
6
+ def validate_arguments(exchange_code: nil, exchange_id: nil, symbol: nil, period: nil, from: nil, to: nil, date: nil)
7
+ exchange_code ||= exchange_id
8
+ validate_exchange_code(exchange_code)
9
+ validate_symbol(symbol)
10
+ validate_period(period)
11
+ validate_date(from, 'from')
12
+ validate_date(to, 'to')
13
+ validate_date_range(from, to)
14
+ end
15
+
16
+ def validate_exchange_code(exchange_code)
7
17
  return unless exchange_code
8
18
  unless exchange_code.match?(/\A[A-Z]{2,6}\z/)
9
19
  raise ArgumentError, "Invalid exchange_code '#{exchange_code}'. Must be 2-6 uppercase letters"
10
20
  end
11
21
  end
12
22
 
13
- def validate_symbol!(symbol)
23
+ def validate_symbol(symbol)
14
24
  return unless symbol
15
25
  if symbol.strip.empty?
16
26
  raise ArgumentError, "Symbol cannot be empty"
@@ -20,7 +30,7 @@ module Eodhd
20
30
  end
21
31
  end
22
32
 
23
- def validate_period!(period)
33
+ def validate_period(period)
24
34
  return unless period
25
35
  valid_periods = %w[d w m]
26
36
  unless valid_periods.include?(period)
@@ -28,7 +38,7 @@ module Eodhd
28
38
  end
29
39
  end
30
40
 
31
- def validate_date!(date, param_name = 'date')
41
+ def validate_date(date, param_name = 'date')
32
42
  return unless date
33
43
  case date
34
44
  when Date
@@ -47,7 +57,7 @@ module Eodhd
47
57
  end
48
58
  end
49
59
 
50
- def validate_date_range!(from, to)
60
+ def validate_date_range(from, to)
51
61
  return unless from && to
52
62
  from_date = from.is_a?(Date) ? from : Date.parse(from.to_s)
53
63
  to_date = to.is_a?(Date) ? to : Date.parse(to.to_s)
@@ -9,41 +9,23 @@
9
9
  # iv. Crypto: wss://ws.eodhistoricaldata.com/ws/crypto?api_token=XXX
10
10
 
11
11
  require 'iodine'
12
- require 'json' # This line doesn't seem to be necessary in Ruby 3.
12
+ require 'json'
13
+
14
+ require_relative './DefaultLogger'
15
+ require_relative './Error'
13
16
 
14
17
  class Eodhd
15
18
  class WebSocketClient
16
19
  API_HOST = 'ws.eodhistoricaldata.com'
17
20
 
18
21
  class Handler
19
- class << self
20
- attr_writer :log_file_path
21
-
22
- def default_log_file_path
23
- File.join(%w{~ log eodhd log.txt})
24
- end
25
-
26
- def log_file_path
27
- File.expand_path(@log_file_path || default_log_file_path)
28
- end
29
-
30
- def log_file
31
- FileUtils.mkdir_p(File.dirname(log_file_path))
32
- File.open(log_file_path, File::WRONLY | File::APPEND | File::CREAT)
33
- end
34
-
35
- def logger
36
- @logger ||= Logger.new(log_file, 'daily')
37
- end
38
- end # class << self
39
-
40
22
  def on_open(connection)
41
23
  connection.write({action: 'subscribe', symbols: @symbols}.to_json)
42
24
  end
43
25
 
44
26
  def on_message(connection, message)
45
27
  dataframe = handle_response(message)
46
- @consumer.call(dataframe)
28
+ @consumer.call(dataframe) if @consumer
47
29
  end
48
30
 
49
31
  def on_close(connection)
@@ -52,29 +34,26 @@ class Eodhd
52
34
 
53
35
  private
54
36
 
55
- def initialize(symbols:, consumer:)
37
+ def initialize(symbols:, consumer:, logger: nil)
56
38
  @symbols = symbols
57
39
  @consumer = consumer
40
+ @logger = logger
58
41
  end
59
42
 
60
43
  def log_error(code:, message:, body:)
61
44
  log_string = "WebSocketError #{code}\n#{message}\n#{body}"
62
- self.class.logger.error(log_string)
45
+ @logger.error(log_string) if use_logging?
63
46
  end
64
47
 
65
48
  def handle_response(message)
66
- if parsed_json = JSON.parse(message)
67
- parsed_json
68
- else
69
- log_error(
70
- message: response.message,
71
- )
72
- raise Eodhd::Error.new(
73
- code: 'ws',
74
- message: response.message,
75
- body: ''
76
- )
77
- end
49
+ JSON.parse(message)
50
+ rescue StandardError => e
51
+ log_error(code: 'ws_parse_error', message: e.message, body: message)
52
+ raise Eodhd::Error.new(code: 'ws_parse_error', message: e.message, body: message)
53
+ end
54
+
55
+ def use_logging?
56
+ !@logger.nil?
78
57
  end
79
58
  end
80
59
 
@@ -104,7 +83,8 @@ class Eodhd
104
83
  attr_accessor\
105
84
  :api_token,
106
85
  :asset_class,
107
- :consumer
86
+ :consumer,
87
+ :logger
108
88
 
109
89
  attr_reader\
110
90
  :symbols
@@ -115,7 +95,7 @@ class Eodhd
115
95
 
116
96
  def run
117
97
  Iodine.threads = 1
118
- Iodine.connect(url: url, handler: Handler.new(symbols: @symbols, consumer: @consumer))
98
+ Iodine.connect(url: url, handler: Handler.new(symbols: @symbols, consumer: @consumer, logger: @logger))
119
99
  Iodine.start
120
100
  rescue SystemExit, Interrupt
121
101
  return
@@ -123,11 +103,16 @@ class Eodhd
123
103
 
124
104
  private
125
105
 
126
- def initialize(api_token:, asset_class:, symbols:, consumer:)
106
+ def initialize(api_token:, asset_class:, symbols:, consumer:, logger: nil, use_default_logger: false)
127
107
  @api_token = api_token
128
108
  @asset_class = asset_class # crypto, forex, us-quote, us
129
109
  @symbols = format_symbols(symbols)
130
110
  @consumer = consumer
111
+ @logger = use_default_logger ? DefaultLogger.logger : logger
112
+ end
113
+
114
+ def use_logging?
115
+ !@logger.nil?
131
116
  end
132
117
 
133
118
  def format_symbols(symbols)
@@ -1,12 +1,12 @@
1
1
  # Eodhd.rb
2
2
  # Eodhd
3
3
 
4
- require_relative 'Eodhd/Client'
5
- require_relative 'Eodhd/EodBulkLastDay'
6
- require_relative 'Eodhd/EodData'
7
- require_relative 'Eodhd/Exchange'
8
- require_relative 'Eodhd/ExchangeSymbol'
9
- require_relative 'Eodhd/WebSocketClient'
4
+ require_relative './Eodhd/Client'
5
+ require_relative './Eodhd/EodBulkLastDay'
6
+ require_relative './Eodhd/EodData'
7
+ require_relative './Eodhd/Exchange'
8
+ require_relative './Eodhd/ExchangeSymbol'
9
+ require_relative './Eodhd/WebSocketClient'
10
10
 
11
11
  class Eodhd
12
12
  def initialize(api_token:, consumer: nil)
@@ -0,0 +1,71 @@
1
+ require_relative "helper"
2
+
3
+ describe Eodhd::Client do
4
+ let(:api_token){ENV.fetch('EODHD_API_TOKEN', '<API_TOKEN>')}
5
+
6
+ let(:client){Eodhd::Client.new(api_token: api_token)}
7
+
8
+ describe "#exchanges_list" do
9
+ it "exchanges_list returns parsed JSON array" do
10
+ VCR.use_cassette("client_exchanges_list") do
11
+ result = client.exchanges_list
12
+ _(result).must_be_kind_of(Array)
13
+ _(result.first["Code"]).wont_be_nil
14
+ end
15
+ end
16
+
17
+ it "raises Eodhd::Error on failure" do
18
+ VCR.use_cassette("client_exchanges_list_401_error") do
19
+ _{client.exchanges_list}.must_raise(Eodhd::Error)
20
+ end
21
+ end
22
+ end
23
+
24
+ describe "#exchange_symbol_list" do
25
+ it "exchange_symbol_list returns parsed JSON array" do
26
+ VCR.use_cassette("client_exchange_symbol_list") do
27
+ result = client.exchange_symbol_list(exchange_code: 'AU')
28
+ _(result).must_be_kind_of(Array)
29
+ _(result.first['Code']).wont_be_nil
30
+ end
31
+ end
32
+
33
+ it "raises Eodhd::Error on failure" do
34
+ VCR.use_cassette("client_exchange_symbol_list_401_error") do
35
+ _{client.exchange_symbol_list(exchange_code: 'AU')}.must_raise(Eodhd::Error)
36
+ end
37
+ end
38
+ end
39
+
40
+ describe "#eod_data" do
41
+ it "eod_data returns parsed JSON array" do
42
+ VCR.use_cassette('client_eod_data') do
43
+ result = client.eod_data(exchange_id: 'AU', symbol: 'BHP', period: 'd')
44
+ _(result).must_be_kind_of(Array)
45
+ _(result.first.keys).must_equal(%w{date open high low close adjusted_close volume})
46
+ end
47
+ end
48
+
49
+ it "raises Eodhd::Error on failure" do
50
+ VCR.use_cassette('client_eod_data_401_error') do
51
+ _{client.eod_data(exchange_id: 'AU', symbol: 'BHP', period: 'd')}.must_raise(Eodhd::Error)
52
+ end
53
+ end
54
+ end
55
+
56
+ describe "#eod_bulk_last_day" do
57
+ it "eod_bulk_last_day returns parsed JSON array" do
58
+ VCR.use_cassette("client_eod_bulk_last_day") do
59
+ result = client.eod_bulk_last_day(exchange_id: 'AU', date: "2024-09-30")
60
+ _(result).must_be_kind_of(Array)
61
+ _(result.first.keys).must_equal(%w{code exchange_short_name date open high low close adjusted_close volume})
62
+ end
63
+ end
64
+
65
+ it "raises Eodhd::Error on failure" do
66
+ VCR.use_cassette('client_eod_bulk_last_day_401_error') do
67
+ _{client.eod_bulk_last_day(exchange_id: 'AU', date: "2024-09-30")}.must_raise(Eodhd::Error)
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,50 @@
1
+ require_relative "helper"
2
+
3
+ describe Eodhd do
4
+ let(:api_token){ENV.fetch('EODHD_API_TOKEN', '<API_TOKEN>')}
5
+ let(:eodhd){Eodhd.new(api_token: api_token)}
6
+
7
+ describe "#exchanges" do
8
+ it "delegates to Eodhd::Exchange.all" do
9
+ VCR.use_cassette('eodhd_exchanges') do
10
+ exchanges = eodhd.exchanges
11
+ _(exchanges).must_be_kind_of(Array)
12
+ _(exchanges).wont_be_empty
13
+ _(exchanges.first).must_be_kind_of(Eodhd::Exchange)
14
+ end
15
+ end
16
+ end
17
+
18
+ describe "#exchange_symbols" do
19
+ it "delegates to Eodhd::ExchangeSymbol.all" do
20
+ VCR.use_cassette('eodhd_exchange_symbols') do
21
+ symbols = eodhd.exchange_symbols(exchange_code: 'AU')
22
+ _(symbols).must_be_kind_of(Array)
23
+ _(symbols).wont_be_empty
24
+ _(symbols.first).must_be_kind_of(Eodhd::ExchangeSymbol)
25
+ end
26
+ end
27
+ end
28
+
29
+ describe "#eod_data" do
30
+ it "delegates to Eodhd::EodData.all" do
31
+ VCR.use_cassette('eodhd_eod_data') do
32
+ eod_data = eodhd.eod_data(exchange_code: 'AU', symbol: 'BHP', period: 'd')
33
+ _(eod_data).must_be_kind_of(Array)
34
+ _(eod_data).wont_be_empty
35
+ _(eod_data.first).must_be_kind_of(Eodhd::EodData)
36
+ end
37
+ end
38
+ end
39
+
40
+ describe "#eod_bulk_last_day" do
41
+ it "delegates to Eodhd::EodBulkLastDay.all" do
42
+ VCR.use_cassette('eodhd_eod_bulk_last_day') do
43
+ bulk_last_day = eodhd.eod_bulk_last_day(exchange_code: 'AU', date: '2024-09-30')
44
+ _(bulk_last_day).must_be_kind_of(Array)
45
+ _(bulk_last_day).wont_be_empty
46
+ _(bulk_last_day.first).must_be_kind_of(Eodhd::EodBulkLastDay)
47
+ end
48
+ end
49
+ end
50
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,14 @@
1
+ require 'minitest/autorun'
2
+ require 'minitest/spec'
3
+ require 'minitest-spec-context'
4
+ require 'vcr'
5
+ require 'webmock/minitest'
6
+
7
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
8
+ require 'Eodhd'
9
+
10
+ VCR.configure do |config|
11
+ config.cassette_library_dir = File.expand_path('./fixtures/vcr_cassettes', __dir__)
12
+ config.hook_into :webmock
13
+ config.filter_sensitive_data('<API_TOKEN>'){ENV['EODHD_API_TOKEN']}
14
+ end
data/test/test_all.rb ADDED
@@ -0,0 +1,5 @@
1
+ require_relative "helper"
2
+
3
+ Dir[File.expand_path("./**/*_test.rb", __dir__)].each do |file|
4
+ require file
5
+ end
@@ -0,0 +1,118 @@
1
+ require_relative 'helper'
2
+
3
+ class DummyIncludingValidations
4
+ include Eodhd::Validations
5
+ end
6
+
7
+ describe "Eodhd::Validations" do
8
+ let(:v){DummyIncludingValidations.new}
9
+
10
+ describe "#validate_arguments" do
11
+ it "accepts all valid arguments" do
12
+ _{v.validate_arguments(
13
+ exchange_code: 'AU',
14
+ symbol: 'BHP',
15
+ period: 'd',
16
+ from: '2024-01-01',
17
+ to: '2024-12-31'
18
+ )}.must_be_silent
19
+ end
20
+
21
+ it "accepts exchange_id as alias for exchange_code" do
22
+ _{v.validate_arguments(exchange_id: 'AU')}.must_be_silent
23
+ end
24
+
25
+ it "raises ArgumentError for invalid exchange_code" do
26
+ _{v.validate_arguments(exchange_code: 'au')}.must_raise(ArgumentError)
27
+ end
28
+
29
+ it "raises ArgumentError for invalid symbol" do
30
+ _{v.validate_arguments(symbol: 'bad$')}.must_raise(ArgumentError)
31
+ end
32
+
33
+ it "raises ArgumentError for invalid period" do
34
+ _{v.validate_arguments(period: 'h')}.must_raise(ArgumentError)
35
+ end
36
+
37
+ it "raises ArgumentError for invalid from date" do
38
+ _{v.validate_arguments(from: '2024/01/01')}.must_raise(ArgumentError)
39
+ end
40
+
41
+ it "raises ArgumentError for invalid to date" do
42
+ _{v.validate_arguments(to: '2024-02-30')}.must_raise(ArgumentError)
43
+ end
44
+
45
+ it "raises ArgumentError for reversed date range" do
46
+ _{v.validate_arguments(from: '2024-12-31', to: '2024-01-01')}.must_raise(ArgumentError)
47
+ end
48
+
49
+ it "accepts nil values for all arguments" do
50
+ _{v.validate_arguments}.must_be_silent
51
+ end
52
+ end
53
+
54
+ describe "#validate_exchange_code" do
55
+ it "accepts valid exchange codes" do
56
+ _{v.validate_exchange_code('AU')}.must_be_silent
57
+ _{v.validate_exchange_code('XETRA')}.must_be_silent
58
+ end
59
+
60
+ it "rejects invalid exchange codes" do
61
+ _{v.validate_exchange_code('us')}.must_raise(ArgumentError)
62
+ _{v.validate_exchange_code('U')}.must_raise(ArgumentError)
63
+ _{v.validate_exchange_code('TOOLONG')}.must_raise(ArgumentError)
64
+ end
65
+ end
66
+
67
+ describe "#validate_symbol" do
68
+ it "accepts valid symbols" do
69
+ _{v.validate_symbol('AAPL')}.must_be_silent
70
+ _{v.validate_symbol('BRK.B')}.must_be_silent
71
+ _{v.validate_symbol('RDS-A')}.must_be_silent
72
+ end
73
+
74
+ it "rejects invalid symbols" do
75
+ _{v.validate_symbol('')}.must_raise(ArgumentError)
76
+ _{v.validate_symbol(' ')}.must_raise(ArgumentError)
77
+ _{v.validate_symbol('THIS_SYMBOL_IS_TOO_LONG')}.must_raise(ArgumentError)
78
+ _{v.validate_symbol('bad$')}.must_raise(ArgumentError)
79
+ end
80
+ end
81
+
82
+ describe "#validate_period" do
83
+ it "accepts valid periods" do
84
+ _{v.validate_period('d')}.must_be_silent
85
+ _{v.validate_period('w')}.must_be_silent
86
+ _{v.validate_period('m')}.must_be_silent
87
+ end
88
+
89
+ it "rejects invalid periods" do
90
+ _{v.validate_period('h')}.must_raise(ArgumentError)
91
+ _{v.validate_period('')}.must_raise(ArgumentError)
92
+ end
93
+ end
94
+
95
+ describe "#validate_date" do
96
+ it "accepts valid date formats and Date objects" do
97
+ _{v.validate_date('2024-09-30')}.must_be_silent
98
+ _{v.validate_date(Date.new(2024,9,30))}.must_be_silent
99
+ end
100
+
101
+ it "rejects invalid dates and formats" do
102
+ _{v.validate_date('2024/09/30')}.must_raise(ArgumentError)
103
+ _{v.validate_date('2024-02-30')}.must_raise(ArgumentError)
104
+ _{v.validate_date(12345)}.must_raise(ArgumentError)
105
+ end
106
+ end
107
+
108
+ describe "#validate_date_range" do
109
+ it "accepts valid date ranges" do
110
+ _{v.validate_date_range('2024-01-01','2024-12-31')}.must_be_silent
111
+ _{v.validate_date_range(Date.new(2024,1,1),Date.new(2024,12,31))}.must_be_silent
112
+ end
113
+
114
+ it "rejects reversed date ranges" do
115
+ _{v.validate_date_range('2024-12-31','2024-01-01')}.must_raise(ArgumentError)
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,97 @@
1
+ require_relative 'helper'
2
+
3
+ describe Eodhd::WebSocketClient do
4
+ let(:api_token){ENV.fetch('EODHD_API_TOKEN', '<API_TOKEN>')}
5
+ let(:consumer){->(data){}}
6
+
7
+ describe "#initialize" do
8
+ it "accepts array of symbols and formats to comma-separated string" do
9
+ client = Eodhd::WebSocketClient.new(api_token: api_token, asset_class: 'us', symbols: ['AAPL', 'MSFT'], consumer: consumer)
10
+ _(client.symbols).must_equal('AAPL,MSFT')
11
+ end
12
+
13
+ it "accepts string of symbols" do
14
+ client = Eodhd::WebSocketClient.new(api_token: api_token, asset_class: 'us', symbols: 'AAPL,MSFT', consumer: consumer)
15
+ _(client.symbols).must_equal('AAPL,MSFT')
16
+ end
17
+ end
18
+
19
+ describe "#url" do
20
+ it "generates correct URL for US trade stream" do
21
+ client = Eodhd::WebSocketClient.new(api_token: api_token, asset_class: 'us', symbols: 'AAPL', consumer: consumer)
22
+ _(client.send(:url)).must_equal("wss://ws.eodhistoricaldata.com/ws/us?api_token=#{api_token}")
23
+ end
24
+
25
+ it "generates correct URL for US quote stream" do
26
+ client = Eodhd::WebSocketClient.new(api_token: api_token, asset_class: 'us-quote', symbols: 'AAPL', consumer: consumer)
27
+ _(client.send(:url)).must_equal("wss://ws.eodhistoricaldata.com/ws/us-quote?api_token=#{api_token}")
28
+ end
29
+
30
+ it "generates correct URL for forex stream" do
31
+ client = Eodhd::WebSocketClient.new(api_token: api_token, asset_class: 'forex', symbols: 'EURUSD', consumer: consumer)
32
+ _(client.send(:url)).must_equal("wss://ws.eodhistoricaldata.com/ws/forex?api_token=#{api_token}")
33
+ end
34
+
35
+ it "generates correct URL for crypto stream" do
36
+ client = Eodhd::WebSocketClient.new(api_token: api_token, asset_class: 'crypto', symbols: 'BTC-USD', consumer: consumer)
37
+ _(client.send(:url)).must_equal("wss://ws.eodhistoricaldata.com/ws/crypto?api_token=#{api_token}")
38
+ end
39
+ end
40
+
41
+ describe "#format_symbols" do
42
+ it "converts array to comma-separated string" do
43
+ client = Eodhd::WebSocketClient.new(api_token: api_token, asset_class: 'us', symbols: ['AAPL', 'MSFT', 'GOOGL'], consumer: consumer)
44
+ _(client.symbols).must_equal('AAPL,MSFT,GOOGL')
45
+ end
46
+
47
+ it "returns string as-is" do
48
+ client = Eodhd::WebSocketClient.new(api_token: api_token, asset_class: 'us', symbols: 'AAPL,MSFT', consumer: consumer)
49
+ _(client.symbols).must_equal('AAPL,MSFT')
50
+ end
51
+ end
52
+
53
+ describe "#run" do
54
+ it "configures Iodine and starts connection" do
55
+ client = Eodhd::WebSocketClient.new(api_token: api_token, asset_class: 'us', symbols: 'AAPL', consumer: consumer)
56
+
57
+ iodine_connect_called = false
58
+ iodine_start_called = false
59
+
60
+ Iodine.stub(:threads=, nil) do
61
+ Iodine.stub(:connect, ->(opts){iodine_connect_called = true}) do
62
+ Iodine.stub(:start, ->{iodine_start_called = true}) do
63
+ client.run
64
+ end
65
+ end
66
+ end
67
+
68
+ _(iodine_connect_called).must_equal(true)
69
+ _(iodine_start_called).must_equal(true)
70
+ end
71
+ end
72
+
73
+ describe "Handler" do
74
+ describe "#on_message" do
75
+ it "parses JSON and calls consumer" do
76
+ received_data = nil
77
+ test_consumer = ->(data){received_data = data}
78
+ handler = Eodhd::WebSocketClient::Handler.new(symbols: 'AAPL', consumer: test_consumer, logger: nil)
79
+
80
+ message = '{"symbol":"AAPL","price":150.0}'
81
+ handler.on_message(nil, message)
82
+
83
+ _(received_data).must_be_kind_of(Hash)
84
+ _(received_data['symbol']).must_equal('AAPL')
85
+ _(received_data['price']).must_equal(150.0)
86
+ end
87
+
88
+ it "raises Eodhd::Error on invalid JSON" do
89
+ test_consumer = ->(data){}
90
+ handler = Eodhd::WebSocketClient::Handler.new(symbols: 'AAPL', consumer: test_consumer, logger: nil)
91
+
92
+ invalid_message = '{invalid json'
93
+ _{handler.on_message(nil, invalid_message)}.must_raise(Eodhd::Error)
94
+ end
95
+ end
96
+ end
97
+ end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: eodhd.rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.15.0
4
+ version: 0.16.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - thoran
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-09-16 00:00:00.000000000 Z
10
+ date: 2025-10-05 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: http.rb
@@ -23,6 +23,104 @@ dependencies:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
25
  version: '0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: iodine
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rake
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: minitest
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: minitest-spec-context
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ - !ruby/object:Gem::Dependency
83
+ name: webmock
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ - !ruby/object:Gem::Dependency
97
+ name: vcr
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ type: :development
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ - !ruby/object:Gem::Dependency
111
+ name: simplecov
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ type: :development
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
26
124
  description: Access the eodhd.com API with Ruby.
27
125
  email: code@thoran.com
28
126
  executables: []
@@ -32,7 +130,9 @@ files:
32
130
  - Gemfile
33
131
  - README.md
34
132
  - eodhd.rb.gemspec
133
+ - lib/Eodhd.rb
35
134
  - lib/Eodhd/Client.rb
135
+ - lib/Eodhd/DefaultLogger.rb
36
136
  - lib/Eodhd/EodBulkLastDay.rb
37
137
  - lib/Eodhd/EodData.rb
38
138
  - lib/Eodhd/Error.rb
@@ -44,7 +144,12 @@ files:
44
144
  - lib/Hash/x_www_form_urlencode.rb
45
145
  - lib/Thoran/Hash/XWwwFormUrlencode/x_www_form_urlencode.rb
46
146
  - lib/Thoran/String/UrlEncode/url_encode.rb
47
- - lib/eodhd.rb
147
+ - test/client_test.rb
148
+ - test/eodhd_test.rb
149
+ - test/helper.rb
150
+ - test/test_all.rb
151
+ - test/validations_test.rb
152
+ - test/web_socket_client_test.rb
48
153
  homepage: http://github.com/thoran/eodhd.rb
49
154
  licenses:
50
155
  - Ruby