Skip to content

Commit

Permalink
Enable user to specify a bus stop by its id (#3)
Browse files Browse the repository at this point in the history
* Enable user to specify a bus stop by its id
* Improve tests
* Update version
  • Loading branch information
paraita committed Jan 26, 2017
1 parent ae1bbfb commit e4a2561
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 79 deletions.
4 changes: 2 additions & 2 deletions main.py
Expand Up @@ -2,10 +2,10 @@
# -*- coding: utf-8 -*-
import sophiabus230

for t in sophiabus230.get_next_buses(debug=True):
for t in sophiabus230.get_next_buses(stop_id=1939, debug=True):
bus_time = t['bus_time']
dest = t['dest']
rt = "theoretical"
if t['is_real_time']:
rt = "real time"
print("Next bus at {0} to {1} ({2})".format(bus_time, dest, rt))
print(u"Next bus at {0} to {1} ({2})".format(bus_time, dest, rt))
6 changes: 4 additions & 2 deletions setup.py
Expand Up @@ -4,7 +4,7 @@
from setuptools import setup

setup(name='sophiabus230',
version='0.4',
version='0.6',
description='Module to get the timetable of the Sophia Antipolis bus line 230',
url='http://github.com/paraita/sophiabus230',
author='Paraita Wohler',
Expand All @@ -21,6 +21,8 @@
'mock',
'nose',
'coverage',
'coveralls'
'coveralls',
'future',
'python-dateutil'
],
zip_safe=False)
71 changes: 42 additions & 29 deletions sophiabus230/__init__.py
@@ -1,15 +1,16 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from future.moves.urllib.request import urlopen

"""
This module fetches the timetable of the bus 230 in Sophia Antipolis
"""
import re
import logging
from datetime import datetime
from datetime import timedelta
from dateutil.parser import parse
from dateutil.tz import gettz
from datetime import timedelta
from bs4 import BeautifulSoup
import logging
from future.moves.urllib import request


def _get_html_from_cg06(stop_id):
Expand All @@ -21,29 +22,33 @@ def _get_html_from_cg06(stop_id):
:rtype: str
"""
cg06_url = "http://cg06.tsi.cityway.fr/qrcode/?id={0}"
req = urlopen(cg06_url.format(stop_id))
req = request.urlopen(cg06_url.format(stop_id))
content = req.read()
return content


def _sanitize_entry(entry):
def _sanitize_entry(entry, debug=False):
"""
Sanitize the entry by removing the extra spaces and empty lines
:param entry: the entry to sanitize
:return: the entry without unnecessary spaces
:rtype: str
"""
if debug:
logging.basicConfig(level=logging.INFO)
logging.info("To sanitize: |%s|", entry)
sane_line = re.sub(r"^\s+$", '\n', entry)
sane_line = re.sub(r"\s+$", '', sane_line)
sane_line = re.sub(r"^\s+", '', sane_line)
sane_line.replace(u"é", "e")
if len(sane_line) > 1:
return sane_line
else:
return None
logging.info('empty entry !')
return None


def _parse_entry(entry):
def _parse_entry(entry, debug=False):
"""
Convert a entry into a dict containing the following attributes:
Expand All @@ -55,7 +60,13 @@ def _parse_entry(entry):
:return: a dict representing the relevant information of the string
:rtype: dict
"""
if debug:
logging.basicConfig(level=logging.INFO)
logging.info("parsing the following entry: %s", entry)

split_entry = entry.split(' ')
if debug:
logging.info("split entry: %s", split_entry)
idx_end_direction = len(split_entry)
is_real_time = True
tz_paris = gettz('Europe/Paris')
Expand All @@ -69,17 +80,22 @@ def _parse_entry(entry):
bus_time = date_now + timedelta(minutes=delta_minutes)
idx_start_direction = 4
else:
# case for later upcoming bus
tzinfos = {'FR': tz_paris}
bus_time = parse(split_entry[1], tzinfos=tzinfos)
bus_time.replace(tzinfo=tz_paris)
if 'approche' in split_entry[1]:
bus_time = datetime.now(tz=tz_paris)
else:
# case for later upcoming bus
tzinfos = {'FR': tz_paris}
bus_time = parse(split_entry[1], tzinfos=tzinfos)
bus_time.replace(tzinfo=tz_paris)
idx_start_direction = 3
dest = ' '.join(split_entry[idx_start_direction:idx_end_direction])
dest = dest
return {'bus_time': bus_time, 'dest': dest, 'is_real_time': is_real_time}
parsed_entry = {'bus_time': bus_time, 'dest': dest, 'is_real_time': is_real_time}
if debug:
logging.info("parsed entry: %s", parsed_entry)
return parsed_entry


def get_next_buses(debug=False):
def get_next_buses(stop_id=1939, bus_id=230, debug=False):
"""
Fetches the list of upcoming buses at the INRIA bus stop for the bus 230 towards Nice
Expand All @@ -89,21 +105,18 @@ def get_next_buses(debug=False):
if debug:
logging.basicConfig(level=logging.INFO)
tt = []
content = _get_html_from_cg06(1939)
content = _get_html_from_cg06(stop_id)
soup = BeautifulSoup(content, "html.parser")
for br in soup.find_all("br"):
br.replace_with('\n')
data = [e for e in soup.find_all("div", attrs={"class": "data"}) if '230' in e.get_text()]
data = [e for e in soup.find_all("div", attrs={"class": "data"}) if str(bus_id) in e.get_text()]
if len(data) != 0:
assert len(data) <= 1
data_230 = data[0]
for e in data_230.div.get_text().split('\n'):
sane_entry = _sanitize_entry(e)
data_230 = data[0].div.get_text()
data_230 = data_230.replace(u"à ", u"DELIMITERà ")
data_230 = data_230.replace("dans ", "DELIMITERdans ")
for e in data_230.split('DELIMITER'):
sane_entry = _sanitize_entry(e, debug)
if sane_entry is not None:
if debug:
logging.info('found {0}'.format(sane_entry.encode('utf-8')))
tt.append(_parse_entry(sane_entry))
logging.info('found: %s', sane_entry.encode('utf-8'))
tt.append(_parse_entry(sane_entry, debug))
return tt


