x 0.12.1 → 0.14.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 553e0e0e9a87126211bf62942b02cb2c7c8b6d9380ee8fd4336058038c9735b0
4
- data.tar.gz: 34922d1e52a0c04de3c6fe090e854174d78d437c8e14b061a95ca4ab5e1a0438
3
+ metadata.gz: e7f8185b86c61142faad23edbd1563f5a10263d7f47114d5f31a83e93af805dd
4
+ data.tar.gz: 8df083a42c16683e27d4face3445e3a835663f11600751024a519c3a658ed0ed
5
5
  SHA512:
6
- metadata.gz: e28c53b17bb053c26326d46940eb539c4612d6f4a547b074e974c2b6ddee616bb37af6c2dd2d4d84b05a21aa2946306de074889b90bd411b2a268793e40d72a0
7
- data.tar.gz: 79ab655009e07915cf71158bb2976301d1519093c753b6f8fc1d128781b4844e867551c3d430bb0226aa113d3c7342efdd401e08d4f3d920a7437dfa8c432ba6
6
+ metadata.gz: 17dda35c335587d569dc1d41c8fe3ca2221e8e1d3b1742a443559c7d558b149e92229d7d767e64bf4a12982f4a3b4d2afb002d9d34db819216e1934eaf48391f
7
+ data.tar.gz: 990d9afb8e30cb0c998765525b47c4bf44779f96f4fbd006903fc9b3fe655d0f50df8aa197c77d9ad05250ded0fc086bb768d4662d539490ab81804d2e86b5f0
data/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ ## [0.14.0] - 2023-12-08
2
+ * Allow passing custom objects per-request (768889f)
3
+
4
+ ## [0.13.0] - 2023-12-04
5
+ * Introduce X::RateLimit, which is returned with X::TooManyRequests errors (196caec)
6
+
1
7
  ## [0.12.1] - 2023-11-28
2
8
  * Ensure split chunks are written as binary (c6e257f)
3
9
  * Require tmpdir in X::MediaUploader (9e7c7f1)
data/README.md CHANGED
@@ -1,7 +1,7 @@
1
- ![Tests](https://github.com/sferik/x-ruby/actions/workflows/test.yml/badge.svg)
2
- ![Linter](https://github.com/sferik/x-ruby/actions/workflows/lint.yml/badge.svg)
3
- ![Mutant](https://github.com/sferik/x-ruby/actions/workflows/mutant.yml/badge.svg)
4
- ![Typer Checker](https://github.com/sferik/x-ruby/actions/workflows/type_check.yml/badge.svg)
1
+ [![Tests](https://github.com/sferik/x-ruby/actions/workflows/test.yml/badge.svg)](https://github.com/sferik/x-ruby/actions/workflows/test.yml)
2
+ [![Linter](https://github.com/sferik/x-ruby/actions/workflows/lint.yml/badge.svg)](https://github.com/sferik/x-ruby/actions/workflows/lint.yml)
3
+ [![Mutant](https://github.com/sferik/x-ruby/actions/workflows/mutant.yml/badge.svg)](https://github.com/sferik/x-ruby/actions/workflows/mutant.yml)
4
+ [![Typer Checker](https://github.com/sferik/x-ruby/actions/workflows/steep.yml/badge.svg)](https://github.com/sferik/x-ruby/actions/workflows/steep.yml)
5
5
  [![Gem Version](https://badge.fury.io/rb/x.svg)](https://rubygems.org/gems/x)
6
6
 
7
7
  # A [Ruby](https://www.ruby-lang.org) interface to the [X API](https://developer.x.com)
@@ -52,10 +52,17 @@ x_client.delete("tweets/#{post["data"]["id"]}")
52
52
  # Initialize an API v1.1 client
53
53
  v1_client = X::Client.new(base_url: "https://api.twitter.com/1.1/", **x_credentials)
54
54
 
55
- # Get your account settings
56
- v1_client.get("account/settings.json")
55
+ # Define a custom response object
56
+ Language = Struct.new(:code, :name, :local_name, :status, :debug)
57
57
 
58
- # Initialize an X Ads API client
58
+ # Parse a response with custom array and object classes
59
+ languages = v1_client.get("help/languages.json", object_class: Language, array_class: Set)
60
+ # #<Set: {#<struct Language code="ur", name="Urdu", local_name="اردو", status="beta", debug=false>, …
61
+
62
+ # Access data with dots instead of brackets
63
+ languages.first.local_name
64
+
65
+ # Initialize an Ads API client
59
66
  ads_client = X::Client.new(base_url: "https://ads-api.twitter.com/12/", **x_credentials)
60
67
 
61
68
  # Get your ad accounts
@@ -80,6 +87,23 @@ The tests for the previous version of this library executed in about 2 seconds.
80
87
 
81
88
  This code is not littered with comments that are intended to generate documentation. Rather, this code is intended to be simple enough to serve as its own documentation. If you want to understand how something works, don’t read the documentation—it might be wrong—read the code. The code is always right.
82
89
 
90
+ ## Features
91
+
92
+ If this entire library is implemented in just 500 lines of code, why should you use it at all vs. writing your own library that suits your needs? If you feel inspired to do that, don’t let me discourage you, but this library has some advanced features that may not be apparent without diving into the code:
93
+
94
+ * OAuth 1.0 Revision A
95
+ * OAuth 2.0 Bearer Token
96
+ * Thread safety
97
+ * HTTP redirect following
98
+ * HTTP proxy support
99
+ * HTTP logging
100
+ * HTTP timeout configuration
101
+ * HTTP error handling
102
+ * Rate limit handling
103
+ * Parsing JSON into custom response objects (e.g. OpenStruct)
104
+ * Configurable base URLs for accessing different APIs/versions
105
+ * Parallel uploading of large media files in chunks
106
+
83
107
  ## Sponsorship
84
108
 
85
109
  The X gem is free to use, but with X API pricing tiers, it actually costs money to develop and maintain. By contributing to the project, you help us:
@@ -132,19 +156,23 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/sferik
132
156
 
133
157
  Pull requests will only be accepted if they meet all the following criteria:
134
158
 
135
- 1. Code must conform to [Standard Ruby](https://github.com/standardrb/standard). This can be verified with:
159
+ 1. Code must conform to [Standard Ruby](https://github.com/standardrb/standard#readme). This can be verified with:
136
160
 
137
161
  bundle exec rake standard
138
162
 
139
- 2. 100% C0 code coverage. This can be verified with:
163
+ 2. Code must conform to the [RuboCop rules](https://github.com/rubocop/rubocop#readme). This can be verified with:
164
+
165
+ bundle exec rake rubocop
166
+
167
+ 3. 100% C0 code coverage. This can be verified with:
140
168
 
141
169
  bundle exec rake test
142
170
 
143
- 3. 100% mutation coverage. This can be verified with:
171
+ 4. 100% mutation coverage. This can be verified with:
144
172
 
145
173
  bundle exec rake mutant
146
174
 
147
- 4. RBS type signatures (in `sig/x.rbs`). This can be verified with:
175
+ 5. RBS type signatures (in `sig/x.rbs`). This can be verified with:
148
176
 
149
177
  bundle exec rake steep
150
178
 
data/lib/x/client.rb CHANGED
@@ -11,16 +11,16 @@ module X
11
11
  extend Forwardable
12
12
 
13
13
  DEFAULT_BASE_URL = "https://api.twitter.com/2/".freeze
14
+ DEFAULT_ARRAY_CLASS = Array
15
+ DEFAULT_OBJECT_CLASS = Hash
14
16
 
15
- attr_accessor :base_url
17
+ attr_accessor :base_url, :default_array_class, :default_object_class
16
18
  attr_reader :api_key, :api_key_secret, :access_token, :access_token_secret, :bearer_token
17
19
 
18
20
  def_delegators :@connection, :open_timeout, :read_timeout, :write_timeout, :proxy_url, :debug_output
19
21
  def_delegators :@connection, :open_timeout=, :read_timeout=, :write_timeout=, :proxy_url=, :debug_output=
20
22
  def_delegators :@redirect_handler, :max_redirects
21
23
  def_delegators :@redirect_handler, :max_redirects=
22
- def_delegators :@response_parser, :array_class, :object_class
23
- def_delegators :@response_parser, :array_class=, :object_class=
24
24
 
25
25
  def initialize(api_key: nil, api_key_secret: nil, access_token: nil, access_token_secret: nil,
26
26
  bearer_token: nil,
@@ -30,36 +30,38 @@ module X
30
30
  write_timeout: Connection::DEFAULT_WRITE_TIMEOUT,
31
31
  debug_output: Connection::DEFAULT_DEBUG_OUTPUT,
32
32
  proxy_url: nil,
33
- array_class: nil,
34
- object_class: nil,
33
+ default_array_class: DEFAULT_ARRAY_CLASS,
34
+ default_object_class: DEFAULT_OBJECT_CLASS,
35
35
  max_redirects: RedirectHandler::DEFAULT_MAX_REDIRECTS)
36
36
 
37
- initialize_oauth(api_key, api_key_secret, access_token, access_token_secret)
38
- @bearer_token = bearer_token
37
+ initialize_oauth(api_key, api_key_secret, access_token, access_token_secret, bearer_token)
39
38
  initialize_authenticator
40
39
  @base_url = base_url
40
+ initialize_default_classes(default_array_class, default_object_class)
41
41
  @connection = Connection.new(open_timeout: open_timeout, read_timeout: read_timeout,
42
42
  write_timeout: write_timeout, debug_output: debug_output, proxy_url: proxy_url)
43
43
  @request_builder = RequestBuilder.new
44
44
  @redirect_handler = RedirectHandler.new(connection: @connection, request_builder: @request_builder,
45
45
  max_redirects: max_redirects)
46
- @response_parser = ResponseParser.new(array_class: array_class, object_class: object_class)
46
+ @response_parser = ResponseParser.new
47
47
  end
48
48
 
49
- def get(endpoint, headers: {})
50
- execute_request(:get, endpoint, headers: headers)
49
+ def get(endpoint, headers: {}, array_class: default_array_class, object_class: default_object_class)
50
+ execute_request(:get, endpoint, headers: headers, array_class: array_class, object_class: object_class)
51
51
  end
52
52
 
53
- def post(endpoint, body = nil, headers: {})
54
- execute_request(:post, endpoint, body: body, headers: headers)
53
+ def post(endpoint, body = nil, headers: {}, array_class: default_array_class, object_class: default_object_class)
54
+ execute_request(:post, endpoint, body: body, headers: headers, array_class: array_class,
55
+ object_class: object_class)
55
56
  end
56
57
 
57
- def put(endpoint, body = nil, headers: {})
58
- execute_request(:put, endpoint, body: body, headers: headers)
58
+ def put(endpoint, body = nil, headers: {}, array_class: default_array_class, object_class: default_object_class)
59
+ execute_request(:put, endpoint, body: body, headers: headers, array_class: array_class,
60
+ object_class: object_class)
59
61
  end
60
62
 
61
- def delete(endpoint, headers: {})
62
- execute_request(:delete, endpoint, headers: headers)
63
+ def delete(endpoint, headers: {}, array_class: default_array_class, object_class: default_object_class)
64
+ execute_request(:delete, endpoint, headers: headers, array_class: array_class, object_class: object_class)
63
65
  end
64
66
 
65
67
  def api_key=(api_key)
@@ -89,11 +91,17 @@ module X
89
91
 
90
92
  private
91
93
 
92
- def initialize_oauth(api_key, api_key_secret, access_token, access_token_secret)
94
+ def initialize_oauth(api_key, api_key_secret, access_token, access_token_secret, bearer_token)
93
95
  @api_key = api_key
94
96
  @api_key_secret = api_key_secret
95
97
  @access_token = access_token
96
98
  @access_token_secret = access_token_secret
99
+ @bearer_token = bearer_token
100
+ end
101
+
102
+ def initialize_default_classes(default_array_class, default_object_class)
103
+ @default_array_class = default_array_class
104
+ @default_object_class = default_object_class
97
105
  end
98
106
 
99
107
  def initialize_authenticator
@@ -109,14 +117,14 @@ module X
109
117
  end
110
118
  end
111
119
 
112
- def execute_request(http_method, endpoint, headers:, body: nil)
120
+ def execute_request(http_method, endpoint, body: nil, headers: {}, array_class: default_array_class, object_class: default_object_class)
113
121
  uri = URI.join(base_url, endpoint)
114
122
  request = @request_builder.build(http_method: http_method, uri: uri, body: body, headers: headers,
115
123
  authenticator: @authenticator)
116
124
  response = @connection.perform(request: request)
117
125
  response = @redirect_handler.handle(response: response, request: request, base_url: base_url,
118
126
  authenticator: @authenticator)
119
- @response_parser.parse(response: response)
127
+ @response_parser.parse(response: response, array_class: array_class, object_class: object_class)
120
128
  end
121
129
  end
122
130
  end
@@ -1,17 +1,20 @@
1
1
  require_relative "client_error"
2
+ require_relative "../rate_limit"
2
3
 
3
4
  module X
4
5
  class TooManyRequests < ClientError
5
- def limit
6
- response["x-rate-limit-limit"].to_i
6
+ def rate_limit
7
+ rate_limits.max_by(&:reset_at)
7
8
  end
8
9
 
9
- def remaining
10
- response["x-rate-limit-remaining"].to_i
10
+ def rate_limits
11
+ @rate_limits ||= RateLimit::TYPES.filter_map do |type|
12
+ RateLimit.new(type: type, response: response) if response["x-#{type}-remaining"].eql?("0")
13
+ end
11
14
  end
12
15
 
13
16
  def reset_at
14
- Time.at(response["x-rate-limit-reset"].to_i)
17
+ rate_limit&.reset_at || Time.at(0)
15
18
  end
16
19
 
17
20
  def reset_in
@@ -0,0 +1,33 @@
1
+ module X
2
+ class RateLimit
3
+ RATE_LIMIT_TYPE = "rate-limit".freeze
4
+ APP_LIMIT_TYPE = "app-limit-24hour".freeze
5
+ USER_LIMIT_TYPE = "user-limit-24hour".freeze
6
+ TYPES = [RATE_LIMIT_TYPE, APP_LIMIT_TYPE, USER_LIMIT_TYPE].freeze
7
+
8
+ attr_accessor :type, :response
9
+
10
+ def initialize(type:, response:)
11
+ @type = type
12
+ @response = response
13
+ end
14
+
15
+ def limit
16
+ Integer(response.fetch("x-#{type}-limit"))
17
+ end
18
+
19
+ def remaining
20
+ Integer(response.fetch("x-#{type}-remaining"))
21
+ end
22
+
23
+ def reset_at
24
+ Time.at(Integer(response.fetch("x-#{type}-reset")))
25
+ end
26
+
27
+ def reset_in
28
+ [(reset_at - Time.now).ceil, 0].max
29
+ end
30
+
31
+ alias_method :retry_after, :reset_in
32
+ end
33
+ end
@@ -36,14 +36,7 @@ module X
36
36
  }.freeze
37
37
  JSON_CONTENT_TYPE_REGEXP = %r{application/json}
38
38
 
39
- attr_accessor :array_class, :object_class
40
-
41
- def initialize(array_class: nil, object_class: nil)
42
- @array_class = array_class
43
- @object_class = object_class
44
- end
45
-
46
- def parse(response:)
39
+ def parse(response:, array_class: nil, object_class: nil)
47
40
  raise error(response) unless response.is_a?(Net::HTTPSuccess)
48
41
 
49
42
  return unless json?(response)
data/lib/x/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  require "rubygems/version"
2
2
 
3
3
  module X
4
- VERSION = Gem::Version.create("0.12.1")
4
+ VERSION = Gem::Version.create("0.14.0")
5
5
  end
data/sig/x.rbs CHANGED
@@ -102,10 +102,10 @@ module X
102
102
  end
103
103
 
104
104
  class TooManyRequests < ClientError
105
- @response: Net::HTTPResponse
105
+ @rate_limits: Array[RateLimit]
106
106
 
107
- def limit: -> Integer
108
- def remaining: -> Integer
107
+ def rate_limit: -> RateLimit?
108
+ def rate_limits: -> Array[RateLimit]
109
109
  def reset_at: -> Time
110
110
  def reset_in: -> Integer?
111
111
  end
@@ -147,6 +147,21 @@ module X
147
147
  def configure_http_client: (Net::HTTP http_client) -> Net::HTTP
148
148
  end
149
149
 
150
+ class RateLimit
151
+ RATE_LIMIT_TYPE: String
152
+ APP_LIMIT_TYPE: String
153
+ USER_LIMIT_TYPE: String
154
+ TYPES: [String, String, String]
155
+
156
+ attr_accessor type: String
157
+ attr_accessor response: Net::HTTPResponse
158
+ def initialize: (type: String, response: Net::HTTPResponse) -> void
159
+ def limit: -> Integer
160
+ def remaining: -> Integer
161
+ def reset_at: -> Time
162
+ def reset_in: -> Integer?
163
+ end
164
+
150
165
  class RequestBuilder
151
166
  HTTP_METHODS: Hash[Symbol, (singleton(Net::HTTP::Get) | singleton(Net::HTTP::Post) | singleton(Net::HTTP::Put) | singleton(Net::HTTP::Delete))]
152
167
  DEFAULT_HEADERS: Hash[String, String]
@@ -178,15 +193,10 @@ module X
178
193
  end
179
194
 
180
195
  class ResponseParser
181
- DEFAULT_ARRAY_CLASS: Class
182
- DEFAULT_OBJECT_CLASS: Class
183
196
  ERROR_MAP: Hash[Integer, singleton(Unauthorized) | singleton(BadRequest) | singleton(Forbidden) | singleton(InternalServerError) | singleton(NotFound) | singleton(PayloadTooLarge) | singleton(ServiceUnavailable) | singleton(TooManyRequests)]
184
197
  JSON_CONTENT_TYPE_REGEXP: Regexp
185
198
 
186
- attr_accessor array_class: Class
187
- attr_accessor object_class: Class
188
- def initialize: (?array_class: Class, ?object_class: Class) -> void
189
- def parse: (response: Net::HTTPResponse) -> untyped
199
+ def parse: (response: Net::HTTPResponse, ?array_class: Class?, ?object_class: Class?) -> untyped
190
200
 
191
201
  private
192
202
  def error: (Net::HTTPResponse response) -> (Unauthorized | BadRequest | Forbidden | InternalServerError | NotFound | PayloadTooLarge | ServiceUnavailable | TooManyRequests)
@@ -196,6 +206,8 @@ module X
196
206
 
197
207
  class Client
198
208
  DEFAULT_BASE_URL: String
209
+ DEFAULT_ARRAY_CLASS: Class
210
+ DEFAULT_OBJECT_CLASS: Class
199
211
 
200
212
  extend Forwardable
201
213
  @authenticator: Authenticator
@@ -215,8 +227,8 @@ module X
215
227
  attr_accessor write_timeout: Float | Integer
216
228
  attr_accessor proxy_url: String
217
229
  attr_accessor debug_output: IO
218
- attr_accessor array_class: Class
219
- attr_accessor object_class: Class
230
+ attr_accessor default_array_class: Class
231
+ attr_accessor default_object_class: Class
220
232
  attr_accessor max_redirects: Integer
221
233
  attr_accessor authenticator: Authenticator
222
234
  attr_accessor connection: Connection
@@ -224,16 +236,17 @@ module X
224
236
  attr_accessor redirect_handler: RedirectHandler
225
237
  attr_accessor response_parser: ResponseParser
226
238
 
227
- def initialize: (?api_key: String?, ?api_key_secret: String?, ?access_token: String?, ?access_token_secret: String?, ?bearer_token: String?, ?base_url: String, ?open_timeout: Float | Integer, ?read_timeout: Float | Integer, ?write_timeout: Float | Integer, ?proxy_url: URI::Generic? | String?, ?debug_output: IO, ?array_class: Class, ?object_class: Class, ?max_redirects: Integer) -> void
228
- def get: (String endpoint, ?headers: Hash[String, String]) -> untyped
229
- def post: (String endpoint, ?String? body, ?headers: Hash[String, String]) -> untyped
230
- def put: (String endpoint, ?String? body, ?headers: Hash[String, String]) -> untyped
231
- def delete: (String endpoint, ?headers: Hash[String, String]) -> untyped
239
+ def initialize: (?api_key: String?, ?api_key_secret: String?, ?access_token: String?, ?access_token_secret: String?, ?bearer_token: String?, ?base_url: String, ?open_timeout: Float | Integer, ?read_timeout: Float | Integer, ?write_timeout: Float | Integer, ?proxy_url: URI::Generic? | String?, ?debug_output: IO, ?default_array_class: Class, ?default_object_class: Class, ?max_redirects: Integer) -> void
240
+ def get: (String endpoint, ?headers: Hash[String, String], ?array_class: Class, ?object_class: Class) -> untyped
241
+ def post: (String endpoint, ?String? body, ?headers: Hash[String, String], ?array_class: Class, ?object_class: Class) -> untyped
242
+ def put: (String endpoint, ?String? body, ?headers: Hash[String, String], ?array_class: Class, ?object_class: Class) -> untyped
243
+ def delete: (String endpoint, ?headers: Hash[String, String], ?array_class: Class, ?object_class: Class) -> untyped
232
244
 
233
245
  private
234
- def initialize_oauth: (String? api_key, String? api_key_secret, String? access_token, String? access_token_secret) -> void
246
+ def initialize_oauth: (String? api_key, String? api_key_secret, String? access_token, String? access_token_secret, String? bearer_token) -> void
247
+ def initialize_default_classes: (Class? default_array_class, Class? default_object_class) -> void
235
248
  def initialize_authenticator: -> Authenticator
236
- def execute_request: (Symbol http_method, String endpoint, headers: Hash[String, String], ?body: String?) -> untyped
249
+ def execute_request: (Symbol http_method, String endpoint, ?body: String?, ?headers: Hash[String, String], ?array_class: Class?, ?object_class: Class?) -> untyped
237
250
  end
238
251
 
239
252
  module MediaUploader
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: x
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.12.1
4
+ version: 0.14.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Erik Berlin
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-11-28 00:00:00.000000000 Z
11
+ date: 2023-12-08 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email:
@@ -50,6 +50,7 @@ files:
50
50
  - lib/x/errors/unprocessable_entity.rb
51
51
  - lib/x/media_uploader.rb
52
52
  - lib/x/oauth_authenticator.rb
53
+ - lib/x/rate_limit.rb
53
54
  - lib/x/redirect_handler.rb
54
55
  - lib/x/request_builder.rb
55
56
  - lib/x/response_parser.rb