twelvedata_ruby 0.2.2 → 0.4.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 +4 -4
- data/.rspec +4 -0
- data/.rubocop.yml +102 -0
- data/.yardopts +14 -0
- data/CHANGELOG.md +199 -4
- data/{LICENSE.txt → LICENSE} +1 -1
- data/README.md +392 -88
- data/Rakefile +110 -4
- data/lib/twelvedata_ruby/client.rb +137 -88
- data/lib/twelvedata_ruby/endpoint.rb +292 -242
- data/lib/twelvedata_ruby/error.rb +93 -45
- data/lib/twelvedata_ruby/request.rb +106 -33
- data/lib/twelvedata_ruby/response.rb +270 -99
- data/lib/twelvedata_ruby/utils.rb +91 -14
- data/lib/twelvedata_ruby/version.rb +6 -0
- data/lib/twelvedata_ruby.rb +41 -30
- data/twelvedata_ruby.gemspec +28 -23
- metadata +25 -143
- data/.gitignore +0 -13
- data/Gemfile +0 -6
- data/Gemfile.lock +0 -80
- data/bin/console +0 -22
- data/bin/setup +0 -8
@@ -1,285 +1,335 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
# Handles endpoint definitions, validation, and parameter management
|
3
4
|
module TwelvedataRuby
|
4
5
|
class Endpoint
|
5
|
-
|
6
|
-
|
6
|
+
DEFAULT_FORMAT = :json
|
7
|
+
VALID_FORMATS = [DEFAULT_FORMAT, :csv].freeze
|
7
8
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
# Complete endpoint definitions with parameters and response structure
|
10
|
+
DEFINITIONS = {
|
11
|
+
api_usage: {
|
12
|
+
parameters: { keys: %i[format] },
|
13
|
+
response: { keys: %i[timestamp current_usage plan_limit] },
|
14
|
+
},
|
15
|
+
stocks: {
|
16
|
+
parameters: { keys: %i[symbol exchange country type format] },
|
17
|
+
response: { data_keys: %i[symbol name currency exchange country type], collection: :data },
|
18
|
+
},
|
19
|
+
forex_pairs: {
|
20
|
+
parameters: { keys: %i[symbol currency_base currency_quote format] },
|
21
|
+
response: { data_keys: %i[symbol currency_group currency_base currency_quote], collection: :data },
|
22
|
+
},
|
23
|
+
cryptocurrencies: {
|
24
|
+
parameters: { keys: %i[symbol exchange currency_base currency_quote format] },
|
25
|
+
response: { data_keys: %i[symbol available_exchanges currency_base currency_quote], collection: :data },
|
26
|
+
},
|
27
|
+
etf: {
|
28
|
+
parameters: { keys: %i[symbol format] },
|
29
|
+
response: { data_keys: %i[symbol name currency exchange], collection: :data },
|
30
|
+
},
|
31
|
+
indices: {
|
32
|
+
parameters: { keys: %i[symbol country format] },
|
33
|
+
response: { data_keys: %i[symbol name country currency], collection: :data },
|
34
|
+
},
|
35
|
+
exchanges: {
|
36
|
+
parameters: { keys: %i[type name code country format] },
|
37
|
+
response: { data_keys: %i[name country code timezone], collection: :data },
|
38
|
+
},
|
39
|
+
cryptocurrency_exchanges: {
|
40
|
+
parameters: { keys: %i[name format] },
|
41
|
+
response: { data_keys: %i[name], collection: :data },
|
42
|
+
},
|
43
|
+
technical_indicators: {
|
44
|
+
parameters: { keys: [] },
|
45
|
+
response: { keys: %i[enable full_name description type overlay parameters output_values tinting] },
|
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,
|
12
52
|
},
|
13
|
-
|
14
|
-
|
15
|
-
|
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[
|
61
|
+
symbol interval exchange country type outputsize format dp order timezone
|
62
|
+
start_date end_date previous_close
|
63
|
+
],
|
64
|
+
required: %i[symbol interval],
|
16
65
|
},
|
17
|
-
|
18
|
-
|
19
|
-
|
66
|
+
response: {
|
67
|
+
value_keys: %i[datetime open high low close volume],
|
68
|
+
collection: :values,
|
69
|
+
meta_keys: %i[symbol interval currency exchange_timezone exchange type],
|
20
70
|
},
|
21
|
-
|
22
|
-
|
23
|
-
|
71
|
+
},
|
72
|
+
quote: {
|
73
|
+
parameters: {
|
74
|
+
keys: %i[symbol interval exchange country volume_time_period type format],
|
75
|
+
required: %i[symbol],
|
24
76
|
},
|
25
|
-
|
26
|
-
|
27
|
-
|
77
|
+
response: {
|
78
|
+
keys: %i[
|
79
|
+
symbol name exchange currency datetime open high low close volume
|
80
|
+
previous_close change percent_change average_volume fifty_two_week
|
81
|
+
],
|
28
82
|
},
|
29
|
-
|
30
|
-
|
31
|
-
|
83
|
+
},
|
84
|
+
price: {
|
85
|
+
parameters: { keys: %i[symbol exchange country type format], required: %i[symbol] },
|
86
|
+
response: { keys: %i[price] },
|
87
|
+
},
|
88
|
+
eod: {
|
89
|
+
parameters: { keys: %i[symbol exchange country type prepost dp], required: %i[symbol] },
|
90
|
+
response: { keys: %i[symbol exchange currency datetime close] },
|
91
|
+
},
|
92
|
+
exchange_rate: {
|
93
|
+
parameters: { keys: %i[symbol format precision timezone], required: %i[symbol] },
|
94
|
+
response: { keys: %i[symbol rate timestamp] },
|
95
|
+
},
|
96
|
+
currency_conversion: {
|
97
|
+
parameters: { keys: %i[symbol amount format precision timezone], required: %i[symbol amount] },
|
98
|
+
response: { keys: %i[symbol rate amount timestamp] },
|
99
|
+
},
|
100
|
+
complex_data: {
|
101
|
+
parameters: {
|
102
|
+
keys: %i[symbols intervals start_date end_date dp order timezone methods name],
|
103
|
+
required: %i[symbols intervals start_date end_date],
|
32
104
|
},
|
33
|
-
|
34
|
-
|
35
|
-
|
105
|
+
response: { keys: %i[data status] },
|
106
|
+
http_verb: :post,
|
107
|
+
},
|
108
|
+
earnings: {
|
109
|
+
parameters: { keys: %i[symbol exchange country type period outputsize format], required: %i[symbol] },
|
110
|
+
response: { keys: %i[date time eps_estimate eps_actual difference surprise_prc] },
|
111
|
+
},
|
112
|
+
earnings_calendar: {
|
113
|
+
parameters: { keys: %i[format] },
|
114
|
+
response: {
|
115
|
+
keys: %i[
|
116
|
+
symbol name currency exchange country time eps_estimate eps_actual difference surprise_prc
|
117
|
+
],
|
36
118
|
},
|
37
|
-
|
38
|
-
|
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[
|
61
|
-
symbol
|
62
|
-
interval
|
63
|
-
exchange
|
64
|
-
country
|
65
|
-
type
|
66
|
-
outputsize
|
67
|
-
format
|
68
|
-
dp
|
69
|
-
order
|
70
|
-
timezone
|
71
|
-
start_date
|
72
|
-
end_date
|
73
|
-
previous_close
|
74
|
-
],
|
75
|
-
required: %i[symbol interval]
|
76
|
-
},
|
77
|
-
response: {
|
78
|
-
value_keys: %i[datetime open high low close volume],
|
79
|
-
collection: :values,
|
80
|
-
meta_keys: %i[symbol interval currency exchange_timezone exchange type]
|
81
|
-
}
|
82
|
-
},
|
83
|
-
quote: {
|
84
|
-
parameters: {
|
85
|
-
keys: %i[symbol interval exchange country volume_time_period type format],
|
86
|
-
required: %i[symbol],
|
87
|
-
},
|
88
|
-
response: {
|
89
|
-
keys: %i[
|
90
|
-
symbol
|
91
|
-
name
|
92
|
-
exchange
|
93
|
-
currency
|
94
|
-
datetime
|
95
|
-
open
|
96
|
-
high
|
97
|
-
low
|
98
|
-
close
|
99
|
-
volume
|
100
|
-
previous_close
|
101
|
-
change
|
102
|
-
percent_change
|
103
|
-
average_volume
|
104
|
-
fifty_two_week
|
105
|
-
]
|
106
|
-
}
|
107
|
-
},
|
108
|
-
price: {
|
109
|
-
parameters: {keys: %i[symbol exchange country type format], required: %i[symbol]},
|
110
|
-
response: {keys: %i[price]}
|
111
|
-
},
|
112
|
-
eod: {
|
113
|
-
parameters: {keys: %i[symbol exchange country type prepost dp], required: %i[symbol]},
|
114
|
-
response: {keys: %i[symbol exchange currency datetime close]}
|
115
|
-
},
|
116
|
-
exchange_rate: {
|
117
|
-
parameters: {keys: %i[symbol format precision timezone], required: %i[symbol]},
|
118
|
-
response: {keys: %i[symbol rate timestamp]}
|
119
|
-
},
|
120
|
-
currency_conversion: {
|
121
|
-
parameters: {keys: %i[symbol amount format precision timezone], required: %i[symbol amount]},
|
122
|
-
response: {keys: %i[symbol rate amount timestamp]}
|
123
|
-
},
|
124
|
-
complex_data: {
|
125
|
-
parameters: {
|
126
|
-
keys: %i[symbols intervals start_date end_date dp order timezone methods name],
|
127
|
-
required: %i[symbols intervals start_date end_date]
|
128
|
-
},
|
129
|
-
response: {keys: %i[data status]},
|
130
|
-
http_verb: :post
|
131
|
-
},
|
132
|
-
earnings: {
|
133
|
-
parameters: {keys: %i[symbol exchange country type period outputsize format], required: %i[symbol]},
|
134
|
-
response: {keys: %i[date time eps_estimate eps_actual difference surprise_prc]}
|
135
|
-
},
|
136
|
-
earnings_calendar: {
|
137
|
-
parameters: {keys: %i[format]},
|
138
|
-
response: {
|
139
|
-
keys: %i[
|
140
|
-
symbol
|
141
|
-
name
|
142
|
-
currency
|
143
|
-
exchange
|
144
|
-
country
|
145
|
-
time
|
146
|
-
eps_estimate
|
147
|
-
eps_estimate
|
148
|
-
eps_actual
|
149
|
-
difference
|
150
|
-
surprise_prc
|
151
|
-
]
|
152
|
-
}
|
153
|
-
}
|
154
|
-
}.freeze
|
155
|
-
|
156
|
-
class << self
|
157
|
-
def definitions
|
158
|
-
@definitions ||= DEFINITIONS.transform_values {|v|
|
159
|
-
v.merge(
|
160
|
-
parameters: {
|
161
|
-
keys: v[:parameters][:keys].push(:apikey),
|
162
|
-
required: (v[:parameters][:required] || []).push(:apikey)
|
163
|
-
}
|
164
|
-
)
|
165
|
-
}.to_h
|
166
|
-
end
|
167
|
-
|
168
|
-
def names
|
169
|
-
@names ||= definitions.keys
|
170
|
-
end
|
171
|
-
|
172
|
-
def default_apikey_params
|
173
|
-
{apikey: Client.instance.apikey}
|
174
|
-
end
|
175
|
-
|
176
|
-
def valid_name?(name)
|
177
|
-
names.include?(name.to_sym)
|
178
|
-
end
|
179
|
-
|
180
|
-
def valid_params?(name, **params)
|
181
|
-
new(name, **params).valid?
|
182
|
-
end
|
183
|
-
alias valid? valid_params?
|
184
|
-
end
|
119
|
+
},
|
120
|
+
}.freeze
|
185
121
|
|
186
|
-
|
122
|
+
class << self
|
123
|
+
# Get processed endpoint definitions with apikey parameter added
|
124
|
+
#
|
125
|
+
# @return [Hash] Complete endpoint definitions
|
126
|
+
def definitions
|
127
|
+
@definitions ||= build_definitions
|
128
|
+
end
|
187
129
|
|
188
|
-
|
189
|
-
|
190
|
-
|
130
|
+
# Get all valid endpoint names
|
131
|
+
#
|
132
|
+
# @return [Array<Symbol>] Array of endpoint names
|
133
|
+
def names
|
134
|
+
@names ||= definitions.keys
|
191
135
|
end
|
192
136
|
|
193
|
-
|
194
|
-
|
137
|
+
# Get default API key parameters
|
138
|
+
#
|
139
|
+
# @return [Hash] Default parameters including API key
|
140
|
+
def default_apikey_params
|
141
|
+
{ apikey: Client.instance.apikey }
|
195
142
|
end
|
196
143
|
|
197
|
-
|
198
|
-
|
144
|
+
# Validate endpoint name
|
145
|
+
#
|
146
|
+
# @param name [Symbol, String] Endpoint name to validate
|
147
|
+
# @return [Boolean] True if valid endpoint name
|
148
|
+
def valid_name?(name)
|
149
|
+
names.include?(name&.to_sym)
|
199
150
|
end
|
200
151
|
|
201
|
-
|
202
|
-
|
152
|
+
# Validate endpoint parameters
|
153
|
+
#
|
154
|
+
# @param name [Symbol, String] Endpoint name
|
155
|
+
# @param params [Hash] Parameters to validate
|
156
|
+
# @return [Boolean] True if parameters are valid
|
157
|
+
def valid_params?(name, **params)
|
158
|
+
new(name, **params).valid?
|
203
159
|
end
|
204
160
|
|
205
|
-
|
206
|
-
return @parameters if definition.nil? || @parameters
|
161
|
+
private
|
207
162
|
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
163
|
+
def build_definitions
|
164
|
+
DEFINITIONS.transform_values do |definition|
|
165
|
+
enhanced_params = definition[:parameters].dup
|
166
|
+
enhanced_params[:keys] = enhanced_params[:keys] + [:apikey]
|
167
|
+
enhanced_params[:required] = (enhanced_params[:required] || []) + [:apikey]
|
212
168
|
|
213
|
-
|
214
|
-
|
215
|
-
keys.push(:filename) if keys && query_params && query_params[:format] == :csv
|
216
|
-
keys
|
169
|
+
definition.merge(parameters: enhanced_params)
|
170
|
+
end.freeze
|
217
171
|
end
|
172
|
+
end
|
218
173
|
|
219
|
-
|
220
|
-
query_params.keys
|
221
|
-
end
|
174
|
+
attr_reader :name, :query_params
|
222
175
|
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
176
|
+
# Initialize endpoint with name and parameters
|
177
|
+
#
|
178
|
+
# @param name [Symbol, String] Endpoint name
|
179
|
+
# @param query_params [Hash] Query parameters
|
180
|
+
def initialize(name, **query_params)
|
181
|
+
@errors = {}
|
182
|
+
self.name = name
|
183
|
+
self.query_params = query_params
|
184
|
+
end
|
231
185
|
|
232
|
-
|
233
|
-
|
234
|
-
|
186
|
+
# Get endpoint definition
|
187
|
+
#
|
188
|
+
# @return [Hash, nil] Endpoint definition hash
|
189
|
+
def definition
|
190
|
+
@definition ||= self.class.definitions[name]
|
191
|
+
end
|
235
192
|
|
236
|
-
|
237
|
-
|
238
|
-
|
193
|
+
# Get validation errors
|
194
|
+
#
|
195
|
+
# @return [Hash] Hash of validation errors
|
196
|
+
def errors
|
197
|
+
@errors.compact
|
198
|
+
end
|
239
199
|
|
240
|
-
|
241
|
-
|
242
|
-
|
200
|
+
# Set endpoint name with validation
|
201
|
+
#
|
202
|
+
# @param name [Symbol, String] Endpoint name
|
203
|
+
def name=(name)
|
204
|
+
reset_cached_data
|
205
|
+
@name = name.to_s.downcase.to_sym
|
206
|
+
validate_name
|
207
|
+
end
|
243
208
|
|
244
|
-
|
245
|
-
|
246
|
-
|
209
|
+
# Get parameter definition
|
210
|
+
#
|
211
|
+
# @return [Hash, nil] Parameter definition
|
212
|
+
def parameters
|
213
|
+
definition&.dig(:parameters)
|
214
|
+
end
|
247
215
|
|
248
|
-
|
249
|
-
|
250
|
-
|
216
|
+
# Get parameter keys including format-specific ones
|
217
|
+
#
|
218
|
+
# @return [Array<Symbol>, nil] Array of valid parameter keys
|
219
|
+
def parameters_keys
|
220
|
+
return nil unless parameters
|
251
221
|
|
252
|
-
|
222
|
+
keys = parameters[:keys].dup
|
223
|
+
keys << :filename if csv_format_with_filename?
|
224
|
+
keys
|
225
|
+
end
|
253
226
|
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
end
|
227
|
+
# Get query parameter keys
|
228
|
+
#
|
229
|
+
# @return [Array<Symbol>] Array of current query parameter keys
|
230
|
+
def query_params_keys
|
231
|
+
query_params.keys
|
232
|
+
end
|
261
233
|
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
234
|
+
# Set query parameters with validation and processing
|
235
|
+
#
|
236
|
+
# @param query_params [Hash] Query parameters to set
|
237
|
+
def query_params=(query_params)
|
238
|
+
reset_cached_data
|
239
|
+
processed_params = process_query_params(query_params)
|
240
|
+
@query_params = self.class.default_apikey_params.merge(processed_params.compact)
|
241
|
+
validate_query_params
|
242
|
+
end
|
266
243
|
|
267
|
-
|
268
|
-
|
244
|
+
# Get required parameter keys
|
245
|
+
#
|
246
|
+
# @return [Array<Symbol>, nil] Array of required parameter keys
|
247
|
+
def required_parameters
|
248
|
+
parameters&.dig(:required)
|
249
|
+
end
|
250
|
+
|
251
|
+
# Check if endpoint and parameters are valid
|
252
|
+
#
|
253
|
+
# @return [Boolean] True if valid
|
254
|
+
def valid?
|
255
|
+
valid_name? && valid_query_params?
|
256
|
+
end
|
257
|
+
|
258
|
+
# Check if name is valid
|
259
|
+
#
|
260
|
+
# @return [Boolean] True if name is valid
|
261
|
+
def valid_name?
|
262
|
+
errors[:name].nil?
|
263
|
+
end
|
264
|
+
|
265
|
+
# Check if query parameters are valid
|
266
|
+
#
|
267
|
+
# @return [Boolean] True if query parameters are valid
|
268
|
+
def valid_query_params?
|
269
|
+
validate_query_params
|
270
|
+
errors[:parameters_keys].nil? && errors[:required_parameters].nil?
|
271
|
+
end
|
272
|
+
|
273
|
+
private
|
274
|
+
|
275
|
+
def reset_cached_data
|
276
|
+
@definition = nil
|
277
|
+
end
|
278
|
+
|
279
|
+
def csv_format_with_filename?
|
280
|
+
parameters&.dig(:keys)&.include?(:format) && query_params&.dig(:format) == :csv
|
281
|
+
end
|
282
|
+
|
283
|
+
def process_query_params(params)
|
284
|
+
processed = params.dup
|
285
|
+
|
286
|
+
# Normalize format parameter
|
287
|
+
if parameters_keys&.include?(:format)
|
288
|
+
processed[:format] = normalize_format(processed[:format])
|
269
289
|
end
|
270
290
|
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
update_errors(:name, is_valid ? nil : invalid_name)
|
275
|
-
validate_query_params if is_valid && query_params && !valid_query_params?
|
291
|
+
# Remove filename if not CSV format
|
292
|
+
if processed[:filename] && processed[:format] != :csv
|
293
|
+
processed.delete(:filename)
|
276
294
|
end
|
277
295
|
|
278
|
-
|
279
|
-
|
296
|
+
processed
|
297
|
+
end
|
280
298
|
|
281
|
-
|
282
|
-
|
283
|
-
|
299
|
+
def normalize_format(format)
|
300
|
+
VALID_FORMATS.include?(format) ? format : DEFAULT_FORMAT
|
301
|
+
end
|
302
|
+
|
303
|
+
def validate_name
|
304
|
+
(@errors.delete(:name) || true) and return if self.class.valid_name?(name)
|
305
|
+
|
306
|
+
invalid_name = name.nil? || name.to_s.empty? ? "blank name" : name
|
307
|
+
@errors[:name] = create_error(:name, invalid_name, EndpointNameError)
|
308
|
+
end
|
309
|
+
|
310
|
+
def validate_query_params
|
311
|
+
return unless valid_name? && parameters_keys
|
312
|
+
|
313
|
+
validate_required_parameters
|
314
|
+
validate_parameter_keys
|
315
|
+
end
|
316
|
+
|
317
|
+
def validate_required_parameters
|
318
|
+
missing = required_parameters - query_params_keys
|
319
|
+
(@errors.delete(:required_parameters) || true) and return if missing.empty?
|
320
|
+
|
321
|
+
@errors[:required_parameters] = create_error(:required_parameters, missing, EndpointRequiredParametersError)
|
322
|
+
end
|
323
|
+
|
324
|
+
def validate_parameter_keys
|
325
|
+
invalid = query_params_keys - parameters_keys
|
326
|
+
(@errors.delete(:parameters_keys) || true) and return if invalid.empty?
|
327
|
+
|
328
|
+
@errors[:parameters_keys] = create_error(:parameters_keys, invalid, EndpointParametersKeysError)
|
329
|
+
end
|
330
|
+
|
331
|
+
def create_error(attr_name, invalid_values, error_class)
|
332
|
+
error_class.new(endpoint: self, invalid: invalid_values)
|
333
|
+
end
|
284
334
|
end
|
285
335
|
end
|