diff --git a/modules/jvmalin/__init__.py b/modules/jvmalin/__init__.py new file mode 100644 index 00000000..87a9e5e5 --- /dev/null +++ b/modules/jvmalin/__init__.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2013 Alexandre Lissy +# +# This file is part of weboob. +# +# weboob is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# weboob is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with weboob. If not, see . + + +from .backend import JVMalinBackend + +__all__ = ['JVMalinBackend'] diff --git a/modules/jvmalin/backend.py b/modules/jvmalin/backend.py new file mode 100644 index 00000000..c1b1071d --- /dev/null +++ b/modules/jvmalin/backend.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2013 Alexandre Lissy +# +# This file is part of weboob. +# +# weboob is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# weboob is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with weboob. If not, see . + +from weboob.capabilities.travel import ICapTravel, RoadStep +from weboob.tools.backend import BaseBackend + +from .browser import JVMalin + + +__all__ = ['JVMalinBackend'] + + +class JVMalinBackend(BaseBackend, ICapTravel): + NAME = 'jvmalin' + MAINTAINER = u'Alexandre Lissy' + EMAIL = 'github@lissy.me' + VERSION = '0.h' + LICENSE = 'AGPLv3+' + DESCRIPTION = "Multimodal public transportation for whole Région Centre, France" + BROWSER = JVMalin + + def iter_roadmap(self, departure, arrival, filters): + with self.browser: + roadmap = self.browser.get_roadmap(departure, arrival, filters) + + for s in roadmap['steps']: + step = RoadStep(s['id']) + step.line = s['line'] + step.start_time = s['start_time'] + step.end_time = s['end_time'] + step.departure = s['departure'] + step.arrival = s['arrival'] + step.duration = s['duration'] + yield step diff --git a/modules/jvmalin/browser.py b/modules/jvmalin/browser.py new file mode 100644 index 00000000..4db4cd38 --- /dev/null +++ b/modules/jvmalin/browser.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2013 Alexandre Lissy +# +# This file is part of weboob. +# +# weboob is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# weboob is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with weboob. If not, see . + + +from datetime import datetime, date, time + +from weboob.tools.browser import BaseBrowser +from weboob.tools.misc import to_unicode +from weboob.tools.browser import BrokenPageError +from .pages import RoadmapSearchPage, RoadmapResultsPage, RoadmapPage, RoadmapAmbiguity + + +__all__ = ['JVMalin'] + + +class JVMalin(BaseBrowser): + DOMAIN = 'www.jvmalin.fr' + PAGES = { + 'http://www\.jvmalin\.fr/Itineraires/Recherche.*': RoadmapSearchPage, + 'http://www\.jvmalin\.fr/Itineraires/Precision.*': RoadmapResultsPage, + 'http://www\.jvmalin\.fr/route/vuesearch/result.*': RoadmapPage + } + + def __init__(self, **kwargs): + BaseBrowser.__init__(self, '', **kwargs) + + def get_roadmap(self, departure, arrival, filters): + self.location('/Itineraires/Recherche') + + assert self.is_on_page(RoadmapSearchPage) + self.page.search(departure, arrival, filters.departure_time, filters.arrival_time) + + assert self.is_on_page(RoadmapResultsPage) + + dest = '' + try: + dest = self.page.find_best() + except RoadmapAmbiguity as am: + self.page.resubmit_best_form() + assert self.is_on_page(RoadmapResultsPage) + dest = self.page.find_best() + + self.location(dest) + + roadmap = {} + roadmap['steps'] = list(self.page.get_steps()) + return roadmap + + def is_logged(self): + """ Do not need to be logged """ + return True diff --git a/modules/jvmalin/pages.py b/modules/jvmalin/pages.py new file mode 100644 index 00000000..9f441cac --- /dev/null +++ b/modules/jvmalin/pages.py @@ -0,0 +1,207 @@ +# -*- coding: utf-8 -*- + +import re +import datetime + +from weboob.capabilities.travel import RoadmapError +from weboob.tools.misc import to_unicode +from weboob.tools.mech import ClientForm +from weboob.tools.browser import BasePage + +__all__ = ['RoadmapSearchPage', 'RoadmapResultsPage', 'RoadmapPage', 'RoadmapAmbiguity'] + +class RoadmapAmbiguity(RoadmapError): + def __init__(self, error): + RoadmapError.__init__(self, error) + +class RoadmapSearchPage(BasePage): + def search(self, departure, arrival, departure_time, arrival_time): + match = -1 + i = 0 + for form in self.browser.forms(): + try: + if form.attrs['id'] == 'rech-iti': + match = i + except KeyError as e: + pass + i += 1 + + self.browser.select_form(nr=match) + self.browser['Departure'] = departure + self.browser['Destination'] = arrival + + time = None + if departure_time: + self.browser['sens'] = ['1'] + time = departure_time + elif arrival_time: + self.browser['sens'] = ['-1'] + time = arrival_time + + if time: + try: + self.browser['dateFull'] = '%02d/%02d/%d' % (time.day, time.month, time.year) + self.browser['hour'] = ['%02d' % time.hour] + self.browser['minute'] = ['%02d' % (time.minute - (time.minute%5))] + except ClientForm.ItemNotFoundError: + raise RoadmapError('Unable to establish a roadmap with %s time at "%s"' % ('departure' if departure_time else 'arrival', time)) + self.browser.submit() + +class RoadmapResultsPage(BasePage): + def html_br_strip(self, text): + return "".join([l.strip() for l in text.split("\n")]).strip().replace(' ', '%20') + + def find_best(self): + if len(self.parser.select(self.document.getroot(), 'img.img-error')) > 0: + if len(self.parser.select(self.document.getroot(), 'form#iti-ambi')) > 0: + raise RoadmapAmbiguity('Ambigious stop name') + else: + raise RoadmapError('Error when submitting form') + + best = self.parser.select(self.document.getroot(), 'div.alerte-bloc-important div.bloc-iti') + if len(best) == 0: + best = self.parser.select(self.document.getroot(), 'div.bloc-iti') + if len(best) == 0: + raise RoadmapError('Unable to get the best roadmap'); + + link = self.parser.select(best[0], 'a.btn-submit') + if len(link) == 0: + raise RoadmapError('Unable to get a link to best roadmap') + + return self.html_br_strip(link[0].attrib['href']) + + def resubmit_best_form(self): + if len(self.parser.select(self.document.getroot(), 'img.img-error')) == 0: + raise RoadmapError('No error reported!') + + ambi = None + i = 0 + for form in self.parser.select(self.document.getroot(), 'form'): + if 'id' in form.attrib and form.attrib['id'] == 'iti-ambi': + ambi = form + break + i += 1 + + if ambi is None: + raise RoadmapError('No ambigous form!') + + props = self.parser.select(ambi, 'span.precision-arret input') + if len(props) == 0: + props = self.parser.select(ambi, 'span.precision-adresse input') + if len(props) == 0: + raise RoadmapError('Nothing to select to get a roadmap') + + self.browser.select_form(nr=i) + propname = props[0].attrib['name'] + propvalue = props[0].attrib['value'].encode('utf-8') + self.browser[propname] = [ propvalue ] + self.browser.submit() + +class RoadmapPage(BasePage): + def get_steps(self): + errors = [] + # for p in self.parser.select(self.document.getroot(), 'p.errors'): + # if p.text: + # errors.append(p.text.strip()) + + if len(errors) > 0: + raise RoadmapError('Unable to establish a roadmap: %s' % ', '.join(errors)) + + current_step = None + i = 0 + for tr in self.parser.select(self.document.getroot(), 'table.itineraire-detail tr'): + if current_step is None: + current_step = { + 'id': i, + 'start_time': datetime.datetime.now(), + 'end_time': datetime.datetime.now(), + 'line': '', + 'departure': '', + 'arrival': '', + 'duration': datetime.timedelta() + } + + if 'class' in tr.attrib: + if 'bg-ligne' in tr.attrib['class']: + continue + + if 'iti-map' in tr.attrib['class']: + continue + + for td in self.parser.select(tr, 'td'): + if not 'class' in td.attrib: + continue + + if 'iti-inner' in td.attrib['class']: + continue + + if 'cell-infos' in td.attrib['class']: + if 'id' in td.attrib: + if td.attrib['id'].find('MapOpenLink') >= 0: + hasA = self.parser.select(td, 'a') + if len(hasA) == 0: + if len(current_step['line']) > 0 and \ + len(current_step['departure']) > 0 and \ + len(current_step['arrival']) > 0: + current_step['line'] = to_unicode("%s : %s" % \ + (current_step['mode'], current_step['line'])) + del current_step['mode'] + yield current_step + i += 1 + current_step = None + continue + + if 'cell-horaires' in td.attrib['class']: + # real start + for heure in self.parser.select(td, 'span.heure'): + if heure.attrib['id'].find('FromTime') >= 0: + current_step['start_time'] = self.parse_time(heure.text) + if heure.attrib['id'].find('ToTime') >= 0: + current_step['end_time'] = self.parse_time(heure.text) + for mode in self.parser.select(td, 'span.mode-locomotion img'): + current_step['mode'] = mode.attrib['title'] + + if 'cell-details' in td.attrib['class']: + # If we get a span, it's a line indication, + # otherwise check for id containing LibDeparture or + # LibDestination + spans = self.parser.select(td, 'span.itineraire-ligne') + if len(spans) == 1: + line = self.html_br_strip(spans[0].text, " ").replace('Ligne ', '') + if line.index('- ') == 0: + line = re.sub(r'^- ', '', line) + current_step['line'] = line + + elif 'id' in td.attrib: + stops = self.parser.select(td, 'strong') + stop = self.html_br_strip(stops[0].text, " ") + + if td.attrib['id'].find('LibDeparture') >= 0: + current_step['departure'] = to_unicode(stop) + + if td.attrib['id'].find('LibDestination') >= 0: + current_step['arrival'] = to_unicode(stop) + + duree = self.parser.select(td, 'span.duree strong') + if len(duree) == 1: + current_step['duration'] = self.parse_duration(duree[0].text) + + def html_br_strip(self, text, joining=""): + return joining.join([l.strip() for l in text.split("\n")]).strip() + + def parse_time(self, time): + time = self.html_br_strip(time) + h, m = time.split('h') + return datetime.time(int(h), int(m)) + + def parse_duration(self, dur): + dur = self.html_br_strip(dur) + m = re.match('(\d+)min', dur) + if m: + return datetime.timedelta(minutes=int(m.group(1))) + m = re.match('(\d+)h(\d+)', dur) + if m: + return datetime.timedelta(hours=int(m.group(1)), + minutes=int(m.group(2))) + + diff --git a/modules/jvmalin/test.py b/modules/jvmalin/test.py new file mode 100644 index 00000000..43c69302 --- /dev/null +++ b/modules/jvmalin/test.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2013 Alexandre Lissy +# +# This file is part of weboob. +# +# weboob is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# weboob is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with weboob. If not, see . + +import datetime + +from weboob.capabilities.travel import RoadmapFilters +from weboob.tools.test import BackendTest + + +class JVMalinTest(BackendTest): + BACKEND = 'jvmalin' + + def test_roadmap_cities(self): + filters = RoadmapFilters() + roadmap = list(self.backend.iter_roadmap('Tours', 'Orléans', filters)) + self.assertTrue(len(roadmap) > 0) + + def test_roadmap_stop_intercity(self): + filters = RoadmapFilters() + roadmap = list(self.backend.iter_roadmap('Tours Jean-Jaurès', 'Orléans', filters)) + self.assertTrue(len(roadmap) > 0) + + def test_roadmap_stop_intracity(self): + filters = RoadmapFilters() + roadmap = list(self.backend.iter_roadmap('Tours Jean-Jaurès', 'Polytech Tours', filters)) + self.assertTrue(len(roadmap) > 0) + + def test_roadmap_stop_intracity2(self): + filters = RoadmapFilters() + roadmap = list(self.backend.iter_roadmap('J.P.Rameau', 'Polytech Tours', filters)) + self.assertTrue(len(roadmap) > 0) + + def test_roadmap_names(self): + filters = RoadmapFilters() + roadmap = list(self.backend.iter_roadmap('Artannes Mairie', 'Château de Blois', filters)) + self.assertTrue(len(roadmap) > 0) + + def test_roadmap_long(self): + filters = RoadmapFilters() + roadmap = list(self.backend.iter_roadmap('Chartres', 'Ballan-Miré', filters)) + self.assertTrue(len(roadmap) > 0) + + def test_roadmap_departure(self): + filters = RoadmapFilters() + filters.departure_time = datetime.datetime.now() + datetime.timedelta(days=1) + roadmap = list(self.backend.iter_roadmap('Chartres', 'Ballan-Miré', filters)) + self.assertTrue(len(roadmap) > 0) + + def test_roadmap_arrival(self): + filters = RoadmapFilters() + filters.arrival_time = datetime.datetime.now() + datetime.timedelta(days=1) + roadmap = list(self.backend.iter_roadmap('Chartres', 'Ballan-Miré', filters)) + self.assertTrue(len(roadmap) > 0)