eodhd.rb 0.14.2 → 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: 44210fb44fe2ba7e5988aa8dc632eb795d2d758f29b004e9723e28564c65c4f5
4
- data.tar.gz: 98581f3f62c684146b423f66f48bef85545ec335c9c831ac80dc41cccb925138
3
+ metadata.gz: 1400c119e7a404b5100e55d51234c2bde712e9227454799556191bcb96bc2923
4
+ data.tar.gz: ed91c5bc5a273e50f5462780c55529dd26bb5a98fa318747ccbb390b5d4b2228
5
5
  SHA512:
6
- metadata.gz: 6021c0cbd10ac0c9d4527f5c08888159aba35ca0286679f041d13599387eb7bc4ee0826abe6b835f47ba1e333f3082c45fcedc9b16e58e641a949d7e864b79ea
7
- data.tar.gz: 43a689fb9bcc2c83fc082090717bdf0defd1add686c9bb1b370bba6b60b70f2b4ac14cde375ada26f85d87e8726452b300be170b5f6354bd91638a6059bd8986
6
+ metadata.gz: 4e13b25610267151935b2e57d2367ed3274ba327d3edcee569b16ad5ac2a956671cebbf81e4146ff727a1b36522d53c6005e1c14c4ffbf4ce49342bf11f70878
7
+ data.tar.gz: 03e6404cc5fc0e7e58758dbb4086deb5039fd9807f3c26e350c4b7f33cbb4cf9e363e123b94f37ea394a5ee973e35616b83c1bbd426403e3e8de1fc9cee64d5f
data/eodhd.rb.gemspec CHANGED
@@ -1,8 +1,10 @@
1
+ require_relative './lib/Eodhd/VERSION'
2
+
1
3
  Gem::Specification.new do |spec|
2
4
  spec.name = 'eodhd.rb'
3
5
 
4
- spec.version = '0.14.2'
5
- spec.date = '2025-03-26'
6
+ spec.version = Eodhd::VERSION
7
+ spec.date = '2025-10-05'
6
8
 
7
9
  spec.summary = "Access the eodhd.com API with Ruby."
8
10
  spec.description = "Access the eodhd.com API with Ruby."
@@ -15,6 +17,15 @@ Gem::Specification.new do |spec|
15
17
  spec.required_ruby_version = '>= 2.5'
16
18
 
