embulk-input-jira 0.2.5 → 0.2.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +10 -5
  3. data/.travis.yml +4 -34
  4. data/CHANGELOG.md +4 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +5 -4
  7. data/build.gradle +116 -0
  8. data/config/checkstyle/checkstyle.xml +128 -0
  9. data/config/checkstyle/default.xml +108 -0
  10. data/gradle/wrapper/gradle-wrapper.jar +0 -0
  11. data/gradle/wrapper/gradle-wrapper.properties +5 -0
  12. data/gradlew +172 -0
  13. data/gradlew.bat +84 -0
  14. data/lib/embulk/guess/jira.rb +24 -0
  15. data/lib/embulk/input/jira.rb +3 -169
  16. data/src/main/java/org/embulk/input/jira/AuthenticateMethod.java +27 -0
  17. data/src/main/java/org/embulk/input/jira/Constant.java +17 -0
  18. data/src/main/java/org/embulk/input/jira/Issue.java +150 -0
  19. data/src/main/java/org/embulk/input/jira/JiraInputPlugin.java +226 -0
  20. data/src/main/java/org/embulk/input/jira/client/JiraClient.java +254 -0
  21. data/src/main/java/org/embulk/input/jira/util/JiraException.java +18 -0
  22. data/src/main/java/org/embulk/input/jira/util/JiraUtil.java +264 -0
  23. data/src/test/java/org/embulk/input/jira/IssueTest.java +278 -0
  24. data/src/test/java/org/embulk/input/jira/JiraInputPluginTest.java +204 -0
  25. data/src/test/java/org/embulk/input/jira/JiraPluginTestRuntime.java +133 -0
  26. data/src/test/java/org/embulk/input/jira/TestHelpers.java +41 -0
  27. data/src/test/java/org/embulk/input/jira/client/JiraClientTest.java +222 -0
  28. data/src/test/java/org/embulk/input/jira/util/JiraUtilTest.java +318 -0
  29. data/src/test/resources/config.yml +13 -0
  30. data/src/test/resources/issue_flatten.json +129 -0
  31. data/src/test/resources/issue_flatten_expected.json +73 -0
  32. data/src/test/resources/issue_get.json +36 -0
  33. data/src/test/resources/issue_get_expected.json +62 -0
  34. data/src/test/resources/jira_client.json +81 -0
  35. data/src/test/resources/jira_input_plugin.json +114 -0
  36. data/src/test/resources/jira_util.json +26 -0
  37. metadata +55 -175
  38. data/Gemfile +0 -3
  39. data/LICENSE +0 -13
  40. data/Rakefile +0 -15
  41. data/embulk-input-jira.gemspec +0 -27
  42. data/gemfiles/embulk-0.8.0-latest +0 -4
  43. data/gemfiles/embulk-0.8.7 +0 -4
  44. data/gemfiles/embulk-0.8.8 +0 -4
  45. data/gemfiles/embulk-latest +0 -4
  46. data/gemfiles/template.erb +0 -4
  47. data/lib/embulk/input/jira_api.rb +0 -9
  48. data/lib/embulk/input/jira_api/client.rb +0 -144
  49. data/lib/embulk/input/jira_api/issue.rb +0 -133
  50. data/lib/embulk/input/jira_input_plugin_utils.rb +0 -58
  51. data/spec/embulk/input/jira-input-plugin-utils_spec.rb +0 -89
  52. data/spec/embulk/input/jira_api/client_spec.rb +0 -224
  53. data/spec/embulk/input/jira_api/issue_spec.rb +0 -394
  54. data/spec/embulk/input/jira_spec.rb +0 -322
  55. data/spec/embulk_spec.rb +0 -32
  56. data/spec/spec_helper.rb +0 -26
  57. data/spec/support/stdout_and_err_capture.rb +0 -45
