From 9ca86e7900be224d75aa3029a39c357d350fa3f1 Mon Sep 17 00:00:00 2001
From: Emmanuel Viennet <emmanuel.viennet@gmail.com>
Date: Tue, 23 Jul 2024 07:07:29 +0200
Subject: [PATCH] =?UTF-8?q?G=C3=A9n=C3=A9ration=20tableau=20API?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 app/api/assiduites.py   |   3 +-
 tools/create_api_map.py | 187 ++++++++++++++++++++++++++++++++++------
 2 files changed, 162 insertions(+), 28 deletions(-)

diff --git a/app/api/assiduites.py b/app/api/assiduites.py
index 8bf50805..6b75b099 100644
--- a/app/api/assiduites.py
+++ b/app/api/assiduites.py
@@ -19,7 +19,8 @@ import app.scodoc.sco_assiduites as scass
 import app.scodoc.sco_utils as scu
 from app.api import api_bp as bp
 from app.api import api_web_bp, get_model_api_object, tools
-from app.decorators import permission_required, scodoc
+from app.api import api_permission_required as permission_required
+from app.decorators import scodoc
 from app.models import (
     Assiduite,
     Evaluation,
diff --git a/tools/create_api_map.py b/tools/create_api_map.py
index 2db5996a..fd1fcdcc 100644
--- a/tools/create_api_map.py
+++ b/tools/create_api_map.py
@@ -7,6 +7,8 @@ Script permettant de générer une carte SVG de l'API de ScoDoc
 import xml.etree.ElementTree as ET
 import re
 
+from app.auth.models import Permission
+
 
 class COLORS:
     """
@@ -437,15 +439,20 @@ def _create_question_mark_group(coords, href):
     return group
 
 
+# point d'entrée de la commande `flask gen-api-map`
 def gen_api_map(app, endpoint_start="api"):
     """
     Fonction permettant de générer une carte SVG de l'API de ScoDoc
     Elle récupère les routes de l'API et les transforme en un arbre de Token
     puis génère un fichier SVG à partir de cet arbre
     """
+
+    print("DEBUG", app.view_functions["apiweb.user_info"].scodoc_permission)
     # Création du token racine
     api_map = Token("")
 
+    doctable_lines: dict[str, dict] = {}
+
     # Parcours de toutes les routes de l'application
     for rule in app.url_map.iter_rules():
         # On ne garde que les routes de l'API / APIWEB
@@ -500,6 +507,28 @@ def gen_api_map(app, endpoint_start="api"):
                 child.method = method
                 current_token.add_child(child)
 
+                # Gestion de doctable
+                doctable = parse_doctable_doc(func.__doc__ or "")
+                href = func_name.replace("_", "-")
+                if child.query and not href.endswith("-query"):
+                    href += "-query"
+
+                permissions: str
+                try:
+                    permissions: str = ", ".join(
+                        sorted(Permission.permissions_names(func.scodoc_permission))
+                    )
+                except AttributeError:
+                    permissions = "Aucune permission requise"
+
+                doctable_lines[func_name] = {
+                    "doctable": doctable,
+                    "method": method,
+                    "nom": func_name,
+                    "href": href,
+                    "permission": permissions,
+                }
+
             # On met à jour le token courant pour le prochain segment
             current_token = child
 
@@ -510,6 +539,9 @@ def gen_api_map(app, endpoint_start="api"):
         + "Vous pouvez la consulter à l'adresse suivante : /tmp/api_map.svg"
     )
 
+    # On génère le tableau à partir de doctable_lines
+    _gen_table(sorted(doctable_lines.values(), key=lambda x: x["nom"]))
+
 
 def _get_bbox(element, x_offset=0, y_offset=0):
     """
@@ -617,6 +649,7 @@ def generate_svg(element, fname):
 def _get_doc_lines(keyword, doc) -> list[str]:
     """
     Renvoie les lignes de la doc qui suivent le mot clé keyword
+    Attention : s'arrête à la première ligne vide
 
     La doc doit contenir des lignes de la forme:
 
@@ -638,10 +671,18 @@ def _get_doc_lines(keyword, doc) -> list[str]:
         return []
     # On récupère les lignes de la doc qui correspondent au keyword (enfin on espère)
     kw_lines = lines[kw_index + 2 :]
+
+    # On s'arrête à la première ligne vide
+    first_empty_line: int
+    try:
+        first_empty_line: int = kw_lines.index("")
+    except ValueError:
+        first_empty_line = len(kw_lines)
+    kw_lines = kw_lines[:first_empty_line]
     return kw_lines
 
 
-def parse_doc_name(doc):
+def parse_doc_name(doc) -> str:
     """
     renvoie le nom de la route à partir de la docstring
 
@@ -658,7 +699,7 @@ def parse_doc_name(doc):
     return name_lines[0] if name_lines else None
 
 
-def parse_query_doc(doc):
+def parse_query_doc(doc) -> dict[str, str]:
     """
     renvoie un dictionnaire {param: <type:nom_param>} (ex: {assiduite_id : <int:assiduite_id>})
 
@@ -691,33 +732,125 @@ def parse_query_doc(doc):
     return query
 
 
-if __name__ == "__main__":
-    # Exemple d'utilisation de la classe Token
-    # Exemple simple de création d'un arbre de Token
+def parse_doctable_doc(doc) -> dict[str, str]:
+    """
+    Retourne un dictionnaire représentant les informations du tableau d'api
+    à partir de la doc (DOC-TABLE)
 
-    root = Token("api")
-    child1 = Token("assiduites", leaf=True)
-    child1.func_name = "assiduites_get"
-    child2 = Token("count")
-    child22 = Token("all")
-    child23 = Token(
-        "query",
-        query={
-            "etat": "<string:etat>",
-            "moduleimpl_id": "<int:moduleimpl_id>",
-            "count": "<int:count>",
-            "formsemestre_id": "<int:formsemestre_id>",
-        },
+    éléments optionnels:
+    - `permissions` permissions nécessaires pour accéder à la route (ScoView, AbsChange, ...)
+    - `href` nom (sans #) de l'ancre dans la page ScoDoc9API
+
+    DOC-TABLE
+    ---------
+    permissions: ScoView
+    href: une-fonction
+    """
+
+    doc_lines: list[str] = _get_doc_lines("DOC-TABLE", doc)
+
+    # On crée un dictionnaire
+    table = {}
+
+    # on parcourt les lignes de la doc
+    for line in doc_lines:
+        # On sépare le paramètre et sa valeur
+        param, value = line.split(":")
+        # On met à jour le dictionnaire
+        table[param.strip()] = value.strip()
+
+    return table
+
+
+def _gen_table_line(nom, href, method, permission, doctable: dict):
+    """
+    Génère une ligne de tableau markdown
+
+    | nom de la route| methode HTTP| Permission |
+    """
+    lien: str = href
+    if "href" in doctable:
+        lien: str = doctable.get("href")
+    nav: str = f"[{nom}]({'#'+lien})"
+
+    table: str = "|"
+    for string in [nav, method, doctable.get("permissions") or permission]:
+        table += f" {string} |"
+
+    return table
+
+
+def _gen_table_head() -> str:
+    """
+    Génère la première ligne du tableau markdown
+    """
+
+    headers: str = "| Route | Méthode | Permission |"
+    line: str = "|---|---|---|"
+
+    return f"{headers}\n{line}\n"
+
+
+def _gen_table(lines: list[dict], filename: str = "/tmp/api_table.md") -> str:
+    """
+    Génère un tableau markdown à partir d'une liste de lignes
+
+    lines : liste de dictionnaire au format :
+
+    - doctable : dict généré par parse_doctable_doc
+    - nom : nom de la fonction associée à la route
+    - method : GET ou POST
+    - permission : Permissions de la route (auto récupérée)
+
+    """
+    table = _gen_table_head()
+    table += "\n".join([_gen_table_line(**line) for line in lines])
+
+    with open(filename, "w", encoding="UTF-8") as f:
+        f.write(table)
+
+    print(
+        f"Le tableau a été généré avec succès. Vous pouvez le consulter à l'adresse suivante : {filename}"
     )
-    child3 = Token("justificatifs", "POST")
-    child3.func_name = "justificatifs_post"
 
-    root.add_child(child1)
-    child1.add_child(child2)
-    child2.add_child(child22)
-    child2.add_child(child23)
-    root.add_child(child3)
 
-    group_element = root.to_svg_group()
+if __name__ == "__main__":
+    # Exemple d'utilisation de la classe Token
+    # Exemple simple de création d'un arbre de Token
 
-    generate_svg(group_element, "/tmp/api_map.svg")
+    # root = Token("api")
+    # child1 = Token("assiduites", leaf=True)
+    # child1.func_name = "assiduites_get"
+    # child2 = Token("count")
+    # child22 = Token("all")
+    # child23 = Token(
+    #     "query",
+    #     query={
+    #         "etat": "<string:etat>",
+    #         "moduleimpl_id": "<int:moduleimpl_id>",
+    #         "count": "<int:count>",
+    #         "formsemestre_id": "<int:formsemestre_id>",
+    #     },
+    # )
+    # child3 = Token("justificatifs", "POST")
+    # child3.func_name = "justificatifs_post"
+
+    # root.add_child(child1)
+    # child1.add_child(child2)
+    # child2.add_child(child22)
+    # child2.add_child(child23)
+    # root.add_child(child3)
+
+    # group_element = root.to_svg_group()
+
+    # generate_svg(group_element, "/tmp/api_map.svg")
+    dt: dict = parse_doctable_doc(parse_doctable_doc.__doc__)
+    md: str = "POST"
+    hf: str = "assiduites-query"
+
+    doc: dict = {
+        "doctable": dt,
+        "method": md,
+        "href": hf,
+    }
+    print(_gen_table([doc]))
-- 
GitLab