birdwatcher 0.1.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 +7 -0
- data/.gitignore +9 -0
- data/.travis.yml +5 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +481 -0
- data/Rakefile +10 -0
- data/bin/console +42 -0
- data/birdwatcher.gemspec +40 -0
- data/data/english_stopwords.txt +319 -0
- data/data/top100Kenglishwords.txt +100000 -0
- data/db/migrations/001_create_workspaces.rb +11 -0
- data/db/migrations/002_create_users.rb +29 -0
- data/db/migrations/003_create_statuses.rb +28 -0
- data/db/migrations/004_create_mentions.rb +13 -0
- data/db/migrations/005_create_mentions_statuses.rb +8 -0
- data/db/migrations/006_create_hashtags.rb +11 -0
- data/db/migrations/007_create_hashtags_statuses.rb +8 -0
- data/db/migrations/008_create_urls.rb +16 -0
- data/db/migrations/009_create_statuses_urls.rb +8 -0
- data/db/migrations/010_create_klout_topics.rb +10 -0
- data/db/migrations/011_create_klout_topics_users.rb +8 -0
- data/db/migrations/012_create_influencers.rb +10 -0
- data/db/migrations/013_create_influencers_users.rb +8 -0
- data/db/migrations/014_create_influencees.rb +10 -0
- data/db/migrations/015_create_influencees_users.rb +8 -0
- data/exe/birdwatcher +12 -0
- data/lib/birdwatcher/command.rb +78 -0
- data/lib/birdwatcher/commands/back.rb +15 -0
- data/lib/birdwatcher/commands/exit.rb +16 -0
- data/lib/birdwatcher/commands/help.rb +60 -0
- data/lib/birdwatcher/commands/irb.rb +34 -0
- data/lib/birdwatcher/commands/module.rb +106 -0
- data/lib/birdwatcher/commands/query.rb +58 -0
- data/lib/birdwatcher/commands/query_csv.rb +56 -0
- data/lib/birdwatcher/commands/resource.rb +45 -0
- data/lib/birdwatcher/commands/run.rb +19 -0
- data/lib/birdwatcher/commands/schema.rb +116 -0
- data/lib/birdwatcher/commands/set.rb +56 -0
- data/lib/birdwatcher/commands/shell.rb +21 -0
- data/lib/birdwatcher/commands/show.rb +86 -0
- data/lib/birdwatcher/commands/status.rb +114 -0
- data/lib/birdwatcher/commands/unset.rb +37 -0
- data/lib/birdwatcher/commands/use.rb +25 -0
- data/lib/birdwatcher/commands/user.rb +155 -0
- data/lib/birdwatcher/commands/workspace.rb +176 -0
- data/lib/birdwatcher/concerns/concurrency.rb +25 -0
- data/lib/birdwatcher/concerns/core.rb +105 -0
- data/lib/birdwatcher/concerns/outputting.rb +114 -0
- data/lib/birdwatcher/concerns/persistence.rb +101 -0
- data/lib/birdwatcher/concerns/presentation.rb +122 -0
- data/lib/birdwatcher/concerns/util.rb +138 -0
- data/lib/birdwatcher/configuration.rb +63 -0
- data/lib/birdwatcher/configuration_wizard.rb +65 -0
- data/lib/birdwatcher/console.rb +201 -0
- data/lib/birdwatcher/http_client.rb +164 -0
- data/lib/birdwatcher/klout_client.rb +83 -0
- data/lib/birdwatcher/kml.rb +125 -0
- data/lib/birdwatcher/module.rb +253 -0
- data/lib/birdwatcher/modules/statuses/kml.rb +106 -0
- data/lib/birdwatcher/modules/statuses/sentiment.rb +77 -0
- data/lib/birdwatcher/modules/statuses/word_cloud.rb +205 -0
- data/lib/birdwatcher/modules/urls/crawl.rb +138 -0
- data/lib/birdwatcher/modules/urls/most_shared.rb +98 -0
- data/lib/birdwatcher/modules/users/activity_plot.rb +62 -0
- data/lib/birdwatcher/modules/users/import.rb +61 -0
- data/lib/birdwatcher/modules/users/influence_graph.rb +93 -0
- data/lib/birdwatcher/modules/users/klout_id.rb +62 -0
- data/lib/birdwatcher/modules/users/klout_influence.rb +83 -0
- data/lib/birdwatcher/modules/users/klout_score.rb +64 -0
- data/lib/birdwatcher/modules/users/klout_topics.rb +72 -0
- data/lib/birdwatcher/modules/users/social_graph.rb +110 -0
- data/lib/birdwatcher/punchcard.rb +183 -0
- data/lib/birdwatcher/util.rb +83 -0
- data/lib/birdwatcher/version.rb +3 -0
- data/lib/birdwatcher.rb +43 -0
- data/models/hashtag.rb +8 -0
- data/models/influencee.rb +8 -0
- data/models/influencer.rb +8 -0
- data/models/klout_topic.rb +8 -0
- data/models/mention.rb +8 -0
- data/models/status.rb +11 -0
- data/models/url.rb +8 -0
- data/models/user.rb +11 -0
- data/models/workspace.rb +26 -0
- metadata +405 -0
@@ -0,0 +1,29 @@
|
|
1
|
+
Sequel.migration do
|
2
|
+
change do
|
3
|
+
create_table(:users) do
|
4
|
+
primary_key :id
|
5
|
+
foreign_key :workspace_id, :workspaces, :on_delete => :cascade, :index => true
|
6
|
+
String :twitter_id, :index => true
|
7
|
+
String :screen_name, :index => true
|
8
|
+
String :name
|
9
|
+
String :location
|
10
|
+
String :description
|
11
|
+
String :url
|
12
|
+
String :profile_image_url
|
13
|
+
Integer :followers_count, :index => true
|
14
|
+
Integer :friends_count, :index => true
|
15
|
+
Integer :listed_count, :index => true
|
16
|
+
Integer :favorites_count, :index => true
|
17
|
+
Integer :statuses_count, :index => true
|
18
|
+
Integer :utc_offset
|
19
|
+
String :timezone
|
20
|
+
Boolean :geo_enabled, :index => true
|
21
|
+
Boolean :verified, :index => true
|
22
|
+
String :lang, :index => true
|
23
|
+
String :klout_id
|
24
|
+
Integer :klout_score, :index => true
|
25
|
+
DateTime :updated_at, :index => true
|
26
|
+
DateTime :created_at, :index => true
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
Sequel.migration do
|
2
|
+
change do
|
3
|
+
create_table(:statuses) do
|
4
|
+
primary_key :id
|
5
|
+
foreign_key :workspace_id, :workspaces, :on_delete => :cascade, :index => true
|
6
|
+
foreign_key :user_id, :users, :on_delete => :cascade, :index => true
|
7
|
+
String :twitter_id, :index => true
|
8
|
+
String :text, :index => true
|
9
|
+
String :source
|
10
|
+
Boolean :retweet, :index => true
|
11
|
+
Boolean :geo, :index => true
|
12
|
+
String :longitude
|
13
|
+
String :latitude
|
14
|
+
String :place_type
|
15
|
+
String :place_name
|
16
|
+
String :place_country_code
|
17
|
+
String :place_country
|
18
|
+
Integer :favorite_count, :index => true
|
19
|
+
Integer :retweet_count, :index => true
|
20
|
+
String :sentiment, :index => true
|
21
|
+
Boolean :possibly_sensitive, :index => true
|
22
|
+
String :lang, :index => true
|
23
|
+
DateTime :posted_at, :index => true
|
24
|
+
DateTime :updated_at, :index => true
|
25
|
+
DateTime :created_at, :index => true
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
Sequel.migration do
|
2
|
+
change do
|
3
|
+
create_table(:mentions) do
|
4
|
+
primary_key :id
|
5
|
+
foreign_key :workspace_id, :workspaces, :on_delete => :cascade, :index => true
|
6
|
+
String :twitter_id, :index => true
|
7
|
+
String :screen_name, :index => true
|
8
|
+
String :name
|
9
|
+
DateTime :updated_at
|
10
|
+
DateTime :created_at, :index => true
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
Sequel.migration do
|
2
|
+
change do
|
3
|
+
create_table(:hashtags) do
|
4
|
+
primary_key :id
|
5
|
+
foreign_key :workspace_id, :workspaces, :on_delete => :cascade, :index => true
|
6
|
+
String :tag, :index => true
|
7
|
+
DateTime :updated_at
|
8
|
+
DateTime :created_at, :index => true
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
Sequel.migration do
|
2
|
+
change do
|
3
|
+
create_table(:urls) do
|
4
|
+
primary_key :id
|
5
|
+
foreign_key :workspace_id, :workspaces, :on_delete => :cascade, :index => true
|
6
|
+
String :url, :index => true
|
7
|
+
String :final_url, :index => true
|
8
|
+
Integer :http_status, :index => true
|
9
|
+
String :content_type, :index => true
|
10
|
+
String :title, :index => true
|
11
|
+
DateTime :crawled_at, :index => true
|
12
|
+
DateTime :updated_at, :index => true
|
13
|
+
DateTime :created_at, :index => true
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
data/exe/birdwatcher
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
module Birdwatcher
|
2
|
+
class Command
|
3
|
+
class Error < StandardError; end
|
4
|
+
class InvalidMetadataError < Error; end
|
5
|
+
class MetadataNotSetError < Error; end
|
6
|
+
|
7
|
+
ARGUMENT_SEPARATOR = " ".freeze
|
8
|
+
|
9
|
+
attr_reader :arguments
|
10
|
+
|
11
|
+
include Birdwatcher::Concerns::Core
|
12
|
+
include Birdwatcher::Concerns::Util
|
13
|
+
include Birdwatcher::Concerns::Outputting
|
14
|
+
include Birdwatcher::Concerns::Presentation
|
15
|
+
include Birdwatcher::Concerns::Persistence
|
16
|
+
include Birdwatcher::Concerns::Concurrency
|
17
|
+
|
18
|
+
def self.meta
|
19
|
+
@meta || fail(MetadataNotSetError, "Metadata has not been set")
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.meta=(meta)
|
23
|
+
validate_metadata(meta)
|
24
|
+
@meta = meta
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.detailed_usage; end
|
28
|
+
|
29
|
+
def self.descendants
|
30
|
+
ObjectSpace.each_object(Class).select { |klass| klass < self }
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.has_name?(name)
|
34
|
+
meta[:names].include?(name)
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.auto_completion_strings
|
38
|
+
(meta[:names] + auto_completion).uniq
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.auto_completion
|
42
|
+
[]
|
43
|
+
end
|
44
|
+
|
45
|
+
def execute(argument_line)
|
46
|
+
@arguments = argument_line.to_s.split(ARGUMENT_SEPARATOR).map { |a| a.to_s.strip }
|
47
|
+
run
|
48
|
+
rescue => e
|
49
|
+
error("#{e.class}".bold + ": #{e.message}")
|
50
|
+
puts e.backtrace.join("\n")
|
51
|
+
end
|
52
|
+
|
53
|
+
protected
|
54
|
+
|
55
|
+
def run
|
56
|
+
fail NotImplementedError, "Commands must implement #run method"
|
57
|
+
end
|
58
|
+
|
59
|
+
def arguments?
|
60
|
+
!arguments.empty?
|
61
|
+
end
|
62
|
+
|
63
|
+
def commands
|
64
|
+
Birdwatcher::Command.descendants
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.validate_metadata(meta)
|
68
|
+
fail InvalidMetadataError, "Metadata is not a hash" unless meta.is_a?(Hash)
|
69
|
+
fail InvalidMetadataError, "Metadata is empty" if meta.empty?
|
70
|
+
fail InvalidMetadataError, "Metadata is missing key: description" unless meta.key?(:description)
|
71
|
+
fail InvalidMetadataError, "Metadata is missing key: names" unless meta.key?(:names)
|
72
|
+
fail InvalidMetadataError, "Metadata is missing key: usage" unless meta.key?(:usage)
|
73
|
+
fail InvalidMetadataError, "Metadata names is not an array" unless meta[:names].is_a?(Array)
|
74
|
+
fail InvalidMetadataError, "Metadata names must contain at least one string" if meta[:names].empty?
|
75
|
+
fail InvalidMetadataError, "Metadata usage is not string" unless meta[:usage].is_a?(String)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Birdwatcher
|
2
|
+
module Commands
|
3
|
+
class Back < Birdwatcher::Command
|
4
|
+
self.meta = {
|
5
|
+
:description => "Unloads current module",
|
6
|
+
:names => %w(back unload),
|
7
|
+
:usage => "back"
|
8
|
+
}
|
9
|
+
|
10
|
+
def run
|
11
|
+
console.current_module = nil
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Birdwatcher
|
2
|
+
module Commands
|
3
|
+
class Help < Birdwatcher::Command
|
4
|
+
self.meta = {
|
5
|
+
:description => "Show help and detailed command usage",
|
6
|
+
:names => %w(help ?),
|
7
|
+
:usage => "help [COMMAND]"
|
8
|
+
}
|
9
|
+
|
10
|
+
def self.detailed_usage
|
11
|
+
<<-USAGE
|
12
|
+
The #{'help'.bold} command shows a general overview of available commands as well as help
|
13
|
+
and detailed usage for specific commands.
|
14
|
+
|
15
|
+
#{'USAGE:'.bold}
|
16
|
+
|
17
|
+
#{'See available commands and short descriptions:'.bold}
|
18
|
+
help
|
19
|
+
|
20
|
+
#{'See help and detailed usage for specific command:'.bold}
|
21
|
+
help COMMAND
|
22
|
+
USAGE
|
23
|
+
end
|
24
|
+
|
25
|
+
def run
|
26
|
+
if arguments?
|
27
|
+
show_command_help
|
28
|
+
else
|
29
|
+
show_general_help
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def show_command_help
|
36
|
+
command_name = arguments.first.downcase
|
37
|
+
commands.each do |command|
|
38
|
+
next unless command.has_name?(command_name)
|
39
|
+
if command.detailed_usage
|
40
|
+
newline
|
41
|
+
output command.detailed_usage
|
42
|
+
else
|
43
|
+
info("There is no detailed usage for this command")
|
44
|
+
end
|
45
|
+
return
|
46
|
+
end
|
47
|
+
error "Unknown command: #{command_name}"
|
48
|
+
end
|
49
|
+
|
50
|
+
def show_general_help
|
51
|
+
longest_command_usage = commands.map { |c| c.meta[:usage] }.max_by(&:length)
|
52
|
+
info "Available commands:\n"
|
53
|
+
commands.sort_by { |c| c.meta[:usage] }.each do |command|
|
54
|
+
output_formatted(" %-#{longest_command_usage.bold.length}s\t\t%s\n", command.meta[:usage].bold, command.meta[:description])
|
55
|
+
end
|
56
|
+
newline
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Birdwatcher
|
2
|
+
module Commands
|
3
|
+
class Irb < Birdwatcher::Command
|
4
|
+
self.meta = {
|
5
|
+
:description => "Start an interactive Ruby shell",
|
6
|
+
:names => %w(irb),
|
7
|
+
:usage => "irb"
|
8
|
+
}
|
9
|
+
|
10
|
+
def self.detailed_usage
|
11
|
+
<<-USAGE
|
12
|
+
The #{'irb'.bold} command can be used start an interactive Ruby shell (IRB) with
|
13
|
+
all of the Birdwatcher classes and models loaded.
|
14
|
+
|
15
|
+
#{'NOTE:'.bold} This command is not intended for normal users of Birdwatcher but
|
16
|
+
can be convenient for debugging or more complex one-off data manipulation, if you
|
17
|
+
know what you're doing.
|
18
|
+
|
19
|
+
#{'USAGE:'.bold}
|
20
|
+
|
21
|
+
#{'Start an interactive Ruby shell:'.bold}
|
22
|
+
irb
|
23
|
+
USAGE
|
24
|
+
end
|
25
|
+
|
26
|
+
def run
|
27
|
+
require "irb"
|
28
|
+
require "awesome_print"
|
29
|
+
AwesomePrint.irb!
|
30
|
+
suppress_warnings { IRB.start }
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
module Birdwatcher
|
2
|
+
module Commands
|
3
|
+
class Module < Birdwatcher::Command
|
4
|
+
self.meta = {
|
5
|
+
:description => "Show modules",
|
6
|
+
:names => %w(module modules),
|
7
|
+
:usage => "module ACTION"
|
8
|
+
}
|
9
|
+
|
10
|
+
def self.detailed_usage
|
11
|
+
<<-USAGE
|
12
|
+
The #{'modules'.bold} command shows information about available Birdwatcher modules.
|
13
|
+
|
14
|
+
#{'USAGE:'.bold}
|
15
|
+
|
16
|
+
#{'See available modules and short descriptions:'.bold}
|
17
|
+
modules list
|
18
|
+
|
19
|
+
#{'See detailed information on specific module:'.bold}
|
20
|
+
modules info MODULE_PATH
|
21
|
+
|
22
|
+
#{'Search for modules with the word "import" in their name, description or path:'.bold}
|
23
|
+
modules search import
|
24
|
+
|
25
|
+
#{'List all modules related to users:'.bold}
|
26
|
+
modules search users/
|
27
|
+
USAGE
|
28
|
+
end
|
29
|
+
|
30
|
+
def run
|
31
|
+
if !arguments?
|
32
|
+
show_modules
|
33
|
+
return
|
34
|
+
end
|
35
|
+
action = arguments.first.downcase
|
36
|
+
case action
|
37
|
+
when "show", "info", "view"
|
38
|
+
show_module(arguments[1])
|
39
|
+
when "list", "-l"
|
40
|
+
show_modules
|
41
|
+
when "search", "-s"
|
42
|
+
search_modules
|
43
|
+
else
|
44
|
+
show_module(arguments.first)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def show_module(path)
|
51
|
+
if !_module = Birdwatcher::Module.module_by_path(path)
|
52
|
+
error("Unknown module: #{arguments[1].bold}")
|
53
|
+
return false
|
54
|
+
end
|
55
|
+
newline
|
56
|
+
output " Name: ".bold + _module.meta[:name]
|
57
|
+
output "Description: ".bold + _module.meta[:description]
|
58
|
+
output " Author: ".bold + _module.meta[:author]
|
59
|
+
output " Path: ".bold + _module.path
|
60
|
+
newline
|
61
|
+
line_separator
|
62
|
+
newline
|
63
|
+
if _module.info
|
64
|
+
output _module.info
|
65
|
+
else
|
66
|
+
info("No further information has been provided for this module")
|
67
|
+
end
|
68
|
+
newline
|
69
|
+
end
|
70
|
+
|
71
|
+
def show_modules
|
72
|
+
info("Available Modules:\n")
|
73
|
+
Birdwatcher::Module.descendants.sort_by(&:path).each do |_module|
|
74
|
+
output_module_summary(_module)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def search_modules
|
79
|
+
search_term = arguments[1..-1].join(" ").downcase
|
80
|
+
modules = []
|
81
|
+
Birdwatcher::Module.descendants.sort_by(&:path).each do |_module|
|
82
|
+
if _module.path.include?(search_term) || _module.meta[:name].downcase.include?(search_term) || _module.meta[:description].downcase.include?(search_term)
|
83
|
+
modules << _module
|
84
|
+
end
|
85
|
+
end
|
86
|
+
if modules.empty?
|
87
|
+
info("No modules found with search: #{search_term.bold}")
|
88
|
+
else
|
89
|
+
info("Module Search Results:\n")
|
90
|
+
modules.each do |_module|
|
91
|
+
output_module_summary(_module)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def output_module_summary(_module)
|
97
|
+
output " Name: ".bold + _module.meta[:name]
|
98
|
+
output "Description: ".bold + _module.meta[:description]
|
99
|
+
output " Path: ".bold + _module.path
|
100
|
+
newline
|
101
|
+
line_separator
|
102
|
+
newline
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Birdwatcher
|
2
|
+
module Commands
|
3
|
+
class Query < Birdwatcher::Command
|
4
|
+
self.meta = {
|
5
|
+
:description => "Execute SQL query",
|
6
|
+
:names => %w(query sql),
|
7
|
+
:usage => "query QUERY"
|
8
|
+
}
|
9
|
+
|
10
|
+
def self.detailed_usage
|
11
|
+
<<-USAGE
|
12
|
+
The #{'query'.bold} command can be used to execute raw SQL queries against the
|
13
|
+
underlying database for Birdwatcher. The query results will be shown in a formatted
|
14
|
+
table.
|
15
|
+
|
16
|
+
#{'IMPORTANT:'.bold} The query command does not automatically isolate the data
|
17
|
+
to the current workspace so queries will need to handle that on their own.
|
18
|
+
Most tables will have a column called #{'workspace_id'.bold} which will contain
|
19
|
+
the numeric ID of the workspace the object belongs to.
|
20
|
+
|
21
|
+
For a more machine-parsable query result, please see the #{'query_csv'.bold} command.
|
22
|
+
|
23
|
+
#{'USAGE EXAMPLES:'.bold}
|
24
|
+
|
25
|
+
#{'See current workspaces:'.bold}
|
26
|
+
query SELECT * from workspaces ORDER BY name
|
27
|
+
|
28
|
+
#{'See geo coordinates for all statuses in a workspace:'.bold}
|
29
|
+
query SELECT longitude,latitude FROM statuses WHERE geo IS TRUE AND workspace_id = 1
|
30
|
+
|
31
|
+
#{'See statuses containing the word "password":'.bold}
|
32
|
+
query SELECT u.screen_name, s.text, s.posted_at FROM users AS u JOIN statuses AS s ON s.user_id = u.id WHERE s.text LIKE '%password%'
|
33
|
+
|
34
|
+
#{'See status geographic places by frequency:'.bold}
|
35
|
+
query SELECT COUNT(*) AS count, place_name FROM statuses WHERE place_name IS NOT NULL GROUP BY place_name ORDER BY count DESC
|
36
|
+
USAGE
|
37
|
+
end
|
38
|
+
|
39
|
+
def run
|
40
|
+
if !arguments?
|
41
|
+
error("You must provide an SQL query to execute")
|
42
|
+
return false
|
43
|
+
end
|
44
|
+
|
45
|
+
query = arguments.join(" ")
|
46
|
+
result = database[query]
|
47
|
+
rows = result.map { |r| r.to_hash.values }
|
48
|
+
table = Terminal::Table.new(
|
49
|
+
:headings => result.columns.map { |c| c.to_s.bold },
|
50
|
+
:rows => rows
|
51
|
+
).to_s
|
52
|
+
page_text(table)
|
53
|
+
rescue Sequel::DatabaseError => e
|
54
|
+
error("Syntax error: #{e.message}")
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Birdwatcher
|
2
|
+
module Commands
|
3
|
+
class QueryCsv < Birdwatcher::Command
|
4
|
+
self.meta = {
|
5
|
+
:description => "Execute SQL query and return result as CSV",
|
6
|
+
:names => %w(query_csv csv),
|
7
|
+
:usage => "query_csv QUERY"
|
8
|
+
}
|
9
|
+
|
10
|
+
def self.detailed_usage
|
11
|
+
<<-USAGE
|
12
|
+
The #{'query_csv'.bold} command can be used to execute raw SQL queries against the
|
13
|
+
underlying database for Birdwatcher. The query results will be shown in CSV format
|
14
|
+
for easy parsing by other tools or code.
|
15
|
+
|
16
|
+
#{'IMPORTANT:'.bold} The query_csv command does not automatically isolate the data
|
17
|
+
to the current workspace so queries will need to handle that on their own.
|
18
|
+
Most tables will have a column called #{'workspace_id'.bold} which will contain
|
19
|
+
the numeric ID of the workspace the object belongs to.
|
20
|
+
|
21
|
+
#{'USAGE EXAMPLES:'.bold}
|
22
|
+
|
23
|
+
#{'See current workspaces:'.bold}
|
24
|
+
query_csv SELECT * from workspaces ORDER BY name
|
25
|
+
|
26
|
+
#{'See geo coordinates for all statuses in a workspace:'.bold}
|
27
|
+
query_csv SELECT longitude,latitude FROM statuses WHERE geo IS TRUE AND workspace_id = 1
|
28
|
+
|
29
|
+
#{'See statuses containing the word "password":'.bold}
|
30
|
+
query_csv SELECT u.screen_name, s.text, s.posted_at FROM users AS u JOIN statuses AS s ON s.user_id = u.id WHERE s.text LIKE '%password%'
|
31
|
+
|
32
|
+
#{'See status geographic places by frequency:'.bold}
|
33
|
+
query_csv SELECT COUNT(*) AS count, place_name FROM statuses WHERE place_name IS NOT NULL GROUP BY place_name ORDER BY count DESC
|
34
|
+
USAGE
|
35
|
+
end
|
36
|
+
|
37
|
+
def run
|
38
|
+
if !arguments?
|
39
|
+
error("You must provide an SQL query to execute")
|
40
|
+
return false
|
41
|
+
end
|
42
|
+
|
43
|
+
query = arguments.join(" ")
|
44
|
+
result = database[query]
|
45
|
+
rows = result.map { |r| r.to_hash.values }
|
46
|
+
headers = result.columns.map { |c| c.to_s }
|
47
|
+
csv = CSV.generate(:write_headers => true, :headers => headers) do |doc|
|
48
|
+
rows.each { |r| doc << r }
|
49
|
+
end
|
50
|
+
page_text(csv)
|
51
|
+
rescue Sequel::DatabaseError => e
|
52
|
+
error("Syntax error: #{e.message}")
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|