get_next_buses()
22 changes: 11 additions & 11 deletions sophiabus230/tests/example_content.html
Expand Up @@ -10,7 +10,7 @@
<meta name="keywords" content="">
<meta name="robots" content="index, follow, all">

<link rel="stylesheet" href="./example_4_50pm_files/style.css">
<link rel="stylesheet" href="./Lignes d&#39;azur _ le Réseau de Transport de Nice Côte d&#39;Azur - Horaires_files/style.css">

<link href="https://cg06.tsi.cityway.fr/img/defaultfavicon.ico" rel="shortcut icon" type="image/x-icon">

Expand All @@ -23,8 +23,8 @@
<link href="https://cg06.tsi.cityway.fr/img/default/favicon-32x32.png" rel="icon" type="image/png" sizes="32x32">
<link href="https://cg06.tsi.cityway.fr/img/default/favicon-96x96.png" rel="icon" type="image/png" sizes="96x96">
<link href="https://cg06.tsi.cityway.fr/img/default/favicon-160x160.png" rel="icon" type="image/png" sizes="160x160">
<script async="" src="./example_4_50pm_files/analytics.js"></script><script type="text/javascript">

<script async="" src="./Lignes d&#39;azur _ le Réseau de Transport de Nice Côte d&#39;Azur - Horaires_files/analytics.js"></script><script type="text/javascript">
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
Expand All @@ -42,31 +42,31 @@
<div class="main">

<div class="entete">
<img src="./example_4_50pm_files/logo.png" alt="Logo Lignes D&#39;Azur" width="134" height="72">
<img src="./Lignes d&#39;azur _ le Réseau de Transport de Nice Côte d&#39;Azur - Horaires_files/logo.png" alt="Logo Lignes D&#39;Azur" width="134" height="72">
</div>

<div class="hours">


<div class="here">
<span class="icon-arrow-right"></span> Vous êtes à l'arrêt
<br>

<span class="txtbold">INRIA</span>

</div>

<div class="encart">
<span class="icon-clock"></span> Horaires
</div>


