twelvedata_ruby 0.3.0 → 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 +268 -110
- 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
@@ -3,146 +3,195 @@
|
|
3
3
|
require "httpx"
|
4
4
|
require "singleton"
|
5
5
|
|
6
|
+
# HTTP client for making requests to the Twelve Data API
|
6
7
|
module TwelvedataRuby
|
7
|
-
# Responsible of the actual communication -- sending a valid request
|
8
|
-
# and receiving the response -- of the API web server
|
9
8
|
class Client
|
10
9
|
include Singleton
|
11
|
-
|
10
|
+
|
11
|
+
# Default environment variable name for API key
|
12
12
|
APIKEY_ENV_NAME = "TWELVEDATA_API_KEY"
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
|
14
|
+
# Default connection timeout in milliseconds
|
15
|
+
DEFAULT_CONNECT_TIMEOUT = 120
|
16
|
+
|
17
|
+
# Base URL for the Twelve Data API
|
16
18
|
BASE_URL = "https://api.twelvedata.com"
|
17
19
|
|
18
20
|
class << self
|
19
|
-
|
20
|
-
|
21
|
+
# Make HTTP requests using HTTPX
|
22
|
+
#
|
23
|
+
# @param request_objects [Request, Array<Request>] Request object(s) to send
|
24
|
+
# @param options [Hash] Additional HTTPX options
|
25
|
+
# @return [HTTPX::Response, Array<HTTPX::Response>] HTTP response(s)
|
26
|
+
def request(request_objects, **options)
|
27
|
+
requests = build_requests(request_objects)
|
28
|
+
http_client = HTTPX.with(http_options.merge(options))
|
29
|
+
|
30
|
+
http_client.request(requests)
|
21
31
|
end
|
22
32
|
|
33
|
+
# Build HTTP requests from Request objects
|
34
|
+
#
|
35
|
+
# @param requests [Request, Array<Request>] Request object(s)
|
36
|
+
# @return [Array] Array of HTTP request specs
|
23
37
|
def build_requests(requests)
|
24
|
-
Utils.
|
38
|
+
Utils.to_array(requests).map(&:build)
|
25
39
|
end
|
26
40
|
|
27
|
-
|
28
|
-
|
41
|
+
# Get HTTP client options
|
42
|
+
#
|
43
|
+
# @return [Hash] HTTPX options
|
44
|
+
def http_options
|
45
|
+
{
|
46
|
+
origin: BASE_URL,
|
47
|
+
timeout: { connect_timeout: instance.connect_timeout },
|
48
|
+
}
|
49
|
+
end
|
29
50
|
end
|
30
51
|
|
31
|
-
|
32
|
-
{timeout: {connect_timeout: instance.connect_timeout}}
|
33
|
-
end
|
52
|
+
attr_reader :configuration
|
34
53
|
|
35
|
-
|
36
|
-
|
37
|
-
|
54
|
+
def initialize
|
55
|
+
@configuration = {}
|
56
|
+
@endpoint_methods_defined = Set.new
|
57
|
+
reset_configuration
|
38
58
|
end
|
39
59
|
|
40
|
-
#
|
41
|
-
#
|
42
|
-
#
|
43
|
-
# @
|
44
|
-
|
60
|
+
# Configure the client with new options
|
61
|
+
#
|
62
|
+
# @param options [Hash] Configuration options
|
63
|
+
# @option options [String] :apikey API key for authentication
|
64
|
+
# @option options [Integer] :connect_timeout Connection timeout in milliseconds
|
65
|
+
# @option options [String] :apikey_env_var_name Environment variable name for API key
|
66
|
+
# @return [self] Returns self for method chaining
|
67
|
+
def configure(**options)
|
68
|
+
@configuration.merge!(options)
|
69
|
+
self
|
70
|
+
end
|
45
71
|
|
46
|
-
#
|
47
|
-
#
|
72
|
+
# Get the current API key
|
73
|
+
#
|
74
|
+
# @return [String, nil] Current API key
|
48
75
|
def apikey
|
49
|
-
Utils.empty_to_nil(
|
76
|
+
Utils.empty_to_nil(@configuration[:apikey]) || ENV[apikey_env_var_name]
|
50
77
|
end
|
51
78
|
|
52
|
-
#
|
53
|
-
#
|
54
|
-
# @
|
79
|
+
# Set the API key
|
80
|
+
#
|
81
|
+
# @param apikey [String] New API key
|
82
|
+
# @return [String] The API key that was set
|
55
83
|
def apikey=(apikey)
|
56
|
-
|
84
|
+
@configuration[:apikey] = apikey
|
57
85
|
end
|
58
86
|
|
87
|
+
# Get the connection timeout
|
88
|
+
#
|
89
|
+
# @return [Integer] Connection timeout in milliseconds
|
59
90
|
def connect_timeout
|
60
|
-
|
91
|
+
parse_timeout(@configuration[:connect_timeout])
|
61
92
|
end
|
62
93
|
|
63
|
-
|
64
|
-
|
94
|
+
# Set the connection timeout
|
95
|
+
#
|
96
|
+
# @param timeout [Integer, String] New timeout value
|
97
|
+
# @return [Integer] The timeout that was set
|
98
|
+
def connect_timeout=(timeout)
|
99
|
+
@configuration[:connect_timeout] = parse_timeout(timeout)
|
65
100
|
end
|
66
101
|
|
67
|
-
#
|
68
|
-
#
|
102
|
+
# Get the environment variable name for the API key
|
103
|
+
#
|
104
|
+
# @return [String] Environment variable name
|
69
105
|
def apikey_env_var_name
|
70
|
-
(
|
106
|
+
(@configuration[:apikey_env_var_name] || APIKEY_ENV_NAME).upcase
|
71
107
|
end
|
72
108
|
|
73
|
-
#
|
74
|
-
#
|
75
|
-
# @
|
76
|
-
# @
|
77
|
-
def apikey_env_var_name=(
|
78
|
-
|
109
|
+
# Set the environment variable name for the API key
|
110
|
+
#
|
111
|
+
# @param var_name [String] New environment variable name
|
112
|
+
# @return [String] The variable name that was set (uppercased)
|
113
|
+
def apikey_env_var_name=(var_name)
|
114
|
+
@configuration[:apikey_env_var_name] = var_name.upcase
|
79
115
|
end
|
80
116
|
|
81
|
-
#
|
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.
|
117
|
+
# Fetch data from an API endpoint
|
96
118
|
#
|
119
|
+
# @param request [Request] Request object to send
|
120
|
+
# @return [Response, Hash, ResponseError] Response or error information
|
97
121
|
def fetch(request)
|
98
122
|
return nil unless request
|
99
123
|
|
100
|
-
request.valid?
|
101
|
-
|
124
|
+
if request.valid?
|
125
|
+
http_response = self.class.request(request)
|
126
|
+
raise HTTPX::Error, "HTTP request failed" if http_response.error && http_response.response.nil?
|
102
127
|
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
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
|
128
|
+
Response.resolve(http_response, request)
|
129
|
+
else
|
130
|
+
{ errors: request.errors }
|
131
|
+
end
|
132
|
+
rescue StandardError => e
|
133
|
+
handle_fetch_error(e, request)
|
116
134
|
end
|
117
135
|
|
118
|
-
|
119
|
-
|
136
|
+
# Handle method calls for API endpoints
|
137
|
+
#
|
138
|
+
# @param endpoint_name [String, Symbol] API endpoint name
|
139
|
+
# @param endpoint_params [Hash] Parameters for the endpoint
|
140
|
+
# @return [Response, Hash, ResponseError] API response or error
|
141
|
+
def method_missing(endpoint_name, **endpoint_params, &block)
|
142
|
+
if Endpoint.valid_name?(endpoint_name)
|
143
|
+
define_endpoint_method(endpoint_name)
|
144
|
+
send(endpoint_name, **endpoint_params)
|
145
|
+
else
|
146
|
+
super
|
147
|
+
end
|
120
148
|
end
|
121
149
|
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
150
|
+
# Check if client responds to endpoint methods
|
151
|
+
#
|
152
|
+
# @param endpoint_name [String, Symbol] Method name to check
|
153
|
+
# @param include_all [Boolean] Include private methods in check
|
154
|
+
# @return [Boolean] True if client responds to the method
|
155
|
+
def respond_to_missing?(endpoint_name, include_all = false)
|
156
|
+
Endpoint.valid_name?(endpoint_name) || super
|
126
157
|
end
|
127
158
|
|
128
159
|
private
|
129
160
|
|
130
|
-
|
131
|
-
|
132
|
-
|
161
|
+
def reset_configuration
|
162
|
+
@configuration = {
|
163
|
+
connect_timeout: DEFAULT_CONNECT_TIMEOUT,
|
164
|
+
}
|
165
|
+
end
|
133
166
|
|
134
|
-
def
|
135
|
-
|
167
|
+
def parse_timeout(value)
|
168
|
+
Utils.to_integer(value, DEFAULT_CONNECT_TIMEOUT)
|
136
169
|
end
|
137
170
|
|
138
|
-
|
139
|
-
|
140
|
-
|
171
|
+
def handle_fetch_error(error, request)
|
172
|
+
case error
|
173
|
+
when HTTPX::Error
|
174
|
+
NetworkError.new(
|
175
|
+
message: "Network error occurred: #{error.message}",
|
176
|
+
original_error: error,
|
177
|
+
)
|
178
|
+
else
|
179
|
+
ResponseError.new(
|
180
|
+
message: "Unexpected error: #{error.message}",
|
181
|
+
request: request,
|
182
|
+
original_error: error,
|
183
|
+
)
|
184
|
+
end
|
141
185
|
end
|
142
|
-
end
|
143
186
|
|
144
|
-
def
|
145
|
-
|
187
|
+
def define_endpoint_method(endpoint_name)
|
188
|
+
return if @endpoint_methods_defined.include?(endpoint_name)
|
189
|
+
|
190
|
+
define_singleton_method(endpoint_name) do |**params|
|
191
|
+
@endpoint_methods_defined.add(endpoint_name)
|
192
|
+
request = Request.new(endpoint_name, **params)
|
193
|
+
fetch(request)
|
194
|
+
end
|
146
195
|
end
|
147
196
|
end
|
148
197
|
end
|