x 0.1.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: 480eb23ebeb098bcc5c2d4924279d2d4608f8cf2a8e5eb7897edf45cc0311bc4
4
- data.tar.gz: c97f4694a91b3d3ad9f60d55001d48080329254d28db75aff74d646264fe38f2
3
+ metadata.gz: 56ad55c544f1c7774395d906e0d9d07b6dc2a6d20e37d7df0b3e5b9f7b7b2bb6
4
+ data.tar.gz: 1747e256413ee2aeae02d8ed0e351b6d411217b00e634a42aeec5299764d6fac
5
5
  SHA512:
6
- metadata.gz: 86adf1889c2e820353c4455a08c443480682d4827addd074a7e1fe3e60937e70b638118d31802c8cc57d457865c917d2f1a83d739448659519f42c9cbede74ed
7
- data.tar.gz: 714dcea3deff757a2d15548ed1fdbb17749ca5b901e9463a5b3a223120b05974ab792d1c3230406dc07984358a6d9e7c4d781402aa025fa5e257d967ee5ecbcf
6
+ metadata.gz: c5b386cd7386457b148c555359e68c4f9cb72b598ff9eb0a75405f3af75599dca0d5fc1700b01653465b2e50e401b69e9f9e5e9dd126d6a56c46b6df96b5736c
7
+ data.tar.gz: 462957aeee740d46f4a83497e69bec852fa0a4d8a9e38aec6bf8ffcd8f432e862737fa34847995dce5a49c09a40d9a08607ace68a1314a32972292d881cbd1db
data/.rubocop.yml CHANGED
@@ -15,8 +15,8 @@ Style/StringLiteralsInInterpolation:
15
15
  Enabled: true
16
16
  EnforcedStyle: double_quotes
17
17
 
18
- Layout/LineLength:
19
- Max: 120
20
-
21
18
  Style/FrozenStringLiteralComment:
22
19
  Enabled: false
20
+
21
+ Metrics/ParameterLists:
22
+ CountKeywordArgs: false
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
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
+
8
+ ## [0.2.0] - 2023-08-02
9
+
10
+ - Allow configuration of base URL (4bc0531)
11
+ - Improve error handling (14dc0cd)
12
+
3
13
  ## [0.1.0] - 2023-08-02
4
14
 
5
15
  - Initial release
data/Gemfile CHANGED
@@ -9,4 +9,5 @@ gem "rubocop", ">= 1.21"
9
9
  gem "rubocop-minitest", ">= 0.31"
10
10
  gem "rubocop-performance", ">= 1.18"
11
11
  gem "rubocop-rake", ">= 0.6"
12
+ gem "simplecov", ">= 0.22"
12
13
  gem "webmock", ">= 3.18.1"
