procore 0.6.7
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 +7 -0
- data/.gitignore +11 -0
- data/.travis.yml +27 -0
- data/CHANGELOG.md +46 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +407 -0
- data/Rakefile +9 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/procore.rb +21 -0
- data/lib/procore/auth/access_token_credentials.rb +35 -0
- data/lib/procore/auth/client_credentials.rb +52 -0
- data/lib/procore/auth/stores/active_record.rb +41 -0
- data/lib/procore/auth/stores/file.rb +32 -0
- data/lib/procore/auth/stores/memory.rb +29 -0
- data/lib/procore/auth/stores/redis.rb +42 -0
- data/lib/procore/auth/stores/session.rb +38 -0
- data/lib/procore/auth/token.rb +20 -0
- data/lib/procore/client.rb +86 -0
- data/lib/procore/configuration.rb +88 -0
- data/lib/procore/defaults.rb +19 -0
- data/lib/procore/errors.rb +63 -0
- data/lib/procore/requestable.rb +217 -0
- data/lib/procore/response.rb +78 -0
- data/lib/procore/util.rb +52 -0
- data/lib/procore/version.rb +3 -0
- data/procore.gemspec +37 -0
- metadata +239 -0
@@ -0,0 +1,19 @@
|
|
1
|
+
require "procore/version"
|
2
|
+
|
3
|
+
module Procore
|
4
|
+
# Specifies some sensible defaults for certain configurations + clients
|
5
|
+
class Defaults
|
6
|
+
# Default API endpoint
|
7
|
+
API_ENDPOINT = "https://app.procore.com".freeze
|
8
|
+
|
9
|
+
# Default User Agent header string
|
10
|
+
USER_AGENT = "Procore Ruby Gem #{Procore::VERSION}".freeze
|
11
|
+
|
12
|
+
def self.client_options
|
13
|
+
{
|
14
|
+
host: Procore.configuration.host,
|
15
|
+
user_agent: Procore.configuration.user_agent,
|
16
|
+
}
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module Procore
|
2
|
+
# Generic / catch all error class. All other errors generated by the gem
|
3
|
+
# inherit from this class.
|
4
|
+
class Error < StandardError
|
5
|
+
# Human readable error message.
|
6
|
+
attr_reader :message
|
7
|
+
|
8
|
+
# A Response object which contains details about the request.
|
9
|
+
attr_reader :response
|
10
|
+
|
11
|
+
def initialize(message, response: nil)
|
12
|
+
@message = message
|
13
|
+
@response = response
|
14
|
+
|
15
|
+
super(message)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# Raised when the gem cannot connect to the Procore API. Possible causes:
|
20
|
+
# Procore is down or the network is doing something funny.
|
21
|
+
APIConnectionError = Class.new(Error)
|
22
|
+
|
23
|
+
# Raised when the request is attempting to access a resource the token's
|
24
|
+
# owner does not have access to.
|
25
|
+
AuthorizationError = Class.new(Error)
|
26
|
+
|
27
|
+
# Raised when the request is incorrectly formated. Possible causes: missing
|
28
|
+
# required parameters or sending a request to access a non-existent resource.
|
29
|
+
InvalidRequestError = Class.new(Error)
|
30
|
+
|
31
|
+
# Raised when the request 404's
|
32
|
+
NotFoundError = Class.new(Error)
|
33
|
+
|
34
|
+
# Raised whenever there is a problem with OAuth. Possible causes: required
|
35
|
+
# credentials are missing or an access token failed to refresh.
|
36
|
+
class OAuthError < Error
|
37
|
+
def initialize(message, response: nil)
|
38
|
+
@message = message
|
39
|
+
|
40
|
+
duck_response = if response
|
41
|
+
OpenStruct.new(
|
42
|
+
code: response.status,
|
43
|
+
body: response.parsed.presence || response.body,
|
44
|
+
headers: response.headers,
|
45
|
+
request: OpenStruct.new(
|
46
|
+
options: {},
|
47
|
+
path: nil
|
48
|
+
),
|
49
|
+
)
|
50
|
+
end
|
51
|
+
|
52
|
+
super(message, response: duck_response)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Raised when a token reaches it's request limit for the current time period.
|
57
|
+
# If you are receiving this error then you are making too many requests
|
58
|
+
# against the Procore API.
|
59
|
+
RateLimitError = Class.new(Error)
|
60
|
+
|
61
|
+
# Raised when a Procore endpoint returns a 500x resonse code.
|
62
|
+
ServerError = Class.new(Error)
|
63
|
+
end
|
@@ -0,0 +1,217 @@
|
|
1
|
+
require "httparty"
|
2
|
+
|
3
|
+
module Procore
|
4
|
+
# Module which defines HTTP verbs GET, POST, PATCH and DELETE. Is included in
|
5
|
+
# Client. Has support for Idempotency Tokens on POST and PATCH.
|
6
|
+
#
|
7
|
+
# @example Using #get:
|
8
|
+
# client.get("my_open_items", per_page: 5)
|
9
|
+
#
|
10
|
+
# @example Using #post:
|
11
|
+
# client.post("projects", name: "New Project")
|
12
|
+
module Requestable
|
13
|
+
# @param path [String] URL path
|
14
|
+
# @param query [Hash] Query options to pass along with the request
|
15
|
+
#
|
16
|
+
# @example Usage
|
17
|
+
# client.get("my_open_items", per_page: 5, filter: {})
|
18
|
+
#
|
19
|
+
# @return [Response]
|
20
|
+
def get(path, query = {})
|
21
|
+
Util.log_info(
|
22
|
+
"API Request Initiated",
|
23
|
+
path: "#{base_api_path}/#{path}",
|
24
|
+
method: "GET",
|
25
|
+
query: "#{query}",
|
26
|
+
)
|
27
|
+
|
28
|
+
with_response_handling do
|
29
|
+
HTTParty.get(
|
30
|
+
"#{base_api_path}/#{path}",
|
31
|
+
query: query,
|
32
|
+
headers: headers,
|
33
|
+
timeout: Procore.configuration.timeout,
|
34
|
+
)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# @param path [String] URL path
|
39
|
+
# @param body [Hash] Body parameters to send with the request
|
40
|
+
# @param options [Hash} Extra request options
|
41
|
+
# TODO Add description for idempotency key
|
42
|
+
# @option options [String] :idempotency_token
|
43
|
+
#
|
44
|
+
# @example Usage
|
45
|
+
# client.post("users", { name: "New User" }, { idempotency_token: "key" })
|
46
|
+
#
|
47
|
+
# @return [Response]
|
48
|
+
def post(path, body = {}, options = {})
|
49
|
+
Util.log_info(
|
50
|
+
"API Request Initiated",
|
51
|
+
path: "#{base_api_path}/#{path}",
|
52
|
+
method: "POST",
|
53
|
+
body: "#{body}",
|
54
|
+
)
|
55
|
+
|
56
|
+
with_response_handling do
|
57
|
+
HTTParty.post(
|
58
|
+
"#{base_api_path}/#{path}",
|
59
|
+
body: body.to_json,
|
60
|
+
headers: headers(options),
|
61
|
+
timeout: Procore.configuration.timeout,
|
62
|
+
)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# @param path [String] URL path
|
67
|
+
# @param body [Hash] Body parameters to send with the request
|
68
|
+
# @param options [Hash} Extra request options
|
69
|
+
# TODO Add description for idempotency token
|
70
|
+
# @option options [String] :idempotency_token
|
71
|
+
#
|
72
|
+
# client.patch("users/1", { name: "Updated" }, { idempotency_token: "key" })
|
73
|
+
#
|
74
|
+
# @return [Response]
|
75
|
+
def patch(path, body = {}, options = {})
|
76
|
+
Util.log_info(
|
77
|
+
"API Request Initiated",
|
78
|
+
path: "#{base_api_path}/#{path}",
|
79
|
+
method: "PATCH",
|
80
|
+
body: "#{body}",
|
81
|
+
)
|
82
|
+
|
83
|
+
with_response_handling do
|
84
|
+
HTTParty.patch(
|
85
|
+
"#{base_api_path}/#{path}",
|
86
|
+
body: body.to_json,
|
87
|
+
headers: headers(options),
|
88
|
+
timeout: Procore.configuration.timeout,
|
89
|
+
)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# @param path [String] URL path
|
94
|
+
# @param query [Hash] Query options to pass along with the request
|
95
|
+
#
|
96
|
+
# @example Usage
|
97
|
+
# client.delete("users/1")
|
98
|
+
#
|
99
|
+
# @return [Response]
|
100
|
+
def delete(path, query = {}, options = {})
|
101
|
+
Util.log_info(
|
102
|
+
"API Request Initiated",
|
103
|
+
path: "#{base_api_path}/#{path}",
|
104
|
+
method: "DELETE",
|
105
|
+
query: "#{query}",
|
106
|
+
)
|
107
|
+
|
108
|
+
with_response_handling do
|
109
|
+
HTTParty.delete(
|
110
|
+
"#{base_api_path}/#{path}",
|
111
|
+
query: query,
|
112
|
+
headers: headers,
|
113
|
+
timeout: Procore.configuration.timeout,
|
114
|
+
)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
private
|
119
|
+
|
120
|
+
def with_response_handling
|
121
|
+
request_start_time = Time.now
|
122
|
+
retries = 0
|
123
|
+
|
124
|
+
begin
|
125
|
+
result = yield
|
126
|
+
rescue Timeout::Error, Errno::ECONNREFUSED => e
|
127
|
+
if retries <= Procore.configuration.max_retries
|
128
|
+
retries += 1
|
129
|
+
sleep 1.5 ** retries
|
130
|
+
retry
|
131
|
+
else
|
132
|
+
raise APIConnectionError.new(
|
133
|
+
"Cannot connect to the Procore API. Double check your timeout " \
|
134
|
+
"settings to ensure requests are not being cancelled before they " \
|
135
|
+
"can complete. Try setting the timeout and max_retries to larger " \
|
136
|
+
"values."
|
137
|
+
), e
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
response = Procore::Response.new(
|
142
|
+
body: result.body,
|
143
|
+
headers: result.headers,
|
144
|
+
code: result.code,
|
145
|
+
request: result.request,
|
146
|
+
)
|
147
|
+
|
148
|
+
case result.code
|
149
|
+
when 200..299
|
150
|
+
Util.log_info(
|
151
|
+
"API Request Finished ",
|
152
|
+
path: result.request.path,
|
153
|
+
status: "#{result.code}",
|
154
|
+
duration: "#{((Time.now - request_start_time) * 1000).round(0)}ms",
|
155
|
+
request_id: result.headers["x-request-id"],
|
156
|
+
)
|
157
|
+
else
|
158
|
+
Util.log_error(
|
159
|
+
"API Request Failed",
|
160
|
+
path: result.request.path,
|
161
|
+
status: "#{result.code}",
|
162
|
+
duration: "#{((Time.now - request_start_time) * 1000).round(0)}ms",
|
163
|
+
request_id: result.headers["x-request-id"],
|
164
|
+
retries: retries
|
165
|
+
)
|
166
|
+
end
|
167
|
+
|
168
|
+
case result.code
|
169
|
+
when 200..299
|
170
|
+
response
|
171
|
+
when 401
|
172
|
+
raise Procore::AuthorizationError.new(
|
173
|
+
"The request failed because you lack the correct credentials to " \
|
174
|
+
"access the target resource",
|
175
|
+
response: response,
|
176
|
+
)
|
177
|
+
when 404
|
178
|
+
raise Procore::NotFoundError.new(
|
179
|
+
"The URI requested is invalid or the resource requested does not " \
|
180
|
+
"exist.",
|
181
|
+
response: response,
|
182
|
+
)
|
183
|
+
when 422
|
184
|
+
raise Procore::InvalidRequestError.new(
|
185
|
+
"Bad Request.",
|
186
|
+
response: response,
|
187
|
+
)
|
188
|
+
when 429
|
189
|
+
raise Procore::RateLimitError.new(
|
190
|
+
"You have surpassed the max number of requests for an hour. Please " \
|
191
|
+
"wait until your limit resets.",
|
192
|
+
response: response,
|
193
|
+
)
|
194
|
+
else
|
195
|
+
raise Procore::ServerError.new(
|
196
|
+
"Something is broken. This is usually a temporary error - Procore " \
|
197
|
+
"may be down or this endpoint may be having issues. Check " \
|
198
|
+
"http://status.procore.com for any known or ongoing issues.",
|
199
|
+
response: response,
|
200
|
+
)
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
def headers(options = {})
|
205
|
+
{
|
206
|
+
"Accepts" => "application/json",
|
207
|
+
"Authorization" => "Bearer #{access_token}",
|
208
|
+
"Content-Type" => "application/json",
|
209
|
+
"User-Agent" => Procore.configuration.user_agent
|
210
|
+
}.tap do |headers|
|
211
|
+
if options[:idempotency_token]
|
212
|
+
headers["Idempotency-Token"] = options[:idempotency_token]
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module Procore
|
2
|
+
# Wrapper class for a response received from the Procore API. Stores the
|
3
|
+
# body, code, headers, and pagination information.
|
4
|
+
#
|
5
|
+
# @example Getting the details about the response.
|
6
|
+
# response = client.get("projects")
|
7
|
+
# response.body #=> [{ id: 5, name: "Project 5" }]
|
8
|
+
# response.code #=> 200
|
9
|
+
#
|
10
|
+
# When a response returns a collection of elements, a Link Header is included
|
11
|
+
# in the response. This header contains one or more URLs that can be used to
|
12
|
+
# access more results.
|
13
|
+
#
|
14
|
+
# The possible values for pagination are:
|
15
|
+
#
|
16
|
+
# next URL for the immediate next page of results.
|
17
|
+
# last URL for the last page of results.
|
18
|
+
# first URL for the first page of results.
|
19
|
+
# prev URL for the immediate previous page of results.
|
20
|
+
#
|
21
|
+
# @example Using pagination
|
22
|
+
# first_page = client.get("projects")
|
23
|
+
#
|
24
|
+
# # The first page will only have URLs for :next & :last
|
25
|
+
# first_page.pagination[:first]
|
26
|
+
# #=> nil
|
27
|
+
# first_page.pagination[:next]
|
28
|
+
# #=> "projects?per_page=20&page=2"
|
29
|
+
#
|
30
|
+
# # Any other page will have all keys
|
31
|
+
# next_page = client.get(first_page.pagination[:next])
|
32
|
+
# next_page.pagination[:first]
|
33
|
+
# #=> "projects?per_page=20&page=1"
|
34
|
+
#
|
35
|
+
# # The last page will only have URLs for :first & :next
|
36
|
+
# last_page = client.get(first_page.pagination[:last])
|
37
|
+
# last_page.pagination[:last]
|
38
|
+
# #=> nil
|
39
|
+
class Response
|
40
|
+
|
41
|
+
# @!attribute [r] headers
|
42
|
+
# @return [Hash<String, String>] Raw headers returned from Procore API.
|
43
|
+
# @!attribute [r] code
|
44
|
+
# @return [Integer] Status Code returned from Procore API.
|
45
|
+
# @!attribute [r] pagination
|
46
|
+
# @return [Hash<Symbol, String>] Pagination URLs
|
47
|
+
attr_reader :headers, :code, :pagination, :request
|
48
|
+
|
49
|
+
def initialize(body:, headers:, code:, request:)
|
50
|
+
@code = code
|
51
|
+
@headers = headers
|
52
|
+
@pagination = parse_pagination
|
53
|
+
@request = request
|
54
|
+
@raw_body = !body.to_s.empty? ? body : "{}".to_json
|
55
|
+
end
|
56
|
+
|
57
|
+
# @return [Array<Hash>, Hash] Ruby representation of JSON response. Hashes are
|
58
|
+
# with indifferent access
|
59
|
+
def body
|
60
|
+
@body ||= parse_body
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
attr_reader :raw_body
|
66
|
+
|
67
|
+
def parse_body
|
68
|
+
JSON.parse(raw_body, object_class: HashWithIndifferentAccess)
|
69
|
+
end
|
70
|
+
|
71
|
+
def parse_pagination
|
72
|
+
headers["link"].to_s.split(", ").map(&:strip).inject({}) do |links, link|
|
73
|
+
url, name = link.match(/vapid\/(.*?)>; rel="(\w+)"/).captures
|
74
|
+
links.merge!(name.to_sym => url)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
data/lib/procore/util.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
module Procore
|
2
|
+
# Collection of utility methods used within the gem.
|
3
|
+
module Util
|
4
|
+
def self.log_info(message, meta = {})
|
5
|
+
return if Procore.configuration.logger.nil?
|
6
|
+
|
7
|
+
meta_string = meta.map do |key, value|
|
8
|
+
"#{colorize(key, :cyan)}: #{colorize(value, :cyan, bold: true)};"
|
9
|
+
end.join(" ")
|
10
|
+
|
11
|
+
Procore.configuration.logger.info(
|
12
|
+
"#{colorize('Procore', :yellow)} <<- " \
|
13
|
+
"#{colorize(message.ljust(22), :cyan)} <<- #{meta_string}",
|
14
|
+
)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.log_error(message, meta = {})
|
18
|
+
return if Procore.configuration.logger.nil?
|
19
|
+
|
20
|
+
meta_string = meta.map do |key, value|
|
21
|
+
"#{colorize(key, :red)}: #{colorize(value, :red, bold: true)};"
|
22
|
+
end.join(" ")
|
23
|
+
|
24
|
+
Procore.configuration.logger.info(
|
25
|
+
"#{colorize('Procore', :red)} <<- " \
|
26
|
+
"#{colorize(message.ljust(22), :red)} <<- #{meta_string}",
|
27
|
+
)
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.colorize(text, color, bold: false)
|
31
|
+
mode = bold ? 1 : 0
|
32
|
+
foreground = 30 + COLOR_CODES.fetch(color)
|
33
|
+
background = 40 + COLOR_CODES.fetch(:default)
|
34
|
+
|
35
|
+
"\033[#{mode};#{foreground};#{background}m#{text}\033[0m"
|
36
|
+
end
|
37
|
+
private_class_method :colorize
|
38
|
+
|
39
|
+
COLOR_CODES = {
|
40
|
+
black: 0,
|
41
|
+
red: 1,
|
42
|
+
green: 2,
|
43
|
+
yellow: 3,
|
44
|
+
blue: 4,
|
45
|
+
magenta: 5,
|
46
|
+
cyan: 6,
|
47
|
+
white: 7,
|
48
|
+
default: 9,
|
49
|
+
}.freeze
|
50
|
+
private_constant :COLOR_CODES
|
51
|
+
end
|
52
|
+
end
|
data/procore.gemspec
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "procore/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "procore"
|
8
|
+
spec.version = Procore::VERSION
|
9
|
+
spec.authors = ["Procore Engineering"]
|
10
|
+
spec.email = ["opensource@procore.comm"]
|
11
|
+
|
12
|
+
spec.summary = %q{Procore Ruby Gem}
|
13
|
+
spec.description = %q{Procore Ruby Gem}
|
14
|
+
spec.homepage = "https://github.com/procore/ruby-sdk"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
18
|
+
f.match(%r{^(test|spec|features)/})
|
19
|
+
end
|
20
|
+
spec.bindir = "exe"
|
21
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
22
|
+
spec.require_paths = ["lib"]
|
23
|
+
|
24
|
+
spec.add_development_dependency "actionpack"
|
25
|
+
spec.add_development_dependency "activerecord"
|
26
|
+
spec.add_development_dependency "bundler"
|
27
|
+
spec.add_development_dependency "fakefs"
|
28
|
+
spec.add_development_dependency "minitest"
|
29
|
+
spec.add_development_dependency "pry"
|
30
|
+
spec.add_development_dependency "rake"
|
31
|
+
spec.add_development_dependency "redis"
|
32
|
+
spec.add_development_dependency "sqlite3"
|
33
|
+
spec.add_development_dependency "webmock"
|
34
|
+
|
35
|
+
spec.add_dependency "httparty", "~> 0.15"
|
36
|
+
spec.add_dependency "oauth2", "~> 1.4"
|
37
|
+
end
|