Merge lp:~marco-gallotta/ibid/flight into lp:~ibid-core/ibid/old-trunk-1.6

Proposed by marcog
Status: Superseded
Proposed branch: lp:~marco-gallotta/ibid/flight
Merge into: lp:~ibid-core/ibid/old-trunk-1.6
Diff against target: 466 lines (+332/-44)
3 files modified
ibid/plugins/ascii.py (+18/-41)
ibid/plugins/flight.py (+300/-0)
ibid/utils/__init__.py (+14/-3)
To merge this branch: bzr merge lp:~marco-gallotta/ibid/flight
Reviewer Review Type Date Requested Status
Ibid Core Team Pending
Review via email: mp+16962@code.launchpad.net

This proposal supersedes a proposal from 2010-01-07.

This proposal has been superseded by a proposal from 2010-01-07.

To post a comment you must log in.
Revision history for this message
marcog (marco-gallotta) wrote : Posted in a previous version of this proposal

I think the flight plugin is mature enough for some reviews. It's quite possible some gaping bugs are still out there, but it seems to be robust enough.

Revision history for this message
Stefano Rivera (stefanor) wrote : Posted in a previous version of this proposal

Whoops, conflicts in the diff.

lp:~marco-gallotta/ibid/flight updated
859. By marcog

Reverting ascii to trunk's version...hopefully i got it right *this* time

860. By marcog

Fix typos; allow "airport *for* cape town

861. By marcog

Condense responses into one

862. By marcog

Remove unused imports

863. By marcog

Make use of etree's findtext and path querying

864. By marcog

Merge flight and airport features

865. By marcog

Use dict substitutions for responses

866. By marcog

Catch ValueError thrown by parse() when the date is invalid

867. By marcog

Make strings unicode in places where they weren't; split(' ') if s -> string.split()

868. By marcog

Make parsing a little more resiliant

869. By marcog

strftime needs an ascii string for pre-2.6

870. By marcog

Merge trunk and move flight into geography

871. By marcog

Travelocity changed some form attribute names, this fixes them but flight is still not working

Unmerged revisions

871. By marcog