data/README.md CHANGED
@@ -15,19 +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
- api_key = "YOUR_X_API_KEY"
19
- api_key_secret = "YOUR_X_API_KEY_SECRET"
20
- access_token = "YOUR_X_ACCESS_TOKEN"
21
- access_token_secret = "YOUR_X_ACCESS_TOKEN_SECRET"
22
-
23
- x_client = X::Client.new(api_key: api_key, api_key_secret: api_key_secret, access_token: access_token, access_token_secret: access_token_secret)
24
-
25
- begin
26
- response = x_client.get("users/me")
27
- puts JSON.pretty_generate(response)
28
- rescue StandardError => e
29
- puts "Error: #{e.message}"
30
- 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")
31
51
  ```
32
52
 
33
53
  ## Development
data/lib/x/client.rb CHANGED
@@ -1,11 +1,80 @@
1
- require "oauth"
1
+ require "forwardable"
2
2
  require "json"
3
3
  require "net/http"
4
+ require "oauth"
5
+ require "uri"
6
+ require_relative "version"
4
7
 
5
8
  module X
6
- # Main client that handles HTTP authentication and requests
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
+
59
+ # HTTP client that handles authentication and requests
7
60
  class Client
8
- BASE_URL = "https://api.twitter.com/2/".freeze
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
+
75
+ DEFAULT_BASE_URL = "https://api.twitter.com/2/".freeze
76
+ DEFAULT_USER_AGENT = "X-Client/#{X::Version} Ruby/#{RUBY_VERSION}".freeze
77
+ DEFAULT_READ_TIMEOUT = 60 # seconds
9
78
  HTTP_METHODS = {
10
79
  get: Net::HTTP::Get,
11
80
  post: Net::HTTP::Post,
@@ -13,52 +82,65 @@ module X
13
82
  delete: Net::HTTP::Delete
14
83
  }.freeze
15
84
 
16
- def initialize(bearer_token: nil, api_key: nil, api_key_secret: nil, access_token: nil, access_token_secret: nil)
17
- @use_bearer_token = !bearer_token.nil?
85
+ def initialize(bearer_token: nil, api_key: nil, api_key_secret: nil, access_token: nil, access_token_secret: nil,
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
18
90
 
19
- if @use_bearer_token
20
- @bearer_token = bearer_token
21
- else
22
- unless api_key && api_key_secret && access_token && access_token_secret
23
- raise ArgumentError, "Missing OAuth credentials."
24
- end
91
+ validate_base_url!
25
92
 
26
- @consumer = OAuth::Consumer.new(api_key, api_key_secret, site: BASE_URL)
27
- @access_token = OAuth::Token.new(access_token, access_token_secret)
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
28
97
  end
29
98
  end
30
99
 
31
100
  def get(endpoint)
32
- response = send_request(:get, endpoint)
33
- handle_response(response)
101
+ send_request(:get, endpoint)
34
102
  end
35
103
 
36
104
  def post(endpoint, body = nil)
37
- response = send_request(:post, endpoint, body)
38
- handle_response(response)
105
+ send_request(:post, endpoint, body)
39
106
  end
40
107
 
41
108
  def put(endpoint, body = nil)
42
- response = send_request(:put, endpoint, body)
43
- handle_response(response)
109
+ send_request(:put, endpoint, body)
44
110
  end
45
111
 
46
112
  def delete(endpoint)
47
- response = send_request(:delete, endpoint)
48
- handle_response(response)
113
+ send_request(:delete, endpoint)
114
+ end
115
+
116
+ def base_url=(base_url)
117
+ @base_url = URI(base_url)
118
+ validate_base_url!
49
119
  end
50
120
 
51
121
  private
52
122
 
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"
126
+ end
127
+
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
131
+
53
132
  def send_request(http_method, endpoint, body = nil)
54
- url = URI.parse(BASE_URL + endpoint)
133
+ url = URI.join(@base_url, endpoint)
55
134
  http = Net::HTTP.new(url.host, url.port)
56
- http.use_ssl = true
135
+ http.use_ssl = url.scheme == "https"
136
+ http.read_timeout = @read_timeout
57
137
 
58
138
  request = create_request(http_method, url, body)
59
- add_authorization(request)
139
+ add_headers(request)
60
140
 
61
- http.request(request)
141
+ handle_response(http.request(request))
142
+ rescue Errno::ECONNREFUSED, Net::OpenTimeout, Net::ReadTimeout => e
143
+ raise X::NetworkError, "Network error: #{e.message}"
62
144
  end
63
145
 
64
146
  def create_request(http_method, url, body)
@@ -71,24 +153,62 @@ module X
71
153
  request
72
154
  end
73
155
 
156
+ def add_headers(request)
157
+ add_authorization(request)
158
+ add_content_type(request)
159
+ add_user_agent(request)
160
+ end
161
+
74
162
  def add_authorization(request)
75
- if @use_bearer_token
76
- request["Authorization"] = "Bearer #{@bearer_token}"
77
- else
163
+ if @bearer_token.nil?
78
164
  @consumer.sign!(request, @access_token)
165
+ else
166
+ request["Authorization"] = "Bearer #{@bearer_token}"
79
167
  end
80
168
  end
81
169
 
170
+ def add_content_type(request)
171
+ request["Content-Type"] = JSON_CONTENT_TYPE
172
+ end
173
+
174
+ def add_user_agent(request)
175
+ request["User-Agent"] = @user_agent if @user_agent
176
+ end
177
+
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
181
+
82
182
  def handle_response(response)
83
- case response
84
- when Net::HTTPSuccess
85
- JSON.parse(response.body)
86
- when Net::HTTPUnauthorized
87
- raise X::AuthenticationError, "Authentication failed. Please check your credentials."
88
- when Net::HTTPServerError
89
- raise X::ServerError, "An internal server error occurred."
90
- else
91
- raise X::Error, "Unexpected response: #{response.code} #{response.message}"
183
+ ResponseHandler.new(response).handle
184
+ end
185
+
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
196
+ }.freeze
197
+
198
+ def initialize(response)
199
+ @response = response
200
+ end
201
+
202
+ def handle
203
+ if @response.is_a?(Net::HTTPSuccess) && @response["content-type"] == JSON_CONTENT_TYPE
204
+ return JSON.parse(@response.body)
205
+ end
206
+
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?
210
+
211
+ raise error_class.new(@response), error_message
92
212
  end
93
213
  end
94
214
  end
data/lib/x/version.rb CHANGED
@@ -1,3 +1,39 @@
1
1
  module X
2
- VERSION = "0.1.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
1
  require_relative "x/client"
3
- require_relative "x/error"
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.1.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/error.rb
43
42
  - lib/x/version.rb
44
43
  - sig/x.rbs
45
44
  homepage: https://github.com/sferik/x-ruby
data/lib/x/error.rb DELETED
@@ -1,5 +0,0 @@
1
- module X
2
- class Error < StandardError; end
3
- class AuthenticationError < Error; end
4
- class ServerError < Error; end
5
- end