attio 0.3.0 → 0.5.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/.gitignore +2 -0
- data/CHANGELOG.md +83 -2
- data/CLAUDE.md +35 -4
- data/Gemfile.lock +1 -1
- data/META_IMPLEMENTATION_PLAN.md +205 -0
- data/README.md +361 -37
- data/lib/attio/circuit_breaker.rb +299 -0
- data/lib/attio/client.rb +11 -10
- data/lib/attio/connection_pool.rb +190 -35
- data/lib/attio/enhanced_client.rb +257 -0
- data/lib/attio/http_client.rb +54 -3
- data/lib/attio/observability.rb +424 -0
- data/lib/attio/resources/attributes.rb +244 -0
- data/lib/attio/resources/base.rb +53 -0
- data/lib/attio/resources/bulk.rb +1 -1
- data/lib/attio/resources/lists.rb +195 -0
- data/lib/attio/resources/meta.rb +103 -42
- data/lib/attio/resources/objects.rb +104 -0
- data/lib/attio/resources/records.rb +97 -2
- data/lib/attio/resources/workspaces.rb +11 -2
- data/lib/attio/version.rb +1 -1
- data/lib/attio/webhooks.rb +220 -0
- data/lib/attio.rb +9 -1
- metadata +6 -1
@@ -0,0 +1,299 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Attio
|
4
|
+
# Circuit breaker pattern for fault tolerance
|
5
|
+
#
|
6
|
+
# @example Basic usage
|
7
|
+
# breaker = CircuitBreaker.new(
|
8
|
+
# threshold: 5,
|
9
|
+
# timeout: 60,
|
10
|
+
# half_open_requests: 3
|
11
|
+
# )
|
12
|
+
#
|
13
|
+
# breaker.call do
|
14
|
+
# # API call that might fail
|
15
|
+
# client.records.list
|
16
|
+
# end
|
17
|
+
class CircuitBreaker
|
18
|
+
class OpenCircuitError < StandardError; end
|
19
|
+
|
20
|
+
# Circuit states
|
21
|
+
CLOSED = :closed
|
22
|
+
OPEN = :open
|
23
|
+
HALF_OPEN = :half_open
|
24
|
+
|
25
|
+
attr_reader :state, :failure_count, :success_count, :last_failure_time
|
26
|
+
attr_accessor :on_state_change
|
27
|
+
|
28
|
+
# Initialize circuit breaker
|
29
|
+
#
|
30
|
+
# @param threshold [Integer] Number of failures before opening circuit
|
31
|
+
# @param timeout [Integer] Seconds before attempting to close circuit
|
32
|
+
# @param half_open_requests [Integer] Successful requests needed to close circuit
|
33
|
+
# @param exceptions [Array<Class>] Exception classes to catch
|
34
|
+
def initialize(
|
35
|
+
threshold: 5,
|
36
|
+
timeout: 60,
|
37
|
+
half_open_requests: 3,
|
38
|
+
exceptions: [StandardError]
|
39
|
+
)
|
40
|
+
@threshold = threshold
|
41
|
+
@timeout = timeout
|
42
|
+
@half_open_requests = half_open_requests
|
43
|
+
@exceptions = exceptions
|
44
|
+
|
45
|
+
@state = CLOSED
|
46
|
+
@failure_count = 0
|
47
|
+
@success_count = 0
|
48
|
+
@last_failure_time = nil
|
49
|
+
@half_open_successes = 0
|
50
|
+
|
51
|
+
@mutex = Mutex.new
|
52
|
+
@on_state_change = nil
|
53
|
+
|
54
|
+
# Statistics
|
55
|
+
@stats = {
|
56
|
+
requests: 0,
|
57
|
+
failures: 0,
|
58
|
+
successes: 0,
|
59
|
+
rejections: 0,
|
60
|
+
state_changes: 0,
|
61
|
+
}
|
62
|
+
end
|
63
|
+
|
64
|
+
# Execute a block with circuit breaker protection
|
65
|
+
#
|
66
|
+
# @yield Block to execute
|
67
|
+
# @return Result of the block
|
68
|
+
# @raise [OpenCircuitError] if circuit is open
|
69
|
+
def call
|
70
|
+
@mutex.synchronize do
|
71
|
+
@stats[:requests] += 1
|
72
|
+
|
73
|
+
case @state
|
74
|
+
when OPEN
|
75
|
+
if can_attempt_reset?
|
76
|
+
transition_to(HALF_OPEN)
|
77
|
+
else
|
78
|
+
@stats[:rejections] += 1
|
79
|
+
raise OpenCircuitError, "Circuit breaker is open (#{time_until_retry}s until retry)"
|
80
|
+
end
|
81
|
+
when HALF_OPEN
|
82
|
+
# Allow limited requests through
|
83
|
+
if @half_open_successes >= @half_open_requests
|
84
|
+
# Already proven stable, close the circuit
|
85
|
+
transition_to(CLOSED)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Execute the block outside the mutex
|
91
|
+
begin
|
92
|
+
result = yield
|
93
|
+
record_success
|
94
|
+
result
|
95
|
+
rescue *@exceptions => e
|
96
|
+
record_failure
|
97
|
+
raise e
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Manually trip the circuit breaker
|
102
|
+
def trip!
|
103
|
+
@mutex.synchronize do
|
104
|
+
transition_to(OPEN)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# Manually reset the circuit breaker
|
109
|
+
def reset!
|
110
|
+
@mutex.synchronize do
|
111
|
+
@failure_count = 0
|
112
|
+
@success_count = 0
|
113
|
+
@half_open_successes = 0
|
114
|
+
@last_failure_time = nil
|
115
|
+
transition_to(CLOSED)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Check if circuit allows requests
|
120
|
+
#
|
121
|
+
# @return [Boolean] True if requests are allowed
|
122
|
+
def allow_request?
|
123
|
+
@state != OPEN || can_attempt_reset?
|
124
|
+
end
|
125
|
+
|
126
|
+
# Get circuit breaker statistics
|
127
|
+
#
|
128
|
+
# @return [Hash] Statistics
|
129
|
+
def stats
|
130
|
+
@mutex.synchronize do
|
131
|
+
@stats.merge(
|
132
|
+
state: @state,
|
133
|
+
failure_count: @failure_count,
|
134
|
+
success_count: @success_count,
|
135
|
+
threshold: @threshold,
|
136
|
+
time_until_retry: @state == OPEN ? time_until_retry : nil
|
137
|
+
)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# Time remaining until circuit can attempt reset
|
142
|
+
#
|
143
|
+
# @return [Integer] Seconds until retry, 0 if ready
|
144
|
+
def time_until_retry
|
145
|
+
return 0 unless @state == OPEN
|
146
|
+
return 0 unless @last_failure_time
|
147
|
+
|
148
|
+
elapsed = Time.now - @last_failure_time
|
149
|
+
remaining = @timeout - elapsed
|
150
|
+
remaining > 0 ? remaining.to_i : 0
|
151
|
+
end
|
152
|
+
|
153
|
+
private def record_success
|
154
|
+
@mutex.synchronize do
|
155
|
+
@stats[:successes] += 1
|
156
|
+
@success_count += 1
|
157
|
+
|
158
|
+
case @state
|
159
|
+
when HALF_OPEN
|
160
|
+
@half_open_successes += 1
|
161
|
+
if @half_open_successes >= @half_open_requests
|
162
|
+
# Circuit has proven stable
|
163
|
+
transition_to(CLOSED)
|
164
|
+
end
|
165
|
+
when CLOSED
|
166
|
+
# Reset failure count on success
|
167
|
+
@failure_count = 0
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
private def record_failure
|
173
|
+
@mutex.synchronize do
|
174
|
+
@stats[:failures] += 1
|
175
|
+
@failure_count += 1
|
176
|
+
@last_failure_time = Time.now
|
177
|
+
|
178
|
+
case @state
|
179
|
+
when CLOSED
|
180
|
+
transition_to(OPEN) if @failure_count >= @threshold
|
181
|
+
when HALF_OPEN
|
182
|
+
# Single failure in half-open state reopens circuit
|
183
|
+
transition_to(OPEN)
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
private def can_attempt_reset?
|
189
|
+
return false unless @last_failure_time
|
190
|
+
|
191
|
+
Time.now - @last_failure_time >= @timeout
|
192
|
+
end
|
193
|
+
|
194
|
+
private def transition_to(new_state)
|
195
|
+
return if @state == new_state
|
196
|
+
|
197
|
+
old_state = @state
|
198
|
+
@state = new_state
|
199
|
+
@stats[:state_changes] += 1
|
200
|
+
|
201
|
+
# Reset counters for new state
|
202
|
+
case new_state
|
203
|
+
when CLOSED
|
204
|
+
@failure_count = 0
|
205
|
+
@half_open_successes = 0
|
206
|
+
when HALF_OPEN
|
207
|
+
@half_open_successes = 0
|
208
|
+
end
|
209
|
+
|
210
|
+
# Notify state change
|
211
|
+
@on_state_change&.call(old_state, new_state)
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
# Wrapper for circuit breaker protected HTTP client
|
216
|
+
class CircuitBreakerClient
|
217
|
+
def initialize(client, circuit_breaker)
|
218
|
+
@client = client
|
219
|
+
@circuit_breaker = circuit_breaker
|
220
|
+
end
|
221
|
+
|
222
|
+
def get(path, params = nil)
|
223
|
+
@circuit_breaker.call { @client.get(path, params) }
|
224
|
+
end
|
225
|
+
|
226
|
+
def post(path, body = {})
|
227
|
+
@circuit_breaker.call { @client.post(path, body) }
|
228
|
+
end
|
229
|
+
|
230
|
+
def patch(path, body = {})
|
231
|
+
@circuit_breaker.call { @client.patch(path, body) }
|
232
|
+
end
|
233
|
+
|
234
|
+
def put(path, body = {})
|
235
|
+
@circuit_breaker.call { @client.put(path, body) }
|
236
|
+
end
|
237
|
+
|
238
|
+
def delete(path, body = nil)
|
239
|
+
@circuit_breaker.call { @client.delete(path, body) }
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
# Composite circuit breaker for multiple endpoints
|
244
|
+
class CompositeCircuitBreaker
|
245
|
+
def initialize(default_config = {})
|
246
|
+
@breakers = {}
|
247
|
+
@default_config = {
|
248
|
+
threshold: 5,
|
249
|
+
timeout: 60,
|
250
|
+
half_open_requests: 3,
|
251
|
+
}.merge(default_config)
|
252
|
+
@mutex = Mutex.new
|
253
|
+
end
|
254
|
+
|
255
|
+
# Get or create circuit breaker for endpoint
|
256
|
+
#
|
257
|
+
# @param key [String] Endpoint key
|
258
|
+
# @param config [Hash] Optional config overrides
|
259
|
+
# @return [CircuitBreaker] Circuit breaker for endpoint
|
260
|
+
def for_endpoint(key, config = {})
|
261
|
+
@mutex.synchronize do
|
262
|
+
@breakers[key] ||= CircuitBreaker.new(**@default_config, **config)
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
# Execute with circuit breaker for endpoint
|
267
|
+
#
|
268
|
+
# @param key [String] Endpoint key
|
269
|
+
# @yield Block to execute
|
270
|
+
def call(key, &block)
|
271
|
+
for_endpoint(key).call(&block)
|
272
|
+
end
|
273
|
+
|
274
|
+
# Get all circuit breaker states
|
275
|
+
#
|
276
|
+
# @return [Hash] States by endpoint
|
277
|
+
def states
|
278
|
+
@mutex.synchronize do
|
279
|
+
@breakers.transform_values(&:state)
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
# Get aggregated statistics
|
284
|
+
#
|
285
|
+
# @return [Hash] Combined statistics
|
286
|
+
def stats
|
287
|
+
@mutex.synchronize do
|
288
|
+
@breakers.transform_values(&:stats)
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
# Reset all circuit breakers
|
293
|
+
def reset_all!
|
294
|
+
@mutex.synchronize do
|
295
|
+
@breakers.each_value(&:reset!)
|
296
|
+
end
|
297
|
+
end
|
298
|
+
end
|
299
|
+
end
|
data/lib/attio/client.rb
CHANGED
@@ -59,10 +59,20 @@ module Attio
|
|
59
59
|
"Content-Type" => "application/json",
|
60
60
|
"User-Agent" => "Attio Ruby Client/#{VERSION}",
|
61
61
|
},
|
62
|
-
timeout: timeout
|
62
|
+
timeout: timeout,
|
63
|
+
rate_limiter: rate_limiter
|
63
64
|
)
|
64
65
|
end
|
65
66
|
|
67
|
+
# Access to the Meta API resource.
|
68
|
+
#
|
69
|
+
# @return [Resources::Meta] Meta resource instance
|
70
|
+
# @example
|
71
|
+
# info = client.meta.identify
|
72
|
+
def meta
|
73
|
+
@meta ||= Resources::Meta.new(self)
|
74
|
+
end
|
75
|
+
|
66
76
|
# Access to the Records API resource.
|
67
77
|
#
|
68
78
|
# @return [Resources::Records] Records resource instance
|
@@ -171,15 +181,6 @@ module Attio
|
|
171
181
|
@deals ||= Resources::Deals.new(self)
|
172
182
|
end
|
173
183
|
|
174
|
-
# Access to the Meta API resource.
|
175
|
-
#
|
176
|
-
# @return [Resources::Meta] Meta resource instance
|
177
|
-
# @example
|
178
|
-
# info = client.meta.identify
|
179
|
-
def meta
|
180
|
-
@meta ||= Resources::Meta.new(self)
|
181
|
-
end
|
182
|
-
|
183
184
|
# Access to the Bulk Operations API resource.
|
184
185
|
#
|
185
186
|
# @return [Resources::Bulk] Bulk operations resource instance
|
@@ -1,36 +1,61 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "timeout"
|
4
|
+
|
3
5
|
module Attio
|
4
|
-
# Thread-safe connection pool for
|
5
|
-
#
|
6
|
-
# This class provides a pool of connections that can be shared
|
7
|
-
# across threads for improved performance and resource management.
|
6
|
+
# Thread-safe connection pool for high-throughput operations
|
8
7
|
#
|
9
|
-
# @example
|
10
|
-
# pool = ConnectionPool.new(size:
|
8
|
+
# @example Basic usage
|
9
|
+
# pool = ConnectionPool.new(size: 5) do
|
10
|
+
# Attio::HttpClient.new(base_url: API_URL, headers: headers)
|
11
|
+
# end
|
11
12
|
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
# conn.get("/endpoint")
|
13
|
+
# pool.with do |connection|
|
14
|
+
# connection.get("records")
|
15
15
|
# end
|
16
16
|
class ConnectionPool
|
17
|
-
|
18
|
-
|
17
|
+
class TimeoutError < StandardError; end
|
18
|
+
class PoolShuttingDownError < StandardError; end
|
19
|
+
|
20
|
+
DEFAULT_SIZE = 5
|
21
|
+
DEFAULT_TIMEOUT = 5
|
22
|
+
|
23
|
+
attr_reader :size, :timeout, :available, :allocated
|
19
24
|
|
20
|
-
|
25
|
+
# Initialize a new connection pool
|
26
|
+
#
|
27
|
+
# @param size [Integer] Maximum number of connections
|
28
|
+
# @param timeout [Integer] Seconds to wait for available connection
|
29
|
+
# @yield Block that creates a new connection
|
30
|
+
def initialize(size: DEFAULT_SIZE, timeout: DEFAULT_TIMEOUT, &block)
|
31
|
+
raise ArgumentError, "Block required to create connections" unless block_given?
|
21
32
|
|
22
|
-
def initialize(size: DEFAULT_POOL_SIZE, timeout: DEFAULT_TIMEOUT, &block)
|
23
33
|
@size = size
|
24
34
|
@timeout = timeout
|
25
|
-
@
|
26
|
-
@
|
27
|
-
@
|
35
|
+
@create_block = block
|
36
|
+
@available = []
|
37
|
+
@allocated = {}
|
28
38
|
@mutex = Mutex.new
|
39
|
+
@resource = ConditionVariable.new
|
40
|
+
@shutting_down = false
|
41
|
+
@created = 0
|
29
42
|
|
30
|
-
|
43
|
+
# Stats tracking
|
44
|
+
@stats = {
|
45
|
+
requests: 0,
|
46
|
+
timeouts: 0,
|
47
|
+
wait_time: 0,
|
48
|
+
active: 0,
|
49
|
+
created: 0,
|
50
|
+
destroyed: 0,
|
51
|
+
}
|
31
52
|
end
|
32
53
|
|
33
|
-
|
54
|
+
# Execute a block with a connection from the pool
|
55
|
+
#
|
56
|
+
# @yield [connection] Block to execute with connection
|
57
|
+
# @return Result of the block
|
58
|
+
def with
|
34
59
|
connection = checkout
|
35
60
|
begin
|
36
61
|
yield connection
|
@@ -39,43 +64,173 @@ module Attio
|
|
39
64
|
end
|
40
65
|
end
|
41
66
|
|
67
|
+
# Check out a connection from the pool
|
68
|
+
#
|
69
|
+
# @return [Object] A connection from the pool
|
70
|
+
# @raise [TimeoutError] if no connection available within timeout
|
42
71
|
def checkout
|
43
|
-
|
72
|
+
start_time = Time.now
|
73
|
+
deadline = start_time + @timeout
|
44
74
|
|
45
|
-
|
46
|
-
|
75
|
+
@mutex.synchronize do
|
76
|
+
raise PoolShuttingDownError, "Pool is shutting down" if @shutting_down
|
77
|
+
|
78
|
+
@stats[:requests] += 1
|
79
|
+
|
80
|
+
loop do
|
81
|
+
# Return available connection
|
82
|
+
if (connection = @available.pop)
|
83
|
+
@allocated[Thread.current] = connection
|
84
|
+
@stats[:active] += 1
|
85
|
+
@stats[:wait_time] += (Time.now - start_time)
|
86
|
+
return connection
|
87
|
+
end
|
47
88
|
|
48
|
-
|
89
|
+
# Create new connection if under limit
|
90
|
+
if @created < @size
|
91
|
+
connection = create_connection
|
92
|
+
@allocated[Thread.current] = connection
|
93
|
+
@stats[:active] += 1
|
94
|
+
@stats[:wait_time] += (Time.now - start_time)
|
95
|
+
return connection
|
96
|
+
end
|
49
97
|
|
50
|
-
|
98
|
+
# Wait for available connection
|
99
|
+
remaining = deadline - Time.now
|
100
|
+
if remaining <= 0
|
101
|
+
@stats[:timeouts] += 1
|
102
|
+
raise TimeoutError, "Timed out waiting for connection after #{@timeout}s"
|
103
|
+
end
|
104
|
+
|
105
|
+
@resource.wait(@mutex, remaining)
|
106
|
+
end
|
51
107
|
end
|
52
|
-
rescue ThreadError
|
53
|
-
raise TimeoutError, "No connections available"
|
54
108
|
end
|
55
109
|
|
110
|
+
# Return a connection to the pool
|
111
|
+
#
|
112
|
+
# @param connection [Object] Connection to return
|
56
113
|
def checkin(connection)
|
57
114
|
@mutex.synchronize do
|
58
|
-
@
|
115
|
+
if @allocated[Thread.current] == connection
|
116
|
+
@allocated.delete(Thread.current)
|
117
|
+
@stats[:active] -= 1
|
118
|
+
|
119
|
+
if @shutting_down
|
120
|
+
destroy_connection(connection)
|
121
|
+
else
|
122
|
+
@available.push(connection)
|
123
|
+
@resource.signal
|
124
|
+
end
|
125
|
+
end
|
59
126
|
end
|
60
127
|
end
|
61
128
|
|
129
|
+
# Shutdown the pool and close all connections
|
62
130
|
def shutdown
|
63
131
|
@mutex.synchronize do
|
64
|
-
@
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
end
|
70
|
-
|
132
|
+
@shutting_down = true
|
133
|
+
|
134
|
+
# Close available connections
|
135
|
+
while (connection = @available.pop)
|
136
|
+
destroy_connection(connection)
|
137
|
+
end
|
138
|
+
|
139
|
+
# NOTE: allocated connections will be closed when checked in
|
140
|
+
@resource.broadcast
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
# Reset the pool by closing all connections
|
145
|
+
def reset!
|
146
|
+
@mutex.synchronize do
|
147
|
+
# Close all available connections
|
148
|
+
while (connection = @available.pop)
|
149
|
+
destroy_connection(connection)
|
71
150
|
end
|
151
|
+
|
152
|
+
@created = 0
|
153
|
+
@stats[:created] = 0
|
154
|
+
@stats[:destroyed] = 0
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
# Get pool statistics
|
159
|
+
#
|
160
|
+
# @return [Hash] Pool statistics
|
161
|
+
def stats
|
162
|
+
@mutex.synchronize do
|
163
|
+
@stats.merge(
|
164
|
+
size: @size,
|
165
|
+
available: @available.size,
|
166
|
+
allocated: @allocated.size,
|
167
|
+
created: @created
|
168
|
+
)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
# Current pool utilization (0.0 to 1.0)
|
173
|
+
#
|
174
|
+
# @return [Float] Utilization percentage
|
175
|
+
def utilization
|
176
|
+
@mutex.synchronize do
|
177
|
+
return 0.0 if @size == 0
|
178
|
+
|
179
|
+
@allocated.size.to_f / @size
|
72
180
|
end
|
73
181
|
end
|
74
182
|
|
183
|
+
# Check if pool is healthy
|
184
|
+
#
|
185
|
+
# @return [Boolean] True if pool is functioning normally
|
186
|
+
def healthy?
|
187
|
+
return false if @shutting_down
|
188
|
+
return true if @stats[:requests] == 0
|
189
|
+
|
190
|
+
@stats[:timeouts] < (@stats[:requests] * 0.01)
|
191
|
+
end
|
192
|
+
|
75
193
|
private def create_connection
|
76
|
-
@
|
194
|
+
connection = @create_block.call
|
195
|
+
@created += 1
|
196
|
+
@stats[:created] += 1
|
197
|
+
connection
|
77
198
|
end
|
78
199
|
|
79
|
-
|
200
|
+
private def destroy_connection(connection)
|
201
|
+
# Call close if connection responds to it
|
202
|
+
connection.close if connection.respond_to?(:close)
|
203
|
+
@stats[:destroyed] += 1
|
204
|
+
rescue StandardError => e
|
205
|
+
# Log but don't raise on close errors
|
206
|
+
warn "Error closing connection: #{e.message}"
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
# Wrapper for pooled HTTP connections
|
211
|
+
class PooledHttpClient
|
212
|
+
def initialize(pool)
|
213
|
+
@pool = pool
|
214
|
+
end
|
215
|
+
|
216
|
+
def get(path, params = nil)
|
217
|
+
@pool.with { |conn| conn.get(path, params) }
|
218
|
+
end
|
219
|
+
|
220
|
+
def post(path, body = {})
|
221
|
+
@pool.with { |conn| conn.post(path, body) }
|
222
|
+
end
|
223
|
+
|
224
|
+
def patch(path, body = {})
|
225
|
+
@pool.with { |conn| conn.patch(path, body) }
|
226
|
+
end
|
227
|
+
|
228
|
+
def put(path, body = {})
|
229
|
+
@pool.with { |conn| conn.put(path, body) }
|
230
|
+
end
|
231
|
+
|
232
|
+
def delete(path, body = nil)
|
233
|
+
@pool.with { |conn| conn.delete(path, body) }
|
234
|
+
end
|
80
235
|
end
|
81
236
|
end
|