embulk-input-jira 0.2.5 → 0.2.6

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.
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