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.
@@ -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
- # @return [String] the exported shell ENV variable name that holds the apikey
10
+
11
+ # Default environment variable name for API key
12
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
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
- def request(request_objects, opts={})
20
- HTTPX.with(options.merge(opts)).request(build_requests(request_objects))
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.to_a(requests).map(&:build)
38
+ Utils.to_array(requests).map(&:build)
25
39
  end
26
40
 
27
- def origin
28
- @origin ||= {origin: BASE_URL}
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
- def timeout
32
- {timeout: {connect_timeout: instance.connect_timeout}}
33
- end
52
+ attr_reader :configuration
34
53
 
35
- def options
36
- origin.merge(timeout)
37
- end
54
+ def initialize
55
+ @configuration = {}
56
+ @endpoint_methods_defined = Set.new
57
+ reset_configuration
38
58
  end
39
59
 
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
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
- # @return [String] apikey value from the instance options Hash object
47
- # but if nill use the value from +ENV[APIKEY_ENV_NAME]+
72
+ # Get the current API key
73
+ #
74
+ # @return [String, nil] Current API key
48
75
  def apikey
49
- Utils.empty_to_nil(options[:apikey]) || ENV[apikey_env_var_name]
76
+ Utils.empty_to_nil(@configuration[:apikey]) || ENV[apikey_env_var_name]
50
77
  end
51
78
 
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
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
- options[:apikey] = apikey
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
- parse_connect_timeout(options[:connect_timeout])
91
+ parse_timeout(@configuration[:connect_timeout])
61
92
  end
62
93
 
63
- def connect_timeout=(connect_timeout)
64
- parse_connect_timeout(connect_timeout)
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
- # 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
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
- (options[:apikey_env_var_name] || APIKEY_ENV_NAME).upcase
106
+ (@configuration[:apikey_env_var_name] || APIKEY_ENV_NAME).upcase
71
107
  end
72
108
 
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
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
- # 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.
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? ? Response.resolve(self.class.request(request), request) : {errors: request.errors}
101
- end
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
- # 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
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
- def options
119
- @options || @options = {}
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
- 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
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
- def build_request(endpoint_name, endpoint_params)
131
- Request.new(endpoint_name, **endpoint_params)
132
- end
161
+ def reset_configuration
162
+ @configuration = {
163
+ connect_timeout: DEFAULT_CONNECT_TIMEOUT,
164
+ }
165
+ end
133
166
 
134
- def try_fetch(endpoint_name, endpoint_params)
135
- respond_to?(endpoint_name) ? fetch(build_request(endpoint_name, endpoint_params)) : nil
167
+ def parse_timeout(value)
168
+ Utils.to_integer(value, DEFAULT_CONNECT_TIMEOUT)
136
169
  end
137
170
 
138
- def define_endpoint_method(endpoint_name)
139
- self.class.define_method(endpoint_name) do |**qparams|
140
- fetch(build_request(__method__, qparams))
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 parse_connect_timeout(milliseconds)
145
- options[:connect_timeout] = Utils.to_d(milliseconds, CONNECT_TIMEOUT)
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