17
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
+
18
29
  spec.files = [
19
30
  'eodhd.rb.gemspec',
20
31
  'Gemfile',
data/lib/Eodhd/Client.rb CHANGED
@@ -1,14 +1,15 @@
1
1
  # Eodhd/Client.rb
2
2
  # Eodhd::Client
3
3
 
4
- require 'fileutils'
5
4
  gem 'http.rb'
5
+
6
6
  require 'http.rb'
7
7
  require 'json'
8
- require 'logger'
9
8
 
9
+ require_relative './DefaultLogger'
10
10
  require_relative './Error'
11
11
  require_relative '../Hash/x_www_form_urlencode'
12
+ require_relative './Validations'
12
13
 
13
14
  class Eodhd
14
15
  class Client
@@ -16,30 +17,13 @@ class Eodhd
16
17
  API_HOST = 'eodhd.com'
17
18
 
18
19
  class << self
19
- attr_writer :log_file_path
20
-
21
20
  def path_prefix
22
21
  '/api'
23
22
  end
24
-
25
- def default_log_file_path
26
- File.join(%w{~ log eodhd log.txt})
27
- end
28
-
29
- def log_file_path
30
- File.expand_path(@log_file_path || default_log_file_path)
31
- end
32
-
33
- def log_file
34
- FileUtils.mkdir_p(File.dirname(log_file_path))
35
- File.open(log_file_path, File::WRONLY | File::APPEND | File::CREAT)
36
- end
37
-
38
- def logger
39
- @logger ||= Logger.new(log_file, 'daily')
40
- end
41
23
  end # class << self
42
24
 
25
+ include Validations
26
+
43
27
  # This endpoint always returns json regardless of what fmt is specified.
44
28
  def exchanges_list
45
29
  response = get(path: '/exchanges-list')
@@ -47,11 +31,19 @@ class Eodhd
47
31
  end
48
32
 
49
33
  def exchange_symbol_list(exchange_code:)
34
+ validate_arguments(exchange_code: exchange_code)
50
35
  response = get(path: "/exchange-symbol-list/#{exchange_code}")
51
36
  handle_response(response)
52
37
  end
53
38
 
54
39
  def eod_data(exchange_id:, symbol:, period:, from: nil, to: nil)
40
+ validate_arguments(
41
+ exchange_id: exchange_id,
42
+ symbol: symbol,
43
+ period: period,
44
+ from: from,
45
+ to: to
46
+ )
55
47
  args = {period: period}
56
48
  args.merge!(from: from) if from
57
49
  args.merge!(to: to) if to
@@ -60,15 +52,21 @@ class Eodhd
60
52
  end
61
53
 
62
54
  def eod_bulk_last_day(exchange_id:, date:)
55
+ validate_arguments(exchange_id: exchange_id, date: date)
63
56
  args = {date: date}
64
57
  response = get(path: "/eod-bulk-last-day/#{exchange_id}", args: args)
65
58
  handle_response(response)
66
59
  end
67
60
 
61
+ attr_accessor\
62
+ :api_token,
63
+ :logger
64
+
68
65
  private
69
66
 
70
- def initialize(api_token:)
67
+ def initialize(api_token: nil, logger: nil, use_default_logger: false)
71
68
  @api_token = api_token
69
+ @logger = use_default_logger ? DefaultLogger.logger : logger
72
70
  end
73
71
 
74
72
  def request_string(path)
@@ -84,19 +82,24 @@ class Eodhd
84
82
  if log_args?(args)
85
83
  log_string << "?#{args.x_www_form_urlencode}"
86
84
  end
87
- self.class.logger.info(log_string)
85
+ @logger.info(log_string)
86
+ end
87
+
88
+ def log_response(code:, message:, body:)
89
+ log_string = "#{code}\n#{message}\n#{body}"
90
+ @logger.info(log_string)
88
91
  end
89
92
 
90
93
  def log_error(code:, message:, body:)
91
94
  log_string = "#{code}\n#{message}\n#{body}"
92
- self.class.logger.error(log_string)
95
+ @logger.error(log_string)
93
96
  end
94
97
 
95
98
  def do_request(verb:, path:, args: {})
96
- log_request(verb: verb, request_string: request_string(path), args: args)
97
99
  api_token = args[:api_token] || @api_token
98
100
  fmt = args[:fmt] || 'json'
99
101
  args.merge!(api_token: api_token, fmt: fmt)
102
+ log_request(verb: verb, request_string: request_string(path), args: args) if use_logging?
100
103
  HTTP.send(verb.to_s.downcase, request_string(path), args)
101
104
  end
102
105
 
@@ -106,19 +109,17 @@ class Eodhd
106
109
 
107
110
  def handle_response(response)
108
111
  if response.success?
109
- JSON.parse(response.body)
112
+ parsed_body = JSON.parse(response.body)
113
+ log_response(code: response.code, message: response.message, body: response.body) if use_logging?
114
+ parsed_body
110
115
  else
111
- log_error(
112
- code: response.code,
113
- message: response.message,
114
- body: response.body
115
- )
116
- raise Eodhd::Error.new(
117
- code: response.code,
118
- message: response.message,
119
- body: response.body
120
- )
116
+ log_error(code: response.code, message: response.message, body: response.body) if use_logging?
117
+ raise Eodhd::Error.new(code: response.code, message: response.message, body: response.body)
121
118
  end
122
119
  end
120
+
121
+ def use_logging?
122
+ !@logger.nil?
123
+ end
123
124
  end
124
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
@@ -0,0 +1,3 @@
1
+ class Eodhd
2
+ VERSION = '0.16.0'
3
+ end
@@ -0,0 +1,69 @@
1
+ # Eodhd/Validations.rb
2
+ # Eodhd::Validations
3
+
4
+ class Eodhd
5
+ module Validations
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)
17
+ return unless exchange_code
18
+ unless exchange_code.match?(/\A[A-Z]{2,6}\z/)
19
+ raise ArgumentError, "Invalid exchange_code '#{exchange_code}'. Must be 2-6 uppercase letters"
20
+ end
21
+ end
22
+
23
+ def validate_symbol(symbol)
24
+ return unless symbol
25
+ if symbol.strip.empty?
26
+ raise ArgumentError, "Symbol cannot be empty"
27
+ end
28
+ unless symbol.match?(/\A[A-Za-z0-9.-]{1,12}\z/)
29
+ raise ArgumentError, "Invalid symbol '#{symbol}'. Must be 1-12 characters, letters/numbers/dots/hyphens only"
30
+ end
31
+ end
32
+
33
+ def validate_period(period)
34
+ return unless period
35
+ valid_periods = %w[d w m]
36
+ unless valid_periods.include?(period)
37
+ raise ArgumentError, "Invalid period '#{period}'. Must be one of: #{valid_periods.join(', ')}"
38
+ end
39
+ end
40
+
41
+ def validate_date(date, param_name = 'date')
42
+ return unless date
43
+ case date
44
+ when Date
45
+ return
46
+ when String
47
+ unless date.match?(/\A\d{4}-\d{2}-\d{2}\z/)
48
+ raise ArgumentError, "Invalid #{param_name} '#{date}'. Must be in format 'yyyy-mm-dd'"
49
+ end
50
+ begin
51
+ Date.parse(date)
52
+ rescue Date::Error
53
+ raise ArgumentError, "Invalid #{param_name} '#{date}'. Not a valid date"
54
+ end
55
+ else
56
+ raise ArgumentError, "Invalid #{param_name}. Must be String in 'yyyy-mm-dd' format or Date object"
57
+ end
58
+ end
59
+
60
+ def validate_date_range(from, to)
61
+ return unless from && to
62
+ from_date = from.is_a?(Date) ? from : Date.parse(from.to_s)
63
+ to_date = to.is_a?(Date) ? to : Date.parse(to.to_s)
64
+ if from_date > to_date
65
+ raise ArgumentError, "from date (#{from}) cannot be after to date (#{to})"
66
+ end
67
+ end
68
+ end
69
+ end
@@ -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,32 +1,12 @@
1
1
  # Eodhd.rb
