td-client 0.9.0dev2 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/lib/td/client.rb +16 -8
  3. data/lib/td/client/api.rb +66 -47
  4. data/lib/td/client/api/bulk_import.rb +1 -2
  5. data/lib/td/client/api/bulk_load.rb +3 -3
  6. data/lib/td/client/api/export.rb +12 -0
  7. data/lib/td/client/api/import.rb +3 -2
  8. data/lib/td/client/api/job.rb +146 -71
  9. data/lib/td/client/api/schedule.rb +1 -1
  10. data/lib/td/client/api_error.rb +5 -0
  11. data/lib/td/client/model.rb +92 -28
  12. data/lib/td/client/version.rb +1 -1
  13. data/spec/spec_helper.rb +5 -5
  14. data/spec/td/client/account_api_spec.rb +5 -5
  15. data/spec/td/client/api_error_spec.rb +77 -0
  16. data/spec/td/client/api_spec.rb +76 -52
  17. data/spec/td/client/api_ssl_connection_spec.rb +1 -1
  18. data/spec/td/client/bulk_import_spec.rb +28 -29
  19. data/spec/td/client/bulk_load_spec.rb +60 -35
  20. data/spec/td/client/db_api_spec.rb +1 -1
  21. data/spec/td/client/export_api_spec.rb +11 -1
  22. data/spec/td/client/import_api_spec.rb +85 -10
  23. data/spec/td/client/job_api_spec.rb +568 -61
  24. data/spec/td/client/model_job_spec.rb +27 -10
  25. data/spec/td/client/model_schedule_spec.rb +2 -2
  26. data/spec/td/client/model_schema_spec.rb +134 -0
  27. data/spec/td/client/partial_delete_api_spec.rb +1 -1
  28. data/spec/td/client/result_api_spec.rb +3 -3
  29. data/spec/td/client/sched_api_spec.rb +12 -4
  30. data/spec/td/client/server_status_api_spec.rb +2 -2
  31. data/spec/td/client/spec_resources.rb +1 -0
  32. data/spec/td/client/table_api_spec.rb +14 -14
  33. data/spec/td/client/user_api_spec.rb +12 -12
  34. data/spec/td/client_sched_spec.rb +31 -6
  35. data/spec/td/client_spec.rb +1 -0
  36. metadata +42 -81
@@ -31,7 +31,7 @@ describe 'API SSL connection' do
31
31
  DIR = File.dirname(File.expand_path(__FILE__))
32
32
 
33
33
  after :each do
34
- @server && @server.shutdown
34
+ @server.shutdown if @server
35
35
  end
36
36
 
37
37
  it 'should fail to connect SSLv3 only server' do
@@ -14,25 +14,24 @@ describe 'BulkImport API' do
14
14
 
15
15
  let :packed do
16
16
  s = StringIO.new
17
- pk = MessagePack::Packer.new(s)
18
- pk.write([1, '2', 3.0])
19
- pk.write([4, '5', 6.0])
20
- pk.write([7, '8', 9.0])
21
- pk.flush
22
- s.rewind
23
- out = StringIO.new
24
- Zlib::GzipWriter.wrap(out) do |f|
25
- f.write s.read
26
- end
27
- out.string
17
+ Zlib::GzipWriter.wrap(s) do |f|
18
+ pk = MessagePack::Packer.new(f)
19
+ pk.write([1, '2', 3.0])
20
+ pk.write([4, '5', 6.0])
21
+ pk.write([7, '8', 9.0])
22
+ pk.flush
23
+ end
24
+ s.string
28
25
  end
29
26
 
27
+ let(:endpoint_domain) { TreasureData::API::DEFAULT_IMPORT_ENDPOINT }
28
+
30
29
  describe 'create_bulk_import' do
31
30
  it 'should create a new bulk_import' do
32
31
  stub_api_request(:post, "/v3/bulk_import/create/#{e(bi_name)}/#{e(db_name)}/#{e(table_name)}").
33
32
  to_return(:body => {'bulk_import' => bi_name}.to_json)
34
33
 
35
- api.create_bulk_import(bi_name, db_name, table_name).should be_nil
34
+ expect(api.create_bulk_import(bi_name, db_name, table_name)).to be_nil
36
35
  end
37
36
 
38
37
  it 'should return 422 error with invalid name' do
