x 0.2.0 → 0.3.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: c40d2b65cf1479d65e6f434d6d9c98cb32ad7aa62b739fd1207b491b7e276e4d
4
- data.tar.gz: 6dee4de59342deb6bbc8a5897f7a96e18ae6b7a856083ae84e1ee26580ce7482
3
+ metadata.gz: 56ad55c544f1c7774395d906e0d9d07b6dc2a6d20e37d7df0b3e5b9f7b7b2bb6
4
+ data.tar.gz: 1747e256413ee2aeae02d8ed0e351b6d411217b00e634a42aeec5299764d6fac
5
5
  SHA512:
6
- metadata.gz: d64f01c2f011e53621214597fd1a9ac80e777ce22db2eb5d48c6b2f760743efa2fa3aa6848e6549b3fc25290c80456873262ddf92dbffaff2d134040cb87555f
7
- data.tar.gz: 8fc7d2aca33c67534ac775cc97f6afce95ef035716c5799e3571fb7b3a0218fd5b4646b41d8c66f61e3bba1d58f22b6c56c67ca273199b99dd7c55829827215a
6
+ metadata.gz: c5b386cd7386457b148c555359e68c4f9cb72b598ff9eb0a75405f3af75599dca0d5fc1700b01653465b2e50e401b69e9f9e5e9dd126d6a56c46b6df96b5736c
7
+ data.tar.gz: 462957aeee740d46f4a83497e69bec852fa0a4d8a9e38aec6bf8ffcd8f432e862737fa34847995dce5a49c09a40d9a08607ace68a1314a32972292d881cbd1db
data/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  ## [Unreleased]
2
2
 
3
+ - Add accessors to X::Client (e61fa73)
4
+ - Add configurable read timeout (41502b9)
5
+ - Handle network-related errors (9ed1fb4)
6
+ - Include response body in errors (a203e6a)
7
+
3
8
  ## [0.2.0] - 2023-08-02
4
9
 
5
10
  - Allow configuration of base URL (4bc0531)
data/README.md CHANGED
@@ -15,22 +15,39 @@ If bundler is not being used to manage dependencies, install the gem by executin
15
15
  ## Usage
16
16
 
17
17
  ```ruby
18
- x_api_key = "YOUR_X_API_KEY"
19
- x_api_key_secret = "YOUR_X_API_KEY_SECRET"
20
- x_access_token = "YOUR_X_ACCESS_TOKEN"
21
- x_access_token_secret = "YOUR_X_ACCESS_TOKEN_SECRET"
22
-
23
- x_client = X::Client.new(api_key: x_api_key,
24
- api_key_secret: x_api_key_secret,
25
- access_token: x_access_token,
26
- access_token_secret: x_access_token_secret)
27
-
28
- begin
29
- response = x_client.get("users/me")
30
- puts JSON.pretty_generate(response)
31
- rescue X::Error => e
32
- puts "Error: #{e.message}"
33
- end
18
+ oauth_credentials = {
19
+ api_key: "INSERT YOUR X API KEY HERE",
20
+ api_key_secret: "INSERT YOUR X API KEY SECRET HERE",
21
+ access_token: "INSERT YOUR X API ACCESS TOKEN HERE",
22
+ access_token_secret: "INSERT YOUR X API ACCESS TOKEN SECRET HERE",
23
+ }
24
+
25
+ # Initialize X API client with OAuth credentials
26
+ x_client = X::Client.new(**oauth_credentials)
27
+
28
+ # Request yourself
29
+ x_client.get("users/me")
30
+ # {"data"=>{"id"=>"7505382", "name"=>"Erik Berlin", "username"=>"sferik"}}
31
+
32
+ # Post a tweet
33
+ tweet = x_client.post("tweets", '{"text":"Hello, World! (from @gem)"}')
34
+ # {"data"=>{"edit_history_tweet_ids"=>["1234567890123456789"], "id"=>"1234567890123456789", "text"=>"Hello, World! (from @gem)"}}
35
+
36
+ # Delete a tweet
37
+ x_client.delete("tweets/#{tweet["data"]["id"]}")
38
+ # {"data"=>{"deleted"=>true}}
39
+
40
+ # Initialize an API v1.1 client
41
+ v1_client = X::Client.new(base_url: "https://api.twitter.com/1.1/", **oauth_credentials)
42
+
43
+ # Request your account settings
44
+ v1_client.get("account/settings.json")
45
+
46
+ # Initialize an X Ads API client
47
+ ads_client = X::Client.new(base_url: "https://ads-api.twitter.com/12/", **oauth_credentials)
48
+
49
+ # Request your ad accounts
50
+ ads_client.get("accounts")
34
51
  ```
