diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..b2be92b7db01b7bfebb8e0aabd2f546906ff651a --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +result diff --git a/default.nix b/default.nix index ba8ea8f10d403e96ce9da3ea924a210055c9eea7..d2ce1019f4f1cc4ceb617e0676b364859c83593d 100644 --- a/default.nix +++ b/default.nix @@ -5,11 +5,43 @@ }) {} }: -pkgs.mkShell { - buildInputs = with pkgs; [ - python3Packages.ipython - python3Packages.requests - python3Packages.pandas - python3Packages.ics - ]; +let + pyPkgs = pkgs.python3Packages; +in rec { + lflex_celcat_survival = pyPkgs.buildPythonPackage { + pname = "lflex_celcat_survival"; + version = "local"; + format = "pyproject"; + + src = pkgs.lib.sourceByRegex ./. [ + "pyproject\.toml" + "LICENSE" + "lflex_celcat_survival" + "lflex_celcat_survival/.*\.py" + "lflex_celcat_survival/cmd" + "lflex_celcat_survival/cmd/.*\.py" + ]; + buildInputs = with pyPkgs; [ + flit + ]; + propagatedBuildInputs = with pyPkgs; [ + ics + pandas + requests + click + ]; + }; + + user-shell = pkgs.mkShell { + buildInputs = with pyPkgs; [ + ipython + lflex_celcat_survival + ]; + }; + + dev-shell = pkgs.mkShell { + buildInputs = with pyPkgs; [ + ipython + ] ++ lflex_celcat_survival.propagatedBuildInputs; + }; } diff --git a/lflex_celcat_survival/__init__.py b/lflex_celcat_survival/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a12d1d53b13924e7b0634dc862c8acb73521a157 --- /dev/null +++ b/lflex_celcat_survival/__init__.py @@ -0,0 +1,4 @@ +from . import course_request +from . import events +from . import fetch +from . import ics diff --git a/lflex_celcat_survival/cmd/fetch_ics.py b/lflex_celcat_survival/cmd/fetch_ics.py new file mode 100755 index 0000000000000000000000000000000000000000..2fc03e03a1b9a41e3d62c2dbcce17a33c5286cf5 --- /dev/null +++ b/lflex_celcat_survival/cmd/fetch_ics.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +import click +import logging +import lflex_celcat_survival as lcs + +@click.command() +@click.argument('course_request_file') +@click.option('--output-file', '-o', default=None, help='Where to write the generated ICS (stdout if unset).') +def main(course_request_file, output_file): + logging.basicConfig(level=logging.INFO) + req = lcs.course_request.CourseRequest(course_request_file) + celcat_raw_response = req.do_request() + + celcat_events = lcs.events.CelcatEvents(celcat_raw_response) + filtered_celcat_events = lcs.events.FilteredCelcatEvents(req, celcat_events) + filtered_celcat_events.check_expected_nb_timeslots() + + calendar = lcs.ics.course_df_to_ics(filtered_celcat_events.df) + + if output_file is None: + print(calendar) + else: + with open(output_file, 'w') as f: + f.write(str(calendar)) + +if __name__ == "__main__": + main() diff --git a/lflex_celcat_survival/course_request.py b/lflex_celcat_survival/course_request.py new file mode 100644 index 0000000000000000000000000000000000000000..8614f2368263ed121bb83a476cb10c20bf12f2c0 --- /dev/null +++ b/lflex_celcat_survival/course_request.py @@ -0,0 +1,18 @@ +import pandas as pd +from . import fetch + +class CourseRequest: + def __init__(self, filename): + self.df = pd.read_csv(filename, parse_dates=['begin_date', 'end_date']) + self.df['course_request_id'] = self.df.index + + def generate_request_input(self): + date_range_min = min(self.df['begin_date']).strftime("%Y-%m-%d") + date_range_max = (max(self.df['end_date']) + pd.Timedelta(days=1)).strftime("%Y-%m-%d") + apogee_codes = self.df['module_apogee'].unique() + + return (date_range_min, date_range_max, apogee_codes) + + def do_request(self, url='https://edt.univ-tlse3.fr/calendar2/Home/GetCalendarData'): + (date_min, date_max, apogee_codes) = self.generate_request_input() + return fetch.do_celcat_calendar_request(date_min, date_max, apogee_codes, url) diff --git a/src/script.py b/lflex_celcat_survival/events.py similarity index 63% rename from src/script.py rename to lflex_celcat_survival/events.py index 5861c327fa3a33bdd7eceb12dd7caf00ad390ff1..5ae5ee29626b9e75d6927993cbf47686139fe203 100644 --- a/src/script.py +++ b/lflex_celcat_survival/events.py @@ -1,42 +1,5 @@ -#!/usr/bin/env python3 -import ics -import requests -import pandas as pd import logging - -class CourseRequest: - def __init__(self, filename): - self.df = pd.read_csv(filename, parse_dates=['begin_date', 'end_date']) - self.df['course_request_id'] = self.df.index - - def generate_request_input(self): - date_range_min = min(self.df['begin_date']).strftime("%Y-%m-%d") - date_range_max = (max(self.df['end_date']) + pd.Timedelta(days=1)).strftime("%Y-%m-%d") - apogee_codes = self.df['module_apogee'].unique() - - return (date_range_min, date_range_max, apogee_codes) - - def do_request(self, url='https://edt.univ-tlse3.fr/calendar2/Home/GetCalendarData'): - (date_min, date_max, apogee_codes) = self.generate_request_input() - return do_celcat_calendar_request(date_min, date_max, apogee_codes, url) - -def do_celcat_calendar_request(min_date, max_date, module_apogee_codes, url='https://edt.univ-tlse3.fr/calendar2/Home/GetCalendarData'): - headers = {"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"} - fields = [ - f'start={min_date}', - f'end={max_date}', - 'resType=100', - 'calView=agendaWeek', - ] + ['federationIds%5B%5D={}'.format(apogee_code) for apogee_code in module_apogee_codes] - fields_str = '&'.join(fields) - - logging.info(f'Sending a POST request with data={fields_str}') - response = requests.post(url, data=fields_str, headers=headers) - if not response.ok: - logging.error(f'POST HTTP request failed (status code {response.status_code}): {response.reason}') - logging.error(f'Request response text:\n---\n{response.text}\n---') - response.raise_for_status() - return response.text +import pandas as pd class CelcatEvents: def __init__(self, celcat_raw_response): @@ -132,30 +95,3 @@ class FilteredCelcatEvents: groups_joined = ' '.join(groups) return pd.Series([room, course_type, groups_joined], index=['room_parsed', 'course_type_parsed', 'groups_parsed']) - -def course_df_to_ics(df): - c = ics.Calendar() - for _, row in df.iterrows(): - event = ics.Event( - name = f'{row["module_readable"]} - {row["course_type"]} - {row["groups_parsed"]}', - begin = row['start'].tz_localize(tz='Europe/Paris'), - end = row['end'].tz_localize(tz='Europe/Paris'), - ) - if row['room_parsed'] != 'unset': - event.location = row['room_parsed'] - c.events.add(event) - - return c - -logging.basicConfig(level=logging.INFO) -req = CourseRequest('input-data.csv') -celcat_raw_response = req.do_request() - -celcat_events = CelcatEvents(celcat_raw_response) -filtered_celcat_events = FilteredCelcatEvents(req, celcat_events) -filtered_celcat_events.check_expected_nb_timeslots() - -c = course_df_to_ics(filtered_celcat_events.df) - -with open('out.ics', 'w') as f: - f.writelines(c) diff --git a/lflex_celcat_survival/fetch.py b/lflex_celcat_survival/fetch.py new file mode 100644 index 0000000000000000000000000000000000000000..858a1ea2615920b1e87d3afcf78680f9291dda34 --- /dev/null +++ b/lflex_celcat_survival/fetch.py @@ -0,0 +1,20 @@ +import logging +import requests + +def do_celcat_calendar_request(min_date, max_date, module_apogee_codes, url='https://edt.univ-tlse3.fr/calendar2/Home/GetCalendarData'): + headers = {"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"} + fields = [ + f'start={min_date}', + f'end={max_date}', + 'resType=100', + 'calView=agendaWeek', + ] + ['federationIds%5B%5D={}'.format(apogee_code) for apogee_code in module_apogee_codes] + fields_str = '&'.join(fields) + + logging.info(f'Fetching modules {module_apogee_codes} from {min_date} to {min_date} on url={url}') + response = requests.post(url, data=fields_str, headers=headers) + if not response.ok: + logging.error(f'POST HTTP request failed (status code {response.status_code}): {response.reason}') + logging.error(f'Request response text:\n---\n{response.text}\n---') + response.raise_for_status() + return response.text diff --git a/lflex_celcat_survival/ics.py b/lflex_celcat_survival/ics.py new file mode 100644 index 0000000000000000000000000000000000000000..5193287200436eec831d9f8ea5849123a054809e --- /dev/null +++ b/lflex_celcat_survival/ics.py @@ -0,0 +1,15 @@ +import ics + +def course_df_to_ics(df): + c = ics.Calendar() + for _, row in df.iterrows(): + event = ics.Event( + name = f'{row["module_readable"]} - {row["course_type"]} - {row["groups_parsed"]}', + begin = row['start'].tz_localize(tz='Europe/Paris'), + end = row['end'].tz_localize(tz='Europe/Paris'), + ) + if row['room_parsed'] != 'unset': + event.location = row['room_parsed'] + c.events.add(event) + + return c diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000000000000000000000000000000000..d33cfe36b692ca35353e6486bf72a2223b2e1de7 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,32 @@ +[build-system] +build-backend = "flit_core.buildapi" +requires = ["flit_core"] + +[project] +name = "lflex_celcat_survival" +version = "0.1.0" +description = "Set of tools to stay sane while using a CELCAT calendar" +authors = [ + {name = "Millian Poquet", email="millian.poquet@irit.fr"}, +] +license = {file = "LICENSE"} +requires-python = ">=3.9" + +keywords = ["calendar", "celcat"] + +classifiers = [ + "Topic :: Software Development", + "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", + "Programming Language :: Python", + "Programming Language :: Python :: 3.9", +] + +dependencies = [ + "ics>=0.7.0", + "pandas>=1.3.0", + "requests>=2.26.0", + "click>=8.0.0" +] + +[project.scripts] +fetch-ics = "lflex_celcat_survival.cmd.fetch_ics:main"