From 355230632d068a7dfd287f920bf0b4bff09fa2a2 Mon Sep 17 00:00:00 2001
From: Bastien Le Querrec <blq@laquadrature.net>
Date: Sun, 9 Jun 2024 14:58:45 +0200
Subject: [PATCH] =?UTF-8?q?Attrap:=20am=C3=A9liore=20la=20documentation=20?=
 =?UTF-8?q?du=20code.?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Closes #7
Closes #8
---
 Attrap.py | 88 +++++++++++++++++++++++++++++++++++++++++++++++++++++--
 README.md |  6 ++++
 2 files changed, 91 insertions(+), 3 deletions(-)

diff --git a/Attrap.py b/Attrap.py
index e02670f..6527cac 100644
--- a/Attrap.py
+++ b/Attrap.py
@@ -41,6 +41,8 @@ logger = logging.getLogger(__name__)
 
 class Attrap:
     class RAA:
+        """La classe représentant un Recueil des actes administratifs. La plupart du temps, il s'agit d'un PDF avec plusieurs arrêtés."""
+
         url = ""
         date = datetime.datetime(1970, 1, 1)
         date_str = ""
@@ -59,11 +61,13 @@ class Attrap:
                 self.name = name
 
         def get_sha256(self):
+            """Calcule et met en cache le hash sha256 de l'URL du PDF, pour servir d'identifiant unique."""
             if (self.sha256 == ""):
                 self.sha256 = hashlib.sha256(self.url.encode('utf-8')).hexdigest()
             return self.sha256
 
         def get_pdf_dates(self, data_dir):
+            """Extrait les dates des PDF pour les ajouter à l'objet du RAA."""
             raa_data_dir = f'{data_dir}/raa/'
 
             reader = PdfReader(f'{raa_data_dir}{self.get_sha256()}.pdf')
@@ -77,6 +81,7 @@ class Attrap:
                     self.pdf_modification_date = pdf_metadata.modification_date
 
         def extract_content(self, data_dir):
+            """Extrait le contenu du PDF OCRisé pour l'écrire dans le fichier qui servira à faire la recherche de mots-clés. Supprime tous les PDF à la fin."""
             raa_data_dir = f'{data_dir}/raa/'
 
             text = ""
@@ -100,6 +105,7 @@ class Attrap:
             os.remove(f'{raa_data_dir}{self.get_sha256()}.flat.pdf')
 
         def write_properties(self, data_dir):
+            """Écris les propriétés du RAA dans un fichier JSON."""
             raa_data_dir = f'{data_dir}/raa/'
 
             pdf_creation_date_json = None
@@ -123,6 +129,7 @@ class Attrap:
             f.close()
 
         def parse_metadata(self, data_dir):
+            """Lance l'extraction des dates du PDF puis l'écriture de ses propriétés dans un fichier JSON."""
             self.get_pdf_dates(data_dir)
             self.write_properties(data_dir)
 
@@ -155,6 +162,7 @@ class Attrap:
         self.print_output(str(self.__class__.__name__))
 
     def configure_mastodon(self, access_token, instance, mastodon_prefix, mastodon_suffix):
