twelvedata_ruby 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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