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

210
betton.py
View File

@@ -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()

View File

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