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:
Frédéric Tronel
2025-08-27 14:18:40 +02:00
parent 107970f241
commit c200e0e95a
2 changed files with 126 additions and 85 deletions

206
betton.py
View File

@@ -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']
return mainpage, cookies
html = requests.get(mainpage, cookies=cookies, allow_redirects=False)
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.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:
logger.debug("Found mercredi: %s" % resa)
else:
logger.error("Impossible to find accueil du mercredi")
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()

View File

@@ -1,3 +1,4 @@
requests
coloredlogs
bs4
ics