terminal-shop 2.0.0 → 2.1.1
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/README.md +2 -8
- data/lib/terminal-shop/client.rb +4 -4
- data/lib/terminal-shop/models/address_create_params.rb +1 -1
- data/lib/terminal-shop/models/address_delete_params.rb +1 -1
- data/lib/terminal-shop/models/address_get_params.rb +1 -1
- data/lib/terminal-shop/models/address_list_params.rb +1 -1
- data/lib/terminal-shop/models/app_create_params.rb +1 -1
- data/lib/terminal-shop/models/app_delete_params.rb +1 -1
- data/lib/terminal-shop/models/app_get_params.rb +1 -1
- data/lib/terminal-shop/models/app_list_params.rb +1 -1
- data/lib/terminal-shop/models/card_collect_params.rb +1 -1
- data/lib/terminal-shop/models/card_create_params.rb +1 -1
- data/lib/terminal-shop/models/card_delete_params.rb +1 -1
- data/lib/terminal-shop/models/card_get_params.rb +1 -1
- data/lib/terminal-shop/models/card_list_params.rb +1 -1
- data/lib/terminal-shop/models/cart_clear_params.rb +1 -1
- data/lib/terminal-shop/models/cart_convert_params.rb +1 -1
- data/lib/terminal-shop/models/cart_get_params.rb +1 -1
- data/lib/terminal-shop/models/cart_set_address_params.rb +1 -1
- data/lib/terminal-shop/models/cart_set_card_params.rb +1 -1
- data/lib/terminal-shop/models/cart_set_item_params.rb +1 -1
- data/lib/terminal-shop/models/email_create_params.rb +1 -1
- data/lib/terminal-shop/models/order_create_params.rb +1 -1
- data/lib/terminal-shop/models/order_get_params.rb +1 -1
- data/lib/terminal-shop/models/order_list_params.rb +1 -1
- data/lib/terminal-shop/models/product_get_params.rb +1 -1
- data/lib/terminal-shop/models/product_list_params.rb +1 -1
- data/lib/terminal-shop/models/profile_me_params.rb +1 -1
- data/lib/terminal-shop/models/profile_update_params.rb +1 -1
- data/lib/terminal-shop/models/subscription_create_params.rb +1 -1
- data/lib/terminal-shop/models/subscription_delete_params.rb +1 -1
- data/lib/terminal-shop/models/subscription_get_params.rb +1 -1
- data/lib/terminal-shop/models/subscription_list_params.rb +1 -1
- data/lib/terminal-shop/models/token_create_params.rb +1 -1
- data/lib/terminal-shop/models/token_delete_params.rb +1 -1
- data/lib/terminal-shop/models/token_get_params.rb +1 -1
- data/lib/terminal-shop/models/token_list_params.rb +1 -1
- data/lib/terminal-shop/models/view_init_params.rb +1 -1
- data/lib/terminal-shop/request_options.rb +0 -33
- data/lib/terminal-shop/transport/base_client.rb +459 -0
- data/lib/terminal-shop/transport/pooled_net_requester.rb +182 -0
- data/lib/terminal-shop/type/array_of.rb +112 -0
- data/lib/terminal-shop/type/base_model.rb +364 -0
- data/lib/terminal-shop/type/base_page.rb +61 -0
- data/lib/terminal-shop/type/boolean_model.rb +52 -0
- data/lib/terminal-shop/type/converter.rb +217 -0
- data/lib/terminal-shop/type/enum.rb +101 -0
- data/lib/terminal-shop/type/hash_of.rb +138 -0
- data/lib/terminal-shop/type/request_parameters.rb +38 -0
- data/lib/terminal-shop/type/union.rb +185 -0
- data/lib/terminal-shop/type/unknown.rb +56 -0
- data/lib/terminal-shop/type.rb +23 -0
- data/lib/terminal-shop/util.rb +3 -5
- data/lib/terminal-shop/version.rb +1 -1
- data/lib/terminal-shop.rb +29 -20
- data/manifest.yaml +1 -0
- data/rbi/lib/terminal-shop/client.rbi +4 -4
- data/rbi/lib/terminal-shop/models/address_create_params.rbi +12 -2
- data/rbi/lib/terminal-shop/models/address_delete_params.rbi +1 -1
- data/rbi/lib/terminal-shop/models/address_get_params.rbi +1 -1
- data/rbi/lib/terminal-shop/models/address_list_params.rbi +1 -1
- data/rbi/lib/terminal-shop/models/app_create_params.rbi +1 -1
- data/rbi/lib/terminal-shop/models/app_delete_params.rbi +1 -1
- data/rbi/lib/terminal-shop/models/app_get_params.rbi +1 -1
- data/rbi/lib/terminal-shop/models/app_list_params.rbi +1 -1
- data/rbi/lib/terminal-shop/models/card_collect_params.rbi +1 -1
- data/rbi/lib/terminal-shop/models/card_create_params.rbi +1 -1
- data/rbi/lib/terminal-shop/models/card_delete_params.rbi +1 -1
- data/rbi/lib/terminal-shop/models/card_get_params.rbi +1 -1
- data/rbi/lib/terminal-shop/models/card_list_params.rbi +1 -1
- data/rbi/lib/terminal-shop/models/cart_clear_params.rbi +1 -1
- data/rbi/lib/terminal-shop/models/cart_convert_params.rbi +1 -1
- data/rbi/lib/terminal-shop/models/cart_get_params.rbi +1 -1
- data/rbi/lib/terminal-shop/models/cart_set_address_params.rbi +1 -1
- data/rbi/lib/terminal-shop/models/cart_set_card_params.rbi +1 -1
- data/rbi/lib/terminal-shop/models/cart_set_item_params.rbi +1 -1
- data/rbi/lib/terminal-shop/models/email_create_params.rbi +1 -1
- data/rbi/lib/terminal-shop/models/order_create_params.rbi +1 -1
- data/rbi/lib/terminal-shop/models/order_get_params.rbi +1 -1
- data/rbi/lib/terminal-shop/models/order_list_params.rbi +1 -1
- data/rbi/lib/terminal-shop/models/product_get_params.rbi +1 -1
- data/rbi/lib/terminal-shop/models/product_list_params.rbi +1 -1
- data/rbi/lib/terminal-shop/models/profile_me_params.rbi +1 -1
- data/rbi/lib/terminal-shop/models/profile_update_params.rbi +1 -1
- data/rbi/lib/terminal-shop/models/subscription_create_params.rbi +1 -1
- data/rbi/lib/terminal-shop/models/subscription_delete_params.rbi +1 -1
- data/rbi/lib/terminal-shop/models/subscription_get_params.rbi +1 -1
- data/rbi/lib/terminal-shop/models/subscription_list_params.rbi +1 -1
- data/rbi/lib/terminal-shop/models/token_create_params.rbi +1 -1
- data/rbi/lib/terminal-shop/models/token_delete_params.rbi +1 -1
- data/rbi/lib/terminal-shop/models/token_get_params.rbi +1 -1
- data/rbi/lib/terminal-shop/models/token_list_params.rbi +1 -1
- data/rbi/lib/terminal-shop/models/view_init_params.rbi +1 -1
- data/rbi/lib/terminal-shop/models/view_init_response.rbi +12 -1
- data/rbi/lib/terminal-shop/request_options.rbi +0 -15
- data/rbi/lib/terminal-shop/transport/base_client.rbi +208 -0
- data/rbi/lib/terminal-shop/transport/pooled_net_requester.rbi +64 -0
- data/rbi/lib/terminal-shop/type/array_of.rbi +82 -0
- data/rbi/lib/terminal-shop/type/base_model.rbi +194 -0
- data/rbi/lib/terminal-shop/type/base_page.rbi +38 -0
- data/rbi/lib/terminal-shop/type/boolean_model.rbi +41 -0
- data/rbi/lib/terminal-shop/type/converter.rbi +101 -0
- data/rbi/lib/terminal-shop/type/enum.rbi +58 -0
- data/rbi/lib/terminal-shop/type/hash_of.rbi +85 -0
- data/rbi/lib/terminal-shop/type/request_parameters.rbi +20 -0
- data/rbi/lib/terminal-shop/type/union.rbi +68 -0
- data/rbi/lib/terminal-shop/type/unknown.rbi +37 -0
- data/rbi/lib/terminal-shop/type.rbi +23 -0
- data/rbi/lib/terminal-shop/version.rbi +1 -1
- data/sig/terminal-shop/client.rbs +3 -3
- data/sig/terminal-shop/models/address_create_params.rbs +1 -1
- data/sig/terminal-shop/models/address_delete_params.rbs +1 -1
- data/sig/terminal-shop/models/address_get_params.rbs +1 -1
- data/sig/terminal-shop/models/address_list_params.rbs +1 -1
- data/sig/terminal-shop/models/app_create_params.rbs +1 -1
- data/sig/terminal-shop/models/app_delete_params.rbs +1 -1
- data/sig/terminal-shop/models/app_get_params.rbs +1 -1
- data/sig/terminal-shop/models/app_list_params.rbs +1 -1
- data/sig/terminal-shop/models/card_collect_params.rbs +1 -1
- data/sig/terminal-shop/models/card_create_params.rbs +1 -1
- data/sig/terminal-shop/models/card_delete_params.rbs +1 -1
- data/sig/terminal-shop/models/card_get_params.rbs +1 -1
- data/sig/terminal-shop/models/card_list_params.rbs +1 -1
- data/sig/terminal-shop/models/cart_clear_params.rbs +1 -1
- data/sig/terminal-shop/models/cart_convert_params.rbs +1 -1
- data/sig/terminal-shop/models/cart_get_params.rbs +1 -1
- data/sig/terminal-shop/models/cart_set_address_params.rbs +1 -1
- data/sig/terminal-shop/models/cart_set_card_params.rbs +1 -1
- data/sig/terminal-shop/models/cart_set_item_params.rbs +1 -1
- data/sig/terminal-shop/models/email_create_params.rbs +1 -1
- data/sig/terminal-shop/models/order_create_params.rbs +1 -1
- data/sig/terminal-shop/models/order_get_params.rbs +1 -1
- data/sig/terminal-shop/models/order_list_params.rbs +1 -1
- data/sig/terminal-shop/models/product_get_params.rbs +1 -1
- data/sig/terminal-shop/models/product_list_params.rbs +1 -1
- data/sig/terminal-shop/models/profile_me_params.rbs +1 -1
- data/sig/terminal-shop/models/profile_update_params.rbs +1 -1
- data/sig/terminal-shop/models/subscription_create_params.rbs +1 -1
- data/sig/terminal-shop/models/subscription_delete_params.rbs +1 -1
- data/sig/terminal-shop/models/subscription_get_params.rbs +1 -1
- data/sig/terminal-shop/models/subscription_list_params.rbs +1 -1
- data/sig/terminal-shop/models/token_create_params.rbs +1 -1
- data/sig/terminal-shop/models/token_delete_params.rbs +1 -1
- data/sig/terminal-shop/models/token_get_params.rbs +1 -1
- data/sig/terminal-shop/models/token_list_params.rbs +1 -1
- data/sig/terminal-shop/models/view_init_params.rbs +1 -1
- data/sig/terminal-shop/request_options.rbs +0 -10
- data/sig/terminal-shop/transport/base_client.rbs +110 -0
- data/sig/terminal-shop/transport/pooled_net_requester.rbs +39 -0
- data/sig/terminal-shop/type/array_of.rbs +36 -0
- data/sig/terminal-shop/type/base_model.rbs +73 -0
- data/sig/terminal-shop/type/base_page.rbs +22 -0
- data/sig/terminal-shop/type/boolean_model.rbs +18 -0
- data/sig/terminal-shop/type/converter.rbs +42 -0
- data/sig/terminal-shop/type/enum.rbs +22 -0
- data/sig/terminal-shop/type/hash_of.rbs +36 -0
- data/sig/terminal-shop/type/request_parameters.rbs +13 -0
- data/sig/terminal-shop/type/union.rbs +40 -0
- data/sig/terminal-shop/type/unknown.rbs +18 -0
- data/sig/terminal-shop/type.rbs +22 -0
- data/sig/terminal-shop/version.rbs +1 -1
- metadata +41 -17
- data/lib/terminal-shop/base_client.rb +0 -457
- data/lib/terminal-shop/base_model.rb +0 -1201
- data/lib/terminal-shop/base_page.rb +0 -59
- data/lib/terminal-shop/extern.rb +0 -7
- data/lib/terminal-shop/pooled_net_requester.rb +0 -180
- data/rbi/lib/terminal-shop/base_client.rbi +0 -197
- data/rbi/lib/terminal-shop/base_model.rbi +0 -645
- data/rbi/lib/terminal-shop/base_page.rbi +0 -36
- data/rbi/lib/terminal-shop/extern.rbi +0 -7
- data/rbi/lib/terminal-shop/pooled_net_requester.rbi +0 -59
- data/sig/terminal-shop/base_client.rbs +0 -108
- data/sig/terminal-shop/base_model.rbs +0 -262
- data/sig/terminal-shop/base_page.rbs +0 -20
- data/sig/terminal-shop/extern.rbs +0 -4
- data/sig/terminal-shop/pooled_net_requester.rbs +0 -37
@@ -0,0 +1,459 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TerminalShop
|
4
|
+
module Transport
|
5
|
+
# @api private
|
6
|
+
#
|
7
|
+
# @abstract
|
8
|
+
class BaseClient
|
9
|
+
# from whatwg fetch spec
|
10
|
+
MAX_REDIRECTS = 20
|
11
|
+
|
12
|
+
# rubocop:disable Style/MutableConstant
|
13
|
+
PLATFORM_HEADERS =
|
14
|
+
{
|
15
|
+
"x-stainless-arch" => TerminalShop::Util.arch,
|
16
|
+
"x-stainless-lang" => "ruby",
|
17
|
+
"x-stainless-os" => TerminalShop::Util.os,
|
18
|
+
"x-stainless-package-version" => TerminalShop::VERSION,
|
19
|
+
"x-stainless-runtime" => ::RUBY_ENGINE,
|
20
|
+
"x-stainless-runtime-version" => ::RUBY_ENGINE_VERSION
|
21
|
+
}
|
22
|
+
# rubocop:enable Style/MutableConstant
|
23
|
+
|
24
|
+
class << self
|
25
|
+
# @api private
|
26
|
+
#
|
27
|
+
# @param req [Hash{Symbol=>Object}]
|
28
|
+
#
|
29
|
+
# @raise [ArgumentError]
|
30
|
+
def validate!(req)
|
31
|
+
keys = [:method, :path, :query, :headers, :body, :unwrap, :page, :stream, :model, :options]
|
32
|
+
case req
|
33
|
+
in Hash
|
34
|
+
req.each_key do |k|
|
35
|
+
unless keys.include?(k)
|
36
|
+
raise ArgumentError.new("Request `req` keys must be one of #{keys}, got #{k.inspect}")
|
37
|
+
end
|
38
|
+
end
|
39
|
+
else
|
40
|
+
raise ArgumentError.new("Request `req` must be a Hash or RequestOptions, got #{req.inspect}")
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# @api private
|
45
|
+
#
|
46
|
+
# @param status [Integer]
|
47
|
+
# @param headers [Hash{String=>String}, Net::HTTPHeader]
|
48
|
+
#
|
49
|
+
# @return [Boolean]
|
50
|
+
def should_retry?(status, headers:)
|
51
|
+
coerced = TerminalShop::Util.coerce_boolean(headers["x-should-retry"])
|
52
|
+
case [coerced, status]
|
53
|
+
in [true | false, _]
|
54
|
+
coerced
|
55
|
+
in [_, 408 | 409 | 429 | (500..)]
|
56
|
+
# retry on:
|
57
|
+
# 408: timeouts
|
58
|
+
# 409: locks
|
59
|
+
# 429: rate limits
|
60
|
+
# 500+: unknown errors
|
61
|
+
true
|
62
|
+
else
|
63
|
+
false
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# @api private
|
68
|
+
#
|
69
|
+
# @param request [Hash{Symbol=>Object}] .
|
70
|
+
#
|
71
|
+
# @option request [Symbol] :method
|
72
|
+
#
|
73
|
+
# @option request [URI::Generic] :url
|
74
|
+
#
|
75
|
+
# @option request [Hash{String=>String}] :headers
|
76
|
+
#
|
77
|
+
# @option request [Object] :body
|
78
|
+
#
|
79
|
+
# @option request [Integer] :max_retries
|
80
|
+
#
|
81
|
+
# @option request [Float] :timeout
|
82
|
+
#
|
83
|
+
# @param status [Integer]
|
84
|
+
#
|
85
|
+
# @param response_headers [Hash{String=>String}, Net::HTTPHeader]
|
86
|
+
#
|
87
|
+
# @return [Hash{Symbol=>Object}]
|
88
|
+
def follow_redirect(request, status:, response_headers:)
|
89
|
+
method, url, headers = request.fetch_values(:method, :url, :headers)
|
90
|
+
location =
|
91
|
+
Kernel.then do
|
92
|
+
URI.join(url, response_headers["location"])
|
93
|
+
rescue ArgumentError
|
94
|
+
message = "Server responded with status #{status} but no valid location header."
|
95
|
+
raise TerminalShop::APIConnectionError.new(url: url, message: message)
|
96
|
+
end
|
97
|
+
|
98
|
+
request = {**request, url: location}
|
99
|
+
|
100
|
+
case [url.scheme, location.scheme]
|
101
|
+
in ["https", "http"]
|
102
|
+
message = "Tried to redirect to a insecure URL"
|
103
|
+
raise TerminalShop::APIConnectionError.new(url: url, message: message)
|
104
|
+
else
|
105
|
+
nil
|
106
|
+
end
|
107
|
+
|
108
|
+
# from whatwg fetch spec
|
109
|
+
case [status, method]
|
110
|
+
in [301 | 302, :post] | [303, _]
|
111
|
+
drop = %w[content-encoding content-language content-length content-location content-type]
|
112
|
+
request = {
|
113
|
+
**request,
|
114
|
+
method: method == :head ? :head : :get,
|
115
|
+
headers: headers.except(*drop),
|
116
|
+
body: nil
|
117
|
+
}
|
118
|
+
else
|
119
|
+
end
|
120
|
+
|
121
|
+
# from undici
|
122
|
+
if TerminalShop::Util.uri_origin(url) != TerminalShop::Util.uri_origin(location)
|
123
|
+
drop = %w[authorization cookie host proxy-authorization]
|
124
|
+
request = {**request, headers: request.fetch(:headers).except(*drop)}
|
125
|
+
end
|
126
|
+
|
127
|
+
request
|
128
|
+
end
|
129
|
+
|
130
|
+
# @api private
|
131
|
+
#
|
132
|
+
# @param status [Integer, TerminalShop::APIConnectionError]
|
133
|
+
# @param stream [Enumerable, nil]
|
134
|
+
def reap_connection!(status, stream:)
|
135
|
+
case status
|
136
|
+
in (..199) | (300..499)
|
137
|
+
stream&.each { next }
|
138
|
+
in TerminalShop::APIConnectionError | (500..)
|
139
|
+
TerminalShop::Util.close_fused!(stream)
|
140
|
+
else
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# @api private
|
146
|
+
# @return [TerminalShop::Transport::PooledNetRequester]
|
147
|
+
attr_accessor :requester
|
148
|
+
|
149
|
+
# @api private
|
150
|
+
#
|
151
|
+
# @param base_url [String]
|
152
|
+
# @param timeout [Float]
|
153
|
+
# @param max_retries [Integer]
|
154
|
+
# @param initial_retry_delay [Float]
|
155
|
+
# @param max_retry_delay [Float]
|
156
|
+
# @param headers [Hash{String=>String, Integer, Array<String, Integer, nil>, nil}]
|
157
|
+
# @param idempotency_header [String, nil]
|
158
|
+
def initialize(
|
159
|
+
base_url:,
|
160
|
+
timeout: 0.0,
|
161
|
+
max_retries: 0,
|
162
|
+
initial_retry_delay: 0.0,
|
163
|
+
max_retry_delay: 0.0,
|
164
|
+
headers: {},
|
165
|
+
idempotency_header: nil
|
166
|
+
)
|
167
|
+
@requester = TerminalShop::Transport::PooledNetRequester.new
|
168
|
+
@headers = TerminalShop::Util.normalized_headers(
|
169
|
+
self.class::PLATFORM_HEADERS,
|
170
|
+
{
|
171
|
+
"accept" => "application/json",
|
172
|
+
"content-type" => "application/json"
|
173
|
+
},
|
174
|
+
headers
|
175
|
+
)
|
176
|
+
@base_url = TerminalShop::Util.parse_uri(base_url)
|
177
|
+
@idempotency_header = idempotency_header&.to_s&.downcase
|
178
|
+
@max_retries = max_retries
|
179
|
+
@timeout = timeout
|
180
|
+
@initial_retry_delay = initial_retry_delay
|
181
|
+
@max_retry_delay = max_retry_delay
|
182
|
+
end
|
183
|
+
|
184
|
+
# @api private
|
185
|
+
#
|
186
|
+
# @return [Hash{String=>String}]
|
187
|
+
private def auth_headers = {}
|
188
|
+
|
189
|
+
# @api private
|
190
|
+
#
|
191
|
+
# @return [String]
|
192
|
+
private def generate_idempotency_key = "stainless-ruby-retry-#{SecureRandom.uuid}"
|
193
|
+
|
194
|
+
# @api private
|
195
|
+
#
|
196
|
+
# @param req [Hash{Symbol=>Object}] .
|
197
|
+
#
|
198
|
+
# @option req [Symbol] :method
|
199
|
+
#
|
200
|
+
# @option req [String, Array<String>] :path
|
201
|
+
#
|
202
|
+
# @option req [Hash{String=>Array<String>, String, nil}, nil] :query
|
203
|
+
#
|
204
|
+
# @option req [Hash{String=>String, Integer, Array<String, Integer, nil>, nil}, nil] :headers
|
205
|
+
#
|
206
|
+
# @option req [Object, nil] :body
|
207
|
+
#
|
208
|
+
# @option req [Symbol, nil] :unwrap
|
209
|
+
#
|
210
|
+
# @option req [Class, nil] :page
|
211
|
+
#
|
212
|
+
# @option req [Class, nil] :stream
|
213
|
+
#
|
214
|
+
# @option req [TerminalShop::Type::Converter, Class, nil] :model
|
215
|
+
#
|
216
|
+
# @param opts [Hash{Symbol=>Object}] .
|
217
|
+
#
|
218
|
+
# @option opts [String, nil] :idempotency_key
|
219
|
+
#
|
220
|
+
# @option opts [Hash{String=>Array<String>, String, nil}, nil] :extra_query
|
221
|
+
#
|
222
|
+
# @option opts [Hash{String=>String, nil}, nil] :extra_headers
|
223
|
+
#
|
224
|
+
# @option opts [Object, nil] :extra_body
|
225
|
+
#
|
226
|
+
# @option opts [Integer, nil] :max_retries
|
227
|
+
#
|
228
|
+
# @option opts [Float, nil] :timeout
|
229
|
+
#
|
230
|
+
# @return [Hash{Symbol=>Object}]
|
231
|
+
private def build_request(req, opts)
|
232
|
+
method, uninterpolated_path = req.fetch_values(:method, :path)
|
233
|
+
|
234
|
+
path = TerminalShop::Util.interpolate_path(uninterpolated_path)
|
235
|
+
|
236
|
+
query = TerminalShop::Util.deep_merge(req[:query].to_h, opts[:extra_query].to_h)
|
237
|
+
|
238
|
+
headers = TerminalShop::Util.normalized_headers(
|
239
|
+
@headers,
|
240
|
+
auth_headers,
|
241
|
+
req[:headers].to_h,
|
242
|
+
opts[:extra_headers].to_h
|
243
|
+
)
|
244
|
+
|
245
|
+
if @idempotency_header &&
|
246
|
+
!headers.key?(@idempotency_header) &&
|
247
|
+
!Net::HTTP::IDEMPOTENT_METHODS_.include?(method.to_s.upcase)
|
248
|
+
headers[@idempotency_header] = opts.fetch(:idempotency_key) { generate_idempotency_key }
|
249
|
+
end
|
250
|
+
|
251
|
+
unless headers.key?("x-stainless-retry-count")
|
252
|
+
headers["x-stainless-retry-count"] = "0"
|
253
|
+
end
|
254
|
+
|
255
|
+
timeout = opts.fetch(:timeout, @timeout).to_f.clamp((0..))
|
256
|
+
unless headers.key?("x-stainless-timeout") || timeout.zero?
|
257
|
+
headers["x-stainless-timeout"] = timeout.to_s
|
258
|
+
end
|
259
|
+
|
260
|
+
headers.reject! { |_, v| v.to_s.empty? }
|
261
|
+
|
262
|
+
body =
|
263
|
+
case method
|
264
|
+
in :get | :head | :options | :trace
|
265
|
+
nil
|
266
|
+
else
|
267
|
+
TerminalShop::Util.deep_merge(*[req[:body], opts[:extra_body]].compact)
|
268
|
+
end
|
269
|
+
|
270
|
+
headers, encoded = TerminalShop::Util.encode_content(headers, body)
|
271
|
+
{
|
272
|
+
method: method,
|
273
|
+
url: TerminalShop::Util.join_parsed_uri(@base_url, {**req, path: path, query: query}),
|
274
|
+
headers: headers,
|
275
|
+
body: encoded,
|
276
|
+
max_retries: opts.fetch(:max_retries, @max_retries),
|
277
|
+
timeout: timeout
|
278
|
+
}
|
279
|
+
end
|
280
|
+
|
281
|
+
# @api private
|
282
|
+
#
|
283
|
+
# @param headers [Hash{String=>String}]
|
284
|
+
# @param retry_count [Integer]
|
285
|
+
#
|
286
|
+
# @return [Float]
|
287
|
+
private def retry_delay(headers, retry_count:)
|
288
|
+
# Non-standard extension
|
289
|
+
span = Float(headers["retry-after-ms"], exception: false)&.then { _1 / 1000 }
|
290
|
+
return span if span
|
291
|
+
|
292
|
+
retry_header = headers["retry-after"]
|
293
|
+
return span if (span = Float(retry_header, exception: false))
|
294
|
+
|
295
|
+
span = retry_header&.then do
|
296
|
+
Time.httpdate(_1) - Time.now
|
297
|
+
rescue ArgumentError
|
298
|
+
nil
|
299
|
+
end
|
300
|
+
return span if span
|
301
|
+
|
302
|
+
scale = retry_count**2
|
303
|
+
jitter = 1 - (0.25 * rand)
|
304
|
+
(@initial_retry_delay * scale * jitter).clamp(0, @max_retry_delay)
|
305
|
+
end
|
306
|
+
|
307
|
+
# @api private
|
308
|
+
#
|
309
|
+
# @param request [Hash{Symbol=>Object}] .
|
310
|
+
#
|
311
|
+
# @option request [Symbol] :method
|
312
|
+
#
|
313
|
+
# @option request [URI::Generic] :url
|
314
|
+
#
|
315
|
+
# @option request [Hash{String=>String}] :headers
|
316
|
+
#
|
317
|
+
# @option request [Object] :body
|
318
|
+
#
|
319
|
+
# @option request [Integer] :max_retries
|
320
|
+
#
|
321
|
+
# @option request [Float] :timeout
|
322
|
+
#
|
323
|
+
# @param redirect_count [Integer]
|
324
|
+
#
|
325
|
+
# @param retry_count [Integer]
|
326
|
+
#
|
327
|
+
# @param send_retry_header [Boolean]
|
328
|
+
#
|
329
|
+
# @raise [TerminalShop::APIError]
|
330
|
+
# @return [Array(Integer, Net::HTTPResponse, Enumerable)]
|
331
|
+
private def send_request(request, redirect_count:, retry_count:, send_retry_header:)
|
332
|
+
url, headers, max_retries, timeout = request.fetch_values(:url, :headers, :max_retries, :timeout)
|
333
|
+
input = {**request.except(:timeout), deadline: TerminalShop::Util.monotonic_secs + timeout}
|
334
|
+
|
335
|
+
if send_retry_header
|
336
|
+
headers["x-stainless-retry-count"] = retry_count.to_s
|
337
|
+
end
|
338
|
+
|
339
|
+
begin
|
340
|
+
status, response, stream = @requester.execute(input)
|
341
|
+
rescue TerminalShop::APIConnectionError => e
|
342
|
+
status = e
|
343
|
+
end
|
344
|
+
|
345
|
+
case status
|
346
|
+
in ..299
|
347
|
+
[status, response, stream]
|
348
|
+
in 300..399 if redirect_count >= self.class::MAX_REDIRECTS
|
349
|
+
self.class.reap_connection!(status, stream: stream)
|
350
|
+
|
351
|
+
message = "Failed to complete the request within #{self.class::MAX_REDIRECTS} redirects."
|
352
|
+
raise TerminalShop::APIConnectionError.new(url: url, message: message)
|
353
|
+
in 300..399
|
354
|
+
self.class.reap_connection!(status, stream: stream)
|
355
|
+
|
356
|
+
request = self.class.follow_redirect(request, status: status, response_headers: response)
|
357
|
+
send_request(
|
358
|
+
request,
|
359
|
+
redirect_count: redirect_count + 1,
|
360
|
+
retry_count: retry_count,
|
361
|
+
send_retry_header: send_retry_header
|
362
|
+
)
|
363
|
+
in TerminalShop::APIConnectionError if retry_count >= max_retries
|
364
|
+
raise status
|
365
|
+
in (400..) if retry_count >= max_retries || !self.class.should_retry?(status, headers: response)
|
366
|
+
decoded = Kernel.then do
|
367
|
+
TerminalShop::Util.decode_content(response, stream: stream, suppress_error: true)
|
368
|
+
ensure
|
369
|
+
self.class.reap_connection!(status, stream: stream)
|
370
|
+
end
|
371
|
+
|
372
|
+
raise TerminalShop::APIStatusError.for(
|
373
|
+
url: url,
|
374
|
+
status: status,
|
375
|
+
body: decoded,
|
376
|
+
request: nil,
|
377
|
+
response: response
|
378
|
+
)
|
379
|
+
in (400..) | TerminalShop::APIConnectionError
|
380
|
+
self.class.reap_connection!(status, stream: stream)
|
381
|
+
|
382
|
+
delay = retry_delay(response, retry_count: retry_count)
|
383
|
+
sleep(delay)
|
384
|
+
|
385
|
+
send_request(
|
386
|
+
request,
|
387
|
+
redirect_count: redirect_count,
|
388
|
+
retry_count: retry_count + 1,
|
389
|
+
send_retry_header: send_retry_header
|
390
|
+
)
|
391
|
+
end
|
392
|
+
end
|
393
|
+
|
394
|
+
# Execute the request specified by `req`. This is the method that all resource
|
395
|
+
# methods call into.
|
396
|
+
#
|
397
|
+
# @param req [Hash{Symbol=>Object}] .
|
398
|
+
#
|
399
|
+
# @option req [Symbol] :method
|
400
|
+
#
|
401
|
+
# @option req [String, Array<String>] :path
|
402
|
+
#
|
403
|
+
# @option req [Hash{String=>Array<String>, String, nil}, nil] :query
|
404
|
+
#
|
405
|
+
# @option req [Hash{String=>String, Integer, Array<String, Integer, nil>, nil}, nil] :headers
|
406
|
+
#
|
407
|
+
# @option req [Object, nil] :body
|
408
|
+
#
|
409
|
+
# @option req [Symbol, nil] :unwrap
|
410
|
+
#
|
411
|
+
# @option req [Class, nil] :page
|
412
|
+
#
|
413
|
+
# @option req [Class, nil] :stream
|
414
|
+
#
|
415
|
+
# @option req [TerminalShop::Type::Converter, Class, nil] :model
|
416
|
+
#
|
417
|
+
# @option req [TerminalShop::RequestOptions, Hash{Symbol=>Object}, nil] :options
|
418
|
+
#
|
419
|
+
# @raise [TerminalShop::APIError]
|
420
|
+
# @return [Object]
|
421
|
+
def request(req)
|
422
|
+
self.class.validate!(req)
|
423
|
+
model = req.fetch(:model) { TerminalShop::Unknown }
|
424
|
+
opts = req[:options].to_h
|
425
|
+
TerminalShop::RequestOptions.validate!(opts)
|
426
|
+
request = build_request(req.except(:options), opts)
|
427
|
+
url = request.fetch(:url)
|
428
|
+
|
429
|
+
# Don't send the current retry count in the headers if the caller modified the header defaults.
|
430
|
+
send_retry_header = request.fetch(:headers)["x-stainless-retry-count"] == "0"
|
431
|
+
status, response, stream = send_request(
|
432
|
+
request,
|
433
|
+
redirect_count: 0,
|
434
|
+
retry_count: 0,
|
435
|
+
send_retry_header: send_retry_header
|
436
|
+
)
|
437
|
+
|
438
|
+
decoded = TerminalShop::Util.decode_content(response, stream: stream)
|
439
|
+
case req
|
440
|
+
in { stream: Class => st }
|
441
|
+
st.new(model: model, url: url, status: status, response: response, stream: decoded)
|
442
|
+
in { page: Class => page }
|
443
|
+
page.new(client: self, req: req, headers: response, page_data: decoded)
|
444
|
+
else
|
445
|
+
unwrapped = TerminalShop::Util.dig(decoded, req[:unwrap])
|
446
|
+
TerminalShop::Type::Converter.coerce(model, unwrapped)
|
447
|
+
end
|
448
|
+
end
|
449
|
+
|
450
|
+
# @return [String]
|
451
|
+
def inspect
|
452
|
+
# rubocop:disable Layout/LineLength
|
453
|
+
base_url = TerminalShop::Util.unparse_uri(@base_url)
|
454
|
+
"#<#{self.class.name}:0x#{object_id.to_s(16)} base_url=#{base_url} max_retries=#{@max_retries} timeout=#{@timeout}>"
|
455
|
+
# rubocop:enable Layout/LineLength
|
456
|
+
end
|
457
|
+
end
|
458
|
+
end
|
459
|
+
end
|
@@ -0,0 +1,182 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TerminalShop
|
4
|
+
module Transport
|
5
|
+
# @api private
|
6
|
+
class PooledNetRequester
|
7
|
+
# from the golang stdlib
|
8
|
+
# https://github.com/golang/go/blob/c8eced8580028328fde7c03cbfcb720ce15b2358/src/net/http/transport.go#L49
|
9
|
+
KEEP_ALIVE_TIMEOUT = 30
|
10
|
+
|
11
|
+
class << self
|
12
|
+
# @api private
|
13
|
+
#
|
14
|
+
# @param url [URI::Generic]
|
15
|
+
#
|
16
|
+
# @return [Net::HTTP]
|
17
|
+
def connect(url)
|
18
|
+
port =
|
19
|
+
case [url.port, url.scheme]
|
20
|
+
in [Integer, _]
|
21
|
+
url.port
|
22
|
+
in [nil, "http" | "ws"]
|
23
|
+
Net::HTTP.http_default_port
|
24
|
+
in [nil, "https" | "wss"]
|
25
|
+
Net::HTTP.https_default_port
|
26
|
+
end
|
27
|
+
|
28
|
+
Net::HTTP.new(url.host, port).tap do
|
29
|
+
_1.use_ssl = %w[https wss].include?(url.scheme)
|
30
|
+
_1.max_retries = 0
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# @api private
|
35
|
+
#
|
36
|
+
# @param conn [Net::HTTP]
|
37
|
+
# @param deadline [Float]
|
38
|
+
def calibrate_socket_timeout(conn, deadline)
|
39
|
+
timeout = deadline - TerminalShop::Util.monotonic_secs
|
40
|
+
conn.open_timeout = conn.read_timeout = conn.write_timeout = conn.continue_timeout = timeout
|
41
|
+
end
|
42
|
+
|
43
|
+
# @api private
|
44
|
+
#
|
45
|
+
# @param request [Hash{Symbol=>Object}] .
|
46
|
+
#
|
47
|
+
# @option request [Symbol] :method
|
48
|
+
#
|
49
|
+
# @option request [URI::Generic] :url
|
50
|
+
#
|
51
|
+
# @option request [Hash{String=>String}] :headers
|
52
|
+
#
|
53
|
+
# @param blk [Proc]
|
54
|
+
#
|
55
|
+
# @yieldparam [String]
|
56
|
+
# @return [Net::HTTPGenericRequest]
|
57
|
+
def build_request(request, &blk)
|
58
|
+
method, url, headers, body = request.fetch_values(:method, :url, :headers, :body)
|
59
|
+
req = Net::HTTPGenericRequest.new(
|
60
|
+
method.to_s.upcase,
|
61
|
+
!body.nil?,
|
62
|
+
method != :head,
|
63
|
+
url.to_s
|
64
|
+
)
|
65
|
+
|
66
|
+
headers.each { req[_1] = _2 }
|
67
|
+
|
68
|
+
case body
|
69
|
+
in nil
|
70
|
+
nil
|
71
|
+
in String
|
72
|
+
req["content-length"] ||= body.bytesize.to_s unless req["transfer-encoding"]
|
73
|
+
req.body_stream = TerminalShop::Util::ReadIOAdapter.new(body, &blk)
|
74
|
+
in StringIO
|
75
|
+
req["content-length"] ||= body.size.to_s unless req["transfer-encoding"]
|
76
|
+
req.body_stream = TerminalShop::Util::ReadIOAdapter.new(body, &blk)
|
77
|
+
in IO | Enumerator
|
78
|
+
req["transfer-encoding"] ||= "chunked" unless req["content-length"]
|
79
|
+
req.body_stream = TerminalShop::Util::ReadIOAdapter.new(body, &blk)
|
80
|
+
end
|
81
|
+
|
82
|
+
req
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# @api private
|
87
|
+
#
|
88
|
+
# @param url [URI::Generic]
|
89
|
+
# @param deadline [Float]
|
90
|
+
# @param blk [Proc]
|
91
|
+
#
|
92
|
+
# @raise [Timeout::Error]
|
93
|
+
# @yieldparam [Net::HTTP]
|
94
|
+
private def with_pool(url, deadline:, &blk)
|
95
|
+
origin = TerminalShop::Util.uri_origin(url)
|
96
|
+
timeout = deadline - TerminalShop::Util.monotonic_secs
|
97
|
+
pool =
|
98
|
+
@mutex.synchronize do
|
99
|
+
@pools[origin] ||= ConnectionPool.new(size: @size) do
|
100
|
+
self.class.connect(url)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
pool.with(timeout: timeout, &blk)
|
105
|
+
end
|
106
|
+
|
107
|
+
# @api private
|
108
|
+
#
|
109
|
+
# @param request [Hash{Symbol=>Object}] .
|
110
|
+
#
|
111
|
+
# @option request [Symbol] :method
|
112
|
+
#
|
113
|
+
# @option request [URI::Generic] :url
|
114
|
+
#
|
115
|
+
# @option request [Hash{String=>String}] :headers
|
116
|
+
#
|
117
|
+
# @option request [Object] :body
|
118
|
+
#
|
119
|
+
# @option request [Float] :deadline
|
120
|
+
#
|
121
|
+
# @return [Array(Integer, Net::HTTPResponse, Enumerable)]
|
122
|
+
def execute(request)
|
123
|
+
url, deadline = request.fetch_values(:url, :deadline)
|
124
|
+
|
125
|
+
eof = false
|
126
|
+
finished = false
|
127
|
+
enum = Enumerator.new do |y|
|
128
|
+
with_pool(url, deadline: deadline) do |conn|
|
129
|
+
next if finished
|
130
|
+
|
131
|
+
req = self.class.build_request(request) do
|
132
|
+
self.class.calibrate_socket_timeout(conn, deadline)
|
133
|
+
end
|
134
|
+
|
135
|
+
self.class.calibrate_socket_timeout(conn, deadline)
|
136
|
+
unless conn.started?
|
137
|
+
conn.keep_alive_timeout = self.class::KEEP_ALIVE_TIMEOUT
|
138
|
+
conn.start
|
139
|
+
end
|
140
|
+
|
141
|
+
self.class.calibrate_socket_timeout(conn, deadline)
|
142
|
+
conn.request(req) do |rsp|
|
143
|
+
y << [conn, req, rsp]
|
144
|
+
break if finished
|
145
|
+
|
146
|
+
rsp.read_body do |bytes|
|
147
|
+
y << bytes
|
148
|
+
break if finished
|
149
|
+
|
150
|
+
self.class.calibrate_socket_timeout(conn, deadline)
|
151
|
+
end
|
152
|
+
eof = true
|
153
|
+
end
|
154
|
+
end
|
155
|
+
rescue Timeout::Error
|
156
|
+
raise TerminalShop::APITimeoutError
|
157
|
+
end
|
158
|
+
|
159
|
+
conn, _, response = enum.next
|
160
|
+
body = TerminalShop::Util.fused_enum(enum, external: true) do
|
161
|
+
finished = true
|
162
|
+
tap do
|
163
|
+
enum.next
|
164
|
+
rescue StopIteration
|
165
|
+
nil
|
166
|
+
end
|
167
|
+
conn.finish if !eof && conn&.started?
|
168
|
+
end
|
169
|
+
[Integer(response.code), response, (response.body = body)]
|
170
|
+
end
|
171
|
+
|
172
|
+
# @api private
|
173
|
+
#
|
174
|
+
# @param size [Integer]
|
175
|
+
def initialize(size: Etc.nprocessors)
|
176
|
+
@mutex = Mutex.new
|
177
|
+
@size = size
|
178
|
+
@pools = {}
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|