td-client 1.0.0-java

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/data/ca-bundle.crt +3448 -0
  3. data/lib/td-client.rb +1 -0
  4. data/lib/td/client.rb +606 -0
  5. data/lib/td/client/api.rb +707 -0
  6. data/lib/td/client/api/access_control.rb +74 -0
  7. data/lib/td/client/api/account.rb +45 -0
  8. data/lib/td/client/api/bulk_import.rb +184 -0
  9. data/lib/td/client/api/bulk_load.rb +172 -0
  10. data/lib/td/client/api/database.rb +50 -0
  11. data/lib/td/client/api/export.rb +38 -0
  12. data/lib/td/client/api/import.rb +38 -0
  13. data/lib/td/client/api/job.rb +390 -0
  14. data/lib/td/client/api/partial_delete.rb +27 -0
  15. data/lib/td/client/api/result.rb +46 -0
  16. data/lib/td/client/api/schedule.rb +120 -0
  17. data/lib/td/client/api/server_status.rb +21 -0
  18. data/lib/td/client/api/table.rb +132 -0
  19. data/lib/td/client/api/user.rb +134 -0
  20. data/lib/td/client/api_error.rb +37 -0
  21. data/lib/td/client/compat_gzip_reader.rb +22 -0
  22. data/lib/td/client/model.rb +816 -0
  23. data/lib/td/client/version.rb +5 -0
  24. data/lib/td/core_ext/openssl/ssl/sslcontext/set_params.rb +18 -0
  25. data/spec/spec_helper.rb +63 -0
  26. data/spec/td/client/access_control_api_spec.rb +37 -0
  27. data/spec/td/client/account_api_spec.rb +34 -0
  28. data/spec/td/client/api_error_spec.rb +77 -0
  29. data/spec/td/client/api_spec.rb +269 -0
  30. data/spec/td/client/api_ssl_connection_spec.rb +109 -0
  31. data/spec/td/client/bulk_import_spec.rb +199 -0
  32. data/spec/td/client/bulk_load_spec.rb +401 -0
  33. data/spec/td/client/db_api_spec.rb +123 -0
  34. data/spec/td/client/export_api_spec.rb +51 -0
  35. data/spec/td/client/import_api_spec.rb +148 -0
  36. data/spec/td/client/job_api_spec.rb +833 -0
  37. data/spec/td/client/model_job_spec.rb +136 -0
  38. data/spec/td/client/model_schedule_spec.rb +26 -0
  39. data/spec/td/client/model_schema_spec.rb +134 -0
  40. data/spec/td/client/partial_delete_api_spec.rb +58 -0
  41. data/spec/td/client/result_api_spec.rb +77 -0
  42. data/spec/td/client/sched_api_spec.rb +109 -0
  43. data/spec/td/client/server_status_api_spec.rb +25 -0
  44. data/spec/td/client/spec_resources.rb +99 -0
  45. data/spec/td/client/table_api_spec.rb +226 -0
  46. data/spec/td/client/user_api_spec.rb +118 -0
  47. data/spec/td/client_sched_spec.rb +79 -0
  48. data/spec/td/client_spec.rb +46 -0
  49. metadata +271 -0
