kayak 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,2 @@
1
+ == 0.0.1 2008-01-23
2
+ * Initial release
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2007 FIXME full name
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.
@@ -0,0 +1,40 @@
1
+ History.txt
2
+ License.txt
3
+ Manifest.txt
4
+ README.txt
5
+ Rakefile
6
+ autotest/discover.rb
7
+ autotest/testunit.rb
8
+ config/hoe.rb
9
+ config/requirements.rb
10
+ init.rb
11
+ lib/kayak.rb
12
+ lib/kayak/flight_search.rb
13
+ lib/kayak/hotel.rb
14
+ lib/kayak/leg.rb
15
+ lib/kayak/search.rb
16
+ lib/kayak/search_results.rb
17
+ lib/kayak/session.rb
18
+ lib/kayak/trip.rb
19
+ lib/kayak/version.rb
20
+ log/debug.log
21
+ script/destroy
22
+ script/generate
23
+ script/txt2html
24
+ setup.rb
25
+ tasks/deployment.rake
26
+ tasks/environment.rake
27
+ tasks/website.rake
28
+ test/fixtures/flight_results.xml
29
+ test/fixtures/flight_start.xml
30
+ test/fixtures/session.xml
31
+ test/fixtures/token_invalid.xml
32
+ test/kayak/search_results_test.rb
33
+ test/kayak/search_test.rb
34
+ test/kayak/session_test.rb
35
+ test/test_helper.rb
36
+ website/index.html
37
+ website/index.txt
38
+ website/javascripts/rounded_corners_lite.inc.js
39
+ website/stylesheets/screen.css
40
+ website/template.rhtml
@@ -0,0 +1 @@
1
+ http://www.kayak.com/labs/api/search/
@@ -0,0 +1,4 @@
1
+ require 'config/requirements'
2
+ require 'config/hoe' # setup Hoe + all gem configuration
3
+
4
+ Dir['tasks/**/*.rake'].each { |rake| load rake }
@@ -0,0 +1,5 @@
1
+ require File.dirname(__FILE__) + '/testunit'
2
+
3
+ Autotest.add_discovery do
4
+ "testunit"
5
+ end
@@ -0,0 +1,28 @@
1
+ require 'autotest'
2
+
3
+ class Autotest::Testunit < Autotest
4
+
5
+ def initialize # :nodoc:
6
+ super
7
+ @exceptions = /^\.\/(?:db|doc|log|public|script|tmp|vendor\/rails)/
8
+
9
+ @test_mappings = {
10
+ %r%^test/.*\.rb$% => proc { |filename, _|
11
+ filename
12
+ },
13
+ %r%^lib/(.*)\.rb$% => proc { |_, m|
14
+ ["test/#{m[1]}_test.rb"]
15
+ },
16
+ %r%^test/test_helper.rb$% => proc {
17
+ files_matching %r%^test/.*_test\.rb$%
18
+ },
19
+ }
20
+ end
21
+
22
+ # Given the string filename as the path, determine
23
+ # the corresponding tests for it, in an array.
24
+ def tests_for_file(filename)
25
+ super.select { |f| @files.has_key? f }
26
+ end
27
+
28
+ end
@@ -0,0 +1,71 @@
1
+ require 'kayak/version'
2
+
3
+ AUTHOR = 'Brandon Keepers'
4
+ EMAIL = "brandon@opensoul.org"
5
+ DESCRIPTION = "Ruby wrapper for Kayak.com's API for searching flights and hotels"
6
+ GEM_NAME = 'kayak'
7
+ RUBYFORGE_PROJECT = 'kayak'
8
+ HOMEPATH = "http://#{RUBYFORGE_PROJECT}.rubyforge.org"
9
+ DOWNLOAD_PATH = "http://rubyforge.org/projects/#{RUBYFORGE_PROJECT}"
10
+
11
+ @config_file = "~/.rubyforge/user-config.yml"
12
+ @config = nil
13
+ RUBYFORGE_USERNAME = "brandon"
14
+ def rubyforge_username
15
+ unless @config
16
+ begin
17
+ @config = YAML.load(File.read(File.expand_path(@config_file)))
18
+ rescue
19
+ puts <<-EOS
20
+ ERROR: No rubyforge config file found: #{@config_file}
21
+ Run 'rubyforge setup' to prepare your env for access to Rubyforge
22
+ - See http://newgem.rubyforge.org/rubyforge.html for more details
23
+ EOS
24
+ exit
25
+ end
26
+ end
27
+ RUBYFORGE_USERNAME.replace @config["username"]
28
+ end
29
+
30
+
31
+ REV = nil
32
+ # UNCOMMENT IF REQUIRED:
33
+ # REV = `svn info`.each {|line| if line =~ /^Revision:/ then k,v = line.split(': '); break v.chomp; else next; end} rescue nil
34
+ VERS = Kayak::VERSION::STRING + (REV ? ".#{REV}" : "")
35
+ RDOC_OPTS = ['--quiet', '--title', 'kayak documentation',
36
+ "--opname", "index.html",
37
+ "--line-numbers",
38
+ "--main", "README",
39
+ "--inline-source"]
40
+
41
+ class Hoe
42
+ def extra_deps
43
+ @extra_deps.reject! { |x| Array(x).first == 'hoe' }
44
+ @extra_deps
45
+ end
46
+ end
47
+
48
+ # Generate all the Rake tasks
49
+ # Run 'rake -T' to see list of generated tasks (from gem root directory)
50
+ hoe = Hoe.new(GEM_NAME, VERS) do |p|
51
+ p.author = AUTHOR
52
+ p.description = DESCRIPTION
53
+ p.email = EMAIL
54
+ p.summary = DESCRIPTION
55
+ p.url = HOMEPATH
56
+ p.rubyforge_name = RUBYFORGE_PROJECT if RUBYFORGE_PROJECT
57
+ p.test_globs = ["test/**/*_test.rb"]
58
+ p.clean_globs |= ['**/.*.sw?', '*.gem', '.config', '**/.DS_Store'] #An array of file patterns to delete on clean.
59
+
60
+ # == Optional
61
+ p.changes = p.paragraphs_of("History.txt", 0..1).join("\n\n")
62
+ #p.extra_deps = [] # An array of rubygem dependencies [name, version], e.g. [ ['active_support', '>= 1.3.1'] ]
63
+
64
+ #p.spec_extras = {} # A hash of extra values to set in the gemspec.
65
+
66
+ end
67
+
68
+ CHANGES = hoe.paragraphs_of('History.txt', 0..1).join("\\n\\n")
69
+ PATH = (RUBYFORGE_PROJECT == GEM_NAME) ? RUBYFORGE_PROJECT : "#{RUBYFORGE_PROJECT}/#{GEM_NAME}"
70
+ hoe.remote_rdoc_dir = File.join(PATH.gsub(/^#{RUBYFORGE_PROJECT}\/?/,''), 'rdoc')
71
+ hoe.rsync_args = '-av --delete --ignore-errors'
@@ -0,0 +1,17 @@
1
+ require 'fileutils'
2
+ include FileUtils
3
+
4
+ require 'rubygems'
5
+ %w[rake hoe newgem rubigen].each do |req_gem|
6
+ begin
7
+ require req_gem
8
+ rescue LoadError
9
+ puts "This Rakefile requires the '#{req_gem}' RubyGem."
10
+ puts "Installation: gem install #{req_gem} -y"
11
+ exit
12
+ end
13
+ end
14
+
15
+ $:.unshift(File.join(File.dirname(__FILE__), %w[.. lib]))
16
+
17
+ require 'kayak'
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'kayak'
@@ -0,0 +1,27 @@
1
+ $:.unshift File.dirname(__FILE__)
2
+
3
+ require 'rubygems'
4
+ require 'hpricot'
5
+ require 'active_support'
6
+
7
+ require 'kayak/version'
8
+ require 'kayak/session'
9
+ require 'kayak/search'
10
+ require 'kayak/flight_search'
11
+ require 'kayak/search_results'
12
+ require 'kayak/trip'
13
+ require 'kayak/leg'
14
+ require 'kayak/hotel'
15
+
16
+ Date::DATE_FORMATS[:kayak] = "%m/%d/%Y"
17
+
18
+ class Hash
19
+ def to_date
20
+ date = symbolize_keys
21
+ [:year, :month, :day].each do |key|
22
+ raise ArgumentError, "Cannot convert to Date, missing #{key}" if !date.has_key?(key)
23
+ date[key] = date[key].to_i
24
+ end
25
+ Date.new(date[:year], date[:month], date[:day])
26
+ end
27
+ end
@@ -0,0 +1,25 @@
1
+ module Kayak
2
+ class FlightSearch < Search
3
+ attribute :oneway, :value => %w(y n) do |value|
4
+ ['y', '1', true].include?(value) ? 'y' : 'n'
5
+ end
6
+ attribute :origin, :destination, :value => /\w{3}/
7
+ attribute :depart_date, :return_date, :value => /\d{2}\/\d{2}\/\d{4}/ do |value|
8
+ value.to_date.to_s(:kayak) unless value.blank?
9
+ end
10
+ attribute :depart_time, :return_time, :values => %w(a r m 12 n e l)
11
+ attribute :travelers, :value => (1..8) do |value|
12
+ value.blank? ? 1 : value.to_i
13
+ end
14
+ attribute :cabin, :values => %w(f b e)
15
+
16
+ def initialize(attrs = {})
17
+ super({:oneway => false, :travelers => 1, :cabin => 'e'}.merge(attrs))
18
+ end
19
+
20
+ def to_search_params
21
+ attributes.merge(:action => 'doFlights')
22
+ end
23
+
24
+ end
25
+ end
@@ -0,0 +1,6 @@
1
+ module Kayak
2
+ class Hotel
3
+ attr_accessor :name, :stars, :price, :hiprice, :loprice, :url, :phone,
4
+ :address, :city, :region, :country
5
+ end
6
+ end
@@ -0,0 +1,11 @@
1
+ module Kayak
2
+ class Leg
3
+ attr_accessor :airline_code, :airline, :depart, :arrive, :stops,
4
+ :duration, :origin, :cabin, :destination
5
+
6
+ def to_s
7
+ "#{self.airline_code}:#{self.origin}>#{self.destination}:#{self.depart}:" +
8
+ "#{self.arrive}:#{self.stops}:#{self.duration}min"
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,41 @@
1
+ module Kayak
2
+ class Search
3
+ class_inheritable_accessor :valid_attributes
4
+ write_inheritable_attribute :valid_attributes, {}.with_indifferent_access
5
+ attr_reader :attributes
6
+
7
+ def initialize(attrs = {})
8
+ # attrs.assert_valid_keys(self.valid_attributes.keys.map(&:to_s))
9
+ @attributes = attrs.dup.with_indifferent_access
10
+ @attributes.assert_valid_keys valid_attributes.keys
11
+ validate!
12
+ end
13
+
14
+ private
15
+
16
+ def self.attribute(*attrs, &block)
17
+ options = attrs.extract_options!
18
+ options[:conversion] = block
19
+ attrs.each do |attr|
20
+ valid_attributes[attr] = options
21
+ end
22
+ end
23
+
24
+ def validate!
25
+ attributes.each do |attr,value|
26
+ options = valid_attributes[attr]
27
+ attributes[attr] = value = options[:conversion].call(value) if options[:conversion]
28
+ case options[:value]
29
+ when Array
30
+ raise ArgumentError, "#{attr} was expected to be one of #{options[:value].to_sentence(:connector => 'or')}" unless options[:value].include?(value)
31
+ when Regexp
32
+ raise "#{attr} is invalid" unless value =~ options[:value]
33
+ else
34
+ true
35
+ end
36
+ end
37
+ end
38
+
39
+
40
+ end
41
+ end
@@ -0,0 +1,112 @@
1
+ module Kayak
2
+ class SearchResults
3
+ instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?$|^send$)/ }
4
+ attr_accessor :id
5
+
6
+ def initialize(session, search_id)
7
+ @session = session
8
+ @id = search_id
9
+ reset!
10
+ end
11
+
12
+ def load!(count = nil)
13
+ params = {:searchid => @id, :apimode => 1, :_sid_ => @session.id, :c => count}
14
+ load_results @session.get("/s/apibasic/flight", params)
15
+ end
16
+
17
+ def loaded?
18
+ @loaded
19
+ end
20
+
21
+ def loaded!
22
+ @loaded = true
23
+ end
24
+
25
+ def complete?
26
+ @complete
27
+ end
28
+
29
+ def reset!
30
+ @loaded = false
31
+ @complete = false
32
+ @results = []
33
+ end
34
+
35
+ def reload!
36
+ reset!
37
+ load!
38
+ end
39
+
40
+ def respond_to?(symbol, include_priv = false)
41
+ @results.respond_to?(symbol, include_priv)
42
+ end
43
+
44
+ # Explicitly define === because the instance method removal above
45
+ # doesn't catch it.
46
+ def ===(other)
47
+ load!
48
+ other === @results
49
+ end
50
+
51
+ def inspect
52
+ @results.inspect
53
+ end
54
+
55
+ private
56
+
57
+ def method_missing(method, *args, &block)
58
+ load! unless loaded?
59
+ @results.send(method, *args, &block)
60
+ end
61
+
62
+ def load_results(xml)
63
+ @complete = xml.elements['/searchresult/morepending'].text == "false"
64
+ # @@lastcount = xml.elements['/searchresult/count'].text
65
+ reset!
66
+ xml.elements.each("/searchresult/trips/trip") do |e|
67
+ trip = Trip.new
68
+ trip.price = e.elements['price'].text
69
+ trip.url = e.elements['price'].attribute('url').value
70
+ e.elements.each('legs/leg') do |l|
71
+ leg = Leg.new
72
+ leg.airline_code = l.elements['airline'].text
73
+ leg.airline = l.elements['airline_display'].text
74
+ leg.origin = l.elements['orig'].text
75
+ leg.destination = l.elements['dest'].text
76
+ leg.stops = l.elements['stops'].text.to_i
77
+ leg.cabin = l.elements['cabin'].text
78
+ leg.depart = Time.parse(l.elements['depart'].text)
79
+ leg.arrive = Time.parse(l.elements['arrive'].text)
80
+ leg.duration = l.elements['duration_minutes'].text.to_i.minutes
81
+ trip.legs << leg
82
+ end
83
+
84
+ @results << trip
85
+ end
86
+ loaded!
87
+ end
88
+
89
+ # if (searchtype == 'h')
90
+ # xml.elements.each("/searchresult/hotels/hotel") do |e|
91
+ # hotel = Hotel.new()
92
+ # e.each_element("price") do |t|
93
+ # hotel.price = t.text
94
+ # hotel.url = t.attribute("url")
95
+ # end
96
+ # e.each_element("name") { |t| hotel.name = t.text }
97
+ # e.each_element("address") { |t| hotel.address = t.text }
98
+ # e.each_element("city") { |t| hotel.city = t.text }
99
+ # e.each_element("region_code") { |t| hotel.region = t.text_code }
100
+ # e.each_element("city") { |t| hotel.city = t.text }
101
+ # e.each_element("stars") { |t| hotel.stars = t.text}
102
+ # e.each_element("phone") { |t| hotel.phone = t.text}
103
+ # e.each_element("pricehistoryhi") { |t| hotel.hiprice = t.text}
104
+ # e.each_element("pricehistorylo") { |t| hotel.loprice = t.text}
105
+ # @results << hotel
106
+ # end # each hotel
107
+ #
108
+ # end # if hotel search
109
+ # end
110
+ # return more
111
+ end
112
+ end
@@ -0,0 +1,72 @@
1
+ require 'net/http'
2
+ require 'rexml/document'
3
+ require 'open-uri'
4
+
5
+ module Kayak
6
+ class TokenError < RuntimeError; end
7
+
8
+ class Session
9
+
10
+ USER_AGENT = "Mozilla/5.0 (compatible; RubyKayak/#{Kayak::VERSION::STRING}; http://kayak.rubyforge.org)"
11
+ URL = URI.parse('http://www.kayak.com')
12
+
13
+ attr_reader :id, :last_response
14
+
15
+ def initialize(id)
16
+ @id = id
17
+ end
18
+
19
+ def self.create(token)
20
+ response = REXML::Document.new(get('/k/ident/apisession', :token => token))
21
+ if session_id = response.elements['//sid'].text
22
+ Session.new session_id
23
+ else
24
+ raise TokenError, response.elements['//error'].text
25
+ end
26
+ end
27
+
28
+ # def hotel_search(params)
29
+ # {:rooms => 1, :guests1 => 1, :minstars => -1}
30
+ # params.
31
+ # # :action => 'dohotels'
32
+ # csc = URI.escape(citystatecountry)
33
+ # dep_date = URI.escape(dep_date)
34
+ # ret_date = URI.escape(ret_date)
35
+ # url = "othercity=#{csc}&checkin_date=#{dep_date}&checkout_date=#{ret_date}&"
36
+ # return start_search(url)
37
+ # search params.merge()
38
+ # end
39
+
40
+ def search(s)
41
+ params = s.to_search_params.merge({:basicmode => true, :apimode => 1, :_sid_ => @id})
42
+ SearchResults.new(self, get("/s/apisearch", params).elements['//search/searchid'].text)
43
+ end
44
+
45
+ def get(path, params = {})
46
+ REXML::Document.new(@last_response = self.class.get(path, params))
47
+ # check_error(response)
48
+ # return parse_response(response)
49
+ # rescue OpenURI::HTTPError => e
50
+ # check_error(prepare_response(e.io.read))
51
+ # raise
52
+ end
53
+
54
+ def self.get(path, params)
55
+ make_url(path, params).open('User-Agent' => USER_AGENT).read
56
+ end
57
+
58
+ private
59
+
60
+ def self.make_url(path, params = {})
61
+ escaped_params = params.sort_by { |k,v| k.to_s }.map do |k,v|
62
+ "#{URI.escape k.to_s}=#{URI.escape v.to_s}"
63
+ end
64
+
65
+ url = URL.dup
66
+ url.path = path
67
+ url.query = escaped_params.join '&' unless escaped_params.empty?
68
+ return url
69
+ end
70
+
71
+ end
72
+ end