kayak 0.0.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.
@@ -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