typho-twitter 0.2.1
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.
- data/.document +5 -0
- data/.gitignore +24 -0
- data/LICENSE +20 -0
- data/README.rdoc +61 -0
- data/Rakefile +47 -0
- data/VERSION +1 -0
- data/lib/typho-twitter.rb +1 -0
- data/lib/typho-twitter/typho-twitter.rb +474 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +15 -0
- data/test/oauth.yaml.example +7 -0
- data/test/typho-twitter-test.rb +243 -0
- data/typho-twitter.gemspec +63 -0
- metadata +128 -0
data/.document
ADDED
data/.gitignore
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 shock
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
= Typho Twitter
|
2
|
+
|
3
|
+
== What
|
4
|
+
|
5
|
+
This is a RubyGem to simplify sending parallel batches of requests to the Twitter API in Ruby applications.
|
6
|
+
|
7
|
+
It is based on Typhoeus and OAuth.
|
8
|
+
|
9
|
+
It is currently a work in progress. Comments, suggestions, and feedback are welcome and encouraged.
|
10
|
+
|
11
|
+
== Why
|
12
|
+
|
13
|
+
Some applications need to send lots of individual requests to the Twitter API to do things such as retrieve details from a group of users, or get the recent statuses for a group of users.
|
14
|
+
|
15
|
+
For a sizable number of requests, doing this serially is extremely slow. TyphoTwitter lets you perform a batch of like requests in parallel, drastically reducing the amount of time it takes to perform the same number of requests.
|
16
|
+
|
17
|
+
== Installing
|
18
|
+
|
19
|
+
sudo gem install typho-twitter
|
20
|
+
|
21
|
+
The source code is hosted on GitHub: http://github.com/capitalthought/typho-twitter
|
22
|
+
|
23
|
+
== The basics
|
24
|
+
|
25
|
+
|
26
|
+
== Demonstration of usage
|
27
|
+
|
28
|
+
Create a TyphoTwitter instance. If you need to authorize:
|
29
|
+
|
30
|
+
@typho_twitter = TyphoTwitter.new(
|
31
|
+
:oauth=>{
|
32
|
+
:consumer_key=>'XXXXXXX',
|
33
|
+
:consumer_secret=>'YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY',
|
34
|
+
:token=>'XXXXXXX-YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY',
|
35
|
+
:secret=>'ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ',
|
36
|
+
:site=>'http://example.com'
|
37
|
+
}
|
38
|
+
)
|
39
|
+
screen_name_array = %w[02Blazer 080808news 0Amna0 100PercentTX 1043LaQueBuena 1049TheHorn 1070thefan 1070WINA 10jackrussel 10rWfe 10tonreverb 1337studios 141chars 1450whtc 1660THEFAN 16mthsapart 1968mike 1capplegate 1LUVMRWAY 1MattHopkins 1OneStone 1realestateteam 1stbassguitar 1stBrand 1ststepsmoney 1TeeTime 1weightliftin 2001MUgrad 203klender 20thCFlicks]
|
40
|
+
responses = @typho_twitter.get_users_show( screen_name_array )
|
41
|
+
responses.each do |response|
|
42
|
+
puts response.to_s
|
43
|
+
end
|
44
|
+
|
45
|
+
== More Information
|
46
|
+
|
47
|
+
* RDoc: http://rdoc.info/projects/capitalthought/typho-twitter/
|
48
|
+
|
49
|
+
== How to submit patches
|
50
|
+
|
51
|
+
The source code is hosted on the GitHub: http://github.com/capitalthought/typho-twitter
|
52
|
+
|
53
|
+
To submit a patch, please fork the typho-twitter project and create a patch with tests. Once you're happy with it send a pull request and post a message to the google group.
|
54
|
+
|
55
|
+
== License
|
56
|
+
|
57
|
+
This code is free to use under the terms of the MIT license.
|
58
|
+
|
59
|
+
== Contact
|
60
|
+
|
61
|
+
Comments are welcome. Send an email to me at typho-twitter@wdd.oib.com
|
data/Rakefile
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "typho-twitter"
|
8
|
+
gem.summary = %Q{Parallel twitter client using Typhoeus.}
|
9
|
+
gem.description = %Q{A Twitter client for performing a batch of Twitter calls in parallel.}
|
10
|
+
gem.email = "billdoughty@capitalthought.com"
|
11
|
+
gem.homepage = "http://github.com/capitalthought/typho-twitter"
|
12
|
+
gem.authors = ["shock"]
|
13
|
+
gem.add_development_dependency "rspec", ">= 1.2.9"
|
14
|
+
gem.add_dependency "wdd-ruby-ext", ">= 0.3.1"
|
15
|
+
gem.add_dependency "oauth", ">=0.4.3"
|
16
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
17
|
+
end
|
18
|
+
Jeweler::GemcutterTasks.new
|
19
|
+
rescue LoadError
|
20
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
21
|
+
end
|
22
|
+
|
23
|
+
require 'spec/rake/spectask'
|
24
|
+
Spec::Rake::SpecTask.new(:spec) do |spec|
|
25
|
+
spec.libs << 'lib' << 'spec'
|
26
|
+
spec.spec_files = FileList['spec/**/*_spec.rb']
|
27
|
+
end
|
28
|
+
|
29
|
+
Spec::Rake::SpecTask.new(:rcov) do |spec|
|
30
|
+
spec.libs << 'lib' << 'spec'
|
31
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
32
|
+
spec.rcov = true
|
33
|
+
end
|
34
|
+
|
35
|
+
task :spec => :check_dependencies
|
36
|
+
|
37
|
+
task :default => :spec
|
38
|
+
|
39
|
+
require 'rake/rdoctask'
|
40
|
+
Rake::RDocTask.new do |rdoc|
|
41
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
42
|
+
|
43
|
+
rdoc.rdoc_dir = 'rdoc'
|
44
|
+
rdoc.title = "typho-twitter #{version}"
|
45
|
+
rdoc.rdoc_files.include('README*')
|
46
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
47
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.2.1
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'typho-twitter/typho-twitter'
|
@@ -0,0 +1,474 @@
|
|
1
|
+
# Note the tests at the bottom. You can test this class by running it standalone in the interpreter.
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'wdd-ruby-ext'
|
5
|
+
require "cgi"
|
6
|
+
require "net/http"
|
7
|
+
require "uri"
|
8
|
+
require "time"
|
9
|
+
require "pp"
|
10
|
+
require 'json'
|
11
|
+
require 'base64'
|
12
|
+
require 'logger'
|
13
|
+
require "thread"
|
14
|
+
require "oauth"
|
15
|
+
require "oauth/request_proxy/typhoeus_request"
|
16
|
+
|
17
|
+
# Class to abstract access to Twitter's Web Traffic API.
|
18
|
+
# Makes use of the Typhoeus gem to enable concurrent API calls.
|
19
|
+
|
20
|
+
class TyphoTwitter
|
21
|
+
|
22
|
+
include WDD::Utils
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def logger
|
27
|
+
@logger
|
28
|
+
end
|
29
|
+
|
30
|
+
def puts message
|
31
|
+
logger.debug message
|
32
|
+
end
|
33
|
+
|
34
|
+
public
|
35
|
+
|
36
|
+
class HTTPException < RuntimeError
|
37
|
+
attr :code
|
38
|
+
attr :body
|
39
|
+
|
40
|
+
def initialize( code, body )
|
41
|
+
@code = code
|
42
|
+
@body = body
|
43
|
+
super( "#{code} - #{body}" )
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class TwitterException < RuntimeError
|
48
|
+
attr :code
|
49
|
+
attr :body
|
50
|
+
|
51
|
+
def initialize( code, body )
|
52
|
+
@code = code
|
53
|
+
@body = body
|
54
|
+
super( "#{code} - #{body}" )
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
attr :login
|
59
|
+
attr :password
|
60
|
+
attr :headers
|
61
|
+
|
62
|
+
# Constants
|
63
|
+
DEFAULT_REQUEST_TIMEOUT = 20000
|
64
|
+
DEFAULT_CONCURRENCY_LIMIT = 40
|
65
|
+
MAX_RETRIES = 10
|
66
|
+
|
67
|
+
# Create a TyphoTwitter instance.
|
68
|
+
# +options+ :
|
69
|
+
# :request_timeout Request timeout value in miliseconds
|
70
|
+
# :request_timeout Request timeout value in miliseconds
|
71
|
+
# :concurrency_limit Maximum number of concurrent Typhoeus requests
|
72
|
+
# :logger Logger to use - defaults to standard out with DEBUG level if not specified.
|
73
|
+
# :oauth Used to authorize with Twitter API.
|
74
|
+
# :consumer_key Consumer key for your application
|
75
|
+
# :consumer_secret Consumer secret for your appilcation
|
76
|
+
# :token Access token for the Twitter user to authenticate with
|
77
|
+
# :secret Access token secret
|
78
|
+
# :site The site URL for your application
|
79
|
+
def initialize options={}
|
80
|
+
if options[:oauth]
|
81
|
+
@oauth_options = {}
|
82
|
+
@oauth_options[:consumer] = OAuth::Consumer.new( options[:oauth][:consumer_key], options[:oauth][:consumer_secret], :site => options[:oauth][:site] )
|
83
|
+
@oauth_options[:access_token] = OAuth::AccessToken.from_hash( @oauth_options[:consumer], :oauth_token=>options[:oauth][:token], :oauth_token_secret=>options[:oauth][:secret] )
|
84
|
+
end
|
85
|
+
@headers = {}
|
86
|
+
@options = options
|
87
|
+
@request_timeout = options[:request_timeout] || DEFAULT_REQUEST_TIMEOUT
|
88
|
+
@concurrency_limit = options[:concurrency_limit] || DEFAULT_CONCURRENCY_LIMIT
|
89
|
+
@logger = options[:logger]
|
90
|
+
if @logger.nil?
|
91
|
+
$stdout.sync = true
|
92
|
+
@logger = Logger.new( $stdout )
|
93
|
+
@logger.level = Logger::DEBUG
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# Executes a batch of Twitter calls. Automatically handles timeout_retries when possible on failures.
|
98
|
+
# Returns a hash where each +data_array+ element is a key mapping to either
|
99
|
+
# a hash containing the results of the twitter call or a TwitterException object of the twitter call that failed.
|
100
|
+
# If +data_array+ is bigger than the concurrency_limit set in the TyphoTwitter constructor, it is broken
|
101
|
+
# up into batches of requests by Typhoeus automatically.
|
102
|
+
# +data_array+ - An array of data inputs, one for each twitter call
|
103
|
+
# +&block+ - A block that accepts an element of +data_array+ and returns a Tyhpoeus::Request object.
|
104
|
+
def typho_twitter_batch data_array, &block
|
105
|
+
|
106
|
+
json_results = {}
|
107
|
+
timeout_retries = 0
|
108
|
+
time_gate = WDD::Utils::TimeGate.new
|
109
|
+
rate_limit_exceeded = false
|
110
|
+
while data_array.length > 0
|
111
|
+
puts "Waiting on rate limiting Time Gate"
|
112
|
+
time_gate.wait
|
113
|
+
puts "Time Gate released"
|
114
|
+
hydra = Typhoeus::Hydra.new(:max_concurrency => @concurrency_limit)
|
115
|
+
hydra.disable_memoization
|
116
|
+
|
117
|
+
retries = 0
|
118
|
+
|
119
|
+
timed_out_inputs = Queue.new
|
120
|
+
data_array.each do |data_input|
|
121
|
+
request = yield( data_input )
|
122
|
+
if @oauth_options
|
123
|
+
oauth_params = {:consumer => @oauth_options[:consumer], :token => @oauth_options[:access_token]}
|
124
|
+
oauth_helper = OAuth::Client::Helper.new(request, oauth_params.merge(:request_uri => request.url))
|
125
|
+
request.headers.merge!({"Authorization" => oauth_helper.header}) # Signs the request
|
126
|
+
end
|
127
|
+
# printvar :request, request
|
128
|
+
request.on_complete do |response|
|
129
|
+
puts "[#{response.code}] - #{request.url}"
|
130
|
+
case response.code
|
131
|
+
when 200:
|
132
|
+
begin
|
133
|
+
json_object = JSON.parse( response.body )
|
134
|
+
json_results[data_input] = json_object
|
135
|
+
retries = 0
|
136
|
+
rescue JSON::ParserError
|
137
|
+
if timeout_retries < MAX_RETRIES
|
138
|
+
timed_out_inputs.push data_input
|
139
|
+
else
|
140
|
+
json_results[data_input] = $!
|
141
|
+
logger.error "#{$!.inspect}"
|
142
|
+
logger.error "#{$!.backtrace.join("\n")}"
|
143
|
+
logger.error "response.body: •#{response.body}•"
|
144
|
+
end
|
145
|
+
end
|
146
|
+
when 0:
|
147
|
+
logger.debug "**** Twitter Timeout (#{response.code}) for #{data_input}."
|
148
|
+
logger.debug "Response body: #{response.body}"
|
149
|
+
if timeout_retries < MAX_RETRIES
|
150
|
+
timed_out_inputs.push data_input
|
151
|
+
else
|
152
|
+
json_results[data_input] = TwitterException.new(response.code, response.body)
|
153
|
+
end
|
154
|
+
when 400:
|
155
|
+
logger.debug "**** Twitter Rate Limit Exceeded (#{response.code}) for #{data_input}."
|
156
|
+
rate_limit_exceeded = true
|
157
|
+
json_results[data_input] = TwitterException.new(response.code, response.body)
|
158
|
+
when 401:
|
159
|
+
logger.debug "**** Twitter Authorization Failed (#{response.code}) for #{data_input}."
|
160
|
+
logger.debug "Request URL: #{request.url}"
|
161
|
+
json_results[data_input] = TwitterException.new(response.code, response.body)
|
162
|
+
when 404:
|
163
|
+
logger.debug "Unknown data_input: #{data_input}"
|
164
|
+
logger.debug "Request URL: #{request.url}"
|
165
|
+
json_results[data_input] = TwitterException.new(response.code, response.body)
|
166
|
+
when 502:
|
167
|
+
logger.debug "Twitter Over capacity (#{response.code}) for data_input: #{data_input}. Will retry."
|
168
|
+
logger.debug "Request URL: #{request.url}"
|
169
|
+
retries += 1
|
170
|
+
if retries < MAX_RETRIES
|
171
|
+
sleep_time = retries**2
|
172
|
+
logger.debug "Will retry after #{sleep_time} seconds."
|
173
|
+
sleep sleep_time
|
174
|
+
hydra.queue request
|
175
|
+
else
|
176
|
+
json_results[data_input] = TwitterException.new(response.code, response.body)
|
177
|
+
end
|
178
|
+
when 503:
|
179
|
+
logger.debug "Twitter Service Unavailable (#{response.code}) for data_input: #{data_input}. Will retry."
|
180
|
+
logger.debug "Request URL: #{request.url}"
|
181
|
+
retries += 1
|
182
|
+
if retries < MAX_RETRIES
|
183
|
+
sleep_time = retries**2
|
184
|
+
logger.debug "Will retry after #{sleep_time} seconds."
|
185
|
+
sleep sleep_time
|
186
|
+
hydra.queue request
|
187
|
+
else
|
188
|
+
json_results[data_input] = TwitterException.new(response.code, response.body)
|
189
|
+
end
|
190
|
+
when 500:
|
191
|
+
logger.debug "Twitter server error for data_input: #{data_input}. Will retry."
|
192
|
+
logger.debug "Request URL: #{request.url}"
|
193
|
+
retries += 1
|
194
|
+
if retries < MAX_RETRIES
|
195
|
+
sleep_time = retries**2
|
196
|
+
logger.debug "Will retry after #{sleep_time} seconds."
|
197
|
+
sleep sleep_time
|
198
|
+
hydra.queue request
|
199
|
+
else
|
200
|
+
json_results[data_input] = TwitterException.new(response.code, response.body)
|
201
|
+
end
|
202
|
+
else
|
203
|
+
logger.error "Unexpected HTTP result code: #{response.code}\n#{response.body}"
|
204
|
+
logger.error "Request URL: #{request.url}"
|
205
|
+
json_results[data_input] = TwitterException.new(response.code, response.body)
|
206
|
+
end
|
207
|
+
end
|
208
|
+
hydra.queue request
|
209
|
+
end
|
210
|
+
logger.debug "+++ Running Hydra."
|
211
|
+
hydra.run
|
212
|
+
logger.debug "--- Hydra run complete."
|
213
|
+
data_array = []
|
214
|
+
if timed_out_inputs.size > 0
|
215
|
+
logger.debug "#{timed_out_inputs.size} ERRORS encountered."
|
216
|
+
while !timed_out_inputs.empty?
|
217
|
+
failed_input = timed_out_inputs.pop
|
218
|
+
logger.debug "Reloading #{failed_input}"
|
219
|
+
data_array << failed_input
|
220
|
+
end
|
221
|
+
timeout_retries += 1
|
222
|
+
sleep_time = timeout_retries ** 2
|
223
|
+
logger.debug "Will retry after #{sleep_time} seconds."
|
224
|
+
sleep sleep_time
|
225
|
+
end
|
226
|
+
end
|
227
|
+
json_results
|
228
|
+
end
|
229
|
+
|
230
|
+
|
231
|
+
# Retrieves user profile data for a group of Twitter users.
|
232
|
+
# +twitter_id_array+ = An array twitter user ids, one for each user to get data for. Can be user_ids or screen_names.
|
233
|
+
# Returns a results Hash (see typho_twitter_batch)
|
234
|
+
def get_users_show twitter_id_array
|
235
|
+
typho_twitter_batch( twitter_id_array ) do |twitter_id|
|
236
|
+
if twitter_id.is_a? Fixnum
|
237
|
+
request = Typhoeus::Request.new("http://twitter.com/users/show.json?user_id=#{twitter_id}",
|
238
|
+
:headers => @headers,
|
239
|
+
:timeout => @request_timeout # miliseconds
|
240
|
+
)
|
241
|
+
else
|
242
|
+
request = Typhoeus::Request.new("http://twitter.com/users/show.json?screen_name=#{twitter_id}",
|
243
|
+
:timeout => @request_timeout, # miliseconds
|
244
|
+
:headers => @headers
|
245
|
+
)
|
246
|
+
end
|
247
|
+
request
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
# Retrieves the followers records for a group of Twitter users.
|
252
|
+
# +twitter_id_array+ = An array twitter user ids, one for each user to get data for
|
253
|
+
def get_statuses_followers twitter_id_array, limit=nil
|
254
|
+
master_results = {}
|
255
|
+
process_statuses_followers( twitter_id_array ) do |twitter_id, results|
|
256
|
+
master_results[twitter_id] ||= []
|
257
|
+
if results.is_a? TwitterException
|
258
|
+
master_results[twitter_id] = results
|
259
|
+
false
|
260
|
+
else
|
261
|
+
master_results[twitter_id] += results
|
262
|
+
if limit && master_results[twitter_id].length >= limit
|
263
|
+
master_results[twitter_id] = master_results[twitter_id].slice(0, limit)
|
264
|
+
continue = false
|
265
|
+
else
|
266
|
+
continue = true
|
267
|
+
end
|
268
|
+
logger.debug "#{twitter_id} - #{master_results[twitter_id].length} followers retrieved."
|
269
|
+
continue
|
270
|
+
end
|
271
|
+
end
|
272
|
+
master_results
|
273
|
+
end
|
274
|
+
|
275
|
+
# Retrieves the followers records for a group of twitter_ids from Twitter and feeds them to the supplied
|
276
|
+
# block one page at a time. The block passed is expected to return a true or false value. If it
|
277
|
+
# returns true, fetching of followers will continue for that twitter_id. If it returns false, fetching
|
278
|
+
# of followers will be aborted for that twitter_id only. This allows a batch of fetches to be started for
|
279
|
+
# multiple users. Fetching of individual user's followers may be aborted while continuing the others.
|
280
|
+
# +twitter_ids+ = An array twitter twitter_ids, one for each user to get data for.
|
281
|
+
#
|
282
|
+
# Returns nil.
|
283
|
+
#
|
284
|
+
# eg.
|
285
|
+
#
|
286
|
+
# process_statuses_followers( ['bdoughty', 'joshuabaer'] ) do |twitter_id, followers|
|
287
|
+
# puts "Twitter user #{twitter_id}"
|
288
|
+
# continue = true
|
289
|
+
# followers.each do |follower|
|
290
|
+
# continue = false if follower[:twitter_id] == 'needle'
|
291
|
+
# end
|
292
|
+
# continue
|
293
|
+
# end
|
294
|
+
def process_statuses_followers twitter_id_array, &block
|
295
|
+
|
296
|
+
raise "You must supply a block to this method." if !block_given?
|
297
|
+
# Track the proper Twitter API cursor for each twitter_id. Twitter requests an initial cursor of -1 (to begin paging)
|
298
|
+
cursor_tracker = {}
|
299
|
+
twitter_id_array.each do |twitter_id|
|
300
|
+
cursor_tracker[twitter_id] = -1
|
301
|
+
end
|
302
|
+
|
303
|
+
while( cursor_tracker.size > 0 )
|
304
|
+
twitter_results = typho_twitter_batch( cursor_tracker.keys ) do |twitter_id|
|
305
|
+
if twitter_id.is_a? Fixnum
|
306
|
+
request = Typhoeus::Request.new("http://twitter.com/statuses/followers.json?cursor=#{cursor_tracker[twitter_id]}&user_id=#{twitter_id}",
|
307
|
+
:headers => @headers,
|
308
|
+
:timeout => @request_timeout
|
309
|
+
)
|
310
|
+
else
|
311
|
+
request = Typhoeus::Request.new("http://twitter.com/statuses/followers.json?cursor=#{cursor_tracker[twitter_id]}&screen_name=#{twitter_id}",
|
312
|
+
:timeout => @request_timeout,
|
313
|
+
:headers => @headers
|
314
|
+
)
|
315
|
+
end
|
316
|
+
end
|
317
|
+
cursor_tracker = {}
|
318
|
+
twitter_results.each do |twitter_id, results|
|
319
|
+
next_cursor = 0
|
320
|
+
if results.is_a?( Hash ) && results['users'] && results['users'].length > 0
|
321
|
+
next_cursor = results["next_cursor"]
|
322
|
+
continue = yield( twitter_id, results['users'] )
|
323
|
+
else
|
324
|
+
continue = yield( twitter_id, results ) # return the exception
|
325
|
+
end
|
326
|
+
if next_cursor != 0 && continue
|
327
|
+
cursor_tracker[twitter_id] = next_cursor
|
328
|
+
else
|
329
|
+
cursor_tracker.delete( twitter_id ) # remove the twitter_id from processing
|
330
|
+
end
|
331
|
+
end
|
332
|
+
end
|
333
|
+
|
334
|
+
nil
|
335
|
+
end
|
336
|
+
|
337
|
+
# Retrieves the followers ids for a group of twitter_ids from Twitter and feeds them to the supplied
|
338
|
+
# block one page at a time. The block passed is expected to return a true or false value. If it
|
339
|
+
# returns true, fetching of follower ids will continue for that twitter_id. If it returns false, fetching
|
340
|
+
# of follower ids will be aborted for that twitter_id only. This allows a batch of fetches to be started for
|
341
|
+
# multiple users. Fetching of individual user's followers ids may be aborted while continuing the others.
|
342
|
+
# +twitter_ids+ = An array twitter twitter_ids, one for each user to get data for.
|
343
|
+
#
|
344
|
+
# Returns nil.
|
345
|
+
#
|
346
|
+
# eg.
|
347
|
+
#
|
348
|
+
# process_followers_ids( ['bdoughty', 'joshuabaer'] ) do |twitter_id, follower_ids|
|
349
|
+
# puts "Twitter user #{twitter_id}"
|
350
|
+
# continue = true
|
351
|
+
# follower_ids.each do |follower_id|
|
352
|
+
# continue = false if follower_id == SOME_TWITTER_USER_ID
|
353
|
+
# end
|
354
|
+
# continue
|
355
|
+
# end
|
356
|
+
def process_followers_ids twitter_id_array, &block
|
357
|
+
|
358
|
+
raise "You must supply a block to this method." if !block_given?
|
359
|
+
# Track the proper Twitter API cursor for each twitter_id. Twitter requests an initial cursor of -1 (to begin paging)
|
360
|
+
cursor_tracker = {}
|
361
|
+
twitter_id_array.each do |twitter_id|
|
362
|
+
cursor_tracker[twitter_id] = -1
|
363
|
+
end
|
364
|
+
|
365
|
+
while( cursor_tracker.size > 0 )
|
366
|
+
twitter_results = typho_twitter_batch( cursor_tracker.keys ) do |twitter_id|
|
367
|
+
if twitter_id.is_a? Fixnum
|
368
|
+
request = Typhoeus::Request.new("http://twitter.com/followers/ids.json?cursor=#{cursor_tracker[twitter_id]}&user_id=#{twitter_id}",
|
369
|
+
:headers => @headers,
|
370
|
+
:timeout => @request_timeout
|
371
|
+
)
|
372
|
+
else
|
373
|
+
request = Typhoeus::Request.new("http://twitter.com/followers/ids.json?cursor=#{cursor_tracker[twitter_id]}&screen_name=#{twitter_id}",
|
374
|
+
:headers => @headers,
|
375
|
+
:timeout => @request_timeout
|
376
|
+
)
|
377
|
+
end
|
378
|
+
end
|
379
|
+
cursor_tracker = {}
|
380
|
+
twitter_results.each do |twitter_id, results|
|
381
|
+
next_cursor = 0
|
382
|
+
if results.is_a?( Hash ) && results['ids'] && results['ids'].length > 0
|
383
|
+
next_cursor = results["next_cursor"]
|
384
|
+
continue = yield( twitter_id, results['ids'] )
|
385
|
+
else
|
386
|
+
continue = yield( twitter_id, results ) # return the exception
|
387
|
+
end
|
388
|
+
if next_cursor != 0 && continue
|
389
|
+
cursor_tracker[twitter_id] = next_cursor
|
390
|
+
else
|
391
|
+
cursor_tracker.delete( twitter_id ) # remove the twitter_id from processing
|
392
|
+
end
|
393
|
+
end
|
394
|
+
end
|
395
|
+
|
396
|
+
nil
|
397
|
+
end
|
398
|
+
|
399
|
+
# Retrieves all timeline updates for a group of twitter_ids from Twitter.
|
400
|
+
# This method calls process_statuses_user_timeline() with a block to aggregate the updates.
|
401
|
+
# +twitter_id_array+ = An array twitter user ids, one for each user to get data for
|
402
|
+
# Returns aggregated updates as a Hash with twitter_ids as keys, and arrays of updates as values.
|
403
|
+
# If an unresolvable exception occurred fetching a particular twitter_id, then the resulting TwitterException
|
404
|
+
# is returned for that screen name instead of an array of updates.
|
405
|
+
def get_statuses_user_timeline twitter_id_array
|
406
|
+
master_results = {}
|
407
|
+
process_statuses_user_timeline( twitter_id_array ) do |twitter_id, results|
|
408
|
+
master_results[twitter_id] ||= []
|
409
|
+
if results.is_a? TwitterException
|
410
|
+
master_results[twitter_id] = results
|
411
|
+
false
|
412
|
+
else
|
413
|
+
master_results[twitter_id] += results
|
414
|
+
true
|
415
|
+
end
|
416
|
+
end
|
417
|
+
master_results
|
418
|
+
end
|
419
|
+
|
420
|
+
# Retrieves the timeline updates for a group of twitter_ids from Twitter and feeds them to the supplied
|
421
|
+
# block one page at a time. The block passed is expected to return a true or false value. If it
|
422
|
+
# returns true, fetching of updates will continue for that twitter_id. If it returns false, fetching
|
423
|
+
# of updates will be aborted for that twitter_id only. This allows a batch of fetches to be started for
|
424
|
+
# multiple users. Fetching of individual user's updates may be aborted while continuing the others.
|
425
|
+
# +twitter_ids+ = An array twitter user ids, one for each user to get data for.
|
426
|
+
#
|
427
|
+
# Returns nil.
|
428
|
+
#
|
429
|
+
# eg.
|
430
|
+
#
|
431
|
+
# process_statuses_user_timeline( ['bdoughty', 'joshuabaer'] ) do |twitter_id, updates|
|
432
|
+
# puts "Twitter user #{twitter_id}"
|
433
|
+
# updates.each do |update|
|
434
|
+
# # do something with each status update
|
435
|
+
# end
|
436
|
+
# (twitter_id == 'bdoughty') # block return value - aborts 'joshuabaer' after the first page, continues 'bdoughty'
|
437
|
+
# end
|
438
|
+
def process_statuses_user_timeline twitter_ids, &block
|
439
|
+
page = 0
|
440
|
+
count = 200
|
441
|
+
while twitter_ids.length > 0
|
442
|
+
page += 1 # Twitter starts with page 1
|
443
|
+
logger.debug "Getting page #{page} for timelines."
|
444
|
+
twitter_results = typho_twitter_batch( twitter_ids ) do |twitter_id|
|
445
|
+
if twitter_id.is_a? Fixnum
|
446
|
+
request = Typhoeus::Request.new("http://twitter.com/statuses/user_timeline.json?user_id=#{twitter_id}&page=#{page}&count=#{count}",
|
447
|
+
:headers => @headers,
|
448
|
+
:timeout => @request_timeout
|
449
|
+
)
|
450
|
+
else
|
451
|
+
request = Typhoeus::Request.new("http://twitter.com/statuses/user_timeline.json?screen_name=#{twitter_id}&page=#{page}&count=#{count}",
|
452
|
+
:headers => @headers,
|
453
|
+
:timeout => @request_timeout
|
454
|
+
)
|
455
|
+
end
|
456
|
+
end
|
457
|
+
|
458
|
+
twitter_ids = []
|
459
|
+
twitter_results.each do |twitter_id, results|
|
460
|
+
if results && !( results.respond_to?( :length ) && results.length == 0 )
|
461
|
+
if block_given?
|
462
|
+
continue = yield( twitter_id, results )
|
463
|
+
else
|
464
|
+
raise "You must supply a block to this method."
|
465
|
+
end
|
466
|
+
# keep fetching for this twitter_id only if the block said to and there are more updates.
|
467
|
+
twitter_ids << twitter_id if continue && !results.is_a?( TwitterException ) && results.length != 0
|
468
|
+
end
|
469
|
+
end
|
470
|
+
end
|
471
|
+
nil
|
472
|
+
end
|
473
|
+
|
474
|
+
end
|
data/spec/spec.opts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
2
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
3
|
+
require 'typho-twitter'
|
4
|
+
require 'spec'
|
5
|
+
require 'spec/autorun'
|
6
|
+
|
7
|
+
module SpecLogger
|
8
|
+
def log message
|
9
|
+
puts message.to_s + "<br/>"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
Spec::Runner.configure do |config|
|
14
|
+
|
15
|
+
end
|
@@ -0,0 +1,243 @@
|
|
1
|
+
# A brutally simple test suite to verify desired functionality for +my+ application.
|
2
|
+
# TODO: Generalize, and make unit tests.
|
3
|
+
# NOTE: These tests are meant to be run against a white-listed account.
|
4
|
+
# To authorize with Twitter, you need to copy the oauth.yaml.example to oauth.yaml
|
5
|
+
# and populate it with the OAuth credentials for a white-listed account.
|
6
|
+
|
7
|
+
BASE_DIR="#{File.dirname(__FILE__)}/../lib"
|
8
|
+
$: << BASE_DIR
|
9
|
+
require 'typho-twitter'
|
10
|
+
require 'test/unit'
|
11
|
+
include Test::Unit::Assertions
|
12
|
+
include WDD::Utils
|
13
|
+
|
14
|
+
UNKNOWN_ID = 'mixtercox'
|
15
|
+
TEST_USER_IDS = [
|
16
|
+
UNKNOWN_ID,
|
17
|
+
"bdoughty",
|
18
|
+
"joshuabaer",
|
19
|
+
"jotto",
|
20
|
+
"hoonpark",
|
21
|
+
"aplusk",
|
22
|
+
"barackobama",
|
23
|
+
"oprah",
|
24
|
+
"damon",
|
25
|
+
"TreyPlaysTunes",
|
26
|
+
"fiveredwoods",
|
27
|
+
"austinonrails",
|
28
|
+
"remlap42",
|
29
|
+
]
|
30
|
+
|
31
|
+
# basic test that all is functioning and that we can lookup an array of screen_names successfully
|
32
|
+
def test_users_show_multi
|
33
|
+
screen_names = TEST_USER_IDS
|
34
|
+
screen_name_array = screen_names
|
35
|
+
# 10.times do |i|
|
36
|
+
# screen_name_array += screen_names.map{|c| c+i}
|
37
|
+
# end
|
38
|
+
twitter = TyphoTwitter.new( @oauth_options )
|
39
|
+
# responses = twitter.typho_twitter_batch screen_name_array
|
40
|
+
responses = twitter.get_users_show( screen_name_array )
|
41
|
+
responses.each do |key, value|
|
42
|
+
puts "#{key} => "
|
43
|
+
if key == UNKNOWN_ID
|
44
|
+
assert_instance_of( TyphoTwitter::TwitterException, value )
|
45
|
+
end
|
46
|
+
puts "#{value}"
|
47
|
+
end
|
48
|
+
puts "# Responses: #{responses.size}"
|
49
|
+
assert_equal( responses.size, screen_name_array.size )
|
50
|
+
end
|
51
|
+
|
52
|
+
# Verify that things still function correctly if we are only looking up one user.
|
53
|
+
def test_users_show_single
|
54
|
+
screen_names = [
|
55
|
+
"bdoughty"
|
56
|
+
]
|
57
|
+
screen_name_array = screen_names
|
58
|
+
# screen_name_array += screen_names
|
59
|
+
twitter = TyphoTwitter.new( @oauth_options )
|
60
|
+
responses = twitter.get_users_show( screen_name_array )
|
61
|
+
responses.each do |key, value|
|
62
|
+
puts "#{key} => "
|
63
|
+
puts "#{value}"
|
64
|
+
end
|
65
|
+
puts "# Responses: #{responses.size}"
|
66
|
+
assert_equal( responses.size, screen_name_array.size )
|
67
|
+
end
|
68
|
+
|
69
|
+
# Test getting timelines passing a block to process_statuses_user_timeline.
|
70
|
+
def test_process_statuses_user_timeline
|
71
|
+
screen_names = TEST_USER_IDS
|
72
|
+
screen_name_array = screen_names
|
73
|
+
# screen_name_array += screen_names
|
74
|
+
twitter = TyphoTwitter.new( @oauth_options )
|
75
|
+
user_updates = {}
|
76
|
+
twitter.process_statuses_user_timeline( screen_name_array ) do |screen_name, updates|
|
77
|
+
if screen_name == UNKNOWN_ID
|
78
|
+
assert_instance_of( TyphoTwitter::TwitterException, updates )
|
79
|
+
puts "Verified that #{UNKNOWN_ID} is unknown."
|
80
|
+
false
|
81
|
+
else
|
82
|
+
user_updates[screen_name] ||= []
|
83
|
+
user_updates[screen_name] += updates
|
84
|
+
true
|
85
|
+
end
|
86
|
+
end
|
87
|
+
user_updates.each do |key, value|
|
88
|
+
puts "#{key} => "
|
89
|
+
puts value.length
|
90
|
+
end
|
91
|
+
# puts "# Responses: #{responses.size}"
|
92
|
+
assert_equal( user_updates.size, screen_name_array.size - 1)
|
93
|
+
end
|
94
|
+
|
95
|
+
# Test getting timelines from get_statuses_user_timeline
|
96
|
+
def test_get_statuses_user_timeline
|
97
|
+
screen_names = TEST_USER_IDS
|
98
|
+
screen_name_array = screen_names
|
99
|
+
# screen_name_array += screen_names
|
100
|
+
twitter = TyphoTwitter.new( @oauth_options )
|
101
|
+
user_updates = twitter.get_statuses_user_timeline( screen_name_array )
|
102
|
+
user_updates.each do |key, value|
|
103
|
+
puts "#{key} => "
|
104
|
+
if key == UNKNOWN_ID
|
105
|
+
assert_instance_of( TyphoTwitter::TwitterException, value )
|
106
|
+
puts "Verified that #{UNKNOWN_ID} is unknown."
|
107
|
+
else
|
108
|
+
case value.class.to_s
|
109
|
+
when 'TyphoTwitter::TwitterException'
|
110
|
+
puts value.inspect
|
111
|
+
else
|
112
|
+
puts value.length
|
113
|
+
end
|
114
|
+
end
|
115
|
+
# pp value
|
116
|
+
end
|
117
|
+
# puts "# Responses: #{responses.size}"
|
118
|
+
assert_equal( user_updates.size, screen_name_array.size )
|
119
|
+
end
|
120
|
+
|
121
|
+
# Test getting followers
|
122
|
+
def test_get_statuses_followers
|
123
|
+
# screen_names = ['hoonpark', 'bdoughty', 'jotto']
|
124
|
+
screen_names = ['bdoughty']
|
125
|
+
screen_name_array = screen_names
|
126
|
+
# screen_name_array += screen_names
|
127
|
+
twitter = TyphoTwitter.new( @oauth_options )
|
128
|
+
followers = twitter.get_statuses_followers( screen_name_array )
|
129
|
+
followers.each do |screen_name, results|
|
130
|
+
puts "#{screen_name}:"
|
131
|
+
puts "#{results.length} followers:"
|
132
|
+
results.each do |follower|
|
133
|
+
printvar :follower, follower
|
134
|
+
end
|
135
|
+
# pp value
|
136
|
+
end
|
137
|
+
assert_equal( followers.size, screen_name_array.size )
|
138
|
+
|
139
|
+
screen_names = ['basdkhasdf']
|
140
|
+
screen_name_array = screen_names
|
141
|
+
# screen_name_array += screen_names
|
142
|
+
twitter = TyphoTwitter.new( @oauth_options )
|
143
|
+
followers = twitter.get_statuses_followers( screen_name_array )
|
144
|
+
followers.each do |screen_name, results|
|
145
|
+
puts "#{screen_name}:"
|
146
|
+
assert_instance_of TyphoTwitter::TwitterException, results
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
# Test getting followers
|
151
|
+
def test_get_statuses_followers_with_limit
|
152
|
+
# screen_names = ['hoonpark', 'bdoughty', 'jotto']
|
153
|
+
screen_names = ['joshuabaer']
|
154
|
+
screen_name_array = screen_names
|
155
|
+
# screen_name_array += screen_names
|
156
|
+
twitter = TyphoTwitter.new( @oauth_options )
|
157
|
+
followers_data = twitter.get_statuses_followers( screen_name_array, 100 )
|
158
|
+
followers_data.each do |screen_name, followers|
|
159
|
+
puts "#{screen_name}:"
|
160
|
+
puts "#{followers.length} followers:"
|
161
|
+
followers.each do |follower|
|
162
|
+
printvar :follower, follower['screen_name']
|
163
|
+
end
|
164
|
+
assert_equal( followers.size <= 100, true )
|
165
|
+
# pp value
|
166
|
+
end
|
167
|
+
assert_equal( followers_data.size, screen_name_array.size )
|
168
|
+
|
169
|
+
screen_names = ['basdkhasdf']
|
170
|
+
screen_name_array = screen_names
|
171
|
+
# screen_name_array += screen_names
|
172
|
+
twitter = TyphoTwitter.new( @oauth_options )
|
173
|
+
followers = twitter.get_statuses_followers( screen_name_array )
|
174
|
+
followers.each do |screen_name, results|
|
175
|
+
puts "#{screen_name}:"
|
176
|
+
assert_instance_of TyphoTwitter::TwitterException, results
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
# Test getting timelines passing a block to process_statuses_user_timeline. Process the updates and abort
|
181
|
+
# on condition
|
182
|
+
def test_process_statuses_user_timeline_with_abort
|
183
|
+
screen_names = TEST_USER_IDS
|
184
|
+
screen_name_array = screen_names
|
185
|
+
# screen_name_array += screen_names
|
186
|
+
twitter = TyphoTwitter.new( @oauth_options )
|
187
|
+
user_updates = {}
|
188
|
+
twitter.process_statuses_user_timeline( screen_name_array ) do |screen_name, results|
|
189
|
+
puts "#{screen_name} =>"
|
190
|
+
continue = true
|
191
|
+
if results.is_a? TyphoTwitter::TwitterException
|
192
|
+
puts "Exception for #{screen_name}: #{results.inspect}"
|
193
|
+
continue = false
|
194
|
+
else
|
195
|
+
results.each do |update|
|
196
|
+
# puts update["text"]
|
197
|
+
if update["text"] =~ /otherinbox/i
|
198
|
+
puts "ABORTING #{screen_name} because of tweet:"
|
199
|
+
puts update["text"]
|
200
|
+
continue = false
|
201
|
+
break
|
202
|
+
end
|
203
|
+
end
|
204
|
+
user_updates[screen_name] ||= []
|
205
|
+
user_updates[screen_name] += results
|
206
|
+
end
|
207
|
+
|
208
|
+
continue
|
209
|
+
end
|
210
|
+
user_updates.each do |key, value|
|
211
|
+
puts "#{key} => "
|
212
|
+
puts value.length
|
213
|
+
# pp value
|
214
|
+
end
|
215
|
+
# puts "# Responses: #{responses.size}"
|
216
|
+
assert_equal( user_updates.size, screen_name_array.size - 1 )
|
217
|
+
end
|
218
|
+
|
219
|
+
def time_it method_id
|
220
|
+
et = WDD::Utils::elapsed_time do
|
221
|
+
self.send( method_id )
|
222
|
+
end
|
223
|
+
puts "========================================"
|
224
|
+
puts "#{method_id.to_s} - #{et} seconds"
|
225
|
+
puts
|
226
|
+
end
|
227
|
+
|
228
|
+
def run_tests
|
229
|
+
@oauth_options = YAML.load_file(File.dirname($0)+'/oauth.yaml')
|
230
|
+
if ARGV[0] && ARGV[0] != ""
|
231
|
+
eval ARGV[0]
|
232
|
+
else
|
233
|
+
time_it :test_users_show_multi
|
234
|
+
time_it :test_users_show_single
|
235
|
+
time_it :test_process_statuses_user_timeline
|
236
|
+
time_it :test_get_statuses_user_timeline
|
237
|
+
time_it :test_get_statuses_followers
|
238
|
+
time_it :test_get_statuses_followers_with_limit
|
239
|
+
time_it :test_process_statuses_user_timeline_with_abort
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
run_tests
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{typho-twitter}
|
8
|
+
s.version = "0.2.1"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["shock"]
|
12
|
+
s.date = %q{2011-01-16}
|
13
|
+
s.description = %q{A Twitter client for performing a batch of Twitter calls in parallel.}
|
14
|
+
s.email = %q{billdoughty@capitalthought.com}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"LICENSE",
|
17
|
+
"README.rdoc"
|
18
|
+
]
|
19
|
+
s.files = [
|
20
|
+
".document",
|
21
|
+
".gitignore",
|
22
|
+
"LICENSE",
|
23
|
+
"README.rdoc",
|
24
|
+
"Rakefile",
|
25
|
+
"VERSION",
|
26
|
+
"lib/typho-twitter.rb",
|
27
|
+
"lib/typho-twitter/typho-twitter.rb",
|
28
|
+
"spec/spec.opts",
|
29
|
+
"spec/spec_helper.rb",
|
30
|
+
"test/oauth.yaml.example",
|
31
|
+
"test/typho-twitter-test.rb",
|
32
|
+
"typho-twitter.gemspec"
|
33
|
+
]
|
34
|
+
s.homepage = %q{http://github.com/capitalthought/typho-twitter}
|
35
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
36
|
+
s.require_paths = ["lib"]
|
37
|
+
s.rubygems_version = %q{1.3.7}
|
38
|
+
s.summary = %q{Parallel twitter client using Typhoeus.}
|
39
|
+
s.test_files = [
|
40
|
+
"spec/spec_helper.rb",
|
41
|
+
"test/typho-twitter-test.rb"
|
42
|
+
]
|
43
|
+
|
44
|
+
if s.respond_to? :specification_version then
|
45
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
46
|
+
s.specification_version = 3
|
47
|
+
|
48
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
49
|
+
s.add_development_dependency(%q<rspec>, [">= 1.2.9"])
|
50
|
+
s.add_runtime_dependency(%q<wdd-ruby-ext>, [">= 0.3.1"])
|
51
|
+
s.add_runtime_dependency(%q<oauth>, [">= 0.4.3"])
|
52
|
+
else
|
53
|
+
s.add_dependency(%q<rspec>, [">= 1.2.9"])
|
54
|
+
s.add_dependency(%q<wdd-ruby-ext>, [">= 0.3.1"])
|
55
|
+
s.add_dependency(%q<oauth>, [">= 0.4.3"])
|
56
|
+
end
|
57
|
+
else
|
58
|
+
s.add_dependency(%q<rspec>, [">= 1.2.9"])
|
59
|
+
s.add_dependency(%q<wdd-ruby-ext>, [">= 0.3.1"])
|
60
|
+
s.add_dependency(%q<oauth>, [">= 0.4.3"])
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
metadata
ADDED
@@ -0,0 +1,128 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: typho-twitter
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 21
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 2
|
9
|
+
- 1
|
10
|
+
version: 0.2.1
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- shock
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-01-16 00:00:00 -06:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: rspec
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: 13
|
30
|
+
segments:
|
31
|
+
- 1
|
32
|
+
- 2
|
33
|
+
- 9
|
34
|
+
version: 1.2.9
|
35
|
+
type: :development
|
36
|
+
version_requirements: *id001
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
name: wdd-ruby-ext
|
39
|
+
prerelease: false
|
40
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ">="
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
hash: 17
|
46
|
+
segments:
|
47
|
+
- 0
|
48
|
+
- 3
|
49
|
+
- 1
|
50
|
+
version: 0.3.1
|
51
|
+
type: :runtime
|
52
|
+
version_requirements: *id002
|
53
|
+
- !ruby/object:Gem::Dependency
|
54
|
+
name: oauth
|
55
|
+
prerelease: false
|
56
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
hash: 9
|
62
|
+
segments:
|
63
|
+
- 0
|
64
|
+
- 4
|
65
|
+
- 3
|
66
|
+
version: 0.4.3
|
67
|
+
type: :runtime
|
68
|
+
version_requirements: *id003
|
69
|
+
description: A Twitter client for performing a batch of Twitter calls in parallel.
|
70
|
+
email: billdoughty@capitalthought.com
|
71
|
+
executables: []
|
72
|
+
|
73
|
+
extensions: []
|
74
|
+
|
75
|
+
extra_rdoc_files:
|
76
|
+
- LICENSE
|
77
|
+
- README.rdoc
|
78
|
+
files:
|
79
|
+
- .document
|
80
|
+
- .gitignore
|
81
|
+
- LICENSE
|
82
|
+
- README.rdoc
|
83
|
+
- Rakefile
|
84
|
+
- VERSION
|
85
|
+
- lib/typho-twitter.rb
|
86
|
+
- lib/typho-twitter/typho-twitter.rb
|
87
|
+
- spec/spec.opts
|
88
|
+
- spec/spec_helper.rb
|
89
|
+
- test/oauth.yaml.example
|
90
|
+
- test/typho-twitter-test.rb
|
91
|
+
- typho-twitter.gemspec
|
92
|
+
has_rdoc: true
|
93
|
+
homepage: http://github.com/capitalthought/typho-twitter
|
94
|
+
licenses: []
|
95
|
+
|
96
|
+
post_install_message:
|
97
|
+
rdoc_options:
|
98
|
+
- --charset=UTF-8
|
99
|
+
require_paths:
|
100
|
+
- lib
|
101
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
102
|
+
none: false
|
103
|
+
requirements:
|
104
|
+
- - ">="
|
105
|
+
- !ruby/object:Gem::Version
|
106
|
+
hash: 3
|
107
|
+
segments:
|
108
|
+
- 0
|
109
|
+
version: "0"
|
110
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
111
|
+
none: false
|
112
|
+
requirements:
|
113
|
+
- - ">="
|
114
|
+
- !ruby/object:Gem::Version
|
115
|
+
hash: 3
|
116
|
+
segments:
|
117
|
+
- 0
|
118
|
+
version: "0"
|
119
|
+
requirements: []
|
120
|
+
|
121
|
+
rubyforge_project:
|
122
|
+
rubygems_version: 1.3.7
|
123
|
+
signing_key:
|
124
|
+
specification_version: 3
|
125
|
+
summary: Parallel twitter client using Typhoeus.
|
126
|
+
test_files:
|
127
|
+
- spec/spec_helper.rb
|
128
|
+
- test/typho-twitter-test.rb
|