embulk-input-zendesk 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 937ca47ed79588673fdf772d2a55dfe53ee0c433
4
+ data.tar.gz: 478a4b349a30dbdb019db9c03b21938755ed78c9
5
+ SHA512:
6
+ metadata.gz: a084ce0b8fa7bc6fe13ebf8d4ebeb0535d779928370aab1f54cf3adf3504972cf0f262d48f459da34526c44f3815db710732b368b8e8477da1fe10dfedd3870d
7
+ data.tar.gz: f404d3e55297b6885b03bb1599fe1b4bb4e5bf50cfdf0a7020d65b0bc2126a3deed9a9939f9aa9d45fe686e50021349db6503bdaecf59a5d82a7683778980f42
data/.gitignore ADDED
@@ -0,0 +1,6 @@
1
+ *~
2
+ /pkg/
3
+ /tmp/
4
+ /.bundle/
5
+ /Gemfile.lock
6
+ /coverage/
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ jruby-9.0.4.0
data/.travis.yml ADDED
@@ -0,0 +1,44 @@
1
+ language: ruby
2
+
3
+ jdk: oraclejdk8
4
+
5
+ before_install:
6
+ - rvm list known
7
+ - rvm list
8
+ - ruby -v
9
+ - |
10
+ # Currently, Travis can't treat jruby 9.0.4.0
11
+ if [[ ${TRAVIS_RUBY_VERSION} != "jruby-9.0.4.0" ]];then
12
+ rvm get head
13
+ rvm use jruby-9.0.4.0 --install
14
+ ruby -v
15
+ fi
16
+ - gem install bundler -v 1.10.6
17
+
18
+ script: bundle exec rake cov
19
+
20
+ rvm:
21
+ - jruby-9.0.4.0
22
+ env:
23
+ # Set on Travis
24
+ # CODECLIMATE_REPO_TOKEN: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
25
+
26
+ gemfile:
27
+ - gemfiles/embulk-0.8.0-latest
28
+ - gemfiles/embulk-0.8.1
29
+ - gemfiles/embulk-latest
30
+
31
+ matrix:
32
+ exclude:
33
+ - jdk: oraclejdk8 # Ignore all matrix at first, use `include` to allow build
34
+ include:
35
+ - {rvm: jruby-9.0.4.0, gemfile: gemfiles/embulk-0.8.0-latest}
36
+ - {rvm: jruby-9.0.4.0, gemfile: gemfiles/embulk-0.8.1}
37
+ - {rvm: jruby-9.0.4.0, gemfile: gemfiles/embulk-latest}
38
+
39
+
40
+ allow_failures:
41
+ # Ignore failure for *-latest
42
+ - gemfile: gemfiles/embulk-0.8.0-latest
43
+ - gemfile: gemfiles/embulk-latest
44
+
data/.travis.yml.erb ADDED
@@ -0,0 +1,43 @@
1
+ language: ruby
2
+
3
+ jdk: oraclejdk8
4
+
5
+ before_install:
6
+ - rvm list known
7
+ - rvm list
8
+ - ruby -v
9
+ - |
10
+ # Currently, Travis can't treat jruby 9.0.4.0
11
+ if [[ ${TRAVIS_RUBY_VERSION} != "jruby-9.0.4.0" ]];then
12
+ rvm get head
13
+ rvm use jruby-9.0.4.0 --install
14
+ ruby -v
15
+ fi
16
+ - gem install bundler -v 1.10.6
17
+
18
+ script: bundle exec rake cov
19
+
20
+ rvm:
21
+ - jruby-9.0.4.0
22
+ env:
23
+ # Set on Travis
24
+ # CODECLIMATE_REPO_TOKEN: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
25
+
26
+ gemfile:
27
+ <% versions.each do |file| -%>
28
+ - gemfiles/<%= file %>
29
+ <% end -%>
30
+
31
+ matrix:
32
+ exclude:
33
+ - jdk: oraclejdk8 # Ignore all matrix at first, use `include` to allow build
34
+ include:
35
+ <% matrix.each do |m| -%>
36
+ <%= m.gsub("9.0.0.0", "9.0.4.0") %>
37
+ <% end %>
38
+
39
+ allow_failures:
40
+ # Ignore failure for *-latest
41
+ <% versions.find_all{|file| file.to_s.match(/-latest/)}.each do |file| -%>
42
+ - gemfile: gemfiles/<%= file %>
43
+ <% end %>
data/CHANGELOG.md ADDED
@@ -0,0 +1,3 @@
1
+ ## 0.1.0 - 2016-01-26
2
+
3
+ The first release!!
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org/'
2
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+
2
+ MIT License
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining
5
+ a copy of this software and associated documentation files (the
6
+ "Software"), to deal in the Software without restriction, including
7
+ without limitation the rights to use, copy, modify, merge, publish,
8
+ distribute, sublicense, and/or sell copies of the Software, and to
9
+ permit persons to whom the Software is furnished to do so, subject to
10
+ the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,51 @@
1
+ [![Build Status](https://travis-ci.org/treasure-data/embulk-input-zendesk.svg?branch=master)](https://travis-ci.org/treasure-data/embulk-input-zendesk)
2
+ [![Code Climate](https://codeclimate.com/github/treasure-data/embulk-input-zendesk/badges/gpa.svg)](https://codeclimate.com/github/treasure-data/embulk-input-zendesk)
3
+ [![Test Coverage](https://codeclimate.com/github/treasure-data/embulk-input-zendesk/badges/coverage.svg)](https://codeclimate.com/github/treasure-data/embulk-input-zendesk/coverage)
4
+ [![Gem Version](https://badge.fury.io/rb/embulk-input-zendesk.svg)](https://badge.fury.io/rb/embulk-input-zendesk)
5
+
6
+ # Zendesk input plugin for Embulk
7
+
8
+ Embulk input plugin for loading [Zendesk](https://www.zendesk.com/) records.
9
+
10
+ ## Overview
11
+
12
+ Required Embulk version >= 0.8.1.
13
+
14
+ **NOTE** This plugin don't support JSON type columns e.g. custom fields, tags, etc for now. But they will be supported soon.
15
+
16
+ * **Plugin type**: input
17
+ * **Resume supported**: no
18
+ * **Cleanup supported**: no
19
+ * **Guess supported**: yes
20
+
21
+ ## Configuration
22
+
23
+ - **login_url**: Login URL for Zendesk (string, required)
24
+ - **auth_method**: `basic`, `token`, or `oauth`. For more detail on [zendesk document](https://developer.zendesk.com/rest_api/docs/core/introduction#security-and-authentication). (string, required)
25
+ - **target**: Which export Zendesk resource. Currently supported are `tickets`, `ticket_events`, `users`, `organizations`, `ticket_fields` or `ticket_forms`. (string, required)
26
+ - **username**: The user name a.k.a. email. Required if `auth_method` is `basic` or `token`. (string, default: `null`)
27
+ - **password**: Password. required if `auth_method` is `basic`. (string, default: `null`)
28
+ - **token**: Token. required if `auth_method` is `token`. (string, default: `null`)
29
+ - **access_token**: OAuth Access Token. required if `auth_method` is `oauth`. (string, default: `null`)
30
+ - **start_time**: Start export from this time if present. (string, default: `null`)
31
+ - **retry_limit**: Try to retry this times (integer, default: 5)
32
+ - **retry_initial_wait_sec**: Wait seconds for exponential backoff initial value (integer, default: 1)
33
+
34
+ ## Example
35
+
36
+ ```yaml
37
+ in:
38
+ type: zendesk
39
+ login_url: https://obscura.zendesk.com
40
+ auth_method: token
41
+ username: jdoe@example.com
42
+ token: 6wiIBWbGkBMo1mRDMuVwkw1EPsNkeUj95PIz2akv
43
+ start_time: "2015-01-01 00:00:00+0000"
44
+ ```
45
+
46
+
47
+ ## Test
48
+
49
+ ```
50
+ $ rake test
51
+ ```
data/Rakefile ADDED
@@ -0,0 +1,21 @@
1
+ require "bundler/gem_tasks"
2
+ require "everyleaf/embulk_helper/tasks"
3
+
4
+ task default: :test
5
+
6
+ desc "Run tests"
7
+ task :test do
8
+ ruby("test/run-test.rb", "--use-color=yes", "--collector=dir")
9
+ end
10
+
11
+ desc "Run tests with coverage"
12
+ task :cov do
13
+ ENV["COVERAGE"] = "1"
14
+ ruby("--debug", "test/run-test.rb", "--use-color=yes", "--collector=dir")
15
+ end
16
+
17
+
18
+ Everyleaf::EmbulkHelper::Tasks.install(
19
+ gemspec: "./embulk-input-zendesk.gemspec",
20
+ github_name: "treasure-data/embulk-input-zendesk",
21
+ )
@@ -0,0 +1,28 @@
1
+
2
+ Gem::Specification.new do |spec|
3
+ spec.name = "embulk-input-zendesk"
4
+ spec.version = "0.1.0"
5
+ spec.authors = ["uu59"]
6
+ spec.summary = "Zendesk input plugin for Embulk"
7
+ spec.description = "Loads records from Zendesk."
8
+ spec.email = ["k@uu59.org"]
9
+ spec.licenses = ["MIT"]
10
+ # TODO set this: spec.homepage = "https://github.com/k/embulk-input-zendesk"
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.4'
17
+ spec.add_dependency 'httpclient'
18
+ spec.add_development_dependency 'embulk', ['~> 0.8.1']
19
+ spec.add_development_dependency 'bundler', ['~> 1.0']
20
+ spec.add_development_dependency 'rake', ['>= 10.0']
21
+ spec.add_development_dependency 'pry'
22
+ spec.add_development_dependency 'test-unit', '~> 3.1.5'
23
+ spec.add_development_dependency 'test-unit-rr'
24
+ spec.add_development_dependency 'rr', '~> 1.1.2'
25
+ spec.add_development_dependency 'simplecov'
26
+ spec.add_development_dependency 'everyleaf-embulk_helper'
27
+ spec.add_development_dependency "codeclimate-test-reporter"
28
+ end
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org/'
2
+ gemspec :path => '../'
3
+
4
+ gem "embulk", "~> 0.8.0"
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org/'
2
+ gemspec :path => '../'
3
+
4
+ gem "embulk", "0.8.1"
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org/'
2
+ gemspec :path => '../'
3
+
4
+ gem "embulk", "> 0.8.1"
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org/'
2
+ gemspec :path => '../'
3
+
4
+ gem "embulk", "<%= version %>"
@@ -0,0 +1,9 @@
1
+ require "embulk/input/zendesk/client"
2
+ require "embulk/input/zendesk/plugin"
3
+
4
+ module Embulk
5
+ module Input
6
+ module Zendesk
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,199 @@
1
+ require "httpclient"
2
+
3
+ module Embulk
4
+ module Input
5
+ module Zendesk
6
+ class Client
7
+ attr_reader :config
8
+
9
+ PARTIAL_RECORDS_SIZE = 50
10
+ AVAILABLE_TARGETS = %w(
11
+ tickets ticket_events users organizations
12
+ ticket_fields ticket_forms
13
+ ).freeze
14
+
15
+ def initialize(config)
16
+ @config = config
17
+ end
18
+
19
+ def httpclient
20
+ httpclient = HTTPClient.new
21
+ # httpclient.debug_dev = STDOUT
22
+ return set_auth(httpclient)
23
+ end
24
+
25
+ def validate_config
26
+ validate_credentials
27
+ validate_target
28
+ end
29
+
30
+ def validate_credentials
31
+ valid = case config[:auth_method]
32
+ when "basic"
33
+ config[:username] && config[:password]
34
+ when "token"
35
+ config[:username] && config[:token]
36
+ when "oauth"
37
+ config[:access_token]
38
+ else
39
+ raise Embulk::ConfigError.new("Unknown auth_method (#{config[:auth_method]}). Should pick one from 'basic', 'token' or 'oauth'.")
40
+ end
41
+
42
+ unless valid
43
+ raise Embulk::ConfigError.new("Missing required credentials for #{config[:auth_method]}")
44
+ end
45
+ end
46
+
47
+ def validate_target
48
+ unless AVAILABLE_TARGETS.include?(config[:target])
49
+ raise Embulk::ConfigError.new("target: '#{config[:target]}' is not supported.")
50
+ end
51
+ end
52
+
53
+ %w(tickets users organizations).each do |target|
54
+ define_method(target) do |partial = true, start_time = 0, &block|
55
+ if partial
56
+ export("/api/v2/#{target}.json", target, PARTIAL_RECORDS_SIZE, &block) # Ignore start_time
57
+ else
58
+ incremental_export("/api/v2/incremental/#{target}.json", target, start_time, [], &block)
59
+ end
60
+ end
61
+ end
62
+
63
+ def ticket_events(partial = true, start_time = 0, &block)
64
+ # NOTE: ticket_events only have incremental export API
65
+ path = "/api/v2/incremental/ticket_events"
66
+ incremental_export(path, "ticket_events", start_time, [], &block)
67
+ end
68
+
69
+ def ticket_fields(partial = true, start_time = 0, &block)
70
+ # NOTE: ticket_fields only have export API (not incremental)
71
+ path = "/api/v2/ticket_fields.json"
72
+ export(path, "ticket_fields", 1000, &block)
73
+ end
74
+
75
+ def ticket_forms(partial = true, start_time = 0, &block)
76
+ # NOTE: ticket_forms only have export API (not incremental)
77
+ path = "/api/v2/ticket_forms.json"
78
+ export(path, "ticket_forms", 1000, &block)
79
+ end
80
+
81
+ private
82
+
83
+ def export(path, key, per_page, &block)
84
+ # for `embulk guess` and `embulk preview` to fetch ~50 records only.
85
+ # incremental export API has supported only 1000 per page, it is too large to guess/preview
86
+ Embulk.logger.debug "#{path} with per_page: #{per_page}"
87
+ response = request(path, per_page: per_page)
88
+
89
+ begin
90
+ data = JSON.parse(response.body)
91
+ rescue => e
92
+ raise Embulk::DataError.new(e)
93
+ end
94
+
95
+ data[key].each do |record|
96
+ block.call record
97
+ end
98
+ end
99
+
100
+ def incremental_export(path, key, start_time = 0, known_ids = [], &block)
101
+ # for `embulk run` to fetch all records.
102
+ response = request(path, start_time: start_time)
103
+
104
+ begin
105
+ data = JSON.parse(response.body)
106
+ rescue => e
107
+ raise Embulk::DataError.new(e)
108
+ end
109
+
110
+ Embulk.logger.debug "start_time:#{start_time} (#{Time.at(start_time)}) count:#{data["count"]} next_page:#{data["next_page"]} end_time:#{data["end_time"]} "
111
+ data[key].each do |record|
112
+ # de-duplicated records.
113
+ # https://developer.zendesk.com/rest_api/docs/core/incremental_export#usage-notes
114
+ # https://github.com/zendesk/zendesk_api_client_rb/issues/251
115
+ next if known_ids.include?(record["id"])
116
+
117
+ known_ids << record["id"]
118
+ block.call record
119
+ end
120
+
121
+ # NOTE: If count is less than 1000, then stop paginating.
122
+ # Otherwise, use the next_page URL to get the next page of results.
123
+ # https://developer.zendesk.com/rest_api/docs/core/incremental_export#pagination
124
+ if data["count"] == 1000
125
+ incremental_export(path, key, data["end_time"], known_ids, &block)
126
+ end
127
+ end
128
+
129
+ def retryer
130
+ PerfectRetry.new do |config|
131
+ config.limit = @config[:retry_limit]
132
+ config.logger = Embulk.logger
133
+ config.log_level = nil
134
+ config.dont_rescues = [Embulk::DataError, Embulk::ConfigError]
135
+ config.sleep = lambda{|n| @config[:retry_initial_wait_sec]* (2 ** (n-1)) }
136
+ end
137
+ end
138
+
139
+ def set_auth(httpclient)
140
+ validate_credentials
141
+
142
+ # https://developer.zendesk.com/rest_api/docs/core/introduction#security-and-authentication
143
+ case config[:auth_method]
144
+ when "basic"
145
+ httpclient.set_auth(config[:login_url], config[:username], config[:password])
146
+ when "token"
147
+ httpclient.set_auth(config[:login_url], "#{config[:username]}/token", config[:token])
148
+ when "oauth"
149
+ httpclient.default_header = {
150
+ "Authorization" => "Bearer #{config[:access_token]}"
151
+ }
152
+ end
153
+ httpclient
154
+ end
155
+
156
+ def request(path, query = {})
157
+ u = URI.parse(config[:login_url])
158
+ u.path = path
159
+
160
+ retryer.with_retry do
161
+ response = httpclient.get(u.to_s, query, follow_redirect: true)
162
+
163
+ # https://developer.zendesk.com/rest_api/docs/core/introduction#response-format
164
+ status_code = response.status
165
+ case status_code
166
+ when 200
167
+ response
168
+ when 400, 401
169
+ raise Embulk::ConfigError.new("[#{status_code}] #{response.body}")
170
+ when 409
171
+ raise "[#{status_code}] temporally failure."
172
+ when 429
173
+ # rate limit
174
+ retry_after = response.headers["Retry-After"]
175
+ wait_rate_limit(retry_after.to_i)
176
+ when 500, 503
177
+ # 503 is possible rate limit
178
+ retry_after = response.headers["Retry-After"]
179
+ if retry_after
180
+ wait_rate_limit(retry_after.to_i)
181
+ else
182
+ raise "[#{status_code}] temporally failure."
183
+ end
184
+ else
185
+ raise "Server returns unknown status code (#{status_code})"
186
+ end
187
+ end
188
+ end
189
+
190
+ def wait_rate_limit(retry_after)
191
+ Embulk.logger.warn "Rate Limited. Waiting #{retry_after} seconds to retry"
192
+ sleep retry_after
193
+ throw :retry
194
+ end
195
+
196
+ end
197
+ end
198
+ end
199
+ end