tracker_api 0.1.0 → 0.2.0
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.
- 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
|
|