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.
@@ -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