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 +4 -4
- data/CHANGELOG.md +6 -0
- data/README.md +39 -11
- data/lib/x/client.rb +27 -19
- data/lib/x/errors/too_many_requests.rb +8 -5
- data/lib/x/rate_limit.rb +33 -0
- data/lib/x/response_parser.rb +1 -8
- data/lib/x/version.rb +1 -1
- data/sig/x.rbs +31 -18
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e7f8185b86c61142faad23edbd1563f5a10263d7f47114d5f31a83e93af805dd
|
4
|
+
data.tar.gz: 8df083a42c16683e27d4face3445e3a835663f11600751024a519c3a658ed0ed
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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/
|
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
|
-
#
|
56
|
-
|
55
|
+
# Define a custom response object
|
56
|
+
Language = Struct.new(:code, :name, :local_name, :status, :debug)
|
57
57
|
|
58
|
-
#
|
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.
|
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
|
-
|
171
|
+
4. 100% mutation coverage. This can be verified with:
|
144
172
|
|
145
173
|
bundle exec rake mutant
|
146
174
|
|
147
|
-
|
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
|
-
|
34
|
-
|
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
|
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
|
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
|
6
|
-
|
6
|
+
def rate_limit
|
7
|
+
rate_limits.max_by(&:reset_at)
|
7
8
|
end
|
8
9
|
|
9
|
-
def
|
10
|
-
|
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(
|
17
|
+
rate_limit&.reset_at || Time.at(0)
|
15
18
|
end
|
16
19
|
|
17
20
|
def reset_in
|
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/response_parser.rb
CHANGED
@@ -36,14 +36,7 @@ module X
|
|
36
36
|
}.freeze
|
37
37
|
JSON_CONTENT_TYPE_REGEXP = %r{application/json}
|
38
38
|
|
39
|
-
|
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
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]
|
@@ -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
|
-
|
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
|
219
|
-
attr_accessor
|
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, ?
|
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], ?
|
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.
|
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
|
+
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
|