embulk-input-soracom_harvest 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|