35
52
 
36
53
  ## Development
data/lib/x/client.rb CHANGED
@@ -1,142 +1,198 @@
1
+ require "forwardable"
1
2
  require "json"
2
3
  require "net/http"
3
4
  require "oauth"
5
+ require "uri"
6
+ require_relative "version"
4
7
 
5
8
  module X
9
+ JSON_CONTENT_TYPE = "application/json; charset=utf-8".freeze
10
+
11
+ # Base error class
12
+ class Error < ::StandardError
13
+ attr_reader :object
14
+
15
+ def initialize(response = nil)
16
+ if response.is_a?(Net::HTTPResponse) && response.body && response["content-type"] == JSON_CONTENT_TYPE
17
+ @object = JSON.parse(response.body)
18
+ end
19
+ super
20
+ end
21
+ end
22
+
23
+ class NetworkError < Error; end
24
+ class ClientError < Error; end
25
+ class AuthenticationError < ClientError; end
26
+ class BadRequestError < ClientError; end
27
+ class ForbiddenError < ClientError; end
28
+ class NotFoundError < ClientError; end
29
+
30
+ # Rate limit error
31
+ class TooManyRequestsError < ClientError
32
+ def initialize(response = nil)
33
+ @response = response
34
+ super
35
+ end
36
+
37
+ def limit
38
+ @response && @response["x-rate-limit-limit"]&.to_i
39
+ end
40
+
41
+ def remaining
42
+ @response && @response["x-rate-limit-remaining"]&.to_i
43
+ end
44
+
45
+ def reset_at
46
+ reset = @response && @response["x-rate-limit-reset"]&.to_i
47
+ Time.at(reset.to_i).utc if reset
48
+ end
49
+
50
+ def reset_in
51
+ [(reset_at - Time.now).ceil, 0].max if reset_at
52
+ end
53
+ alias retry_after reset_in
54
+ end
55
+
56
+ class ServerError < Error; end
57
+ class ServiceUnavailableError < ServerError; end
58
+
6
59
  # HTTP client that handles authentication and requests
7
60
  class Client
61
+ extend Forwardable
62
+
63
+ attr_accessor :bearer_token, :user_agent, :read_timeout
64
+ attr_reader :base_url
65
+
66
+ def_delegator :@access_token, :secret, :access_token_secret
67
+ def_delegator :@access_token, :secret=, :access_token_secret=
68
+ def_delegator :@access_token, :token, :access_token
69
+ def_delegator :@access_token, :token=, :access_token=
70
+ def_delegator :@consumer, :key, :api_key
71
+ def_delegator :@consumer, :key=, :api_key=
72
+ def_delegator :@consumer, :secret, :api_key_secret
73
+ def_delegator :@consumer, :secret=, :api_key_secret=
74
+
8
75
  DEFAULT_BASE_URL = "https://api.twitter.com/2/".freeze
9
- DEFAULT_USER_AGENT = "X-Client/#{VERSION} Ruby/#{RUBY_VERSION}".freeze
76
+ DEFAULT_USER_AGENT = "X-Client/#{X::Version} Ruby/#{RUBY_VERSION}".freeze
77
+ DEFAULT_READ_TIMEOUT = 60 # seconds
78
+ HTTP_METHODS = {
79
+ get: Net::HTTP::Get,
80
+ post: Net::HTTP::Post,
81
+ put: Net::HTTP::Put,
82
+ delete: Net::HTTP::Delete
83
+ }.freeze
10
84
 
