x-core 1.0.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 +7 -0
- data/.yardopts +8 -0
- data/CHANGELOG.md +353 -0
- data/LICENSE.txt +21 -0
- data/README.md +54 -0
- data/lib/x/core/app_only_authenticator.rb +198 -0
- data/lib/x/core/authenticator.rb +74 -0
- data/lib/x/core/authenticator_request.rb +56 -0
- data/lib/x/core/bearer_token_authenticator.rb +46 -0
- data/lib/x/core/built_response.rb +100 -0
- data/lib/x/core/client.rb +728 -0
- data/lib/x/core/client_app_only.rb +115 -0
- data/lib/x/core/client_credentials.rb +193 -0
- data/lib/x/core/client_internals.rb +315 -0
- data/lib/x/core/client_memo.rb +58 -0
- data/lib/x/core/client_settings.rb +166 -0
- data/lib/x/core/client_token_refresh.rb +281 -0
- data/lib/x/core/connection.rb +328 -0
- data/lib/x/core/connection_pool.rb +123 -0
- data/lib/x/core/connection_proxy.rb +151 -0
- data/lib/x/core/connection_request.rb +90 -0
- data/lib/x/core/credential_holder.rb +78 -0
- data/lib/x/core/credential_validator.rb +286 -0
- data/lib/x/core/errors/authorization_denied.rb +55 -0
- data/lib/x/core/errors/authorization_error.rb +41 -0
- data/lib/x/core/errors/bad_gateway.rb +9 -0
- data/lib/x/core/errors/bad_request.rb +10 -0
- data/lib/x/core/errors/callback_error.rb +88 -0
- data/lib/x/core/errors/client_error.rb +17 -0
- data/lib/x/core/errors/conflict.rb +9 -0
- data/lib/x/core/errors/error.rb +66 -0
- data/lib/x/core/errors/forbidden.rb +10 -0
- data/lib/x/core/errors/gateway_timeout.rb +9 -0
- data/lib/x/core/errors/gone.rb +9 -0
- data/lib/x/core/errors/http_error.rb +333 -0
- data/lib/x/core/errors/internal_server_error.rb +9 -0
- data/lib/x/core/errors/invalid_response.rb +96 -0
- data/lib/x/core/errors/method_not_allowed.rb +10 -0
- data/lib/x/core/errors/network_error.rb +59 -0
- data/lib/x/core/errors/not_acceptable.rb +10 -0
- data/lib/x/core/errors/not_found.rb +15 -0
- data/lib/x/core/errors/payload_too_large.rb +10 -0
- data/lib/x/core/errors/payment_required.rb +10 -0
- data/lib/x/core/errors/request_timeout.rb +9 -0
- data/lib/x/core/errors/server_error.rb +15 -0
- data/lib/x/core/errors/service_unavailable.rb +9 -0
- data/lib/x/core/errors/token_report_failed.rb +59 -0
- data/lib/x/core/errors/too_many_redirects.rb +53 -0
- data/lib/x/core/errors/too_many_requests.rb +80 -0
- data/lib/x/core/errors/unauthorized.rb +15 -0
- data/lib/x/core/errors/unavailable_for_legal_reasons.rb +10 -0
- data/lib/x/core/errors/unprocessable_entity.rb +10 -0
- data/lib/x/core/errors/unsupported_format.rb +22 -0
- data/lib/x/core/errors/unsupported_media_type.rb +10 -0
- data/lib/x/core/errors/unsupported_operation.rb +18 -0
- data/lib/x/core/oauth1_authenticator.rb +142 -0
- data/lib/x/core/oauth2_authenticator.rb +296 -0
- data/lib/x/core/oauth2_authorization.rb +405 -0
- data/lib/x/core/oauth2_refresh.rb +272 -0
- data/lib/x/core/oauth2_tokens.rb +277 -0
- data/lib/x/core/origin.rb +95 -0
- data/lib/x/core/problem.rb +302 -0
- data/lib/x/core/proxy_setting.rb +22 -0
- data/lib/x/core/rate_limit.rb +137 -0
- data/lib/x/core/rate_limit_handler.rb +166 -0
- data/lib/x/core/redirect_handler.rb +208 -0
- data/lib/x/core/refresh_reporter.rb +92 -0
- data/lib/x/core/request_builder.rb +155 -0
- data/lib/x/core/request_context.rb +85 -0
- data/lib/x/core/request_encoding.rb +135 -0
- data/lib/x/core/response.rb +196 -0
- data/lib/x/core/response_headers.rb +31 -0
- data/lib/x/core/response_parser.rb +155 -0
- data/lib/x/core/retry_handler.rb +149 -0
- data/lib/x/core/setting_validator.rb +399 -0
- data/lib/x/core/stream_body.rb +77 -0
- data/lib/x/core/stream_response.rb +267 -0
- data/lib/x/core/token_endpoint.rb +238 -0
- data/lib/x/core/version.rb +25 -0
- data/lib/x/core.rb +6 -0
- data/sig/manifest.yaml +8 -0
- data/sig/x-core.rbs +396 -0
- metadata +162 -0
|
@@ -0,0 +1,728 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "client_internals"
|
|
4
|
+
require_relative "connection"
|
|
5
|
+
require_relative "credential_holder"
|
|
6
|
+
require_relative "rate_limit_handler"
|
|
7
|
+
require_relative "redirect_handler"
|
|
8
|
+
require_relative "retry_handler"
|
|
9
|
+
require_relative "setting_validator"
|
|
10
|
+
|
|
11
|
+
module X
|
|
12
|
+
module Core
|
|
13
|
+
# A client for interacting with the X API
|
|
14
|
+
#
|
|
15
|
+
# An endpoint is resolved against the base URL, and a request carries the client's credentials to the origin of
|
|
16
|
+
# that base URL alone: the scheme, host, and port it names. An endpoint that names a whole URL of another origin
|
|
17
|
+
# is sent there without them, as a redirect that leads to one is, so that the credentials of the API never reach
|
|
18
|
+
# a host they were not meant for; see {Core::Origin}.
|
|
19
|
+
#
|
|
20
|
+
# The object_class of a request, of a stream, or of a client is one of two things. A class that JSON.parse builds
|
|
21
|
+
# each JSON object of the body into, as it does Hash, the default, OpenStruct, or a Struct, whose new takes no
|
|
22
|
+
# arguments and whose instances take each member with []=. Or anything that responds to from_response, which
|
|
23
|
+
# builds the result from the whole body instead, as the resource classes of x-resources do: it is passed the body
|
|
24
|
+
# parsed into Hashes and Arrays, whatever the array_class, and the client that made the request as client:, and
|
|
25
|
+
# what it returns is what the request returns, or, for a stream, what its block is passed for each object. Later
|
|
26
|
+
# versions of 1.x may pass it keyword arguments of their own, so it accepts the ones it does not read with **, as
|
|
27
|
+
# in def self.from_response(body, client:, **). The signatures of x-core state it as the X::_ResponseBuilder
|
|
28
|
+
# interface.
|
|
29
|
+
#
|
|
30
|
+
# A client keeps its credentials, settings, and connection in an object of x-core it delegates to, and has no
|
|
31
|
+
# private methods but initialize, so that the methods x-resources and x-uploads include into it, which may be
|
|
32
|
+
# named as they like, take the place of none of its own.
|
|
33
|
+
#
|
|
34
|
+
# @api public
|
|
35
|
+
class ::X::Client
|
|
36
|
+
include CredentialHolder
|
|
37
|
+
|
|
38
|
+
# Default base URL for the X API
|
|
39
|
+
DEFAULT_BASE_URL = "https://api.x.com/2/"
|
|
40
|
+
# Default class for parsing JSON arrays
|
|
41
|
+
DEFAULT_ARRAY_CLASS = Array
|
|
42
|
+
# Default class for parsing JSON objects
|
|
43
|
+
DEFAULT_OBJECT_CLASS = Hash
|
|
44
|
+
# Default timeout for opening connections in seconds
|
|
45
|
+
DEFAULT_OPEN_TIMEOUT = Connection::DEFAULT_OPEN_TIMEOUT
|
|
46
|
+
# Default timeout for reading responses in seconds
|
|
47
|
+
DEFAULT_READ_TIMEOUT = Connection::DEFAULT_READ_TIMEOUT
|
|
48
|
+
# Default timeout for writing requests in seconds
|
|
49
|
+
DEFAULT_WRITE_TIMEOUT = Connection::DEFAULT_WRITE_TIMEOUT
|
|
50
|
+
# Default time to keep a connection open for the next request to the same host, in seconds
|
|
51
|
+
DEFAULT_KEEP_ALIVE_TIMEOUT = Connection::DEFAULT_KEEP_ALIVE_TIMEOUT
|
|
52
|
+
# Default maximum number of redirects to follow
|
|
53
|
+
DEFAULT_MAX_REDIRECTS = RedirectHandler::DEFAULT_MAX_REDIRECTS
|
|
54
|
+
# Default maximum number of times to retry a request refused for a rate limit
|
|
55
|
+
DEFAULT_MAX_RATE_LIMIT_RETRIES = RateLimitHandler::DEFAULT_MAX_RETRIES
|
|
56
|
+
# Default maximum number of seconds to wait for a rate limit to reset
|
|
57
|
+
DEFAULT_MAX_RATE_LIMIT_WAIT = RateLimitHandler::DEFAULT_MAX_WAIT
|
|
58
|
+
# Default maximum number of times to send an idempotent request again after a failure
|
|
59
|
+
DEFAULT_MAX_RETRIES = RetryHandler::DEFAULT_MAX_RETRIES
|
|
60
|
+
|
|
61
|
+
# The timeout for opening connections, in seconds
|
|
62
|
+
# @api public
|
|
63
|
+
# @return [Integer, Float, nil] the timeout, or nil for none
|
|
64
|
+
# @example Get the open timeout
|
|
65
|
+
# client.open_timeout # => 10
|
|
66
|
+
def open_timeout = @internals.open_timeout
|
|
67
|
+
|
|
68
|
+
# The timeout for reading responses, in seconds
|
|
69
|
+
# @api public
|
|
70
|
+
# @return [Integer, Float, nil] the timeout, or nil for none
|
|
71
|
+
# @example Get the read timeout
|
|
72
|
+
# client.read_timeout # => 60
|
|
73
|
+
def read_timeout = @internals.read_timeout
|
|
74
|
+
|
|
75
|
+
# The timeout for writing requests, in seconds
|
|
76
|
+
# @api public
|
|
77
|
+
# @return [Integer, Float, nil] the timeout, or nil for none
|
|
78
|
+
# @example Get the write timeout
|
|
79
|
+
# client.write_timeout # => 60
|
|
80
|
+
def write_timeout = @internals.write_timeout
|
|
81
|
+
|
|
82
|
+
# The time to keep an idle connection open for the next request, in seconds
|
|
83
|
+
# @api public
|
|
84
|
+
# @return [Integer, Float] the timeout
|
|
85
|
+
# @example Get the keep-alive timeout
|
|
86
|
+
# client.keep_alive_timeout # => 30
|
|
87
|
+
def keep_alive_timeout = @internals.keep_alive_timeout
|
|
88
|
+
|
|
89
|
+
# The IO debug output is written to
|
|
90
|
+
# @api public
|
|
91
|
+
# @return [IO, #<<, nil] the IO, or anything else that takes a String with <<, or nil for none
|
|
92
|
+
# @example Get the debug output
|
|
93
|
+
# client.debug_output
|
|
94
|
+
def debug_output = @internals.debug_output
|
|
95
|
+
|
|
96
|
+
# The maximum number of redirects to follow
|
|
97
|
+
# @api public
|
|
98
|
+
# @return [Integer] the maximum number of redirects
|
|
99
|
+
# @example Get the maximum number of redirects
|
|
100
|
+
# client.max_redirects # => 10
|
|
101
|
+
def max_redirects = @internals.max_redirects
|
|
102
|
+
|
|
103
|
+
# The maximum number of times to retry a request refused for a rate limit
|
|
104
|
+
# @api public
|
|
105
|
+
# @return [Integer] the maximum number of retries
|
|
106
|
+
# @example Get the maximum number of rate limit retries
|
|
107
|
+
# client.max_rate_limit_retries # => 0
|
|
108
|
+
def max_rate_limit_retries = @internals.max_rate_limit_retries
|
|
109
|
+
|
|
110
|
+
# The maximum number of seconds to wait for a rate limit to reset
|
|
111
|
+
# @api public
|
|
112
|
+
# @return [Integer, Float] the maximum wait, in seconds
|
|
113
|
+
# @example Get the maximum rate limit wait
|
|
114
|
+
# client.max_rate_limit_wait # => 900
|
|
115
|
+
def max_rate_limit_wait = @internals.max_rate_limit_wait
|
|
116
|
+
|
|
117
|
+
# The maximum number of times to send an idempotent request again after a failure
|
|
118
|
+
# @api public
|
|
119
|
+
# @return [Integer] the maximum number of retries
|
|
120
|
+
# @example Get the maximum number of retries
|
|
121
|
+
# client.max_retries # => 2
|
|
122
|
+
def max_retries = @internals.max_retries
|
|
123
|
+
|
|
124
|
+
# The authenticator for API requests
|
|
125
|
+
#
|
|
126
|
+
# It is the one the client was given, or else the one it built of its credentials. A client sends the token
|
|
127
|
+
# requests of an authenticator that makes them, an AppOnlyAuthenticator or an OAuth2Authenticator, over its own
|
|
128
|
+
# connection, with its proxy, timeouts, and debug output, whether it built the authenticator or was given it; an
|
|
129
|
+
# authenticator given to several clients sends them over the connection of the first, but for an OAuth 2.0
|
|
130
|
+
# refresh a request of another of them makes, which goes to the token endpoint at the origin of that client's
|
|
131
|
+
# base URL, over its connection, with its headers. The refreshes of an OAuth2Authenticator reach the save_tokens
|
|
132
|
+
# of each client that authenticates with it, a refresh reads the stored tokens with the load_tokens of the
|
|
133
|
+
# authenticator, or else with the load_tokens of a client that authenticates with it, and the expires_at of the
|
|
134
|
+
# client is the authenticator's.
|
|
135
|
+
#
|
|
136
|
+
# @api public
|
|
137
|
+
# @return [Authenticator] the authenticator instance
|
|
138
|
+
# @example Check if the OAuth 2.0 token has expired
|
|
139
|
+
# client.authenticator.token_expired?
|
|
140
|
+
def authenticator = @internals.authenticator
|
|
141
|
+
|
|
142
|
+
# A callable passed the OAuth2Tokens of each refresh, and of an authorization
|
|
143
|
+
# @api public
|
|
144
|
+
# @return [#call, nil] the callable, or nil for none
|
|
145
|
+
# @example Read the hook a refresh reports to
|
|
146
|
+
# client.save_tokens
|
|
147
|
+
def save_tokens = @internals.save_tokens
|
|
148
|
+
|
|
149
|
+
# The callable a refresh reads the stored OAuth2Tokens with
|
|
150
|
+
#
|
|
151
|
+
# It returns the tokens in the storage that processes sharing the tokens of a user read, or nil for none there.
|
|
152
|
+
#
|
|
153
|
+
# @api public
|
|
154
|
+
# @return [#call, nil] the callable, or nil for none
|
|
155
|
+
# @example Read the loader a refresh reads stored tokens with
|
|
156
|
+
# client.load_tokens
|
|
157
|
+
def load_tokens = @internals.load_tokens
|
|
158
|
+
|
|
159
|
+
# The API key for OAuth 1.0a authentication
|
|
160
|
+
#
|
|
161
|
+
# It is a frozen copy of the one the client was given, or the one the OAuth1Authenticator or AppOnlyAuthenticator
|
|
162
|
+
# it was given in place of credentials holds.
|
|
163
|
+
#
|
|
164
|
+
# @api public
|
|
165
|
+
# @return [String, nil] the API key for OAuth 1.0a authentication
|
|
166
|
+
# @example Get the API key
|
|
167
|
+
# client.api_key
|
|
168
|
+
def api_key = @internals.api_key_in_use
|
|
169
|
+
|
|
170
|
+
# The OAuth 2.0 client ID
|
|
171
|
+
#
|
|
172
|
+
# It is a frozen copy of the one the client was given, or the one the OAuth2Authenticator it was given in place of
|
|
173
|
+
# credentials holds.
|
|
174
|
+
#
|
|
175
|
+
# @api public
|
|
176
|
+
# @return [String, nil] the OAuth 2.0 client ID
|
|
177
|
+
# @example Get the client ID
|
|
178
|
+
# client.client_id
|
|
179
|
+
def client_id = @internals.client_id_in_use
|
|
180
|
+
|
|
181
|
+
# The time the OAuth 2.0 access token expires, as last refreshed
|
|
182
|
+
#
|
|
183
|
+
# A refresh that reports no lifetime leaves it nil, rather than the time the client was given.
|
|
184
|
+
#
|
|
185
|
+
# @api public
|
|
186
|
+
# @return [Time, nil] the expiration time, or nil if it is not known
|
|
187
|
+
# @example Get the expiration time
|
|
188
|
+
# client.expires_at
|
|
189
|
+
def expires_at = @internals.expires_at
|
|
190
|
+
|
|
191
|
+
# The scopes X granted the OAuth 2.0 access token, as last refreshed
|
|
192
|
+
#
|
|
193
|
+
# A refresh that names no scopes keeps those the client held, as OAuth 2.0 has it. They are the ones the client
|
|
194
|
+
# was given, as the client of OAuth2Authorization#client is given those of the exchange of the code, until a
|
|
195
|
+
# refresh names others.
|
|
196
|
+
#
|
|
197
|
+
# @api public
|
|
198
|
+
# @return [Array<String>, nil] the scopes, frozen, or nil if they are not known
|
|
199
|
+
# @example Check that the user let the app post
|
|
200
|
+
# client.scopes&.include?("tweet.write")
|
|
201
|
+
def scopes = @internals.scopes
|
|
202
|
+
|
|
203
|
+
# The base URL for API requests
|
|
204
|
+
# @api public
|
|
205
|
+
# @return [String] the base URL for API requests, which ends with a slash
|
|
206
|
+
# @example Get the base URL
|
|
207
|
+
# client.base_url # => "https://api.x.com/2/"
|
|
208
|
+
def base_url = @internals.base_url
|
|
209
|
+
|
|
210
|
+
# The default class for parsing JSON arrays
|
|
211
|
+
# @api public
|
|
212
|
+
# @return [Class] the default class for parsing JSON arrays
|
|
213
|
+
# @example Get the default array class
|
|
214
|
+
# client.default_array_class # => Array
|
|
215
|
+
def default_array_class = @internals.default_array_class
|
|
216
|
+
|
|
217
|
+
# The default class for parsing JSON objects
|
|
218
|
+
#
|
|
219
|
+
# It is a class that JSON.parse builds each JSON object into, or one that responds to from_response and builds
|
|
220
|
+
# the result from the whole body; see {Client}.
|
|
221
|
+
#
|
|
222
|
+
# @api public
|
|
223
|
+
# @return [Class, #from_response] the default class for parsing JSON objects
|
|
224
|
+
# @example Get the default object class
|
|
225
|
+
# client.default_object_class # => Hash
|
|
226
|
+
def default_object_class = @internals.default_object_class
|
|
227
|
+
|
|
228
|
+
# A callable passed an X::Response after each request and streamed object
|
|
229
|
+
#
|
|
230
|
+
# It is the hook of every request a client makes. A block passed to a single request receives the same
|
|
231
|
+
# summary, after this, for code that reads the response of that one request rather than of all of them.
|
|
232
|
+
#
|
|
233
|
+
# @api public
|
|
234
|
+
# @return [#call, nil] the callable, or nil for none
|
|
235
|
+
# @example Read the hook a client reports to
|
|
236
|
+
# client.on_response
|
|
237
|
+
def on_response = @internals.on_response
|
|
238
|
+
|
|
239
|
+
# The headers sent with every request the client makes
|
|
240
|
+
#
|
|
241
|
+
# They are defaults: a header of the same name passed to a request, or to a stream, is sent in place of the
|
|
242
|
+
# client's, and each of them is sent in place of a default of the gem, such as its User-Agent. A header that
|
|
243
|
+
# carries credentials, such as Authorization or Cookie, is dropped by a redirect to another origin, as one
|
|
244
|
+
# passed to a request is.
|
|
245
|
+
#
|
|
246
|
+
# Each is named by a String, a header the client was given by a Symbol among them, so that a header is read by
|
|
247
|
+
# the name it is sent with, whichever the client was given: a Symbol names the header its underscores name with
|
|
248
|
+
# hyphens, as :user_agent names User-Agent.
|
|
249
|
+
#
|
|
250
|
+
# @api public
|
|
251
|
+
# @return [Hash{String => String}] the headers, frozen, each named by a String
|
|
252
|
+
# @example Read the headers a client sends
|
|
253
|
+
# client.headers # => {"User-Agent" => "my-app/1.0"}
|
|
254
|
+
def headers = @internals.headers
|
|
255
|
+
|
|
256
|
+
# Initialize a new X API client
|
|
257
|
+
#
|
|
258
|
+
# @api public
|
|
259
|
+
# @param api_key [String, nil] the API key for OAuth 1.0a authentication
|
|
260
|
+
# @param api_key_secret [String, nil] the API key secret for OAuth 1.0a authentication
|
|
261
|
+
# @param access_token [String, nil] the access token for OAuth authentication
|
|
262
|
+
# @param access_token_secret [String, nil] the access token secret for OAuth 1.0a authentication
|
|
263
|
+
# @param bearer_token [String, nil] the bearer token for authentication
|
|
264
|
+
# @param client_id [String, nil] the OAuth 2.0 client ID
|
|
265
|
+
# @param client_secret [String, nil] the OAuth 2.0 client secret
|
|
266
|
+
# @param refresh_token [String, nil] the OAuth 2.0 refresh token, or nil beside a client ID and access token issued
|
|
267
|
+
# without offline.access, which authenticate as the user until the access token expires, and cannot refresh
|
|
268
|
+
# @param expires_at [Time, nil] the time the OAuth 2.0 access token expires, after which a request refreshes it,
|
|
269
|
+
# given only beside the client_id and access_token the client authenticates with
|
|
270
|
+
# @param scopes [Array<String>, nil] the scopes X granted the OAuth 2.0 access token, as OAuth2Tokens#scopes
|
|
271
|
+
# holds them, given only beside the client_id and access_token the client authenticates with
|
|
272
|
+
# @param authenticator [Authenticator, nil] an authenticator to authenticate with in place of credentials, such as
|
|
273
|
+
# an OAuth2Authenticator built elsewhere, or nil to build one of the credentials; see {#authenticator}
|
|
274
|
+
# @param base_url [String] the base URL for API requests
|
|
275
|
+
# @param open_timeout [Integer, Float, nil] the timeout for opening connections in seconds, or nil for none
|
|
276
|
+
# @param read_timeout [Integer, Float, nil] the timeout for reading responses in seconds, or nil for none
|
|
277
|
+
# @param write_timeout [Integer, Float, nil] the timeout for writing requests in seconds, or nil for none
|
|
278
|
+
# @param keep_alive_timeout [Integer, Float] the time to keep a connection open for the next request to the same
|
|
279
|
+
# host, in seconds, which a proxy that closes idle connections sooner than X does may need lowered
|
|
280
|
+
# @param debug_output [IO, #<<, nil] the IO object for debug output, or anything else that takes a String with <<,
|
|
281
|
+
# such as a StringIO. It is written every request and response whole, in the clear: the Authorization header,
|
|
282
|
+
# the client secret a token request sends, and the tokens a token response holds. Send it to a file you
|
|
283
|
+
# control while debugging, never to a log that is shipped elsewhere, and leave it nil in production.
|
|
284
|
+
# @param proxy_url [String, URI::Generic, nil] the proxy URL for requests, or nil to take the proxy the
|
|
285
|
+
# environment names for each request: https_proxy for HTTPS and http_proxy for HTTP, unless no_proxy names the
|
|
286
|
+
# host; one of them that cannot be parsed, or is not an http or https URL with a host, as one with no scheme
|
|
287
|
+
# is not, is refused by the request that reads it
|
|
288
|
+
# @param default_array_class [Class] the default class for parsing JSON arrays
|
|
289
|
+
# @param default_object_class [Class, #from_response] the default class for parsing JSON objects, or one that
|
|
290
|
+
# responds to from_response and builds the result from the whole body; see {Client}
|
|
291
|
+
# @param headers [Hash{String, Symbol => String}] headers sent with every request the client makes, as defaults: a
|
|
292
|
+
# header of the same name passed to a request is sent in place of one of these, and each of these is sent in
|
|
293
|
+
# place of a default of the gem, such as its User-Agent; a Symbol names the header its underscores name with
|
|
294
|
+
# hyphens, as :user_agent names User-Agent
|
|
295
|
+
# @param max_redirects [Integer] the maximum number of redirects to follow, beyond which a redirect raises
|
|
296
|
+
# TooManyRedirects; 0 follows none, and raises for each redirect that could be followed
|
|
297
|
+
# @param max_rate_limit_retries [Integer] the maximum number of times to retry a request refused for a rate limit,
|
|
298
|
+
# after waiting for the limit to reset
|
|
299
|
+
# @param max_rate_limit_wait [Integer, Float] the maximum number of seconds to wait for a rate limit to reset; a request
|
|
300
|
+
# whose limit resets later raises TooManyRequests at once, as does a stream that would wait longer to reconnect,
|
|
301
|
+
# and a few seconds are added at random to each wait of a request, so that the requests one reset releases are
|
|
302
|
+
# not sent again in one burst
|
|
303
|
+
# @param max_retries [Integer] the maximum number of times to send a request again after the API failed to answer
|
|
304
|
+
# it, with a 5xx status or a 408, or after its answer never arrived, which is twice by default and is 0 for a client
|
|
305
|
+
# that raises at once; a retry waits up to a second before the first and up to twice as long before each after,
|
|
306
|
+
# but never more than a minute, a random share of each wait taken off so that the requests one failure of the
|
|
307
|
+
# API ended are not sent again together, or for as long as the response asks when it carries a Retry-After
|
|
308
|
+
# header, whichever is longer, and a response that asks for longer than a minute raises at once; a 429 is not
|
|
309
|
+
# among these, and waits for its rate limit to reset as max_rate_limit_wait allows, however long past a minute; only a GET, PUT, or DELETE is sent again, since
|
|
310
|
+
# the API may have acted on a POST whose answer never arrived, and one whose answer never arrived is sent again
|
|
311
|
+
# only when it never reached the API, such as for a connection refused or one that timed out opening, since the
|
|
312
|
+
# API bills a read it answered, such as one that timed out reading its response, whether or not the answer came
|
|
313
|
+
# @param on_response [#call, nil] a callable passed an X::Response after every request, failed ones included, and
|
|
314
|
+
# every object a stream delivers; a block passed to a single request receives the same summary, after this
|
|
315
|
+
# @param save_tokens [#call, nil] a callable passed the OAuth2Tokens of each refresh, to store them; the
|
|
316
|
+
# refreshes are reported one at a time, in the order they were made, and one already replaced is not reported;
|
|
317
|
+
# the client of OAuth2Authorization#client passes it the tokens of the exchange of the code as well; a callable
|
|
318
|
+
# that raises, as one whose storage is briefly down may, raises TokenReportFailed from the request that
|
|
319
|
+
# refreshed, which holds the tokens, since the refresh token they replaced is spent, and the client, with the
|
|
320
|
+
# error of the callable as its cause
|
|
321
|
+
# @param load_tokens [#call, nil] a callable that takes no arguments and returns the OAuth2Tokens in the storage
|
|
322
|
+
# that save_tokens writes to, or nil for none there, for processes that share the tokens of a user: X
|
|
323
|
+
# accepts a refresh token once, so a refresh reads the storage first, under its lock, and takes the tokens there
|
|
324
|
+
# in place of its own when their refresh token is another, as it is once another process has refreshed; it
|
|
325
|
+
# sends the request with them when their access token has not expired, and refreshes with them when it has,
|
|
326
|
+
# and a refresh X refuses for a refresh token another process spent reads the storage again, and takes the
|
|
327
|
+
# tokens there in place of raising once that process has stored them, and raises AuthorizationError if it has
|
|
328
|
+
# yet to, when the next request takes them; the tokens it takes came from the storage, so save_tokens is not
|
|
329
|
+
# passed them; see {#authenticator}
|
|
330
|
+
# @return [Client] a new client instance
|
|
331
|
+
# @raise [ArgumentError] if credentials are given that do not form a complete set, which would send requests
|
|
332
|
+
# without them, or authenticate as the app rather than a user
|
|
333
|
+
# @raise [ArgumentError] if a credential is an empty String, as an environment variable that is not set is often
|
|
334
|
+
# read, which would send an Authorization header that authenticates nothing
|
|
335
|
+
# @raise [ArgumentError] if expires_at is neither a Time nor nil, or scopes neither an Array of Strings that each
|
|
336
|
+
# name a scope nor nil, or either is given to a client that does not authenticate with OAuth 2.0 credentials,
|
|
337
|
+
# which would leave it unused
|
|
338
|
+
# @raise [ArgumentError] if an authenticator is given that is not an Authenticator, or beside credentials,
|
|
339
|
+
# expires_at, or scopes, which it would leave unused
|
|
340
|
+
# @raise [ArgumentError] if a timeout is neither a finite number of seconds of at least 0 nor, for any but
|
|
341
|
+
# keep_alive_timeout, nil, or if a maximum is not a count or a number of seconds of at least 0
|
|
342
|
+
# @raise [ArgumentError] if debug_output is a String, as the name of a file is, which would be written every
|
|
343
|
+
# request in place of the file, or is neither nil nor takes a String with <<, as true and a Proc do not
|
|
344
|
+
# @raise [ArgumentError] if base_url is not an absolute http or https URL with no user, password, query, or
|
|
345
|
+
# fragment, or headers are not a Hash that names each header with a String or a Symbol and gives it a String
|
|
346
|
+
# @raise [ArgumentError] if on_response, save_tokens, or load_tokens is neither nil nor responds to call
|
|
347
|
+
# @raise [ArgumentError] if default_array_class is not a Class, or default_object_class is neither a Class nor
|
|
348
|
+
# responds to from_response, which a response would be parsed with once the API had answered the request
|
|
349
|
+
# @example Create a client with bearer token authentication
|
|
350
|
+
# client = X::Client.new(bearer_token: "your_bearer_token")
|
|
351
|
+
# @example Create a client with OAuth 2.0 authentication that stores the tokens of each refresh
|
|
352
|
+
# client = X::Client.new(client_id: "id", client_secret: "secret", access_token: "token", refresh_token: "refresh",
|
|
353
|
+
# expires_at: Time.now + 7200, save_tokens: ->(tokens) { store.save(tokens.refresh_token) })
|
|
354
|
+
# @example Share the tokens of a user among processes, which store each refresh and read the store before one
|
|
355
|
+
# stored = store.load(user)
|
|
356
|
+
# client = X::Client.new(client_id: "id", **stored.to_h,
|
|
357
|
+
# save_tokens: ->(tokens) { store.save(user, tokens) },
|
|
358
|
+
# load_tokens: -> { store.load(user) })
|
|
359
|
+
# @example Create a client with OAuth 1.0a authentication
|
|
360
|
+
# client = X::Client.new(api_key: "key", api_key_secret: "secret", access_token: "token", access_token_secret: "token_secret")
|
|
361
|
+
# @example Create a client that authenticates with an authenticator built elsewhere
|
|
362
|
+
# client = X::Client.new(authenticator: X::OAuth2Authenticator.new(client_id: "id", access_token: "token",
|
|
363
|
+
# refresh_token: "refresh", expires_at: Time.now + 7200), save_tokens: ->(tokens) { store.save(tokens) })
|
|
364
|
+
# @example Create a client that fetches an app-only bearer token with the API key and secret
|
|
365
|
+
# client = X::Client.new(api_key: "key", api_key_secret: "secret")
|
|
366
|
+
# @example Create a client that retries a rate-limited request up to three times
|
|
367
|
+
# client = X::Client.new(bearer_token: "your_bearer_token", max_rate_limit_retries: 3)
|
|
368
|
+
# @example Create a client that raises at once rather than send a lookup again the API failed to answer
|
|
369
|
+
# client = X::Client.new(bearer_token: "your_bearer_token", max_retries: 0)
|
|
370
|
+
# @example Create a client that names the application in the User-Agent of every request
|
|
371
|
+
# client = X::Client.new(bearer_token: "your_bearer_token", headers: {"User-Agent" => "my-app/1.0"})
|
|
372
|
+
def initialize(api_key: nil, api_key_secret: nil, access_token: nil, access_token_secret: nil,
|
|
373
|
+
bearer_token: nil, client_id: nil, client_secret: nil, refresh_token: nil, expires_at: nil, scopes: nil,
|
|
374
|
+
authenticator: nil,
|
|
375
|
+
base_url: DEFAULT_BASE_URL,
|
|
376
|
+
open_timeout: DEFAULT_OPEN_TIMEOUT,
|
|
377
|
+
read_timeout: DEFAULT_READ_TIMEOUT,
|
|
378
|
+
write_timeout: DEFAULT_WRITE_TIMEOUT,
|
|
379
|
+
keep_alive_timeout: DEFAULT_KEEP_ALIVE_TIMEOUT,
|
|
380
|
+
debug_output: nil,
|
|
381
|
+
proxy_url: nil,
|
|
382
|
+
default_array_class: DEFAULT_ARRAY_CLASS,
|
|
383
|
+
default_object_class: DEFAULT_OBJECT_CLASS,
|
|
384
|
+
headers: {},
|
|
385
|
+
max_redirects: DEFAULT_MAX_REDIRECTS,
|
|
386
|
+
max_rate_limit_retries: DEFAULT_MAX_RATE_LIMIT_RETRIES,
|
|
387
|
+
max_rate_limit_wait: DEFAULT_MAX_RATE_LIMIT_WAIT,
|
|
388
|
+
max_retries: DEFAULT_MAX_RETRIES,
|
|
389
|
+
on_response: nil,
|
|
390
|
+
save_tokens: nil,
|
|
391
|
+
load_tokens: nil)
|
|
392
|
+
@internals = ClientInternals.new(self, api_key:, api_key_secret:, access_token:, access_token_secret:, bearer_token:,
|
|
393
|
+
client_id:, client_secret:, refresh_token:, expires_at:, scopes:, authenticator:, base_url:, open_timeout:, read_timeout:,
|
|
394
|
+
write_timeout:, keep_alive_timeout:, debug_output:, proxy_url:, default_array_class:, default_object_class:,
|
|
395
|
+
headers:, max_redirects:, max_rate_limit_retries:, max_rate_limit_wait:, max_retries:, on_response:,
|
|
396
|
+
save_tokens:, load_tokens:)
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
# Summarize the client for the console without revealing credentials
|
|
400
|
+
#
|
|
401
|
+
# @api public
|
|
402
|
+
# @return [String] the class name, base URL, and authenticator
|
|
403
|
+
# @example Inspect a client
|
|
404
|
+
# client.inspect # => #<X::Client base_url="https://api.x.com/2/" authenticator=#<X::BearerTokenAuthenticator>>
|
|
405
|
+
def inspect
|
|
406
|
+
"#<#{self.class} base_url=#{base_url.inspect} authenticator=#{authenticator.inspect}>"
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
# Copy the client with some of its options changed
|
|
410
|
+
#
|
|
411
|
+
# A copy that authenticates with OAuth 2.0 shares the client's authenticator, so that a refresh by either
|
|
412
|
+
# client reaches the other, since X accepts a refresh token once, unless it is given a client ID, client secret,
|
|
413
|
+
# access token, or refresh token that the authenticator does not hold. It shares it whatever the tokens are when
|
|
414
|
+
# it is built, so a refresh on another thread while it is built reaches it too. A refresh then passes the
|
|
415
|
+
# tokens it issued to the save_tokens of each client that shares it, once for each distinct callable. The
|
|
416
|
+
# expiration time and the scopes belong to the access token they share, so such a copy is refused either: give
|
|
417
|
+
# expires_at or scopes beside the access token and refresh token they describe.
|
|
418
|
+
#
|
|
419
|
+
# A copy that does not share the OAuth 2.0 authenticator, since it is given credentials the authenticator does
|
|
420
|
+
# not hold, an authenticator of its own, or, as a copy of a client given the authenticator, any credential,
|
|
421
|
+
# holds tokens that may be another user's, so it holds none of the refresh token, expiration time, scopes,
|
|
422
|
+
# save_tokens, or load_tokens of the client unless it is given them. So does a copy of a client that does not
|
|
423
|
+
# authenticate with OAuth 2.0 that is given a credential or an authenticator.
|
|
424
|
+
#
|
|
425
|
+
# A copy of a client that was given its authenticator shares it, unless the copy is given a credential, which
|
|
426
|
+
# replaces it, or an authenticator of its own, which also replaces the credentials of a client that holds them.
|
|
427
|
+
#
|
|
428
|
+
# A copy that opens its connections as the client does, with the same timeouts, keep-alive timeout, debug output,
|
|
429
|
+
# and proxy, shares the connections the client keeps open, so that a copy made for each request, such as to send
|
|
430
|
+
# a header of its own, opens none of its own; {#close} on either closes them for both, and a later request of
|
|
431
|
+
# either opens them again. A copy given another debug output, though one equal to the client's, as an empty
|
|
432
|
+
# Array is to another, opens connections of its own, which write to its own.
|
|
433
|
+
#
|
|
434
|
+
# @api public
|
|
435
|
+
# @param options [Hash] the options to change, as accepted by initialize
|
|
436
|
+
# @return [Client] a new client with the same credentials and settings, apart from the options given
|
|
437
|
+
# @raise [ArgumentError] if the copy shares the OAuth 2.0 authenticator of the client and is given expires_at or
|
|
438
|
+
# scopes
|
|
439
|
+
# @example Derive an API v1.1 client
|
|
440
|
+
# v1_client = client.with(base_url: "https://api.x.com/1.1/")
|
|
441
|
+
# @example Derive an app-only client from the API key and secret
|
|
442
|
+
# app_client = client.with(access_token: nil, access_token_secret: nil)
|
|
443
|
+
# @example Derive a client that authenticates with another authenticator
|
|
444
|
+
# user_client = app_client.with(authenticator: X::OAuth2Authenticator.new(**stored_tokens))
|
|
445
|
+
def with(**options) = @internals.with(self, options) # steep:ignore DifferentMethodParameterKind
|
|
446
|
+
|
|
447
|
+
# A client that authenticates as the app, for the endpoints that refuse OAuth 1.0a
|
|
448
|
+
#
|
|
449
|
+
# A client that authenticates as a user, signing with OAuth 1.0a or with OAuth 2.0, returns a copy that
|
|
450
|
+
# authenticates with the app's bearer token: the one it was given, or one it fetches with its API key and secret
|
|
451
|
+
# the first time. It returns the same copy, with the connections it keeps open, from then on, since the
|
|
452
|
+
# credentials and settings of a client never change; threads that ask for the copy together get one. A copy of
|
|
453
|
+
# the client made with {#with} that holds the same API key and secret, and the same base URL, builds a copy of
|
|
454
|
+
# its own, but sends the token the client fetched, or fetches, rather than fetch one of its own from the token
|
|
455
|
+
# endpoint, which X limits the rate of. A client with a bearer token or an API key and secret alone already authenticates as the app, and is returned as it is,
|
|
456
|
+
# as is one given an authenticator that authenticates as the app, or as no one. A client given an
|
|
457
|
+
# OAuth1Authenticator fetches the token with the API key and secret it signs with. A client that authenticates
|
|
458
|
+
# with OAuth 2.0 as a user and holds neither the app's bearer token nor its API key and secret, as a client
|
|
459
|
+
# given an OAuth2Authenticator holds neither, raises, rather than send the user's credentials to an endpoint
|
|
460
|
+
# that would refuse them with 403 Forbidden.
|
|
461
|
+
#
|
|
462
|
+
# @api public
|
|
463
|
+
# @return [Client] a copy that authenticates with the bearer token, or the client itself
|
|
464
|
+
# @raise [UnsupportedOperation] if the client authenticates with OAuth 2.0 as a user and holds no credentials of
|
|
465
|
+
# the app
|
|
466
|
+
# @example Add a filtered stream rule, which takes app-only authentication
|
|
467
|
+
# client.app_only.post("tweets/search/stream/rules", {add: [{value: "ruby"}]})
|
|
468
|
+
def app_only = @internals.app_only(self)
|
|
469
|
+
|
|
470
|
+
# Perform a GET request to the X API
|
|
471
|
+
#
|
|
472
|
+
# @api public
|
|
473
|
+
# @param endpoint [String] the endpoint, relative to the base URL with or without a leading slash, with or
|
|
474
|
+
# without a query string
|
|
475
|
+
# @param params [Hash, nil] query parameters appended to the endpoint; nil values are dropped and arrays are joined with commas
|
|
476
|
+
# @param headers [Hash] additional headers for the request
|
|
477
|
+
# @param array_class [Class] the class for parsing JSON arrays
|
|
478
|
+
# @param object_class [Class, #from_response] the class for parsing JSON objects, or one that responds to
|
|
479
|
+
# from_response and builds the result from the whole body; see {Client}
|
|
480
|
+
# @return [Object, nil] the parsed response body, or what an object_class that responds to from_response builds
|
|
481
|
+
# @raise [ArgumentError] if the endpoint is not a String, is not a valid URL, or does not resolve to an http or
|
|
482
|
+
# https URL, before the request is sent
|
|
483
|
+
# @raise [ArgumentError] if array_class is not a Class, or object_class is neither a Class nor responds to
|
|
484
|
+
# from_response, before the request is sent
|
|
485
|
+
# @raise [ArgumentError] if headers are not a Hash of header names to Strings, before the request is sent
|
|
486
|
+
# @raise [ArgumentError] if the client was given no proxy_url and the proxy the environment names for the request
|
|
487
|
+
# cannot be parsed, or is not an http or https URL with a host, with a message that names the variables read for
|
|
488
|
+
# its scheme and leaves out the value
|
|
489
|
+
# @yieldparam response [Response] the summary of each response the request got, as {#on_response} receives it
|
|
490
|
+
# @example Get a user by username
|
|
491
|
+
# client.get("users/by/username/sferik")
|
|
492
|
+
# @example Get users by identifier, requesting only some fields
|
|
493
|
+
# client.get("users", params: {ids: [1, 2], "user.fields": %w[id username]})
|
|
494
|
+
# @example Read what a response reported of the rate limit it spent
|
|
495
|
+
# user = client.get("users/me") { |response| limit = response.rate_limit }
|
|
496
|
+
def get(endpoint, params: nil, headers: {}, array_class: default_array_class, object_class: default_object_class, &)
|
|
497
|
+
@internals.execute_request(self, :get, endpoint, params:, headers:, array_class:, object_class:, &)
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
# Perform a POST request to the X API
|
|
501
|
+
#
|
|
502
|
+
# @api public
|
|
503
|
+
# @param endpoint [String] the endpoint, relative to the base URL with or without a leading slash, with or
|
|
504
|
+
# without a query string
|
|
505
|
+
# @param body [String, Hash, Array, nil] the request body; a String is sent as given, and a Hash or an Array
|
|
506
|
+
# is encoded as JSON
|
|
507
|
+
# @param params [Hash, nil] query parameters appended to the endpoint
|
|
508
|
+
# @param form [Hash, nil] fields to send as a form-encoded body, in place of a body; as with params, nil values
|
|
509
|
+
# are dropped and arrays are joined with commas
|
|
510
|
+
# @param headers [Hash] additional headers for the request
|
|
511
|
+
# @param array_class [Class] the class for parsing JSON arrays
|
|
512
|
+
# @param object_class [Class, #from_response] the class for parsing JSON objects, or one that responds to
|
|
513
|
+
# from_response and builds the result from the whole body; see {Client}
|
|
514
|
+
# @return [Object, nil] the parsed response body, or what an object_class that responds to from_response builds
|
|
515
|
+
# @raise [ArgumentError] if both a body and form fields are given, or a keyword is given that the method takes
|
|
516
|
+
# none of, as the fields of a body given without the braces of a Hash are, and as body: is, since the body is
|
|
517
|
+
# the second argument, before the request is sent
|
|
518
|
+
# @raise [ArgumentError] if the body is not a String, a Hash, an Array, or nil, as an IO or a Symbol is not,
|
|
519
|
+
# before the request is sent
|
|
520
|
+
# @raise [ArgumentError] if the endpoint is not a String, is not a valid URL, or does not resolve to an http or
|
|
521
|
+
# https URL, before the request is sent
|
|
522
|
+
# @raise [ArgumentError] if array_class is not a Class, or object_class is neither a Class nor responds to
|
|
523
|
+
# from_response, before the request is sent
|
|
524
|
+
# @raise [ArgumentError] if headers are not a Hash of header names to Strings, before the request is sent
|
|
525
|
+
# @raise [ArgumentError] if the client was given no proxy_url and the proxy the environment names for the request
|
|
526
|
+
# cannot be parsed, or is not an http or https URL with a host, with a message that names the variables read for
|
|
527
|
+
# its scheme and leaves out the value
|
|
528
|
+
# @yieldparam response [Response] the summary of each response the request got, as {#on_response} receives it
|
|
529
|
+
# @example Create a post
|
|
530
|
+
# client.post("tweets", {text: "Hello, World!"})
|
|
531
|
+
# @example Post a form to the v1.1 API
|
|
532
|
+
# v1_client.post("account/settings.json", form: {lang: "en"})
|
|
533
|
+
def post(endpoint, body = nil, params: nil, form: nil, headers: {}, array_class: default_array_class, object_class: default_object_class, **unknown, &) # steep:ignore DifferentMethodParameterKind
|
|
534
|
+
SettingValidator.no_unknown_keywords!(:post, endpoint, unknown)
|
|
535
|
+
@internals.execute_request(self, :post, endpoint, body:, params:, form:, headers:, array_class:, object_class:, &)
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
# Perform a PUT request to the X API
|
|
539
|
+
#
|
|
540
|
+
# @api public
|
|
541
|
+
# @param endpoint [String] the endpoint, relative to the base URL with or without a leading slash, with or
|
|
542
|
+
# without a query string
|
|
543
|
+
# @param body [String, Hash, Array, nil] the request body; a String is sent as given, and a Hash or an Array
|
|
544
|
+
# is encoded as JSON
|
|
545
|
+
# @param params [Hash, nil] query parameters appended to the endpoint
|
|
546
|
+
# @param form [Hash, nil] fields to send as a form-encoded body, in place of a body; as with params, nil values
|
|
547
|
+
# are dropped and arrays are joined with commas
|
|
548
|
+
# @param headers [Hash] additional headers for the request
|
|
549
|
+
# @param array_class [Class] the class for parsing JSON arrays
|
|
550
|
+
# @param object_class [Class, #from_response] the class for parsing JSON objects, or one that responds to
|
|
551
|
+
# from_response and builds the result from the whole body; see {Client}
|
|
552
|
+
# @return [Object, nil] the parsed response body, or what an object_class that responds to from_response builds
|
|
553
|
+
# @raise [ArgumentError] if both a body and form fields are given, or a keyword is given that the method takes
|
|
554
|
+
# none of, as the fields of a body given without the braces of a Hash are, and as body: is, since the body is
|
|
555
|
+
# the second argument, before the request is sent
|
|
556
|
+
# @raise [ArgumentError] if the body is not a String, a Hash, an Array, or nil, as an IO or a Symbol is not,
|
|
557
|
+
# before the request is sent
|
|
558
|
+
# @raise [ArgumentError] if the endpoint is not a String, is not a valid URL, or does not resolve to an http or
|
|
559
|
+
# https URL, before the request is sent
|
|
560
|
+
# @raise [ArgumentError] if array_class is not a Class, or object_class is neither a Class nor responds to
|
|
561
|
+
# from_response, before the request is sent
|
|
562
|
+
# @raise [ArgumentError] if headers are not a Hash of header names to Strings, before the request is sent
|
|
563
|
+
# @raise [ArgumentError] if the client was given no proxy_url and the proxy the environment names for the request
|
|
564
|
+
# cannot be parsed, or is not an http or https URL with a host, with a message that names the variables read for
|
|
565
|
+
# its scheme and leaves out the value
|
|
566
|
+
# @yieldparam response [Response] the summary of each response the request got, as {#on_response} receives it
|
|
567
|
+
# @example Update a resource
|
|
568
|
+
# client.put("some/endpoint", {key: "value"})
|
|
569
|
+
def put(endpoint, body = nil, params: nil, form: nil, headers: {}, array_class: default_array_class, object_class: default_object_class, **unknown, &) # steep:ignore DifferentMethodParameterKind
|
|
570
|
+
SettingValidator.no_unknown_keywords!(:put, endpoint, unknown)
|
|
571
|
+
@internals.execute_request(self, :put, endpoint, body:, params:, form:, headers:, array_class:, object_class:, &)
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
# Perform a DELETE request to the X API
|
|
575
|
+
#
|
|
576
|
+
# @api public
|
|
577
|
+
# @param endpoint [String] the endpoint, relative to the base URL with or without a leading slash, with or
|
|
578
|
+
# without a query string
|
|
579
|
+
# @param params [Hash, nil] query parameters appended to the endpoint
|
|
580
|
+
# @param headers [Hash] additional headers for the request
|
|
581
|
+
# @param array_class [Class] the class for parsing JSON arrays
|
|
582
|
+
# @param object_class [Class, #from_response] the class for parsing JSON objects, or one that responds to
|
|
583
|
+
# from_response and builds the result from the whole body; see {Client}
|
|
584
|
+
# @return [Object, nil] the parsed response body, or what an object_class that responds to from_response builds
|
|
585
|
+
# @raise [ArgumentError] if the endpoint is not a String, is not a valid URL, or does not resolve to an http or
|
|
586
|
+
# https URL, before the request is sent
|
|
587
|
+
# @raise [ArgumentError] if array_class is not a Class, or object_class is neither a Class nor responds to
|
|
588
|
+
# from_response, before the request is sent
|
|
589
|
+
# @raise [ArgumentError] if headers are not a Hash of header names to Strings, before the request is sent
|
|
590
|
+
# @raise [ArgumentError] if the client was given no proxy_url and the proxy the environment names for the request
|
|
591
|
+
# cannot be parsed, or is not an http or https URL with a host, with a message that names the variables read for
|
|
592
|
+
# its scheme and leaves out the value
|
|
593
|
+
# @yieldparam response [Response] the summary of each response the request got, as {#on_response} receives it
|
|
594
|
+
# @example Delete a post
|
|
595
|
+
# client.delete("tweets/1234567890")
|
|
596
|
+
def delete(endpoint, params: nil, headers: {}, array_class: default_array_class, object_class: default_object_class, &)
|
|
597
|
+
@internals.execute_request(self, :delete, endpoint, params:, headers:, array_class:, object_class:, &)
|
|
598
|
+
end
|
|
599
|
+
|
|
600
|
+
# Open a GET request whose body the block reads as it arrives, as a stream's is
|
|
601
|
+
#
|
|
602
|
+
# The response is passed to the block as a {StreamResponse} before its body is read, so the block reads it, with
|
|
603
|
+
# its read_body, for as long as it likes; the connection is opened for the request alone, with the client's
|
|
604
|
+
# timeouts and proxy, and closed once the block returns. The request carries the client's credentials and headers as any other does, and
|
|
605
|
+
# a token the API rejects is refreshed, or fetched again, and the request sent once more, as for any other. It is
|
|
606
|
+
# neither retried after a failure nor redirected, and a response that is not successful raises the HTTPError of
|
|
607
|
+
# its status, once on_response is passed it, without reaching the block. Nothing the block reads is passed to
|
|
608
|
+
# on_response, since the body is the block's to read.
|
|
609
|
+
#
|
|
610
|
+
# An error the block raises reaches the caller as it was raised. An error of the socket the body is read from,
|
|
611
|
+
# such as a connection that dropped, or a read that timed out, is raised by the read_body of the response, inside
|
|
612
|
+
# the block, as a NetworkError that names the request, as a connection that fails does, with the error of the
|
|
613
|
+
# socket as its cause, so the block rescues a stream that dropped as X::NetworkError, with no class of the
|
|
614
|
+
# transport, and one it does not rescue reaches the caller as it is, told apart from a block that failed. An
|
|
615
|
+
# error is the socket's when read_body raises it from the socket, whatever its class, so the IOError or
|
|
616
|
+
# Errno::ENOSPC of a file the block writes to, in the block passed to read_body or out of it, is the block's,
|
|
617
|
+
# and raised as it was, as is an error of the socket read from the http_response of the response, the escape
|
|
618
|
+
# hatch, rather than with read_body.
|
|
619
|
+
#
|
|
620
|
+
# The body is read once, and inside the block, since the connection it is read from is closed once the block
|
|
621
|
+
# returns: a second read_body, unless both read the body whole, without a block, and a read_body of a response
|
|
622
|
+
# that outlived the block, raise an Error that says so. It is the caller's, as any other error of the block is,
|
|
623
|
+
# rather than a NetworkError, so with_retries does not open the stream again for it.
|
|
624
|
+
#
|
|
625
|
+
# @api public
|
|
626
|
+
# @param endpoint [String] the endpoint, relative to the base URL with or without a leading slash, with or
|
|
627
|
+
# without a query string
|
|
628
|
+
# @param params [Hash, nil] query parameters appended to the endpoint
|
|
629
|
+
# @param headers [Hash] additional headers for the request
|
|
630
|
+
# @yieldparam response [StreamResponse] the successful response, whose body is not yet read
|
|
631
|
+
# @return [Object] what the block returns
|
|
632
|
+
# @raise [ArgumentError] if no block is given, or the endpoint is not a String, is not a valid URL, or does not
|
|
633
|
+
# resolve to an http or https URL, before the request is sent
|
|
634
|
+
# @raise [ArgumentError] if headers are not a Hash of header names to Strings, before the request is sent
|
|
635
|
+
# @raise [ArgumentError] if the client was given no proxy_url and the proxy the environment names for the request
|
|
636
|
+
# cannot be parsed, or is not an http or https URL with a host, with a message that names the variables read for
|
|
637
|
+
# its scheme and leaves out the value
|
|
638
|
+
# @raise [HTTPError] if the response is not successful
|
|
639
|
+
# @raise [NetworkError] if the request cannot be sent, or its body cannot be read, which read_body raises
|
|
640
|
+
# inside the block
|
|
641
|
+
# @example Print the body of the sample stream as it arrives
|
|
642
|
+
# client.app_only.get_stream("tweets/sample/stream") { |response| response.read_body { |chunk| print chunk } }
|
|
643
|
+
# @example Rescue a stream that dropped inside the block
|
|
644
|
+
# client.app_only.get_stream("tweets/sample/stream") do |response|
|
|
645
|
+
# response.read_body { |chunk| file.write(chunk) }
|
|
646
|
+
# rescue X::NetworkError
|
|
647
|
+
# file.flush
|
|
648
|
+
# end
|
|
649
|
+
# @example Read the connections a stream may open before its rate limit resets, then its body
|
|
650
|
+
# client.app_only.get_stream("tweets/sample/stream") do |response|
|
|
651
|
+
# logger.info("#{response.status}: #{response.rate_limit&.remaining} connections left")
|
|
652
|
+
# response.read_body { |chunk| print chunk }
|
|
653
|
+
# end
|
|
654
|
+
def get_stream(endpoint, params: nil, headers: {}, &block)
|
|
655
|
+
Kernel.raise ArgumentError, "get_stream takes a block, which reads the body of the response" if block.nil?
|
|
656
|
+
|
|
657
|
+
@internals.execute_stream(self, endpoint, params:, headers:, &block)
|
|
658
|
+
end
|
|
659
|
+
|
|
660
|
+
# Close the connections the client keeps open between requests
|
|
661
|
+
#
|
|
662
|
+
# A later request opens a connection again. A client and the copies made of it with {#with} that open their
|
|
663
|
+
# connections as it does share their connections, as the app-only copy of a client that signs with OAuth 1.0a
|
|
664
|
+
# does, so closing one closes them for all of them.
|
|
665
|
+
#
|
|
666
|
+
# @api public
|
|
667
|
+
# @return [void]
|
|
668
|
+
# @example Close the connections before a long pause
|
|
669
|
+
# client.close
|
|
670
|
+
def close = @internals.close
|
|
671
|
+
|
|
672
|
+
# Send a request that is safe to send twice again after a failure
|
|
673
|
+
#
|
|
674
|
+
# The client sends no POST again, since the API may have acted on one whose answer never arrived, and sends no
|
|
675
|
+
# request again after its answer failed to arrive, since the API bills a read it answered whether or not the
|
|
676
|
+
# answer arrived. A request that is safe to send again anyway, such as the chunk of an upload, which names the
|
|
677
|
+
# segment it is appended at and which the API bills nothing for, is sent again with this: after a ServerError, a
|
|
678
|
+
# RequestTimeout, or a NetworkError of any kind, even a timeout, up to max_retries times, after the wait a
|
|
679
|
+
# response asks for, or a backoff that doubles with each retry up to a minute and is cut short at random. A
|
|
680
|
+
# response that asks to be left alone for longer than a minute raises at once. The block must build its request
|
|
681
|
+
# anew each time, so that each attempt is signed afresh, as a request of the client is.
|
|
682
|
+
#
|
|
683
|
+
# Wrap a request the client sends no more than once, such as a POST: the client sends a GET, a PUT, or a DELETE
|
|
684
|
+
# again itself, so one wrapped in this is sent max_retries times more for each time this sends it, nine times in
|
|
685
|
+
# all with the defaults, rather than three.
|
|
686
|
+
#
|
|
687
|
+
# For the gems that extend a client, such as x-uploads, which sends each chunk of an upload with it, so that a
|
|
688
|
+
# later x-core 1.x, which installs beside an earlier x-uploads 1.x, keeps its name and behavior throughout 1.x.
|
|
689
|
+
#
|
|
690
|
+
# @api semipublic
|
|
691
|
+
# @yield sends the request
|
|
692
|
+
# @return [Object] what the block returns
|
|
693
|
+
# @raise [NetworkError] if the request fails once more than the retries allow
|
|
694
|
+
# @raise [ServerError, RequestTimeout] if the API fails to answer once more than the retries allow, or asks for a
|
|
695
|
+
# wait longer than a minute
|
|
696
|
+
# @example Append a chunk of an upload, again after a failure
|
|
697
|
+
# client.with_retries { client.post("media/upload/1/append", body, headers:) }
|
|
698
|
+
def with_retries(&) = @internals.with_retries(&)
|
|
699
|
+
|
|
700
|
+
# The value kept under a key for the authenticator the client holds
|
|
701
|
+
#
|
|
702
|
+
# For the gems that extend a client, such as x-resources, which keeps the identifier of the user its credentials
|
|
703
|
+
# act for with it, so that a later x-core 1.x, which installs beside an earlier x-resources 1.x, keeps its name and
|
|
704
|
+
# behavior throughout 1.x. A value is read only while the client holds the authenticator it was kept with, and a
|
|
705
|
+
# client keeps values though it is frozen, which a copy made with dup or clone shares, and a copy made with
|
|
706
|
+
# {#with} does not.
|
|
707
|
+
#
|
|
708
|
+
# @api semipublic
|
|
709
|
+
# @param key [Symbol] the key, which names the gem that keeps it
|
|
710
|
+
# @return [Object, nil] the value, or nil if none is kept for the authenticator of the client
|
|
711
|
+
# @example Read the identifier of the authenticated user that x-resources kept
|
|
712
|
+
# client.memoized(:x_resources_current_user_id) # => 7505382
|
|
713
|
+
def memoized(key) = @internals.memoized(key)
|
|
714
|
+
|
|
715
|
+
# Keep a value under a key, for the authenticator of the client
|
|
716
|
+
#
|
|
717
|
+
# For the gems that extend a client, and kept throughout 1.x, as {#memoized} is.
|
|
718
|
+
#
|
|
719
|
+
# @api semipublic
|
|
720
|
+
# @param key [Symbol] the key, which names the gem that keeps it
|
|
721
|
+
# @param value [Object] the value
|
|
722
|
+
# @return [Object] the value
|
|
723
|
+
# @example Keep the identifier of the authenticated user
|
|
724
|
+
# client.memoize(:x_resources_current_user_id, 7_505_382) # => 7505382
|
|
725
|
+
def memoize(key, value) = @internals.memoize(key, value)
|
|
726
|
+
end
|
|
727
|
+
end
|
|
728
|
+
end
|