add module VoyagesSNCF (CapTravel)

This commit is contained in:
Romain Bignon 2013-12-14 15:46:48 +01:00
commit e6e715f63c
5 changed files with 290 additions and 0 deletions

View file

@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
# Copyright(C) 2013 Romain Bignon
#
# 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 VoyagesSNCFBackend
__all__ = ['VoyagesSNCFBackend']

View file

@ -0,0 +1,75 @@
# -*- coding: utf-8 -*-
# Copyright(C) 2013 Romain Bignon
#
# 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.tools.backend import BaseBackend
from weboob.capabilities.travel import ICapTravel, Station, Departure
from weboob.capabilities import UserError
from .browser import VoyagesSNCFBrowser
__all__ = ['VoyagesSNCFBackend']
class VoyagesSNCFBackend(BaseBackend, ICapTravel):
NAME = 'voyagessncf'
DESCRIPTION = u'Voyages SNCF'
MAINTAINER = u'Romain Bignon'
EMAIL = 'romain@weboob.org'
LICENSE = 'AGPLv3+'
VERSION = '0.h'
BROWSER = VoyagesSNCFBrowser
STATIONS = []
def _populate_stations(self):
if len(self.STATIONS) == 0:
with self.browser:
self.STATIONS = self.browser.get_stations()
def iter_station_search(self, pattern):
self._populate_stations()
pattern = pattern.lower()
for _id, name in enumerate(self.STATIONS):
if name.lower().startswith(pattern):
yield Station(_id, unicode(name))
def iter_station_departures(self, station_id, arrival_id=None, date=None):
self._populate_stations()
if arrival_id is None:
raise UserError('The arrival station is required')
try:
station = self.STATIONS[int(station_id)]
arrival = self.STATIONS[int(arrival_id)]
except (IndexError, ValueError):
raise UserError('Unknown station')
with self.browser:
for i, d in enumerate(self.browser.iter_departures(station, arrival, date)):
departure = Departure(i, d['type'], d['time'])
departure.departure_station = d['departure']
departure.arrival_station = d['arrival']
departure.arrival_time = d['arrival_time']
departure.price = d['price']
departure.currency = d['currency']
yield departure

View file

@ -0,0 +1,51 @@
# -*- coding: utf-8 -*-
# Copyright(C) 2013 Romain Bignon
#
# 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.tools.browser import BaseBrowser
from .pages import CitiesPage, SearchPage, SearchErrorPage, \
SearchInProgressPage, ResultsPage
__all__ = ['VoyagesSNCFBrowser']
class VoyagesSNCFBrowser(BaseBrowser):
PROTOCOL = 'http'
DOMAIN = 'www.voyages-sncf.com'
ENCODING = 'utf-8'
PAGES = {
'http://www.voyages-sncf.com/completion/VSC/FR/fr/cityList.js': (CitiesPage, 'raw'),
'http://www.voyages-sncf.com/billet-train': SearchPage,
'http://www.voyages-sncf.com/billet-train\?.+': SearchErrorPage,
'http://www.voyages-sncf.com/billet-train/recherche-en-cours.*': SearchInProgressPage,
'http://www.voyages-sncf.com/billet-train/resultat.*': ResultsPage,
}
def get_stations(self):
self.location('/completion/VSC/FR/fr/cityList.js')
return self.page.get_stations()
def iter_departures(self, departure, arrival, date):
self.location('/billet-train')
self.page.search(departure, arrival, date)
return self.page.iter_results()

View file

@ -0,0 +1,103 @@
# -*- coding: utf-8 -*-
# Copyright(C) 2013 Romain Bignon
#
# 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 re
from decimal import Decimal
from datetime import time, datetime
from weboob.tools.browser import BasePage
from weboob.tools.json import json
from weboob.tools.mech import ClientForm
from weboob.capabilities.base import UserError, Currency
__all__ = ['CitiesPage', 'SearchPage']
class CitiesPage(BasePage):
def get_stations(self):
result = json.loads(self.document[self.document.find('{'):-2])
return result['CITIES']
class SearchPage(BasePage):
def search(self, departure, arrival, date):
self.browser.select_form(name='saisie')
self.browser['ORIGIN_CITY'] = departure.encode(self.browser.ENCODING)
self.browser['DESTINATION_CITY'] = arrival.encode(self.browser.ENCODING)
if date is None:
date = datetime.now()
self.browser['OUTWARD_DATE'] = date.strftime('%d/%m/%y')
self.browser['OUTWARD_TIME'] = [str(date.hour + 1)]
self.browser['PASSENGER_1'] = ['ADULT']
self.browser.controls.append(ClientForm.TextControl('text', 'nbAnimalsForTravel', {'value': ''}))
self.browser['nbAnimalsForTravel'] = '0'
self.browser.submit()
class SearchErrorPage(BasePage):
def on_loaded(self):
p = self.document.getroot().cssselect('div.messagesError p')
if len(p) > 0:
message = p[0].text.strip()
raise UserError(message)
class SearchInProgressPage(BasePage):
def on_loaded(self):
link = self.document.xpath('//a[@id="url_redirect_proposals"]')[0]
self.browser.location(link.attrib['href'])
class ResultsPage(BasePage):
def get_value(self, div, name):
p = div.cssselect(name)[0]
sub = p.find('p')
if sub is not None:
txt = sub.tail.strip()
if txt == '':
p.remove(sub)
else:
return unicode(txt)
return unicode(self.parser.tocleanstring(p))
def parse_hour(self, div, name):
txt = self.get_value(div, name)
hour, minute = map(int, txt.split('h'))
return time(hour, minute)
def iter_results(self):
for div in self.document.getroot().cssselect('div.train_info'):
price = None
currency = None
for td in div.cssselect('td.price'):
txt = self.parser.tocleanstring(td)
p = Decimal(re.sub('([^\d\.]+)', '', txt))
currency = Currency.get_currency(txt)
if price is None or p < price:
price = p
yield {'type': self.get_value(div, 'div.transporteur-txt'),
'time': self.parse_hour(div, 'div.departure div.hour'),
'departure': self.get_value(div, 'div.departure div.station'),
'arrival': self.get_value(div, 'div.arrival div.station'),
'arrival_time': self.parse_hour(div, 'div.arrival div.hour'),
'price': price,
'currency': currency,
}

View file

@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
# Copyright(C) 2013 Romain Bignon
#
# 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.tools.test import BackendTest
class VoyagesSNCFTest(BackendTest):
BACKEND = 'voyagessncf'
def test_stations(self):
stations = list(self.backend.iter_station_search('paris'))
self.assertTrue(len(stations) > 0)
self.assertTrue('Paris Massy' in stations[-1].name)
def test_departures(self):
departure = list(self.backend.iter_station_search('paris'))[0]
arrival = list(self.backend.iter_station_search('lyon'))[0]
prices = list(self.backend.iter_station_departures(departure.id, arrival.id))
self.assertTrue(len(prices) > 0)