Travelocity changed some form attribute names, this fixes them but flight is still not working

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'ibid/plugins/ascii.py'
--- ibid/plugins/ascii.py 2010-01-05 08:00:15 +0000
+++ ibid/plugins/ascii.py 2010-01-07 12:36:20 +0000
@@ -1,12 +1,11 @@
1from BaseHTTPServer import BaseHTTPRequestHandler
2from cStringIO import StringIO1from cStringIO import StringIO
3import Image2import Image
4from os import remove3from os import remove
5import os.path4import os.path
6import subprocess5import subprocess
6from sys import stderr
7from tempfile import mkstemp7from tempfile import mkstemp
8from urllib2 import HTTPError, URLError, urlopen8from urllib2 import urlopen
9from urlparse import urlparse
10from zipfile import ZipFile9from zipfile import ZipFile
1110
12from aalib import AsciiScreen11from aalib import AsciiScreen
@@ -14,7 +13,7 @@
1413
15from ibid.config import Option, IntOption14from ibid.config import Option, IntOption
16from ibid.plugins import Processor, match15from ibid.plugins import Processor, match
17from ibid.utils import file_in_path, url_to_bytestring16from ibid.utils import file_in_path
1817
19"""18"""
20Dependencies:19Dependencies:
@@ -44,43 +43,24 @@
4443
45 @match(r'^draw\s+(\S+\.\S+)(\s+in\s+colou?r)?(?:\s+w(?:idth)?\s+(\d+))?(?:\s+h(?:eight)\s+(\d+))?$')44 @match(r'^draw\s+(\S+\.\S+)(\s+in\s+colou?r)?(?:\s+w(?:idth)?\s+(\d+))?(?:\s+h(?:eight)\s+(\d+))?$')
46 def draw(self, event, url, colour, width, height):45 def draw(self, event, url, colour, width, height):
47 if not urlparse(url).netloc:46 f = urlopen(url)
48 url = 'http://' + url47
49 if urlparse(url).scheme == 'file':48 filesize = int(f.info().getheaders('Content-Length')[0])
50 event.addresponse(u'Are you trying to haxor me?')49 if filesize > self.max_filesize * 1024:
51 return50 event.addresponse(u'File too large (limit is %i KiB)', self.max_filesize)
52 if not urlparse(url).path:51 return
53 url += '/'52
54
55 try:
56 f = urlopen(url_to_bytestring(url))
57 except HTTPError, e:
58 event.addresponse(u'Sorry, error fetching URL: %s', BaseHTTPRequestHandler.responses[e.code][0])
59 return
60 except URLError:
61 event.addresponse(u'Sorry, error fetching URL')
62 return
63
64 content_length = f.info().getheaders('Content-Length')
65 if content_length and int(content_length[0]) > self.max_filesize * 1024:
66 event.addresponse(u'File too large (limit is %i KiB)', self.max_filesize)
67 return
68
69 buffer = f.read(self.max_filesize * 1024)
70 if f.read(1) != '':
71 event.addresponse(u'File too large (limit is %i KiB)', self.max_filesize)
72 return
73 try:53 try:
74 ext = os.path.splitext(url)[1]54 ext = os.path.splitext(url)[1]
75 image = mkstemp(suffix=ext)[1]55 image = mkstemp(suffix=ext)[1]
76 file = open(image, 'w')56 file = open(image, 'w')
77 file.write(buffer)57 file.write(f.read())
78 file.close()58 file.close()
7959
80 try:60 try:
81 img = Image.open(StringIO(open(image, 'r').read())).convert('L')61 img = Image.open(StringIO(open(image, 'r').read())).convert('L')
82 except IOError:62 except:
83 event.addresponse(u"Sorry, that doesn't look like an image")63 event.addresponse(u'Cannot understand image format')
84 return64 return
85 input_width, input_height = img.size[0], img.size[1]65 input_width, input_height = img.size[0], img.size[1]
8666
@@ -120,8 +100,8 @@
120 def draw_aa(self, event, image, width, height):100 def draw_aa(self, event, image, width, height):
121 try:101 try:
122 image = Image.open(StringIO(open(image, 'r').read())).convert('L')102 image = Image.open(StringIO(open(image, 'r').read())).convert('L')
123 except IOError:103 except:
124 event.addresponse(u"Sorry, that doesn't look like an image")104 event.addresponse(u'Cannot understand image format')
125 return105 return
126 screen = AsciiScreen(width=width, height=height)106 screen = AsciiScreen(width=width, height=height)
127 image = image.resize(screen.virtual_size)107 image = image.resize(screen.virtual_size)
@@ -129,6 +109,7 @@
129 event.addresponse(unicode(screen.render()), address=False, conflate=False)109 event.addresponse(unicode(screen.render()), address=False, conflate=False)
130110
131 def draw_caca(self, event, image, width, height):111 def draw_caca(self, event, image, width, height):
112 from sys import stderr
132 process = subprocess.Popen(113 process = subprocess.Popen(
133 [self.img2txt_bin, '-f', 'irc', '-W', str(width), '-H', str(height), image],114 [self.img2txt_bin, '-f', 'irc', '-W', str(width), '-H', str(height), image],
134 shell=False, stdout=subprocess.PIPE)115 shell=False, stdout=subprocess.PIPE)
@@ -137,15 +118,14 @@
137 if code == 0:118 if code == 0:
138 event.addresponse(unicode(response.replace('\r', '')), address=False, conflate=False)119 event.addresponse(unicode(response.replace('\r', '')), address=False, conflate=False)
139 else:120 else:
140 event.addresponse(u"Sorry, that doesn't look like an image")121 event.addresponse(u'Sorry, cannot understand image format')
141122
142class WriteFiglet(Processor):123class WriteFiglet(Processor):
143 u"""figlet <text> [in <font>]124 u"""figlet <text> [in <font>]
144 list figlet fonts [from <index>]"""125 list figlet fonts [from <index>]"""
145 feature = 'figlet'126 feature = 'figlet'
146127
147 max_width = IntOption('max_width', 'Maximum width for ascii output', 60)128 fonts_zip = Option('fonts_zip', 'Zip file containing figlet fonts', 'data/figlet-fonts.zip')
148 fonts_zip = Option('fonts_zip', 'Zip file containing figlet fonts', 'ibid/data/figlet-fonts.zip')
149129
150 def __init__(self, name):130 def __init__(self, name):
151 Processor.__init__(self, name)131 Processor.__init__(self, name)
@@ -178,7 +158,4 @@
178 del rendered[0]158 del rendered[0]
179 while rendered and rendered[-1].strip() == '':159 while rendered and rendered[-1].strip() == '':
180 del rendered[-1]160 del rendered[-1]
181 if rendered and len(rendered[0]) > self.max_width:
182 event.addresponse(u"Sorry that's too long, nobody will be able to read it")
183 return
184 event.addresponse(unicode('\n'.join(rendered)), address=False, conflate=False)161 event.addresponse(unicode('\n'.join(rendered)), address=False, conflate=False)
185162
=== added file 'ibid/plugins/flight.py'
--- ibid/plugins/flight.py 1970-01-01 00:00:00 +0000
+++ ibid/plugins/flight.py 2010-01-07 12:36:20 +0000
@@ -0,0 +1,300 @@
1import csv
2import re
3from sys import maxint
4from urllib import urlencode
5from xml.etree import ElementTree
6
7from dateutil.parser import parse
8
9from ibid.config import IntOption
10from ibid.plugins import Processor, match
11from ibid.utils import cacheable_download, human_join
12from ibid.utils.html import get_html_parse_tree
13
14help = { u'airport' : u'Search for airports',
15 u'flight' : u'Search for flights on travelocity' }
16
17airports_url = 'http://openflights.svn.sourceforge.net/viewvc/openflights/openflights/data/airports.dat'
18
19airports = {}
20
21def read_data():
22 # File is listed as ISO 8859-1 (Latin-1) encoded on
23 # http://openflights.org/data.html, but from decoding it appears to
24 # actually be UTF8
25 filename = cacheable_download(airports_url, 'flight/airports.dat')
26 reader = csv.reader(open(filename), delimiter=',', quotechar='"')
27 for row in reader:
28 airports[int(row[0])] = [unicode(r, 'utf-8') for r in row[1:]]
29
30def airport_search(query, search_loc = True):
31 if not airports:
32 read_data()
33 if search_loc:
34 ids = airport_search(query, False)
35 if len(ids) == 1:
36 return ids
37 query = [unicode(q) for q in query.lower().split(' ') if q]
38 else:
39 query = [unicode(query.lower())]
40 ids = []
41 for id, airport in airports.items():
42 if search_loc:
43 data = (u' '.join(c.lower() for c in airport[:5])).split(' ')
44 elif len(query[0]) == 3:
45 data = [airport[3].lower()]
46 else: # assume lenght 4 (won't break if not)
47 data = [airport[4].lower()]
48 if len(filter(lambda q: q in data, query)) == len(query):
49 ids.append(id)
50 return ids
51
52def repr_airport(id):
53 airport = airports[id]
54 code = ''
55 if airport[3] or airport[4]:
56 code = ' (%s)' % u'/'.join(filter(lambda c: c, airport[3:5]))
57 return '%s%s' % (airport[0], code)
58
59class AirportSearch(Processor):
60 """airport [in] <name|location|code>"""
61
62 feature = 'airport'
63
64 @match(r'^airports?\s+(in\s+)?(.+)$')
65 def airport_search(self, event, search_loc, query):
66 search_loc = search_loc is not None
67 if not search_loc and not 3 <= len(query) <= 4:
68 event.addresponse(u'Airport code must be 3 or 4 characters')
69 return
70 ids = airport_search(query, search_loc)
71 if len(ids) == 0:
72 event.addresponse(u"Sorry, I don't know that airport")
73 elif len(ids) == 1:
74 id = ids[0]
75 airport = airports[id]
76 code = 'unknown code'
77 if airport[3] and airport[4]:
78 code = 'codes %s and %s' % (airport[3], airport[4])
79 elif airport[3]:
80 code = 'code %s' % airport[3]
81 elif airport[4]:
82 code = 'code %s' % airport[4]
83 event.addresponse(u'%s in %s, %s has %s' %
84 (airport[0], airport[1], airport[2], code))
85 else:
86 event.addresponse(u'Found the following airports: %s', human_join(repr_airport(id) for id in ids)[:480])
87
88class Flight:
89 def __init__(self):
90 self.flight, self.depart_time, self.depart_ap, self.arrive_time, \
91 self.arrive_ap, self.duration, self.stops, self.price = \
92 [], None, None, None, None, None, None, None
93
94 def int_price(self):
95 try:
96 return int(self.price[1:])
97 except ValueError:
98 return maxint
99
100 def int_duration(self):
101 hours, minutes = 0, 0
102 match = re.search(r'(\d+)hr', self.duration)
103 if match:
104 hours = int(match.group(1))
105 match = re.search(r'(\d+)min', self.duration)
106 if match:
107 minutes = int(match.group(1))
108 return int(hours)*60 + int(minutes)
109
110MONTH_SHORT = ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec')
111MONTH_LONG = ('January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December')
112OTHER_STUFF = ('am', 'pm', 'st', 'nd', 'rd', 'th', 'morning', 'afternoon', 'evening', 'anytime')
113DATE = r'(?:[0-9.:/hT -]|%s)+' % '|'.join(MONTH_SHORT+MONTH_LONG+OTHER_STUFF)
114
115class FlightException(Exception):
116 pass
117
118class FlightSearch(Processor):
119 """[<cheapest|quickest]> flight from <departure> to <destination> from <depart_date> [anytime|morning|afternoon|evening|<time>] to <return_date> [anytime|morning|afternoon|evening|<time>]"""
120
121 feature = 'flight'
122
123 max_results = IntOption('max_results', 'Maximum number of results to list', 5)
124
125 def _flight_search(self, event, dpt, to, dep_date, ret_date):
126 airport_dpt = airport_search(dpt)
127 airport_to = airport_search(to)
128 if len(airport_dpt) == 0:
129 event.addresponse(u"Sorry, I don't know the airport you want to leave from")
130 return
131 if len(airport_to) == 0:
132 event.addresponse(u"Sorry, I don't know the airport you want to fly to")
133 return
134 if len(airport_dpt) > 1:
135 event.addresponse(u'The following airports match the departure: %s', human_join(repr_airport(id) for id in airport_dpt)[:480])
136 return
137 if len(airport_to) > 1:
138 event.addresponse(u'The following airports match the destination: %s', human_join(repr_airport(id) for id in airport_to)[:480])
139 return
140
141 dpt = airport_dpt[0]
142 to = airport_to[0]
143
144 def to_travelocity_date(date):
145 date = date.lower()
146 time = None
147 for period in ['anytime', 'morning', 'afternoon', 'evening']:
148 if period in date:
149 time = period.title()
150 date = date.replace(period, '')
151 break
152 date = parse(date)
153 if time is None:
154 if date.hour == 0 and date.minute == 0:
155 time = 'Anytime'
156 else:
157 time = date.strftime('%I:00')
158 if time[0] == '0':
159 time = time[1:]
160 if date.hour < 12:
161 time += 'am'
162 else:
163 time += 'pm'
164 date = date.strftime('%m/%d/%Y')
165 return (date, time)
166
167 (dep_date, dep_time) = to_travelocity_date(dep_date)
168 (ret_date, ret_time) = to_travelocity_date(ret_date)
169
170 params = {}
171 params['leavingFrom'] = airports[dpt][3]
172 params['goingTo'] = airports[to][3]
173 params['leavingDate'] = dep_date
174 params['dateLeavingTime'] = dep_time
175 params['returningDate'] = ret_date
176 params['dateReturningTime'] = ret_time
177 etree = get_html_parse_tree('http://travel.travelocity.com/flights/InitialSearch.do', data=urlencode(params), treetype='etree')
178 while True:
179 script = [script for script in etree.getiterator('script')][1]
180 matches = script.text and re.search(r'var finurl = "(.*)"', script.text)
181 if matches:
182 url = 'http://travel.travelocity.com/flights/%s' % matches.group(1)
183 etree = get_html_parse_tree(url, treetype='etree')
184 else:
185 break
186
187 # Handle error
188 div = [d for d in etree.getiterator('div') if d.get(u'class') == 'e_content']
189 if len(div):
190 error = div[0].find('h3').text
191 raise FlightException(error)
192
193 departing_flights = self._parse_travelocity(etree)
194 return_url = None
195 table = [t for t in etree.getiterator('table')][3]
196 for tr in table.getiterator('tr'):
197 for td in tr.getiterator('td'):
198 if td.get(u'class').strip() in ['tfPrice', 'tfPriceOrButton']:
199 div = td.find('div')
200 if div is not None:
201 button = div.find('button')
202 if button is not None:
203 onclick = button.get('onclick')
204 match = re.search(r"location.href='\.\./flights/(.+)'", onclick)
205 url_page = match.group(1)
206 match = re.search(r'^(.*?)[^/]*$', url)
207 url_base = match.group(1)
208 return_url = url_base + url_page
209
210 etree = get_html_parse_tree(return_url, treetype='etree')
211 returning_flights = self._parse_travelocity(etree)
212
213 return (departing_flights, returning_flights, url)
214
215 def _parse_travelocity(self, etree):
216 flights = []
217 table = [t for t in etree.getiterator('table') if t.get(u'id') == 'tfGrid'][0]
218 trs = [t for t in table.getiterator('tr')]
219 tr_index = 1
220 while tr_index < len(trs):
221 tds = []
222 while True:
223 new_tds = [t for t in trs[tr_index].getiterator('td')]
224 tds.extend(new_tds)
225 tr_index += 1
226 if len(filter(lambda t: t.get(u'class').strip() == u'tfAirlineSeatsMR', new_tds)):
227 break
228 flight = Flight()
229 for td in tds:
230 if td.get(u'class').strip() == u'tfAirline':
231 anchor = td.find('a')
232 if anchor is not None:
233 airline = anchor.text.strip()
234 else:
235 airline = td.text.split('\n')[0].strip()
236 flight.flight.append(u'%s %s' % (airline, td.find('div').text.strip()))
237 if td.get(u'class').strip() == u'tfDepart' and td.text:
238 flight.depart_time = td.text.split('\n')[0].strip()
239 flight.depart_ap = '%s %s' % (td.find('div').text.strip(),
240 td.find('div').find('span').text.strip())
241 if td.get(u'class').strip() == u'tfArrive' and td.text:
242 flight.arrive_time = td.text.split('\n')[0].strip()
243 span = td.find('span')
244 if span is not None and span.get(u'class').strip() == u'tfNextDayDate':
245 flight.arrive_time = u'%s %s' % (flight.arrive_time, span.text.strip()[2:])
246 span = [s for s in td.find('div').getiterator('span')][1]
247 flight.arrive_ap = '%s %s' % (td.find('div').text.strip(),
248 span.text.strip())
249 else:
250 flight.arrive_ap = '%s %s' % (td.find('div').text.strip(),
251 td.find('div').find('span').text.strip())
252 if td.get(u'class').strip() == u'tfTime' and td.text:
253 flight.duration = td.text.strip()
254 flight.stops = td.find('span').find('a').text.strip()
255 if td.get(u'class').strip() in [u'tfPrice', u'tfPriceOr'] and td.text:
256 flight.price = td.text.strip()
257 flight.flight = human_join(flight.flight)
258 flights.append(flight)
259
260 return flights
261
262 @match(r'^(?:(cheapest|quickest)\s+)?flights?\s+from\s+(.+)\s+to\s+(.+)\s+from\s+(%s)\s+to\s+(%s)$' % (DATE, DATE))
263 def flight_search(self, event, priority, dpt, to, dep_date, ret_date):
264 try:
265 flights = self._flight_search(event, dpt, to, dep_date, ret_date)
266 except FlightException, e:
267 event.addresponse(unicode(e))
268 return
269 if flights is None:
270 return
271 if len(flights[0]) == 0:
272 event.addresponse(u'No matching departure flights found')
273 return
274 if len(flights[1]) == 0:
275 event.addresponse(u'No matching return flights found')
276 return
277
278 cmp = None
279 if priority == 'cheapest':
280 cmp = lambda a, b: a.int_price() < b.int_price()
281 elif priority == 'quickest':
282 cmp = lambda a, b: a.int_duration() < b.int_duration()
283 if cmp:
284 # select best flight based on priority
285 for i in xrange(2):
286 flights[i].sort(cmp=cmp)
287 del flights[i][1:]
288 for i, flight_type in zip(xrange(2), ['Departing', 'Returning']):
289 if len(flights[i]) > 1:
290 event.addresponse(u'%s flights:', flight_type)
291 for flight in flights[i][:self.max_results]:
292 leading = ''
293 if len(flights[i]) == 1:
294 leading = u'%s flight: ' % flight_type
295 event.addresponse('%s%s departing %s from %s, arriving %s at %s (flight time %s, %s) costs %s per person',
296 (leading, flight.flight, flight.depart_time, flight.depart_ap, flight.arrive_time,
297 flight.arrive_ap, flight.duration, flight.stops, flight.price or 'unknown'))
298 event.addresponse(u'Full results: %s', flights[2])
299
300# vi: set et sta sw=4 ts=4:
0301
=== modified file 'ibid/utils/__init__.py'
--- ibid/utils/__init__.py 2010-01-06 23:19:42 +0000
+++ ibid/utils/__init__.py 2010-01-07 12:36:20 +0000
@@ -80,9 +80,14 @@
80 req.add_header('User-Agent', 'Ibid/' + (ibid_version() or 'dev'))80 req.add_header('User-Agent', 'Ibid/' + (ibid_version() or 'dev'))
8181
82 if exists:82 if exists:
83 modified = os.path.getmtime(cachefile)83 if os.path.isfile(cachefile + '.etag'):
84 modified = time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(modified))84 f = file(cachefile + '.etag', 'r')
85 req.add_header("If-Modified-Since", modified)85 req.add_header("If-None-Match", f.readline().strip())
86 f.close()
87 else:
88 modified = os.path.getmtime(cachefile)
89 modified = time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(modified))
90 req.add_header("If-Modified-Since", modified)
8691
87 try:92 try:
88 connection = urllib2.urlopen(req)93 connection = urllib2.urlopen(req)
@@ -106,6 +111,12 @@
106 gzipper = GzipFile(fileobj=compressedstream)111 gzipper = GzipFile(fileobj=compressedstream)
107 data = gzipper.read()112 data = gzipper.read()
108113
114 etag = connection.headers.get('etag')
115 if etag:
116 f = file(cachefile + '.etag', 'w')
117 f.write(etag + '\n')
118 f.close()
119
109 outfile = file(cachefile, 'wb')120 outfile = file(cachefile, 'wb')
110 outfile.write(data)121 outfile.write(data)
111 outfile.close()122 outfile.close()

Subscribers

People subscribed via source and target branches