tracker_api 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +6 -0
- data/README.md +2 -2
- data/lib/tracker_api.rb +1 -0
- data/lib/tracker_api/client.rb +135 -22
- data/lib/tracker_api/endpoints/epic.rb +1 -4
- data/lib/tracker_api/endpoints/epics.rb +1 -5
- data/lib/tracker_api/endpoints/iterations.rb +1 -5
- data/lib/tracker_api/endpoints/project.rb +1 -5
- data/lib/tracker_api/endpoints/projects.rb +1 -5
- data/lib/tracker_api/endpoints/stories.rb +1 -5
- data/lib/tracker_api/endpoints/story.rb +1 -4
- data/lib/tracker_api/resources/iteration.rb +0 -3
- data/lib/tracker_api/resources/project.rb +7 -1
- data/lib/tracker_api/resources/story.rb +5 -0
- data/lib/tracker_api/version.rb +1 -1
- data/test/client_test.rb +34 -0
- data/test/project_test.rb +1 -1
- data/test/vcr/cassettes/client_done_iterations_with_pagination.json +1 -0
- data/test/vcr/cassettes/client_get_all_stories_with_pagination.json +504 -0
- data/tracker_api.gemspec +4 -4
- metadata +10 -50
- data/test/vcr/cassettes/get_done_iterations.json +0 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 415d625d6e47d65a310da72f3a89b4c8d18cf48a
|
4
|
+
data.tar.gz: 02a6be2414450453a95a2a045a94e7d6d9cebc93
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 22c7a09d01872c391e0bd0d7ce80b0efa00cd087e20a2ecacf63e0e2a099071bcf4a826be55353629010ab65838c77a0088cb7eb241dc8b68d96983246ce3714
|
7
|
+
data.tar.gz: a26439fcf3b28af52c43e737978bf8da4058bb35e953fbfae3ec2794b672911d853c0c478cf804f2524a8b8f1eebdf02b6ade4a882f380d7dbb4a7dadaaa5075
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
data/lib/tracker_api.rb
CHANGED
data/lib/tracker_api/client.rb
CHANGED
@@ -1,14 +1,18 @@
|
|
1
1
|
module TrackerApi
|
2
2
|
class Client
|
3
|
-
USER_AGENT
|
3
|
+
USER_AGENT = "Ruby/#{RUBY_VERSION} (#{RUBY_PLATFORM}; #{RUBY_ENGINE}) TrackerApi/#{TrackerApi::VERSION} Faraday/#{Faraday::VERSION}".freeze
|
4
4
|
|
5
|
-
|
5
|
+
# Header keys that can be passed in options hash to {#get},{#paginate}
|
6
|
+
CONVENIENCE_HEADERS = Set.new([:accept, :content_type])
|
7
|
+
|
8
|
+
attr_reader :url, :api_version, :token, :logger, :connection, :auto_paginate, :last_response
|
6
9
|
|
7
10
|
# Create Pivotal Tracker API client.
|
8
11
|
#
|
9
12
|
# @param [Hash] options the connection options
|
10
13
|
# @option options [String] :token API token to use for requests
|
11
14
|
# @option options [String] :url Main HTTP API root
|
15
|
+
# @option options [Boolean] :auto_paginate Client should perform pagination automatically. Default true.
|
12
16
|
# @option options [String] :api_version The API version URL path
|
13
17
|
# @option options [String] :logger Custom logger
|
14
18
|
# @option options [String] :adapter Custom http adapter to configure Faraday with
|
@@ -17,15 +21,14 @@ module TrackerApi
|
|
17
21
|
# @example Creating a Client
|
18
22
|
# Client.new token: 'my-super-special-token'
|
19
23
|
def initialize(options={})
|
20
|
-
url
|
21
|
-
@url
|
22
|
-
|
23
|
-
@
|
24
|
-
|
25
|
-
|
26
|
-
|
24
|
+
url = options.fetch(:url, 'https://www.pivotaltracker.com')
|
25
|
+
@url = Addressable::URI.parse(url).to_s
|
26
|
+
@api_version = options.fetch(:api_version, '/services/v5')
|
27
|
+
@logger = options.fetch(:logger, Logger.new(nil))
|
28
|
+
adapter = options.fetch(:adapter, :net_http)
|
29
|
+
connection_options = options.fetch(:connection_options, { ssl: { verify: true } })
|
30
|
+
@auto_paginate = options.fetch(:auto_paginate, true)
|
27
31
|
@token = options[:token]
|
28
|
-
|
29
32
|
raise 'Missing required options: :token' unless @token
|
30
33
|
|
31
34
|
@connection = Faraday.new({ url: @url }.merge(connection_options)) do |builder|
|
@@ -42,30 +45,140 @@ module TrackerApi
|
|
42
45
|
end
|
43
46
|
end
|
44
47
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
48
|
+
# Make a HTTP GET request
|
49
|
+
#
|
50
|
+
# @param path [String] The path, relative to api endpoint
|
51
|
+
# @param options [Hash] Query and header params for request
|
52
|
+
# @return [Faraday::Response]
|
53
|
+
def get(path, options = {})
|
54
|
+
request(:get, parse_query_and_convenience_headers(path, options))
|
55
|
+
end
|
56
|
+
|
57
|
+
# Make one or more HTTP GET requests, optionally fetching
|
58
|
+
# the next page of results from information passed back in headers
|
59
|
+
# based on value in {#auto_paginate}.
|
60
|
+
#
|
61
|
+
# @param path [String] The path, relative to {#api_endpoint}
|
62
|
+
# @param options [Hash] Query and header params for request
|
63
|
+
# @param block [Block] Block to perform the data concatenation of the
|
64
|
+
# multiple requests. The block is called with two parameters, the first
|
65
|
+
# contains the contents of the requests so far and the second parameter
|
66
|
+
# contains the latest response.
|
67
|
+
# @return [Array]
|
68
|
+
def paginate(path, options = {}, &block)
|
69
|
+
opts = parse_query_and_convenience_headers path, options.dup
|
70
|
+
@last_response = request :get, opts
|
71
|
+
data = @last_response.body
|
72
|
+
raise TrackerApi::Errors::UnexpectedData, 'Array expected' unless data.is_a? Array
|
73
|
+
|
74
|
+
if @auto_paginate
|
75
|
+
pager = Pagination.new @last_response.headers
|
76
|
+
|
77
|
+
while pager.more?
|
78
|
+
opts[:params].update(pager.next_page_params)
|
79
|
+
|
80
|
+
@last_response = request :get, opts
|
81
|
+
pager = Pagination.new @last_response.headers
|
82
|
+
if block_given?
|
83
|
+
yield(data, @last_response)
|
84
|
+
else
|
85
|
+
data.concat(@last_response.body) if @last_response.body.is_a?(Array)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
data
|
91
|
+
end
|
92
|
+
|
93
|
+
# Get projects
|
94
|
+
#
|
95
|
+
# @param [Hash] params
|
96
|
+
# @return [Array[TrackerApi::Resources::Project]]
|
97
|
+
def projects(params={})
|
98
|
+
Endpoints::Projects.new(self).get(params)
|
99
|
+
end
|
100
|
+
|
101
|
+
# Get project
|
102
|
+
#
|
103
|
+
# @param [Hash] params
|
104
|
+
# @return [TrackerApi::Resources::Project]
|
105
|
+
def project(id, params={})
|
106
|
+
Endpoints::Project.new(self).get(id, params)
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
|
111
|
+
def parse_query_and_convenience_headers(path, options)
|
112
|
+
raise 'Path can not be blank.' if path.to_s.empty?
|
113
|
+
|
114
|
+
opts = { body: options[:body] }
|
115
|
+
|
116
|
+
opts[:url] = options[:url] || File.join(@url, @api_version, path.to_s)
|
117
|
+
opts[:method] = options[:method] || :get
|
118
|
+
opts[:params] = options[:params] || {}
|
119
|
+
opts[:token] = options[:token] || @token
|
120
|
+
headers = { 'User-Agent' => USER_AGENT,
|
121
|
+
'X-TrackerToken' => opts.fetch(:token) }.merge(options.fetch(:headers, {}))
|
122
|
+
|
123
|
+
CONVENIENCE_HEADERS.each do |h|
|
124
|
+
if header = options[h]
|
125
|
+
headers[h] = header
|
126
|
+
end
|
127
|
+
end
|
128
|
+
opts[:headers] = headers
|
129
|
+
|
130
|
+
opts
|
131
|
+
end
|
132
|
+
|
133
|
+
def request(method, options = {})
|
134
|
+
url = options.fetch(:url)
|
49
135
|
params = options[:params] || {}
|
50
136
|
body = options[:body]
|
51
|
-
headers =
|
137
|
+
headers = options[:headers]
|
52
138
|
|
53
|
-
connection.send(method) do |req|
|
54
|
-
req.url
|
139
|
+
@last_response = response = connection.send(method) do |req|
|
140
|
+
req.url(url)
|
55
141
|
req.headers.merge!(headers)
|
56
142
|
req.params.merge!(params)
|
57
143
|
req.body = body
|
58
144
|
end
|
145
|
+
response
|
59
146
|
rescue Faraday::Error::ClientError => e
|
60
147
|
raise TrackerApi::Error.new(e)
|
61
148
|
end
|
62
149
|
|
63
|
-
|
64
|
-
|
65
|
-
end
|
150
|
+
class Pagination
|
151
|
+
attr_accessor :headers, :total, :limit, :offset, :returned
|
66
152
|
|
67
|
-
|
68
|
-
|
153
|
+
def initialize(headers)
|
154
|
+
@headers = headers
|
155
|
+
@total = headers['x-tracker-pagination-total'].to_i
|
156
|
+
@limit = headers['x-tracker-pagination-limit'].to_i
|
157
|
+
@offset = headers['x-tracker-pagination-offset'].to_i
|
158
|
+
@returned = headers['x-tracker-pagination-returned'].to_i
|
159
|
+
|
160
|
+
# if offset is negative (e.g. Iterations Endpoint).
|
161
|
+
# For the 'Done' scope, negative numbers can be passed, which
|
162
|
+
# specifies the number of iterations preceding the 'Current' iteration.
|
163
|
+
# then need to adjust the negative offset to account for a smaller total,
|
164
|
+
# and set total to zero since we are paginating from -X to 0.
|
165
|
+
if @offset < 0
|
166
|
+
@offset = -@total if @offset.abs > @total
|
167
|
+
@total = 0
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
def more?
|
172
|
+
(offset + limit) < total
|
173
|
+
end
|
174
|
+
|
175
|
+
def next_offset
|
176
|
+
offset + limit
|
177
|
+
end
|
178
|
+
|
179
|
+
def next_page_params
|
180
|
+
{ limit: limit, offset: next_offset }
|
181
|
+
end
|
69
182
|
end
|
70
183
|
end
|
71
184
|
end
|
@@ -8,10 +8,7 @@ module TrackerApi
|
|
8
8
|
end
|
9
9
|
|
10
10
|
def get(project_id, id)
|
11
|
-
data = client.
|
12
|
-
method: :get,
|
13
|
-
:path => "/projects/#{project_id}/epics/#{id}"
|
14
|
-
).body
|
11
|
+
data = client.get("/projects/#{project_id}/epics/#{id}").body
|
15
12
|
|
16
13
|
Resources::Epic.new({ client: client }.merge(data))
|
17
14
|
end
|
@@ -8,11 +8,7 @@ module TrackerApi
|
|
8
8
|
end
|
9
9
|
|
10
10
|
def get(project_id, params={})
|
11
|
-
data = client.
|
12
|
-
method: :get,
|
13
|
-
path: "/projects/#{project_id}/epics",
|
14
|
-
params: params
|
15
|
-
).body
|
11
|
+
data = client.paginate("/projects/#{project_id}/epics", params: params)
|
16
12
|
raise TrackerApi::Errors::UnexpectedData, 'Array of epics expected' unless data.is_a? Array
|
17
13
|
|
18
14
|
data.map { |epic| Resources::Epic.new({ client: client }.merge(epic)) }
|
@@ -8,11 +8,7 @@ module TrackerApi
|
|
8
8
|
end
|
9
9
|
|
10
10
|
def get(project_id, params={})
|
11
|
-
data = client.
|
12
|
-
method: :get,
|
13
|
-
path: "/projects/#{project_id}/iterations",
|
14
|
-
params: params
|
15
|
-
).body
|
11
|
+
data = client.paginate("/projects/#{project_id}/iterations", params: params)
|
16
12
|
raise TrackerApi::Errors::UnexpectedData, 'Array of iterations expected' unless data.is_a? Array
|
17
13
|
|
18
14
|
data.map { |iteration| Resources::Iteration.new({ client: client }.merge(iteration)) }
|
@@ -8,11 +8,7 @@ module TrackerApi
|
|
8
8
|
end
|
9
9
|
|
10
10
|
def get(id, params={})
|
11
|
-
data = client.
|
12
|
-
method: :get,
|
13
|
-
path: "/projects/#{id}",
|
14
|
-
params: params
|
15
|
-
).body
|
11
|
+
data = client.get("/projects/#{id}", params: params).body
|
16
12
|
|
17
13
|
Resources::Project.new({ client: client }.merge(data))
|
18
14
|
end
|
@@ -8,11 +8,7 @@ module TrackerApi
|
|
8
8
|
end
|
9
9
|
|
10
10
|
def get(params={})
|
11
|
-
data = client.
|
12
|
-
method: :get,
|
13
|
-
path: '/projects',
|
14
|
-
params: params
|
15
|
-
).body
|
11
|
+
data = client.paginate('/projects', params: params)
|
16
12
|
raise TrackerApi::Errors::UnexpectedData, 'Array of projects expected' unless data.is_a? Array
|
17
13
|
|
18
14
|
data.map { |project| Resources::Project.new({ client: client }.merge(project)) }
|
@@ -8,11 +8,7 @@ module TrackerApi
|
|
8
8
|
end
|
9
9
|
|
10
10
|
def get(project_id, params={})
|
11
|
-
data = client.
|
12
|
-
method: :get,
|
13
|
-
path: "/projects/#{project_id}/stories",
|
14
|
-
params: params
|
15
|
-
).body
|
11
|
+
data = client.paginate("/projects/#{project_id}/stories", params: params)
|
16
12
|
raise TrackerApi::Errors::UnexpectedData, 'Array of stories expected' unless data.is_a? Array
|
17
13
|
|
18
14
|
data.map { |story| Resources::Story.new({ client: client }.merge(story)) }
|
@@ -8,10 +8,7 @@ module TrackerApi
|
|
8
8
|
end
|
9
9
|
|
10
10
|
def get(project_id, id)
|
11
|
-
data = client.
|
12
|
-
method: :get,
|
13
|
-
:path => "/projects/#{project_id}/stories/#{id}"
|
14
|
-
).body
|
11
|
+
data = client.get("/projects/#{project_id}/stories/#{id}").body
|
15
12
|
|
16
13
|
Resources::Story.new({ client: client }.merge(data))
|
17
14
|
end
|
@@ -5,9 +5,7 @@ module TrackerApi
|
|
5
5
|
|
6
6
|
attribute :client
|
7
7
|
|
8
|
-
attribute :created_at, DateTime
|
9
8
|
attribute :finish, DateTime
|
10
|
-
attribute :id, Integer
|
11
9
|
attribute :kind, String
|
12
10
|
attribute :length, Integer
|
13
11
|
attribute :number, Integer
|
@@ -17,7 +15,6 @@ module TrackerApi
|
|
17
15
|
attribute :stories, [TrackerApi::Resources::Story]
|
18
16
|
attribute :story_ids, [Integer]
|
19
17
|
attribute :team_strength, Float
|
20
|
-
attribute :updated_at, DateTime
|
21
18
|
end
|
22
19
|
end
|
23
20
|
end
|
@@ -11,6 +11,7 @@ module TrackerApi
|
|
11
11
|
attribute :bugs_and_chores_are_estimatable, Boolean
|
12
12
|
attribute :created_at, DateTime
|
13
13
|
attribute :current_iteration_number, Integer
|
14
|
+
attribute :current_velocity, Integer
|
14
15
|
attribute :description, String
|
15
16
|
attribute :enable_following, Boolean
|
16
17
|
attribute :enable_incoming_emails, Boolean
|
@@ -39,6 +40,11 @@ module TrackerApi
|
|
39
40
|
attribute :version, Integer
|
40
41
|
attribute :week_start_day, String
|
41
42
|
|
43
|
+
# @return [String] Comma separated list of labels.
|
44
|
+
def label_list
|
45
|
+
@label_list ||= labels.collect(&:name).join(',')
|
46
|
+
end
|
47
|
+
|
42
48
|
# @return [Array[Epic]] epics associated with this project
|
43
49
|
def epics(params={})
|
44
50
|
raise ArgumentError, 'Expected @epics to be an Array' unless @epics.is_a? Array
|
@@ -48,7 +54,7 @@ module TrackerApi
|
|
48
54
|
end
|
49
55
|
|
50
56
|
# @param [Hash] params
|
51
|
-
# @option params [String] :scope
|
57
|
+
# @option params [String] :scope Restricts the state of iterations to return.
|
52
58
|
# If not specified, it defaults to all iterations including done.
|
53
59
|
# Valid enumeration values: done, current, backlog, current_backlog.
|
54
60
|
# @option params [Integer] :offset The offset of first iteration to return, relative to the
|
@@ -28,6 +28,11 @@ module TrackerApi
|
|
28
28
|
attribute :task_ids, Array[Integer]
|
29
29
|
attribute :updated_at, DateTime
|
30
30
|
attribute :url, String
|
31
|
+
|
32
|
+
# @return [String] Comma separated list of labels.
|
33
|
+
def label_list
|
34
|
+
@label_list ||= labels.collect(&:name).join(',')
|
35
|
+
end
|
31
36
|
end
|
32
37
|
end
|
33
38
|
end
|
data/lib/tracker_api/version.rb
CHANGED
data/test/client_test.rb
CHANGED
@@ -58,4 +58,38 @@ describe TrackerApi::Client do
|
|
58
58
|
end
|
59
59
|
end
|
60
60
|
end
|
61
|
+
|
62
|
+
describe '.paginate' do
|
63
|
+
let(:pt_user) { PT_USER_1 }
|
64
|
+
let(:client) { TrackerApi::Client.new token: pt_user[:token] }
|
65
|
+
let(:project_id) { pt_user[:project_id] }
|
66
|
+
|
67
|
+
it 'auto paginates when needed' do
|
68
|
+
VCR.use_cassette('client: get all stories with pagination', record: :new_episodes) do
|
69
|
+
project = client.project(project_id)
|
70
|
+
|
71
|
+
# skip pagination with a hugh limit
|
72
|
+
unpaged_stories = project.stories(limit: 300)
|
73
|
+
unpaged_stories.wont_be_empty
|
74
|
+
unpaged_stories.length.must_be :>, 7
|
75
|
+
|
76
|
+
# force pagination with a small limit
|
77
|
+
paged_stories = project.stories(limit: 7)
|
78
|
+
paged_stories.wont_be_empty
|
79
|
+
paged_stories.length.must_equal unpaged_stories.length
|
80
|
+
paged_stories.map(&:id).sort.uniq.must_equal unpaged_stories.map(&:id).sort.uniq
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
it 'can handle negative offsets' do
|
85
|
+
VCR.use_cassette('client: done iterations with pagination', record: :new_episodes) do
|
86
|
+
project = client.project(project_id)
|
87
|
+
|
88
|
+
done_iterations = project.iterations(scope: :done, offset: -12, limit: 5)
|
89
|
+
|
90
|
+
done_iterations.wont_be_empty
|
91
|
+
done_iterations.length.must_be :<=, 12
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
61
95
|
end
|
data/test/project_test.rb
CHANGED
@@ -66,7 +66,7 @@ describe TrackerApi::Resources::Project do
|
|
66
66
|
end
|
67
67
|
|
68
68
|
describe '.stories' do
|
69
|
-
it 'can get unscheduled stories
|
69
|
+
it 'can get unscheduled stories' do
|
70
70
|
VCR.use_cassette('get unscheduled stories', record: :new_episodes) do
|
71
71
|
stories = project.stories(with_state: :unscheduled)
|
72
72
|
|