11
85
  def initialize(bearer_token: nil, api_key: nil, api_key_secret: nil, access_token: nil, access_token_secret: nil,
12
- base_url: DEFAULT_BASE_URL, user_agent: DEFAULT_USER_AGENT)
13
- @http_request = HttpRequest.new(bearer_token: bearer_token,
14
- api_key: api_key,
15
- api_key_secret: api_key_secret,
16
- access_token: access_token,
17
- access_token_secret: access_token_secret,
18
- base_url: base_url,
19
- user_agent: user_agent)
86
+ base_url: DEFAULT_BASE_URL, user_agent: DEFAULT_USER_AGENT, read_timeout: DEFAULT_READ_TIMEOUT)
87
+ @base_url = URI(base_url)
88
+ @user_agent = user_agent
89
+ @read_timeout = read_timeout
90
+
91
+ validate_base_url!
92
+
93
+ if bearer_token.nil?
94
+ initialize_oauth(api_key, api_key_secret, access_token, access_token_secret)
95
+ else
96
+ @bearer_token = bearer_token
97
+ end
20
98
  end
21
99
 
22
100
  def get(endpoint)
23
- handle_response { @http_request.get(endpoint) }
101
+ send_request(:get, endpoint)
24
102
  end
25
103
 
26
104
  def post(endpoint, body = nil)
27
- handle_response { @http_request.post(endpoint, body) }
105
+ send_request(:post, endpoint, body)
28
106
  end
29
107
 
30
108
  def put(endpoint, body = nil)
31
- handle_response { @http_request.put(endpoint, body) }
109
+ send_request(:put, endpoint, body)
32
110
  end
33
111
 
34
112
  def delete(endpoint)
35
- handle_response { @http_request.delete(endpoint) }
113
+ send_request(:delete, endpoint)
36
114
  end
37
115
 
38
- private
39
-
40
- def handle_response
41
- response = yield
42
- ErrorHandler.new(response).handle
116
+ def base_url=(base_url)
117
+ @base_url = URI(base_url)
118
+ validate_base_url!
43
119
  end
44
120
 
45
- # HTTP client requester
46
- class HttpRequest
47
- HTTP_METHODS = {
48
- get: Net::HTTP::Get,
49
- post: Net::HTTP::Post,
50
- put: Net::HTTP::Put,
51
- delete: Net::HTTP::Delete
52
- }.freeze
53
-
54
- def initialize(bearer_token: nil, api_key: nil, api_key_secret: nil, access_token: nil, access_token_secret: nil,
55
- base_url: nil, user_agent: nil)
56
- @base_url = base_url
57
- @use_bearer_token = !bearer_token.nil?
58
- @user_agent = user_agent || Client::DEFAULT_USER_AGENT
121
+ private
59
122
 
60
- if @use_bearer_token
61
- @bearer_token = bearer_token
62
- else
63
- initialize_oauth(api_key, api_key_secret, access_token, access_token_secret)
64
- end
123
+ def initialize_oauth(api_key, api_key_secret, access_token, access_token_secret)
124
+ unless api_key && api_key_secret && access_token && access_token_secret
125
+ raise ArgumentError, "Missing OAuth credentials"
65
126
  end
66
127
 
67
- def get(endpoint)
68
- send_request(:get, endpoint)
69
- end
128
+ @consumer = OAuth::Consumer.new(api_key, api_key_secret, site: @base_url)
129
+ @access_token = OAuth::Token.new(access_token, access_token_secret)
130
+ end
70
131
 
71
- def post(endpoint, body = nil)
72
- send_request(:post, endpoint, body)
73
- end
132
+ def send_request(http_method, endpoint, body = nil)
133
+ url = URI.join(@base_url, endpoint)
134
+ http = Net::HTTP.new(url.host, url.port)
135
+ http.use_ssl = url.scheme == "https"
136
+ http.read_timeout = @read_timeout
74
137
 
75
- def put(endpoint, body = nil)
76
- send_request(:put, endpoint, body)
77
- end
138
+ request = create_request(http_method, url, body)
139
+ add_headers(request)
78
140
 
79
- def delete(endpoint)
80
- send_request(:delete, endpoint)
81
- end
141
+ handle_response(http.request(request))
142
+ rescue Errno::ECONNREFUSED, Net::OpenTimeout, Net::ReadTimeout => e
143
+ raise X::NetworkError, "Network error: #{e.message}"
144
+ end
82
145
 