@@ -73,7 +72,7 @@ describe 'BulkImport API' do
73
72
  it 'runs' do
74
73
  stub_api_request(:post, '/v3/bulk_import/delete/name').
75
74
  with(:body => 'foo=bar')
76
- api.delete_bulk_import('name', 'foo' => 'bar').should == nil
75
+ expect(api.delete_bulk_import('name', 'foo' => 'bar')).to eq(nil)
77
76
  end
78
77
  end
79
78
 
@@ -81,7 +80,7 @@ describe 'BulkImport API' do
81
80
  it 'runs' do
82
81
  stub_api_request(:get, '/v3/bulk_import/show/name').
83
82
  to_return(:body => {'status' => 'status', 'other' => 'other'}.to_json)
84
- api.show_bulk_import('name')['status'].should == 'status'
83
+ expect(api.show_bulk_import('name')['status']).to eq('status')
85
84
  end
86
85
  end
87
86
 
@@ -90,7 +89,7 @@ describe 'BulkImport API' do
90
89
  stub_api_request(:get, '/v3/bulk_import/list').
91
90
  with(:query => 'foo=bar').
92
91
  to_return(:body => {'bulk_imports' => %w(1 2 3)}.to_json)
93
- api.list_bulk_imports('foo' => 'bar').should == %w(1 2 3)
92
+ expect(api.list_bulk_imports('foo' => 'bar')).to eq(%w(1 2 3))
94
93
  end
95
94
  end
96
95
 
@@ -99,7 +98,7 @@ describe 'BulkImport API' do
99
98
  stub_api_request(:get, '/v3/bulk_import/list_parts/name').
100
99
  with(:query => 'foo=bar').
101
100
  to_return(:body => {'parts' => %w(1 2 3)}.to_json)
102
- api.list_bulk_import_parts('name', 'foo' => 'bar').should == %w(1 2 3)
101
+ expect(api.list_bulk_import_parts('name', 'foo' => 'bar')).to eq(%w(1 2 3))
103
102
  end
104
103
  end
105
104
 
@@ -109,10 +108,10 @@ describe 'BulkImport API' do
109
108
  File.open(t.path, 'w') do |f|
110
109
  f << '12345'
111
110
  end
112
- stub_request(:put, 'http://api.treasure-data.com/v3/bulk_import/upload_part/name/part').
111
+ stub_request(:put, "https://#{TreasureData::API::DEFAULT_ENDPOINT}/v3/bulk_import/upload_part/name/part").
113
112
  with(:body => '12345')
114
113
  File.open(t.path) do |f|
115
- api.bulk_import_upload_part('name', 'part', f, 5).should == nil
114
+ expect(api.bulk_import_upload_part('name', 'part', f, 5)).to eq(nil)
116
115
  end
117
116
  end
118
117
 
@@ -122,10 +121,10 @@ describe 'BulkImport API' do
122
121
  File.open(t.path, 'w') do |f|
123
122
  f << '12345'
124
123
  end
125
- stub_request(:put, 'http://api.treasure-data.com/v3/bulk_import/upload_part/name/' + CGI.escape('日本語(Japanese)'.encode('UTF-8'))).
124
+ stub_request(:put, "https://#{TreasureData::API::DEFAULT_ENDPOINT}/v3/bulk_import/upload_part/name/" + CGI.escape('日本語(Japanese)'.encode('UTF-8'))).
126
125
  with(:body => '12345')
127
126
  File.open(t.path) do |f|
128
- api.bulk_import_upload_part('name', '日本語(Japanese)'.encode('Windows-31J'), f, 5).should == nil
127
+ expect(api.bulk_import_upload_part('name', '日本語(Japanese)'.encode('Windows-31J'), f, 5)).to eq(nil)
129
128
  end
130
129
  end
131
130
  end
@@ -134,21 +133,21 @@ describe 'BulkImport API' do
134
133
  describe 'bulk_import_delete_part' do
135
134
  it 'runs' do
136
135
  stub_api_request(:post, '/v3/bulk_import/delete_part/name/part')
137
- api.bulk_import_delete_part('name', 'part').should == nil
136
+ expect(api.bulk_import_delete_part('name', 'part')).to eq(nil)
138
137
  end
139
138
  end
140
139
 
