stackoverflow 0.1.0 → 0.1.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.
Files changed (3) hide show
  1. data/bin/so +6 -0
  2. data/lib/stackoverflow.rb +172 -13
  3. metadata +82 -7
data/bin/so ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'stackoverflow'
4
+
5
+ StackOverflow::Command.new.run
6
+
@@ -1,4 +1,22 @@
1
1
  #!/usr/bin/env ruby
2
+ #
3
+ # Copyright (C) 2012 Xavier Antoviaque <xavier@antoviaque.org>
4
+ #
5
+ # This software's license gives you freedom; you can copy, convey,
6
+ # propagate, redistribute and/or modify this program under the terms of
7
+ # the GNU Affero General Public License (AGPL) as published by the Free
8
+ # Software Foundation (FSF), either version 3 of the License, or (at your
9
+ # option) any later version of the AGPL published by the FSF.
10
+ #
11
+ # This program is distributed in the hope that it will be useful, but
12
+ # WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
14
+ # General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU Affero General Public License
17
+ # along with this program in a file in the toplevel directory called
18
+ # "AGPLv3". If not, see <http://www.gnu.org/licenses/>.
19
+ #
2
20
 
3
21
  require 'rubygems'
4
22
  require 'open-uri'
@@ -39,16 +57,31 @@ module StackOverflow
39
57
 
40
58
  class DB
41
59
  def initialize
42
- @db = SQLite3::Database.new("../data/stackoverflow.db")
60
+ @db = SQLite3::Database.new(File.join(Dir.home, ".stackoverflow/stackoverflow.db"))
61
+ @db_idx = SQLite3::Database.new(File.join(Dir.home, ".stackoverflow/stackoverflow_idx.db"))
43
62
  end
44
63
 
45
64
  def search(search_string)
46
- sql = "SELECT id, score, body, title FROM posts WHERE post_type_id=1 AND "
65
+ # Search on titles in the small index DB, to get the ids faster
66
+ sql = "SELECT id FROM questions WHERE "
47
67
  sub_sql = []
48
68
  for search_term in search_string.split(' ')
49
- sub_sql << "title LIKE '%#{search_term}%"
69
+ sub_sql << "title LIKE '%#{search_term}%'"
50
70
  end
51
71
  sql += sub_sql.join(' AND ')
72
+
73
+ questions_ids = []
74
+ @db_idx.execute(sql) do |row|
75
+ questions_ids << row[0]
76
+ end
77
+
78
+ # Then retreive details from the main (large) DB
79
+ sql = "SELECT id, score, body, title FROM posts WHERE "
80
+ sub_sql = []
81
+ for question_id in questions_ids
82
+ sub_sql << "id=#{question_id}"
83
+ end
84
+ sql += sub_sql.join(' OR ')
52
85
  sql += ' ORDER BY score DESC'
53
86
 
54
87
  questions = []
@@ -63,8 +96,23 @@ module StackOverflow
63
96
  end
64
97
 
65
98
  def get_answers(question_id)
99
+ # Search on parent ids in the small index DB, to get the ids faster
100
+ sql = "SELECT id FROM answers WHERE parent_id=#{question_id}"
101
+ answers_ids = []
102
+ @db_idx.execute(sql) do |row|
103
+ answers_ids << row[0]
104
+ end
105
+
106
+ # Then retreive details from the main (large) DB
107
+ sql = "SELECT id, score, body FROM posts WHERE "
108
+ sub_sql = []
109
+ for answer_id in answers_ids
110
+ sub_sql << "id=#{answer_id}"
111
+ end
112
+ sql += sub_sql.join(' OR ')
113
+ sql += ' ORDER BY score DESC'
114
+
66
115
  answers = []
67
- sql = "SELECT id, score, body FROM posts WHERE post_type_id=2 AND parent_id=#{question_id} ORDER BY score DESC"
68
116
  @db.execute(sql) do |row|
