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 +4 -4
- data/CHANGELOG.md +7 -0
- data/README.md +35 -17
- data/lib/x/authenticator.rb +0 -1
- data/lib/x/bearer_token_authenticator.rb +0 -1
- data/lib/x/cgi.rb +0 -1
- data/lib/x/client.rb +0 -1
- data/lib/x/connection.rb +0 -1
- data/lib/x/errors/http_error.rb +0 -1
- data/lib/x/errors/too_many_requests.rb +8 -6
- data/lib/x/media_uploader.rb +2 -2
- data/lib/x/oauth_authenticator.rb +0 -1
- data/lib/x/rate_limit.rb +33 -0
- data/lib/x/redirect_handler.rb +0 -1
- data/lib/x/request_builder.rb +0 -1
- data/lib/x/response_parser.rb +0 -1
- data/lib/x/version.rb +1 -1
- data/sig/x.rbs +18 -7
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4f4970075e9256b31a7f53ca6a3a06df7559631b9104c2069372b3797845cd77
|
4
|
+
data.tar.gz: a0e25d20e521f8a3408b9e04b8826d7f8959257ecf6748687a0d1b0292719f38
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
![
|
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/
|
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
|
45
|
-
|
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
|
49
|
-
x_client.delete("tweets/#{
|
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
|
-
#
|
55
|
+
# Get your account settings
|
56
56
|
v1_client.get("account/settings.json")
|
57
57
|
|
58
|
-
# Initialize an
|
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
|
-
#
|
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)
|
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
|
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 [
|
152
|
+
1. Code must conform to [RuboCop rules](https://github.com/rubocop/rubocop). This can be verified with:
|
136
153
|
|
137
|
-
bundle exec
|
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:
|
data/lib/x/authenticator.rb
CHANGED
data/lib/x/cgi.rb
CHANGED
data/lib/x/client.rb
CHANGED
data/lib/x/connection.rb
CHANGED
data/lib/x/errors/http_error.rb
CHANGED
@@ -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
|
7
|
-
|
6
|
+
def rate_limit
|
7
|
+
rate_limits.max_by(&:reset_at)
|
8
8
|
end
|
9
9
|
|
10
|
-
def
|
11
|
-
|
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(
|
17
|
+
rate_limit&.reset_at || Time.at(0)
|
16
18
|
end
|
17
19
|
|
18
20
|
def reset_in
|
data/lib/x/media_uploader.rb
CHANGED
@@ -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.
|
75
|
+
File.binwrite(path, chunk)
|
76
76
|
end
|
77
77
|
end
|
78
78
|
end
|
data/lib/x/rate_limit.rb
ADDED
@@ -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
|
data/lib/x/redirect_handler.rb
CHANGED
data/lib/x/request_builder.rb
CHANGED
data/lib/x/response_parser.rb
CHANGED
data/lib/x/version.rb
CHANGED
data/sig/x.rbs
CHANGED
@@ -102,10 +102,10 @@ module X
|
|
102
102
|
end
|
103
103
|
|
104
104
|
class TooManyRequests < ClientError
|
105
|
-
@
|
105
|
+
@rate_limits: Array[RateLimit]
|
106
106
|
|
107
|
-
def
|
108
|
-
def
|
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.
|
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
|
+
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.
|
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.
|