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.
Files changed (86) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +9 -0
  3. data/.travis.yml +5 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +481 -0
  7. data/Rakefile +10 -0
  8. data/bin/console +42 -0
  9. data/birdwatcher.gemspec +40 -0
  10. data/data/english_stopwords.txt +319 -0
  11. data/data/top100Kenglishwords.txt +100000 -0
  12. data/db/migrations/001_create_workspaces.rb +11 -0
  13. data/db/migrations/002_create_users.rb +29 -0
  14. data/db/migrations/003_create_statuses.rb +28 -0
  15. data/db/migrations/004_create_mentions.rb +13 -0
  16. data/db/migrations/005_create_mentions_statuses.rb +8 -0
  17. data/db/migrations/006_create_hashtags.rb +11 -0
  18. data/db/migrations/007_create_hashtags_statuses.rb +8 -0
  19. data/db/migrations/008_create_urls.rb +16 -0
  20. data/db/migrations/009_create_statuses_urls.rb +8 -0
  21. data/db/migrations/010_create_klout_topics.rb +10 -0
  22. data/db/migrations/011_create_klout_topics_users.rb +8 -0
  23. data/db/migrations/012_create_influencers.rb +10 -0
  24. data/db/migrations/013_create_influencers_users.rb +8 -0
  25. data/db/migrations/014_create_influencees.rb +10 -0
  26. data/db/migrations/015_create_influencees_users.rb +8 -0
  27. data/exe/birdwatcher +12 -0
  28. data/lib/birdwatcher/command.rb +78 -0
  29. data/lib/birdwatcher/commands/back.rb +15 -0
  30. data/lib/birdwatcher/commands/exit.rb +16 -0
  31. data/lib/birdwatcher/commands/help.rb +60 -0
  32. data/lib/birdwatcher/commands/irb.rb +34 -0
  33. data/lib/birdwatcher/commands/module.rb +106 -0
  34. data/lib/birdwatcher/commands/query.rb +58 -0
  35. data/lib/birdwatcher/commands/query_csv.rb +56 -0
  36. data/lib/birdwatcher/commands/resource.rb +45 -0
  37. data/lib/birdwatcher/commands/run.rb +19 -0
  38. data/lib/birdwatcher/commands/schema.rb +116 -0
  39. data/lib/birdwatcher/commands/set.rb +56 -0
  40. data/lib/birdwatcher/commands/shell.rb +21 -0
  41. data/lib/birdwatcher/commands/show.rb +86 -0
  42. data/lib/birdwatcher/commands/status.rb +114 -0
  43. data/lib/birdwatcher/commands/unset.rb +37 -0
  44. data/lib/birdwatcher/commands/use.rb +25 -0
  45. data/lib/birdwatcher/commands/user.rb +155 -0
  46. data/lib/birdwatcher/commands/workspace.rb +176 -0
  47. data/lib/birdwatcher/concerns/concurrency.rb +25 -0
  48. data/lib/birdwatcher/concerns/core.rb +105 -0
  49. data/lib/birdwatcher/concerns/outputting.rb +114 -0
  50. data/lib/birdwatcher/concerns/persistence.rb +101 -0
  51. data/lib/birdwatcher/concerns/presentation.rb +122 -0
  52. data/lib/birdwatcher/concerns/util.rb +138 -0
  53. data/lib/birdwatcher/configuration.rb +63 -0
  54. data/lib/birdwatcher/configuration_wizard.rb +65 -0
  55. data/lib/birdwatcher/console.rb +201 -0
  56. data/lib/birdwatcher/http_client.rb +164 -0
  57. data/lib/birdwatcher/klout_client.rb +83 -0
  58. data/lib/birdwatcher/kml.rb +125 -0
  59. data/lib/birdwatcher/module.rb +253 -0
  60. data/lib/birdwatcher/modules/statuses/kml.rb +106 -0
  61. data/lib/birdwatcher/modules/statuses/sentiment.rb +77 -0
  62. data/lib/birdwatcher/modules/statuses/word_cloud.rb +205 -0
  63. data/lib/birdwatcher/modules/urls/crawl.rb +138 -0
  64. data/lib/birdwatcher/modules/urls/most_shared.rb +98 -0
  65. data/lib/birdwatcher/modules/users/activity_plot.rb +62 -0
  66. data/lib/birdwatcher/modules/users/import.rb +61 -0
  67. data/lib/birdwatcher/modules/users/influence_graph.rb +93 -0
  68. data/lib/birdwatcher/modules/users/klout_id.rb +62 -0
  69. data/lib/birdwatcher/modules/users/klout_influence.rb +83 -0
  70. data/lib/birdwatcher/modules/users/klout_score.rb +64 -0
  71. data/lib/birdwatcher/modules/users/klout_topics.rb +72 -0
  72. data/lib/birdwatcher/modules/users/social_graph.rb +110 -0
  73. data/lib/birdwatcher/punchcard.rb +183 -0
  74. data/lib/birdwatcher/util.rb +83 -0
  75. data/lib/birdwatcher/version.rb +3 -0
  76. data/lib/birdwatcher.rb +43 -0
  77. data/models/hashtag.rb +8 -0
  78. data/models/influencee.rb +8 -0
  79. data/models/influencer.rb +8 -0
  80. data/models/klout_topic.rb +8 -0
  81. data/models/mention.rb +8 -0
  82. data/models/status.rb +11 -0
  83. data/models/url.rb +8 -0
  84. data/models/user.rb +11 -0
  85. data/models/workspace.rb +26 -0
  86. metadata +405 -0
