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.
- data/bin/so +6 -0
- data/lib/stackoverflow.rb +172 -13
- metadata +82 -7
data/bin/so
ADDED
data/lib/stackoverflow.rb
CHANGED
@@ -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
|
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
|
-
|
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
|
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
|
150
|
-
puts
|
151
|
-
puts "\
|
152
|
-
puts "
|
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 =
|
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
|
-
-
|
9
|
-
version: 0.1.
|
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
|
-
|
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: []
|