embulk-input-soracom_harvest 0.1.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.
@@ -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