@@ -0,0 +1,11 @@
1
+ Sequel.migration do
2
+ change do
3
+ create_table(:workspaces) do
4
+ primary_key :id
5
+ String :name, :index => true, :unique => true
6
+ String :description
7
+ DateTime :updated_at
8
+ DateTime :created_at
9
+ end
10
+ end
11
+ end
@@ -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,8 @@
1
+ Sequel.migration do
2
+ change do
3
+ create_table(:mentions_statuses) do
4
+ foreign_key :mention_id, :mentions, :on_delete => :cascade, :index => true
5
+ foreign_key :status_id, :statuses, :on_delete => :cascade, :index => true
6
+ end
7
+ end
8
+ 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,8 @@
1
+ Sequel.migration do
2
+ change do
3
+ create_table(:hashtags_statuses) do
4
+ foreign_key :hashtag_id, :hashtags, :on_delete => :cascade, :index => true
5
+ foreign_key :status_id, :statuses, :on_delete => :cascade, :index => true
6
+ end
7
+ end
8
+ 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
@@ -0,0 +1,8 @@
1
+ Sequel.migration do
2
+ change do
3
+ create_table(:statuses_urls) do
4
+ foreign_key :url_id, :urls, :on_delete => :cascade, :index => true
5
+ foreign_key :status_id, :statuses, :on_delete => :cascade, :index => true
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,10 @@
1
+ Sequel.migration do
2
+ change do
3
+ create_table(:klout_topics) do
4
+ primary_key :id
5
+ foreign_key :workspace_id, :workspaces, :on_delete => :cascade, :index => true
6
+ String :topic, :index => true
7
+ DateTime :created_at, :index => true
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,8 @@
1
+ Sequel.migration do
2
+ change do
3
+ create_table(:klout_topics_users) do
4
+ foreign_key :user_id, :users, :on_delete => :cascade, :index => true
5
+ foreign_key :klout_topic_id, :klout_topics, :on_delete => :cascade, :index => true
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,10 @@
1
+ Sequel.migration do
2
+ change do
3
+ create_table(:influencers) do
4
+ primary_key :id
5
+ foreign_key :workspace_id, :workspaces, :on_delete => :cascade, :index => true
6
+ String :screen_name, :index => true
7
+ DateTime :created_at, :index => true
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,8 @@
1
+ Sequel.migration do
2
+ change do
3
+ create_table(:influencers_users) do
4
+ foreign_key :user_id, :users, :on_delete => :cascade, :index => true
5
+ foreign_key :influencer_id, :influencers, :on_delete => :cascade, :index => true
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,10 @@
1
+ Sequel.migration do
2
+ change do
3
+ create_table(:influencees) do
4
+ primary_key :id
5
+ foreign_key :workspace_id, :workspaces, :on_delete => :cascade, :index => true
6
+ String :screen_name, :index => true
7
+ DateTime :created_at, :index => true
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,8 @@
1
+ Sequel.migration do
2
+ change do
3
+ create_table(:influencees_users) do
4
+ foreign_key :user_id, :users, :on_delete => :cascade, :index => true
5
+ foreign_key :influencee_id, :influencees, :on_delete => :cascade, :index => true
6
+ end
7
+ end
8
+ end
data/exe/birdwatcher ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "birdwatcher"
4
+ require "readline"
5
+
6
+ trap "SIGINT" do
7
+ print "\n"
8
+ Birdwatcher::Console.instance.warn("Caught interrupt; Exiting.")
9
+ exit
10
+ end
11
+
12
+ Birdwatcher::Console.instance.start!
@@ -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,16 @@
1
+ module Birdwatcher
2
+ module Commands
3
+ class Exit < Birdwatcher::Command
4
+ self.meta = {
5
+ :description => "Exit Birdwatcher",
6
+ :names => %w(exit quit q),
7
+ :usage => "exit"
8
+ }
9
+
10
+ def run
11
+ output "Goodbye."
12
+ exit
13
+ end
14
+ end
15
+ end
16
+ 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