From c200e0e95adf63ead4a4bf2373da95d450373b2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Tronel?= Date: Wed, 27 Aug 2025 14:18:40 +0200 Subject: [PATCH] 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. --- betton.py | 210 ++++++++++++++++++++++++++++------------------- requirements.txt | 1 + 2 files changed, 126 insertions(+), 85 deletions(-) diff --git a/betton.py b/betton.py index 02917af..2958b51 100755 --- a/betton.py +++ b/betton.py @@ -11,74 +11,31 @@ import re from io import StringIO import getpass import os +from bs4 import BeautifulSoup from zoneinfo import ZoneInfo from datetime import datetime from math import floor from ics import Calendar, Event + def getKey(dictionnary, key): + logger = logging.getLogger(__name__) + try: return dictionnary[key] except: logger.error('Missing key: %s' % key) exit(-1) - - -def main(): + +def authenticate(baseURL, city, login, password): 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') html = requests.get(url, allow_redirects=False) 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 @@ -116,48 +73,53 @@ def main(): 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) 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: logger.info('Authentication successful') mainpage = baseURL+auth.headers['Location'] - - 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[^"]+)".*fleche.png".*$') - foundCategory = True - else: - found = True - resa = baseURL+m.group('url') - break + return mainpage, cookies - if found: - logger.debug("Found mercredi: %s" % resa) - else: - logger.error("Impossible to find accueil du mercredi") +def getReservationsKind(baseURL, url, cookies): + logger = logging.getLogger(__name__) + + # 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) + 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: logger.info('Impossible to retrieve reservation page: %d' % html.status_code) + exit(-1) variables = ['idPer', 'idIns', 'idLie', 'idClg'] values = {} @@ -178,9 +140,11 @@ def main(): else: 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: logger.info('Impossible to retrieve calendar: %d' % html.status_code) + exit(-1) calendar = json.load(StringIO(calendar.content.decode('utf8'))) weeks = getKey(calendar, 'listeSemainesAffichees') @@ -221,14 +185,90 @@ def main(): begin = datetime(year,month,day,12,00,0, tzinfo=ZoneInfo("Europe/Paris")) end = datetime(year,month,day,13,30,0, tzinfo=ZoneInfo("Europe/Paris")) else: - logger.error('Impossible to determine the type of reservation: %s' % code) - exit(-1) + logger.warning('Impossible to determine the type of reservation: %s' % code) + begin = datetime(year,month,day) + end = datetime(year,month,day) e.begin = begin e.end = end cal.events.add(e) - with open(args.calendar, 'w') as f: + with open(output, 'w') as f: 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__": main() diff --git a/requirements.txt b/requirements.txt index 710d9f5..6e8a9d1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ requests coloredlogs +bs4 ics