141
140
  describe 'freeze_bulk_import' do
142
141
  it 'runs' do
143
142
  stub_api_request(:post, '/v3/bulk_import/freeze/name')
144
- api.freeze_bulk_import('name').should == nil
143
+ expect(api.freeze_bulk_import('name')).to eq(nil)
145
144
  end
146
145
  end
147
146
 
148
147
  describe 'unfreeze_bulk_import' do
149
148
  it 'runs' do
150
149
  stub_api_request(:post, '/v3/bulk_import/unfreeze/name')
151
- api.unfreeze_bulk_import('name').should == nil
150
+ expect(api.unfreeze_bulk_import('name')).to eq(nil)
152
151
  end
153
152
  end
154
153
 
@@ -156,7 +155,7 @@ describe 'BulkImport API' do
156
155
  it 'runs' do
157
156
  stub_api_request(:post, '/v3/bulk_import/perform/name').
158
157
  to_return(:body => {'job_id' => 12345}.to_json)
159
- api.perform_bulk_import('name').should == '12345'
158
+ expect(api.perform_bulk_import('name')).to eq('12345')
160
159
  end
161
160
  end
162
161
 
@@ -164,7 +163,7 @@ describe 'BulkImport API' do
164
163
  it 'runs' do
165
164
  stub_api_request(:post, '/v3/bulk_import/commit/name').
166
165
  to_return(:body => {'job_id' => 12345}.to_json)
167
- api.commit_bulk_import('name').should == nil
166
+ expect(api.commit_bulk_import('name')).to eq(nil)
168
167
  end
169
168
  end
170
169
 
@@ -172,19 +171,19 @@ describe 'BulkImport API' do
172
171
  it 'returns [] on empty' do
173
172
  stub_api_request(:get, '/v3/bulk_import/error_records/name').
174
173
  to_return(:body => '')
175
- api.bulk_import_error_records('name').should == []
174
+ expect(api.bulk_import_error_records('name')).to eq([])
176
175
  end
177
176
 
178
177
  it 'returns nil on empty if block given' do
179
178
  stub_api_request(:get, '/v3/bulk_import/error_records/name').
180
179
  to_return(:body => '')
181
- api.bulk_import_error_records('name'){}.should == nil
180
+ expect(api.bulk_import_error_records('name'){}).to eq(nil)
182
181
  end
183
182
 
184
183
  it 'returns unpacked result' do
185
184
  stub_api_request(:get, '/v3/bulk_import/error_records/name').
186
185
  to_return(:body => packed)
187
- api.bulk_import_error_records('name').should == [[1, '2', 3.0], [4, '5', 6.0], [7, '8', 9.0]]
186
+ expect(api.bulk_import_error_records('name')).to eq([[1, '2', 3.0], [4, '5', 6.0], [7, '8', 9.0]])
188
187
  end
189
188
 
190
189
  it 'yields unpacked result if block given' do
@@ -194,7 +193,7 @@ describe 'BulkImport API' do
194
193
  api.bulk_import_error_records('name') do |row|
195
194
  result << row
196
195
  end
197
- result.should == [[1, '2', 3.0], [4, '5', 6.0], [7, '8', 9.0]]
196
+ expect(result).to eq([[1, '2', 3.0], [4, '5', 6.0], [7, '8', 9.0]])
198
197
  end
199
198
  end
200
199
  end
@@ -113,9 +113,9 @@ describe 'BulkImport API' do
113
113
  stub_api_request(:post, '/v3/bulk_loads/guess').
114
114
  with(:body => original_config.to_json).
115
115
  to_return(:body => guessed_config.to_json)
116
- api.bulk_load_guess(
116
+ expect(api.bulk_load_guess(
117
117
  original_config
118
- ).should == guessed_config
118
+ )).to eq(guessed_config)
119
119
  end
120
120
 
121
121
  it 'raises an error' do
@@ -134,23 +134,23 @@ describe 'BulkImport API' do
134
134
  with(:body => original_config.to_json).
135
135
  to_return(:status => 500, :body => guessed_config.to_json)
136
136
  begin
137
- retry_api.bulk_load_guess(
137
+ expect(retry_api.bulk_load_guess(
138
138
  original_config
139
- ).should != nil
139
+ )).to != nil
140
140
  rescue TreasureData::APIError => e
