Adding JVMalin.fr support
This commit is contained in:
parent
33641953fc
commit
b0112a7e85
5 changed files with 416 additions and 0 deletions
23
modules/jvmalin/__init__.py
Normal file
23
modules/jvmalin/__init__.py
Normal file
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
|
from .backend import JVMalinBackend
|
||||||
|
|
||||||
|
__all__ = ['JVMalinBackend']
|
||||||
50
modules/jvmalin/backend.py
Normal file
50
modules/jvmalin/backend.py
Normal file
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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
|
||||||
67
modules/jvmalin/browser.py
Normal file
67
modules/jvmalin/browser.py
Normal file
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
207
modules/jvmalin/pages.py
Normal file
207
modules/jvmalin/pages.py
Normal file
|
|
@ -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)))
|
||||||
|
|
||||||
|
|
||||||
69
modules/jvmalin/test.py
Normal file
69
modules/jvmalin/test.py
Normal file
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue