x 0.12.0 → 0.13.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: 5c6252a6765246fe6ca0820e5ce96f9e66d9611e91b2d59753730fad6b87f67e
4
- data.tar.gz: 7a7dc55ece60d848cde08c151e887a1cc86808408a864c05934c4be906b2021a
3
+ metadata.gz: 4f4970075e9256b31a7f53ca6a3a06df7559631b9104c2069372b3797845cd77
4
+ data.tar.gz: a0e25d20e521f8a3408b9e04b8826d7f8959257ecf6748687a0d1b0292719f38
5
5
  SHA512:
6
- metadata.gz: 4216ad5a466dd1a714a638a45d9c0996f40c16247f7fc6eb0ff756d301c3cd63aac5ca95fd421940634e3661db5cb41e19300b73f8ef01056c14e21ad89edea1
7
- data.tar.gz: a27036e7dd594f9b13b5cd66267c57b63679352ea3eeeaada13adbba838c6cdad5d8f841fe35b808950fc610808c219679e72334091b3d8de1dca5899ba386b5
6
+ metadata.gz: 7be87f0b82af146429afa88288f60d53d07185d2ed47f57d534c729758a3bfc426c4a57e11703665eedd19b9d994ce4e51bd4f08e3de84cdc49ca7a2de28eae6
7
+ data.tar.gz: 3e28cfc6ee5d244eff7671e0f724c0a07071d1872792d9fb742ee44bcf2fc2f1a797053dc0d2a415a51a625d85629115f71ad003bb3b9fcaf75049cde6b5d0d1
data/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ ## [0.13.0] - 2023-12-04
2
+ * Introduce X::RateLimit, which is returned with X::TooManyRequests errors (196caec)
3
+
4
+ ## [0.12.1] - 2023-11-28
5
+ * Ensure split chunks are written as binary (c6e257f)
6
+ * Require tmpdir in X::MediaUploader (9e7c7f1)
7
+
1
8
  ## [0.12.0] - 2023-11-02
2
9
  * Ensure Authenticator is passed to RedirectHandler (fc8557b)
3
10
  * Add AUTHENTICATION_HEADER to X::Authenticator base class (85a2818)
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
+ [![RuboCop](https://github.com/sferik/x-ruby/actions/workflows/rubocop.yml/badge.svg)](https://github.com/sferik/x-ruby/actions/workflows/rubocop.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)
@@ -41,24 +41,24 @@ x_client = X::Client.new(**x_credentials)
41
41
  x_client.get("users/me")
42
42
  # {"data"=>{"id"=>"7505382", "name"=>"Erik Berlin", "username"=>"sferik"}}
43
43
 
44
- # Post a tweet
45
- tweet = x_client.post("tweets", '{"text":"Hello, World! (from @gem)"}')
44
+ # Post
45
+ post = x_client.post("tweets", '{"text":"Hello, World! (from @gem)"}')
46
46
  # {"data"=>{"edit_history_tweet_ids"=>["1234567890123456789"], "id"=>"1234567890123456789", "text"=>"Hello, World! (from @gem)"}}
47
47
 
48
- # Delete the tweet you just posted
49
- x_client.delete("tweets/#{tweet["data"]["id"]}")
48
+ # Delete the post
49
+ x_client.delete("tweets/#{post["data"]["id"]}")
50
50
  # {"data"=>{"deleted"=>true}}
51
51
 
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
- # Request your account settings
55
+ # Get your account settings
56
56
  v1_client.get("account/settings.json")
57
57
 
58
- # Initialize an X Ads API client
58
+ # Initialize an Ads API client
59
59
  ads_client = X::Client.new(base_url: "https://ads-api.twitter.com/12/", **x_credentials)
60
60
 
61
- # Request your ad accounts
61
+ # Get your ad accounts
62
62
  ads_client.get("accounts")
63
63
  ```
64
64
 
@@ -66,7 +66,7 @@ See other common usage [examples](https://github.com/sferik/x-ruby/tree/main/exa
66
66
 
67
67
  ## History and Philosophy
68
68
 
69
- This library is a rewrite of the [Twitter Ruby library](https://github.com/sferik/twitter). Over 16 years of development, that library ballooned to over 3,000 lines of code (plus 7,500 lines of tests). At the time of writing, this library is about 300 lines of code (plus 200 test lines) and I’d like to keep it that way. That doesn’t mean new features won’t be added over time, but the benefits of more code must be weighted against the benefits of less:
69
+ This library is a rewrite of the [Twitter Ruby library](https://github.com/sferik/twitter). Over 16 years of development, that library ballooned to over 3,000 lines of code (plus 7,500 lines of tests), not counting dependencies. This library is about 500 lines of code (plus 1000 test lines) and has no runtime dependencies. That doesn’t mean new features won’t be added over time, but the benefits of more code must be weighed against the benefits of less:
70
70
 
71
71
  * Less code is easier to maintain.
72
72
  * Less code means fewer bugs.
@@ -76,12 +76,27 @@ In the immortal words of [Ezra Zygmuntowicz](https://github.com/ezmobius) and hi
76
76
 
77
77
  > No code is faster than no code.
78
78
 
79
- The fastest code is the code that is never executed because it doesn’t exist. That principle should apply not just to this library itself but to third-party dependencies. At present, this library has no runtime dependencies and I’d like to keep it that way.
80
-
81
- The tests for the previous version of this library executed in about 2 seconds. That sounds pretty fast until you see that tests for this library run in 2 hundredths of a second. This means you can automatically run the tests any time you write a file and receive immediate feedback. For such of workflows, 2 seconds feels painfully slow.
79
+ The tests for the previous version of this library executed in about 2 seconds. That sounds pretty fast until you see that tests for this library run in one-twentieth of a second. This means you can automatically run the tests any time you write a file and receive immediate feedback. For such of workflows, 2 seconds feels painfully slow.
82
80
 
83
81
  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.
84
82
 
83
+ ## Features
84
+
85
+ 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:
86
+
87
+ * OAuth 1.0 Revision A
88
+ * OAuth 2.0 Bearer Token
89
+ * Thread safety
90
+ * HTTP redirect following
91
+ * HTTP proxy support
92
+ * HTTP logging
93
+ * HTTP timeout configuration
94
+ * HTTP error handling
95
+ * Rate limit handling
96
+ * Parsing JSON into custom response objects (e.g. OpenStruct)
97
+ * Configurable base URLs for accessing different APIs/versions
98
+ * Parallel uploading of large media files in chunks
99
+
85
100
  ## Sponsorship
86
101
 
87
102
  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:
@@ -103,6 +118,8 @@ Many thanks to our sponsors (listed in order of when they sponsored this project
103
118
  <a href="https://betterstack.com"><img src="https://raw.githubusercontent.com/sferik/x-ruby/main/sponsor_logos/better_stack.svg" alt="Better Stack" width="200" align="middle"></a>
104
119
  <img src="https://raw.githubusercontent.com/sferik/x-ruby/main/sponsor_logos/spacer.png" width="20" align="middle">
105
120
  <a href="https://sentry.io"><img src="https://raw.githubusercontent.com/sferik/x-ruby/main/sponsor_logos/sentry.svg" alt="Sentry" width="200" align="middle"></a>
121
+ <img src="https://raw.githubusercontent.com/sferik/x-ruby/main/sponsor_logos/spacer.png" width="20" align="middle">
122
+ <a href="https://ifttt.com"><img src="https://raw.githubusercontent.com/sferik/x-ruby/main/sponsor_logos/ifttt.svg" alt="IFTTT" width="200" align="middle"></a>
106
123
 
107
124
  ## Development
108
125
 
@@ -132,9 +149,9 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/sferik
132
149
 
133
150
  Pull requests will only be accepted if they meet all the following criteria:
134
151
 
135
- 1. Code must conform to [Standard Ruby](https://github.com/standardrb/standard). This can be verified with:
152
+ 1. Code must conform to [RuboCop rules](https://github.com/rubocop/rubocop). This can be verified with:
136
153
 
137
- bundle exec rake standard
154
+ bundle exec rubocop
138
155
 
139
156
  2. 100% C0 code coverage. This can be verified with:
140
157
 
@@ -142,6 +159,7 @@ Pull requests will only be accepted if they meet all the following criteria:
142
159
 
143
160
  3. 100% mutation coverage. This can be verified with:
144
161
 
162
+ git remote add upstream https://github.com/sferik/x-ruby
145
163
  bundle exec rake mutant
146
164
 
147
165
  4. RBS type signatures (in `sig/x.rbs`). This can be verified with:
@@ -1,5 +1,4 @@
1
1
  module X
2
- # Base Authenticator class
3
2
  class Authenticator
4
3
  AUTHENTICATION_HEADER = "Authorization".freeze
5
4
 
@@ -1,7 +1,6 @@
1
1
  require_relative "authenticator"
2
2
 
3
3
  module X
4
- # Handles bearer token authentication
5
4
  class BearerTokenAuthenticator < Authenticator
6
5
  attr_accessor :bearer_token
7
6
 
data/lib/x/cgi.rb CHANGED
@@ -1,7 +1,6 @@
1
1
  require "cgi"
2
2
 
3
3
  module X
4
- # Namespaced CGI class
5
4
  class CGI
6
5
  # TODO: Replace CGI.escape with CGI.escapeURIComponent when support for Ruby 3.1 is dropped
7
6
  def self.escape(value)
data/lib/x/client.rb CHANGED
@@ -7,7 +7,6 @@ require_relative "request_builder"
7
7
  require_relative "response_parser"
8
8
 
9
9
  module X
10
- # Main public interface
11
10
  class Client
12
11
  extend Forwardable
13
12
 
data/lib/x/connection.rb CHANGED
@@ -5,7 +5,6 @@ require "uri"
5
5
  require_relative "errors/network_error"
6
6
 
7
7
  module X
8
- # Sends HTTP requests
9
8
  class Connection
10
9
  extend Forwardable
11
10
 
@@ -2,7 +2,6 @@ require "json"
2
2
  require_relative "error"
3
3
 
4
4
  module X
5
- # Base HTTP error class
6
5
  class HTTPError < Error
7
6
  JSON_CONTENT_TYPE_REGEXP = %r{application/(problem\+|)json}
8
7
 
@@ -1,18 +1,20 @@
1
1
  require_relative "client_error"
2
+ require_relative "../rate_limit"
2
3
 
3
4
  module X
4
- # Rate limit error class
5
5
  class TooManyRequests < ClientError
6
- def limit
7
- response["x-rate-limit-limit"].to_i
6
+ def rate_limit
7
+ rate_limits.max_by(&:reset_at)
8
8
  end
9
9
 
10
- def remaining
11
- 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
12
14
  end
13
15
 
14
16
  def reset_at
15
- Time.at(response["x-rate-limit-reset"].to_i)
17
+ rate_limit&.reset_at || Time.at(0)
16
18
  end
17
19
 
18
20
  def reset_in
@@ -1,7 +1,7 @@
1
1
  require "securerandom"
2
+ require "tmpdir"
2
3
 
3
4
  module X
4
- # Helper module for uploading images and videos
5
5
  module MediaUploader
6
6
  extend self
7
7
 
@@ -72,7 +72,7 @@ module X
72
72
  File.open(file_path, "rb") do |f|
73
73
  while (chunk = f.read(chunk_size))
74
74
  file_paths << "#{Dir.mktmpdir}/x#{format("%03d", file_number += 1)}".tap do |path|
75
- File.write(path, chunk)
75
+ File.binwrite(path, chunk)
76
76
  end
77
77
  end
78
78
  end
@@ -7,7 +7,6 @@ require_relative "authenticator"
7
7
  require_relative "cgi"
8
8
 
9
9
  module X
10
- # Handles OAuth authentication
11
10
  class OAuthAuthenticator < Authenticator
12
11
  OAUTH_VERSION = "1.0".freeze
13
12
  OAUTH_SIGNATURE_METHOD = "HMAC-SHA1".freeze
@@ -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
@@ -6,7 +6,6 @@ require_relative "errors/too_many_redirects"
6
6
  require_relative "request_builder"
7
7
 
8
8
  module X
9
- # Handles HTTP redirects
10
9
  class RedirectHandler
11
10
  DEFAULT_MAX_REDIRECTS = 10
12
11
 
@@ -5,7 +5,6 @@ require_relative "cgi"
5
5
  require_relative "version"
6
6
 
7
7
  module X
8
- # Creates HTTP requests
9
8
  class RequestBuilder
10
9
  DEFAULT_HEADERS = {
11
10
  "Content-Type" => "application/json; charset=utf-8",
@@ -17,7 +17,6 @@ require_relative "errors/unauthorized"
17
17
  require_relative "errors/unprocessable_entity"
18
18
 
19
19
  module X
20
- # Process HTTP responses
21
20
  class ResponseParser
22
21
  ERROR_MAP = {
23
22
  400 => BadRequest,
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.0")
4
+ VERSION = Gem::Version.create("0.13.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]
@@ -279,7 +294,3 @@ module X
279
294
  def self.escape_params: (Hash[String, String] | Array[[String, String]] params) -> String
280
295
  end
281
296
  end
282
-
283
- class Dir
284
- def self.mktmpdir: (?String? prefix_suffix) -> String
285
- end
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.0
4
+ version: 0.13.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-02 00:00:00.000000000 Z
11
+ date: 2023-12-04 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
@@ -81,7 +82,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
81
82
  - !ruby/object:Gem::Version
82
83
  version: '0'
83
84
  requirements: []
84
- rubygems_version: 3.4.21
85
+ rubygems_version: 3.4.22
85
86
  signing_key:
86
87
  specification_version: 4
87
88
  summary: A Ruby interface to the X API.