141
- e.message.should =~ /^500: BulkLoad configuration guess failed/
141
+ expect(e.message).to match(/^500: BulkLoad configuration guess failed/)
142
142
  end
143
143
  end
144
144
 
145
145
  it 'perform retries on connection failure' do
146
146
  api = retry_api
147
- api.instance_eval { @api }.stub(:post).and_raise(SocketError.new('>>'))
147
+ allow(api.instance_eval { @api }).to receive(:post).and_raise(SocketError.new('>>'))
148
148
  begin
149
149
  retry_api.bulk_load_guess(
150
150
  original_config
151
151
  )
152
152
  rescue SocketError => e
153
- e.message.should == '>> (Retried 1 times in 1 seconds)'
153
+ expect(e.message).to eq('>> (Retried 1 times in 1 seconds)')
154
154
  end
155
155
  end
156
156
  end
@@ -160,9 +160,9 @@ describe 'BulkImport API' do
160
160
  stub_api_request(:post, '/v3/bulk_loads/guess').
161
161
  with(:body => original_config.to_json).
162
162
  to_return(:body => guessed_config.to_json)
163
- api.bulk_load_guess(
163
+ expect(api.bulk_load_guess(
164
164
  original_config
165
- ).should == guessed_config
165
+ )).to eq(guessed_config)
166
166
  end
167
167
 
168
168
  it 'raises an error' do
@@ -181,23 +181,23 @@ describe 'BulkImport API' do
181
181
  with(:body => original_config.to_json).
182
182
  to_return(:status => 500, :body => guessed_config.to_json)
183
183
  begin
184
- retry_api.bulk_load_guess(
184
+ expect(retry_api.bulk_load_guess(
185
185
  original_config
186
- ).should != nil
186
+ )).to != nil
187
187
  rescue TreasureData::APIError => e
188
- e.message.should =~ /^500: BulkLoad configuration guess failed/
188
+ expect(e.message).to match(/^500: BulkLoad configuration guess failed/)
189
189
  end
190
190
  end
191
191
 
192
192
  it 'perform retries on connection failure' do
193
193
  api = retry_api
194
- api.instance_eval { @api }.stub(:post).and_raise(SocketError.new('>>'))
194
+ allow(api.instance_eval { @api }).to receive(:post).and_raise(SocketError.new('>>'))
195
195
  begin
196
196
  retry_api.bulk_load_guess(
197
197
  original_config
198
198
  )
199
199
  rescue SocketError => e
200
- e.message.should == '>> (Retried 1 times in 1 seconds)'
200
+ expect(e.message).to eq('>> (Retried 1 times in 1 seconds)')
201
201
  end
202
202
  end
203
203
  end
@@ -207,9 +207,9 @@ describe 'BulkImport API' do
207
207
  stub_api_request(:post, '/v3/bulk_loads/preview').
208
208
  with(:body => guessed_config.to_json).
209
209
  to_return(:body => preview_result.to_json)
210
- api.bulk_load_preview(
210
+ expect(api.bulk_load_preview(
211
211
  guessed_config
212
- ).should == preview_result
212
+ )).to eq(preview_result)
213
213
  end
214
214
 
215
215
  it 'raises an error' do
@@ -232,11 +232,11 @@ describe 'BulkImport API' do
232
232
  stub_api_request(:post, '/v3/job/issue/bulkload/database').
233
233
  with(:body => expected_request.to_json).
234
234
  to_return(:body => {'job_id' => 12345}.to_json)
235
- api.bulk_load_issue(
235
+ expect(api.bulk_load_issue(
236
236
  'database',
237
237
  'table',
238
238
  guessed_config
239
- ).should == '12345'
239
+ )).to eq('12345')
240
240
  end
241
241
  end
242
242
 
@@ -245,14 +245,14 @@ describe 'BulkImport API' do
245
245
  stub_api_request(:get, '/v3/bulk_loads').
246
246
  to_return(:body => [bulk_load_session, bulk_load_session].to_json)
247
247
  result = api.bulk_load_list
248
- result.size.should == 2
249
- result.first.should == bulk_load_session
248
+ expect(result.size).to eq(2)
249
+ expect(result.first).to eq(bulk_load_session)
250
250
  end
251
251
 
252
252
  it 'returns empty' do