<div class="infos">
<span class="txtbold">Date</span> : 14/12/2016
<span class="txtbold">Date</span> : 25/01/2017
<br>
<span>Il est</span> <span class="txtbold">16h49</span> <a href="https://cg06.tsi.cityway.fr/qrcode/?id=1939">Actualiser</a>
<span>Il est</span> <span class="txtbold">17h10</span> <a href="https://cg06.tsi.cityway.fr/qrcode/?id=1939">Actualiser</a>
</div>
<div class="data"> <span class="txtbold">Ligne</span> : 230 <div>dans <span class="txtbold">9</span> min direction <span class="txtbold">Cathédrale-Vieille Ville</span> <br>dans <span class="txtbold">12</span> min direction <span class="txtbold">Gambetta / France</span> <br>à <span class="txtbold">17h12</span> direction <span class="txtbold">Cathédrale-Vieille Ville</span> <br>à <span class="txtbold">17h22</span> direction <span class="txtbold">Cathédrale-Vieille Ville</span> * <br>à <span class="txtbold">17h32</span> direction <span class="txtbold">Cathédrale-Vieille Ville</span> * <br>à <span class="txtbold">17h37</span> direction <span class="txtbold">Gambetta / France</span> * <br>à <span class="txtbold">17h42</span> direction <span class="txtbold">Cathédrale-Vieille Ville</span> * <br> </div> </div> <div class="data"> <span class="txtbold">Ligne</span> : 233 <div>à <span class="txtbold">17h28</span> direction <span class="txtbold">Halte Routière de l'Ara</span> * <br> </div> </div> <div class="data"> <span class="txtbold">Ligne</span> : 232 <div>à <span class="txtbold">17h33</span> direction <span class="txtbold">Carros Médiathèque</span> * <br> </div> </div>
<div class="data"> <span class="txtbold">Ligne</span> : 230 <div>à l'approche direction <span class="txtbold">Cathédrale-Vieille Ville</span> <br>dans <span class="txtbold">11</span> min direction <span class="txtbold">Cathédrale-Vieille Ville</span> <br>dans <span class="txtbold">11</span> min direction <span class="txtbold">Cathédrale-Vieille Ville</span> <br>à <span class="txtbold">17h37</span> direction <span class="txtbold">Gambetta / France</span> <br>à <span class="txtbold">17h42</span> direction <span class="txtbold">Cathédrale-Vieille Ville</span> * <br>à <span class="txtbold">17h52</span> direction <span class="txtbold">Cathédrale-Vieille Ville</span> * <br>à <span class="txtbold">17h59</span> direction <span class="txtbold">Cathédrale-Vieille Ville</span> * <br>à <span class="txtbold">18h07</span> direction <span class="txtbold">Gambetta / France</span> * <br> </div> </div> <div class="data"> <span class="txtbold">Ligne</span> : 233 <div>dans <span class="txtbold">14</span> min direction <span class="txtbold">Halte Routière de l'Ara</span> <br> </div> </div> <div class="data"> <span class="txtbold">Ligne</span> : 232 <div>à <span class="txtbold">17h33</span> direction <span class="txtbold">Carros Médiathèque</span> * <br> </div> </div>

<div class="legend">
* horaires théoriques
Expand Down
35 changes: 0 additions & 35 deletions sophiabus230/tests/test_get_next_buses.py

This file was deleted.

71 changes: 71 additions & 0 deletions sophiabus230/tests/test_sophiabus230.py
@@ -0,0 +1,71 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from unittest import TestCase
import os
from datetime import datetime
from datetime import timedelta
import sophiabus230
from dateutil.tz import gettz
from dateutil.tz import tzfile
from mock import patch
from future.moves.urllib import request


class TestSophiabus230(TestCase):

expected = [
{'dest': u'Cathédrale-Vieille Ville',
'bus_time': None,
'is_real_time': True},
{'dest': u'Cathédrale-Vieille Ville',
'bus_time': None,
'is_real_time': True},
{'dest': u'Cathédrale-Vieille Ville',
'bus_time': None,
'is_real_time': True},
{'dest': u'Gambetta / France',
'bus_time': datetime(2017, 1, 26, 17, 37),
'is_real_time': True},
{'dest': u'Cathédrale-Vieille Ville',
'bus_time': datetime(2017, 1, 26, 17, 42),
'is_real_time': False},
{'dest': u'Cathédrale-Vieille Ville',
'bus_time': datetime(2017, 1, 26, 17, 52),
'is_real_time': False},
{'dest': u'Cathédrale-Vieille Ville',
'bus_time': datetime(2017, 1, 26, 17, 59),
'is_real_time': False},
{'dest': u'Gambetta / France',
'bus_time': datetime(2017, 1, 26, 18, 7),
'is_real_time': False}
]

@patch('sophiabus230._get_html_from_cg06')
def test_get_next_buses(self, mock_content):
parent_path = os.path.dirname(os.path.abspath(__file__))
with open(parent_path + os.sep + "example_content.html", 'rb') as fd:
mock_content.return_value = fd.read()
result_list = sophiabus230.get_next_buses(debug=True)
self.assertEqual(len(result_list), 8)
#TODO: add the expected tt's here (look at the logging stuff for the full list)
for index in range(len(result_list)):
if index > 2:
actual_date = result_list[index]['bus_time']
expected_date = self.expected[index]['bus_time']
self.assertEqual(actual_date.year, expected_date.year)
self.assertEqual(actual_date.month, expected_date.month)
self.assertEqual(actual_date.day, expected_date.day)
self.assertEqual(actual_date.hour, expected_date.hour)
self.assertEqual(actual_date.minute, expected_date.minute)
self.assertEqual(result_list[index]['dest'], self.expected[index]['dest'])
self.assertEqual(result_list[index]['is_real_time'], self.expected[index]['is_real_time'])

@patch('future.moves.urllib.request.urlopen')
def test_get_html_from_cg06(self, mock_urlopen):
parent_path = os.path.dirname(os.path.abspath(__file__))
with open(parent_path + os.sep + "example_content.html", 'rb') as fd:
mock_urlopen.return_value = fd
sophiabus230._get_html_from_cg06(1939)
assert mock_urlopen.called
mock_urlopen.assert_called_once_with('http://cg06.tsi.cityway.fr/qrcode/?id=1939')

0 comments on commit e4a2561

Please sign in to comment.