procore 0.6.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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
@@ -0,0 +1,3 @@
1
+ module Procore
2
+ VERSION = "0.6.7"
3
+ end
@@ -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