gds-api-adapters 4.2.0 → 4.3.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.
- data/lib/gds_api/exceptions.rb +3 -0
- data/lib/gds_api/json_client.rb +82 -53
- data/lib/gds_api/version.rb +1 -1
- data/test/json_client_test.rb +119 -0
- metadata +35 -24
data/lib/gds_api/exceptions.rb
CHANGED
data/lib/gds_api/json_client.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
require_relative 'response'
|
2
2
|
require_relative 'exceptions'
|
3
3
|
require_relative 'version'
|
4
|
-
require 'net/http'
|
5
4
|
require 'lrucache'
|
5
|
+
require 'rest-client'
|
6
6
|
|
7
7
|
module GdsApi
|
8
8
|
class JsonClient
|
@@ -37,7 +37,9 @@ module GdsApi
|
|
37
37
|
DEFAULT_CACHE_TTL = 15 * 60 # 15 minutes
|
38
38
|
|
39
39
|
def get_raw(url)
|
40
|
-
|
40
|
+
ignoring GdsApi::HTTPNotFound do
|
41
|
+
do_raw_request(:get, url)
|
42
|
+
end
|
41
43
|
end
|
42
44
|
|
43
45
|
# Define "safe" methods for each supported HTTP method
|
@@ -55,108 +57,135 @@ module GdsApi
|
|
55
57
|
end
|
56
58
|
|
57
59
|
def get_json!(url, &create_response)
|
58
|
-
@cache[url] ||= do_json_request(
|
60
|
+
@cache[url] ||= do_json_request(:get, url, nil, &create_response)
|
59
61
|
end
|
60
62
|
|
61
63
|
def post_json!(url, params)
|
62
|
-
do_json_request(
|
64
|
+
do_json_request(:post, url, params)
|
63
65
|
end
|
64
66
|
|
65
67
|
def put_json!(url, params)
|
66
|
-
do_json_request(
|
68
|
+
do_json_request(:put, url, params)
|
67
69
|
end
|
68
70
|
|
69
71
|
def delete_json!(url, params = nil)
|
70
|
-
do_request(
|
72
|
+
do_request(:delete, url, params)
|
71
73
|
end
|
72
74
|
|
73
75
|
private
|
74
|
-
def do_raw_request(
|
75
|
-
response
|
76
|
+
def do_raw_request(method, url, params = nil)
|
77
|
+
response = do_request(method, url, params)
|
76
78
|
response.body
|
79
|
+
|
80
|
+
rescue RestClient::ResourceNotFound => e
|
81
|
+
raise GdsApi::HTTPNotFound.new(e.http_code)
|
82
|
+
|
83
|
+
rescue RestClient::Exception => e
|
84
|
+
raise GdsApi::HTTPErrorResponse.new(response.code.to_i), e.response.body
|
77
85
|
end
|
78
86
|
|
79
|
-
#
|
87
|
+
# method: the symbolic name of the method to use, e.g. :get, :post
|
80
88
|
# url: the request URL
|
81
89
|
# params: the data to send (JSON-serialised) in the request body
|
82
90
|
# create_response: optional block to instantiate a custom response object
|
83
91
|
# from the Net::HTTPResponse
|
84
|
-
def do_json_request(
|
92
|
+
def do_json_request(method, url, params = nil, &create_response)
|
85
93
|
|
86
|
-
|
94
|
+
begin
|
95
|
+
response = do_request(method, url, params)
|
87
96
|
|
88
|
-
|
89
|
-
|
97
|
+
rescue RestClient::ResourceNotFound => e
|
98
|
+
raise GdsApi::HTTPNotFound.new(e.http_code)
|
90
99
|
|
91
|
-
|
92
|
-
|
93
|
-
create_response.call(response)
|
94
|
-
elsif response.is_a?(Net::HTTPNotFound)
|
95
|
-
raise GdsApi::HTTPNotFound.new(response.code.to_i)
|
96
|
-
else
|
100
|
+
rescue RestClient::Exception => e
|
101
|
+
# Attempt to parse the body as JSON if possible
|
97
102
|
body = begin
|
98
|
-
JSON.parse(
|
103
|
+
e.http_body ? JSON.parse(e.http_body) : nil
|
99
104
|
rescue JSON::ParserError
|
100
|
-
|
105
|
+
e.http_body
|
101
106
|
end
|
102
|
-
|
103
|
-
logger.warn loggable.to_json
|
104
|
-
raise GdsApi::HTTPErrorResponse.new(response.code.to_i), body
|
107
|
+
raise GdsApi::HTTPErrorResponse.new(e.http_code), body
|
105
108
|
end
|
106
|
-
end
|
107
109
|
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
path = path + "?" + url.query if url.query
|
112
|
-
return url, path
|
110
|
+
# If no custom response is given, just instantiate Response
|
111
|
+
create_response ||= Proc.new { |r| Response.new(r) }
|
112
|
+
create_response.call(response.net_http_res)
|
113
113
|
end
|
114
114
|
|
115
|
-
|
115
|
+
# Take a hash of parameters for Request#execute; return a hash of
|
116
|
+
# parameters with authentication information included
|
117
|
+
def with_auth_options(method_params)
|
116
118
|
if @options[:bearer_token]
|
117
|
-
|
119
|
+
headers = method_params[:headers] || {}
|
120
|
+
method_params.merge(headers: headers.merge(
|
121
|
+
{"Authorization" => "Bearer #{@options[:bearer_token]}"}
|
122
|
+
))
|
118
123
|
elsif @options[:basic_auth]
|
119
|
-
|
124
|
+
method_params.merge(
|
125
|
+
user: @options[:basic_auth][:user],
|
126
|
+
password: @options[:basic_auth][:password]
|
127
|
+
)
|
128
|
+
else
|
129
|
+
method_params
|
120
130
|
end
|
121
131
|
end
|
122
132
|
|
123
|
-
|
124
|
-
|
125
|
-
|
133
|
+
# Take a hash of parameters for Request#execute; return a hash of
|
134
|
+
# parameters with timeouts included
|
135
|
+
def with_timeout(method_params)
|
136
|
+
if options[:disable_timeout]
|
137
|
+
method_params.merge(timeout: -1)
|
138
|
+
else
|
139
|
+
method_params.merge(
|
140
|
+
timeout: options[:timeout] || DEFAULT_TIMEOUT_IN_SECONDS
|
141
|
+
)
|
126
142
|
end
|
127
143
|
end
|
128
144
|
|
129
|
-
def
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
end
|
145
|
+
def with_ssl_options(method_params)
|
146
|
+
method_params.merge(
|
147
|
+
# This is the default value anyway, but we should probably be explicit
|
148
|
+
verify_ssl: OpenSSL::SSL::VERIFY_NONE
|
149
|
+
)
|
135
150
|
end
|
136
151
|
|
137
|
-
def do_request(
|
152
|
+
def do_request(method, url, params = nil)
|
138
153
|
loggable = {request_uri: url, start_time: Time.now.to_f}
|
139
154
|
start_logging = loggable.merge(action: 'start')
|
140
155
|
logger.debug start_logging.to_json
|
141
156
|
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
157
|
+
method_params = {
|
158
|
+
method: method,
|
159
|
+
url: url,
|
160
|
+
headers: DEFAULT_REQUEST_HEADERS
|
161
|
+
}
|
162
|
+
method_params[:payload] = params.to_json if params
|
163
|
+
method_params = with_auth_options(method_params)
|
164
|
+
method_params = with_timeout(method_params)
|
165
|
+
if URI.parse(url).is_a? URI::HTTPS
|
166
|
+
method_params = with_ssl_options(method_params)
|
150
167
|
end
|
151
168
|
|
152
|
-
return
|
169
|
+
return ::RestClient::Request.execute(method_params)
|
153
170
|
|
154
171
|
rescue Errno::ECONNREFUSED => e
|
155
172
|
logger.error loggable.merge(status: 'refused', error_message: e.message, error_class: e.class.name, end_time: Time.now.to_f).to_json
|
156
173
|
raise GdsApi::EndpointNotFound.new("Could not connect to #{url}")
|
157
|
-
|
174
|
+
|
175
|
+
rescue RestClient::RequestTimeout => e
|
158
176
|
logger.error loggable.merge(status: 'timeout', error_message: e.message, error_class: e.class.name, end_time: Time.now.to_f).to_json
|
159
177
|
raise GdsApi::TimedOutException.new
|
178
|
+
|
179
|
+
rescue RestClient::MaxRedirectsReached => e
|
180
|
+
raise GdsApi::TooManyRedirects
|
181
|
+
|
182
|
+
rescue RestClient::Exception => e
|
183
|
+
# Log the error here, since we have access to loggable, but raise the
|
184
|
+
# exception up to the calling method to deal with
|
185
|
+
loggable.merge!(status: e.http_code, end_time: Time.now.to_f, body: e.http_body)
|
186
|
+
logger.warn loggable.to_json
|
187
|
+
raise
|
188
|
+
|
160
189
|
rescue Errno::ECONNRESET => e
|
161
190
|
logger.error loggable.merge(status: 'connection_reset', error_message: e.message, error_class: e.class.name, end_time: Time.now.to_f).to_json
|
162
191
|
raise GdsApi::TimedOutException.new
|
data/lib/gds_api/version.rb
CHANGED
data/test/json_client_test.rb
CHANGED
@@ -51,6 +51,14 @@ class JsonClientTest < MiniTest::Spec
|
|
51
51
|
end
|
52
52
|
end
|
53
53
|
|
54
|
+
def test_get_should_raise_error_on_restclient_error
|
55
|
+
url = "http://some.endpoint/some.json"
|
56
|
+
stub_request(:get, url).to_raise(RestClient::ServerBrokeConnection)
|
57
|
+
assert_raises GdsApi::HTTPErrorResponse do
|
58
|
+
@client.get_json(url)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
54
62
|
def test_should_fetch_and_parse_json_into_response
|
55
63
|
url = "http://some.endpoint/some.json"
|
56
64
|
stub_request(:get, url).to_return(:body => "{}", :status => 200)
|
@@ -112,6 +120,7 @@ class JsonClientTest < MiniTest::Spec
|
|
112
120
|
assert_equal response_b.to_hash, response_c.to_hash
|
113
121
|
assert_equal response_a.to_hash, response_d.to_hash
|
114
122
|
end
|
123
|
+
|
115
124
|
def test_should_cache_requests_for_15_mins_by_default
|
116
125
|
GdsApi::JsonClient.cache = nil # cause it to contruct a new cache instance.
|
117
126
|
|
@@ -188,6 +197,103 @@ class JsonClientTest < MiniTest::Spec
|
|
188
197
|
end
|
189
198
|
end
|
190
199
|
|
200
|
+
def test_get_should_follow_permanent_redirect
|
201
|
+
url = "http://some.endpoint/some.json"
|
202
|
+
new_url = "http://some.endpoint/other.json"
|
203
|
+
stub_request(:get, url).to_return(
|
204
|
+
:body => "",
|
205
|
+
:status => 301,
|
206
|
+
:headers => {"Location" => new_url}
|
207
|
+
)
|
208
|
+
stub_request(:get, new_url).to_return(:body => '{"a": 1}', :status => 200)
|
209
|
+
result = @client.get_json(url)
|
210
|
+
assert_equal 1, result.a
|
211
|
+
end
|
212
|
+
|
213
|
+
def test_get_should_follow_found_redirect
|
214
|
+
url = "http://some.endpoint/some.json"
|
215
|
+
new_url = "http://some.endpoint/other.json"
|
216
|
+
stub_request(:get, url).to_return(
|
217
|
+
:body => "",
|
218
|
+
:status => 302,
|
219
|
+
:headers => {"Location" => new_url}
|
220
|
+
)
|
221
|
+
stub_request(:get, new_url).to_return(:body => '{"a": 1}', :status => 200)
|
222
|
+
result = @client.get_json(url)
|
223
|
+
assert_equal 1, result.a
|
224
|
+
end
|
225
|
+
|
226
|
+
def test_get_should_follow_see_other
|
227
|
+
url = "http://some.endpoint/some.json"
|
228
|
+
new_url = "http://some.endpoint/other.json"
|
229
|
+
stub_request(:get, url).to_return(
|
230
|
+
:body => "",
|
231
|
+
:status => 302,
|
232
|
+
:headers => {"Location" => new_url}
|
233
|
+
)
|
234
|
+
stub_request(:get, new_url).to_return(:body => '{"a": 1}', :status => 200)
|
235
|
+
result = @client.get_json(url)
|
236
|
+
assert_equal 1, result.a
|
237
|
+
end
|
238
|
+
|
239
|
+
def test_get_should_follow_temporary_redirect
|
240
|
+
url = "http://some.endpoint/some.json"
|
241
|
+
new_url = "http://some.endpoint/other.json"
|
242
|
+
stub_request(:get, url).to_return(
|
243
|
+
:body => "",
|
244
|
+
:status => 307,
|
245
|
+
:headers => {"Location" => new_url}
|
246
|
+
)
|
247
|
+
stub_request(:get, new_url).to_return(:body => '{"a": 1}', :status => 200)
|
248
|
+
result = @client.get_json(url)
|
249
|
+
assert_equal 1, result.a
|
250
|
+
end
|
251
|
+
|
252
|
+
def test_should_handle_infinite_redirects
|
253
|
+
url = "http://some.endpoint/some.json"
|
254
|
+
redirect = {
|
255
|
+
:body => "",
|
256
|
+
:status => 302,
|
257
|
+
:headers => {"Location" => url}
|
258
|
+
}
|
259
|
+
|
260
|
+
# Theoretically, we could set this up to mock out any number of requests
|
261
|
+
# with a redirect to the same URL, but we'd risk getting the test code into
|
262
|
+
# an infinite loop if the code didn't do what it was supposed to. The
|
263
|
+
# failure response block aborts the test if we have too many requests.
|
264
|
+
failure = lambda { |request| flunk("Request called too many times") }
|
265
|
+
stub_request(:get, url).to_return(redirect).times(11).then.to_return(failure)
|
266
|
+
|
267
|
+
assert_raises GdsApi::TooManyRedirects do
|
268
|
+
@client.get_json(url)
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
def test_should_handle_mutual_redirects
|
273
|
+
first_url = "http://some.endpoint/some.json"
|
274
|
+
second_url = "http://some.endpoint/some-other.json"
|
275
|
+
|
276
|
+
first_redirect = {
|
277
|
+
:body => "",
|
278
|
+
:status => 302,
|
279
|
+
:headers => {"Location" => second_url}
|
280
|
+
}
|
281
|
+
second_redirect = {
|
282
|
+
:body => "",
|
283
|
+
:status => 302,
|
284
|
+
:headers => {"Location" => first_url}
|
285
|
+
}
|
286
|
+
|
287
|
+
# See the comment in the above test for an explanation of this
|
288
|
+
failure = lambda { |request| flunk("Request called too many times") }
|
289
|
+
stub_request(:get, first_url).to_return(first_redirect).times(6).then.to_return(failure)
|
290
|
+
stub_request(:get, second_url).to_return(second_redirect).times(6).then.to_return(failure)
|
291
|
+
|
292
|
+
assert_raises GdsApi::TooManyRedirects do
|
293
|
+
@client.get_json(first_url)
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
191
297
|
def test_post_should_be_nil_if_404_returned_from_endpoint
|
192
298
|
url = "http://some.endpoint/some.json"
|
193
299
|
stub_request(:post, url).to_return(:body => "{}", :status => 404)
|
@@ -202,6 +308,19 @@ class JsonClientTest < MiniTest::Spec
|
|
202
308
|
end
|
203
309
|
end
|
204
310
|
|
311
|
+
def test_post_should_error_on_found_redirect
|
312
|
+
url = "http://some.endpoint/some.json"
|
313
|
+
new_url = "http://some.endpoint/other.json"
|
314
|
+
stub_request(:post, url).to_return(
|
315
|
+
:body => "",
|
316
|
+
:status => 302,
|
317
|
+
:headers => {"Location" => new_url}
|
318
|
+
)
|
319
|
+
assert_raises GdsApi::HTTPErrorResponse do
|
320
|
+
@client.post_json(url, {})
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
205
324
|
def test_put_should_be_nil_if_404_returned_from_endpoint
|
206
325
|
url = "http://some.endpoint/some.json"
|
207
326
|
stub_request(:put, url).to_return(:body => "{}", :status => 404)
|
metadata
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
name: gds-api-adapters
|
3
3
|
version: !ruby/object:Gem::Version
|
4
4
|
prerelease:
|
5
|
-
version: 4.
|
5
|
+
version: 4.3.0
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- James Stewart
|
@@ -10,7 +10,7 @@ autorequire:
|
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
12
|
|
13
|
-
date: 2013-01-
|
13
|
+
date: 2013-01-29 00:00:00 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: plek
|
@@ -57,8 +57,19 @@ dependencies:
|
|
57
57
|
prerelease: false
|
58
58
|
version_requirements: *id004
|
59
59
|
- !ruby/object:Gem::Dependency
|
60
|
-
name:
|
60
|
+
name: rest-client
|
61
61
|
requirement: &id005 !ruby/object:Gem::Requirement
|
62
|
+
none: false
|
63
|
+
requirements:
|
64
|
+
- - ~>
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: 1.6.3
|
67
|
+
type: :runtime
|
68
|
+
prerelease: false
|
69
|
+
version_requirements: *id005
|
70
|
+
- !ruby/object:Gem::Dependency
|
71
|
+
name: rdoc
|
72
|
+
requirement: &id006 !ruby/object:Gem::Requirement
|
62
73
|
none: false
|
63
74
|
requirements:
|
64
75
|
- - "="
|
@@ -66,10 +77,10 @@ dependencies:
|
|
66
77
|
version: "3.12"
|
67
78
|
type: :development
|
68
79
|
prerelease: false
|
69
|
-
version_requirements: *
|
80
|
+
version_requirements: *id006
|
70
81
|
- !ruby/object:Gem::Dependency
|
71
82
|
name: rake
|
72
|
-
requirement: &
|
83
|
+
requirement: &id007 !ruby/object:Gem::Requirement
|
73
84
|
none: false
|
74
85
|
requirements:
|
75
86
|
- - ~>
|
@@ -77,10 +88,10 @@ dependencies:
|
|
77
88
|
version: 0.9.2.2
|
78
89
|
type: :development
|
79
90
|
prerelease: false
|
80
|
-
version_requirements: *
|
91
|
+
version_requirements: *id007
|
81
92
|
- !ruby/object:Gem::Dependency
|
82
93
|
name: webmock
|
83
|
-
requirement: &
|
94
|
+
requirement: &id008 !ruby/object:Gem::Requirement
|
84
95
|
none: false
|
85
96
|
requirements:
|
86
97
|
- - ~>
|
@@ -88,10 +99,10 @@ dependencies:
|
|
88
99
|
version: "1.8"
|
89
100
|
type: :development
|
90
101
|
prerelease: false
|
91
|
-
version_requirements: *
|
102
|
+
version_requirements: *id008
|
92
103
|
- !ruby/object:Gem::Dependency
|
93
104
|
name: mocha
|
94
|
-
requirement: &
|
105
|
+
requirement: &id009 !ruby/object:Gem::Requirement
|
95
106
|
none: false
|
96
107
|
requirements:
|
97
108
|
- - ~>
|
@@ -99,10 +110,10 @@ dependencies:
|
|
99
110
|
version: 0.12.4
|
100
111
|
type: :development
|
101
112
|
prerelease: false
|
102
|
-
version_requirements: *
|
113
|
+
version_requirements: *id009
|
103
114
|
- !ruby/object:Gem::Dependency
|
104
115
|
name: minitest
|
105
|
-
requirement: &
|
116
|
+
requirement: &id010 !ruby/object:Gem::Requirement
|
106
117
|
none: false
|
107
118
|
requirements:
|
108
119
|
- - ~>
|
@@ -110,10 +121,10 @@ dependencies:
|
|
110
121
|
version: 3.4.0
|
111
122
|
type: :development
|
112
123
|
prerelease: false
|
113
|
-
version_requirements: *
|
124
|
+
version_requirements: *id010
|
114
125
|
- !ruby/object:Gem::Dependency
|
115
126
|
name: rack
|
116
|
-
requirement: &
|
127
|
+
requirement: &id011 !ruby/object:Gem::Requirement
|
117
128
|
none: false
|
118
129
|
requirements:
|
119
130
|
- - ">="
|
@@ -121,10 +132,10 @@ dependencies:
|
|
121
132
|
version: "0"
|
122
133
|
type: :development
|
123
134
|
prerelease: false
|
124
|
-
version_requirements: *
|
135
|
+
version_requirements: *id011
|
125
136
|
- !ruby/object:Gem::Dependency
|
126
137
|
name: simplecov
|
127
|
-
requirement: &
|
138
|
+
requirement: &id012 !ruby/object:Gem::Requirement
|
128
139
|
none: false
|
129
140
|
requirements:
|
130
141
|
- - ~>
|
@@ -132,10 +143,10 @@ dependencies:
|
|
132
143
|
version: 0.5.4
|
133
144
|
type: :development
|
134
145
|
prerelease: false
|
135
|
-
version_requirements: *
|
146
|
+
version_requirements: *id012
|
136
147
|
- !ruby/object:Gem::Dependency
|
137
148
|
name: simplecov-rcov
|
138
|
-
requirement: &
|
149
|
+
requirement: &id013 !ruby/object:Gem::Requirement
|
139
150
|
none: false
|
140
151
|
requirements:
|
141
152
|
- - ">="
|
@@ -143,10 +154,10 @@ dependencies:
|
|
143
154
|
version: "0"
|
144
155
|
type: :development
|
145
156
|
prerelease: false
|
146
|
-
version_requirements: *
|
157
|
+
version_requirements: *id013
|
147
158
|
- !ruby/object:Gem::Dependency
|
148
159
|
name: gem_publisher
|
149
|
-
requirement: &
|
160
|
+
requirement: &id014 !ruby/object:Gem::Requirement
|
150
161
|
none: false
|
151
162
|
requirements:
|
152
163
|
- - ~>
|
@@ -154,10 +165,10 @@ dependencies:
|
|
154
165
|
version: 1.1.1
|
155
166
|
type: :development
|
156
167
|
prerelease: false
|
157
|
-
version_requirements: *
|
168
|
+
version_requirements: *id014
|
158
169
|
- !ruby/object:Gem::Dependency
|
159
170
|
name: timecop
|
160
|
-
requirement: &
|
171
|
+
requirement: &id015 !ruby/object:Gem::Requirement
|
161
172
|
none: false
|
162
173
|
requirements:
|
163
174
|
- - ~>
|
@@ -165,7 +176,7 @@ dependencies:
|
|
165
176
|
version: 0.5.1
|
166
177
|
type: :development
|
167
178
|
prerelease: false
|
168
|
-
version_requirements: *
|
179
|
+
version_requirements: *id015
|
169
180
|
description: A set of adapters providing easy access to the GDS GOV.UK APIs
|
170
181
|
email:
|
171
182
|
- jystewart@gmail.com
|
@@ -225,7 +236,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
225
236
|
requirements:
|
226
237
|
- - ">="
|
227
238
|
- !ruby/object:Gem::Version
|
228
|
-
hash:
|
239
|
+
hash: 4441910162548111668
|
229
240
|
segments:
|
230
241
|
- 0
|
231
242
|
version: "0"
|
@@ -234,7 +245,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
234
245
|
requirements:
|
235
246
|
- - ">="
|
236
247
|
- !ruby/object:Gem::Version
|
237
|
-
hash:
|
248
|
+
hash: 4441910162548111668
|
238
249
|
segments:
|
239
250
|
- 0
|
240
251
|
version: "0"
|