253
253
  stub_api_request(:get, '/v3/bulk_loads').
254
254
  to_return(:body => [].to_json)
255
- api.bulk_load_list.size.should == 0
255
+ expect(api.bulk_load_list.size).to eq(0)
256
256
  end
257
257
  end
258
258
 
@@ -268,7 +268,7 @@ describe 'BulkImport API' do
268
268
  stub_api_request(:post, '/v3/bulk_loads').
269
269
  with(:body => expected_request.to_json).
270
270
  to_return(:body => bulk_load_session.to_json)
271
- api.bulk_load_create(
271
+ expect(api.bulk_load_create(
272
272
  'nahi_test_1',
273
273
  'database',
274
274
  'table',
@@ -278,7 +278,7 @@ describe 'BulkImport API' do
278
278
  timezone: 'Asia/Tokyo',
279
279
  delay: 3600
280
280
  }
281
- ).should == bulk_load_session
281
+ )).to eq(bulk_load_session)
282
282
  end
283
283
 
284
284
  it 'accepts empty option' do
@@ -289,12 +289,12 @@ describe 'BulkImport API' do
289
289
  stub_api_request(:post, '/v3/bulk_loads').
290
290
  with(:body => expected_request.to_json).
291
291
  to_return(:body => bulk_load_session.to_json)
292
- api.bulk_load_create(
292
+ expect(api.bulk_load_create(
293
293
  'nahi_test_1',
294
294
  'database',
295
295
  'table',
296
296
  guessed_config
297
- ).should == bulk_load_session
297
+ )).to eq(bulk_load_session)
298
298
  end
299
299
 
300
300
  it 'accepts time_column option' do
@@ -306,7 +306,7 @@ describe 'BulkImport API' do
306
306
  stub_api_request(:post, '/v3/bulk_loads').
307
307
  with(:body => expected_request.to_json).
308
308
  to_return(:body => bulk_load_session.to_json)
309
- api.bulk_load_create(
309
+ expect(api.bulk_load_create(
310
310
  'nahi_test_1',
311
311
  'database',
312
312
  'table',
@@ -314,7 +314,7 @@ describe 'BulkImport API' do
314
314
  {
315
315
  time_column: 'c0'
316
316
  }
317
- ).should == bulk_load_session
317
+ )).to eq(bulk_load_session)
318
318
  end
319
319
  end
320
320
 
@@ -322,7 +322,7 @@ describe 'BulkImport API' do
322
322
  it 'returns bulk_load_session' do
323
323
  stub_api_request(:get, '/v3/bulk_loads/nahi_test_1').
324
324
  to_return(:body => bulk_load_session.to_json)
325
- api.bulk_load_show('nahi_test_1').should == bulk_load_session
325
+ expect(api.bulk_load_show('nahi_test_1')).to eq(bulk_load_session)
326
326
  end
327
327
  end
328
328
 
@@ -331,10 +331,35 @@ describe 'BulkImport API' do
331
331
  stub_api_request(:put, '/v3/bulk_loads/nahi_test_1').
332
332
  with(:body => bulk_load_session.to_json).
333
333
  to_return(:body => bulk_load_session.to_json)
334
- api.bulk_load_update(
334
+ expect(api.bulk_load_update(
335
335
  'nahi_test_1',
336
336
  bulk_load_session
337
- ).should == bulk_load_session
337
+ )).to eq(bulk_load_session)
338
+ end
339
+
340
+ it 'returns updated bulk_load_session' do
341
+ updated_bulk_load_session = bulk_load_session.merge({'timezone' => 'America/Los Angeles'})
342
+ stub_api_request(:put, '/v3/bulk_loads/nahi_test_1').
343
+ with(:body => updated_bulk_load_session.to_json).
344
+ to_return(:body => updated_bulk_load_session.to_json)
345
+ expect(api.bulk_load_update(
346
+ 'nahi_test_1',
347
+ updated_bulk_load_session
348
+ )).to eq updated_bulk_load_session
349
+ end
350
+
351
+ it 'can remove the cron schedule ' do
352
+ updated_bulk_load_session = bulk_load_session.merge({'cron' => ''})
353
+ # NOTE: currently the API just ignores an empty 'cron' specification update
354
+ # I am assuming that once fixed, the API will return a nil for cron if unscheduled.
355
+ expected_bulk_load_session = (bulk_load_session['cron'] = nil)
356
+ stub_api_request(:put, '/v3/bulk_loads/nahi_test_1').
357
+ with(:body => updated_bulk_load_session.to_json).
358
+ to_return(:body => expected_bulk_load_session.to_json)
359
+ expect(api.bulk_load_update(
360
+ 'nahi_test_1',
361
+ updated_bulk_load_session
362
+ )).to eq expected_bulk_load_session
338
363
  end
