x 0.18.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +3 -0
- data/README.md +15 -2
- data/lib/x/client.rb +34 -3
- data/lib/x/connection.rb +20 -3
- data/lib/x/stream_parser.rb +75 -0
- data/lib/x/version.rb +1 -1
- data/sig/x.rbs +16 -1
- 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: 5aaa181ca277a20cc6ddb1ec8d4219535357611891da51642471f54c032a2b47
|
|
4
|
+
data.tar.gz: 43f3949ff58bb6e3248adad99324f25f6709ad507fc96bb75ff47eba832bddba
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 612f01c108c9c219363f2910fe393795881151b008c6dc452210ead69c02d1421141061575e8d92f5108fbdc18a3949bc6c3330c9c73444495ed37b4df587e03
|
|
7
|
+
data.tar.gz: 1a5759aa9862066f735632e0ff63defeb6827bd63657013b88dd33d97792ca177f95859784f08ac32f0f31249ee6a26bdfab582218aee1abd79fcac71281b067
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
|
@@ -71,11 +71,23 @@ ads_client = X::Client.new(base_url: "https://ads-api.twitter.com/12/", **x_cred
|
|
|
71
71
|
ads_client.get("accounts")
|
|
72
72
|
```
|
|
73
73
|
|
|
74
|
+
### Streaming
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
# Set up rules for filtered stream
|
|
78
|
+
x_client.post("tweets/search/stream/rules", '{"add": [{"value": "ruby"}]}')
|
|
79
|
+
|
|
80
|
+
# Stream matching posts in real time
|
|
81
|
+
x_client.stream("tweets/search/stream") do |tweet|
|
|
82
|
+
puts tweet["data"]["text"]
|
|
83
|
+
end
|
|
84
|
+
```
|
|
85
|
+
|
|
74
86
|
See other common usage [examples](https://github.com/sferik/x-ruby/tree/main/examples).
|
|
75
87
|
|
|
76
88
|
## History and Philosophy
|
|
77
89
|
|
|
78
|
-
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
|
|
90
|
+
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 less than 1,000 lines of code (plus 2,000 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:
|
|
79
91
|
|
|
80
92
|
* Less code is easier to maintain.
|
|
81
93
|
* Less code means fewer bugs.
|
|
@@ -91,7 +103,7 @@ This code is not littered with comments that are intended to generate documentat
|
|
|
91
103
|
|
|
92
104
|
## Features
|
|
93
105
|
|
|
94
|
-
If this entire library is implemented in
|
|
106
|
+
If this entire library is implemented in under 1,000 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:
|
|
95
107
|
|
|
96
108
|
* OAuth 1.0 Revision A
|
|
97
109
|
* OAuth 2.0
|
|
@@ -102,6 +114,7 @@ If this entire library is implemented in just 750 lines of code, why should you
|
|
|
102
114
|
* HTTP timeout configuration
|
|
103
115
|
* HTTP error handling
|
|
104
116
|
* Rate limit handling
|
|
117
|
+
* Streaming (filtered stream, volume stream)
|
|
105
118
|
* Parsing JSON into custom response objects (e.g. OpenStruct)
|
|
106
119
|
* Configurable base URLs for accessing different APIs/versions
|
|
107
120
|
* Parallel uploading of large media files in chunks
|
data/lib/x/client.rb
CHANGED
|
@@ -8,6 +8,7 @@ require_relative "oauth2_authenticator"
|
|
|
8
8
|
require_relative "redirect_handler"
|
|
9
9
|
require_relative "request_builder"
|
|
10
10
|
require_relative "response_parser"
|
|
11
|
+
require_relative "stream_parser"
|
|
11
12
|
|
|
12
13
|
module X
|
|
13
14
|
# A client for interacting with the X API
|
|
@@ -87,7 +88,7 @@ module X
|
|
|
87
88
|
open_timeout: Connection::DEFAULT_OPEN_TIMEOUT,
|
|
88
89
|
read_timeout: Connection::DEFAULT_READ_TIMEOUT,
|
|
89
90
|
write_timeout: Connection::DEFAULT_WRITE_TIMEOUT,
|
|
90
|
-
debug_output:
|
|
91
|
+
debug_output: nil,
|
|
91
92
|
proxy_url: nil,
|
|
92
93
|
default_array_class: DEFAULT_ARRAY_CLASS,
|
|
93
94
|
default_object_class: DEFAULT_OBJECT_CLASS,
|
|
@@ -96,12 +97,12 @@ module X
|
|
|
96
97
|
client_id:, client_secret:, refresh_token:)
|
|
97
98
|
initialize_authenticator
|
|
98
99
|
@base_url = base_url
|
|
99
|
-
|
|
100
|
-
@default_object_class = default_object_class
|
|
100
|
+
initialize_default_classes(default_array_class:, default_object_class:)
|
|
101
101
|
@connection = Connection.new(open_timeout:, read_timeout:, write_timeout:, debug_output:, proxy_url:)
|
|
102
102
|
@request_builder = RequestBuilder.new
|
|
103
103
|
@redirect_handler = RedirectHandler.new(connection: @connection, request_builder: @request_builder, max_redirects:)
|
|
104
104
|
@response_parser = ResponseParser.new
|
|
105
|
+
@stream_parser = StreamParser.new
|
|
105
106
|
end
|
|
106
107
|
|
|
107
108
|
# Perform a GET request to the X API
|
|
@@ -144,8 +145,38 @@ module X
|
|
|
144
145
|
execute_request(:delete, endpoint, headers:, array_class:, object_class:)
|
|
145
146
|
end
|
|
146
147
|
|
|
148
|
+
# Stream data from the X API
|
|
149
|
+
#
|
|
150
|
+
# @api public
|
|
151
|
+
# @param endpoint [String] the streaming API endpoint
|
|
152
|
+
# @param headers [Hash] additional headers for the request
|
|
153
|
+
# @param array_class [Class] the class for parsing JSON arrays
|
|
154
|
+
# @param object_class [Class] the class for parsing JSON objects
|
|
155
|
+
# @yield [Hash, Array] each parsed JSON object from the stream
|
|
156
|
+
# @return [void]
|
|
157
|
+
# @raise [HTTPError] if the response is not successful
|
|
158
|
+
# @example Stream filtered tweets
|
|
159
|
+
# client.stream("tweets/search/stream") { |tweet| puts tweet }
|
|
160
|
+
def stream(endpoint, headers: {}, array_class: default_array_class, object_class: default_object_class, &block)
|
|
161
|
+
uri = URI.join(base_url, endpoint)
|
|
162
|
+
request = @request_builder.build(http_method: :get, uri:, headers:, authenticator:)
|
|
163
|
+
@connection.perform_stream(request:) do |response|
|
|
164
|
+
@stream_parser.process(response:, response_parser: @response_parser, array_class:, object_class:, &block)
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
147
168
|
private
|
|
148
169
|
|
|
170
|
+
# Initialize default JSON parsing classes
|
|
171
|
+
# @api private
|
|
172
|
+
# @param default_array_class [Class] the default class for parsing JSON arrays
|
|
173
|
+
# @param default_object_class [Class] the default class for parsing JSON objects
|
|
174
|
+
# @return [void]
|
|
175
|
+
def initialize_default_classes(default_array_class:, default_object_class:)
|
|
176
|
+
@default_array_class = default_array_class
|
|
177
|
+
@default_object_class = default_object_class
|
|
178
|
+
end
|
|
179
|
+
|
|
149
180
|
# Execute an HTTP request to the X API
|
|
150
181
|
# @api private
|
|
151
182
|
# @return [Hash, Array, nil] the parsed response body
|
data/lib/x/connection.rb
CHANGED
|
@@ -20,8 +20,6 @@ module X
|
|
|
20
20
|
DEFAULT_READ_TIMEOUT = 60 # seconds
|
|
21
21
|
# Default timeout for writing requests in seconds
|
|
22
22
|
DEFAULT_WRITE_TIMEOUT = 60 # seconds
|
|
23
|
-
# Default debug output destination
|
|
24
|
-
DEFAULT_DEBUG_OUTPUT = File.open(IO::NULL, "w")
|
|
25
23
|
# Network errors that should be wrapped in NetworkError
|
|
26
24
|
NETWORK_ERRORS = [
|
|
27
25
|
Errno::ECONNREFUSED,
|
|
@@ -92,7 +90,7 @@ module X
|
|
|
92
90
|
# @example Create a connection with custom timeouts
|
|
93
91
|
# connection = X::Connection.new(open_timeout: 30, read_timeout: 30)
|
|
94
92
|
def initialize(open_timeout: DEFAULT_OPEN_TIMEOUT, read_timeout: DEFAULT_READ_TIMEOUT,
|
|
95
|
-
write_timeout: DEFAULT_WRITE_TIMEOUT, debug_output:
|
|
93
|
+
write_timeout: DEFAULT_WRITE_TIMEOUT, debug_output: nil, proxy_url: nil)
|
|
96
94
|
@open_timeout = open_timeout
|
|
97
95
|
@read_timeout = read_timeout
|
|
98
96
|
@write_timeout = write_timeout
|
|
@@ -118,6 +116,25 @@ module X
|
|
|
118
116
|
raise NetworkError, "Network error: #{e}"
|
|
119
117
|
end
|
|
120
118
|
|
|
119
|
+
# Perform a streaming HTTP request
|
|
120
|
+
#
|
|
121
|
+
# @api public
|
|
122
|
+
# @param request [Net::HTTPRequest] the HTTP request to perform
|
|
123
|
+
# @yield [Net::HTTPResponse] the HTTP response for streaming
|
|
124
|
+
# @return [void]
|
|
125
|
+
# @raise [NetworkError] if a network error occurs
|
|
126
|
+
# @example Perform a streaming request
|
|
127
|
+
# connection.perform_stream(request: request) { |response| response.read_body { |chunk| } }
|
|
128
|
+
def perform_stream(request:, &)
|
|
129
|
+
host = request.uri.host || DEFAULT_HOST
|
|
130
|
+
port = request.uri.port || DEFAULT_PORT
|
|
131
|
+
http_client = build_http_client(host, port)
|
|
132
|
+
http_client.use_ssl = request.uri.scheme.eql?("https")
|
|
133
|
+
http_client.request(request, &)
|
|
134
|
+
rescue *NETWORK_ERRORS => e
|
|
135
|
+
raise NetworkError, "Network error: #{e}"
|
|
136
|
+
end
|
|
137
|
+
|
|
121
138
|
# Set the proxy URL for requests
|
|
122
139
|
#
|
|
123
140
|
# @api public
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "net/http"
|
|
3
|
+
require_relative "response_parser"
|
|
4
|
+
|
|
5
|
+
module X
|
|
6
|
+
# Handles streaming responses from the X API
|
|
7
|
+
# @api public
|
|
8
|
+
class StreamParser
|
|
9
|
+
# Line delimiter for streaming responses
|
|
10
|
+
LINE_DELIMITER = "\r\n".freeze
|
|
11
|
+
|
|
12
|
+
# Process a streaming response and yield parsed JSON objects
|
|
13
|
+
#
|
|
14
|
+
# @api public
|
|
15
|
+
# @param response [Net::HTTPResponse] the HTTP response to stream
|
|
16
|
+
# @param response_parser [ResponseParser] the response parser for error handling
|
|
17
|
+
# @param array_class [Class, nil] the class for parsing JSON arrays
|
|
18
|
+
# @param object_class [Class, nil] the class for parsing JSON objects
|
|
19
|
+
# @yield [Hash, Array] each parsed JSON object from the stream
|
|
20
|
+
# @return [void]
|
|
21
|
+
# @raise [HTTPError] if the response is not successful
|
|
22
|
+
# @example Process a streaming response
|
|
23
|
+
# handler.process(response: response, response_parser: parser) { |json| puts json }
|
|
24
|
+
def process(response:, response_parser:, array_class: nil, object_class: nil, &block)
|
|
25
|
+
response_parser.parse(response:) unless response.is_a?(Net::HTTPSuccess)
|
|
26
|
+
|
|
27
|
+
buffer = +""
|
|
28
|
+
response.read_body do |chunk|
|
|
29
|
+
buffer << chunk
|
|
30
|
+
process_buffer(buffer:, array_class:, object_class:, &block)
|
|
31
|
+
end
|
|
32
|
+
process_remaining(buffer:, array_class:, object_class:, &block)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
# Process complete lines from the buffer
|
|
38
|
+
# @api private
|
|
39
|
+
# @param buffer [String] the accumulated data buffer
|
|
40
|
+
# @param array_class [Class, nil] the class for parsing JSON arrays
|
|
41
|
+
# @param object_class [Class, nil] the class for parsing JSON objects
|
|
42
|
+
# @yield [Hash, Array] each parsed JSON object
|
|
43
|
+
# @return [void]
|
|
44
|
+
def process_buffer(buffer:, array_class:, object_class:, &)
|
|
45
|
+
while (line_end = buffer.index(LINE_DELIMITER))
|
|
46
|
+
line = buffer.slice!(0, line_end) # : String
|
|
47
|
+
buffer.delete_prefix!(LINE_DELIMITER)
|
|
48
|
+
yield_json(line:, array_class:, object_class:, &) unless line.empty?
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Process any remaining data after the stream ends
|
|
53
|
+
# @api private
|
|
54
|
+
# @param buffer [String] the remaining data buffer
|
|
55
|
+
# @param array_class [Class, nil] the class for parsing JSON arrays
|
|
56
|
+
# @param object_class [Class, nil] the class for parsing JSON objects
|
|
57
|
+
# @yield [Hash, Array] the parsed JSON object
|
|
58
|
+
# @return [void]
|
|
59
|
+
def process_remaining(buffer:, array_class:, object_class:, &)
|
|
60
|
+
buffer.strip!
|
|
61
|
+
yield_json(line: buffer, array_class:, object_class:, &) unless buffer.empty?
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Parse a line as JSON and yield the result
|
|
65
|
+
# @api private
|
|
66
|
+
# @param line [String] the JSON line to parse
|
|
67
|
+
# @param array_class [Class, nil] the class for parsing JSON arrays
|
|
68
|
+
# @param object_class [Class, nil] the class for parsing JSON objects
|
|
69
|
+
# @yield [Hash, Array] the parsed JSON object
|
|
70
|
+
# @return [void]
|
|
71
|
+
def yield_json(line:, array_class:, object_class:)
|
|
72
|
+
yield JSON.parse(line, array_class:, object_class:)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
data/lib/x/version.rb
CHANGED
data/sig/x.rbs
CHANGED
|
@@ -125,7 +125,7 @@ module X
|
|
|
125
125
|
DEFAULT_OPEN_TIMEOUT: Integer
|
|
126
126
|
DEFAULT_READ_TIMEOUT: Integer
|
|
127
127
|
DEFAULT_WRITE_TIMEOUT: Integer
|
|
128
|
-
|
|
128
|
+
|
|
129
129
|
NETWORK_ERRORS: Array[(singleton(Errno::ECONNREFUSED) | singleton(Errno::ECONNRESET) | singleton(Net::OpenTimeout) | singleton(Net::ReadTimeout) | singleton(OpenSSL::SSL::SSLError))]
|
|
130
130
|
|
|
131
131
|
@proxy_url: URI::Generic | String
|
|
@@ -146,6 +146,7 @@ module X
|
|
|
146
146
|
def initialize: (?open_timeout: Float | Integer, ?read_timeout: Float | Integer, ?write_timeout: Float | Integer, ?proxy_url: URI::Generic? | String?, ?debug_output: IO) -> void
|
|
147
147
|
def proxy_url=: (URI::Generic | String proxy_url) -> void
|
|
148
148
|
def perform: (request: Net::HTTPRequest) -> Net::HTTPResponse
|
|
149
|
+
def perform_stream: (request: Net::HTTPRequest) { (Net::HTTPResponse) -> void } -> void
|
|
149
150
|
|
|
150
151
|
private
|
|
151
152
|
def build_http_client: (?String host, ?Integer port) -> Net::HTTP
|
|
@@ -208,6 +209,17 @@ module X
|
|
|
208
209
|
def json?: (Net::HTTPResponse response) -> bool
|
|
209
210
|
end
|
|
210
211
|
|
|
212
|
+
class StreamParser
|
|
213
|
+
LINE_DELIMITER: String
|
|
214
|
+
|
|
215
|
+
def process: (response: Net::HTTPResponse, response_parser: ResponseParser, ?array_class: Class?, ?object_class: Class?) { (untyped) -> void } -> void
|
|
216
|
+
|
|
217
|
+
private
|
|
218
|
+
def process_buffer: (buffer: String, array_class: Class?, object_class: Class?) { (untyped) -> void } -> void
|
|
219
|
+
def process_remaining: (buffer: String, array_class: Class?, object_class: Class?) { (untyped) -> void } -> void
|
|
220
|
+
def yield_json: (line: String, array_class: Class?, object_class: Class?) { (untyped) -> void } -> void
|
|
221
|
+
end
|
|
222
|
+
|
|
211
223
|
class OAuth2Authenticator < Authenticator
|
|
212
224
|
TOKEN_PATH: String
|
|
213
225
|
TOKEN_HOST: String
|
|
@@ -270,6 +282,7 @@ module X
|
|
|
270
282
|
@request_builder: RequestBuilder
|
|
271
283
|
@redirect_handler: RedirectHandler
|
|
272
284
|
@response_parser: ResponseParser
|
|
285
|
+
@stream_parser: StreamParser
|
|
273
286
|
|
|
274
287
|
attr_accessor base_url: String
|
|
275
288
|
attr_accessor default_array_class: singleton(Array)
|
|
@@ -280,8 +293,10 @@ module X
|
|
|
280
293
|
def post: (String endpoint, ?String? body, ?headers: Hash[String, String], ?array_class: Class, ?object_class: Class) -> untyped
|
|
281
294
|
def put: (String endpoint, ?String? body, ?headers: Hash[String, String], ?array_class: Class, ?object_class: Class) -> untyped
|
|
282
295
|
def delete: (String endpoint, ?headers: Hash[String, String], ?array_class: Class, ?object_class: Class) -> untyped
|
|
296
|
+
def stream: (String endpoint, ?headers: Hash[String, String], ?array_class: Class, ?object_class: Class) { (untyped) -> void } -> void
|
|
283
297
|
|
|
284
298
|
private
|
|
299
|
+
def initialize_default_classes: (default_array_class: singleton(Array), default_object_class: singleton(Hash)) -> void
|
|
285
300
|
def execute_request: (:delete | :get | :post | :put http_method, String endpoint, ?body: String?, ?headers: Hash[String, String], ?array_class: Class, ?object_class: Class) -> nil
|
|
286
301
|
end
|
|
287
302
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: x
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.19.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Erik Berlin
|
|
@@ -70,6 +70,7 @@ files:
|
|
|
70
70
|
- lib/x/redirect_handler.rb
|
|
71
71
|
- lib/x/request_builder.rb
|
|
72
72
|
- lib/x/response_parser.rb
|
|
73
|
+
- lib/x/stream_parser.rb
|
|
73
74
|
- lib/x/version.rb
|
|
74
75
|
- sig/x.rbs
|
|
75
76
|
homepage: https://sferik.github.io/x-ruby
|
|
@@ -98,7 +99,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
98
99
|
- !ruby/object:Gem::Version
|
|
99
100
|
version: '0'
|
|
100
101
|
requirements: []
|
|
101
|
-
rubygems_version: 4.0.
|
|
102
|
+
rubygems_version: 4.0.7
|
|
102
103
|
specification_version: 4
|
|
103
104
|
summary: A Ruby interface to the X API.
|
|
104
105
|
test_files: []
|