restaurant_week_boston 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/ChangeLog ADDED
@@ -0,0 +1,2 @@
1
+ 1.0.0 / 2010-08-14
2
+ * Birthday!
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Gabe Berke-Williams
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.rdoc ADDED
@@ -0,0 +1,65 @@
1
+ = Restaurant Week Boston
2
+ This is just a silly little gem I made that parses the Restaurant Week
3
+ Boston (http://www.restaurantweekboston.com/) website and lets you mark which
4
+ ones you want to go to for lunch, dinner, or lunch and dinner.
5
+
6
+ = Installation
7
+ <tt>gem install restaurant_week_boston</tt>
8
+
9
+ = Usage
10
+ Use the +restaurant_week_boston+ command line script.
11
+ Use -a/--all, -l/--lunch, and -d/--dinner options to mark restaurants that
12
+ you want to go to for lunch *and* dinner, lunch, or dinner, respectively.
13
+
14
+ So if you want to go to Bond and Aquitaine Bis for lunch and dinner, you'd
15
+ do
16
+
17
+ <tt>$ restaurant_week_boston -a 'aquitaine bis',bond</tt>
18
+
19
+ === More involved demo
20
+
21
+ <tt>$ restaurant_week_boston -a 'aquitaine bis' -l bond,asana -d bergamot,clink</tt>
22
+ Getting doc...done.
23
+ Parsing doc...done.
24
+
25
+ === LUNCH ===
26
+ Name: Asana (short-name: asana)
27
+ Meals: Lunch, Dinner
28
+ Neighborhood: Back Bay
29
+ Phone: 617-535-8800
30
+ Lunch Menu: http://www.restaurantweekboston.com/fetch/asana/lunch/
31
+ Dinner Menu: http://www.restaurantweekboston.com/fetch/asana/dinner/
32
+ Map: http://www.restaurantweekboston.com/map/back-bay/asana/#topOfMap
33
+ --------------------------------------------------------------------------------
34
+ Name: Bond (short-name: bond)
35
+ Meals: Lunch, Dinner
36
+ Neighborhood: Downtown
37
+ Phone: 617-451-1900
38
+ Lunch Menu: http://www.restaurantweekboston.com/fetch/bond/lunch/
39
+ Dinner Menu: http://www.restaurantweekboston.com/fetch/bond/dinner/
40
+ Map: http://www.restaurantweekboston.com/map/downtown/bond/#topOfMap
41
+
42
+ === DINNER ===
43
+ Name: Bergamot (short-name: bergamot)
44
+ Meals: Dinner
45
+ Neighborhood: Somerville
46
+ Phone: <no phone given>
47
+ Dinner Menu: http://www.restaurantweekboston.com/fetch/bergamot/dinner/
48
+ Map: http://www.restaurantweekboston.com/map/somerville/bergamot/#topOfMap
49
+ --------------------------------------------------------------------------------
50
+ Name: Clink. at the Liberty Hotel (short-name: clink)
51
+ Meals: Lunch, Dinner
52
+ Neighborhood: Beacon Hill
53
+ Phone: 617-224-4004
54
+ Lunch Menu: http://www.restaurantweekboston.com/fetch/clink/lunch/
55
+ Dinner Menu: http://www.restaurantweekboston.com/fetch/clink/dinner/
56
+ Map: http://www.restaurantweekboston.com/map/beacon-hill/clink/#topOfMap
57
+
58
+ === LUNCH OR DINNER ===
59
+ Name: Aquitaine Bis (short-name: aquitaine-bis)
60
+ Meals: Lunch, Dinner
61
+ Neighborhood: Chestnut Hill
62
+ Phone: 617-734-8400
63
+ Lunch Menu: http://www.restaurantweekboston.com/fetch/aquitaine-bis/lunch/
64
+ Dinner Menu: http://www.restaurantweekboston.com/fetch/aquitaine-bis/dinner/
65
+ Map: http://www.restaurantweekboston.com/map/chestnut-hill/aquitaine-bis/#topOfMap
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'restaurant_week_boston'
4
+ runner = RestaurantWeekBoston::Runner.new(ARGV)
5
+ runner.run()
@@ -0,0 +1,63 @@
1
+ require 'restaurant_week_boston/scraper'
2
+
3
+ module RestaurantWeekBoston
4
+ # Uses a given Scraper and marks restaurants as good for lunch, dinner, any,
5
+ # etc.
6
+ class Marker
7
+ # +opts+ is a hash with keys for +:lunch+, +:dinner+, and +:any+.
8
+ # The values of each of these keys should be an array suitable for passing
9
+ # to Scraper#special_find().
10
+ def initialize(scraper, opts)
11
+ @scraper = scraper
12
+ @opts = opts
13
+ end
14
+
15
+ # Print out restaurants that are good for lunch.
16
+ def lunch
17
+ unless @lunch
18
+ if @opts.key?(:lunch)
19
+ @lunch = @scraper.special_find(@opts[:lunch])
20
+ else
21
+ @lunch = '[no lunch options specified]'
22
+ end
23
+ end
24
+ puts "=== LUNCH ==="
25
+ puts @lunch
26
+ end
27
+
28
+ # Print out restaurants that are good for dinner.
29
+ def dinner
30
+ unless @dinner
31
+ if @opts.key?(:dinner)
32
+ @dinner = @scraper.special_find(@opts[:dinner])
33
+ else
34
+ @dinner = '[no dinner options specified]'
35
+ end
36
+ end
37
+ puts "=== DINNER ==="
38
+ puts @dinner
39
+ end
40
+
41
+ # Print out restaurants that are good for lunch or dinner.
42
+ def any
43
+ unless @any
44
+ if @opts.key?(:any)
45
+ @any = @scraper.special_find(@opts[:any])
46
+ else
47
+ @any = '[no "lunch or dinner" options specified]'
48
+ end
49
+ end
50
+ puts "=== LUNCH OR DINNER ==="
51
+ puts @any
52
+ end
53
+
54
+ # Print lunch(), dinner(), and any(), with newlines between them.
55
+ def all
56
+ lunch()
57
+ puts
58
+ dinner()
59
+ puts
60
+ any()
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,134 @@
1
+ module RestaurantWeekBoston
2
+ # A Restaurant has the following properties:
3
+ # * name ("224 Boston Street")
4
+ # * short-name ("224-boston-street", used internally by RWB site)
5
+ # * meals: %w{lunch dinner}, %w{lunch}, %w{dinner}
6
+ # * neighborhood: "back-bay", etc
7
+ # * phone: "617-555-1234"
8
+ # * menu: use menu_link_for("lunch" || "dinner")
9
+ # * map_link: a link to the RWB map page for that Restaurant
10
+ class Restaurant
11
+ include Comparable
12
+
13
+ # +entry+ is a Nokogiri::XML::Node from the RWB web site.
14
+ def initialize(entry)
15
+ @entry = entry
16
+ end
17
+
18
+
19
+ # Compares this Restaurant's name to +other+'s name, and returns -1, 0, 1
20
+ # as appropriate.
21
+ def <=>(other)
22
+ return name <=> other.name
23
+ end
24
+
25
+
26
+ # Generate a pretty representation of the Restaurant.
27
+ def to_s
28
+ # back-bay -> "Back Bay"
29
+ pretty_neighborhood = neighborhood.split('-').map{|x| x.capitalize }.join(' ')
30
+
31
+ s = "Name: #{name} (short-name: #{short_name})"
32
+ s += "\n"
33
+ s += "Meals: #{meals.map(&:capitalize).join(', ')}"
34
+ s += "\n"
35
+ s += "Neighborhood: #{pretty_neighborhood}"
36
+ s += "\n"
37
+ s += "Phone: #{phone}"
38
+ @meals.each do |meal|
39
+ s += "\n"
40
+ s += "%s Menu: #{menu_link_for(meal)}" % [meal.capitalize]
41
+ end
42
+ s += "\n"
43
+ s += "Map: #{map_link}"
44
+ s
45
+ end
46
+
47
+
48
+ # Returns "<Restaurant: #{restaurant-name}>"
49
+ def inspect
50
+ "<Restaurant: #{name}>"
51
+ end
52
+
53
+ # Returns true if this Restaurant offers lunch.
54
+ def offers_lunch?
55
+ meals.include?('lunch')
56
+ end
57
+
58
+ # Returns true if this Restaurant offers dinner.
59
+ def offers_dinner?
60
+ meals.include?('dinner')
61
+ end
62
+
63
+ # Return the short name (e.g. '224-boston-street' for "224 Boston Street").
64
+ def short_name
65
+ @short_name ||= @entry[:id].sub('restaurantID-', '')
66
+ end
67
+
68
+ # Return the full name of this Restaurant, e.g. "224 Boston Street".
69
+ def name
70
+ unless @name
71
+ # UGLY, but it's not wrapped in a tag so there it is.
72
+ # split: ["224", "Boston", "Street", "[", "map", "]"]
73
+ split = @entry.css('h4')[0].text.strip.split(/\s+/)
74
+ # Remove [[", "map", "]"]
75
+ split.slice!(-3, 3)
76
+ @name = split.join(' ')
77
+ end
78
+ @name
79
+ end
80
+
81
+ # Return a 0-2 element array containing the meals offered by this
82
+ # Restaurant: "lunch" and/or "dinner".
83
+ def meals
84
+ unless @meals
85
+ @meals = []
86
+ lunch = ! @entry.css('a.lunchMenuButton').empty?
87
+ dinner = ! @entry.css('a.dinnerMenuButton').empty?
88
+ @meals << 'lunch' if lunch
89
+ @meals << 'dinner' if dinner
90
+ end
91
+ @meals
92
+ end
93
+
94
+ # Return the neighborhood this Restaurant is in (e.g. "back-bay").
95
+ def neighborhood
96
+ unless @neighborhood
97
+ link = @entry.css('a[@href*="neighborhood"]').first
98
+ @neighborhood = link.attributes['href'].value.sub('/?neighborhood=', '')
99
+ end
100
+ @neighborhood
101
+ end
102
+
103
+
104
+ # Return this Restaurant's phone number, or "<no phone given>" is none is
105
+ # provided.
106
+ def phone
107
+ unless @phone
108
+ # UGLY, but it's not wrapped in a tag so there it is.
109
+ phone = @entry.css('.restaurantInfoBasic > p').children[6].to_s
110
+ if phone == '<br>'
111
+ @phone = '<no phone given>'
112
+ else
113
+ @phone = phone
114
+ end
115
+ end
116
+ @phone
117
+ end
118
+
119
+ # +meal+ should be either "lunch" or "dinner"
120
+ def menu_link_for(meal)
121
+ if meal == 'lunch'
122
+ @lunch_menu_link ||= "http://www.restaurantweekboston.com/fetch/#{short_name}/lunch/"
123
+ elsif meal == 'dinner'
124
+ @dinner_menu_link ||= "http://www.restaurantweekboston.com/fetch/#{short_name}/dinner/"
125
+ end
126
+ end
127
+
128
+
129
+ # Return the URL to the map of this Restaurant at the RWB web site.
130
+ def map_link
131
+ @map_link ||= "http://www.restaurantweekboston.com/map/#{neighborhood}/#{short_name}/#topOfMap"
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,55 @@
1
+ require 'optparse'
2
+ require 'restaurant_week_boston/scraper'
3
+ require 'restaurant_week_boston/marker'
4
+
5
+ module RestaurantWeekBoston
6
+ # Used in the executable script, +restaurant_week_boston+.
7
+ class Runner
8
+ # Pass in ARGV.
9
+ def initialize(argv)
10
+ options = {}
11
+ optparse = OptionParser.new do |opts|
12
+ opts.banner = "Usage: #{$0} [options] restaurant1, restaurant2, ..."
13
+
14
+ options[:meal] = :any
15
+ opts.on('-m', '--meal MEAL', 'Only get restaurants that offer this meal (lunch/dinner/both/any)') do |m|
16
+ options[:meal] = m
17
+ end
18
+
19
+ opts.on('-l', '--lunch RESTO1,RESTO2', 'Mark RESTAURANTS as good for lunch') do |lunches|
20
+ options[:lunch] = lunches.split(',')
21
+ end
22
+
23
+ opts.on('-d', '--dinner RESTO1,RESTO2', 'Mark RESTAURANTS as good for dinner') do |dinners|
24
+ options[:dinner] = dinners.split(',')
25
+ end
26
+
27
+ opts.on('-a', '--any RESTO1,RESTO2', 'Mark RESTAURANTS as good for any meal') do |any|
28
+ options[:any] = any.split(',')
29
+ end
30
+
31
+ options[:neighborhood] = :all # NOT any!
32
+ opts.on('-n', '--neighborhood HOOD',
33
+ 'Only get restaurants in this neighborhood (dorchester, back-bay, etc)') do |n|
34
+ n = :all if n == 'any' # :any makes it choke
35
+ options[:neighborhood] = n
36
+ end
37
+
38
+ opts.on('-h', '--help', 'Display this help') do
39
+ puts opts
40
+ return
41
+ end
42
+ end
43
+ optparse.parse!(argv)
44
+ @options = options
45
+ end
46
+
47
+ def run
48
+ scraper = Scraper.new(:meal => @options[:meal],
49
+ :neighborhood => @options[:neighborhood])
50
+ marker = Marker.new(scraper, @options)
51
+
52
+ puts marker.all
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,85 @@
1
+ require 'nokogiri'
2
+ require 'open-uri'
3
+
4
+ require 'restaurant_week_boston/restaurant'
5
+
6
+ module RestaurantWeekBoston
7
+ # Scrapes Restaurant Week site.
8
+ class Scraper
9
+ include Enumerable
10
+ # +opts+ is a hash of options, with the following keys:
11
+ # :neighborhood:: dorchester, back-bay, etc. (default: "all")
12
+ # :meal:: lunch, dinner, both, or any (default: "any"). Will create a
13
+ # file in your home directory called ".restaurant_week_boston.cache"
14
+ # which contains the HTML from the RWB site, just so it doesn't have to
15
+ # keep getting it. You can delete that file, it'll just take longer next
16
+ # time since it will have to re-get the HTML.
17
+ def initialize(opts = {})
18
+ @url = create_url(opts)
19
+ @dump = File.expand_path('~/.restaurant_week_boston.cache')
20
+ entries = doc().css('.restaurantEntry')
21
+ @restaurants = entries.map{ |entry| Restaurant.new(entry) }
22
+ end
23
+
24
+ # +opts+ is a hash of options, with the following keys:
25
+ # :neighborhood:: dorchester, back-bay, etc. (default: :all)
26
+ # :meal:: lunch, dinner, both, or any (default: :any)
27
+ def create_url(opts = {})
28
+ # meal: any/lunch/dinner/both
29
+ # &view=all
30
+ default_opts = {:neighborhood => :all,
31
+ :meal => :any }
32
+ opts = default_opts.merge!(opts)
33
+ sprintf('http://www.restaurantweekboston.com/?neighborhood=%s&meal=%s&view=all',
34
+ opts[:neighborhood].to_s, opts[:meal].to_s)
35
+ end
36
+
37
+
38
+ # Iterates over @restaurants. All methods in Enumerable work.
39
+ def each(&blk)
40
+ @restaurants.each(&blk)
41
+ end
42
+
43
+ # Returns the result of open()ing the url from create_url(), as a String.
44
+ def get_html
45
+ print "Getting doc..."
46
+ if File.size? @dump
47
+ html = File.read(@dump)
48
+ else
49
+ html = open(@url).read()
50
+ f = File.new(@dump, 'w')
51
+ f.write(html)
52
+ f.close()
53
+ end
54
+ puts "done."
55
+ html
56
+ end
57
+
58
+ # Return a Nokogiri::HTML::Document parsed from get_html. Prints status
59
+ # messages along the way.
60
+ def doc
61
+ # get_html beforehand for good output messages
62
+ html = get_html
63
+ print "Parsing doc..."
64
+ doc = Nokogiri::HTML(html)
65
+ puts "done."
66
+ puts
67
+ doc
68
+ end
69
+
70
+ # Pass in an array of names that =~ (case-insensitive) the ones you're
71
+ # thinking of, and this will get those. So, if you're thinking of Bond,
72
+ # 224 Boston Street, and Artu, you can pass in ['bond', 'boston street',
73
+ # 'artu'].
74
+ def special_find(array)
75
+ array.map! do |name|
76
+ /#{Regexp.escape(name)}/i
77
+ end
78
+ results = @restaurants.find_all do |restaurant|
79
+ array.detect{ |regexp| regexp =~ restaurant.name }
80
+ end
81
+ # Add separators
82
+ results.join("\n" + '-' * 80 + "\n")
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,5 @@
1
+ require 'rubygems'
2
+ require 'restaurant_week_boston/scraper'
3
+ require 'restaurant_week_boston/marker'
4
+ require 'restaurant_week_boston/restaurant'
5
+ require 'restaurant_week_boston/runner'
@@ -0,0 +1,39 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{restaurant_week_boston}
5
+ s.version = "1.0.1"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["Gabe Berke-Williams"]
9
+ s.date = %q{2010-08-14}
10
+ s.summary= %q{A fast, easy way to search the Boston Restaurant Week site and mark your favorites.}
11
+ s.description = %q{A fast, easy way to search the Boston Restaurant Week site and mark your favorites.}
12
+ s.email = %q{gbw@brandeis.edu}
13
+ s.extra_rdoc_files = ["README.rdoc"]
14
+ s.executables = ["restaurant_week_boston"]
15
+ s.default_executable = %q{restaurant_week_boston}
16
+ s.files = [
17
+ "ChangeLog",
18
+ "LICENSE",
19
+ "README.rdoc",
20
+ "bin/restaurant_week_boston",
21
+ "lib/restaurant_week_boston.rb",
22
+ "lib/restaurant_week_boston/scraper.rb",
23
+ "lib/restaurant_week_boston/marker.rb",
24
+ "lib/restaurant_week_boston/restaurant.rb",
25
+ "lib/restaurant_week_boston/runner.rb",
26
+ "restaurant_week_boston.gemspec"
27
+ ]
28
+ s.homepage = %q{http://github.com/gabebw/restaurant_week_boston}
29
+ s.rdoc_options = ["--charset=UTF-8"]
30
+ s.require_paths = ["lib"]
31
+ s.rubygems_version = %q{1.3.7}
32
+ s.test_files = []
33
+
34
+ if s.respond_to? :specification_version then
35
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
36
+ s.specification_version = 3
37
+ end
38
+ end
39
+
metadata ADDED
@@ -0,0 +1,73 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: restaurant_week_boston
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 1
7
+ - 0
8
+ - 1
9
+ version: 1.0.1
10
+ platform: ruby
11
+ authors:
12
+ - Gabe Berke-Williams
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-08-14 00:00:00 -07:00
18
+ default_executable: restaurant_week_boston
19
+ dependencies: []
20
+
21
+ description: A fast, easy way to search the Boston Restaurant Week site and mark your favorites.
22
+ email: gbw@brandeis.edu
23
+ executables:
24
+ - restaurant_week_boston
25
+ extensions: []
26
+
27
+ extra_rdoc_files:
28
+ - README.rdoc
29
+ files:
30
+ - ChangeLog
31
+ - LICENSE
32
+ - README.rdoc
33
+ - bin/restaurant_week_boston
34
+ - lib/restaurant_week_boston.rb
35
+ - lib/restaurant_week_boston/scraper.rb
36
+ - lib/restaurant_week_boston/marker.rb
37
+ - lib/restaurant_week_boston/restaurant.rb
38
+ - lib/restaurant_week_boston/runner.rb
39
+ - restaurant_week_boston.gemspec
40
+ has_rdoc: true
41
+ homepage: http://github.com/gabebw/restaurant_week_boston
42
+ licenses: []
43
+
44
+ post_install_message:
45
+ rdoc_options:
46
+ - --charset=UTF-8
47
+ require_paths:
48
+ - lib
49
+ required_ruby_version: !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ segments:
55
+ - 0
56
+ version: "0"
57
+ required_rubygems_version: !ruby/object:Gem::Requirement
58
+ none: false
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ segments:
63
+ - 0
64
+ version: "0"
65
+ requirements: []
66
+
67
+ rubyforge_project:
68
+ rubygems_version: 1.3.7
69
+ signing_key:
70
+ specification_version: 3
71
+ summary: A fast, easy way to search the Boston Restaurant Week site and mark your favorites.
72
+ test_files: []
73
+