339
364
  end
340
365
 
@@ -342,7 +367,7 @@ describe 'BulkImport API' do
342
367
  it 'returns updated bulk_load_session' do
343
368
  stub_api_request(:delete, '/v3/bulk_loads/nahi_test_1').
344
369
  to_return(:body => bulk_load_session.to_json)
345
- api.bulk_load_delete('nahi_test_1').should == bulk_load_session
370
+ expect(api.bulk_load_delete('nahi_test_1')).to eq(bulk_load_session)
346
371
  end
347
372
  end
348
373
 
@@ -351,8 +376,8 @@ describe 'BulkImport API' do
351
376
  stub_api_request(:get, '/v3/bulk_loads/nahi_test_1/jobs').
352
377
  to_return(:body => [bulk_load_job, bulk_load_job].to_json)
353
378
  result = api.bulk_load_history('nahi_test_1')
354
- result.size.should == 2
355
- result.first.should == bulk_load_job
379
+ expect(result.size).to eq(2)
380
+ expect(result.first).to eq(bulk_load_job)
356
381
  end
357
382
  end
358
383
 
@@ -361,7 +386,7 @@ describe 'BulkImport API' do
361
386
  stub_api_request(:post, '/v3/bulk_loads/nahi_test_1/jobs').
362
387
  with(:body => '{}').
363
388
  to_return(:body => {'job_id' => 12345}.to_json)
364
- api.bulk_load_run('nahi_test_1').should == '12345'
389
+ expect(api.bulk_load_run('nahi_test_1')).to eq('12345')
365
390
  end
366
391
 
367
392
  it 'accepts scheduled_time' do
@@ -369,7 +394,7 @@ describe 'BulkImport API' do
369
394
  stub_api_request(:post, '/v3/bulk_loads/nahi_test_1/jobs').
370
395
  with(:body => {scheduled_time: now.to_s}.to_json).
371
396
  to_return(:body => {'job_id' => 12345}.to_json)
372
- api.bulk_load_run('nahi_test_1', now).should == '12345'
397
+ expect(api.bulk_load_run('nahi_test_1', now)).to eq('12345')
373
398
  end
374
399
  end
375
400
 
@@ -18,7 +18,7 @@ describe 'Database API' do
18
18
  stub_api_request(:post, "/v3/database/create/#{e(db_name)}").
19
19
  to_return(:body => {'database' => db_name}.to_json)
20
20
 
21
- api.create_database(db_name).should be true
21
+ expect(api.create_database(db_name)).to be true
22
22
  end
23
23
 
24
24
  it 'should return 400 error with invalid name' do
@@ -20,7 +20,7 @@ describe 'Export API' do
20
20
  stub_api_request(:post, "/v3/export/run/#{e(db_name)}/#{e(table_name)}").with(:body => params.merge('storage_type' => storage_type)).
21
21
  to_return(:body => {'database' => db_name, 'job_id' => '1', 'debug' => {}}.to_json)
22
22
 
23
- api.export(db_name, table_name, storage_type, params).should == '1'
23
+ expect(api.export(db_name, table_name, storage_type, params)).to eq('1')
24
24
  end
25
25
 
26
26
  it 'should return 400 error with invalid storage type' do
@@ -37,5 +37,15 @@ describe 'Export API' do
37
37
 
38
38
  # TODO: Add other parameters spec
39
39
  end
40
+
41
+ describe 'result_export' do
42
+ it 'should export result successfully' do
43
+ params = {'result' => 'mysql://user:pass@host.com/database/table'}
44
+ stub_api_request(:post, "/v3/job/result_export/100").with(:body => params).
45
+ to_return(:body => {'job_id' => '101'}.to_json)
46
+
47
+ expect(api.result_export(100, params)).to eq('101')
48
+ end
49
+ end
40
50
  end
41
51