Refactor "scotty"; new "utils" module
[pywienerlinien] / gotovienna / routing.py
1 #!/usr/bin/env python
2 # -*- coding: UTF-8 -*-
3
4 from BeautifulSoup import BeautifulSoup, NavigableString
5 from urllib2 import urlopen
6 from urllib import urlencode
7 from datetime import datetime, time
8 from textwrap import wrap
9 import argparse
10 import sys
11 import os.path
12
13 from gotovienna import defaults
14
15 POSITION_TYPES = ('stop', 'address', 'poi')
16 TIMEFORMAT = '%H:%M'
17 DEBUGLOG = os.path.expanduser('~/gotoVienna.debug')
18
19 class ParserError(Exception):
20
21     def __init__(self, msg='Parser error'):
22         self.message = msg
23
24 class PageType:
25     UNKNOWN, CORRECTION, RESULT = range(3)
26
27
28 def search(origin_tuple, destination_tuple, dtime=None):
29     """ build route request
30     returns html result (as urllib response)
31     """
32     if not dtime:
33         dtime = datetime.now()
34
35     origin, origin_type = origin_tuple
36     destination, destination_type = destination_tuple
37     if not origin_type in POSITION_TYPES or\
38         not destination_type in POSITION_TYPES:
39         raise ParserError('Invalid position type')
40
41     post = defaults.search_post
42     post['name_origin'] = origin
43     post['type_origin'] = origin_type
44     post['name_destination'] = destination
45     post['type_destination'] = destination_type
46     post['itdDateDayMonthYear'] = dtime.strftime('%d.%m.%Y')
47     post['itdTime'] = dtime.strftime('%H:%M')
48     params = urlencode(post)
49     url = '%s?%s' % (defaults.action, params)
50
51     try:
52         f = open(DEBUGLOG, 'a')
53         f.write(url + '\n')
54         f.close()
55     except:
56         print 'Unable to write to DEBUGLOG: %s' % DEBUGLOG
57
58     return urlopen(url)
59
60
61 class sParser:
62     """ Parser for search response
63     """
64
65     def __init__(self, html):
66         self.soup = BeautifulSoup(html)
67
68     def check_page(self):
69         if self.soup.find('form', {'id': 'form_efaresults'}):
70             return PageType.RESULT
71
72         if self.soup.find('div', {'class':'form_error'}):
73             return PageType.CORRECTION
74
75         return PageType.UNKNOWN
76
77     state = property(check_page)
78
79     def get_correction(self):
80         nlo = self.soup.find('select', {'id': 'nameList_origin'})
81         nld = self.soup.find('select', {'id': 'nameList_destination'})
82
83         if not nlo and not nld:
84             raise ParserError('Unable to parse html')
85
86         if nlo:
87             origin = map(lambda x: x.text, nlo.findAll('option'))
88         else:
89             origin = []
90         if nld:
91             destination = map(lambda x: x.text, nld.findAll('option'))
92         else:
93             destination = []
94
95         return (origin, destination)
96
97     def get_result(self):
98         return rParser(str(self.soup))
99
100
101
102 class rParser:
103     """ Parser for routing results
104     """
105
106     def __init__(self, html):
107         self.soup = BeautifulSoup(html)
108         self._overview = None
109         self._details = None
110
111     @classmethod
112     def get_tdtext(cls, x, cl):
113             return x.find('td', {'class': cl}).text
114
115     @classmethod
116     def get_change(cls, x):
117         y = rParser.get_tdtext(x, 'col_change')
118         if y:
119             return int(y)
120         else:
121             return 0
122
123     @classmethod
124     def get_price(cls, x):
125         y = rParser.get_tdtext(x, 'col_price')
126         if y == '*':
127             return 0.0
128         if y.find(','):
129             return float(y.replace(',', '.'))
130         else:
131             return 0.0
132
133     @classmethod
134     def get_date(cls, x):
135         y = rParser.get_tdtext(x, 'col_date')
136         if y:
137             return datetime.strptime(y, '%d.%m.%Y').date()
138         else:
139             return None
140
141     @classmethod
142     def get_time(cls, x):
143         y = rParser.get_tdtext(x, 'col_time')
144         if y:
145             if (y.find("-") > 0):
146                 return map(lambda z: time(*map(int, z.split(':'))), y.split('-'))
147             else:
148                 return map(lambda z: time(*map(int, z.split(':'))), wrap(y, 5))
149         else:
150             return []
151
152     @classmethod
153     def get_duration(cls, x):
154         y = rParser.get_tdtext(x, 'col_duration')
155         if y:
156             return time(*map(int, y.split(":")))
157         else:
158             return None
159
160     def __iter__(self):
161         for detail in self.details():
162             yield detail
163
164     def _parse_details(self):
165         tours = self.soup.findAll('div', {'class': 'data_table tourdetail'})
166
167         trips = map(lambda x: map(lambda y: {
168                         'time': rParser.get_time(y),
169                         'station': map(lambda z: z[2:].strip(),
170                                        filter(lambda x: type(x) == NavigableString, y.find('td', {'class': 'col_station'}).contents)), # filter non NaviStrings
171                         'info': map(lambda x: x.strip(),
172                                     filter(lambda z: type(z) == NavigableString, y.find('td', {'class': 'col_info'}).contents)),
173                     }, x.find('tbody').findAll('tr')),
174                     tours) # all routes
175         return trips
176
177     @property
178     def details(self):
179         """returns list of trip details
180         [ [ { 'time': [datetime.time, datetime.time] if time else [],
181               'station': [u'start', u'end'] if station else [],
182               'info': [u'start station' if station else u'details for walking', u'end station' if station else u'walking duration']
183             }, ... # next trip step
184           ], ... # next trip possibility
185         ]
186         """
187         if not self._details:
188             self._details = self._parse_details()
189
190         return self._details
191
192     def _parse_overview(self):
193
194         # get overview table
195         table = self.soup.find('table', {'id': 'tbl_fahrten'})
196
197         # check if there is an overview table
198         if table and table.findAll('tr'):
199             # get rows
200             rows = table.findAll('tr')[1:] # cut off headline
201
202             overview = map(lambda x: {
203                                'date': rParser.get_date(x),
204                                'time': rParser.get_time(x),
205                                'duration': rParser.get_duration(x), # grab duration
206                                'change': rParser.get_change(x),
207                                'price': rParser.get_price(x),
208                            },
209                            rows)
210         else:
211             raise ParserError('Unable to parse overview')
212
213         return overview
214
215     @property
216     def overview(self):
217         """dict containing
218         date: datetime
219         time: [time, time]
220         duration: time
221         change: int
222         price: float
223         """
224         if not self._overview:
225             try:
226                 self._overview = self._parse_overview()
227             except AttributeError:
228                 f = open(DEBUGLOG, 'w')
229                 f.write(str(self.soup))
230                 f.close()
231
232         return self._overview
233