From e6e715f63cd271b493220a29b572855c024020ff Mon Sep 17 00:00:00 2001 From: Romain Bignon Date: Sat, 14 Dec 2013 15:46:48 +0100 Subject: [PATCH] add module VoyagesSNCF (CapTravel) --- modules/voyagessncf/__init__.py | 24 ++++++++ modules/voyagessncf/backend.py | 75 +++++++++++++++++++++++ modules/voyagessncf/browser.py | 51 ++++++++++++++++ modules/voyagessncf/pages.py | 103 ++++++++++++++++++++++++++++++++ modules/voyagessncf/test.py | 37 ++++++++++++ 5 files changed, 290 insertions(+) create mode 100644 modules/voyagessncf/__init__.py create mode 100644 modules/voyagessncf/backend.py create mode 100644 modules/voyagessncf/browser.py create mode 100644 modules/voyagessncf/pages.py create mode 100644 modules/voyagessncf/test.py diff --git a/modules/voyagessncf/__init__.py b/modules/voyagessncf/__init__.py new file mode 100644 index 00000000..509b9e95 --- /dev/null +++ b/modules/voyagessncf/__init__.py @@ -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 . + + +from .backend import VoyagesSNCFBackend + + +__all__ = ['VoyagesSNCFBackend'] diff --git a/modules/voyagessncf/backend.py b/modules/voyagessncf/backend.py new file mode 100644 index 00000000..6220278a --- /dev/null +++ b/modules/voyagessncf/backend.py @@ -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 . + + +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 diff --git a/modules/voyagessncf/browser.py b/modules/voyagessncf/browser.py new file mode 100644 index 00000000..d12aebc2 --- /dev/null +++ b/modules/voyagessncf/browser.py @@ -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 . + + +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() diff --git a/modules/voyagessncf/pages.py b/modules/voyagessncf/pages.py new file mode 100644 index 00000000..d27bb4ac --- /dev/null +++ b/modules/voyagessncf/pages.py @@ -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 . + + +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, + } diff --git a/modules/voyagessncf/test.py b/modules/voyagessncf/test.py new file mode 100644 index 00000000..f22103ed --- /dev/null +++ b/modules/voyagessncf/test.py @@ -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 . + + +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)