+        """Configuration de Mastodon afin de publier un toot à chaque RAA détecté."""
         if access_token and access_token != "" and instance and instance != "":
             self.mastodon = Mastodon(
                 access_token=access_token,
@@ -164,6 +172,7 @@ class Attrap:
             self.mastodon_suffix = mastodon_suffix
 
     def mastodon_toot(self, content):
+        """Publie le toot en ajoutant le header et footer configurés."""
         if self.mastodon:
             toot = content
             if not self.mastodon_prefix == '':
@@ -173,6 +182,7 @@ class Attrap:
             self.mastodon.toot(toot)
 
     def enable_tor(self, max_requests=0):
+        """Active l'utilisation de Tor pour effectuer les requêtes."""
         proxies = {
             "http": f"socks5h://127.0.0.1:9050",
             "https": f"socks5h://127.0.0.1:9050",
@@ -184,6 +194,7 @@ class Attrap:
         self.tor_get_new_id()
 
     def disable_tor(self):
+        """Désactive l'utilisation de Tor."""
         proxies = {}
         self.tor_enabled = False
         self.tor_max_requests = 0
@@ -191,6 +202,7 @@ class Attrap:
         self.session.proxies.update(proxies)
 
     def tor_get_new_id(self):
+        """Change d'identité Tor. Cela permet de changer de noeud de sortie donc d'IP."""
         if self.tor_enabled:
             logger.info('Changement d\'identité Tor')
             try:
@@ -204,6 +216,14 @@ class Attrap:
                 logger.debug(f'Impossible de changer d\'identité Tor: {exc}')
 
     def get_sub_pages(self, page_content, element, host, recursive_until_pdf):
+        """
+        Récupère, à partir d'un chemin CSS, les sous-pages d'une page.
+
+        page_content -- Un contenu HTML à analyser
+        element -- Le chemin CSS vers l'objet renvoyant vers la sous-page recherchée
+        host -- Le nom d'hôte du site
+        recursive_until_pdf -- Un booléen pour savoir s'il faut rechercher un fichier PDF dans le chemin CSS. Le cas échéant, relance la recherche sur la sous-page si le lien n'est pas un PDF.
+        """
         soup = BeautifulSoup(page_content, 'html.parser')
         sub_pages = []
         for a in soup.select(element):
@@ -237,6 +257,15 @@ class Attrap:
         return sub_pages
 
     def get_sub_pages_with_pager(self, page, sub_page_element, pager_element, details_element, host):
+        """
+        Récupère, à partir d'un chemin CSS, les sous-pages d'une page contenant un pager.
+
+        page -- L'URL de la page à analyser
+        sub_page_element -- Le chemin CSS vers l'objet renvoyant vers la sous-page recherchée
+        pager_element -- Le chemin CSS vers le lien de page suivante du pager
+        details_element -- Le chemin CSS vers l'objet contenant les détails de la sous-page recherchée
+        host -- Le nom d'hôte du site
+        """
         pages = []
         page_content = self.get_page(page, 'get').content
 
@@ -276,6 +305,13 @@ class Attrap:
         return pages
 
     def get_raa_with_pager(self, pages_list, pager_element, host):
+        """
+        Récupère et analyse les RAA d'une page contenant un pager.
+
+        pages_list -- Un tableau contenant la liste des pages
+        pager_element -- Le chemin CSS vers le lien de page suivante du pager
+        host -- Le nom d'hôte du site
+        """
         elements = []
         # On parse chaque page passée en paramètre
         for page in pages_list:
@@ -303,9 +339,15 @@ class Attrap:
         return elements
 
     def set_sleep_time(self, sleep_time):
+        """Configure le temps de temporisation"""
         self.sleep_time = sleep_time
 
     def has_pdf(self, page_content):
+        """
+        Renvoie un booléen Vrai si la page contient un lien vers un PDF
+
+        page_content -- Un contenu HTML à analyser
+        """
         elements = []
         soup = BeautifulSoup(page_content, 'html.parser')
         for a in soup.find_all('a', href=True):
@@ -315,6 +357,13 @@ class Attrap:
 
     # On démarre le navigateur
     def get_session(self, url, wait_element, remaining_retries=0):
+        """
+        Lance un navigateur avec Selenium.
+
+        url -- URL à interroger
+        wait_element -- Élement (désigné par son identifiant CSS) qui indique que la page est chargée
+        remaining_retries -- Nombre d'échecs autorisé avant de soulever une erreur
+        """
         webdriver_options = webdriver.ChromeOptions()
         webdriver_options.add_argument("--no-sandbox")
         webdriver_options.add_argument("--disable-extensions")
@@ -369,6 +418,7 @@ class Attrap:
         return page_content
 
     def print_output(self, data):
+        """Affiche dans le terminal et dans le fichier de log un texte"""
         print(data)
         data = data.replace('\033[92m', '')
         data = data.replace('\033[0m', '')
@@ -378,6 +428,13 @@ class Attrap:
         f.close()
 
     def get_page(self, url, method, data={}):
+        """
+        Récupère le contenu HTML d'une page web
+
+        url -- L'URL de la page demandée
+        method -- 'post' ou 'get', selon le type de requête
+        data -- Un dictionnaire contenant les données à envoyer au site
+        """
         try:
             logger.debug(f'Chargement de la page {url}')
             if self.sleep_time > 0:
@@ -412,10 +469,12 @@ class Attrap:
             return self.get_page(url, method, data)
 
     def update_user_agent(self, user_agent):
+        """Change la valeur du user-agent"""
         self.user_agent = user_agent
         self.session.headers.update({'User-Agent': self.user_agent})
 
     def download_file(self, raa):
+        """Télécharge un RAA"""
         try:
             os.makedirs(
                 os.path.dirname(f'{self.data_dir}/raa/{raa.get_sha256()}.pdf'),
@@ -433,6 +492,7 @@ class Attrap:
             logger.warning(f'ATTENTION: Impossible de télécharger le fichier {raa.url}: {exc}')
 
     def ocr(self, raa, retry_on_failure=True):
+        """OCRise un RAA"""
         cmd = [
             'ocrmypdf',
             '-l', 'eng+fra',
@@ -460,7 +520,7 @@ class Attrap:
                 shutil.copy(f'{self.data_dir}/raa/{raa.get_sha256()}.pdf', f'{self.data_dir}/raa/{raa.get_sha256()}.ocr.pdf')
 
     def flatten_pdf(self, raa):
-        # OCRmyPDF ne sait pas gérer les formulaires, donc on les enlève avant OCRisation
+        """Supprime les formulaires d'un PDF pour pouvoir les OCRiser après dans OCRmyPDF."""
         reader = PdfReader(f'{self.data_dir}/raa/{raa.get_sha256()}.pdf')
         writer = PdfWriter()
 
@@ -475,6 +535,7 @@ class Attrap:
         writer.write(f'{self.data_dir}/raa/{raa.get_sha256()}.flat.pdf')
 
     def search_keywords(self, raa, keywords):
+        """Recherche des mots-clés dans le texte extrait du PDF"""
         if keywords and not keywords == '':
             text = open(f'{self.data_dir}/raa/{raa.get_sha256()}.txt').read()
 
@@ -503,6 +564,12 @@ class Attrap:
                 )
 
     def parse_raa(self, elements, keywords):
+        """
+        Démarre l'analyse des RAA.
+
+        elements -- Un tableau contenant les RAA à analyser
+        keywords -- Les mots-clés à rechercher dans chaque RAA
+        """
         self.print_output(f'Termes recherchés: {keywords}')
         self.print_output('')
 
@@ -525,6 +592,19 @@ class Attrap:
     def configure_mailer(self, smtp_host, smtp_username, smtp_password,
                          smtp_port, smtp_starttls, smtp_ssl, email_from,
                          email_to, email_object):
+        """
+        Configure les courriers électroniques.
+
+        smtp_host -- Nom d'hôte du serveur SMTP
+        smtp_username -- Nom d'utilisateur pour se connecter au serveur SMTP
+        smtp_password -- Mot de passe pour se connecter au serveur SMTP
+        smtp_port -- Port de connexion au serveur SMTP
+        smtp_starttls -- Booléen. Si vrai, se connecte avec STARTTLS
+        smtp_ssl -- Booléen. Si vrai, se connecte avec SSL/TLS
+        email_from -- Adresse électronique de l'expéditeur des notifications
+        email_to -- Tableau contenant les adresses électroniques à notifier
+        email_object -- Objet du courrier électronique de notification
+        """
         self.smtp_host = smtp_host
         self.smtp_username = smtp_username
         self.smtp_password = smtp_password
@@ -585,9 +665,11 @@ class Attrap:
             except Exception as exc:
                 logger.warning(f'Impossible d\'envoyer le courrier électronique : {exc}')
 
-    # Fonction qui essaie de deviner la date d'un RAA à partir de son nom.
-    # Utile pour limiter les requêtes lors de l'obtention des RAA à scanner.
     def guess_date(string, regex):
+        """
+        Essaie de deviner la date d'un RAA à partir de son nom.
+        Utile pour limiter les requêtes lors de l'obtention des RAA à scanner.
+        """
         try:
             search = re.search(regex, string, re.IGNORECASE)
             guessed_date = dateparser.parse(search.group(1))
diff --git a/README.md b/README.md
index 61bbb4d..17ed5d6 100644
--- a/README.md
+++ b/README.md
@@ -86,6 +86,12 @@ Les contributions à ce projet sont les bienvenues !
 
 Chaque administration est gérée par un fichier dont le nom correspond à son identifiant (`Attrap_XXX.py`). Commencez par copier un de ces fichiers puis adaptez son code à l'administration que vous voulez ajouter. Il est impératif de lancer le moins de requêtes possibles vers le site de l'administration : lorsqu'une administration a une page par année ou par mois, ne lancez une requête que vers les pages qui correspondent à la plage temporelle demandée dans la valeur de configuration `NOT_BEFORE`.
 
+Vous pouvez lancer la commande suivante pour connaître fonctions disponibles pour récupérer les RAA sur le site d'une administration :
+
+```
+bin/python -m pydoc Attrap
+```
+
 Avant d'ouvrir une merge request, assurez-vous que :
 - l'administration est activée dans `cli.py` et dans `Makefile` ;
 - il existe un job dans la CI (`.gitlab-ci.yml`) pour l'administration ;
-- 
GitLab