83
- private
146
+ def create_request(http_method, url, body)
147
+ http_method_class = HTTP_METHODS[http_method]
84
148
 
85
- def initialize_oauth(api_key, api_key_secret, access_token, access_token_secret)
86
- unless api_key && api_key_secret && access_token && access_token_secret
87
- raise ArgumentError, "Missing OAuth credentials."
88
- end
89
-
90
- @consumer = OAuth::Consumer.new(api_key, api_key_secret, site: @base_url)
91
- @access_token = OAuth::Token.new(access_token, access_token_secret)
92
- end
149
+ raise ArgumentError, "Unsupported HTTP method: #{http_method}" unless http_method_class
93
150
 
94
- def send_request(http_method, endpoint, body = nil)
95
- url = URI.parse(@base_url + endpoint)
96
- http = Net::HTTP.new(url.host, url.port)
97
- http.use_ssl = true
151
+ request = http_method_class.new(url)
152
+ request.body = body if body && http_method != :get
153
+ request
154
+ end
98
155
 
99
- request = create_request(http_method, url, body)
100
- add_authorization(request)
101
- add_user_agent(request)
156
+ def add_headers(request)
157
+ add_authorization(request)
158
+ add_content_type(request)
159
+ add_user_agent(request)
160
+ end
102
161
 
103
- http.request(request)
162
+ def add_authorization(request)
163
+ if @bearer_token.nil?
164
+ @consumer.sign!(request, @access_token)
165
+ else
166
+ request["Authorization"] = "Bearer #{@bearer_token}"
104
167
  end
168
+ end
105
169
 
106
- def create_request(http_method, url, body)
107
- http_method_class = HTTP_METHODS[http_method]
108
-
109
- raise ArgumentError, "Unsupported HTTP method: #{http_method}" unless http_method_class
170
+ def add_content_type(request)
171
+ request["Content-Type"] = JSON_CONTENT_TYPE
172
+ end
110
173
 
111
- request = http_method_class.new(url)
112
- request.body = body if body && http_method != :get
113
- request
114
- end
174
+ def add_user_agent(request)
175
+ request["User-Agent"] = @user_agent if @user_agent
176
+ end
115
177
 
116
- def add_authorization(request)
117
- if @use_bearer_token
118
- request["Authorization"] = "Bearer #{@bearer_token}"
119
- else
120
- @consumer.sign!(request, @access_token)
121
- end
122
- end
178
+ def validate_base_url!
179
+ raise ArgumentError, "Invalid base URL" unless @base_url.is_a?(URI::HTTPS) || @base_url.is_a?(URI::HTTP)
180
+ end
123
181
 
124
- def add_user_agent(request)
125
- request["User-Agent"] = @user_agent if @user_agent
126
- end
182
+ def handle_response(response)
183
+ ResponseHandler.new(response).handle
127
184
  end
128
185
 
