typesense 0.1.1 → 0.5.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.circleci/config.yml +4 -2
- data/.gitignore +2 -0
- data/.rubocop.yml +7 -0
- data/.rubocop_todo.yml +36 -27
- data/LICENSE +198 -10
- data/README.md +10 -2
- data/codecov.yml +10 -0
- data/examples/aliases.rb +58 -0
- data/examples/client_initialization.rb +67 -0
- data/examples/collections_and_documents.rb +45 -68
- data/examples/keys.rb +127 -0
- data/examples/overrides.rb +108 -0
- data/examples/search.rb +26 -53
- data/lib/typesense.rb +8 -0
- data/lib/typesense/alias.rb +24 -0
- data/lib/typesense/aliases.rb +30 -0
- data/lib/typesense/api_call.rb +186 -69
- data/lib/typesense/client.rb +12 -3
- data/lib/typesense/collection.rb +8 -6
- data/lib/typesense/collections.rb +6 -6
- data/lib/typesense/configuration.rb +35 -19
- data/lib/typesense/debug.rb +3 -3
- data/lib/typesense/document.rb +4 -4
- data/lib/typesense/documents.rb +11 -6
- data/lib/typesense/error.rb +6 -0
- data/lib/typesense/health.rb +15 -0
- data/lib/typesense/key.rb +24 -0
- data/lib/typesense/keys.rb +34 -0
- data/lib/typesense/metrics.rb +15 -0
- data/lib/typesense/override.rb +25 -0
- data/lib/typesense/overrides.rb +31 -0
- data/lib/typesense/version.rb +1 -1
- data/typesense.gemspec +14 -11
- metadata +79 -25
data/examples/search.rb
CHANGED
@@ -3,89 +3,62 @@
|
|
3
3
|
##
|
4
4
|
# These examples walk you through operations specifically related to search
|
5
5
|
|
6
|
-
require_relative '
|
7
|
-
require 'awesome_print'
|
8
|
-
|
9
|
-
AwesomePrint.defaults = {
|
10
|
-
indent: -2
|
11
|
-
}
|
12
|
-
|
13
|
-
##
|
14
|
-
# Setup
|
15
|
-
#
|
16
|
-
# Start the master
|
17
|
-
# $ docker run -p 8108:8108 -it -v/tmp/typesense-data-master/:/data -it typesense/typesense:0.8.0-rc1 --data-dir /data --api-key=abcd --listen-port 8108
|
18
|
-
#
|
19
|
-
# Start the read replica
|
20
|
-
# $ docker run -p 8109:8109 -it -v/tmp/typesense-data-read-replica-1/:/data -it typesense/typesense:0.8.0-rc1 --data-dir /data --api-key=wxyz --listen-port 8109 --master http://localhost:8108
|
21
|
-
|
22
|
-
##
|
23
|
-
# Create a client
|
24
|
-
typesense = Typesense::Client.new(
|
25
|
-
master_node: {
|
26
|
-
host: 'localhost',
|
27
|
-
port: 8108,
|
28
|
-
protocol: 'http',
|
29
|
-
api_key: 'abcd'
|
30
|
-
},
|
31
|
-
read_replica_nodes: [
|
32
|
-
{
|
33
|
-
host: 'localhost',
|
34
|
-
port: 8109,
|
35
|
-
protocol: 'http',
|
36
|
-
api_key: 'wxyz'
|
37
|
-
}
|
38
|
-
],
|
39
|
-
timeout_seconds: 10
|
40
|
-
)
|
6
|
+
require_relative './client_initialization'
|
41
7
|
|
42
8
|
##
|
43
9
|
# Create a collection
|
44
10
|
schema = {
|
45
|
-
'name'
|
46
|
-
'fields'
|
11
|
+
'name' => 'companies',
|
12
|
+
'fields' => [
|
47
13
|
{
|
48
14
|
'name' => 'company_name',
|
49
15
|
'type' => 'string'
|
50
16
|
},
|
51
17
|
{
|
52
|
-
'name'
|
53
|
-
'type'
|
18
|
+
'name' => 'num_employees',
|
19
|
+
'type' => 'int32'
|
54
20
|
},
|
55
21
|
{
|
56
|
-
'name'
|
57
|
-
'type'
|
22
|
+
'name' => 'country',
|
23
|
+
'type' => 'string',
|
58
24
|
'facet' => true
|
59
25
|
}
|
60
26
|
],
|
61
27
|
'default_sorting_field' => 'num_employees'
|
62
28
|
}
|
63
29
|
|
64
|
-
|
30
|
+
# Delete the collection if it already exists
|
31
|
+
begin
|
32
|
+
@typesense.collections['companies'].delete
|
33
|
+
rescue Typesense::Error::ObjectNotFound
|
34
|
+
end
|
35
|
+
|
36
|
+
# Now create the collection
|
37
|
+
@typesense.collections.create(schema)
|
65
38
|
|
66
39
|
# Let's create a couple documents for us to use in our search examples
|
67
|
-
typesense.collections['companies'].documents.create(
|
40
|
+
@typesense.collections['companies'].documents.create(
|
68
41
|
'id' => '124',
|
69
42
|
'company_name' => 'Stark Industries',
|
70
43
|
'num_employees' => 5215,
|
71
44
|
'country' => 'USA'
|
72
45
|
)
|
73
46
|
|
74
|
-
typesense.collections['companies'].documents.create(
|
47
|
+
@typesense.collections['companies'].documents.create(
|
75
48
|
'id' => '127',
|
76
49
|
'company_name' => 'Stark Corp',
|
77
50
|
'num_employees' => 1031,
|
78
51
|
'country' => 'USA'
|
79
52
|
)
|
80
53
|
|
81
|
-
typesense.collections['companies'].documents.create(
|
54
|
+
@typesense.collections['companies'].documents.create(
|
82
55
|
'id' => '125',
|
83
56
|
'company_name' => 'Acme Corp',
|
84
57
|
'num_employees' => 1002,
|
85
58
|
'country' => 'France'
|
86
59
|
)
|
87
60
|
|
88
|
-
typesense.collections['companies'].documents.create(
|
61
|
+
@typesense.collections['companies'].documents.create(
|
89
62
|
'id' => '126',
|
90
63
|
'company_name' => 'Doofenshmirtz Inc',
|
91
64
|
'num_employees' => 2,
|
@@ -94,7 +67,7 @@ typesense.collections['companies'].documents.create(
|
|
94
67
|
|
95
68
|
##
|
96
69
|
# Search for documents
|
97
|
-
results = typesense.collections['companies'].documents.search(
|
70
|
+
results = @typesense.collections['companies'].documents.search(
|
98
71
|
'q' => 'Stark',
|
99
72
|
'query_by' => 'company_name'
|
100
73
|
)
|
@@ -133,11 +106,11 @@ ap results
|
|
133
106
|
|
134
107
|
##
|
135
108
|
# Search for more documents
|
136
|
-
results = typesense.collections['companies'].documents.search(
|
109
|
+
results = @typesense.collections['companies'].documents.search(
|
137
110
|
'q' => 'Inc',
|
138
|
-
'query_by'
|
111
|
+
'query_by' => 'company_name',
|
139
112
|
'filter_by' => 'num_employees:<100',
|
140
|
-
'sort_by'
|
113
|
+
'sort_by' => 'num_employees:desc'
|
141
114
|
)
|
142
115
|
ap results
|
143
116
|
|
@@ -163,8 +136,8 @@ ap results
|
|
163
136
|
|
164
137
|
##
|
165
138
|
# Search for more documents
|
166
|
-
results = typesense.collections['companies'].documents.search(
|
167
|
-
'q'
|
139
|
+
results = @typesense.collections['companies'].documents.search(
|
140
|
+
'q' => 'Non-existent',
|
168
141
|
'query_by' => 'company_name'
|
169
142
|
)
|
170
143
|
ap results
|
@@ -179,4 +152,4 @@ ap results
|
|
179
152
|
##
|
180
153
|
# Cleanup
|
181
154
|
# Drop the collection
|
182
|
-
typesense.collections['companies'].delete
|
155
|
+
@typesense.collections['companies'].delete
|
data/lib/typesense.rb
CHANGED
@@ -11,5 +11,13 @@ require_relative 'typesense/collections'
|
|
11
11
|
require_relative 'typesense/collection'
|
12
12
|
require_relative 'typesense/documents'
|
13
13
|
require_relative 'typesense/document'
|
14
|
+
require_relative 'typesense/overrides'
|
15
|
+
require_relative 'typesense/override'
|
16
|
+
require_relative 'typesense/aliases'
|
17
|
+
require_relative 'typesense/alias'
|
18
|
+
require_relative 'typesense/keys'
|
19
|
+
require_relative 'typesense/key'
|
14
20
|
require_relative 'typesense/debug'
|
21
|
+
require_relative 'typesense/health'
|
22
|
+
require_relative 'typesense/metrics'
|
15
23
|
require_relative 'typesense/error'
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Typesense
|
4
|
+
class Alias
|
5
|
+
def initialize(name, api_call)
|
6
|
+
@name = name
|
7
|
+
@api_call = api_call
|
8
|
+
end
|
9
|
+
|
10
|
+
def retrieve
|
11
|
+
@api_call.get(endpoint_path)
|
12
|
+
end
|
13
|
+
|
14
|
+
def delete
|
15
|
+
@api_call.delete(endpoint_path)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def endpoint_path
|
21
|
+
"#{Aliases::RESOURCE_PATH}/#{@name}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Typesense
|
4
|
+
class Aliases
|
5
|
+
RESOURCE_PATH = '/aliases'
|
6
|
+
|
7
|
+
def initialize(api_call)
|
8
|
+
@api_call = api_call
|
9
|
+
@aliases = {}
|
10
|
+
end
|
11
|
+
|
12
|
+
def upsert(alias_name, mapping)
|
13
|
+
@api_call.put(endpoint_path(alias_name), mapping)
|
14
|
+
end
|
15
|
+
|
16
|
+
def retrieve
|
17
|
+
@api_call.get(RESOURCE_PATH)
|
18
|
+
end
|
19
|
+
|
20
|
+
def [](alias_name)
|
21
|
+
@aliases[alias_name] ||= Alias.new(alias_name, @api_call)
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def endpoint_path(alias_name)
|
27
|
+
"#{Aliases::RESOURCE_PATH}/#{alias_name}"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/typesense/api_call.rb
CHANGED
@@ -10,106 +10,223 @@ module Typesense
|
|
10
10
|
|
11
11
|
def initialize(configuration)
|
12
12
|
@configuration = configuration
|
13
|
+
|
14
|
+
@api_key = @configuration.api_key
|
15
|
+
@nodes = @configuration.nodes.dup # Make a copy, since we'll be adding additional metadata to the nodes
|
16
|
+
@nearest_node = @configuration.nearest_node.dup
|
17
|
+
@connection_timeout_seconds = @configuration.connection_timeout_seconds
|
18
|
+
@healthcheck_interval_seconds = @configuration.healthcheck_interval_seconds
|
19
|
+
@num_retries_per_request = @configuration.num_retries
|
20
|
+
@retry_interval_seconds = @configuration.retry_interval_seconds
|
21
|
+
|
22
|
+
@logger = @configuration.logger
|
23
|
+
|
24
|
+
initialize_metadata_for_nodes
|
25
|
+
@current_node_index = -1
|
13
26
|
end
|
14
27
|
|
15
28
|
def post(endpoint, parameters = {})
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
end.parsed_response
|
29
|
+
headers, body = extract_headers_and_body_from(parameters)
|
30
|
+
|
31
|
+
perform_request :post,
|
32
|
+
endpoint,
|
33
|
+
body: body,
|
34
|
+
headers: default_headers.merge(headers)
|
23
35
|
end
|
24
36
|
|
25
|
-
def
|
26
|
-
|
37
|
+
def put(endpoint, parameters = {})
|
38
|
+
headers, body = extract_headers_and_body_from(parameters)
|
39
|
+
|
40
|
+
perform_request :put,
|
41
|
+
endpoint,
|
42
|
+
body: body,
|
43
|
+
headers: default_headers.merge(headers)
|
27
44
|
end
|
28
45
|
|
29
|
-
def
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
end
|
46
|
+
def get(endpoint, parameters = {})
|
47
|
+
headers, query = extract_headers_and_query_from(parameters)
|
48
|
+
|
49
|
+
perform_request :get,
|
50
|
+
endpoint,
|
51
|
+
query: query,
|
52
|
+
headers: default_headers.merge(headers)
|
37
53
|
end
|
38
54
|
|
39
55
|
def delete(endpoint, parameters = {})
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
56
|
+
headers, query = extract_headers_and_query_from(parameters)
|
57
|
+
|
58
|
+
perform_request :delete,
|
59
|
+
endpoint,
|
60
|
+
query: query,
|
61
|
+
headers: default_headers.merge(headers)
|
62
|
+
end
|
63
|
+
|
64
|
+
def perform_request(method, endpoint, options = {})
|
65
|
+
@configuration.validate!
|
66
|
+
last_exception = nil
|
67
|
+
@logger.debug "Performing #{method.to_s.upcase} request: #{endpoint}"
|
68
|
+
(1..(@num_retries_per_request + 1)).each do |num_tries|
|
69
|
+
node = next_node
|
70
|
+
|
71
|
+
@logger.debug "Attempting #{method.to_s.upcase} request Try ##{num_tries} to Node #{node[:index]}"
|
72
|
+
|
73
|
+
begin
|
74
|
+
response_object = self.class.send(method,
|
75
|
+
uri_for(endpoint, node),
|
76
|
+
default_options.merge(options))
|
77
|
+
response_code = response_object.response.code.to_i
|
78
|
+
set_node_healthcheck(node, is_healthy: true) if response_code >= 1 && response_code <= 499
|
79
|
+
|
80
|
+
@logger.debug "Request to Node #{node[:index]} was successfully made (at the network layer). Response Code was #{response_code}."
|
81
|
+
|
82
|
+
# If response is 2xx return the object, else raise the response as an exception
|
83
|
+
return response_object.parsed_response if response_object.response.code_type <= Net::HTTPSuccess # 2xx
|
84
|
+
|
85
|
+
raise custom_exception_klass_for(response_object.response), response_object.parsed_response['message'] || 'Error message not available'
|
86
|
+
rescue Net::ReadTimeout, Net::OpenTimeout,
|
87
|
+
EOFError, Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError,
|
88
|
+
Errno::EINVAL, Errno::ENETDOWN, Errno::ENETUNREACH, Errno::ENETRESET, Errno::ECONNABORTED, Errno::ECONNRESET,
|
89
|
+
Errno::ETIMEDOUT, Errno::ECONNREFUSED, Errno::EHOSTDOWN, Errno::EHOSTUNREACH,
|
90
|
+
Timeout::Error, HTTParty::ResponseError, Typesense::Error::ServerError, Typesense::Error::HTTPStatus0Error => e
|
91
|
+
# Rescue network layer exceptions and HTTP 5xx errors, so the loop can continue.
|
92
|
+
# Using loops for retries instead of rescue...retry to maintain consistency with client libraries in
|
93
|
+
# other languages that might not support the same construct.
|
94
|
+
set_node_healthcheck(node, is_healthy: false)
|
95
|
+
last_exception = e
|
96
|
+
@logger.warn "Request to Node #{node[:index]} failed due to \"#{e.class}: #{e.message}\""
|
97
|
+
@logger.warn "Sleeping for #{@retry_interval_seconds}s and then retrying request..."
|
98
|
+
sleep @retry_interval_seconds
|
99
|
+
end
|
100
|
+
end
|
101
|
+
@logger.debug "No retries left. Raising last error \"#{last_exception.class}: #{last_exception.message}\"..."
|
102
|
+
raise last_exception
|
47
103
|
end
|
48
104
|
|
49
105
|
private
|
50
106
|
|
51
|
-
def
|
52
|
-
if
|
53
|
-
|
107
|
+
def extract_headers_and_body_from(parameters)
|
108
|
+
if json_request?(parameters)
|
109
|
+
headers = { 'Content-Type' => 'application/json' }
|
110
|
+
body = sanitize_parameters(parameters).to_json
|
54
111
|
else
|
55
|
-
|
112
|
+
headers = {}
|
113
|
+
body = parameters[:body]
|
56
114
|
end
|
115
|
+
[headers, body]
|
57
116
|
end
|
58
117
|
|
59
|
-
def
|
60
|
-
|
118
|
+
def extract_headers_and_query_from(parameters)
|
119
|
+
if json_request?(parameters)
|
120
|
+
headers = { 'Content-Type' => 'application/json' }
|
121
|
+
query = sanitize_parameters(parameters)
|
122
|
+
else
|
123
|
+
headers = {}
|
124
|
+
query = parameters[:query]
|
125
|
+
end
|
126
|
+
[headers, query]
|
127
|
+
end
|
128
|
+
|
129
|
+
def json_request?(parameters)
|
130
|
+
parameters[:as_json].nil? ? true : parameters[:as_json]
|
131
|
+
end
|
61
132
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
raise error_klass, response_object.parsed_response['message']
|
87
|
-
rescue Net::ReadTimeout, Net::OpenTimeout,
|
88
|
-
EOFError, Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError,
|
89
|
-
Errno::EINVAL, Errno::ENETDOWN, Errno::ENETUNREACH, Errno::ENETRESET, Errno::ECONNABORTED, Errno::ECONNRESET,
|
90
|
-
Errno::ETIMEDOUT, Errno::ECONNREFUSED, Errno::EHOSTDOWN, Errno::EHOSTUNREACH,
|
91
|
-
Timeout::Error, Error::ServerError, HTTParty::ResponseError
|
92
|
-
if (use_read_replicas == :use_read_replicas || use_read_replicas == true) &&
|
93
|
-
!@configuration.read_replica_nodes.nil?
|
94
|
-
node = :read_replica
|
95
|
-
node_index += 1
|
96
|
-
|
97
|
-
retry unless @configuration.read_replica_nodes[node_index].nil?
|
133
|
+
def sanitize_parameters(parameters)
|
134
|
+
sanitized_parameters = parameters.dup
|
135
|
+
sanitized_parameters.delete(:as_json)
|
136
|
+
sanitized_parameters.delete(:body)
|
137
|
+
sanitized_parameters.delete(:query)
|
138
|
+
|
139
|
+
sanitized_parameters
|
140
|
+
end
|
141
|
+
|
142
|
+
def uri_for(endpoint, node)
|
143
|
+
"#{node[:protocol]}://#{node[:host]}:#{node[:port]}#{endpoint}"
|
144
|
+
end
|
145
|
+
|
146
|
+
## Attempts to find the next healthy node, looping through the list of nodes once.
|
147
|
+
# But if no healthy nodes are found, it will just return the next node, even if it's unhealthy
|
148
|
+
# so we can try the request for good measure, in case that node has become healthy since
|
149
|
+
def next_node
|
150
|
+
# Check if nearest_node is set and is healthy, if so return it
|
151
|
+
unless @nearest_node.nil?
|
152
|
+
@logger.debug "Nodes health: Node #{@nearest_node[:index]} is #{@nearest_node[:is_healthy] == true ? 'Healthy' : 'Unhealthy'}"
|
153
|
+
if @nearest_node[:is_healthy] == true || node_due_for_healthcheck?(@nearest_node)
|
154
|
+
@logger.debug "Updated current node to Node #{@nearest_node[:index]}"
|
155
|
+
return @nearest_node
|
98
156
|
end
|
157
|
+
@logger.debug 'Falling back to individual nodes'
|
158
|
+
end
|
159
|
+
|
160
|
+
# Fallback to nodes as usual
|
161
|
+
@logger.debug "Nodes health: #{@nodes.each_with_index.map { |node, i| "Node #{i} is #{node[:is_healthy] == true ? 'Healthy' : 'Unhealthy'}" }.join(' || ')}"
|
162
|
+
candidate_node = nil
|
163
|
+
(0..@nodes.length).each do |_i|
|
164
|
+
@current_node_index = (@current_node_index + 1) % @nodes.length
|
165
|
+
candidate_node = @nodes[@current_node_index]
|
166
|
+
if candidate_node[:is_healthy] == true || node_due_for_healthcheck?(candidate_node)
|
167
|
+
@logger.debug "Updated current node to Node #{candidate_node[:index]}"
|
168
|
+
return candidate_node
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
# None of the nodes are marked healthy, but some of them could have become healthy since last health check.
|
173
|
+
# So we will just return the next node.
|
174
|
+
@logger.debug "No healthy nodes were found. Returning the next node, Node #{candidate_node[:index]}"
|
175
|
+
candidate_node
|
176
|
+
end
|
177
|
+
|
178
|
+
def node_due_for_healthcheck?(node)
|
179
|
+
is_due_for_check = Time.now.to_i - node[:last_access_timestamp] > @healthcheck_interval_seconds
|
180
|
+
@logger.debug "Node #{node[:index]} has exceeded healthcheck_interval_seconds of #{@healthcheck_interval_seconds}. Adding it back into rotation." if is_due_for_check
|
181
|
+
is_due_for_check
|
182
|
+
end
|
183
|
+
|
184
|
+
def initialize_metadata_for_nodes
|
185
|
+
unless @nearest_node.nil?
|
186
|
+
@nearest_node[:index] = 'nearest_node'
|
187
|
+
set_node_healthcheck(@nearest_node, is_healthy: true)
|
188
|
+
end
|
189
|
+
@nodes.each_with_index do |node, index|
|
190
|
+
node[:index] = index
|
191
|
+
set_node_healthcheck(node, is_healthy: true)
|
192
|
+
end
|
193
|
+
end
|
99
194
|
|
100
|
-
|
195
|
+
def set_node_healthcheck(node, is_healthy:)
|
196
|
+
node[:is_healthy] = is_healthy
|
197
|
+
node[:last_access_timestamp] = Time.now.to_i
|
198
|
+
end
|
199
|
+
|
200
|
+
def custom_exception_klass_for(response)
|
201
|
+
response_code_type = response.code_type
|
202
|
+
if response_code_type <= Net::HTTPBadRequest # 400
|
203
|
+
Typesense::Error::RequestMalformed
|
204
|
+
elsif response_code_type <= Net::HTTPUnauthorized # 401
|
205
|
+
Typesense::Error::RequestUnauthorized
|
206
|
+
elsif response_code_type <= Net::HTTPNotFound # 404
|
207
|
+
Typesense::Error::ObjectNotFound
|
208
|
+
elsif response_code_type <= Net::HTTPConflict # 409
|
209
|
+
Typesense::Error::ObjectAlreadyExists
|
210
|
+
elsif response_code_type <= Net::HTTPUnprocessableEntity # 422
|
211
|
+
Typesense::Error::ObjectUnprocessable
|
212
|
+
elsif response_code_type <= Net::HTTPServerError # 5xx
|
213
|
+
Typesense::Error::ServerError
|
214
|
+
elsif response.code.to_i.zero?
|
215
|
+
Typesense::Error::HTTPStatus0Error
|
216
|
+
else
|
217
|
+
Typesense::Error::HTTPError
|
101
218
|
end
|
102
219
|
end
|
103
220
|
|
104
221
|
def default_options
|
105
222
|
{
|
106
|
-
timeout: @
|
223
|
+
timeout: @connection_timeout_seconds
|
107
224
|
}
|
108
225
|
end
|
109
226
|
|
110
227
|
def default_headers
|
111
228
|
{
|
112
|
-
API_KEY_HEADER_NAME.to_s => @
|
229
|
+
API_KEY_HEADER_NAME.to_s => @api_key
|
113
230
|
}
|
114
231
|
end
|
115
232
|
end
|