Make the script more robust for other cities. Creation of two subcommands: one for listing reservation kind, the second one to dump a kind of reservation.
This commit is contained in:
210
betton.py
210
betton.py
@@ -11,74 +11,31 @@ import re
|
|||||||
from io import StringIO
|
from io import StringIO
|
||||||
import getpass
|
import getpass
|
||||||
import os
|
import os
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from math import floor
|
from math import floor
|
||||||
from ics import Calendar, Event
|
from ics import Calendar, Event
|
||||||
|
|
||||||
|
|
||||||
def getKey(dictionnary, key):
|
def getKey(dictionnary, key):
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return dictionnary[key]
|
return dictionnary[key]
|
||||||
except:
|
except:
|
||||||
logger.error('Missing key: %s' % key)
|
logger.error('Missing key: %s' % key)
|
||||||
exit(-1)
|
exit(-1)
|
||||||
|
|
||||||
|
def authenticate(baseURL, city, login, password):
|
||||||
def main():
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
coloredlogs.install()
|
|
||||||
parser = argparse.ArgumentParser()
|
|
||||||
parser.add_argument("-l", "--login", dest='login', type=str, required=False, help="Username or login (usually a phone number with international prefix).")
|
|
||||||
parser.add_argument("-C", "--city", dest='city', type=str, required=False, help="City.")
|
|
||||||
parser.add_argument("-c", "--config", dest='configFileName', required=False, default=None, help="Configuration file.")
|
|
||||||
parser.add_argument("-p", "--password", dest='password', nargs='?', required=False, default=None, help="Password.")
|
|
||||||
parser.add_argument("-o", "--output", dest='calendar', required=True, default='cal.ics', help="Output calendar file.")
|
|
||||||
parser.add_argument("-d", "--debug", dest='debug', action='store_true', required=False, help="Activate debug.")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
logger.info("Initial arguments: %s" % args)
|
|
||||||
|
|
||||||
if args.debug:
|
|
||||||
logger.info('Setting logging to debug mode')
|
|
||||||
coloredlogs.set_level(level=logging.DEBUG)
|
|
||||||
|
|
||||||
if args.configFileName != None:
|
|
||||||
try:
|
|
||||||
configFile = open(args.configFileName, 'r')
|
|
||||||
except Exception as e:
|
|
||||||
logger.info('Impossible to open configuration file. Error: %s' % e)
|
|
||||||
exit(-1)
|
|
||||||
|
|
||||||
config = configparser.ConfigParser()
|
|
||||||
config.read_file(configFile)
|
|
||||||
|
|
||||||
sections = config.sections()
|
|
||||||
|
|
||||||
if 'Login' in sections:
|
|
||||||
options = config.options('Login')
|
|
||||||
if 'login' in options:
|
|
||||||
args.login = config.get('Login','login')
|
|
||||||
if 'password' in options:
|
|
||||||
args.password = config.get('Login','password')
|
|
||||||
|
|
||||||
logger.info("Final arguments: %s" % args)
|
|
||||||
|
|
||||||
if args.login == None:
|
|
||||||
logger.info('Login must be provided.')
|
|
||||||
parser.print_help()
|
|
||||||
exit(-1)
|
|
||||||
|
|
||||||
if args.password == None:
|
|
||||||
args.password = getpass.getpass()
|
|
||||||
|
|
||||||
# Récupération des cookies de base
|
|
||||||
baseURL = 'https://www.espace-citoyens.net'
|
|
||||||
url = baseURL + '/betton/espace-citoyens'
|
|
||||||
|
|
||||||
|
url = baseURL + '/%s/espace-citoyens' % city
|
||||||
logger.info('Retrieve base site')
|
logger.info('Retrieve base site')
|
||||||
html = requests.get(url, allow_redirects=False)
|
html = requests.get(url, allow_redirects=False)
|
||||||
if html.status_code != 200:
|
if html.status_code != 200:
|
||||||
logger.info('Impossible to retrieve Web site: %d' % html.status_code)
|
logger.error('Impossible to retrieve Web site: %d' % html.status_code)
|
||||||
|
exit(-1)
|
||||||
|
|
||||||
cookies = html.cookies
|
cookies = html.cookies
|
||||||
|
|
||||||
@@ -116,48 +73,53 @@ def main():
|
|||||||
|
|
||||||
logger.debug('Found token: %s' % (token))
|
logger.debug('Found token: %s' % (token))
|
||||||
|
|
||||||
payload = { 'username':args.login, 'password':args.password, '__RequestVerificationToken':token }
|
payload = { 'username':login, 'password':password, '__RequestVerificationToken':token }
|
||||||
|
|
||||||
auth = requests.post(logonURL, data=payload, cookies=cookies, allow_redirects=False)
|
auth = requests.post(logonURL, data=payload, cookies=cookies, allow_redirects=False)
|
||||||
if auth.status_code != 302:
|
if auth.status_code != 302:
|
||||||
logger.info('Impossible to login: %d' % html.status_code)
|
logger.error('Impossible to login: %d' % html.status_code)
|
||||||
|
exit(-1)
|
||||||
else:
|
else:
|
||||||
logger.info('Authentication successful')
|
logger.info('Authentication successful')
|
||||||
|
|
||||||
mainpage = baseURL+auth.headers['Location']
|
mainpage = baseURL+auth.headers['Location']
|
||||||
|
return mainpage, cookies
|
||||||
html = requests.get(mainpage, cookies=cookies, allow_redirects=False)
|
|
||||||
if html.status_code != 200:
|
|
||||||
logger.info('Impossible to retrieve main page: %d' % html.status_code)
|
|
||||||
|
|
||||||
|
|
||||||
foundCategory = False
|
|
||||||
found = False
|
|
||||||
p = re.compile('^.*>Accueil de loisirs mercredi<.*$')
|
|
||||||
content = StringIO(html.content.decode('utf8'))
|
|
||||||
|
|
||||||
for line in content.readlines():
|
|
||||||
m = p.match(line)
|
|
||||||
if m != None:
|
|
||||||
if not foundCategory:
|
|
||||||
# If we found the kind of reservation we change the regexp
|
|
||||||
p = re.compile('^.*href="(?P<url>[^"]+)".*fleche.png".*$')
|
|
||||||
foundCategory = True
|
|
||||||
else:
|
|
||||||
found = True
|
|
||||||
resa = baseURL+m.group('url')
|
|
||||||
break
|
|
||||||
|
|
||||||
if found:
|
def getReservationsKind(baseURL, url, cookies):
|
||||||
logger.debug("Found mercredi: %s" % resa)
|
logger = logging.getLogger(__name__)
|
||||||
else:
|
|
||||||
logger.error("Impossible to find accueil du mercredi")
|
# Retrieve main page
|
||||||
|
html = requests.get(url, cookies=cookies, allow_redirects=False)
|
||||||
|
if html.status_code != 200:
|
||||||
|
logger.error('Impossible to retrieve main page: %d' % html.status_code)
|
||||||
exit(-1)
|
exit(-1)
|
||||||
|
|
||||||
|
soup = BeautifulSoup(html.text, 'html.parser')
|
||||||
|
resas = soup.find_all('table', id='tblDalleDetail_Reservation')
|
||||||
|
|
||||||
html = requests.get(resa, cookies=cookies, allow_redirects=False)
|
if len(resas) != 1:
|
||||||
|
logger.error('Too many kind of reservations')
|
||||||
|
exit(-1)
|
||||||
|
|
||||||
|
resas = resas[0]
|
||||||
|
resas = resas.find_all('tr')
|
||||||
|
|
||||||
|
resaTypes = {}
|
||||||
|
for resa in resas:
|
||||||
|
resaType = resa.find('td', id=re.compile('ReservationActivite')).get_text()
|
||||||
|
url = resa.find('a').get('href')
|
||||||
|
resaTypes[resaType] = baseURL+url
|
||||||
|
|
||||||
|
return resaTypes
|
||||||
|
|
||||||
|
# Dump reservation into a calendar file
|
||||||
|
def dumpReservation(resaType, baseURL, city, url, cookies, output):
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
html = requests.get(url, cookies=cookies, allow_redirects=False)
|
||||||
if html.status_code != 200:
|
if html.status_code != 200:
|
||||||
logger.info('Impossible to retrieve reservation page: %d' % html.status_code)
|
logger.info('Impossible to retrieve reservation page: %d' % html.status_code)
|
||||||
|
exit(-1)
|
||||||
|
|
||||||
variables = ['idPer', 'idIns', 'idLie', 'idClg']
|
variables = ['idPer', 'idIns', 'idLie', 'idClg']
|
||||||
values = {}
|
values = {}
|
||||||
@@ -178,9 +140,11 @@ def main():
|
|||||||
else:
|
else:
|
||||||
logger.debug('Found value for var %s: %d' % (var, value))
|
logger.debug('Found value for var %s: %d' % (var, value))
|
||||||
|
|
||||||
calendar = requests.get(baseURL+'/betton/espace-citoyens/DemandeEnfance/NouvelleDemandeReservationGetCalendrier', params=values, cookies=cookies)
|
# This URL should be retrieved more automatically ...
|
||||||
|
calendar = requests.get(baseURL+'/%s/espace-citoyens/DemandeEnfance/NouvelleDemandeReservationGetCalendrier' % city, params=values, cookies=cookies)
|
||||||
if calendar.status_code != 200:
|
if calendar.status_code != 200:
|
||||||
logger.info('Impossible to retrieve calendar: %d' % html.status_code)
|
logger.info('Impossible to retrieve calendar: %d' % html.status_code)
|
||||||
|
exit(-1)
|
||||||
|
|
||||||
calendar = json.load(StringIO(calendar.content.decode('utf8')))
|
calendar = json.load(StringIO(calendar.content.decode('utf8')))
|
||||||
weeks = getKey(calendar, 'listeSemainesAffichees')
|
weeks = getKey(calendar, 'listeSemainesAffichees')
|
||||||
@@ -221,14 +185,90 @@ def main():
|
|||||||
begin = datetime(year,month,day,12,00,0, tzinfo=ZoneInfo("Europe/Paris"))
|
begin = datetime(year,month,day,12,00,0, tzinfo=ZoneInfo("Europe/Paris"))
|
||||||
end = datetime(year,month,day,13,30,0, tzinfo=ZoneInfo("Europe/Paris"))
|
end = datetime(year,month,day,13,30,0, tzinfo=ZoneInfo("Europe/Paris"))
|
||||||
else:
|
else:
|
||||||
logger.error('Impossible to determine the type of reservation: %s' % code)
|
logger.warning('Impossible to determine the type of reservation: %s' % code)
|
||||||
exit(-1)
|
begin = datetime(year,month,day)
|
||||||
|
end = datetime(year,month,day)
|
||||||
e.begin = begin
|
e.begin = begin
|
||||||
e.end = end
|
e.end = end
|
||||||
cal.events.add(e)
|
cal.events.add(e)
|
||||||
|
|
||||||
with open(args.calendar, 'w') as f:
|
with open(output, 'w') as f:
|
||||||
f.writelines(cal.serialize_iter())
|
f.writelines(cal.serialize_iter())
|
||||||
|
|
||||||
|
def main():
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
coloredlogs.install()
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("-l", "--login", dest='login', type=str, required=False, help="Login")
|
||||||
|
parser.add_argument("-C", "--city", dest='city', type=str, required=False, help="City.")
|
||||||
|
parser.add_argument("-c", "--config", dest='configFileName', required=False, default=None, help="Configuration file.")
|
||||||
|
parser.add_argument("-p", "--password", dest='password', nargs='?', required=False, default=None, help="Password.")
|
||||||
|
parser.add_argument("-d", "--debug", dest='debug', action='store_true', required=False, help="Activate debug.")
|
||||||
|
subparsers = parser.add_subparsers(title='subcommands', dest='command', help='subcommand help')
|
||||||
|
parserlist = subparsers.add_parser('L', help='List all possible reservations types.')
|
||||||
|
parserdump = subparsers.add_parser('D', help='Dump all reservation of some kind')
|
||||||
|
parserdump.add_argument("-i", "--index", dest='index', type=int, required=True, help="Index of reservations to dump.")
|
||||||
|
parserdump.add_argument("-o", "--output", dest='calendar', required=True, default='cal.ics', help="Output calendar file.")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
logger.info("Initial arguments: %s" % args)
|
||||||
|
|
||||||
|
if args.debug:
|
||||||
|
logger.info('Setting logging to debug mode')
|
||||||
|
coloredlogs.set_level(level=logging.DEBUG)
|
||||||
|
|
||||||
|
if args.configFileName != None:
|
||||||
|
try:
|
||||||
|
configFile = open(args.configFileName, 'r')
|
||||||
|
except Exception as e:
|
||||||
|
logger.info('Impossible to open configuration file. Error: %s' % e)
|
||||||
|
exit(-1)
|
||||||
|
|
||||||
|
config = configparser.ConfigParser()
|
||||||
|
config.read_file(configFile)
|
||||||
|
|
||||||
|
sections = config.sections()
|
||||||
|
|
||||||
|
if 'Login' in sections:
|
||||||
|
options = config.options('Login')
|
||||||
|
if 'login' in options:
|
||||||
|
args.login = config.get('Login','login')
|
||||||
|
if 'password' in options:
|
||||||
|
args.password = config.get('Login','password')
|
||||||
|
|
||||||
|
logger.info("Final arguments: %s" % args)
|
||||||
|
|
||||||
|
if args.city == None:
|
||||||
|
logger.info('City must be provided.')
|
||||||
|
parser.print_help()
|
||||||
|
exit(-1)
|
||||||
|
|
||||||
|
if args.login == None:
|
||||||
|
logger.info('Login must be provided.')
|
||||||
|
parser.print_help()
|
||||||
|
exit(-1)
|
||||||
|
|
||||||
|
if args.password == None:
|
||||||
|
args.password = getpass.getpass()
|
||||||
|
|
||||||
|
baseURL = 'https://www.espace-citoyens.net'
|
||||||
|
|
||||||
|
# Authentication
|
||||||
|
mainpage, cookies = authenticate(baseURL, args.city, args.login, args.password)
|
||||||
|
|
||||||
|
|
||||||
|
# Switch between commands
|
||||||
|
resaTypes = getReservationsKind(baseURL, mainpage, cookies)
|
||||||
|
|
||||||
|
if (args.command == None) or (args.command == 'L'):
|
||||||
|
resaNum = 1
|
||||||
|
for resaType in resaTypes.keys():
|
||||||
|
print("%d - %s" % (resaNum, resaType))
|
||||||
|
resaNum+=1
|
||||||
|
elif (args.command == 'D'):
|
||||||
|
resaType = list(resaTypes)[args.index-1]
|
||||||
|
url = resaTypes[resaType]
|
||||||
|
dumpReservation(resaType, baseURL, args.city, url, cookies, args.calendar)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
requests
|
requests
|
||||||
coloredlogs
|
coloredlogs
|
||||||
|
bs4
|
||||||
ics
|
ics
|
||||||
|
|||||||
Reference in New Issue
Block a user