diff --git a/config.py b/config.py new file mode 100644 index 0000000..5c7ae7b --- /dev/null +++ b/config.py @@ -0,0 +1,13 @@ +import os + +from dotenv import load_dotenv + +load_dotenv() + +MENU_CRECHE_PDF_URL = os.environ.get("MENU_CRECHE_PDF_URL") +MENU_TYPE = os.environ.get("MENU_KIND") +SIGNAL_SENDER = os.environ.get("SIGNAL_SENDER") +SIGNAL_RECIPIENTS = os.environ.get("SIGNAL_RECIPIENTS", default=SIGNAL_SENDER).split( + "," +) +SIGNAL_API_URL = os.environ.get("SIGNAL_API_URL") diff --git a/main.py b/main.py index d584fa0..73e71fc 100644 --- a/main.py +++ b/main.py @@ -1,128 +1,79 @@ import base64 import tempfile from pathlib import Path -import os + import requests from dotenv import load_dotenv from pdfminer.high_level import extract_text +from config import ( + MENU_CRECHE_PDF_URL, + MENU_TYPE, + SIGNAL_SENDER, + SIGNAL_RECIPIENTS, + SIGNAL_API_URL, +) +from menus import Menus, MenuMessageFormatter +from message_sender import SignalMessager +from pdf_downloader import download + load_dotenv() -def main(): - menu_link = os.environ.get("MENU_CRECHE_PDF_URL") - menu_pdf = requests.get(menu_link).content - - tmp_folder = Path(tempfile.mkdtemp()) - menu_file = tmp_folder / "menu.pdf" - menu_file.write_bytes(menu_pdf) - - text = extract_text(menu_file, page_numbers=[0]) - - previous_was_title = False - curated_text = "" - for line in text.splitlines(): - if len(line) == 1: - previous_was_title = True - elif len(line) > 1 and line[1] == " ": - curated_text += f"{line[2:]}\n" - previous_was_title = True - elif len(line) > 0 and line[0].islower(): - curated_text = curated_text.removesuffix("\n") - curated_text += f"{line}\n" - previous_was_title = False - elif line == '' and previous_was_title: - previous_was_title = False - else: - curated_text += f"{line}\n" - previous_was_title = False - - cells = curated_text.split("\n\n") - - # remove month - month = cells.pop(0) - - # remove meals titles - for i in range(5): - cells.pop(0) - - days = [] - for i in range(5): - days.append(cells.pop(0)) - - menus = { - "introduction": { - days[0]: {"midi": cells[0], "gouter": cells[5]}, - days[1]: {"midi": cells[1], "gouter": cells[6]}, - days[2]: {"midi": cells[2], "gouter": cells[7]}, - days[3]: {"midi": cells[3], "gouter": cells[8]}, - days[4]: {"midi": cells[4], "gouter": cells[9]}, - }, - "diversification": { - days[0]: {"midi": cells[10], "gouter": cells[15]}, - days[1]: {"midi": cells[11], "gouter": cells[16]}, - days[2]: {"midi": cells[12], "gouter": cells[17]}, - days[3]: {"midi": cells[13], "gouter": cells[18]}, - days[4]: {"midi": cells[14], "gouter": cells[19]}, - }, - "petit musclé": { - days[0]: {"midi": cells[20], "gouter": cells[24]}, - days[1]: {"midi": cells[21], "gouter": cells[25]}, - days[2]: {"midi": cells[22], "gouter": cells[26]}, - days[3]: {"midi": cells[23], "gouter": cells[27]}, - days[4]: {"midi": cells[32], "gouter": cells[39]}, - }, - "petit lion": { - days[0]: {"midi": cells[28], "gouter": cells[35]}, - days[1]: {"midi": cells[29], "gouter": cells[36]}, - days[2]: {"midi": cells[30], "gouter": cells[37]}, - days[3]: {"midi": cells[31], "gouter": cells[38]}, - days[4]: {"midi": cells[34], "gouter": cells[33]}, - } - } - - menu = os.environ.get("MENU_KIND") - +def send_signal_message( + days: list[str], menu: dict[str, dict[str, str]], menu_pdf_file: Path +) -> None: headers = {"Content-Type": "application/json"} message = f""" **Menu de la crèche** (message automatique) -Menu {menu}: +Menu {MENU_TYPE}: {days[0].upper()}: -*Midi*: {menus[menu][days[0]]["midi"]} -*Goûter*: {menus[menu][days[0]]["gouter"]} +*Midi*: {menu[days[0]]["midi"]} +*Goûter*: {menu[days[0]]["gouter"]} {days[1].upper()}: -*Midi*: {menus[menu][days[1]]["midi"]} -*Goûter*: {menus[menu][days[1]]["gouter"]} +*Midi*: {menu[days[1]]["midi"]} +*Goûter*: {menu[days[1]]["gouter"]} {days[2].upper()}: -*Midi*: {menus[menu][days[2]]["midi"]} -*Goûter*: {menus[menu][days[2]]["gouter"]} +*Midi*: {menu[days[2]]["midi"]} +*Goûter*: {menu[days[2]]["gouter"]} {days[3].upper()}: -*Midi*: {menus[menu][days[3]]["midi"]} -*Goûter*: {menus[menu][days[3]]["gouter"]} +*Midi*: {menu[days[3]]["midi"]} +*Goûter*: {menu[days[3]]["gouter"]} {days[4].upper()}: -*Midi*: {menus[menu][days[4]]["midi"]} -*Goûter*: {menus[menu][days[4]]["gouter"]} +*Midi*: {menu[days[4]]["midi"]} +*Goûter*: {menu[days[4]]["gouter"]} """ text_mode = "styled" - sender = os.environ.get("SIGNAL_SENDER") - recipient = os.environ.get("SIGNAL_RECIPIENT") - attachment = base64.b64encode(menu_pdf) + attachment = base64.b64encode(menu_pdf_file.read_bytes()) body = { "message": message, "text_mode": text_mode, - "number": sender, - "recipients": [recipient], - "base64_attachments": [f"data:application/pdf;filename=menu.pdf;base64,{attachment.decode()}"], + "number": SIGNAL_SENDER, + "recipients": SIGNAL_RECIPIENTS, + "base64_attachments": [ + f"data:application/pdf;filename=menu.pdf;base64,{attachment.decode()}" + ], } - host = os.environ.get("SIGNAL_API_URL") - requests.post(host, headers=headers, json=body) + requests.post(SIGNAL_API_URL, headers=headers, json=body) -if __name__ == '__main__': +def main(): + menu_pdf_file = download(MENU_CRECHE_PDF_URL, file_name="menu.pdf") + + menus = Menus(menu_pdf_file) + + message_formatter = MenuMessageFormatter() + message = message_formatter.create_message(menus, MENU_TYPE) + + signal_messager = SignalMessager(SIGNAL_API_URL, SIGNAL_SENDER) + signal_messager.send_message(message, menu_pdf_file, SIGNAL_RECIPIENTS) + + +if __name__ == "__main__": main() diff --git a/menus.py b/menus.py new file mode 100644 index 0000000..4811bc5 --- /dev/null +++ b/menus.py @@ -0,0 +1,144 @@ +import base64 +from pathlib import Path + +from pdfminer.high_level import extract_text + + +class Menus: + def __init__(self, menu_pdf_file: Path): + self.menu_pdf_file = menu_pdf_file + + self._cells = self.extract_cells() + self.month = self.extract_month() + self.days = self.extract_days() + + self.introduction = self.extract_introduction_menu() + self.diversification = self.extract_diversification_menu() + self.petit_muscle = self.extract_petit_muscle_menu() + self.petit_lion = self.extract_petit_lion_menu() + + def extract_cells(self) -> list[str]: + text = extract_text(self.menu_pdf_file, page_numbers=[0]) + + previous_was_title = False + curated_text = "" + for element in text.splitlines(): + line = Line(element) + if line.is_glitch(): + previous_was_title = True + elif line.starts_with_glitch(): + curated_text += f"{line.text[2:]}\n" + previous_was_title = True + elif line.is_part_of_previous_line(): + curated_text = curated_text.removesuffix("\n") + curated_text += f"{line.text}\n" + previous_was_title = False + elif line.text == "" and previous_was_title: + previous_was_title = False + else: + curated_text += f"{line.text}\n" + previous_was_title = False + + return curated_text.split("\n\n") + + def extract_month(self) -> str: + return self._cells[0] + + def extract_days(self) -> list[str]: + return self._cells[6:11] + + def extract_introduction_menu(self) -> dict[str, dict[str, str]]: + return { + self.days[0]: self.build_menu_of_day(11, 16), + self.days[1]: self.build_menu_of_day(12, 17), + self.days[2]: self.build_menu_of_day(13, 18), + self.days[3]: self.build_menu_of_day(14, 19), + self.days[4]: self.build_menu_of_day(15, 20), + } + + def extract_diversification_menu(self) -> dict[str, dict[str, str]]: + return { + self.days[0]: self.build_menu_of_day(21, 26), + self.days[1]: self.build_menu_of_day(22, 27), + self.days[2]: self.build_menu_of_day(23, 28), + self.days[3]: self.build_menu_of_day(24, 29), + self.days[4]: self.build_menu_of_day(25, 30), + } + + def extract_petit_muscle_menu(self) -> dict[str, dict[str, str]]: + return { + self.days[0]: self.build_menu_of_day(31, 35), + self.days[1]: self.build_menu_of_day(32, 36), + self.days[2]: self.build_menu_of_day(33, 37), + self.days[3]: self.build_menu_of_day(34, 38), + self.days[4]: self.build_menu_of_day(43, 50), + } + + def extract_petit_lion_menu(self): + return { + self.days[0]: self.build_menu_of_day(39, 46), + self.days[1]: self.build_menu_of_day(40, 47), + self.days[2]: self.build_menu_of_day(41, 48), + self.days[3]: self.build_menu_of_day(42, 49), + self.days[4]: self.build_menu_of_day(45, 44), + } + + def build_menu_of_day(self, midi: int, gouter: int) -> dict[str, str]: + return {"midi": self._cells[midi], "gouter": self._cells[gouter]} + + +class Line: + def __init__(self, text: str): + self.text = text + + def is_glitch(self) -> bool: + return len(self.text) == 1 + + def starts_with_glitch(self) -> bool: + return len(self.text) > 1 and self.text[1] == " " + + def is_part_of_previous_line(self) -> bool: + return len(self.text) > 0 and self.text[0].islower() + + +class MenuMessageFormatter: + @staticmethod + def create_message(menus: Menus, menu_type: str) -> str: + days = menus.days + if menu_type.lower() == "introduction": + menu = menus.introduction + elif menu_type.lower() == "diversification": + menu = menus.diversification + elif menu_type.lower() == "petit muscle": + menu = menus.petit_muscle + elif menu_type.lower() == "petit lion": + menu = menus.petit_lion + else: + raise Exception(f"Unknown menu type: {menu_type}") + + return f""" +**Menu de la crèche** (message automatique) + +Menu {menu_type}: + +{days[0].upper()}: +*Midi*: {menu[days[0]]["midi"]} +*Goûter*: {menu[days[0]]["gouter"]} + +{days[1].upper()}: +*Midi*: {menu[days[1]]["midi"]} +*Goûter*: {menu[days[1]]["gouter"]} + +{days[2].upper()}: +*Midi*: {menu[days[2]]["midi"]} +*Goûter*: {menu[days[2]]["gouter"]} + +{days[3].upper()}: +*Midi*: {menu[days[3]]["midi"]} +*Goûter*: {menu[days[3]]["gouter"]} + +{days[4].upper()}: +*Midi*: {menu[days[4]]["midi"]} +*Goûter*: {menu[days[4]]["gouter"]} +""" + diff --git a/message_sender.py b/message_sender.py new file mode 100644 index 0000000..c97feaa --- /dev/null +++ b/message_sender.py @@ -0,0 +1,36 @@ +import base64 +from pathlib import Path + +import requests + +from menus import MenuMessageFormatter, Menus + + +class SignalMessager: + def __init__(self, api_url: str, user_number: str): + self.api_url = api_url + self.user_number = user_number + + @staticmethod + def create_headers() -> dict[str, str]: + return {"Content-Type": "application/json"} + + @staticmethod + def create_attachment(pdf: Path): + return base64.b64encode(pdf.read_bytes()) + + def send_message(self, message: str, pdf_file: Path, recipients: list[str]): + headers = self.create_headers() + attachment = self.create_attachment(pdf_file) + + body = { + "message": message, + "text_mode": "styled", + "number": self.user_number, + "recipients": recipients, + "base64_attachments": [ + f"data:application/pdf;filename=menu.pdf;base64,{attachment.decode()}" + ], + } + + requests.post(self.api_url, headers=headers, json=body) diff --git a/pdf_downloader.py b/pdf_downloader.py new file mode 100644 index 0000000..a9ccd72 --- /dev/null +++ b/pdf_downloader.py @@ -0,0 +1,18 @@ +import tempfile +from pathlib import Path + +import requests + + +def download( + url: str, + destination_folder: Path = Path(tempfile.mkdtemp()), + file_name: str = "file.pdf", +) -> Path: + response = requests.get(url) + response.raise_for_status() + + output_file = destination_folder / file_name + output_file.write_bytes(response.content) + + return output_file diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..40cd4be --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,2 @@ +-r requirements.txt +ruff \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 7bbec04..105fdb4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ requests~=2.32.3 -pdfminer.six~=20250506 +pdfminer.six==20250506 python-dotenv~=1.1.0