129
- # HTTP client error handler
130
- class ErrorHandler
131
- HTTP_STATUS_HANDLERS = {
132
- Net::HTTPOK => :handle_success_response,
133
- Net::HTTPBadRequest => :handle_bad_request_response,
134
- Net::HTTPForbidden => :handle_forbidden_response,
135
- Net::HTTPUnauthorized => :handle_unauthorized_response,
136
- Net::HTTPNotFound => :handle_not_found_response,
137
- Net::HTTPTooManyRequests => :handle_too_many_requests_response,
138
- Net::HTTPInternalServerError => :handle_server_error_response,
139
- Net::HTTPServiceUnavailable => :handle_service_unavailable_response
186
+ # HTTP client response handler
187
+ class ResponseHandler
188
+ ERROR_CLASSES = {
189
+ Net::HTTPBadRequest => X::BadRequestError,
190
+ Net::HTTPUnauthorized => X::AuthenticationError,
191
+ Net::HTTPForbidden => X::ForbiddenError,
192
+ Net::HTTPNotFound => X::NotFoundError,
193
+ Net::HTTPTooManyRequests => X::TooManyRequestsError,
194
+ Net::HTTPInternalServerError => X::ServerError,
195
+ Net::HTTPServiceUnavailable => X::ServiceUnavailableError
140
196
  }.freeze
141
197
 
142
198
  def initialize(response)
@@ -144,50 +200,15 @@ module X
144
200
  end
145
201
 
146
202
  def handle
147
- handler_method = HTTP_STATUS_HANDLERS[@response.class]
148
- if handler_method
149
- send(handler_method)
150
- else
151
- handle_unexpected_response
203
+ if @response.is_a?(Net::HTTPSuccess) && @response["content-type"] == JSON_CONTENT_TYPE
204
+ return JSON.parse(@response.body)
152
205
  end
153
- end
154
206
 
155
- private
156
-
157
- def handle_success_response
158
- JSON.parse(@response.body)
159
- end
160
-
161
- def handle_bad_request_response
162
- raise X::BadRequestError, "Bad request: #{@response.code} #{@response.message}"
163
- end
164
-
165
- def handle_forbidden_response
166
- raise X::ForbiddenError, "Forbidden: #{@response.code} #{@response.message}"
167
- end
168
-
169
- def handle_unauthorized_response
170
- raise X::AuthenticationError, "Authentication failed. Please check your credentials."
171
- end
172
-
173
- def handle_not_found_response
174
- raise X::NotFoundError, "Not found: #{@response.code} #{@response.message}"
175
- end
176
-
177
- def handle_too_many_requests_response
178
- raise X::TooManyRequestsError, "Too many requests: #{@response.code} #{@response.message}"
179
- end
180
-
181
- def handle_server_error_response
182
- raise X::ServerError, "Internal server error: #{@response.code} #{@response.message}"
183
- end
184
-
185
- def handle_service_unavailable_response
186
- raise X::ServiceUnavailableError, "Service unavailable: #{@response.code} #{@response.message}"
187
- end
207
+ error_class = ERROR_CLASSES[@response.class] || X::Error
208
+ error_message = "#{@response.code} #{@response.message}"
209
+ raise error_class, error_message if @response.body.nil? || @response.body.empty?
188
210
 
189
- def handle_unexpected_response
190
- raise X::Error, "Unexpected response: #{@response.code}"
211
+ raise error_class.new(@response), error_message
191
212
  end
192
213
  end
193
214
  end
data/lib/x/version.rb CHANGED
@@ -1,3 +1,39 @@
1
1
  module X
2
- VERSION = "0.2.0".freeze
2
+ # The version of this library
3
+ module Version
4
+ module_function
5
+
6
+ def major
7
+ 0
8
+ end
9
+
10
+ def minor
11
+ 3
12
+ end
13
+
14
+ def patch
15
+ 0
16
+ end
17
+
18
+ def pre
19
+ nil
20
+ end
21
+
22
+ def to_h
23
+ {
24
+ major: major,
25
+ minor: minor,
26
+ patch: patch,
27
+ pre: pre
28
+ }
29
+ end
30
+
31
+ def to_a
32
+ [major, minor, patch, pre].compact
33
+ end
34
+
35
+ def to_s
36
+ to_a.join(".")
37
+ end
38
+ end
3
39
  end
data/lib/x.rb CHANGED
@@ -1,3 +1 @@
1
- require_relative "x/version"
2
- require_relative "x/errors"
3
1
  require_relative "x/client"
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.2.0
4
+ version: 0.3.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-08-02 00:00:00.000000000 Z
11
+ date: 2023-08-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: oauth
@@ -39,7 +39,6 @@ files:
39
39
  - Rakefile
40
40
  - lib/x.rb
41
41
  - lib/x/client.rb
42
- - lib/x/errors.rb
43
42
  - lib/x/version.rb
44
43
  - sig/x.rbs
45
44
  homepage: https://github.com/sferik/x-ruby
data/lib/x/errors.rb DELETED
@@ -1,11 +0,0 @@
1
- module X
2
- class Error < ::StandardError; end
3
- class ClientError < Error; end
4
- class AuthenticationError < ClientError; end
5
- class BadRequestError < ClientError; end
6
- class ForbiddenError < ClientError; end
7
- class NotFoundError < ClientError; end
8
- class TooManyRequestsError < ClientError; end
9
- class ServerError < Error; end
10
- class ServiceUnavailableError < ServerError; end
11
- end