elastomer-client 0.7.0 → 0.8.1
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/.overcommit.yml +5 -0
- data/.rubocop.yml +83 -0
- data/CHANGELOG.md +8 -0
- data/Gemfile +2 -2
- data/README.md +1 -1
- data/Rakefile +2 -2
- data/elastomer-client.gemspec +4 -2
- data/lib/elastomer/client.rb +42 -37
- data/lib/elastomer/client/bulk.rb +2 -2
- data/lib/elastomer/client/cluster.rb +19 -19
- data/lib/elastomer/client/delete_by_query.rb +7 -7
- data/lib/elastomer/client/docs.rb +81 -24
- data/lib/elastomer/client/errors.rb +2 -2
- data/lib/elastomer/client/index.rb +65 -29
- data/lib/elastomer/client/multi_percolate.rb +127 -0
- data/lib/elastomer/client/multi_search.rb +2 -2
- data/lib/elastomer/client/nodes.rb +4 -4
- data/lib/elastomer/client/percolator.rb +77 -0
- data/lib/elastomer/client/repository.rb +7 -7
- data/lib/elastomer/client/scroller.rb +14 -14
- data/lib/elastomer/client/snapshot.rb +9 -9
- data/lib/elastomer/client/template.rb +3 -3
- data/lib/elastomer/client/warmer.rb +5 -16
- data/lib/elastomer/core_ext/time.rb +1 -1
- data/lib/elastomer/middleware/encode_json.rb +5 -5
- data/lib/elastomer/middleware/opaque_id.rb +3 -3
- data/lib/elastomer/middleware/parse_json.rb +5 -5
- data/lib/elastomer/notifications.rb +4 -4
- data/lib/elastomer/version.rb +1 -1
- data/script/bootstrap +2 -0
- data/script/console +5 -5
- data/test/assertions.rb +26 -24
- data/test/client/bulk_test.rb +111 -111
- data/test/client/cluster_test.rb +58 -58
- data/test/client/delete_by_query_test.rb +53 -53
- data/test/client/docs_test.rb +279 -203
- data/test/client/errors_test.rb +1 -1
- data/test/client/index_test.rb +143 -109
- data/test/client/multi_percolate_test.rb +130 -0
- data/test/client/multi_search_test.rb +30 -28
- data/test/client/nodes_test.rb +30 -29
- data/test/client/percolator_test.rb +52 -0
- data/test/client/repository_test.rb +23 -23
- data/test/client/scroller_test.rb +40 -40
- data/test/client/snapshot_test.rb +15 -15
- data/test/client/stubbed_client_test.rb +15 -15
- data/test/client/template_test.rb +10 -10
- data/test/client/warmer_test.rb +18 -18
- data/test/client_test.rb +53 -53
- data/test/core_ext/time_test.rb +12 -12
- data/test/middleware/encode_json_test.rb +20 -20
- data/test/middleware/opaque_id_test.rb +10 -10
- data/test/middleware/parse_json_test.rb +14 -14
- data/test/notifications_test.rb +32 -32
- data/test/test_helper.rb +24 -24
- metadata +38 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9539ab968e233a1329b616dfd377bf7fe9c73905
|
4
|
+
data.tar.gz: e7cdb14136521443f70f6eb7f7d7f5d747dcd521
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1346de81716c760aa851e323617cd7318941a41636a32351ac498975c7a7714e283c2d0fcaaaf14d5f0ba4bd6dadbcd2913c4a590d30f558cd3d657f37c274d3
|
7
|
+
data.tar.gz: 0610f7decbac4ba346017093fccd95b75bfb429ec885a6412995fb3367fee06fc075acdd738d50bff54f417dce133681f62390dcc798526d7d10757a66907f19
|
data/.overcommit.yml
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
# Ruby linting configuration.
|
2
|
+
# We only worry about two kinds of issues: 'error' and anything less than that.
|
3
|
+
# Error is not about severity, but about taste. Simple style choices that
|
4
|
+
# never have a great excuse to be broken, such as 1.9 JSON-like hash syntax,
|
5
|
+
# are errors. Choices that tend to have good exceptions in practice, such as
|
6
|
+
# line length, are warnings.
|
7
|
+
|
8
|
+
# If you'd like to make changes, a full list of available issues is at
|
9
|
+
# https://github.com/bbatsov/rubocop/blob/master/config/enabled.yml
|
10
|
+
# A list of configurable issues is at:
|
11
|
+
# https://github.com/bbatsov/rubocop/blob/master/config/default.yml
|
12
|
+
#
|
13
|
+
# If you disable a check, document why.
|
14
|
+
|
15
|
+
AllCops:
|
16
|
+
DisabledByDefault: true
|
17
|
+
|
18
|
+
StringLiterals:
|
19
|
+
EnforcedStyle: double_quotes
|
20
|
+
Severity: error
|
21
|
+
|
22
|
+
HashSyntax:
|
23
|
+
EnforcedStyle: hash_rockets
|
24
|
+
Severity: error
|
25
|
+
|
26
|
+
AlignHash:
|
27
|
+
SupportedLastArgumentHashStyles: always_ignore
|
28
|
+
|
29
|
+
AlignParameters:
|
30
|
+
Enabled: false # This is usually true, but we often want to roll back to
|
31
|
+
# the start of a line.
|
32
|
+
|
33
|
+
Attr:
|
34
|
+
Enabled: false # We have no styleguide guidance here, and it seems to be
|
35
|
+
# in frequent use.
|
36
|
+
|
37
|
+
ClassAndModuleChildren:
|
38
|
+
Enabled: false # module X<\n>module Y is just as good as module X::Y.
|
39
|
+
|
40
|
+
Documentation:
|
41
|
+
Exclude:
|
42
|
+
- !ruby/regexp /test\/*.rb/
|
43
|
+
|
44
|
+
ClassLength:
|
45
|
+
Exclude:
|
46
|
+
- !ruby/regexp /test\/*.rb/
|
47
|
+
|
48
|
+
PercentLiteralDelimiters:
|
49
|
+
PreferredDelimiters:
|
50
|
+
'%w': '{}'
|
51
|
+
|
52
|
+
LineLength:
|
53
|
+
Max: 79
|
54
|
+
Severity: warning
|
55
|
+
|
56
|
+
MultilineTernaryOperator:
|
57
|
+
Severity: error
|
58
|
+
|
59
|
+
UnreachableCode:
|
60
|
+
Severity: error
|
61
|
+
|
62
|
+
AndOr:
|
63
|
+
Severity: error
|
64
|
+
|
65
|
+
EndAlignment:
|
66
|
+
Severity: error
|
67
|
+
|
68
|
+
IndentationWidth:
|
69
|
+
Severity: error
|
70
|
+
|
71
|
+
MethodLength:
|
72
|
+
CountComments: false # count full line comments?
|
73
|
+
Max: 20
|
74
|
+
Severity: error
|
75
|
+
|
76
|
+
Alias:
|
77
|
+
Enabled: false # We have no guidance on alias vs alias_method
|
78
|
+
|
79
|
+
RedundantSelf:
|
80
|
+
Enabled: false # Sometimes a self.field is a bit more clear
|
81
|
+
|
82
|
+
IfUnlessModifier:
|
83
|
+
Enabled: false
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,11 @@
|
|
1
|
+
## 0.8.1 (2015-11-04)
|
2
|
+
- Replace yanked 0.8.0
|
3
|
+
- Fix code style based on Rubocop recommendations
|
4
|
+
|
5
|
+
## 0.8.0 (2015-09-23) yanked due to invalid build
|
6
|
+
- BREAKING: Remove `Client#warmer` method
|
7
|
+
- Add the Percolate API
|
8
|
+
|
1
9
|
## 0.7.0 (2015-09-18)
|
2
10
|
- Add streaming bulk functionality via `bulk_stream_items`
|
3
11
|
- Make Delete by Query compatible with Elasticsearch 2.0
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -102,6 +102,6 @@ ElasticSearch. You may find that Excon performs better for your use. To enable
|
|
102
102
|
Excon, add it to your bundle and then change your Elastomer initialization
|
103
103
|
thusly:
|
104
104
|
|
105
|
-
```
|
105
|
+
```ruby
|
106
106
|
Elastomer::Client.new(url: YOUR_ES_URL, adapter: :excon)
|
107
107
|
```
|
data/Rakefile
CHANGED
data/elastomer-client.gemspec
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# coding: utf-8
|
2
|
-
lib = File.expand_path(
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
3
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
-
require
|
4
|
+
require "elastomer/version"
|
5
5
|
|
6
6
|
Gem::Specification.new do |spec|
|
7
7
|
spec.name = "elastomer-client"
|
@@ -29,4 +29,6 @@ Gem::Specification.new do |spec|
|
|
29
29
|
spec.add_development_dependency "minitest","~> 4.7"
|
30
30
|
spec.add_development_dependency "webmock","~> 1.21"
|
31
31
|
spec.add_development_dependency "rake"
|
32
|
+
spec.add_development_dependency "overcommit"
|
33
|
+
spec.add_development_dependency "rubocop"
|
32
34
|
end
|
data/lib/elastomer/client.rb
CHANGED
@@ -1,9 +1,9 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require
|
4
|
-
require
|
1
|
+
require "addressable/template"
|
2
|
+
require "faraday"
|
3
|
+
require "multi_json"
|
4
|
+
require "semantic"
|
5
5
|
|
6
|
-
require
|
6
|
+
require "elastomer/version"
|
7
7
|
|
8
8
|
module Elastomer
|
9
9
|
|
@@ -22,7 +22,7 @@ module Elastomer
|
|
22
22
|
# :opaque_id - set to `true` to use the 'X-Opaque-Id' request header
|
23
23
|
#
|
24
24
|
def initialize( opts = {} )
|
25
|
-
host = opts.fetch :host,
|
25
|
+
host = opts.fetch :host, "localhost"
|
26
26
|
port = opts.fetch :port, 9200
|
27
27
|
@url = opts.fetch :url, "http://#{host}:#{port}"
|
28
28
|
|
@@ -41,7 +41,7 @@ module Elastomer
|
|
41
41
|
|
42
42
|
# Returns true if the server is available; returns false otherwise.
|
43
43
|
def ping
|
44
|
-
response = head
|
44
|
+
response = head "/", :action => "cluster.ping"
|
45
45
|
response.success?
|
46
46
|
rescue StandardError
|
47
47
|
false
|
@@ -50,7 +50,7 @@ module Elastomer
|
|
50
50
|
|
51
51
|
# Returns the version String of the attached ElasticSearch instance.
|
52
52
|
def version
|
53
|
-
@version ||= info[
|
53
|
+
@version ||= info["version"]["number"]
|
54
54
|
end
|
55
55
|
|
56
56
|
# Returns a Semantic::Version for the attached ElasticSearch instance.
|
@@ -61,7 +61,7 @@ module Elastomer
|
|
61
61
|
|
62
62
|
# Returns the information Hash from the attached ElasticSearch instance.
|
63
63
|
def info
|
64
|
-
response = get
|
64
|
+
response = get "/", :action => "cluster.info"
|
65
65
|
response.body
|
66
66
|
end
|
67
67
|
|
@@ -75,9 +75,11 @@ module Elastomer
|
|
75
75
|
conn.response :parse_json
|
76
76
|
conn.request :opaque_id if @opaque_id
|
77
77
|
|
78
|
-
@adapter.is_a?(Array)
|
79
|
-
conn.adapter(*@adapter)
|
78
|
+
if @adapter.is_a?(Array)
|
79
|
+
conn.adapter(*@adapter)
|
80
|
+
else
|
80
81
|
conn.adapter(@adapter)
|
82
|
+
end
|
81
83
|
|
82
84
|
conn.options[:timeout] = read_timeout
|
83
85
|
conn.options[:open_timeout] = open_timeout
|
@@ -150,6 +152,7 @@ module Elastomer
|
|
150
152
|
#
|
151
153
|
# Returns a Faraday::Response
|
152
154
|
# Raises an Elastomer::Client::Error on 4XX and 5XX responses
|
155
|
+
# rubocop:disable Metrics/MethodLength
|
153
156
|
def request( method, path, params )
|
154
157
|
read_timeout = params.delete :read_timeout
|
155
158
|
body = extract_body params
|
@@ -157,42 +160,44 @@ module Elastomer
|
|
157
160
|
|
158
161
|
instrument(path, body, params) do
|
159
162
|
begin
|
160
|
-
response =
|
161
|
-
|
162
|
-
|
163
|
+
response =
|
164
|
+
case method
|
165
|
+
when :head
|
166
|
+
connection.head(path) { |req| req.options[:timeout] = read_timeout if read_timeout }
|
163
167
|
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
168
|
+
when :get
|
169
|
+
connection.get(path) { |req|
|
170
|
+
req.body = body if body
|
171
|
+
req.options[:timeout] = read_timeout if read_timeout
|
172
|
+
}
|
169
173
|
|
170
|
-
|
171
|
-
|
174
|
+
when :put
|
175
|
+
connection.put(path, body) { |req| req.options[:timeout] = read_timeout if read_timeout }
|
172
176
|
|
173
|
-
|
174
|
-
|
177
|
+
when :post
|
178
|
+
connection.post(path, body) { |req| req.options[:timeout] = read_timeout if read_timeout }
|
175
179
|
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
180
|
+
when :delete
|
181
|
+
connection.delete(path) { |req|
|
182
|
+
req.body = body if body
|
183
|
+
req.options[:timeout] = read_timeout if read_timeout
|
184
|
+
}
|
181
185
|
|
182
|
-
|
183
|
-
|
184
|
-
|
186
|
+
else
|
187
|
+
raise ArgumentError, "unknown HTTP request method: #{method.inspect}"
|
188
|
+
end
|
185
189
|
|
186
190
|
handle_errors response
|
187
191
|
|
188
192
|
# wrap Faraday errors with appropriate Elastomer::Client error classes
|
189
193
|
rescue Faraday::Error::ClientError => boom
|
190
|
-
error_name = boom.class.name.split(
|
194
|
+
error_name = boom.class.name.split("::").last
|
191
195
|
error_class = Elastomer::Client.const_get(error_name) rescue Elastomer::Client::Error
|
192
196
|
raise error_class.new(boom, method.upcase, path)
|
193
197
|
end
|
194
198
|
end
|
195
199
|
end
|
200
|
+
# rubocop:enable Metrics/MethodLength
|
196
201
|
|
197
202
|
# Internal: Extract the :body from the params Hash and convert it to a
|
198
203
|
# JSON String format. If the params Hash does not contain a :body then no
|
@@ -290,7 +295,7 @@ module Elastomer
|
|
290
295
|
# containing and 'error' field.
|
291
296
|
def handle_errors( response )
|
292
297
|
raise ServerError, response if response.status >= 500
|
293
|
-
raise RequestError, response if response.body.is_a?(Hash) && response.body[
|
298
|
+
raise RequestError, response if response.body.is_a?(Hash) && response.body["error"]
|
294
299
|
|
295
300
|
response
|
296
301
|
end
|
@@ -310,7 +315,7 @@ module Elastomer
|
|
310
315
|
#
|
311
316
|
# Returns the validated param as a String.
|
312
317
|
# Raises an ArgumentError if the param is not valid.
|
313
|
-
def assert_param_presence( param, name =
|
318
|
+
def assert_param_presence( param, name = "input value" )
|
314
319
|
case param
|
315
320
|
when String, Symbol, Numeric
|
316
321
|
param = param.to_s.strip
|
@@ -318,7 +323,7 @@ module Elastomer
|
|
318
323
|
param
|
319
324
|
|
320
325
|
when Array
|
321
|
-
param.flatten.map { |item| assert_param_presence(item, name) }.join(
|
326
|
+
param.flatten.map { |item| assert_param_presence(item, name) }.join(",")
|
322
327
|
|
323
328
|
when nil
|
324
329
|
raise ArgumentError, "#{name} cannot be nil"
|
@@ -332,7 +337,7 @@ module Elastomer
|
|
332
337
|
end # Elastomer
|
333
338
|
|
334
339
|
# require all files in the `client` sub-directory
|
335
|
-
Dir.glob(File.expand_path(
|
340
|
+
Dir.glob(File.expand_path("../client/*.rb", __FILE__)).each { |fn| require fn }
|
336
341
|
|
337
342
|
# require all files in the `middleware` sub-directory
|
338
|
-
Dir.glob(File.expand_path(
|
343
|
+
Dir.glob(File.expand_path("../middleware/*.rb", __FILE__)).each { |fn| require fn }
|
@@ -35,10 +35,10 @@ module Elastomer
|
|
35
35
|
bulk_obj.call
|
36
36
|
|
37
37
|
else
|
38
|
-
raise
|
38
|
+
raise "bulk request body cannot be nil" if body.nil?
|
39
39
|
params ||= {}
|
40
40
|
|
41
|
-
response = self.post
|
41
|
+
response = self.post "{/index}{/type}/_bulk", params.merge(:body => body, :action => "bulk")
|
42
42
|
response.body
|
43
43
|
end
|
44
44
|
end
|
@@ -34,7 +34,7 @@ module Elastomer
|
|
34
34
|
#
|
35
35
|
# Returns the response as a Hash
|
36
36
|
def health( params = {} )
|
37
|
-
response = client.get
|
37
|
+
response = client.get "/_cluster/health{/index}", params.merge(:action => "cluster.health")
|
38
38
|
response.body
|
39
39
|
end
|
40
40
|
|
@@ -52,7 +52,7 @@ module Elastomer
|
|
52
52
|
#
|
53
53
|
# Returns the response as a Hash
|
54
54
|
def state( params = {} )
|
55
|
-
response = client.get
|
55
|
+
response = client.get "/_cluster/state{/metrics}{/indices}", params.merge(:action => "cluster.state")
|
56
56
|
response.body
|
57
57
|
end
|
58
58
|
|
@@ -67,7 +67,7 @@ module Elastomer
|
|
67
67
|
#
|
68
68
|
# Returns the response as a Hash
|
69
69
|
def stats( params = {} )
|
70
|
-
response = client.get
|
70
|
+
response = client.get "/_cluster/stats", params.merge(:action => "cluster.stats")
|
71
71
|
response.body
|
72
72
|
end
|
73
73
|
|
@@ -80,7 +80,7 @@ module Elastomer
|
|
80
80
|
#
|
81
81
|
# Returns the response as a Hash
|
82
82
|
def pending_tasks( params = {} )
|
83
|
-
response = client.get
|
83
|
+
response = client.get "/_cluster/pending_tasks", params.merge(:action => "cluster.pending_tasks")
|
84
84
|
response.body
|
85
85
|
end
|
86
86
|
|
@@ -101,7 +101,7 @@ module Elastomer
|
|
101
101
|
#
|
102
102
|
# Returns the response as a Hash
|
103
103
|
def get_settings( params = {} )
|
104
|
-
response = client.get
|
104
|
+
response = client.get "/_cluster/settings", params.merge(:action => "cluster.get_settings")
|
105
105
|
response.body
|
106
106
|
end
|
107
107
|
alias_method :settings, :get_settings
|
@@ -117,7 +117,7 @@ module Elastomer
|
|
117
117
|
#
|
118
118
|
# Returns the response as a Hash
|
119
119
|
def update_settings( body, params = {} )
|
120
|
-
response = client.put
|
120
|
+
response = client.put "/_cluster/settings", params.merge(:body => body, :action => "cluster.update_settings")
|
121
121
|
response.body
|
122
122
|
end
|
123
123
|
|
@@ -157,7 +157,7 @@ module Elastomer
|
|
157
157
|
body = {:commands => Array(commands)}
|
158
158
|
end
|
159
159
|
|
160
|
-
response = client.post
|
160
|
+
response = client.post "/_cluster/reroute", params.merge(:body => body, :action => "cluster.reroute")
|
161
161
|
response.body
|
162
162
|
end
|
163
163
|
|
@@ -170,7 +170,7 @@ module Elastomer
|
|
170
170
|
#
|
171
171
|
# Returns the response as a Hash
|
172
172
|
def shutdown( params = {} )
|
173
|
-
response = client.post
|
173
|
+
response = client.post "/_shutdown", params.merge(:action => "cluster.shutdown")
|
174
174
|
response.body
|
175
175
|
end
|
176
176
|
|
@@ -191,7 +191,7 @@ module Elastomer
|
|
191
191
|
#
|
192
192
|
# Returns the response body as a Hash
|
193
193
|
def get_aliases( params = {} )
|
194
|
-
response = client.get
|
194
|
+
response = client.get "{/index}/_aliases", params.merge(:action => "cluster.get_aliases")
|
195
195
|
response.body
|
196
196
|
end
|
197
197
|
alias_method :aliases, :get_aliases
|
@@ -227,7 +227,7 @@ module Elastomer
|
|
227
227
|
body = {:actions => Array(actions)}
|
228
228
|
end
|
229
229
|
|
230
|
-
response = client.post
|
230
|
+
response = client.post "/_aliases", params.merge(:body => body, :action => "cluster.update_aliases")
|
231
231
|
response.body
|
232
232
|
end
|
233
233
|
|
@@ -238,8 +238,8 @@ module Elastomer
|
|
238
238
|
def templates
|
239
239
|
# ES 1.x supports state filtering via a path segment called metrics.
|
240
240
|
# ES 0.90 uses query parameters instead.
|
241
|
-
if client.semantic_version >=
|
242
|
-
h = state(:metrics =>
|
241
|
+
if client.semantic_version >= "1.0.0"
|
242
|
+
h = state(:metrics => "metadata")
|
243
243
|
else
|
244
244
|
h = state(
|
245
245
|
:filter_blocks => true,
|
@@ -247,7 +247,7 @@ module Elastomer
|
|
247
247
|
:filter_routing_table => true,
|
248
248
|
)
|
249
249
|
end
|
250
|
-
h[
|
250
|
+
h["metadata"]["templates"]
|
251
251
|
end
|
252
252
|
|
253
253
|
# List all indices currently defined. This is just a convenience method
|
@@ -257,8 +257,8 @@ module Elastomer
|
|
257
257
|
def indices
|
258
258
|
# ES 1.x supports state filtering via a path segment called metrics.
|
259
259
|
# ES 0.90 uses query parameters instead.
|
260
|
-
if client.semantic_version >=
|
261
|
-
h = state(:metrics =>
|
260
|
+
if client.semantic_version >= "1.0.0"
|
261
|
+
h = state(:metrics => "metadata")
|
262
262
|
else
|
263
263
|
h = state(
|
264
264
|
:filter_blocks => true,
|
@@ -266,7 +266,7 @@ module Elastomer
|
|
266
266
|
:filter_routing_table => true,
|
267
267
|
)
|
268
268
|
end
|
269
|
-
h[
|
269
|
+
h["metadata"]["indices"]
|
270
270
|
end
|
271
271
|
|
272
272
|
# List all nodes currently part of the cluster. This is just a convenience
|
@@ -277,8 +277,8 @@ module Elastomer
|
|
277
277
|
def nodes
|
278
278
|
# ES 1.x supports state filtering via a path segment called metrics.
|
279
279
|
# ES 0.90 uses query parameters instead.
|
280
|
-
if client.semantic_version >=
|
281
|
-
h = state(:metrics =>
|
280
|
+
if client.semantic_version >= "1.0.0"
|
281
|
+
h = state(:metrics => "nodes")
|
282
282
|
else
|
283
283
|
h = state(
|
284
284
|
:filter_blocks => true,
|
@@ -286,7 +286,7 @@ module Elastomer
|
|
286
286
|
:filter_routing_table => true,
|
287
287
|
)
|
288
288
|
end
|
289
|
-
h[
|
289
|
+
h["nodes"]
|
290
290
|
end
|
291
291
|
|
292
292
|
end
|