twelvedata_ruby 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +13 -0
  3. data/CHANGELOG.md +5 -0
  4. data/CODE_OF_CONDUCT.md +84 -0
  5. data/Gemfile +6 -0
  6. data/Gemfile.lock +80 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +125 -0
  9. data/Rakefile +12 -0
  10. data/bin/console +22 -0
  11. data/bin/setup +8 -0
  12. data/doc/TwelvedataRuby.html +353 -0
  13. data/doc/TwelvedataRuby/BadRequestResponseError.html +178 -0
  14. data/doc/TwelvedataRuby/Client.html +1443 -0
  15. data/doc/TwelvedataRuby/Endpoint.html +1478 -0
  16. data/doc/TwelvedataRuby/EndpointError.html +247 -0
  17. data/doc/TwelvedataRuby/EndpointNameError.html +167 -0
  18. data/doc/TwelvedataRuby/EndpointParametersKeysError.html +167 -0
  19. data/doc/TwelvedataRuby/EndpointRequiredParametersError.html +167 -0
  20. data/doc/TwelvedataRuby/Error.html +318 -0
  21. data/doc/TwelvedataRuby/ForbiddenResponseError.html +178 -0
  22. data/doc/TwelvedataRuby/InternalServerResponseErro.html +178 -0
  23. data/doc/TwelvedataRuby/NotFoundResponseError.html +178 -0
  24. data/doc/TwelvedataRuby/PageNotFoundResponseError.html +178 -0
  25. data/doc/TwelvedataRuby/ParameterTooLongResponseError.html +178 -0
  26. data/doc/TwelvedataRuby/Request.html +683 -0
  27. data/doc/TwelvedataRuby/Response.html +1622 -0
  28. data/doc/TwelvedataRuby/ResponseError.html +565 -0
  29. data/doc/TwelvedataRuby/TooManyRequestsResponseError.html +178 -0
  30. data/doc/TwelvedataRuby/UnauthorizedResponseError.html +178 -0
  31. data/doc/TwelvedataRuby/Utils.html +503 -0
  32. data/doc/_index.html +315 -0
  33. data/doc/class_list.html +51 -0
  34. data/doc/css/common.css +1 -0
  35. data/doc/css/full_list.css +58 -0
  36. data/doc/css/style.css +497 -0
  37. data/doc/file.README.html +194 -0
  38. data/doc/file_list.html +56 -0
  39. data/doc/frames.html +17 -0
  40. data/doc/index.html +194 -0
  41. data/doc/js/app.js +314 -0
  42. data/doc/js/full_list.js +216 -0
  43. data/doc/js/jquery.js +4 -0
  44. data/doc/method_list.html +707 -0
  45. data/doc/top-level-namespace.html +110 -0
  46. data/lib/twelvedata_ruby.rb +43 -0
  47. data/lib/twelvedata_ruby/client.rb +148 -0
  48. data/lib/twelvedata_ruby/endpoint.rb +271 -0
  49. data/lib/twelvedata_ruby/error.rb +90 -0
  50. data/lib/twelvedata_ruby/request.rb +54 -0
  51. data/lib/twelvedata_ruby/response.rb +132 -0
  52. data/lib/twelvedata_ruby/utils.rb +36 -0
  53. data/twelvedata_ruby.gemspec +37 -0
  54. metadata +201 -0
