flamingo 0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/README.md +73 -0
- data/Rakefile +3 -0
- data/bin/flamingo +20 -0
- data/bin/flamingo-web +8 -0
- data/bin/flamingod +63 -0
- data/examples/Rakefile +15 -0
- data/examples/flamingo.yml +10 -0
- data/lib/flamingo.rb +151 -0
- data/lib/flamingo/config.rb +52 -0
- data/lib/flamingo/daemon/child_process.rb +16 -0
- data/lib/flamingo/daemon/dispatcher_process.rb +15 -0
- data/lib/flamingo/daemon/flamingod.rb +113 -0
- data/lib/flamingo/daemon/pid_file.rb +48 -0
- data/lib/flamingo/daemon/wader_process.rb +28 -0
- data/lib/flamingo/daemon/web_server_process.rb +12 -0
- data/lib/flamingo/dispatch_error.rb +11 -0
- data/lib/flamingo/dispatch_event.rb +40 -0
- data/lib/flamingo/logging/formatter.rb +12 -0
- data/lib/flamingo/stream.rb +62 -0
- data/lib/flamingo/stream_params.rb +74 -0
- data/lib/flamingo/subscription.rb +39 -0
- data/lib/flamingo/version.rb +3 -0
- data/lib/flamingo/wader.rb +57 -0
- data/lib/flamingo/web/server.rb +129 -0
- metadata +204 -0
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2010 Hayes Davis
|
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.md
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
Flamingo
|
2
|
+
========
|
3
|
+
Flamingo is a resque-based system for handling the Twitter Streaming API.
|
4
|
+
|
5
|
+
This is *early alpha* code. There will be a lot of change and things like tests
|
6
|
+
coming in the future. That said, it does work so give it a try if you have the
|
7
|
+
need.
|
8
|
+
|
9
|
+
Dependencies
|
10
|
+
------------
|
11
|
+
* redis
|
12
|
+
* resque
|
13
|
+
* sinatra
|
14
|
+
* twitter-stream
|
15
|
+
* yajl-ruby
|
16
|
+
|
17
|
+
By default, the `resque` gem installs the latest 2.x `redis` gem, so if
|
18
|
+
you are using Redis 1.x, you may want to swap it out.
|
19
|
+
|
20
|
+
$ gem list | grep redis
|
21
|
+
redis (2.0.3)
|
22
|
+
$ gem remove redis --version=2.0.3 -V
|
23
|
+
|
24
|
+
$ gem install redis --version=1.0.7
|
25
|
+
$ gem list | grep redis
|
26
|
+
redis (1.0.7)
|
27
|
+
|
28
|
+
Getting Started
|
29
|
+
---------------
|
30
|
+
1. Install the gem
|
31
|
+
sudo gem install flamingo
|
32
|
+
|
33
|
+
2. Create a config file (see `examples/flamingo.yml`) with at least a username and password
|
34
|
+
|
35
|
+
username: USERNAME
|
36
|
+
password: PASSWORD
|
37
|
+
stream: filter
|
38
|
+
logging:
|
39
|
+
dest: /YOUR/LOG/PATH.LOG
|
40
|
+
level: LOGLEVEL
|
41
|
+
|
42
|
+
`LOGLEVEL` is one of the following:
|
43
|
+
`DEBUG` < `INFO` < `WARN` < `ERROR` < `FATAL` < `UNKNOWN`
|
44
|
+
|
45
|
+
3. Start the Redis server
|
46
|
+
|
47
|
+
$ redis-server
|
48
|
+
|
49
|
+
4. Configure tracking using `flamingo` client (installed during `gem install`)
|
50
|
+
|
51
|
+
$ flamingo
|
52
|
+
>> s = Stream.get(:filter)
|
53
|
+
>> s.params[:track] = %w(FOO BAR BAZ)
|
54
|
+
>> Subscription.new('YOUR_QUEUE').save
|
55
|
+
|
56
|
+
5. Start the Flamingo Daemon (`flamingod` installed during `gem install`)
|
57
|
+
|
58
|
+
$ flamingod -c your/config/file.yml
|
59
|
+
|
60
|
+
|
61
|
+
6. Consume events with a resque worker
|
62
|
+
|
63
|
+
class HandleFlamingoEvent
|
64
|
+
|
65
|
+
# type: One of "tweet" or "delete"
|
66
|
+
# event: a hash of the json data from twitter
|
67
|
+
def self.perform(type,event)
|
68
|
+
# Do stuff with the data
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
|
73
|
+
$ QUEUE=YOUR_QUEUE rake resque:work
|
data/Rakefile
ADDED
data/bin/flamingo
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
|
4
|
+
require 'flamingo'
|
5
|
+
|
6
|
+
include Flamingo
|
7
|
+
|
8
|
+
puts "Flamingo client #{Flamingo::VERSION}"
|
9
|
+
|
10
|
+
begin
|
11
|
+
Flamingo.configure!(ARGV[0])
|
12
|
+
rescue => e
|
13
|
+
$stderr.puts "Could not start: #{e.message}"
|
14
|
+
exit(-1)
|
15
|
+
end
|
16
|
+
|
17
|
+
ARGV.clear # Remove args so IRB doesn't try to load them
|
18
|
+
|
19
|
+
require 'irb'
|
20
|
+
IRB.start
|
data/bin/flamingo-web
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
|
4
|
+
require 'flamingo'
|
5
|
+
|
6
|
+
Flamingo.configure!(ARGV[0])
|
7
|
+
host,port = Flamingo.config.web.host('0.0.0.0:4711').split(":")
|
8
|
+
Flamingo::Web::Server.run! :host=>host, :port=>port.to_i
|
data/bin/flamingod
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
|
4
|
+
require 'flamingo'
|
5
|
+
require 'optparse'
|
6
|
+
|
7
|
+
puts "Starting flamingod #{Flamingo::VERSION}"
|
8
|
+
|
9
|
+
begin
|
10
|
+
options = {:daemonize => nil, :config => nil}
|
11
|
+
|
12
|
+
opts = OptionParser.new do |opts|
|
13
|
+
opts.banner = <<-EOF
|
14
|
+
Usage:
|
15
|
+
flamingod [<options>]
|
16
|
+
EOF
|
17
|
+
|
18
|
+
opts.on("-cCONFIG", "--config-file=CONFIG", "Configuration File") do |x|
|
19
|
+
options[:config_file] = x
|
20
|
+
end
|
21
|
+
|
22
|
+
opts.on("-d", "--daemonize", "Run in Background (Daemonize)") do |x|
|
23
|
+
options[:daemonize] = true
|
24
|
+
end
|
25
|
+
|
26
|
+
opts.on("-b", "--background", "Run in Background (Daemonize)") do |x|
|
27
|
+
options[:daemonize] = true
|
28
|
+
end
|
29
|
+
|
30
|
+
opts.on("-f", "--foreground", "Run in Foreground (default)") do |x|
|
31
|
+
options[:daemonize] = nil
|
32
|
+
end
|
33
|
+
|
34
|
+
opts.on("-pPID_FILE", "--pid-file=PID_FILE", "PID File (only when daemonized)") do |x|
|
35
|
+
options[:pid_file] = x
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
opts.parse!
|
41
|
+
|
42
|
+
Flamingo.configure!(options[:config_file])
|
43
|
+
if options[:pid_file]
|
44
|
+
Flamingo.config.pid_file = File.expand_path(options[:pid_file])
|
45
|
+
end
|
46
|
+
|
47
|
+
rescue => e
|
48
|
+
$stderr.puts "Could not start: #{e.message}"
|
49
|
+
exit(-1)
|
50
|
+
end
|
51
|
+
|
52
|
+
flamingod = Flamingo::Daemon::Flamingod.new
|
53
|
+
if options[:daemonize]
|
54
|
+
begin
|
55
|
+
pid = flamingod.run_as_daemon()
|
56
|
+
puts "flamingod process #{pid} started"
|
57
|
+
rescue => e
|
58
|
+
$stderr.puts "Could not start: #{e.message}"
|
59
|
+
exit(-2)
|
60
|
+
end
|
61
|
+
else
|
62
|
+
flamingod.run()
|
63
|
+
end
|
data/examples/Rakefile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# Simple example that reads from a subscription queue and writes the events
|
2
|
+
# to STDOUT
|
3
|
+
# Usage (from this directory):
|
4
|
+
# $ QUEUE=YOUR_QUEUE rake resque:work
|
5
|
+
|
6
|
+
require 'rubygems'
|
7
|
+
require 'resque/tasks'
|
8
|
+
|
9
|
+
class HandleFlamingoEvent
|
10
|
+
|
11
|
+
def self.perform(type,event)
|
12
|
+
puts type, event
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
data/lib/flamingo.rb
ADDED
@@ -0,0 +1,151 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'redis/namespace'
|
3
|
+
require 'twitter/json_stream'
|
4
|
+
require 'resque'
|
5
|
+
require 'logger'
|
6
|
+
require 'yaml'
|
7
|
+
require 'erb'
|
8
|
+
require 'cgi'
|
9
|
+
require 'active_support'
|
10
|
+
require 'sinatra/base'
|
11
|
+
|
12
|
+
require 'flamingo/version'
|
13
|
+
require 'flamingo/config'
|
14
|
+
require 'flamingo/dispatch_event'
|
15
|
+
require 'flamingo/dispatch_error'
|
16
|
+
require 'flamingo/stream_params'
|
17
|
+
require 'flamingo/stream'
|
18
|
+
require 'flamingo/subscription'
|
19
|
+
require 'flamingo/wader'
|
20
|
+
require 'flamingo/daemon/pid_file'
|
21
|
+
require 'flamingo/daemon/child_process'
|
22
|
+
require 'flamingo/daemon/dispatcher_process'
|
23
|
+
require 'flamingo/daemon/web_server_process'
|
24
|
+
require 'flamingo/daemon/wader_process'
|
25
|
+
require 'flamingo/daemon/flamingod'
|
26
|
+
require 'flamingo/logging/formatter'
|
27
|
+
require 'flamingo/web/server'
|
28
|
+
|
29
|
+
module Flamingo
|
30
|
+
|
31
|
+
class << self
|
32
|
+
|
33
|
+
def configure!(config_file=nil)
|
34
|
+
config_file = find_config_file(config_file)
|
35
|
+
@config = Flamingo::Config.load(config_file)
|
36
|
+
validate_config!
|
37
|
+
logger.info "Loaded config file from #{config_file}"
|
38
|
+
end
|
39
|
+
|
40
|
+
def config
|
41
|
+
@config
|
42
|
+
end
|
43
|
+
|
44
|
+
# PHD: Lovingly borrowed from Resque
|
45
|
+
|
46
|
+
# Accepts:
|
47
|
+
# 1. A 'hostname:port' string
|
48
|
+
# 2. A 'hostname:port:db' string (to select the Redis db)
|
49
|
+
# 3. An instance of `Redis`, `Redis::Client`, `Redis::DistRedis`,
|
50
|
+
# or `Redis::Namespace`.
|
51
|
+
def redis=(server)
|
52
|
+
case server
|
53
|
+
when String
|
54
|
+
host, port, db = server.split(':')
|
55
|
+
redis = Redis.new(:host => host, :port => port,
|
56
|
+
:thread_safe => true, :db => db)
|
57
|
+
@redis = Redis::Namespace.new(:flamingo, :redis => redis)
|
58
|
+
when Redis, Redis::Client, Redis::DistRedis
|
59
|
+
@redis = Redis::Namespace.new(:flamingo, :redis => server)
|
60
|
+
when Redis::Namespace
|
61
|
+
@redis = server
|
62
|
+
else
|
63
|
+
raise "Invalid redis configuration: #{server.inspect}"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Returns the current Redis connection. If none has been created, will
|
68
|
+
# create a new one.
|
69
|
+
def redis
|
70
|
+
return @redis if @redis
|
71
|
+
self.redis = config.redis.host('localhost:6379')
|
72
|
+
self.redis
|
73
|
+
end
|
74
|
+
|
75
|
+
def new_logger
|
76
|
+
# determine log file location (default is root_dir/log/flamingo.log)
|
77
|
+
if valid_logging_dest?(config.logging.dest(nil))
|
78
|
+
log_dest = config.logging.dest
|
79
|
+
else
|
80
|
+
log_dest = File.join(root_dir,'log','flamingo.log')
|
81
|
+
end
|
82
|
+
|
83
|
+
# determine logging level (default is Logger::INFO)
|
84
|
+
begin
|
85
|
+
log_level = Logger.const_get(config.logging.level.upcase)
|
86
|
+
rescue
|
87
|
+
log_level = Logger::INFO
|
88
|
+
end
|
89
|
+
|
90
|
+
# create logger facility
|
91
|
+
logger = Logger.new(log_dest)
|
92
|
+
logger.level = log_level
|
93
|
+
logger.formatter = Flamingo::Logging::Formatter.new
|
94
|
+
logger
|
95
|
+
end
|
96
|
+
|
97
|
+
def logger
|
98
|
+
@logger ||= new_logger
|
99
|
+
end
|
100
|
+
|
101
|
+
private
|
102
|
+
def root_dir
|
103
|
+
File.expand_path(File.dirname(__FILE__)+'/..')
|
104
|
+
end
|
105
|
+
|
106
|
+
def new_logger
|
107
|
+
dest = config.logging.dest(nil)
|
108
|
+
if valid_logging_dest?(dest)
|
109
|
+
log_file = dest
|
110
|
+
else
|
111
|
+
log_file = File.join(root_dir,'log','flamingo.log')
|
112
|
+
end
|
113
|
+
|
114
|
+
# determine logging level (default is Logger::INFO)
|
115
|
+
begin
|
116
|
+
log_level = Logger.const_get(config.logging.level('INFO').upcase)
|
117
|
+
rescue
|
118
|
+
log_level = Logger::INFO
|
119
|
+
end
|
120
|
+
|
121
|
+
# create logger facility
|
122
|
+
logger = Logger.new(log_file)
|
123
|
+
logger.level = log_level
|
124
|
+
logger.formatter = Flamingo::Logging::Formatter.new
|
125
|
+
logger
|
126
|
+
end
|
127
|
+
|
128
|
+
def valid_logging_dest?(dest)
|
129
|
+
return false unless dest
|
130
|
+
File.writable?(File.dirname(dest))
|
131
|
+
end
|
132
|
+
|
133
|
+
def validate_config!
|
134
|
+
unless config.username(nil) && config.password(nil)
|
135
|
+
raise "The config file must be YAML formatted and contain a username and password. See examples/flamingo.yml."
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def find_config_file(config_file=nil)
|
140
|
+
locations = [config_file,"./flamingo.yml","~/flamingo.yml"].compact.uniq
|
141
|
+
found = locations.find do |file|
|
142
|
+
file && File.exist?(file)
|
143
|
+
end
|
144
|
+
unless found
|
145
|
+
raise "No config file found in any of #{locations.join(",")}"
|
146
|
+
end
|
147
|
+
File.expand_path(found)
|
148
|
+
end
|
149
|
+
|
150
|
+
end
|
151
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Flamingo
|
2
|
+
class Config
|
3
|
+
|
4
|
+
def self.load(file)
|
5
|
+
new(YAML.load(IO.read(file)))
|
6
|
+
end
|
7
|
+
|
8
|
+
def initialize(hash={})
|
9
|
+
@data = hash
|
10
|
+
end
|
11
|
+
|
12
|
+
def method_missing(name,*args,&block)
|
13
|
+
if name.to_s =~ /(.+)=$/
|
14
|
+
@data[$1] = *args
|
15
|
+
else
|
16
|
+
value = @data[name.to_s]
|
17
|
+
if value.is_a?(Hash)
|
18
|
+
self.class.new(value)
|
19
|
+
elsif value.nil? || empty_config?(value)
|
20
|
+
if !args.empty?
|
21
|
+
# Return a default if the value isn't set and there's an argument
|
22
|
+
args.length == 1 ? args[0] : args
|
23
|
+
elsif block_given?
|
24
|
+
# Run the block to get the default value
|
25
|
+
yield
|
26
|
+
else
|
27
|
+
# Return back a config object
|
28
|
+
value = self.class.new
|
29
|
+
@data[name.to_s] = value
|
30
|
+
value
|
31
|
+
end
|
32
|
+
else
|
33
|
+
value
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def respond_to?(name)
|
39
|
+
true
|
40
|
+
end
|
41
|
+
|
42
|
+
def empty?
|
43
|
+
@data.empty?
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
def empty_config?(value)
|
48
|
+
value.is_a?(self.class) && value.empty?
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Flamingo
|
2
|
+
module Daemon
|
3
|
+
class DispatcherProcess < ChildProcess
|
4
|
+
def run
|
5
|
+
worker = Resque::Worker.new(:flamingo)
|
6
|
+
def worker.procline(value)
|
7
|
+
# Hack to get around resque insisting on setting the proces name
|
8
|
+
$0 = "flamingod-dispatcher"
|
9
|
+
end
|
10
|
+
Flamingo.logger.info "Starting dispatcher on pid=#{Process.pid} under pid=#{Process.ppid}"
|
11
|
+
worker.work(1) # Wait 1s between jobs
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
module Flamingo
|
2
|
+
module Daemon
|
3
|
+
class Flamingod
|
4
|
+
|
5
|
+
def exit_signaled?
|
6
|
+
@exit_signaled
|
7
|
+
end
|
8
|
+
|
9
|
+
def exit_signaled=(val)
|
10
|
+
Flamingo.logger.info "Exit signal set to #{val}"
|
11
|
+
@exit_signaled = val
|
12
|
+
end
|
13
|
+
|
14
|
+
def start_new_wader
|
15
|
+
Flamingo.logger.info "Flamingod starting new wader"
|
16
|
+
wader = WaderProcess.new
|
17
|
+
wader.start
|
18
|
+
wader
|
19
|
+
end
|
20
|
+
|
21
|
+
def start_new_dispatcher
|
22
|
+
Flamingo.logger.info "Flamingod starting new dispatcher"
|
23
|
+
dispatcher = DispatcherProcess.new
|
24
|
+
dispatcher.start
|
25
|
+
dispatcher
|
26
|
+
end
|
27
|
+
|
28
|
+
def start_new_web_server
|
29
|
+
Flamingo.logger.info "Flamingod starting new web server"
|
30
|
+
ws = WebServerProcess.new
|
31
|
+
ws.start
|
32
|
+
ws
|
33
|
+
end
|
34
|
+
|
35
|
+
def trap_signals
|
36
|
+
trap("KILL") { terminate! }
|
37
|
+
trap("TERM") { terminate! }
|
38
|
+
trap("INT") { terminate! }
|
39
|
+
trap("USR1") { restart_wader }
|
40
|
+
end
|
41
|
+
|
42
|
+
def restart_wader
|
43
|
+
Flamingo.logger.info "Flamingod restarting wader pid=#{@wader.pid} with SIGINT"
|
44
|
+
@wader.kill("INT")
|
45
|
+
end
|
46
|
+
|
47
|
+
def signal_children(sig)
|
48
|
+
pids = (children.map {|c| c.pid}).join(",")
|
49
|
+
Flamingo.logger.info "Flamingod sending SIG#{sig} to pids=#{pids}"
|
50
|
+
children.each {|child| child.signal(sig) }
|
51
|
+
end
|
52
|
+
|
53
|
+
def terminate!
|
54
|
+
Flamingo.logger.info "Flamingod terminating"
|
55
|
+
self.exit_signaled = true
|
56
|
+
signal_children("INT")
|
57
|
+
end
|
58
|
+
|
59
|
+
def children
|
60
|
+
[@wader,@web_server] + @dispatchers
|
61
|
+
end
|
62
|
+
|
63
|
+
def start_children
|
64
|
+
Flamingo.logger.info "Flamingod starting children"
|
65
|
+
@wader = start_new_wader
|
66
|
+
@dispatchers = [start_new_dispatcher]
|
67
|
+
@web_server = start_new_web_server
|
68
|
+
end
|
69
|
+
|
70
|
+
def wait_on_children()
|
71
|
+
until exit_signaled?
|
72
|
+
child_pid = Process.wait(-1)
|
73
|
+
unless exit_signaled?
|
74
|
+
if @wader.pid == child_pid
|
75
|
+
@wader = start_new_wader
|
76
|
+
elsif @web_server.pid == child_pid
|
77
|
+
@web_server = start_new_web_server
|
78
|
+
elsif (to_delete = @dispatchers.find{|d| d.pid == child_pid})
|
79
|
+
@dispatchers.delete(to_delete)
|
80
|
+
@dispatchers << start_new_dispatcher
|
81
|
+
else
|
82
|
+
Flamingo.logger.info "Received exit from unknown child #{child_pid}"
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def run_as_daemon
|
89
|
+
pid_file = PidFile.new
|
90
|
+
if pid_file.running?
|
91
|
+
raise "flamingod process #{pid_file.read} appears to be running"
|
92
|
+
end
|
93
|
+
pid = fork do
|
94
|
+
pid_file.write(Process.pid)
|
95
|
+
[$stdout,$stdin,$stderr].each do |io|
|
96
|
+
io.reopen '/dev/null' rescue nil
|
97
|
+
end
|
98
|
+
run
|
99
|
+
pid_file.delete
|
100
|
+
end
|
101
|
+
Process.detach(pid)
|
102
|
+
pid
|
103
|
+
end
|
104
|
+
|
105
|
+
def run
|
106
|
+
$0 = 'flamingod'
|
107
|
+
trap_signals
|
108
|
+
start_children
|
109
|
+
wait_on_children
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Flamingo
|
2
|
+
module Daemon
|
3
|
+
class PidFile
|
4
|
+
|
5
|
+
def read
|
6
|
+
File.read(file).strip rescue nil
|
7
|
+
end
|
8
|
+
|
9
|
+
def exists?
|
10
|
+
File.exist?(file) rescue false
|
11
|
+
end
|
12
|
+
|
13
|
+
def running?
|
14
|
+
#PHD The code below borrowed from the daemons gem
|
15
|
+
return false unless exists?
|
16
|
+
# Check if process is in existence
|
17
|
+
# The simplest way to do this is to send signal '0'
|
18
|
+
# (which is a single system call) that doesn't actually
|
19
|
+
# send a signal
|
20
|
+
begin
|
21
|
+
Process.kill(0, pid)
|
22
|
+
return true
|
23
|
+
rescue Errno::ESRCH
|
24
|
+
return false
|
25
|
+
rescue ::Exception # for example on EPERM (process exists but does not belong to us)
|
26
|
+
return true
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def delete
|
31
|
+
File.delete(file) if file
|
32
|
+
end
|
33
|
+
|
34
|
+
def write(pid)
|
35
|
+
File.open(file,"w") {|f| f.write("#{pid}\n") }
|
36
|
+
true
|
37
|
+
rescue
|
38
|
+
false
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
def file
|
43
|
+
Flamingo.config.pid_file(nil)
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Flamingo
|
2
|
+
module Daemon
|
3
|
+
class WaderProcess < ChildProcess
|
4
|
+
def register_signal_handlers
|
5
|
+
trap("INT") { stop }
|
6
|
+
end
|
7
|
+
|
8
|
+
def run
|
9
|
+
register_signal_handlers
|
10
|
+
$0 = 'flamingod-wader'
|
11
|
+
config = Flamingo.config
|
12
|
+
|
13
|
+
screen_name = config.username
|
14
|
+
password = config.password
|
15
|
+
stream = Stream.get(config.stream)
|
16
|
+
|
17
|
+
@wader = Flamingo::Wader.new(screen_name,password,stream)
|
18
|
+
Flamingo.logger.info "Starting wader on pid=#{Process.pid} under pid=#{Process.ppid}"
|
19
|
+
@wader.run
|
20
|
+
Flamingo.logger.info "Wader pid=#{Process.pid} stopped"
|
21
|
+
end
|
22
|
+
|
23
|
+
def stop
|
24
|
+
@wader.stop
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Flamingo
|
2
|
+
module Daemon
|
3
|
+
class WebServerProcess < ChildProcess
|
4
|
+
def run
|
5
|
+
$0 = 'flamingod-web'
|
6
|
+
host, port = Flamingo.config.web.host('0.0.0.0:4711').split(":")
|
7
|
+
Flamingo::Web::Server.run! :host=>host, :port=>port.to_i,
|
8
|
+
:daemon_pid=>Process.ppid
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Flamingo
|
2
|
+
class DispatchEvent
|
3
|
+
|
4
|
+
@queue = :flamingo
|
5
|
+
@parser = Yajl::Parser.new(:symbolize_keys => true)
|
6
|
+
|
7
|
+
class << self
|
8
|
+
|
9
|
+
def perform(event_json)
|
10
|
+
#TODO Track stats including: tweets per second and last tweet time
|
11
|
+
#TODO Provide some first-level check for repeated status ids
|
12
|
+
#TODO Consider subscribers for receiving particular terms - do the heavy
|
13
|
+
# lifting of parsing tweets and delivering them to particular subscribers
|
14
|
+
#TODO Consider window of tweets (approx 3 seconds) and sort before
|
15
|
+
# dispatching to improve in-order delivery (helps with "k-sorted")
|
16
|
+
type, event = typed_event(parse(event_json))
|
17
|
+
# Flamingo.logger.info Flamingo.router.destinations(type,event).inspect
|
18
|
+
Subscription.all.each do |sub|
|
19
|
+
Resque::Job.create(sub.name, "HandleFlamingoEvent", type, event)
|
20
|
+
Flamingo.logger.debug "Put job on subscription queue #{sub.name} for #{event_json}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def parse(json)
|
25
|
+
@parser.parse(json)
|
26
|
+
end
|
27
|
+
|
28
|
+
def typed_event(event)
|
29
|
+
if event[:delete]
|
30
|
+
[:delete,event[:delete]]
|
31
|
+
elsif event[:link]
|
32
|
+
[:link,event[:link]]
|
33
|
+
else
|
34
|
+
[:tweet,event]
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Flamingo
|
2
|
+
module Logging
|
3
|
+
class Formatter < Logger::Formatter
|
4
|
+
def call(severity, time, progname, msg)
|
5
|
+
entry = "\n[#{time.utc.strftime("%Y-%m-%d %H:%M:%S")}, #{severity}"
|
6
|
+
entry << ", #{progname}" if progname
|
7
|
+
entry << "] - #{msg2str(msg)}"
|
8
|
+
entry
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Flamingo
|
2
|
+
|
3
|
+
class Stream
|
4
|
+
|
5
|
+
VERSION = 1
|
6
|
+
|
7
|
+
RESOURCES = HashWithIndifferentAccess.new(
|
8
|
+
:filter => "statuses/filter",
|
9
|
+
:firehose => "statuses/firehose",
|
10
|
+
:retweet => "statuses/retweet",
|
11
|
+
:sample => "statuses/sample"
|
12
|
+
)
|
13
|
+
|
14
|
+
class << self
|
15
|
+
def get(name)
|
16
|
+
new(name,StreamParams.new(name))
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
attr_accessor :name, :params
|
21
|
+
|
22
|
+
def initialize(name,params)
|
23
|
+
self.name = name
|
24
|
+
self.params = params
|
25
|
+
end
|
26
|
+
|
27
|
+
def connect(options)
|
28
|
+
conn_opts = {:ssl => true, :user_agent => "Flamingo/0.1" }.
|
29
|
+
merge(options).merge(:path=>path)
|
30
|
+
Twitter::JSONStream.connect(conn_opts)
|
31
|
+
end
|
32
|
+
|
33
|
+
def path
|
34
|
+
"/#{VERSION}/#{resource}.json?#{query}"
|
35
|
+
end
|
36
|
+
|
37
|
+
def resource
|
38
|
+
RESOURCES[name.to_sym]
|
39
|
+
end
|
40
|
+
|
41
|
+
def to_json
|
42
|
+
ActiveSupport::JSON.encode(
|
43
|
+
:name=>name,:resource=>resource,:params=>params.all
|
44
|
+
)
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
def query
|
49
|
+
params.map{|key,value| "#{key}=#{param_value(value)}" }.join("&")
|
50
|
+
end
|
51
|
+
|
52
|
+
def param_value(val)
|
53
|
+
case val
|
54
|
+
when String then CGI.escape(val)
|
55
|
+
when Array then val.map{|v| CGI.escape(v) }.join(",")
|
56
|
+
else nil
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module Flamingo
|
2
|
+
|
3
|
+
class StreamParams
|
4
|
+
|
5
|
+
include Enumerable
|
6
|
+
|
7
|
+
attr_accessor :stream_name
|
8
|
+
|
9
|
+
def initialize(stream_name)
|
10
|
+
self.stream_name = stream_name
|
11
|
+
end
|
12
|
+
|
13
|
+
def set(key,*values)
|
14
|
+
delete(key)
|
15
|
+
add(key,*values)
|
16
|
+
end
|
17
|
+
|
18
|
+
def []=(key,values)
|
19
|
+
values = [values] unless values.is_a?(Array)
|
20
|
+
set(key,*values)
|
21
|
+
end
|
22
|
+
|
23
|
+
def add(key,*values)
|
24
|
+
values.each do |value|
|
25
|
+
Flamingo.redis.sadd redis_key(key), value
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def remove(key,*values)
|
30
|
+
values.each do |value|
|
31
|
+
Flamingo.redis.srem redis_key(key), value
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def delete(key)
|
36
|
+
Flamingo.redis.del redis_key(key)
|
37
|
+
end
|
38
|
+
|
39
|
+
def get(key)
|
40
|
+
Flamingo.redis.smembers redis_key(key)
|
41
|
+
end
|
42
|
+
alias_method :[], :get
|
43
|
+
|
44
|
+
def keys
|
45
|
+
Flamingo.redis.keys(redis_key_pattern).map do |key|
|
46
|
+
key.split("?")[1].to_sym
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def all
|
51
|
+
keys.inject({}) do |h,key|
|
52
|
+
h[key] = get(key)
|
53
|
+
h
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def each
|
58
|
+
keys.each do |key|
|
59
|
+
yield(key,get(key))
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
def redis_key_pattern
|
65
|
+
"streams/#{stream_name}?*"
|
66
|
+
end
|
67
|
+
|
68
|
+
def redis_key(key)
|
69
|
+
"streams/#{stream_name}?#{key}"
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Flamingo
|
2
|
+
|
3
|
+
class Subscription
|
4
|
+
|
5
|
+
class << self
|
6
|
+
|
7
|
+
def all
|
8
|
+
Flamingo.redis.smembers("subscriptions").map do |name|
|
9
|
+
new(name)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def find(name)
|
14
|
+
if Flamingo.redis.sismember("subscriptions",name)
|
15
|
+
Subscription.new(name)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_accessor :name
|
22
|
+
|
23
|
+
def initialize(name)
|
24
|
+
self.name = name
|
25
|
+
end
|
26
|
+
|
27
|
+
def save
|
28
|
+
Flamingo.logger.info("Adding #{name} to subscriptions")
|
29
|
+
Flamingo.redis.sadd("subscriptions",name)
|
30
|
+
end
|
31
|
+
|
32
|
+
def delete
|
33
|
+
Flamingo.logger.info("Removing #{name} from subscriptions")
|
34
|
+
Flamingo.redis.srem("subscriptions",name)
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Flamingo
|
2
|
+
class Wader
|
3
|
+
|
4
|
+
attr_accessor :screen_name, :password, :stream, :connection
|
5
|
+
|
6
|
+
def initialize(screen_name,password,stream)
|
7
|
+
self.screen_name = screen_name
|
8
|
+
self.password = password
|
9
|
+
self.stream = stream
|
10
|
+
end
|
11
|
+
|
12
|
+
def run
|
13
|
+
EventMachine::run do
|
14
|
+
self.connection = stream.connect(:auth=>"#{screen_name}:#{password}")
|
15
|
+
Flamingo.logger.info("Listening on stream: #{stream.path}")
|
16
|
+
|
17
|
+
connection.each_item do |event_json|
|
18
|
+
dispatch_event(event_json)
|
19
|
+
end
|
20
|
+
|
21
|
+
connection.on_error do |message|
|
22
|
+
dispatch_error(:generic,message)
|
23
|
+
end
|
24
|
+
|
25
|
+
connection.on_reconnect do |timeout, retries|
|
26
|
+
dispatch_error(:reconnection,
|
27
|
+
"Will reconnect after #{timeout}. Retry \##{retries}",
|
28
|
+
{:timeout=>timeout,:retries=>retries}
|
29
|
+
)
|
30
|
+
end
|
31
|
+
|
32
|
+
connection.on_max_reconnects do |timeout, retries|
|
33
|
+
dispatch_error(:fatal,
|
34
|
+
"Failed to reconnect after #{retries} retries",
|
35
|
+
{:timeout=>timeout,:retries=>retries}
|
36
|
+
)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def stop
|
42
|
+
connection.stop
|
43
|
+
EM.stop
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
def dispatch_event(event_json)
|
48
|
+
Flamingo.logger.debug "Wader dispatched event"
|
49
|
+
Resque.enqueue(Flamingo::DispatchEvent,event_json)
|
50
|
+
end
|
51
|
+
|
52
|
+
def dispatch_error(type,message,data={})
|
53
|
+
Flamingo.logger.error "Received error: #{message}"
|
54
|
+
Resque.enqueue(Flamingo::DispatchError,type,message,data)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
module Flamingo
|
2
|
+
module Web
|
3
|
+
class Server < Sinatra::Base
|
4
|
+
|
5
|
+
set :root, File.expand_path(File.dirname(__FILE__))
|
6
|
+
set :static, true
|
7
|
+
set :logging, true
|
8
|
+
|
9
|
+
get '/' do
|
10
|
+
content_type 'text/plain'
|
11
|
+
api = self.methods.select do |method|
|
12
|
+
(method =~ /^(GET|POST) /) && !(method =~ /png$/)
|
13
|
+
end
|
14
|
+
api.sort.join("\n")
|
15
|
+
end
|
16
|
+
|
17
|
+
get '/streams/:name.json' do
|
18
|
+
stream = Stream.get(params[:name])
|
19
|
+
to_json(
|
20
|
+
:name=>stream.name,
|
21
|
+
:resource=>stream.resource,
|
22
|
+
:params=>stream.params.all
|
23
|
+
)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Usage:
|
27
|
+
# streams/filter?track=a,b,c&follow=d,e,f
|
28
|
+
put '/streams/:name.json' do
|
29
|
+
key = params[:key]
|
30
|
+
stream = Stream.get(params[:name])
|
31
|
+
params.keys.each do |key|
|
32
|
+
unless key.to_sym == :name
|
33
|
+
Flamingo.logger.info "Setting #{key} to #{params[key]}"
|
34
|
+
stream.params[key] = params[key].split(",")
|
35
|
+
end
|
36
|
+
end
|
37
|
+
change_predicates
|
38
|
+
to_json(
|
39
|
+
:name=>stream.name,
|
40
|
+
:resource=>stream.resource,
|
41
|
+
:params=>stream.params.all
|
42
|
+
)
|
43
|
+
end
|
44
|
+
|
45
|
+
get '/streams/:name/:key.json' do
|
46
|
+
stream = Stream.get(params[:name])
|
47
|
+
to_json(stream.params[params[:key]])
|
48
|
+
end
|
49
|
+
|
50
|
+
# One of:
|
51
|
+
# Add values to the existing key
|
52
|
+
# ?values=A,B,C
|
53
|
+
# Add and remove in a single request
|
54
|
+
# ?add=A,B&remove=C
|
55
|
+
post '/streams/:name/:key.json' do
|
56
|
+
key = params[:key]
|
57
|
+
stream = Stream.get(params[:name])
|
58
|
+
new_terms = params[:add] || params[:values]
|
59
|
+
stream.params.add(key,*new_terms.split(","))
|
60
|
+
remove_terms = params[:remove]
|
61
|
+
if remove_terms
|
62
|
+
stream.params.remove(key,*remove_terms.split(","))
|
63
|
+
end
|
64
|
+
change_predicates
|
65
|
+
to_json(stream.params[key])
|
66
|
+
end
|
67
|
+
|
68
|
+
put '/streams/:name/:key.json' do
|
69
|
+
key = params[:key]
|
70
|
+
stream = Stream.get(params[:name])
|
71
|
+
new_terms = params[:values]
|
72
|
+
stream.params[key] = new_terms.split(",")
|
73
|
+
change_predicates
|
74
|
+
to_json(stream.params[key])
|
75
|
+
end
|
76
|
+
|
77
|
+
delete '/streams/:name/:key.json' do
|
78
|
+
key = params[:key]
|
79
|
+
stream = Stream.get(params[:name])
|
80
|
+
if params[:values].blank?
|
81
|
+
stream.params.delete(key)
|
82
|
+
else
|
83
|
+
stream.params.remove(key,*params[:values].split(","))
|
84
|
+
end
|
85
|
+
change_predicates
|
86
|
+
to_json(stream.params[key])
|
87
|
+
end
|
88
|
+
|
89
|
+
#Subscriptions
|
90
|
+
get '/subscriptions.json' do
|
91
|
+
subs = Subscription.all.map do |sub|
|
92
|
+
{:name=>sub.name}
|
93
|
+
end
|
94
|
+
to_json(subs)
|
95
|
+
end
|
96
|
+
|
97
|
+
post '/subscriptions.json' do
|
98
|
+
sub = Subscription.new(params[:name])
|
99
|
+
sub.save
|
100
|
+
to_json(:name=>sub.name)
|
101
|
+
end
|
102
|
+
|
103
|
+
get '/subscriptions/:name.json' do
|
104
|
+
sub = Subscription.find(params[:name])
|
105
|
+
not_found(to_json(:error=>"Subscription does not exist")) unless sub
|
106
|
+
to_json(:name=>sub.name)
|
107
|
+
end
|
108
|
+
|
109
|
+
delete '/subscriptions/:name.json' do
|
110
|
+
sub = Subscription.find(params[:name])
|
111
|
+
not_found(to_json(:error=>"Subscription does not exist")) unless sub
|
112
|
+
sub.delete
|
113
|
+
to_json(:name=>sub.name)
|
114
|
+
end
|
115
|
+
|
116
|
+
private
|
117
|
+
def change_predicates
|
118
|
+
if options.respond_to?(:daemon_pid)
|
119
|
+
Process.kill("USR1",options.daemon_pid)
|
120
|
+
Flamingo.logger.info "Rotating wader in daemon"
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def to_json(value)
|
125
|
+
ActiveSupport::JSON.encode(value)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
metadata
ADDED
@@ -0,0 +1,204 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: flamingo
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 9
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
version: "0.1"
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Hayes Davis
|
13
|
+
- Jerry Chen
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2010-07-19 00:00:00 -05:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: redis
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: 25
|
30
|
+
segments:
|
31
|
+
- 1
|
32
|
+
- 0
|
33
|
+
- 7
|
34
|
+
version: 1.0.7
|
35
|
+
type: :runtime
|
36
|
+
version_requirements: *id001
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
name: redis-namespace
|
39
|
+
prerelease: false
|
40
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ">="
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
hash: 3
|
46
|
+
segments:
|
47
|
+
- 0
|
48
|
+
- 7
|
49
|
+
- 0
|
50
|
+
version: 0.7.0
|
51
|
+
type: :runtime
|
52
|
+
version_requirements: *id002
|
53
|
+
- !ruby/object:Gem::Dependency
|
54
|
+
name: resque
|
55
|
+
prerelease: false
|
56
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
hash: 61
|
62
|
+
segments:
|
63
|
+
- 1
|
64
|
+
- 9
|
65
|
+
- 7
|
66
|
+
version: 1.9.7
|
67
|
+
type: :runtime
|
68
|
+
version_requirements: *id003
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: sinatra
|
71
|
+
prerelease: false
|
72
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ">="
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
hash: 63
|
78
|
+
segments:
|
79
|
+
- 0
|
80
|
+
- 9
|
81
|
+
- 2
|
82
|
+
version: 0.9.2
|
83
|
+
type: :runtime
|
84
|
+
version_requirements: *id004
|
85
|
+
- !ruby/object:Gem::Dependency
|
86
|
+
name: twitter-stream
|
87
|
+
prerelease: false
|
88
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ">="
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
hash: 19
|
94
|
+
segments:
|
95
|
+
- 0
|
96
|
+
- 1
|
97
|
+
- 4
|
98
|
+
version: 0.1.4
|
99
|
+
type: :runtime
|
100
|
+
version_requirements: *id005
|
101
|
+
- !ruby/object:Gem::Dependency
|
102
|
+
name: yajl-ruby
|
103
|
+
prerelease: false
|
104
|
+
requirement: &id006 !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ">="
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
hash: 9
|
110
|
+
segments:
|
111
|
+
- 0
|
112
|
+
- 6
|
113
|
+
- 7
|
114
|
+
version: 0.6.7
|
115
|
+
type: :runtime
|
116
|
+
version_requirements: *id006
|
117
|
+
- !ruby/object:Gem::Dependency
|
118
|
+
name: activesupport
|
119
|
+
prerelease: false
|
120
|
+
requirement: &id007 !ruby/object:Gem::Requirement
|
121
|
+
none: false
|
122
|
+
requirements:
|
123
|
+
- - ">="
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
hash: 11
|
126
|
+
segments:
|
127
|
+
- 2
|
128
|
+
- 1
|
129
|
+
- 0
|
130
|
+
version: 2.1.0
|
131
|
+
type: :runtime
|
132
|
+
version_requirements: *id007
|
133
|
+
description: " Flamingo makes it easy to wade through the Twitter Streaming API by \n handling all connectivity and resource management for you. You just tell \n it what to track and consume the information in a resque queue. \n\n Flamingo isn't a traditional ruby gem. You don't require it into your code.\n Instead, it's designed to run as a daemon like redis or mysql. It provides \n a REST interface to change the parameters sent to the Twitter Streaming \n resource. All events from the streaming API are placed on a resque job \n queue where your application can process them.\n\n"
|
134
|
+
email: hayes@appozite.com
|
135
|
+
executables:
|
136
|
+
- flamingo
|
137
|
+
- flamingod
|
138
|
+
extensions: []
|
139
|
+
|
140
|
+
extra_rdoc_files:
|
141
|
+
- LICENSE
|
142
|
+
- README.md
|
143
|
+
files:
|
144
|
+
- README.md
|
145
|
+
- Rakefile
|
146
|
+
- lib/flamingo/config.rb
|
147
|
+
- lib/flamingo/daemon/child_process.rb
|
148
|
+
- lib/flamingo/daemon/dispatcher_process.rb
|
149
|
+
- lib/flamingo/daemon/flamingod.rb
|
150
|
+
- lib/flamingo/daemon/pid_file.rb
|
151
|
+
- lib/flamingo/daemon/wader_process.rb
|
152
|
+
- lib/flamingo/daemon/web_server_process.rb
|
153
|
+
- lib/flamingo/dispatch_error.rb
|
154
|
+
- lib/flamingo/dispatch_event.rb
|
155
|
+
- lib/flamingo/logging/formatter.rb
|
156
|
+
- lib/flamingo/stream.rb
|
157
|
+
- lib/flamingo/stream_params.rb
|
158
|
+
- lib/flamingo/subscription.rb
|
159
|
+
- lib/flamingo/version.rb
|
160
|
+
- lib/flamingo/wader.rb
|
161
|
+
- lib/flamingo/web/server.rb
|
162
|
+
- lib/flamingo.rb
|
163
|
+
- bin/flamingo
|
164
|
+
- bin/flamingo-web
|
165
|
+
- bin/flamingod
|
166
|
+
- examples/flamingo.yml
|
167
|
+
- examples/Rakefile
|
168
|
+
- LICENSE
|
169
|
+
has_rdoc: true
|
170
|
+
homepage: http://github.com/hayesdavis/flamingo
|
171
|
+
licenses: []
|
172
|
+
|
173
|
+
post_install_message:
|
174
|
+
rdoc_options: []
|
175
|
+
|
176
|
+
require_paths:
|
177
|
+
- lib
|
178
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
179
|
+
none: false
|
180
|
+
requirements:
|
181
|
+
- - ">="
|
182
|
+
- !ruby/object:Gem::Version
|
183
|
+
hash: 3
|
184
|
+
segments:
|
185
|
+
- 0
|
186
|
+
version: "0"
|
187
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
188
|
+
none: false
|
189
|
+
requirements:
|
190
|
+
- - ">="
|
191
|
+
- !ruby/object:Gem::Version
|
192
|
+
hash: 3
|
193
|
+
segments:
|
194
|
+
- 0
|
195
|
+
version: "0"
|
196
|
+
requirements: []
|
197
|
+
|
198
|
+
rubyforge_project:
|
199
|
+
rubygems_version: 1.3.7
|
200
|
+
signing_key:
|
201
|
+
specification_version: 3
|
202
|
+
summary: Flamingo is an elegant way to wade into the Twitter Streaming API.
|
203
|
+
test_files: []
|
204
|
+
|