69
117
  answers << { :id => row[0],
70
118
  :score => row[1],
@@ -73,6 +121,104 @@ module StackOverflow
73
121
  end
74
122
  end
75
123
 
124
+ class DBUpdater
125
+ def initialize
126
+ @dir_path = File.join(Dir.home, ".stackoverflow")
127
+ @db_version_path = File.join(@dir_path, "db_version")
128
+
129
+ @remote_hostname = "foobbs.org"
130
+ @remote_path = "/tmp/so/"
131
+ end
132
+
133
+ def check_local_dir
134
+ Dir.mkdir(@dir_path) if not directory_exists?(@dir_path)
135
+ end
136
+
137
+ def get_db_version
138
+ db_version = 0
139
+ if file_exists?(@db_version_path)
140
+ File.open(@db_version_path, 'r') { |f| db_version = f.read().to_i }
141
+ end
142
+ db_version
143
+ end
144
+
145
+ def set_db_version(version_nb)
146
+ File.open(@db_version_path, 'w+') { |f| f.write(version_nb.to_s) }
147
+ end
148
+
149
+ def get_remote_db_version
150
+ remote_db_version = 0
151
+ Net::HTTP.start(@remote_hostname) do |http|
152
+ resp = http.get(File.join(@remote_path, "db_version"))
153
+ resp.body.to_i
154
+ end
155
+ end
156
+
157
+ def update
158
+ check_local_dir
159
+ remote_db_version = get_remote_db_version
160
+ db_version = get_db_version
161
+ if db_version < remote_db_version
162
+ puts "Database update found!"
163
+ puts "Updating from version #{db_version} to version #{remote_db_version} (several GB to download - this can take a while)..."
164
+ download_all
165
+ set_db_version(remote_db_version)
166
+ end
167
+ puts "The database is up to date (version #{get_db_version})."
168
+ end
169
+
170
+ def download_all
171
+ download_file "stackoverflow_idx.db.gz"
172
+ gunzip_file "stackoverflow_idx.db.gz"
173
+ download_file "stackoverflow.db.gz"
174
+ gunzip_file "stackoverflow.db.gz"
175
+ end
176
+
177
+ def download_file(filename)
178
+ puts "Downloading #{filename}..."
179
+ Net::HTTP.start(@remote_hostname) do |http|
180
+ f = open(File.join(@dir_path, filename), "wb")
181
+ begin
182
+ http.request_get(File.join(@remote_path, filename)) do |resp|
183
+ resp.read_body do |segment|
184
+ f.write(segment)
185
+ end
186
+ end
187
+ ensure
188
+ f.close()
189
+ end
190
+ end
191
+ end
192
+
193
+ def gunzip_file(filename)
194
+ puts "Unpacking #{filename}..."
195
+ gz_file_path = File.join(@dir_path, filename)
196
+ z = Zlib::Inflate.new(16+Zlib::MAX_WBITS)
197
+
198
+ File.open(gz_file_path) do |f|
199
+ File.open(gz_file_path[0...-3], "w") do |w|
200
+ f.each do |str|
201
+ w << z.inflate(str)
202
+ end
203
+ end
204
+ end
205
+ z.finish
206
+ z.close
207
+ File.delete(gz_file_path)
208
+ end
209
+
210
+ def directory_exists?(path)
211
+ return false if not File.exist?(path) or not File.directory?(path)
212
+ true
213
+ end
214
+
215
+ def file_exists?(path)
216
+ return false if not File.exist?(path) or not File.file?(path)
217
+ true
218
+ end
219
+
220
+ end
221
+
76
222
  class Formatter
77
223
  def questions_list(questions)
78
224
  nb = 1
@@ -126,15 +272,25 @@ module StackOverflow
126
272
  def run
127
273
  options = {}
128
274
  OptionParser.new do |opts|
129
- opts.banner = "Usage: so [options] <search string> [<question_id>]"
275
+ opts.banner = "** Usage: so [options] <search string> [<question_id>]"
130
276
  opts.on("-h", "--help", "Help") do |v|
131
- help(opts)
277
+ help
278
+ exit
279
+ end
280
+ opts.on("-u", "--update", "Update local database") do |v|
281
+ DBUpdater.new.update
282
+ exit
132
283
  end
133
284
  opts.on("-o", "--offline", "Offline mode") do |v|
134
285
  options['offline'] = true
135
286
  end
136
287
  end.parse!
137
288
 
289
+ if ARGV.length < 1
290
+ help
291
+ exit
292
+ end
293
+
138
294
  # last argument is integer when user is specifing a question_nb from the results
139
295
  question_nb = nil
140
296
  if ARGV[-1] =~ /^[0-9]+$/
@@ -142,20 +298,23 @@ module StackOverflow
142
298
  end
143
299
 
144
300
  search_string = ARGV.join(' ')
145
-
146
301
  search(search_string, question_nb, options)
147
302
  end
148
303
 
149
- def help(opts)
150
- puts opts.banner + "\n\n"
151
- puts "\t<search_string>: Search Stack Overflow for a combination of words"
152
- puts "\t<question_id>: (Optional) Display the question with this #id from the search results"
304
+ def help
305
+ puts "** Usage: so [options] <search string> [<question_id>]"
306
+ puts " so --update\n\n"
307
+ puts "Arguments:"
308
+ puts "\t<search_string> : Search Stack Overflow for a combination of words"
309
+ puts "\t<question_id> : (Optional) Display the question with this #id from the search results\n\n"
310
+ puts "Options:"
311
+ puts "\t-o --offline : Query the local database instead of the online StackOverflow API (offline mode)"
312
+ puts "\t-u --update : Download or update the local database of StackOverflow answers (7GB+)\n\n"
153
313
  end
154
314
 
155
315
  def search(search_string, question_nb, options)
156
316
  if options['offline']
157
- api = API.new
158
- #api = DB.new
317
+ api = DB.new
159
318
  else
160
319
  api = API.new
161
320
  end
metadata CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
5
5
  segments:
6
6
  - 0
7
7
  - 1
8
- - 0
9
- version: 0.1.0
8
+ - 1
9
+ version: 0.1.1
10
10
  platform: ruby
11
11
  authors:
12
12
  - Xavier Antoviaque
@@ -16,18 +16,93 @@ cert_chain: []
16
16
 
17
17
  date: 2012-03-24 00:00:00 -04:00
18
18
  default_executable:
19
- dependencies: []
20
-
21
- description: Query StackOverflow from the command line (offline/online modes)
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: json
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ segments:
29
+ - 1
30
+ - 6
31
+ - 5
32
+ version: 1.6.5
33
+ type: :runtime
34
+ version_requirements: *id001
35
+ - !ruby/object:Gem::Dependency
36
+ name: libxml-ruby
37
+ prerelease: false
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ segments:
44
+ - 2
45
+ - 3
46
+ - 2
47
+ version: 2.3.2
48
+ type: :runtime
49
+ version_requirements: *id002
50
+ - !ruby/object:Gem::Dependency
51
+ name: nokogiri
52
+ prerelease: false
53
+ requirement: &id003 !ruby/object:Gem::Requirement
54
+ none: false
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ segments:
59
+ - 1
60
+ - 5
61
+ - 2
62
+ version: 1.5.2
63
+ type: :runtime
64
+ version_requirements: *id003
65
+ - !ruby/object:Gem::Dependency
66
+ name: sqlite3
67
+ prerelease: false
68
+ requirement: &id004 !ruby/object:Gem::Requirement
69
+ none: false
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ segments:
74
+ - 1
75
+ - 3
76
+ - 5
77
+ version: 1.3.5
78
+ type: :runtime
79
+ version_requirements: *id004
80
+ - !ruby/object:Gem::Dependency
81
+ name: terminal-table
82
+ prerelease: false
83
+ requirement: &id005 !ruby/object:Gem::Requirement
84
+ none: false
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ segments:
89
+ - 1
90
+ - 4
91
+ - 5
92
+ version: 1.4.5
93
+ type: :runtime
94
+ version_requirements: *id005
95
+ description: Allows to query Stack Overflow's questions & answers from the command line. It can either be used in 'online' mode, where the StackOverflow API is queried, or offline, by downloading the latest dump released by StackOverflow.
22
96
  email: xavier@antoviaque.org
23
- executables: []
24
-
97
+ executables:
98
+ - so
25
99
  extensions: []
26
100
 
27
101
  extra_rdoc_files: []
28
102
 
29
103
  files:
30
104
  - lib/stackoverflow.rb
105
+ - bin/so
31
106
  has_rdoc: true
32
107
  homepage: https://github.com/antoviaque/stack-overflow-command-line
33
108
  licenses: []