@@ -0,0 +1,110 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>
7
+ Top Level Namespace
8
+
9
+ &mdash; Documentation by YARD 0.9.26
10
+
11
+ </title>
12
+
13
+ <link rel="stylesheet" href="css/style.css" type="text/css" />
14
+
15
+ <link rel="stylesheet" href="css/common.css" type="text/css" />
16
+
17
+ <script type="text/javascript">
18
+ pathId = "";
19
+ relpath = '';
20
+ </script>
21
+
22
+
23
+ <script type="text/javascript" charset="utf-8" src="js/jquery.js"></script>
24
+
25
+ <script type="text/javascript" charset="utf-8" src="js/app.js"></script>
26
+
27
+
28
+ </head>
29
+ <body>
30
+ <div class="nav_wrap">
31
+ <iframe id="nav" src="class_list.html?1"></iframe>
32
+ <div id="resizer"></div>
33
+ </div>
34
+
35
+ <div id="main" tabindex="-1">
36
+ <div id="header">
37
+ <div id="menu">
38
+
39
+ <a href="_index.html">Index</a> &raquo;
40
+
41
+
42
+ <span class="title">Top Level Namespace</span>
43
+
44
+ </div>
45
+
46
+ <div id="search">
47
+
48
+ <a class="full_list_link" id="class_list_link"
49
+ href="class_list.html">
50
+
51
+ <svg width="24" height="24">
52
+ <rect x="0" y="4" width="24" height="4" rx="1" ry="1"></rect>
53
+ <rect x="0" y="12" width="24" height="4" rx="1" ry="1"></rect>
54
+ <rect x="0" y="20" width="24" height="4" rx="1" ry="1"></rect>
55
+ </svg>
56
+ </a>
57
+
58
+ </div>
59
+ <div class="clear"></div>
60
+ </div>
61
+
62
+ <div id="content"><h1>Top Level Namespace
63
+
64
+
65
+
66
+ </h1>
67
+ <div class="box_info">
68
+
69
+
70
+
71
+
72
+
73
+
74
+
75
+
76
+
77
+
78
+
79
+ </div>
80
+
81
+ <h2>Defined Under Namespace</h2>
82
+ <p class="children">
83
+
84
+
85
+ <strong class="modules">Modules:</strong> <span class='object_link'><a href="TwelvedataRuby.html" title="TwelvedataRuby (module)">TwelvedataRuby</a></span>
86
+
87
+
88
+
89
+
90
+ </p>
91
+
92
+
93
+
94
+
95
+
96
+
97
+
98
+
99
+
100
+ </div>
101
+
102
+ <div id="footer">
103
+ Generated on Tue Jul 13 08:56:46 2021 by
104
+ <a href="http://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">yard</a>
105
+ 0.9.26 (ruby-3.0.1).
106
+ </div>
107
+
108
+ </div>
109
+ </body>
110
+ </html>
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "twelvedata_ruby/utils"
4
+ require_relative "twelvedata_ruby/error"
5
+ require_relative "twelvedata_ruby/endpoint"
6
+ require_relative "twelvedata_ruby/request"
7
+ require_relative "twelvedata_ruby/response"
8
+ require_relative "twelvedata_ruby/client"
9
+
10
+ # The one module that all the classes and modules of this gem are namespaced
11
+
12
+ module TwelvedataRuby
13
+ # Holds the current version
14
+ # @return [String] version number
15
+ VERSION = "0.1.1"
16
+
17
+ # A convenient and clearer way of getting and overriding default attribute values of the singleton `Client.instance`
18
+ #
19
+ # @param [Hash] options the optional Hash object that may contain values to override the defaults
20
+ # @option options [Symbol, String] :apikey the private key from Twelvedata API key
21
+ # @option options [Integer, String] :connect_timeout milliseconds
22
+ #
23
+ # @example Passing a nil options
24
+ # TwelvedataRuby.client
25
+ #
26
+ # The singleton instance object returned will use the default values for its attributes
27
+ #
28
+ # @example Passing values of `:apikey` and `:connect_timeout`
29
+ # TwelvedataRuby.client(apikey: "my-twelvedata-apikey", connect_timeout: 3000)
30
+ #
31
+ # @example or, chain with other Client instance method
32
+ # TwelvedataRuby.client(apikey: "my-twelvedata-apikey", connect_timeout: 3000).quote(symbol: "IBM")
33
+ #
34
+ # In the last example, calling `#quote`, a valid API endpoint, an instance method with the same name
35
+ # was dynamically defined and then fired up an API request to Twelvedata.
36
+ #
37
+ # @return [Client] singleton instance
38
+ def self.client(**options)
39
+ client = Client.instance
40
+ client.options = (client.options || {}).merge(options)
41
+ client
42
+ end
43
+ end
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "httpx"
4
+ require "singleton"
5
+
6
+ module TwelvedataRuby
7
+ # Responsible of the actual communication -- sending a valid request
8
+ # and receiving the response -- of the API web server
9
+ class Client
10
+ include Singleton
11
+ # @return [String] the exported shell ENV variable name that holds the apikey
12
+ APIKEY_ENV_NAME = "TWELVEDATA_API_KEY"
13
+ # @return [Integer] CONNECT_TIMEOUT default connection timeout in milliseconds
14
+ CONNECT_TIMEOUT = 120
15
+ # @return [String] valid URI base url string of the API
16
+ BASE_URL = "https://api.twelvedata.com"
17
+
18
+ class << self
19
+ def request(request_objects, opts={})
20
+ HTTPX.with(options.merge(opts)).request(build_requests(request_objects))
21
+ end
22
+
23
+ def build_requests(requests)
24
+ Utils.to_a(requests).map(&:build)
25
+ end
26
+
27
+ def origin
28
+ @origin ||= {origin: BASE_URL}
29
+ end
30
+
31
+ def timeout
32
+ {timeout: {connect_timeout: instance.connect_timeout}}
33
+ end
34
+
35
+ def options
36
+ origin.merge(timeout)
37
+ end
38
+ end
39
+
40
+ # @!attribute options
41
+ # @return [Hash] the options writeonly attribute that may contain values to override the default attribute values.
42
+ # This attribute writer was automatically called in @see TwelvedataRuby.client(**options).
43
+ # @see TwelvedataRuby.client
44
+ attr_writer :options
45
+
46
+ # @return [String] apikey value from the instance options Hash object
47
+ # but if nill use the value from +ENV[APIKEY_ENV_NAME]+
48
+ def apikey
49
+ Utils.empty_to_nil(options[:apikey]) || ENV[apikey_env_var_name]
50
+ end
51
+
52
+ # The writer method that can be used to pass manually the value of the +apikey+
53
+ # @param [String] apikey
54
+ # @return [String] +apikey+ value
55
+ def apikey=(apikey)
56
+ options[:apikey] = apikey
57
+ end
58
+
59
+ def connect_timeout
60
+ parse_connect_timeout(options[:connect_timeout])
61
+ end
62
+
63
+ def connect_timeout=(connect_timeout)
64
+ parse_connect_timeout(connect_timeout)
65
+ end
66
+
67
+ # The name of the ENVIRONMENT variable that may hold the value of the Twelve Data API key
68
+ # # @return [String] the ENV variable that will be used to fetch from ENV the value of the API key
69
+ def apikey_env_var_name
70
+ (options[:apikey_env_var_name] || APIKEY_ENV_NAME).upcase
71
+ end
72
+
73
+ # A setter helper method to configure the ENV variable name of the API key
74
+ # @param [String] apikey_env_var_name
75
+ # @return [String] the ENV variable name
76
+ # @see #apikey_env_var_name
77
+ def apikey_env_var_name=(apikey_env_var_name)
78
+ options[:apikey_env_var_name] = apikey_env_var_name
79
+ end
80
+
81
+ # The actual API fetch that transport the built request object.
82
+ # +Request#valid?+ guards the actual fetch and instead will return a Hash instance of endpoint errors.
83
+ # If +Request#valid?+ returns true, request object will be sent to the API and returned response will
84
+ # will be resolved which may or may not contain a kind of +ResponseError+ instance.
85
+ # @see Response.resolve for more details
86
+
87
+ # @param [Request] request built API request object that holds the endpoint payload
88
+ #
89
+ # @return [NilClass] +nil+ if @param +request+ is not truthy
90
+ # @return [Hash] :errors if the request is not valid will hold the endpoint errors details
91
+ # @see Endpoint#errors
92
+ # @return [Response] if +request+ is valid and received an actual response from the API server.
93
+ # The response object's #error may or may not return a kind of ResponseError
94
+ # @see Response#error
95
+ # @return [ResponseError] if the response received did not come from the API server itself.
96
+ #
97
+ def fetch(request)
98
+ return nil unless request
99
+
100
+ request.valid? ? Response.resolve(self.class.request(request), request) : {errors: request.errors}
101
+ end
102
+
103
+ # The entry point in dynamically defining instance methods based on the called the valid endpoint names.
104
+ # @param [String] endpoint_name valid API endpoint name to fetch
105
+ # @param [Hash] endpoint_params the optional/required valid query params of the API endpoint.
106
+ # If +:apikey+ key-value pair is present, the pair will override the +#apikey+ of singleton client instance
107
+ # If +:format+ key-value pair is present and is a valid parameter key and value can only be +:csv+ or +:json+
108
+ # If +:filename+ key-value is present and +:format+ is +:csv+, then this is will be added to the payload too.
109
+ # Otherwise, this will just discarded and will not be part of the payload
110
+ # If endpoint name and query params used are not valid, EndpointError instances will be returned
111
+ # actual API fetch will not happen. @see #fetch for the rest of the documentation
112
+ #
113
+ # @todo define all the method signatures of the endpoint methods that will meta-programatically defined at runtime.
114
+ def method_missing(endpoint_name, **endpoint_params, &_block)
115
+ try_fetch(endpoint_name, endpoint_params) || super
116
+ end
117
+
118
+ def options
119
+ @options || @options = {}
120
+ end
121
+
122
+ def respond_to_missing?(endpoint_name, _include_all=false)
123
+ Utils.return_nil_unless_true(Endpoint.valid_name?(endpoint_name)) {
124
+ define_endpoint_method(endpoint_name)
125
+ } || super
126
+ end
127
+
128
+ private
129
+
130
+ def build_request(endpoint_name, endpoint_params)
131
+ Request.new(endpoint_name, **endpoint_params)
132
+ end
133
+
134
+ def try_fetch(endpoint_name, endpoint_params)
135
+ respond_to?(endpoint_name) ? fetch(build_request(endpoint_name, endpoint_params)) : nil
136
+ end
137
+
138
+ def define_endpoint_method(endpoint_name)
139
+ self.class.define_method(endpoint_name) do |**qparams|
140
+ fetch(build_request(__method__, qparams))
141
+ end
142
+ end
143
+
144
+ def parse_connect_timeout(milliseconds)
145
+ options[:connect_timeout] = Utils.to_d(milliseconds, CONNECT_TIMEOUT)
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,271 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TwelvedataRuby
4
+ class Endpoint
5
+ DEFAULT_FORMAT = :json
6
+ VALID_FORMATS = [DEFAULT_FORMAT, :csv].freeze
7
+
8
+ DEFINITIONS = {
9
+ api_usage: {
10
+ parameters: {keys: %i[format]},
11
+ response: {keys: %i[timestamp current_usage plan_limit]}
12
+ },
13
+ stocks: {
14
+ parameters: {keys: %i[symbol exchange country type format]},
15
+ response: {data_keys: %i[symbol name currency exchange country type], collection: :data}
16
+ },
17
+ forex_pairs: {
18
+ parameters: {keys: %i[symbol currency_base currency_quote format]},
19
+ response: {data_keys: %i[symbol currency_group currency_base currency_quote], collection: :data}
20
+ },
21
+ cryptocurrencies: {
22
+ parameters: {keys: %i[symbol exchange currency_base currency_quote format]},
23
+ response: {data_keys: %i[symbol available_exchanges currency_base currency_quote], collection: :data}
24
+ },
25
+ etf: {
26
+ parameters: {keys: %i[symbol format]},
27
+ response: {data_keys: %i[symbol name currency exchange], collection: :data}
28
+ },
29
+ indices: {
30
+ parameters: {keys: %i[symbol country format]},
31
+ response: {data_keys: %i[symbol name country currency], collection: :data}
32
+ },
33
+ exchanges: {
34
+ parameters: {keys: %i[type name code country format]},
35
+ response: {data_keys: %i[name country code timezone], collection: :data}
36
+ },
37
+ cryptocurrency_exchanges: {
38
+ parameters: {keys: %i[name format]},
39
+ response: {data_keys: %i[name], collection: :data}
40
+ },
41
+ technical_indicators: {
42
+ parameters: {keys: []},
43
+ response: {
44
+ keys: %i[enable full_name description type overlay parameters output_values tinting]
45
+ }
46
+ },
47
+ symbol_search: {
48
+ parameters: {keys: %i[symbol outputsize], required: %i[symbol]},
49
+ response: {
50
+ data_keys: %i[symbol instrument_name exchange exchange_timezone instrument_type country],
51
+ collection: :data
52
+ }
53
+ },
54
+ earliest_timestamp: {
55
+ parameters: {keys: %i[symbol interval exchange]},
56
+ response: {keys: %i[datetime unix_time]}
57
+ },
58
+ time_series: {
59
+ parameters: {
60
+ keys: %i[symbol interval exchange country type outputsize format],
61
+ required: %i[symbol interval]
62
+ },
63
+ response: {
64
+ value_keys: %i[datetime open high low close volume],
65
+ collection: :values,
66
+ meta_keys: %i[symbol interval currency exchange_timezone exchange type]
67
+ }
68
+ },
69
+ quote: {
70
+ parameters: {
71
+ keys: %i[symbol interval exchange country volume_time_period type format],
72
+ required: %i[symbol],
73
+ },
74
+ response: {
75
+ keys: %i[
76
+ symbol
77
+ name
78
+ exchange
79
+ currency
80
+ datetime
81
+ open
82
+ high
83
+ low
84
+ close
85
+ volume
86
+ previous_close
87
+ change
88
+ percent_change
89
+ average_volume
90
+ fifty_two_week
91
+ ]
92
+ }
93
+ },
94
+ price: {
95
+ parameters: {keys: %i[symbol exchange country type format], required: %i[symbol]},
96
+ response: {keys: %i[price]}
97
+ },
98
+ eod: {
99
+ parameters: {keys: %i[symbol exchange country type], required: %i[symbol]},
100
+ response: {keys: %i[symbol exchange currency datetime close]}
101
+ },
102
+ exchange_rate: {
103
+ parameters: {keys: %i[symbol format], required: %i[symbol]},
104
+ response: {keys: %i[symbol rate timestamp]}
105
+ },
106
+ currency_conversion: {
107
+ parameters: {keys: %i[symbol amount format], required: %i[symbol amount]},
108
+ response: {keys: %i[symbol rate amount timestamp]}
109
+ },
110
+ complex_data: {
111
+ parameters: {
112
+ keys: %i[symbols intervals start_date end_date dp order timezone methods name],
113
+ required: %i[symbols intervals start_date end_date]
114
+ },
115
+ response: {keys: %i[data status]},
116
+ http_verb: :post
117
+ },
118
+ earnings: {
119
+ parameters: {keys: %i[symbol exchange country type period outputsize format], required: %i[symbol]},
120
+ response: {keys: %i[date time eps_estimate eps_actual difference surprise_prc]}
121
+ },
122
+ earnings_calendar: {
123
+ parameters: {keys: %i[format]},
124
+ response: {
125
+ keys: %i[
126
+ symbol
127
+ name
128
+ currency
129
+ exchange
130
+ country
131
+ time
132
+ eps_estimate
133
+ eps_estimate
134
+ eps_actual
135
+ difference
136
+ surprise_prc
137
+ ]
138
+ }
139
+ }
140
+ }.freeze
141
+
142
+ class << self
143
+ def definitions
144
+ @definitions ||= DEFINITIONS.transform_values {|v|
145
+ v.merge(
146
+ parameters: {
147
+ keys: v[:parameters][:keys].push(:apikey),
148
+ required: (v[:parameters][:required] || []).push(:apikey)
149
+ }
150
+ )
151
+ }.to_h
152
+ end
153
+
154
+ def names
155
+ @names ||= definitions.keys
156
+ end
157
+
158
+ def default_apikey_params
159
+ {apikey: Client.instance.apikey}
160
+ end
161
+
162
+ def valid_name?(name)
163
+ names.include?(name.to_sym)
164
+ end
165
+
166
+ def valid_params?(name, **params)
167
+ new(name, **params).valid?
168
+ end
169
+ alias valid? valid_params?
170
+ end
171
+
172
+ attr_reader :name, :query_params
173
+
174
+ def initialize(name, **query_params)
175
+ self.name = name
176
+ self.query_params = query_params
177
+ end
178
+
179
+ def definition
180
+ @definition ||= self.class.definitions[name]
181
+ end
182
+
183
+ def errors
184
+ (@errors || {}).compact
185
+ end
186
+
187
+ def name=(name)
188
+ assign_attribute(:name, name.to_s.downcase.to_sym)
189
+ end
190
+
191
+ def parameters
192
+ return @parameters if definition.nil? || @parameters
193
+
194
+ params = definition[:parameters]
195
+ params.push(:filename) if params.include?(:format) && query_parameters[:format] == :csv
196
+ params
197
+ end
198
+
199
+ def parameters_keys
200
+ keys = parameters&.send(:[], :keys)
201
+ keys.push(:filename) if keys && query_params && query_params[:format] == :csv
202
+ keys
203
+ end
204
+
205
+ def query_params_keys
206
+ query_params.keys
207
+ end
208
+
209
+ def query_params=(query_params)
210
+ if (parameters_keys || []).include?(:format) &&
211
+ !VALID_FORMATS.include?(query_params[:format])
212
+ query_params[:format] = DEFAULT_FORMAT
213
+ end
214
+ query_params.delete(:filename) if query_params[:filename] && query_params[:format] != :csv
215
+ assign_attribute(:query_params, self.class.default_apikey_params.merge(query_params.compact))
216
+ end
217
+
218
+ def required_parameters
219
+ parameters&.send(:[], :required)
220
+ end
221
+
222
+ def valid?
223
+ valid_name? && valid_query_params?
224
+ end
225
+
226
+ def valid_at_attributes?(*attrs)
227
+ errors.values_at(*attrs).compact.empty?
228
+ end
229
+
230
+ def valid_name?
231
+ valid_at_attributes?(:name)
232
+ end
233
+
234
+ def valid_query_params?
235
+ valid_at_attributes?(:parameters_keys, :required_parameters)
236
+ end
237
+
238
+ private
239
+
240
+ def assign_attribute(attr_name, value)
241
+ @parameters = nil
242
+ @definition = nil
243
+ instance_variable_set(:"@#{attr_name}", value)
244
+ send(:"validate_#{attr_name}")
245
+ send(attr_name)
246
+ end
247
+
248
+ def init_error(attr_name, invalid_values, error_klass=nil)
249
+ error_klass ||= Kernel.const_get("#{self.class.name}#{Utils.camelize(attr_name)}Error")
250
+ error_klass.new(endpoint: self, invalid: invalid_values)
251
+ end
252
+
253
+ def update_errors(attrib, invalids, klass=nil)
254
+ @errors = errors.merge(attrib => !invalids.nil? && !invalids.empty? ? init_error(attrib, invalids, klass) : nil)
255
+ end
256
+
257
+ def validate_name
258
+ is_valid = self.class.valid_name?(name)
259
+ invalid_name = name.nil? || name.empty? ? "a blank name" : name
260
+ update_errors(:name, is_valid ? nil : invalid_name)
261
+ validate_query_params if is_valid && query_params && !valid_query_params?
262
+ end
263
+
264
+ def validate_query_params
265
+ return update_errors(:required_parameters, "Invalid name", EndpointError) unless parameters_keys
266
+
267
+ update_errors(:required_parameters, required_parameters.difference(query_params_keys))
268
+ update_errors(:parameters_keys, query_params_keys.difference(parameters_keys))
269
+ end
270
+ end
271
+ end