@@ -0,0 +1,136 @@
1
+ require 'spec_helper'
2
+ require 'td/client/spec_resources'
3
+
4
+ describe 'Job Model' do
5
+ include_context 'spec symbols'
6
+ include_context 'common helper'
7
+ include_context 'job resources'
8
+
9
+ before do
10
+ stub_api_request(:post, "/v3/user/authenticate").
11
+ to_return(:body => {'apikey' => 'apikey'}.to_json)
12
+ end
13
+
14
+ describe '#client' do
15
+ subject do
16
+ Job.new(client, *arguments).client
17
+ end
18
+
19
+ let :client do
20
+ Client.authenticate('user', 'password')
21
+ end
22
+
23
+ let :arguments do
24
+ job_attributes = raw_jobs.first
25
+ [
26
+ 'job_id', 'type', 'query', 'status', 'url', 'debug',
27
+ 'start_at', 'end_at', 'cpu_time', 'result_size', 'result', 'result_url',
28
+ 'hive_result_schema', 'priority', 'retry_limit', 'org_name', 'db_name',
29
+ 'duration', 'num_records'
30
+ ].map {|name| job_attributes[name]}
31
+ end
32
+
33
+ it 'returns Job object having client' do
34
+ expect(subject).to eq client
35
+ end
36
+ end
37
+
38
+ describe '#result_raw' do
39
+ let(:client) { Client.authenticate('user', 'password') }
40
+ let(:job_id) { 12345678 }
41
+ let(:job) { Job.new(client, job_id, nil, nil) }
42
+ let(:format) { 'json' }
43
+ let(:io) { StringIO.new }
44
+
45
+ context 'not finished?' do
46
+ before { allow(job).to receive(:finished?) { false } }
47
+
48
+ it 'do not call #job_result_raw' do
49
+ expect(client).not_to receive(:job_result_raw)
50
+
51
+ expect(job.result_raw(format, io)).to_not be
52
+ end
53
+ end
54
+
55
+ context 'finished?' do
56
+ before { allow(job).to receive(:finished?) { true } }
57
+
58
+ it 'call #job_result_raw' do
59
+ expect(client).to receive(:job_result_raw).with(job_id, format, io)
60
+
61
+ job.result_raw(format, io)
62
+ end
63
+ end
64
+ end
65
+
66
+ describe '#wait' do
67
+ let(:client) { Client.authenticate('user', 'password') }
68
+ let(:job_id) { 12345678 }
69
+ let(:job) { Job.new(client, job_id, nil, nil) }
70
+
71
+ def change_job_status(status)
72
+ allow(client).to receive(:job_status).with(job_id).and_return(status)
73
+ end
74
+
75
+ before do
76
+ change_job_status(Job::STATUS_QUEUED)
77
+ end
78
+
79
+ context 'without timeout' do
80
+ it 'waits the job to be finished' do
81
+ begin
82
+ thread = Thread.start { job.wait }
83
+ expect(thread).to be_alive
84
+ change_job_status(Job::STATUS_SUCCESS)
85
+ thread.join(1)
86
+ expect(thread).to be_stop
87
+ ensure
88
+ thread.kill # just in case
89
+ end
90
+ end
91
+
92
+ it 'calls a given block in every wait_interval second' do
93
+ now = 1_400_000_000
94
+ allow(self).to receive(:sleep){|arg| now += arg }
95
+ allow(Process).to receive(:clock_gettime){ now }
96
+ expect { |b|
97
+ begin
98
+ thread = Thread.start {
99
+ job.wait(nil, 2, &b)
100
+ }
101
+ sleep 6
102
+ change_job_status(Job::STATUS_SUCCESS)
103
+ thread.join(1)
104
+ expect(thread).to be_stop
105
+ ensure
106
+ thread.kill # just in case
107
+ end
108
+ }.to yield_control.at_least(2).at_most(3).times
109
+ end
110
+ end
111
+
112
+ context 'with timeout' do
113
+ context 'the job running time is too long' do
114
+ it 'raise Timeout::Error' do
115
+ expect {
116
+ job.wait(0.1)
117
+ }.to raise_error(Timeout::Error)
118
+ end
119
+ end
120
+
121
+ it 'calls a given block in every wait_interval second, and timeout' do
122
+ expect { |b|
123
+ begin
124
+ thread = Thread.start {
125
+ job.wait(0.3, 0.1, &b)
126
+ }
127
+ expect{ thread.value }.to raise_error(Timeout::Error)
128
+ expect(thread).to be_stop
129
+ ensure
130
+ thread.kill # just in case
131
+ end
132
+ }.to yield_control.at_least(2).times
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,26 @@
1
+ require 'spec_helper'
2
+ require 'td/client/spec_resources'
3
+
4
+ describe 'Schedule Model' do
5
+ describe '#run' do
6
+ let(:api_key) { '1234567890abcd' }
7
+ let(:api) { double(:api) }
8
+ let(:client) { Client.new(api_key) }
9
+ let(:name) { 'schedule' }
10
+ let(:schedule) {
11
+ Schedule.new(client, name, '0 0 * * * *', 'select 1')
12
+ }
13
+ let(:time) { "2013-01-01 00:00:00" }
14
+ let(:num) { 1 }
15
+
16
+ before do
17
+ allow(API).to receive(:new).with(api_key, {}).and_return(api)
18
+ end
19
+
20
+ it 'success call api' do
21
+ expect(api).to receive(:run_schedule).with(name, time, num).and_return([])
22
+
23
+ schedule.run(time, num)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,134 @@
1
+ require 'spec_helper'
2
+ require 'td/client/spec_resources'
3
+
4
+ describe 'TreasureData::Schema::Field' do
5
+ describe '.new' do
6
+ context 'name="v"' do
7
+ it 'raises ParameterValidationError' do
8
+ expect{ Schema::Field.new('v', 'int') }.to raise_error(ParameterValidationError)
9
+ end
10
+ end
11
+ context 'name="time"' do
12
+ it 'raises ParameterValidationError' do
13
+ expect{ Schema::Field.new('time', 'int') }.to raise_error(ParameterValidationError)
14
+ end
15
+ end
16
+ context 'name with UTF-8' do
17
+ it 'works' do
18
+ name = "\u3042\u3044\u3046"
19
+ f = Schema::Field.new(name, 'int')
20
+ expect(f.name).to eq name
21
+ expect(f.type).to eq 'int'
22
+ expect(f.sql_alias).to be_nil
23
+ end
24
+ end
25
+ context 'with sql_alias' do
26
+ it 'raises' do
27
+ f = Schema::Field.new('t:t', 'int', 'alice')
28
+ expect(f.name).to eq 't:t'
29
+ expect(f.type).to eq 'int'
30
+ expect(f.sql_alias).to eq 'alice'
31
+ end
32
+ end
33
+ context 'with invalid sql_alias' do
34
+ it 'raises' do
35
+ expect{ Schema::Field.new('t:t', 'int', 't:t') }.to raise_error(ParameterValidationError)
36
+ end
37
+ end
38
+ end
39
+ end
40
+
41
+ describe 'TreasureData::Schema' do
42
+ describe '.parse' do
43
+ let(:columns){ ["foo:int", "BAR\u3070\u30FC:string@bar", "baz:baz!:array<double>@baz"] }
44
+ it do
45
+ sc = Schema.parse(columns)
46
+ expect(sc.fields.size).to eq 3
47
+ expect(sc.fields[0].name).to eq 'foo'
48
+ expect(sc.fields[0].type).to eq 'int'
49
+ expect(sc.fields[0].sql_alias).to be_nil
50
+ expect(sc.fields[1].name).to eq "BAR\u3070\u30FC"
51
+ expect(sc.fields[1].type).to eq 'string'
52
+ expect(sc.fields[1].sql_alias).to eq 'bar'
53
+ expect(sc.fields[2].name).to eq 'baz:baz!'
54
+ expect(sc.fields[2].type).to eq 'array<double>'
55
+ expect(sc.fields[2].sql_alias).to eq 'baz'
56
+ end
57
+ end
58
+
59
+ describe '.new' do
60
+ it do
61
+ f = Schema::Field.new('a', 'int')
62
+ sc = Schema.new([f])
63
+ expect(sc.fields[0]).to eq f
64
+ end
65
+ end
66
+
67
+ describe '#fields' do
68
+ it do
69
+ f = Schema::Field.new('a', 'int')
70
+ sc = Schema.new([f])
71
+ expect(sc.fields[0]).to eq f
72
+ end
73
+ end
74
+
75
+ describe '#add_field' do
76
+ it do
77
+ f = Schema::Field.new('a', 'int')
78
+ sc = Schema.new([f])
79
+ sc.add_field('b', 'double', 'bb')
80
+ expect(sc.fields[1].name).to eq 'b'
81
+ end
82
+ it 'raises ParameterValidationError if name is duplicated' do
83
+ f = Schema::Field.new('a', 'int')
84
+ sc = Schema.new([f])
85
+ expect{ sc.add_field('a', 'double') }.to raise_error(ParameterValidationError)
86
+ end
87
+ it 'raises ParameterValidationError if sql_alias is duplicated' do
88
+ f = Schema::Field.new('a', 'int')
89
+ sc = Schema.new([f])
90
+ expect{ sc.add_field('abc', 'double', 'a') }.to raise_error(ParameterValidationError)
91
+ end
92
+ end
93
+
94
+ describe '#merge' do
95
+ it do
96
+ sc1 = Schema.parse(['foo:int', 'bar:float'])
97
+ sc2 = Schema.parse(['bar:double', 'baz:string'])
98
+ sc3 = sc1.merge(sc2)
99
+ expect(sc3.fields.size).to eq 3
100
+ expect(sc3.fields[0].name).to eq 'foo'
101
+ expect(sc3.fields[0].type).to eq 'int'
102
+ expect(sc3.fields[1].name).to eq 'bar'
103
+ expect(sc3.fields[1].type).to eq 'double'
104
+ expect(sc3.fields[2].name).to eq 'baz'
105
+ expect(sc3.fields[2].type).to eq 'string'
106
+ end
107
+ it do
108
+ sc1 = Schema.parse(['foo:int', 'bar:float'])
109
+ sc2 = Schema.parse(['bar:double@foo'])
110
+ expect{ sc1.merge(sc2) }.to raise_error(ArgumentError)
111
+ end
112
+ end
113
+
114
+ describe '#to_json' do
115
+ it do
116
+ sc = Schema.parse(['foo:int', 'bar:float@baz'])
117
+ expect(sc.to_json).to eq '[["foo","int"],["bar","float","baz"]]'
118
+ end
119
+ end
120
+
121
+ describe '#from_json' do
122
+ it do
123
+ sc = Schema.new
124
+ sc.from_json [["foo","int"],["bar","float","baz"]]
125
+ expect(sc.fields.size).to eq 2
126
+ expect(sc.fields[0].name).to eq 'foo'
127
+ expect(sc.fields[0].type).to eq 'int'
128
+ expect(sc.fields[0].sql_alias).to be_nil
129
+ expect(sc.fields[1].name).to eq 'bar'
130
+ expect(sc.fields[1].type).to eq 'float'
131
+ expect(sc.fields[1].sql_alias).to eq 'baz'
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,58 @@
1
+ require 'spec_helper'
2
+ require 'td/client/spec_resources'
3
+
4
+ describe 'PartialDelete API' do
5
+ include_context 'spec symbols'
6
+ include_context 'common helper'
7
+
8
+ let :api do
9
+ API.new(nil)
10
+ end
11
+
12
+ describe 'partialdelete' do
13
+ let :from do
14
+ 0
15
+ end
16
+
17
+ let :to do
18
+ 3600 * 10
19
+ end
20
+
21
+ let :from_to do
22
+ {'from' => from.to_s, 'to' => to.to_s}
23
+ end
24
+
25
+ it 'should partial_delete successfully' do
26
+ # TODO: Use correnty values
27
+ stub_api_request(:post, "/v3/table/partialdelete/#{e(db_name)}/#{e(table_name)}").with(:body => from_to).
28
+ to_return(:body => {'database' => db_name, 'table' => table_name, 'job_id' => '1'}.to_json)
29
+
30
+ expect(api.partial_delete(db_name, table_name, to, from)).to eq('1')
31
+ end
32
+
33
+ it 'should return 404 error with non exist database name' do
34
+ db = 'no_such_db'
35
+ err_msg = "Couldn't find UserDatabase with name = #{db}"
36
+ stub_api_request(:post, "/v3/table/partialdelete/#{e(db)}/#{e(table_name)}").with(:body => from_to).
37
+ to_return(:status => 404, :body => {'message' => err_msg}.to_json)
38
+
39
+ expect {
40
+ api.partial_delete(db, table_name, to, from)
41
+ }.to raise_error(TreasureData::APIError, /#{err_msg}/)
42
+ end
43
+
44
+ it 'should return 404 error with non exist table name' do
45
+ table = 'no_such_table'
46
+ err_msg = "Unknown table: #{table}"
47
+ stub_api_request(:post, "/v3/table/partialdelete/#{e(db_name)}/#{e(table)}").with(:body => from_to).
48
+ to_return(:status => 404, :body => {'message' => err_msg}.to_json)
49
+
50
+ expect {
51
+ api.partial_delete(db_name, table, to, from)
52
+ }.to raise_error(TreasureData::APIError, /#{err_msg}/)
53
+ end
54
+
55
+ # TODO: Add from / to parameters spec
56
+ end
57
+ end
58
+
@@ -0,0 +1,77 @@
1
+ require 'spec_helper'
2
+ require 'td/client/spec_resources'
3
+
4
+ describe 'Result API' do
5
+ include_context 'spec symbols'
6
+ include_context 'common helper'
7
+
8
+ let :api do
9
+ API.new(nil)
10
+ end
11
+
12
+ describe 'create_result' do
13
+ it 'should create a new result' do
14
+ params = {'url' => result_url}
15
+ stub_api_request(:post, "/v3/result/create/#{e(result_name)}").with(:body => params).to_return(:body => {'result' => result_name}.to_json)
16
+
17
+ expect(api.create_result(result_name, result_url)).to be true
18
+ end
19
+
20
+ it 'should return 422 error with invalid name' do
21
+ name = '1'
22
+ params = {'url' => result_url}
23
+ err_msg = "Validation failed: Name is too short" # " (minimum is 3 characters)"
24
+ stub_api_request(:post, "/v3/result/create/#{e(name)}").with(:body => params).
25
+ to_return(:status => 422, :body => {'message' => err_msg}.to_json)
26
+
27
+ expect {
28
+ api.create_result(name, result_url)
29
+ }.to raise_error(TreasureData::APIError, /#{err_msg}/)
30
+ end
31
+
32
+ it 'should return 422 error without url' do
33
+ params = {'url' => 'false'} # I want to use nil, but nil doesn't work on WebMock...
34
+ err_msg = "'url' parameter is required"
35
+ stub_api_request(:post, "/v3/result/create/#{e(result_name)}").with(:body => params).
36
+ to_return(:status => 422, :body => {'message' => err_msg}.to_json)
37
+
38
+ expect {
39
+ api.create_result(result_name, false)
40
+ }.to raise_error(TreasureData::APIError, /#{err_msg}/)
41
+ end
42
+
43
+ it 'should return 409 error with duplicated name' do
44
+ params = {'url' => result_url}
45
+ err_msg = "Result must be unique"
46
+ stub_api_request(:post, "/v3/result/create/#{e(result_name)}").with(:body => params).
47
+ to_return(:status => 409, :body => {'message' => err_msg}.to_json)
48
+
49
+ expect {
50
+ api.create_result(result_name, result_url)
51
+ }.to raise_error(TreasureData::APIError, /#{err_msg}/)
52
+ end
53
+ end
54
+
55
+ describe 'list_result' do
56
+ it 'should return name and url' do
57
+ stub_api_request(:get, '/v3/result/list').
58
+ to_return(:body => {'results' => [{'name' => 'name', 'url' => 'url'}]}.to_json)
59
+ expect(api.list_result).to eq([['name', 'url', nil]])
60
+ end
61
+ end
62
+
63
+ describe 'delete_result' do
64
+ it 'should delete the result' do
65
+ stub_api_request(:post, "/v3/result/delete/#{e(result_name)}")
66
+ expect(api.delete_result(result_name)).to eq(true)
67
+ end
68
+
69
+ it 'should raise error' do
70
+ stub_api_request(:post, "/v3/result/delete/#{e(result_name)}").
71
+ to_return(:status => 404)
72
+ expect {
73
+ api.delete_result(result_name)
74
+ }.to raise_error(TreasureData::APIError)
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,109 @@
1
+ require 'spec_helper'
2
+ require 'td/client/spec_resources'
3
+
4
+ describe 'Schedule API' do
5
+ include_context 'spec symbols'
6
+ include_context 'common helper'
7
+
8
+ let :api do
9
+ API.new(nil)
10
+ end
11
+
12
+ describe 'create_schedule' do
13
+ let :opts do
14
+ {'cron' => cron, 'query' => query, 'database' => db_name}
15
+ end
16
+
17
+ it 'should create a new schedule' do
18
+ start = Time.now
19
+ stub_api_request(:post, "/v3/schedule/create/#{e(sched_name)}").
20
+ with(:body => opts.merge('type' => 'hive')).
21
+ to_return(:body => {'name' => sched_name, 'start' => start.to_s}.to_json)
22
+
23
+ expect(api.create_schedule(sched_name, opts.merge('type' => 'hive'))).to eq(start.to_s)
24
+ end
25
+
26
+ it 'should create a dummy schedule' do
27
+ stub_api_request(:post, "/v3/schedule/create/#{e(sched_name)}").
28
+ with(:body => opts.merge('type' => 'hive')).
29
+ to_return(:body => {'name' => sched_name, 'start' => nil}.to_json)
30
+
31
+ expect(api.create_schedule(sched_name, opts.merge('type' => 'hive'))).to be_nil
32
+ end
33
+
34
+ it 'should return 422 error with invalid name' do
35
+ name = '1'
36
+ err_msg = "Validation failed: Name is too short" # " (minimum is 3 characters)"
37
+ stub_api_request(:post, "/v3/schedule/create/#{e(name)}").
38
+ with(:body => opts.merge('type' => 'hive')).
39
+ to_return(:status => 422, :body => {'message' => err_msg}.to_json)
40
+
41
+ expect {
42
+ api.create_schedule(name, opts.merge('type' => 'hive'))
43
+ }.to raise_error(TreasureData::APIError, /#{err_msg}/)
44
+ end
45
+ end
46
+
47
+ describe 'delete_schedule' do
48
+ it 'should delete the schedule' do
49
+ stub_api_request(:post, "/v3/schedule/delete/#{e(sched_name)}").
50
+ to_return(:body => {'cron' => 'cron', 'query' => 'query'}.to_json)
51
+ expect(api.delete_schedule(sched_name)).to eq(['cron', 'query'])
52
+ end
53
+ end
54
+
55
+ describe 'update_schedule' do
56
+ let :pig_query do
57
+ "OUT = FOREACH (GROUP plt364 ALL) GENERATE COUNT(plt364);\n" * 200
58
+ end
59
+ let :opts do
60
+ {'cron' => cron, 'query' => pig_query, 'database' => db_name}
61
+ end
62
+
63
+ it 'should not return 414 even if the query text is very long' do
64
+ stub_api_request(:post, "/v3/schedule/update/#{e(sched_name)}").
65
+ with(:body => opts.merge('type' => 'pig')).
66
+ to_return(:body => {'name' => sched_name, 'query' => pig_query}.to_json)
67
+
68
+ expect {
69
+ api.update_schedule(sched_name, opts.merge('type' => 'pig'))
70
+ }.not_to raise_error
71
+ end
72
+
73
+ it 'should update the schedule with the new query' do
74
+ stub_api_request(:post, "/v3/schedule/update/#{e(sched_name)}").
75
+ with(:body => opts.merge('type' => 'pig')).
76
+ to_return(:body => {'name' => sched_name, 'query' => pig_query}.to_json)
77
+
78
+ stub_api_request(:get, "/v3/schedule/list").
79
+ to_return(:body => {'schedules' => [{'name' => sched_name, 'query' => pig_query}]}.to_json)
80
+
81
+ expect(api.list_schedules.first[2]).to eq(pig_query)
82
+ end
83
+ end
84
+
85
+ describe 'history' do
86
+ let :history do
87
+ ['history', 'job_id', 'type', 'database', 'status', 'query', 'start_at', 'end_at', 'result', 'priority'].inject({}) { |r, e|
88
+ r[e] = e
89
+ r
90
+ }
91
+ end
92
+
93
+ it 'should return history records' do
94
+ stub_api_request(:get, "/v3/schedule/history/#{e(sched_name)}").
95
+ with(:query => {'from' => 0, 'to' => 100}).
96
+ to_return(:body => {'history' => [history]}.to_json)
97
+ expect(api.history(sched_name, 0, 100)).to eq([[nil, 'job_id', :type, 'status', 'query', 'start_at', 'end_at', 'result', 'priority', 'database']])
98
+ end
99
+ end
100
+
101
+ describe 'run_schedule' do
102
+ it 'should return history records' do
103
+ stub_api_request(:post, "/v3/schedule/run/#{e(sched_name)}/123456789").
104
+ with(:body => {'num' => '5'}).
105
+ to_return(:body => {'jobs' => [{'job_id' => 'job_id', 'scheduled_at' => 'scheduled_at', 'type' => 'type'}]}.to_json)
106
+ expect(api.run_schedule(sched_name, 123456789, 5)).to eq([['job_id', :type, 'scheduled_at']])
107
+ end
108
+ end
109
+ end