embulk-input-soracom_harvest 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 40c87d26a847cbe55a6691cad0de156bd65a3ca9
4
+ data.tar.gz: eeacfde1ae3dde5a5648c350002decbf555da102
5
+ SHA512:
6
+ metadata.gz: eb4105a8f08c9eae5b6e4d7590c5688f1dd49a8804d55089ffd3533c69fa268efa51077a220d98cc1c1baa753f10b3f67a26a19db3a38374add32cceb3f2af31
7
+ data.tar.gz: 29fbee6de9f830127b244d9486652a9b047d7a3def856d0e902322bf8c4c7a2a9639c38765688a4785a86bda7fd1b463d93debc79c94b86c90007accb86a3111
@@ -0,0 +1,6 @@
1
+ *~
2
+ /pkg/
3
+ /tmp/
4
+ /.bundle/
5
+ /Gemfile.lock
6
+ /coverage/
@@ -0,0 +1 @@
1
+ jruby-9.1.5.0
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org/'
2
+ gemspec
@@ -0,0 +1,20 @@
1
+ MIT License
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,133 @@
1
+ # Soracom Harvest input plugin for Embulk
2
+
3
+ [Soracom Harvest](https://soracom.jp/services/harvest/) is the data store service to store the data collected from IoT devices.
4
+ This plugin allows you to load data from Soracom Harvest and load into other data store and RDBMS with other [Embulk plugins](http://www.embulk.org/plugins/).
5
+
6
+ ## Overview
7
+
8
+ * **Plugin type**: input
9
+ * **Resume supported**: yes
10
+ * **Cleanup supported**: yes
11
+ * **Guess supported**: yes
12
+
13
+ ## Configuration
14
+
15
+ - **auth_key_id**: AUTH_KEY for SORACOM (string, required)
16
+ - **auth_key**: AUTH_KEY_ID for SORACOM (string, required)
17
+ - **target**: 'harvest' or 'sims'(string, default: 'harvest')
18
+ - **filter**: filter to when get SIMs(string, default: `null`)
19
+ - **tag_value_match_mode**: Tag search mode exact` or `prefix` (string, optional, default: `exact`)
20
+ <!-- - **incremental**: enables incremental loading(boolean, default: true). If incremental loading is enabled, config diff for the next execution will include `last_path` parameter so that next execution skips files before the path. Otherwise, `last_path` will not be included.-->
21
+ - **start_datetime**: get data time is after this value (works only when target is 'harvest')
22
+ - **end_datetime**: get data time is after this value (works only when target is 'harvest')
23
+ - **retry_limit**: Try to retry this times (integer, default: 5)
24
+ - **retry_initial_wait_sec**: Wait seconds for exponential backoff initial value (integer, default: 2)
25
+ - **endpoint**: endpoint url of SORACOM API server. e.g. "https://api.soracom.io/v1" (string, default: `null`)
26
+
27
+ ## Example
28
+
29
+ ```yaml
30
+ in:
31
+ type: soracom_harvest
32
+ auth_key_id: keyId-ABCDEFGHIJKLMNOPQRSTUVWXYZ
33
+ auth_key: secret-abcdefghijklmnopqrstuvwxyz
34
+ tartet: harvest
35
+ filter: status: active|ready
36
+ start_datetime: '2016-07-01T13:12:59.035692+09:00'
37
+ end_datetime: '2017-01-05T16:32:43.021312+09:00'
38
+ ```
39
+
40
+ # Usage
41
+
42
+ 1. Please configure minimum seed config.
43
+ 2. Run `embulk guess /path/to/seed.yml -o /path/to/config.yml`.
44
+ * If you have no registered SIMs, guess doesn't work.
45
+ * If you have no records at Harvest, guess doesn't work.
46
+ 3. Run `embulk preview /path/to/config.yml`
47
+ 4. Run `embulk run /path/to/config.yml`
48
+
49
+ ### filter
50
+
51
+ You can filter SIMS when get data by filter option.
52
+
53
+ This plugin doesn't support multiple filter condition.
54
+
55
+ #### imsi
56
+
57
+ ```yaml
58
+ filter: imsi: 440123456789012
59
+ ```
60
+
61
+ #### msisdn
62
+
63
+ ```yaml
64
+ filter: msisdn: 811234567890
65
+ ```
66
+
67
+ #### status
68
+
69
+ ```yaml
70
+ filter: status: active
71
+ ```
72
+
73
+ ```yaml
74
+ filter: status: active|ready
75
+ ```
76
+
77
+ status value can be taken (active, inactive, ready, instock, shipped, suspended, terminated).
78
+
79
+ Also accepts multiple vaules separated with `|`
80
+
81
+ #### speed_class
82
+
83
+ ```yaml
84
+ filter: speed_class: s1.minimum
85
+ ```
86
+
87
+ ```yaml
88
+ filter: speed_class: s1.minimum|s1.slow
89
+ ```
90
+
91
+ #### tag
92
+
93
+ ```yaml
94
+ filter: tag_name: tag_value
95
+ tag_value_match_mode: exact # or 'prefix'
96
+ ```
97
+
98
+ You can set `tag_value_match_mode`. This option can be taken (exact, prefix).
99
+
100
+
101
+ ### FAQ
102
+
103
+ * Q1. I stores data at SORACOM Harvest with **JSON** format and want to expand its columns.
104
+
105
+ * A. Please use [embulk-filter_expand_json](https://github.com/civitaspo/embulk-filter-expand_json)
106
+
107
+ * Q2. I want to filter by value with more complex conditions like SQL.
108
+
109
+ * A. Please use [embulk-filter-row](https://github.com/sonots/embulk-filter-row)
110
+
111
+ * Q3. Want to drop column.
112
+
113
+ * A. Please use[embulk-filter-column](https://github.com/sonots/embulk-filter-column)
114
+
115
+ * Q4. Want to add time column like current time.
116
+
117
+ * A. Use [embulk-filter-add_time](https://github.com/treasure-data/embulk-filter-add_time)
118
+
119
+
120
+ ## Build
121
+
122
+ ```
123
+ $ rake
124
+ ```
125
+
126
+ ## Development
127
+
128
+ ```
129
+ $ git clone git@github.com:sakama/embulk-input-soracom_harvest.git
130
+ $ cd embulk-input-soracom_harvest
131
+ $ embulk bundle install --path vendor/bundle
132
+ $ embulk run -I ./lib /path/to/config.yml
133
+ ```
@@ -0,0 +1,21 @@
1
+ require "bundler/gem_tasks"
2
+ require "gem_release_helper/tasks"
3
+
4
+ task default: :test
5
+
6
+ desc "Run tests"
7
+ task :test do
8
+ # TODO
9
+ #ruby("--debug", "test/run-test.rb", "--use-color=yes", "--collector=dir")
10
+ end
11
+
12
+ desc "Run tests with coverage"
13
+ task :cov do
14
+ ENV["COVERAGE"] = "1"
15
+ ruby("--debug", "test/run-test.rb", "--use-color=yes", "--collector=dir")
16
+ end
17
+
18
+ GemReleaseHelper::Tasks.install({
19
+ gemspec: "./embulk-input-soracom_harvest.gemspec",
20
+ github_name: "sakama/embulk-input-soracom_harvest",
21
+ })
@@ -0,0 +1,28 @@
1
+
2
+ Gem::Specification.new do |spec|
3
+ spec.name = "embulk-input-soracom_harvest"
4
+ spec.version = "0.1.0"
5
+ spec.authors = ["Satoshi Akama"]
6
+ spec.summary = "Soracom Harvest input plugin for Embulk"
7
+ spec.description = "Loads records from Soracom Harvest."
8
+ spec.email = ["satoshiakama@gmail.com"]
9
+ spec.licenses = ["MIT"]
10
+ spec.homepage = "https://github.com/sakama/embulk-input-soracom_harvest"
11
+
12
+ spec.files = `git ls-files`.split("\n") + Dir["classpath/*.jar"]
13
+ spec.test_files = spec.files.grep(%r{^(test|spec)/})
14
+ spec.require_paths = ["lib"]
15
+
16
+ spec.add_dependency 'perfect_retry', '~> 0.5'
17
+ spec.add_dependency 'httpclient', '>= 2.8.3'
18
+
19
+ spec.add_development_dependency 'embulk', ['>= 0.8.15']
20
+ spec.add_development_dependency 'bundler', ['>= 1.10.6']
21
+ spec.add_development_dependency 'rake', ['>= 10.0']
22
+ spec.add_development_dependency 'test-unit'
23
+ spec.add_development_dependency 'test-unit-rr'
24
+ spec.add_development_dependency 'simplecov'
25
+ spec.add_development_dependency 'codeclimate-test-reporter'
26
+ spec.add_development_dependency 'pry'
27
+ spec.add_development_dependency 'gem_release_helper', '~> 1.0'
28
+ end
@@ -0,0 +1,9 @@
1
+ require 'embulk/input/soracom_harvest/soracom_client'
2
+ require 'embulk/input/soracom_harvest/plugin'
3
+
4
+ module Embulk
5
+ module Input
6
+ module SoracomHarvest
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,250 @@
1
+ module Embulk
2
+ module Input
3
+ module SoracomHarvest
4
+ class Plugin < InputPlugin
5
+ ::Embulk::Plugin.register_input('soracom_harvest', self)
6
+
7
+ PREVIEW_COUNT = 15
8
+ END_POINT_URL_DEFAULT = 'https://api.soracom.io/v1'
9
+ TAG_VALUE_MATCH_MODE_DEFAULT = 'exact'
10
+ RETRY_LIMIT_DEFAULT = 5
11
+ RETRY_INITIAL_WAIT_SEC_DEFAULT = 2
12
+
13
+ attr_reader :start_datetime
14
+ attr_reader :end_datetime
15
+ attr_reader :last_record # TODO
16
+ attr_reader :filter
17
+
18
+ def self.transaction(config, &control)
19
+ # configuration code:
20
+ task = {
21
+ 'auth_key' => config.param('auth_key', :string),
22
+ 'auth_key_id' => config.param('auth_key_id', :string),
23
+ 'target' => config.param('target', :string, default: 'harvest'),
24
+ # TODO
25
+ 'incremental' => config.param('incremental', :bool, default: false),
26
+ 'start_datetime' => config.param('start_datetime', :string, default: nil),
27
+ 'end_datetime' => config.param('end_datetime', :string, default: nil),
28
+ #'last_record' => config.param("last_record", :string, default: nil),
29
+ 'endpoint' => config.param('endpoint', :string, default: END_POINT_URL_DEFAULT),
30
+ 'filter' => config.param('filter', :string, default: nil),
31
+ 'tag_value_match_mode' => config.param('tag_value_match_mode', :string, default: TAG_VALUE_MATCH_MODE_DEFAULT),
32
+ 'retry_limit' => config.param('retry_limit', :integer, default: RETRY_LIMIT_DEFAULT),
33
+ 'retry_initial_wait_sec' => config.param('retry_initial_wait_sec', :integer, default: RETRY_INITIAL_WAIT_SEC_DEFAULT),
34
+ 'columns' => config.param('columns', :array),
35
+ }
36
+
37
+ columns = embulk_columns(config)
38
+
39
+ resume(task, columns, 1, &control)
40
+ end
41
+
42
+ def self.resume(task, columns, count, &control)
43
+ task_reports = yield(task, columns, count)
44
+
45
+ next_config_diff = task_reports.first
46
+ return next_config_diff
47
+ end
48
+
49
+ def init
50
+ if task['start_datetime']
51
+ raise ConfigError.new "'start_datetime' can't be used when 'target: sims'" if task['target'] == 'sims'
52
+ @start_datetime = convert_to_unixtimestamp(task['start_datetime'])
53
+ end
54
+
55
+ if task['end_datetime']
56
+ raise ConfigError.new "'end_datetime' can't be used when 'target: sims'" if task['target'] == 'sims'
57
+ @end_datetime = convert_to_unixtimestamp(task['end_datetime'])
58
+ end
59
+
60
+ # if task['last_record']
61
+ # @last_record = convert_to_unixtimestamp(task['last_record'])
62
+ # end
63
+
64
+ @filter = to_hash(task['filter'])
65
+ end
66
+
67
+ def self.guess(config)
68
+ auth_key_id = config.param(:auth_key_id, :string)
69
+ auth_key = config.param(:auth_key, :string)
70
+ target = config.param(:target, :string)
71
+ tag_value_match_mode = config.param(:tag_value_match_mode, :string, default: TAG_VALUE_MATCH_MODE_DEFAULT)
72
+
73
+ retry_limit = config.param(:retry_limit, :integer, default: RETRY_LIMIT_DEFAULT)
74
+ retry_initial_wait_sec = config.param(:retry_initial_wait_sec, :integer, default: RETRY_INITIAL_WAIT_SEC_DEFAULT)
75
+
76
+ options = {
77
+ endpoint: config.param(:endpoint, :string, default:END_POINT_URL_DEFAULT),
78
+ retry_limit: retry_limit,
79
+ retry_initial_wait_sec: retry_initial_wait_sec,
80
+ }
81
+ client = SoracomClient.new(auth_key_id, auth_key, options)
82
+
83
+ # TODO last_record
84
+ sims = client.list_subscribers(filter: @filter, limit: 1, last_record: nil, tag_value_match_mode: tag_value_match_mode)
85
+ raise ConfigError.new "Failed to guess. No registered SIM found" if sims.size == 0
86
+
87
+ Embulk::logger.info "Getting schema for target: '#{target}'"
88
+ if target == 'sims'
89
+ columns = self.get_sim_schema(sims.first)
90
+ else
91
+ records = client.list_subscribers_imsi_data(imsi: sims.first['imsi'], from: @start_datetime, to: @end_datetime, limit: 1)
92
+ raise ConfigError.new "Failed to guess. No records found at Soracom Harvest" if records.size == 0
93
+ columns = self.get_harvest_schema(records.first)
94
+ end
95
+
96
+ {
97
+ 'columns' => columns
98
+ }
99
+ end
100
+
101
+ def run
102
+ client = SoracomClient.new(task['auth_key_id'], task['auth_key'], get_request_options(task))
103
+
104
+ # TODO last_record
105
+ sims = client.list_subscribers(filter: @filter, last_record: nil, tag_value_match_mode: task['tag_value_match_mode'])
106
+
107
+ if sims.size > 0
108
+ columns = task['columns']
109
+
110
+ counter = 0
111
+ last_record = nil
112
+ sims.each do |sim|
113
+ if task['target'] == 'sims'
114
+ page_builder.add(format_record(sim, columns, false))
115
+ else
116
+ # TODO last_record
117
+ records = client.list_subscribers_imsi_data(imsi: sim['imsi'], from: @start_datetime, to: @end_datetime, last_record: @last_record)
118
+ if records.size > 0
119
+ records.each do |record|
120
+ page_builder.add(format_record(record, columns, true))
121
+ last_record = record['time']
122
+ end
123
+ end
124
+ end
125
+ break if preview? && (counter += 1) >= PREVIEW_COUNT
126
+ end
127
+ end
128
+
129
+ page_builder.finish
130
+
131
+ return {} unless task[:incremental]
132
+
133
+ task_report = {
134
+ last_record: convert_unixtime_to_date(last_record)
135
+ }
136
+ end
137
+
138
+ def self.get_sim_schema(sim)
139
+ columns = []
140
+ sim.each do |k, v|
141
+ type =
142
+ case k
143
+ when 'plan'
144
+ 'long'
145
+ when 'createdAt', 'lastModifiedAt', 'expiredAt', 'expiryTime', 'createdTime', 'lastModifiedTime'
146
+ 'timestamp'
147
+ when 'imeiLock', 'terminationEnabled'
148
+ 'boolean'
149
+ when 'tags', 'sessionStatus'
150
+ 'json'
151
+ else
152
+ 'string'
153
+ end
154
+ columns << {name: k, type: type}
155
+ end
156
+ columns
157
+ end
158
+
159
+ def self.get_harvest_schema(record)
160
+ content_type = record['contentType']
161
+ type = content_type == 'application/json' ? 'json' : 'string'
162
+ [
163
+ {name: 'content', type: type},
164
+ {name: 'contentType', type: 'string'},
165
+ {name: 'time', type: 'timestamp'},
166
+ ]
167
+ end
168
+
169
+ def format_record(record, columns, is_harvest)
170
+ values = columns.map do |column|
171
+ name = column['name'].to_s
172
+ value = record[name]
173
+ cast_value(column, value, is_harvest)
174
+ end
175
+ end
176
+
177
+ def cast_value(column, value, is_harvest)
178
+ return if value.to_s.empty? # nil or empty string
179
+
180
+ case column['type'].to_s
181
+ when 'timestamp'
182
+ begin
183
+ Time.at(value / 1000.0).round(3)
184
+ rescue
185
+ raise DataError.new "Can't parse as Time '#{value}' (column is #{column['name']})"
186
+ end
187
+ when 'json'
188
+ if is_harvest
189
+ begin
190
+ JSON.parse(value)
191
+ rescue
192
+ raise DataError.new "Can't parse as JSON '#{value}' (column is #{column['name']})"
193
+ end
194
+ else
195
+ value.to_json
196
+ end
197
+ else
198
+ value
199
+ end
200
+ end
201
+
202
+ def preview?
203
+ begin
204
+ # http://www.embulk.org/docs/release/release-0.6.12.html
205
+ org.embulk.spi.Exec.isPreview()
206
+ rescue java.lang.NullPointerException => e
207
+ false
208
+ end
209
+ end
210
+
211
+ def self.embulk_columns(config)
212
+ config.param(:columns, :array).map do |column|
213
+ name = column['name']
214
+ type = column['type'].to_sym
215
+
216
+ Column.new(nil, name, type, column['format'])
217
+ end
218
+ end
219
+
220
+ def get_request_options(task)
221
+ {
222
+ endpoint: task[:endpoint],
223
+ retry_limit: task[:retry_limit],
224
+ retry_initial_wait_sec: task[:retry_initial_wait_sec],
225
+ }
226
+ end
227
+
228
+ def to_hash(str)
229
+ return nil if str.nil?
230
+ array = str.delete(' ').split(/[:,]/)
231
+ array.each_slice(2).map {|k, v| [k.to_sym, v] }.to_h
232
+ end
233
+
234
+ def convert_unixtime_to_date(unixtime)
235
+ return nil if unixtime.nil?
236
+ Time.at(unixtime / 1000.0).strftime('%Y-%m-%d %H:%M:%S.%3N %z')
237
+ end
238
+
239
+ def convert_to_unixtimestamp(time)
240
+ begin
241
+ v = Time.parse(time)
242
+ v.to_i * 1000 + v.usec/1000
243
+ rescue
244
+ raise ConfigError.new "Failed to convert ['#{time}'] to UNIX timestamp"
245
+ end
246
+ end
247
+ end
248
+ end
249
+ end
250
+ end
@@ -0,0 +1,176 @@
1
+ require 'perfect_retry'
2
+ require 'httpclient'
3
+
4
+ # SORACOM Ruby SDK doesn't support SORACOM Harvest for now(Dec 2016).
5
+ # So I send HTTP request to API directly.
6
+ # Want to remove this Class in the future when Ruby SDK supports Harvest.
7
+
8
+ module Embulk
9
+ module Input
10
+ module SoracomHarvest
11
+ class SoracomClient
12
+ attr_reader :auth_key_id
13
+ attr_reader :auth_key
14
+ attr_reader :options
15
+
16
+ attr_reader :client
17
+
18
+ attr_reader :api_key
19
+ attr_reader :token
20
+
21
+ def initialize(auth_key_id, auth_key, options)
22
+ @auth_key_id = auth_key_id
23
+ @auth_key = auth_key
24
+ @options = options
25
+ auth
26
+ end
27
+
28
+ def auth
29
+ @client = HTTPClient.new
30
+ postdata = {'authKeyId' => auth_key_id, 'authKey' => auth_key}
31
+ header = {'Content-Type' => 'application/json'}
32
+
33
+ response = get(path: '/auth', header: header, postdata: postdata)
34
+
35
+ @api_key = response['apiKey']
36
+ @token = response['token']
37
+ end
38
+
39
+ def list_subscribers(filter: {}, limit: 10000, last_record: nil, tag_value_match_mode: nil)
40
+ query = {
41
+ 'limit' => limit
42
+ }
43
+ query['last_evaluated_key'] = last_record unless last_record.nil?
44
+
45
+ if filter.nil?
46
+ path = '/subscribers'
47
+ else
48
+ key = filter.keys.first.to_s
49
+ value = filter.values.first
50
+ Embulk.logger.info "Requesting with filter '#{key}: #{value}'"
51
+ case key
52
+ when 'imsi'
53
+ path = "/subscribers/#{value}"
54
+ when 'msisdn'
55
+ path = "/subscribers/msisdn/#{value}"
56
+ when 'status'
57
+ path = '/subscribers'
58
+ query['status_filter'] = value
59
+ when 'speed_class'
60
+ path = '/subscribers'
61
+ query['speed_class_filter'] = value
62
+ else
63
+ path = '/subscribers'
64
+ query['tag_name'] = key
65
+ query['tag_value'] = value
66
+ query['tag_value_match_mode'] = tag_value_match_mode unless tag_value_match_mode.nil?
67
+ end
68
+ end
69
+
70
+ response = get(path: path, query: query)
71
+ Embulk.logger.info "#{response.size} SIMs found"
72
+
73
+ response
74
+ end
75
+
76
+ def list_subscribers_imsi_data(imsi: nil, from: nil, to: nil, limit: 100000, last_record: nil)
77
+ path = "/subscribers/#{imsi}/data"
78
+ query = {
79
+ 'sort' => 'asc',
80
+ 'limit' => limit,
81
+ }
82
+ query['from'] = from unless from.nil?
83
+ query['to'] = to unless to.nil?
84
+ query['last_evaluated_key'] = last_record unless last_record.nil?
85
+ response = get(path: path, query: query)
86
+ Embulk.logger.info "#{response.size} records found at Soracom Harvest for SIM: #{imsi}"
87
+
88
+ response
89
+ end
90
+
91
+ def get(path: nil, header: {}, query: nil, postdata: nil)
92
+ header = header.merge(
93
+ 'X-Soracom-API-Key' => @api_key,
94
+ 'X-Soracom-Token' => @token,
95
+ 'Accept' => 'application/json',
96
+ ) unless path == '/auth'
97
+
98
+ retryer.with_retry do
99
+ url = @options[:endpoint] + path
100
+
101
+ if postdata
102
+ response = @client.post(url, postdata.to_json, header)
103
+ else
104
+ response = @client.get(url, query, header)
105
+ end
106
+
107
+ Embulk::logger.debug "url: #{url}"
108
+ Embulk::logger.debug "Query: #{query}"
109
+ Embulk::logger.debug "POST data: #{postdata}"
110
+ Embulk::logger.debug "Status code: #{response.code}"
111
+ Embulk::logger.debug "Response body: #{response.body}"
112
+
113
+ handle_error(response)
114
+
115
+ response_body = JSON.parse(response.body)
116
+ if path == '/auth' || response_body.is_a?(Array)
117
+ body = response_body
118
+ else
119
+ body = Array[response_body]
120
+ end
121
+
122
+ body
123
+ end
124
+ end
125
+
126
+ def handle_error(response)
127
+ code = response.code
128
+
129
+ case code
130
+ when 400..499
131
+ message = "StatusCode: #{code}"
132
+
133
+ body = nil
134
+ begin
135
+ body = JSON.parse(response.body)
136
+ rescue
137
+ message << ": #{response.body}"
138
+ raise ConfigError.new message
139
+ end
140
+
141
+ if body.is_a?(Array)
142
+ body = body.first
143
+ end
144
+ message << ", ErrorCode: #{body['code']}" if body["code"]
145
+ message << ", Message: #{body['message']}" if body["message"]
146
+ case body["code"]
147
+ # TODO
148
+ when "INVALID_QUERY_LOCATOR", "QUERY_TIMEOUT"
149
+ # will be retried
150
+ raise message
151
+ else
152
+ # won't retry
153
+ raise ConfigError.new message
154
+ end
155
+ when 500..599
156
+ raise "SORACOM API returns StatusCode: #{code}. Retrying..."
157
+ end
158
+ end
159
+
160
+ def retryer
161
+ PerfectRetry.new do |config|
162
+ config.limit = options[:retry_limit]
163
+ config.logger = Embulk.logger
164
+ config.log_level = nil
165
+
166
+ # TODO
167
+ #config.rescues = Google::Apis::Core::HttpCommand::RETRIABLE_ERRORS
168
+ config.dont_rescues = [Embulk::DataError, Embulk::ConfigError]
169
+ config.sleep = lambda{|n| options[:retry_initial_wait_sec]* (2 ** (n-1)) }
170
+ config.raise_original_error = true
171
+ end
172
+ end
173
+ end
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ base_dir = File.expand_path(File.join(File.dirname(__FILE__), ".."))
4
+ lib_dir = File.join(base_dir, "lib")
5
+ test_dir = File.join(base_dir, "test")
6
+
7
+ require "test-unit"
8
+ require "test/unit/rr"
9
+
10
+ $LOAD_PATH.unshift(lib_dir)
11
+ $LOAD_PATH.unshift(test_dir)
12
+
13
+ ENV["TEST_UNIT_MAX_DIFF_TARGET_STRING_SIZE"] ||= "5000"
14
+
15
+ if ENV["COVERAGE"]
16
+ if ENV["CI"]
17
+ require "codeclimate-test-reporter"
18
+ CodeClimate::TestReporter.start
19
+ else
20
+ require 'simplecov'
21
+ SimpleCov.start 'test_frameworks'
22
+ end
23
+ end
24
+
25
+ exit Test::Unit::AutoRunner.run(true, test_dir)
metadata ADDED
@@ -0,0 +1,210 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: embulk-input-soracom_harvest
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Satoshi Akama
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-12-22 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '0.5'
19
+ name: perfect_retry
20
+ prerelease: false
21
+ type: :runtime
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.5'
27
+ - !ruby/object:Gem::Dependency
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 2.8.3
33
+ name: httpclient
34
+ prerelease: false
35
+ type: :runtime
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: 2.8.3
41
+ - !ruby/object:Gem::Dependency
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: 0.8.15
47
+ name: embulk
48
+ prerelease: false
49
+ type: :development
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: 0.8.15
55
+ - !ruby/object:Gem::Dependency
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: 1.10.6
61
+ name: bundler
62
+ prerelease: false
63
+ type: :development
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: 1.10.6
69
+ - !ruby/object:Gem::Dependency
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '10.0'
75
+ name: rake
76
+ prerelease: false
77
+ type: :development
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '10.0'
83
+ - !ruby/object:Gem::Dependency
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ name: test-unit
90
+ prerelease: false
91
+ type: :development
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ name: test-unit-rr
104
+ prerelease: false
105
+ type: :development
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ name: simplecov
118
+ prerelease: false
119
+ type: :development
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ requirement: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - ">="
129
+ - !ruby/object:Gem::Version
130
+ version: '0'
131
+ name: codeclimate-test-reporter
132
+ prerelease: false
133
+ type: :development
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ requirement: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - ">="
143
+ - !ruby/object:Gem::Version
144
+ version: '0'
145
+ name: pry
146
+ prerelease: false
147
+ type: :development
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ - !ruby/object:Gem::Dependency
154
+ requirement: !ruby/object:Gem::Requirement
155
+ requirements:
156
+ - - "~>"
157
+ - !ruby/object:Gem::Version
158
+ version: '1.0'
159
+ name: gem_release_helper
160
+ prerelease: false
161
+ type: :development
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - "~>"
165
+ - !ruby/object:Gem::Version
166
+ version: '1.0'
167
+ description: Loads records from Soracom Harvest.
168
+ email:
169
+ - satoshiakama@gmail.com
170
+ executables: []
171
+ extensions: []
172
+ extra_rdoc_files: []
173
+ files:
174
+ - ".gitignore"
175
+ - ".ruby-version"
176
+ - Gemfile
177
+ - LICENSE.txt
178
+ - README.md
179
+ - Rakefile
180
+ - embulk-input-soracom_harvest.gemspec
181
+ - lib/embulk/input/soracom_harvest.rb
182
+ - lib/embulk/input/soracom_harvest/plugin.rb
183
+ - lib/embulk/input/soracom_harvest/soracom_client.rb
184
+ - test/run_test.rb
185
+ homepage: https://github.com/sakama/embulk-input-soracom_harvest
186
+ licenses:
187
+ - MIT
188
+ metadata: {}
189
+ post_install_message:
190
+ rdoc_options: []
191
+ require_paths:
192
+ - lib
193
+ required_ruby_version: !ruby/object:Gem::Requirement
194
+ requirements:
195
+ - - ">="
196
+ - !ruby/object:Gem::Version
197
+ version: '0'
198
+ required_rubygems_version: !ruby/object:Gem::Requirement
199
+ requirements:
200
+ - - ">="
201
+ - !ruby/object:Gem::Version
202
+ version: '0'
203
+ requirements: []
204
+ rubyforge_project:
205
+ rubygems_version: 2.6.6
206
+ signing_key:
207
+ specification_version: 4
208
+ summary: Soracom Harvest input plugin for Embulk
209
+ test_files:
210
+ - test/run_test.rb