data/Gemfile DELETED
@@ -1,3 +0,0 @@
1
- source 'https://rubygems.org/'
2
-
3
- gemspec
data/LICENSE DELETED
@@ -1,13 +0,0 @@
1
- Copyright 2015 Everyleaf Corporation
2
-
3
- Licensed under the Apache License, Version 2.0 (the "License");
4
- you may not use this file except in compliance with the License.
5
- You may obtain a copy of the License at
6
-
7
- http://www.apache.org/licenses/LICENSE-2.0
8
-
9
- Unless required by applicable law or agreed to in writing, software
10
- distributed under the License is distributed on an "AS IS" BASIS,
11
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- See the License for the specific language governing permissions and
13
- limitations under the License.
data/Rakefile DELETED
@@ -1,15 +0,0 @@
1
- require "bundler/gem_tasks"
2
- require 'rspec/core/rake_task'
3
- require "everyleaf/embulk_helper/tasks"
4
-
5
- Everyleaf::EmbulkHelper::Tasks.install({
6
- gemspec: "./embulk-input-jira.gemspec",
7
- github_name: "treasure-data/embulk-input-jira",
8
- })
9
-
10
- task default: :spec
11
-
12
- desc "Run all examples"
13
- RSpec::Core::RakeTask.new(:spec) do |t|
14
- t.rspec_opts = %w[--color]
15
- end
@@ -1,27 +0,0 @@
1
- Gem::Specification.new do |spec|
2
- spec.name = "embulk-input-jira"
3
- spec.version = "0.2.5"
4
- spec.authors = ["uu59", "yoshihara"]
5
- spec.summary = "Jira input plugin for Embulk"
6
- spec.description = "Loads records from Jira."
7
- spec.email = ["k@uu59.org", "h.yoshihara@everyleaf.com"]
8
- spec.licenses = ["Apache2"]
9
- spec.homepage = "https://github.com/treasure-data/embulk-input-jira"
10
-
11
- spec.files = `git ls-files`.split("\n") + Dir["classpath/*.jar"]
12
- spec.test_files = spec.files.grep(%r{^(test|spec)/})
13
- spec.require_paths = ["lib"]
14
-
15
- spec.add_dependency 'jiralicious', ['~> 0.5.0']
16
- spec.add_dependency 'parallel', ['~> 1.6.0']
17
- spec.add_dependency 'ruby-limiter', ['~> 1.0']
18
- spec.add_dependency 'perfect_retry', ['~> 0.3']
19
- spec.add_development_dependency 'bundler', ['~> 1.0']
20
- spec.add_development_dependency 'rake', ['< 11.0']
21
- spec.add_development_dependency 'rspec', "~> 3.2.0"
22
- spec.add_development_dependency 'embulk', ["~> 0.8.7"]
23
- spec.add_development_dependency 'simplecov'
24
- spec.add_development_dependency 'pry'
25
- spec.add_development_dependency 'codeclimate-test-reporter'
26
- spec.add_development_dependency 'everyleaf-embulk_helper'
27
- end
@@ -1,4 +0,0 @@
1
- source 'https://rubygems.org/'
2
- gemspec :path => '../'
3
-
4
- gem "embulk", "~> 0.8.0"
@@ -1,4 +0,0 @@
1
- source 'https://rubygems.org/'
2
- gemspec :path => '../'
3
-
4
- gem "embulk", "0.8.7"
@@ -1,4 +0,0 @@
1
- source 'https://rubygems.org/'
2
- gemspec :path => '../'
3
-
4
- gem "embulk", "0.8.8"
@@ -1,4 +0,0 @@
1
- source 'https://rubygems.org/'
2
- gemspec :path => '../'
3
-
4
- gem "embulk", "> 0.8.7"
@@ -1,4 +0,0 @@
1
- source 'https://rubygems.org/'
2
- gemspec :path => '../'
3
-
4
- gem "embulk", "<%= version %>"
@@ -1,9 +0,0 @@
1
- require "embulk/input/jira_api/client"
2
- require "embulk/input/jira_api/issue"
3
-
4
- module Embulk
5
- module Input
6
- module JiraApi
7
- end
8
- end
9
- end
@@ -1,144 +0,0 @@
1
- require "jiralicious"
2
- require "parallel"
3
- require "limiter"
4
- require "embulk/input/jira_api/issue"
5
- require "timeout"
6
-
7
- module Embulk
8
- module Input
9
- module JiraApi
10
- class Client
11
- MAX_RATE_LIMIT = 50
12
- MIN_RATE_LIMIT = 2
13
- # Normal http request timeout is 300s
14
- SEARCH_ISSUES_TIMEOUT_SECONDS = 300
15
- DEFAULT_SEARCH_RETRY_TIMES = 10
16
-
17
- def initialize
18
- @rate_limiter = Limiter::RateQueue.new(MAX_RATE_LIMIT, interval: 2)
19
- end
20
-
21
- def self.setup(&block)
22
- Jiralicious.configure(&block)
23
- new
24
- end
25
-
26
- def search_issues(jql, options={})
27
- issues_raw = search(jql, options).issues_raw
28
- # Maximum number of issues to retrieve is 50
29
- rate_limit = MAX_RATE_LIMIT
30
- success_items = []
31
- fail_items = []
32
- error_object = nil
33
- timeout_and_retry(SEARCH_ISSUES_TIMEOUT_SECONDS * MAX_RATE_LIMIT ) do
34
- retry_count = 0
35
- semaphore = Mutex.new
36
- @rate_limiter = Limiter::RateQueue.new(rate_limit, interval: 2)
37
- error_object = nil
38
- while issues_raw.length > 0 && retry_count <= DEFAULT_SEARCH_RETRY_TIMES do
39
- Parallel.each(issues_raw, in_threads: rate_limit) do |issue_raw|
40
- # https://github.com/dorack/jiralicious/blob/v0.4.0/lib/jiralicious/search_result.rb#L32-34
41
- begin
42
- issue = find_issue(issue_raw["key"])
43
- semaphore.synchronize {
44
- success_items.push(JiraApi::Issue.new(issue))
45
- }
46
- rescue MultiJson::ParseError => e
47
- html = e.message
48
- title = html[%r|<title>(.*?)</title>|, 1]
49
- # 401 due to high number of concurrent requests with current account
50
- # The number of concurrent requests is not fixed by every account
51
- # Hence catch the error item and retry later
52
- raise title if title != "Unauthorized (401)"
53
- semaphore.synchronize {
54
- fail_items.push(issue_raw)
55
- error_object = e
56
- }
57
- end
58
- end
59
- retry_count += 1
60
- rate_limit = calculate_rate_limit(rate_limit, issues_raw.length, fail_items.length, retry_count)
61
- issues_raw = fail_items
62
- fail_items = []
63
- raise error_object if retry_count > DEFAULT_SEARCH_RETRY_TIMES && !error_object.nil?
64
- # Sleep after some seconds for JIRA API perhaps under the overload
65
- sleep retry_count if fail_items.length > 0
66
- end
67
- success_items
68
- end
69
- end
70
-
71
- def search(jql, options={})
72
- timeout_and_retry(SEARCH_ISSUES_TIMEOUT_SECONDS) do
73
- Jiralicious.search(jql, options)
74
- end
75
- end
76
-
77
- def total_count(jql)
78
- search(jql, max_results: 1).num_results
79
- end
80
-
81
- def check_user_credential(username)
82
- Jiralicious::User.search(username)
83
- rescue Jiralicious::JqlError, Jiralicious::AuthenticationError, Jiralicious::NotLoggedIn, Jiralicious::InvalidLogin => e
84
- raise Embulk::ConfigError.new(e.message)
85
- rescue ::SocketError => e
86
- # wrong `uri` option given
87
- raise Embulk::ConfigError.new(e.message)
88
- rescue MultiJson::ParseError => e
89
- html = e.message
90
- title = html[%r|<title>(.*?)</title>|, 1] #=> e.g. "Unauthorized (401)"
91
- raise ConfigError.new("Can not authorize with your credential.") if title == 'Unauthorized (401)'
92
- end
93
-
94
- # Calculate rate limit based on previous run result
95
- # Return 2 MIN_RATE_LIMIT in case turning from the 5th times or success_items is less than 2
96
- # Otherwise return the min number between fail_items, success_items and current_limit
97
- def calculate_rate_limit(current_limit, all_items, fail_items, times)
98
- success_items = all_items - fail_items
99
- return MIN_RATE_LIMIT if times >= DEFAULT_SEARCH_RETRY_TIMES/2 || success_items < MIN_RATE_LIMIT
100
- return [fail_items, success_items, current_limit].min
101
- end
102
-
103
- private
104
-
105
- def timeout_and_retry(wait, retry_times = DEFAULT_SEARCH_RETRY_TIMES, &block)
106
- count = 0
107
- begin
108
- Timeout.timeout(wait) do
109
- yield
110
- end
111
- rescue Jiralicious::JqlError, Jiralicious::AuthenticationError, Jiralicious::NotLoggedIn, Jiralicious::InvalidLogin => e
112
- raise Embulk::ConfigError.new(e.message)
113
- rescue ::SocketError => e
114
- # wrong `uri` option given
115
- raise Embulk::ConfigError.new(e.message)
116
- rescue MultiJson::ParseError => e
117
- # same as this Mailchimp plugin issue: https://github.com/treasure-data/embulk-output-mailchimp/issues/10
118
- # (a) JIRA returns error as HTML, but HTTParty try to parse it as JSON.
119
- # And (b) `search_issues` method has race-condition bug. If it occurred, MultiJson::ParseError raised too.
120
- html = e.message
121
- title = html[%r|<title>(.*?)</title>|, 1] #=> e.g. "Unauthorized (401)"
122
- raise title if title == "Atlassian Cloud Notifications - Page Unavailable"
123
- count += 1
124
- raise title.nil? ? "Unknown Error" : title if count > retry_times
125
- Embulk.logger.warn "JIRA returns error: #{title == 'Unauthorized (401)' ? title + " due to overloading API requests. Retrying on failed items only" : title}."
126
- sleep count
127
- retry
128
- rescue Timeout::Error => e
129
- count += 1
130
- raise e if count > retry_times
131
- Embulk.logger.warn "Time out error."
132
- sleep count # retry after some seconds for JIRA API perhaps under the overload
133
- retry
134
- end
135
- end
136
-
137
- def find_issue(issue_key)
138
- @rate_limiter.shift
139
- Jiralicious::Issue.find(issue_key)
140
- end
141
- end
142
- end
143
- end
144
- end
@@ -1,133 +0,0 @@
1
- module Embulk
2
- module Input
3
- module JiraApi
4
- class Issue
5
- attr_reader :id, :key, :fields
6
-
7
- def initialize(attributes)
8
- @id = attributes.fetch("id")
9
-
10
- # https://github.com/dorack/jiralicious/blob/404b7b6d5b7020f42064cf8d7a745ab02057e728/lib/jiralicious/issue.rb#L11-L12
11
- @key = attributes.fetch("jira_key")
12
- @fields = attributes.fetch("fields")
13
- end
14
-
15
- def [](attribute)
16
- case attribute
17
- when "id"
18
- return id
19
- when "key"
20
- return key
21
- end
22
-
23
- attribute_keys = attribute.split('.')
24
-
25
- fetch(fields, attribute_keys)
26
- end
27
-
28
- def to_record
29
- @record = {}
30
-
31
- @record["id"] = id
32
- @record["key"] = key
33
-
34
- generate_record(fields, "")
35
-
36
- @record
37
- end
38
-
39
- private
40
-
41
- def fetch(fields, keys)
42
- return fields if fields.nil? || (fields.is_a?(Array) && fields.empty?)
43
-
44
- if keys.empty?
45
- case fields
46
- when Array
47
- values = fields.map do |field|
48
- if field.is_a?(String)
49
- field.to_s
50
- else
51
- field.to_json
52
- end
53
- end
54
-
55
- return values.join(",")
56
- when Hash
57
- return fields.to_json
58
- else
59
- return fields
60
- end
61
- end
62
-
63
- target_key = keys.shift
64
- if fields.is_a?(Array)
65
- values = fields.map do |field|
66
- if field.is_a?(Hash)
67
- field[target_key]
68
- else
69
- field.to_json
70
- end
71
- end
72
-
73
- fetch(values, keys)
74
- else
75
- fetch(fields[target_key], keys)
76
- end
77
- end
78
-
79
- def generate_record(value, prefix_key)
80
- case value
81
- when Hash
82
- # NOTE: If you want to flatten JSON completely, please
83
- # remove this if...end and #add_heuristic_value.
84
- if prefix_key.count(".") > 1
85
- add_heuristic_value(value, prefix_key)
86
- return
87
- end
88
-
89
- value.each_pair do |_key, _value|
90
- generate_record(_value, record_key(prefix_key, _key))
91
- end
92
- when Array
93
- if value.empty? || value.any? {|v| !v.is_a?(Hash) }
94
- @record[prefix_key] = "\"#{value.map(&:to_s).join(',')}\""
95
- return
96
- end
97
-
98
- # gathering values from each Hash
99
- keys = value.map(&:keys).inject([]) {|sum, key| sum + key }.uniq
100
- values = value.inject({}) do |sum, elem|
101
- keys.each {|key| sum[key] = (sum[key] || []) << elem[key] }
102
- sum
103
- end
104
-
105
- generate_record(values, prefix_key)
106
- else
107
- @record[prefix_key] = value
108
- end
109
- end
110
-
111
- def record_key(prefix, key)
112
- return key if prefix.empty?
113
-
114
- "#{prefix}.#{key}"
115
- end
116
-
117
- def add_heuristic_value(hash, prefix_key)
118
- heuristic_values = hash.select do |key, value|
119
- ["name", "key", "id"].include?(key) && !value.nil?
120
- end
121
-
122
- if heuristic_values.empty?
123
- @record[prefix_key] = hash.to_json
124
- else
125
- heuristic_values.each do |key, value|
126
- @record[record_key(prefix_key, key)] = value
127
- end
128
- end
129
- end
130
- end
131
- end
132
- end
133
- end
@@ -1,58 +0,0 @@
1
- # This module contains methods for Plugin.
2
-
3
- module Embulk::Guess
4
- module SchemaGuess
5
- class << self
6
-
7
- # NOTE: Original #from_hash_records uses keys of the first record only,
8
- # but JSON from JIRA API has fields which of value is sometimes nil,
9
- # sometimes JSON,
10
- # so the first record doesn't have all key for guess always.
11
- # original Embulk::Guess::SchemaGuess is https://github.com/embulk/embulk/blob/57b42c31d1d539177e1e818f294550cde5b69e1f/lib/embulk/guess/schema_guess.rb#L16-L24
12
- def from_hash_records(array_of_hash)
13
- array_of_hash = Array(array_of_hash)
14
- if array_of_hash.empty?
15
- raise "SchemaGuess Can't guess schema from no records"
16
- end
17
- column_names = array_of_hash.map(&:keys).inject([]) {|r, a| r + a }.uniq.sort
18
- samples = array_of_hash.to_a.map {|hash| column_names.map {|name| hash[name] } }
19
- from_array_records(column_names, samples)
20
- end
21
- end
22
- end
23
- end
24
-
25
- module Embulk
26
- module Input
27
- module JiraInputPluginUtils
28
- # Guess::SchemaGuess.from_hash_records returns Columns
29
- # containing 'index' key, but it is needless.
30
- def self.guess_columns(records)
31
- schema = Guess::SchemaGuess.from_hash_records(records)
32
-
33
- schema.map do |c|
34
- column = {name: c.name, type: c.type}
35
- column[:format] = c.format if c.format
36
- column
37
- end
38
- end
39
-
40
- def self.cast(value, type)
41
- return value if value.nil?
42
-
43
- case type.to_sym
44
- when :long
45
- Integer(value)
46
- when :double
47
- Float(value)
48
- when :timestamp
49
- Time.parse(value)
50
- when :boolean
51
- !!value
52
- else
53
- value.to_s
54
- end
55
- end
56
- end
57
- end
58
- end
@@ -1,89 +0,0 @@
1
- require "spec_helper"
2
-
3
- describe Embulk::Input::JiraInputPluginUtils do
4
- describe ".guess_columns" do
5
- subject do
6
- Embulk::Input::JiraInputPluginUtils.guess_columns(records)
7
- end
8
-
9
- let(:records) do
10
- [
11
- {"project.key" => "FOO-1", "comment.total" => 3, "created" => "2015-03-01T00:12:00"},
12
- {"project.id" => "1", "project.key" => "FOO", "comment.total" => 3, "created" => "2015-03-01T00:12:00"}
13
- ]
14
- end
15
-
16
- let(:expected) do
17
- [
18
- {name: "comment.total", type: :long},
19
- {name: "created", type: :timestamp, format: "%Y-%m-%dT%H:%M:%S"},
20
- {name: "project.id", type: :long},
21
- {name: "project.key", type: :string},
22
- ]
23
- end
24
-
25
- it "returns Array containing columns without 'index' key from each record" do
26
- expect(subject).to eq expected
27
- end
28
- end
29
-
30
- describe ".cast" do
31
- subject do
32
- Embulk::Input::JiraInputPluginUtils.cast(value, type)
33
- end
34
-
35
- context "when value is nil" do
36
- let(:value) { nil }
37
- let(:type) { :string }
38
-
39
- it "returns nil" do
40
- expect(subject).to be_nil
41
- end
42
- end
43
-
44
- context "when value is not nil" do
45
- let(:value) { 123 }
46
-
47
- context "and type is :string" do
48
- let(:type) { :string }
49
-
50
- it "returns '123'" do
51
- expect(subject).to eq "123"
52
- end
53
- end
54
-
55
- context "and type is :long" do
56
- let(:type) { :long }
57
-
58
- it "returns 123" do
59
- expect(subject).to eq 123
60
- end
61
- end
62
-
63
- context "and type is :double" do
64
- let(:type) { :double }
65
-
66
- it "returns 123.0" do
67
- expect(subject).to eq 123.0
68
- end
69
- end
70
-
71
- context "and type is :timestamp" do
72
- let(:value) { "2015-03-01T00:12:00" }
73
- let(:type) { :timestamp }
74
-
75
- it "returns Time object" do
76
- expect(subject).to eq Time.new(2015, 3, 1, 0, 12, 0)
77
- end
78
- end
79
-
80
- context "and type is :boolean" do
81
- let(:type) { :boolean }
82
-
83
- it "returns true" do
84
- expect(subject).to be true
85
- end
86
- end
87
- end
88
- end
89
- end