2
2
  # Eodhd
3
3
 
4
- # 20250326
5
- # 0.14.2
6
-
7
- # Changes since 0.13:
8
- # -/0: Add WebSockets.
9
- # 1. + Eodhd::WebSocketClient
10
- # 2. + Eodhd#web_socket
11
- # 3. + Eodhd#stream
12
- # 4. + Eodhd#us_quote_stream
13
- # 5. + Eodhd#us_trade_stream
14
- # 6. + Eodhd#forex_stream
15
- # 7. + Eodhd#crypto_stream
16
- # 8. Moved Eodhd::Error from Eodhd::Client to a separate file.
17
- # 0/1: Some fixes for the WebSocketClient interface.
18
- # 9. ~ Eodhd::WebSocketClient: + attr_accessor :consumer
19
- # 10. ~ Eodhd::WebSocketClient: /attr_accessor :symbols/attr_reader :symbols/
20
- # 11. + Eodhd::WebSocketClient#symbols=
21
- # 1/2: Remove reference to currently unused library UnixTime.
22
- # 12. ~ Eodhd::WebSocketClient: - require 'UnixTime'
23
-
24
- require_relative 'Eodhd/Client'
25
- require_relative 'Eodhd/EodBulkLastDay'
26
- require_relative 'Eodhd/EodData'
27
- require_relative 'Eodhd/Exchange'
28
- require_relative 'Eodhd/ExchangeSymbol'
29
- 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'
30
10
 
31
11
  class Eodhd
32
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.14.2
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-03-26 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,17 +130,26 @@ 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
39
139
  - lib/Eodhd/Exchange.rb
40
140
  - lib/Eodhd/ExchangeSymbol.rb
141
+ - lib/Eodhd/VERSION.rb
142
+ - lib/Eodhd/Validations.rb
41
143
  - lib/Eodhd/WebSocketClient.rb
42
144
  - lib/Hash/x_www_form_urlencode.rb
43
145
  - lib/Thoran/Hash/XWwwFormUrlencode/x_www_form_urlencode.rb
44
146
  - lib/Thoran/String/UrlEncode/url_encode.rb
45
- - 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
46
153
  homepage: http://github.com/thoran/eodhd.rb
47
154
  licenses:
48
155
  - Ruby
@@ -61,7 +168,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
61
168
  - !ruby/object:Gem::Version
62
169
  version: '0'
63
170
  requirements: []
64
- rubygems_version: 3.6.6
171
+ rubygems_version: 3.7.2
65
172
  specification_version: 4
66
173
  summary: Access the eodhd.com API with Ruby.
67
174
  test_files: []