Skip to content

Commit 6fd3874

Browse files
committed
add rate limit protection
1 parent 68d45b7 commit 6fd3874

File tree

4 files changed

+75
-28
lines changed

4 files changed

+75
-28
lines changed

Diff for: lib/codeship_api.rb

+2-2
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
module CodeshipApi
1414
ROOT = URI('https://api.codeship.com/v2/')
1515

16-
USERNAME = ENV.fetch('CODESHIP_API_USERNAME')
17-
PASSWORD = ENV.fetch('CODESHIP_API_PASSWORD')
16+
USERNAME = ENV.fetch('CODESHIP_API_USERNAME', nil)
17+
PASSWORD = ENV.fetch('CODESHIP_API_PASSWORD', nil)
1818

1919
class << self
2020
def client

Diff for: lib/codeship_api/build.rb

+19
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,25 @@ class Build < Base
1212
end
1313
end
1414

15+
def self.find_all_by_project(
16+
project,
17+
page: 1, builds: [],
18+
filter_by: -> (build) { true },
19+
stop_when: -> (new_builds) { new_builds.empty? }
20+
)
21+
new_builds = find_by_project(project, page: page)
22+
builds += new_builds.select(&filter_by)
23+
24+
if stop_when.call(new_builds)
25+
builds
26+
else
27+
find_all_by_project(
28+
project, page: page + 1, builds: builds,
29+
stop_when: stop_when, filter_by: filter_by
30+
)
31+
end
32+
end
33+
1534
def self.find_by_project(project, per_page: 50, page: 1)
1635
uri = "#{project.uri}/builds?per_page=#{per_page}&page=#{page}"
1736
CodeshipApi.client.get(uri)["builds"].map {|build| new(build) }

Diff for: lib/codeship_api/client.rb

+51-23
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
module CodeshipApi
22
class Client
3+
class RateLimitExceededError < StandardError; end
4+
35
def initialize(username=USERNAME, password=PASSWORD)
46
@username = username
57
@password = password
@@ -10,21 +12,27 @@ def authenticated?
1012
end
1113

1214
def get(path)
13-
uri = URI.join(ROOT, path.sub(/^\//, ""))
15+
rate_limit_protected! do
16+
uri = URI.join(ROOT, path.sub(/^\//, ""))
1417

15-
request = Net::HTTP::Get.new(uri, default_headers)
16-
response = http.request(request)
18+
request = Net::HTTP::Get.new(uri, default_headers)
19+
response = http.request(request)
1720

18-
parsed_json_from(response)
21+
parse_errors_from(response)
22+
parsed_json_from(response)
23+
end
1924
end
2025

2126
def post(path)
22-
uri = URI.join(ROOT, path.sub(/^\//, ""))
27+
rate_limit_protected! do
28+
uri = URI.join(ROOT, path.sub(/^\//, ""))
2329

24-
request = Net::HTTP::Post.new(uri, default_headers)
25-
response = http.request(request)
30+
request = Net::HTTP::Post.new(uri, default_headers)
31+
response = http.request(request)
2632

27-
parsed_json_from(response)
33+
parse_errors_from(response)
34+
parsed_json_from(response)
35+
end
2836
end
2937

3038
def organizations
@@ -39,6 +47,20 @@ def projects
3947

4048
attr_reader :username, :password
4149

50+
def rate_limit_protected!(&block)
51+
time_start = Time.now
52+
block.call.tap do
53+
duration = Time.now - time_start
54+
sleep 1.second - duration if duration < 1.second
55+
end
56+
rescue RateLimitExceededError => e
57+
sleep_duration = 60
58+
print "waiting #{sleep_duration}s for rate limit..."
59+
sleep sleep_duration
60+
puts " done."
61+
rate_limit_protected!(&block)
62+
end
63+
4264
def token
4365
authenticate unless authenticated?
4466
authentication.access_token
@@ -49,18 +71,20 @@ def authentication
4971
end
5072

5173
def authenticate
52-
uri = URI.join(ROOT, 'auth')
53-
54-
request = Net::HTTP::Post.new(uri.request_uri, {
55-
'Content-Type' => 'application/json',
56-
'Accept' => 'application/json'
57-
}).tap do |req|
58-
req.basic_auth(username, password)
59-
end
74+
rate_limit_protected! do
75+
uri = URI.join(ROOT, 'auth')
76+
77+
request = Net::HTTP::Post.new(uri.request_uri, {
78+
'Content-Type' => 'application/json',
79+
'Accept' => 'application/json'
80+
}).tap do |req|
81+
req.basic_auth(username, password)
82+
end
6083

61-
response = http.request(request)
84+
response = http.request(request)
6285

63-
@authentication = Authentication.new(JSON.parse(response.body))
86+
@authentication = Authentication.new(JSON.parse(response.body))
87+
end
6488
end
6589

6690
def http
@@ -79,12 +103,16 @@ def default_headers
79103
}
80104
end
81105

82-
def parsed_json_from(response)
83-
if response.body.length > 0
84-
JSON.parse(response.body)
85-
else
86-
nil
106+
def parse_errors_from(response)
107+
if response.code.to_i == 403 && response.entity == "Rate Limit Exceeded"
108+
raise RateLimitExceededError.new(response)
87109
end
88110
end
111+
112+
def parsed_json_from(response)
113+
return nil unless response.body.length > 0
114+
115+
JSON.parse(response.body)
116+
end
89117
end
90118
end

Diff for: test/codeship_api/client_test.rb

+3-3
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
describe CodeshipApi::Client do
44
before do
55
@client = CodeshipApi::Client.new
6-
@fake_success_response = Struct.new(:body).new('{"status": "OK"}')
7-
@fake_null_response = Struct.new(:body).new('')
6+
@fake_success_response = Struct.new(:code, :body).new("200", '{"status": "OK"}')
7+
@fake_null_response = Struct.new(:code, :body).new("200", '')
88
@mock_http = Minitest::Mock.new
99

1010
def @client.token
@@ -18,7 +18,7 @@ def @client.token
1818
expires_at = Time.now + (10 * 60 * 60) # 10 minutes from now
1919
org = {uuid: "asdf-asdf-asdf-asdf", name: "fake_org", scopes: []}
2020

21-
auth_response = Struct.new(:body).new({
21+
auth_response = Struct.new(:code, :body).new("200", {
2222
access_token: token,
2323
expires_at: expires_at.to_i,
2424
organizations: [org]

0 commit comments

Comments
 (0)