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.
- checksums.yaml +7 -0
- data/.gitignore +6 -0
- data/.ruby-version +1 -0
- data/Gemfile +2 -0
- data/LICENSE.txt +20 -0
- data/README.md +133 -0
- data/Rakefile +21 -0
- data/embulk-input-soracom_harvest.gemspec +28 -0
- data/lib/embulk/input/soracom_harvest.rb +9 -0
- data/lib/embulk/input/soracom_harvest/plugin.rb +250 -0
- data/lib/embulk/input/soracom_harvest/soracom_client.rb +176 -0
- data/test/run_test.rb +25 -0
- metadata +210 -0
checksums.yaml
ADDED
@@ -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
|
data/.gitignore
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
jruby-9.1.5.0
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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
|
+
```
|
data/Rakefile
ADDED
@@ -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,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
|
data/test/run_test.rb
ADDED
@@ -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
|