diff --git a/app/api/assiduites.py b/app/api/assiduites.py index 3f3aee5d..7d46ddfe 100644 --- a/app/api/assiduites.py +++ b/app/api/assiduites.py @@ -161,8 +161,17 @@ def count_assiduites( query?est_just=f query?est_just=t - - + QUERY + ----- + user_id: + est_just: + moduleimpl_id: + date_debut: + date_fin: + etat: + formsemestre_id: + metric: + split: """ @@ -253,6 +262,15 @@ def assiduites(etudid: int = None, nip=None, ine=None, with_query: bool = False) query?est_just=f query?est_just=t + QUERY + ----- + user_id: + est_just: + moduleimpl_id: + date_debut: + date_fin: + etat: + formsemestre_id: """ @@ -329,6 +347,16 @@ def assiduites_group(with_query: bool = False): query?est_just=f query?est_just=t + QUERY + ----- + user_id: + est_just: + moduleimpl_id: + date_debut: + date_fin: + etat: + etudids: """ @@ -388,7 +416,16 @@ def assiduites_group(with_query: bool = False): @as_json @permission_required(Permission.ScoView) def assiduites_formsemestre(formsemestre_id: int, with_query: bool = False): - """Retourne toutes les assiduités du formsemestre""" + """Retourne toutes les assiduités du formsemestre + QUERY + ----- + user_id: + est_just: + moduleimpl_id: + date_debut: + date_fin: + etat: + """ # Récupération du formsemestre à partir du formsemestre_id formsemestre: FormSemestre = None @@ -438,7 +475,20 @@ def assiduites_formsemestre(formsemestre_id: int, with_query: bool = False): def count_assiduites_formsemestre( formsemestre_id: int = None, with_query: bool = False ): - """Comptage des assiduités du formsemestre""" + """Comptage des assiduités du formsemestre + + QUERY + ----- + user_id: + est_just: + moduleimpl_id: + date_debut: + date_fin: + etat: + formsemestre_id: + metric: + split: + """ # Récupération du formsemestre à partir du formsemestre_id formsemestre: FormSemestre = None diff --git a/app/api/justificatifs.py b/app/api/justificatifs.py index b3528362..ad77f54c 100644 --- a/app/api/justificatifs.py +++ b/app/api/justificatifs.py @@ -3,8 +3,8 @@ # Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # See LICENSE ############################################################################## -"""ScoDoc 9 API : Justificatifs -""" +"""ScoDoc 9 API : Justificatifs""" + from datetime import datetime from flask_json import as_json @@ -113,6 +113,16 @@ def justificatifs(etudid: int = None, nip=None, ine=None, with_query: bool = Fal user_id (l'id de l'auteur du justificatif) query?user_id=[int] ex query?user_id=3 + QUERY + ----- + user_id: + est_just: + date_debut: + date_fin: + etat: + order: + courant: + group_id: """ # Récupération de l'étudiant etud: Identite = tools.get_etud(etudid, nip, ine) @@ -154,6 +164,17 @@ def justificatifs_dept(dept_id: int = None, with_query: bool = False): """ Renvoie tous les justificatifs d'un département (en ajoutant un champ "formsemestre" si possible) + + QUERY + ----- + user_id: + est_just: + date_debut: + date_fin: + etat: + order: + courant: + group_id: """ # Récupération du département et des étudiants du département @@ -225,7 +246,19 @@ def _set_sems(justi: Justificatif, restrict: bool) -> dict: @as_json @permission_required(Permission.ScoView) def justificatifs_formsemestre(formsemestre_id: int, with_query: bool = False): - """Retourne tous les justificatifs du formsemestre""" + """Retourne tous les justificatifs du formsemestre + + QUERY + ----- + user_id: + est_just: + date_debut: + date_fin: + etat: + order: + courant: + group_id: + """ # Récupération du formsemestre formsemestre: FormSemestre = None diff --git a/scodoc.py b/scodoc.py index 1b5b8ecc..2e2f06d8 100755 --- a/scodoc.py +++ b/scodoc.py @@ -1,10 +1,8 @@ # -*- coding: UTF-8 -*- -"""Application Flask: ScoDoc +"""Application Flask: ScoDoc""" - -""" import datetime from pprint import pprint as pp import re @@ -723,3 +721,16 @@ def generate_ens_calendars(): # generate-ens-calendars from tools.edt import edt_ens edt_ens.generate_ens_calendars() + + +@app.cli.command() +@click.option( + "-e", + "--endpoint", + default="api", + help="Endpoint à partir duquel générer la carte des routes", +) +@with_appcontext +def gen_api_map(endpoint): + """Génère la carte des routes de l'API.""" + tools.gen_api_map(app, endpoint_start=endpoint) diff --git a/tools/__init__.py b/tools/__init__.py index da9214bf..c7c13e6b 100644 --- a/tools/__init__.py +++ b/tools/__init__.py @@ -10,3 +10,4 @@ from tools.migrate_scodoc7_archives import migrate_scodoc7_dept_archives from tools.migrate_scodoc7_logos import migrate_scodoc7_dept_logos from tools.migrate_abs_to_assiduites import migrate_abs_to_assiduites from tools.downgrade_assiduites import downgrade_module +from tools.create_api_map import gen_api_map diff --git a/tools/create_api_map.py b/tools/create_api_map.py new file mode 100644 index 00000000..953f384a --- /dev/null +++ b/tools/create_api_map.py @@ -0,0 +1,691 @@ +""" +Script permettant de générer une carte SVG de l'API de ScoDoc + +Écrit par Matthias HARTMANN +""" + +import xml.etree.ElementTree as ET +import re + + +class COLORS: + """ + Couleurs utilisées pour les éléments de la carte + """ + + BLUE = "rgb(114,159,207)" # Couleur de base / élément simple + GREEN = "rgb(165,214,165)" # Couleur route GET / valeur query + PINK = "rgb(230,156,190)" # Couleur route POST + GREY = "rgb(224,224,224)" # Couleur séparateur + + +class Token: + """ + Classe permettant de représenter un élément de l'API + Exemple : + + /ScoDoc/api/test + + Token(ScoDoc)-> Token(api) -> Token(test) + + Chaque token peut avoir des enfants (Token) et des paramètres de query + Chaque token dispose + d'un nom (texte écrit dans le rectangle), + d'une méthode (GET ou POST) par défaut GET, + d'une func_name (nom de la fonction associée à ce token) + [OPTIONNEL] d'une query (dictionnaire {param: }) + + Un token est une leaf si il n'a pas d'enfants. + Une LEAF possède un `?` renvoyant vers la doc de la route + + Il est possible de forcer un token à être une pseudo LEAF en mettant force_leaf=True + Une PSEUDO LEAF possède aussi un `?` renvoyant vers la doc de la route + tout en ayant des enfants. + """ + + def __init__(self, name, method="GET", query=None, leaf=False): + self.children: list["Token"] = [] + self.name: str = name + self.method: str = method + self.query: dict[str, str] = query or {} + self.force_leaf: bool = leaf + self.func_name = "" + + def add_child(self, child): + """ + Ajoute un enfant à ce token + """ + self.children.append(child) + + def find_child(self, name): + """ + Renvoie l'enfant portant le nom `name` ou None si aucun enfant ne correspond + """ + for child in self.children: + if child.name == name: + return child + return None + + def __repr__(self, level=0): + """ + représentation textuelle simplifiée de l'arbre + (ne prend pas en compte les query, les méthodes, les func_name, ...) + """ + ret = "\t" * level + f"({self.name})\n" + for child in self.children: + ret += child.__repr__(level + 1) + return ret + + def is_leaf(self): + """ + Renvoie True si le token est une leaf, False sinon + (i.e. s'il n'a pas d'enfants) + (force_leaf n'est pas pris en compte ici) + """ + return len(self.children) == 0 + + def get_height(self, y_step): + """ + Renvoie la hauteur de l'élément en prenant en compte la hauteur de ses enfants + """ + # Calculer la hauteur totale des enfants + children_height = sum(child.get_height(y_step) for child in self.children) + + # Calculer la hauteur des éléments de la query + query_height = len(self.query) * (y_step * 1.33) + + # La hauteur totale est la somme de la hauteur des enfants et des éléments de la query + height = children_height + query_height + if height == 0: + height = y_step + return height + + def to_svg_group( + self, + x_offset: int = 0, + y_offset: int = 0, + x_step: int = 150, + y_step: int = 50, + parent_coords: tuple[tuple[int, int], tuple[int, int]] = None, + parent_children_nb: int = 0, + ): + """ + Transforme un token en un groupe SVG + (récursif, appelle la fonction sur ses enfants) + """ + + group = ET.Element("g") # groupe principal + color = COLORS.BLUE + if self.is_leaf(): + if self.method == "GET": + color = COLORS.GREEN + elif self.method == "POST": + color = COLORS.PINK + + # Création du rectangle avec le nom du token et placement sur la carte + element = _create_svg_element(self.name, color) + element.set("transform", f"translate({x_offset}, {y_offset})") + group.append(element) + + # On récupère les coordonnées de début et de fin de l'élément pour les flèches + current_start_coords, current_end_coords = _get_anchor_coords( + element, x_offset, y_offset + ) + # Préparation du lien vers la doc de la route + href = "#" + self.func_name.replace("_", "-") + if self.query: + href += "-query" + question_mark_group = _create_question_mark_group(current_end_coords, href) + + # Ajout de la flèche partant du parent jusqu'à l'élément courant + if parent_coords and parent_children_nb > 1: + arrow = _create_arrow(parent_coords, current_start_coords) + group.append(arrow) + + # Ajout du `/` si le token n'est pas une leaf (ne prend pas en compte force_leaf) + if not self.is_leaf(): + slash_group = _create_svg_element("/", COLORS.GREY) + slash_group.set( + "transform", + f"translate({x_offset + _get_element_width(element)}, {y_offset})", + ) + group.append(slash_group) + # Ajout du `?` si le token est une leaf et possède une query + if self.is_leaf() and self.query: + slash_group = _create_svg_element("?", COLORS.GREY) + slash_group.set( + "transform", + f"translate({x_offset + _get_element_width(element)}, {y_offset})", + ) + group.append(slash_group) + + # Actualisation des coordonnées de fin + current_end_coords = _get_anchor_coords(group, 0, 0)[1] + + # Gestion des éléments de la query + # Pour chaque élément on va créer : + # (param) (=) (valeur) (&) + query_y_offset = y_offset + query_sub_element = ET.Element("g") + for key, value in self.query.items(): + # Création d'un sous-groupe pour chaque élément de la query + sub_group = ET.Element("g") + + # On décale l'élément de la query vers la droite par rapport à l'élément parent + translate_x = x_offset + x_step + + # création élément (param) + param_el = _create_svg_element(key, COLORS.BLUE) + param_el.set( + "transform", + f"translate({translate_x}, {query_y_offset})", + ) + sub_group.append(param_el) + + # Ajout d'une flèche partant de l'élément "query" vers le paramètre courant + coords = ( + current_end_coords, + _get_anchor_coords(param_el, translate_x, query_y_offset)[0], + ) + sub_group.append(_create_arrow(*coords)) + + # création élément (=) + equal_el = _create_svg_element("=", COLORS.GREY) + # On met à jour le décalage en fonction de l'élément précédent + translate_x = x_offset + x_step + _get_element_width(param_el) + equal_el.set( + "transform", + f"translate({translate_x}, {query_y_offset})", + ) + sub_group.append(equal_el) + + # création élément (value) + value_el = _create_svg_element(value, COLORS.GREEN) + # On met à jour le décalage en fonction des éléments précédents + translate_x = ( + x_offset + + x_step + + sum(_get_element_width(el) for el in [param_el, equal_el]) + ) + value_el.set( + "transform", + f"translate({translate_x}, {query_y_offset})", + ) + sub_group.append(value_el) + # Si il y a qu'un seul élément dans la query, on ne met pas de `&` + if len(self.query) == 1: + continue + + # création élément (&) + ampersand_group = _create_svg_element("&", "rgb(224,224,224)") + # On met à jour le décalage en fonction des éléments précédents + translate_x = ( + x_offset + + x_step + + sum(_get_element_width(el) for el in [param_el, equal_el, value_el]) + ) + ampersand_group.set( + "transform", + f"translate({translate_x}, {query_y_offset})", + ) + sub_group.append(ampersand_group) + + # On décale le prochain élément de la query vers le bas + query_y_offset += y_step * 1.33 + # On ajoute le sous-groupe (param = value &) au groupe de la query + query_sub_element.append(sub_group) + + # On ajoute le groupe de la query à l'élément principal + group.append(query_sub_element) + + # Gestion des enfants du Token + + # On met à jour les décalages en fonction des éléments précédents + y_offset = query_y_offset + current_y_offset = y_offset + + # Pour chaque enfant, on crée un groupe SVG de façon récursive + for child in self.children: + # On décale l'enfant vers la droite par rapport à l'élément parent + # Si il n'y a qu'un enfant, alors on colle l'enfant à l'élément parent + rel_x_offset = x_offset + _get_group_width(group) + if len(self.children) > 1: + rel_x_offset += x_step + + # On crée le groupe SVG de l'enfant + child_group = child.to_svg_group( + rel_x_offset, + current_y_offset, + x_step, + y_step, + parent_coords=current_end_coords, + parent_children_nb=len(self.children), + ) + # On ajoute le groupe de l'enfant au groupe principal + group.append(child_group) + # On met à jour le décalage Y en fonction de la hauteur de l'enfant + current_y_offset += child.get_height(y_step) + + # Ajout du `?` si le token est une pseudo leaf ou une leaf + if self.force_leaf or self.is_leaf(): + group.append(question_mark_group) + + return group + + +def _create_svg_element(text, color="rgb(230,156,190)"): + """ + Fonction générale pour créer un élément SVG simple + (rectangle avec du texte à l'intérieur) + + text : texte à afficher dans l'élément + color : couleur de l'élément + """ + + # Paramètres de style de l'élément + padding = 5 + font_size = 16 + rect_height = 30 + rect_x = 10 + rect_y = 20 + + # Estimation de la largeur du texte + text_width = ( + len(text) * font_size * 0.6 + ) # On suppose que la largeur d'un caractère est 0.6 * font_size + # Largeur du rectangle = Largeur du texte + padding à gauche et à droite + rect_width = text_width + padding * 2 + + # Création du groupe SVG + group = ET.Element("g") + + # Création du rectangle + ET.SubElement( + group, + "rect", + { + "x": str(rect_x), + "y": str(rect_y), + "width": str(rect_width), + "height": str(rect_height), + "style": f"fill:{color};stroke:black;stroke-width:2;fill-opacity:1;stroke-opacity:1", + }, + ) + + # Création du texte + text_element = ET.SubElement( + group, + "text", + { + "x": str(rect_x + padding), + "y": str( + rect_y + rect_height / 2 + font_size / 2.5 + ), # Ajustement pour centrer verticalement + "font-family": "Courier New, monospace", + "font-size": str(font_size), + "fill": "black", + "style": "white-space: pre;", + }, + ) + + # Ajout du texte à l'élément + text_element.text = text + + return group + + +def _get_anchor_coords(element, x_offset, y_offset): + """ + Récupération des coordonnées des points d'ancrage d'un élément SVG + (début et fin de l'élément pour les flèches) + (le milieu vertical de l'élément est utilisé pour les flèches) + """ + bbox = _get_bbox(element, x_offset, y_offset) + startX = bbox["x_min"] + endX = bbox["x_max"] + # Milieu vertical de l'élément + y = bbox["y_min"] + (bbox["y_max"] - bbox["y_min"]) / 2 + return (startX, y), (endX, y) + + +def _create_arrow(start_coords, end_coords): + """ + Création d'une flèche entre deux points + """ + # On récupère les coordonnées de début et de fin de la flèche + start_x, start_y = start_coords + end_x, end_y = end_coords + # On calcule le milieu horizontal de la flèche + mid_x = (start_x + end_x) / 2 + + # On crée le chemin de la flèche + path_data = ( + f"M {start_x},{start_y} L {mid_x},{start_y} L {mid_x},{end_y} L {end_x},{end_y}" + ) + # On crée l'élément path de la flèche + path = ET.Element( + "path", + { + "d": path_data, + "style": "stroke:black;stroke-width:2;fill:none", + "marker-end": "url(#arrowhead)", # Ajout de la flèche à la fin du path + }, + ) + return path + + +def _get_element_width(element): + """ + Retourne la largueur d'un élément simple + L'élément simple correspond à un rectangle avec du texte à l'intérieur + on récupère la largueur du rectangle + """ + rect = element.find("rect") + if rect is not None: + return float(rect.get("width", 0)) + return 0 + + +def _get_group_width(group): + """ + Récupère la largeur d'un groupe d'éléments + on fait la somme des largeurs de chaque élément du groupe + """ + return sum(_get_element_width(child) for child in group) + + +def _create_question_mark_group(coords, href): + """ + Création d'un groupe SVG contenant un cercle et un lien vers la doc de la route + le `?` renvoie vers la doc de la route + """ + # Récupération du point d'ancrage de l'élément + x, y = coords + radius = 10 # Rayon du cercle + y -= radius * 2 + font_size = 17 # Taille de la police + + group = ET.Element("g") + + # Création du cercle + ET.SubElement( + group, + "circle", + { + "cx": str(x), + "cy": str(y), + "r": str(radius), + "fill": COLORS.GREY, + "stroke": "black", + "stroke-width": "2", + }, + ) + + # Création du lien (a) vers la doc de la route + link = ET.Element("a", {"href": href}) + + # Création du texte `?` + text_element = ET.SubElement( + link, + "text", + { + "x": str(x + 1), + "y": str(y + font_size / 3), # Ajustement pour centrer verticalement + "text-anchor": "middle", # Centrage horizontal + "font-family": "Arial", + "font-size": str(font_size), + "fill": "black", + }, + ) + text_element.text = "?" + + group.append(link) + + return group + + +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 + """ + # Création du token racine + api_map = Token("") + + # 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 + if not rule.endpoint.lower().startswith(endpoint_start.lower()): + continue + + # Transformation de la route en segments + # ex : /ScoDoc/api/test -> ["ScoDoc", "api", "test"] + segments = rule.rule.strip("/").split("/") + + # On positionne le token courant sur le token racine + current_token = api_map + + # Pour chaque segment de la route + for i, segment in enumerate(segments): + # On cherche si le segment est déjà un enfant du token courant + child = current_token.find_child(segment) + + # Si ce n'est pas le cas on crée un nouveau token et on l'ajoute comme enfant + if child is None: + func = app.view_functions[rule.endpoint] + # Si c'est le dernier segment, on marque le token comme une leaf + # On utilise force_leaf car il est possible que le token ne soit que + # momentanément une leaf + # ex : + # - /ScoDoc/api/test/ -> ["ScoDoc", "api", "test"] + # - /ScoDoc/api/test/1 -> ["ScoDoc", "api", "test", "1"] + # dans le premier cas test est une leaf, dans le deuxième cas test n'est pas une leaf + # force_leaf permet de forcer le token à être une leaf même s'il a des enfants + # permettant d'afficher le `?` renvoyant vers la doc de la route + # car la route peut être utilisée sans forcément la continuer. + + if i == len(segments) - 1: + # Un Token sera query si parse_query_doc retourne un dictionnaire non vide + child = Token( + segment, + leaf=True, + query=parse_query_doc(func.__doc__ or ""), + ) + else: + child = Token( + segment, + ) + + # On ajoute le token comme enfant du token courant + # en donnant la méthode et le nom de la fonction associée + child.func_name = func.__name__ + method: str = "POST" if "POST" in rule.methods else "GET" + child.method = method + current_token.add_child(child) + + # On met à jour le token courant pour le prochain segment + current_token = child + + # On génère le SVG à partir de l'arbre de Token + generate_svg(api_map.to_svg_group(), "/tmp/api_map.svg") + print( + "La carte a été générée avec succès. " + + "Vous pouvez la consulter à l'adresse suivante : /tmp/api_map.svg" + ) + + +def _get_bbox(element, x_offset=0, y_offset=0): + """ + Récupérer les coordonnées de la boîte englobante d'un élément SVG + Utilisé pour calculer les coordonnées d'un élément SVG et pour avoir la taille + total du SVG + """ + # Initialisation des coordonnées de la boîte englobante + bbox = { + "x_min": float("inf"), + "y_min": float("inf"), + "x_max": float("-inf"), + "y_max": float("-inf"), + } + + # Parcours récursif des enfants de l'élément + for child in element: + # On récupère la transformation (position) de l'enfant + # On met les OffSet par défaut à leur valeur donnée en paramètre + transform = child.get("transform") + child_x_offset = x_offset + child_y_offset = y_offset + + # Si la transformation est définie, on récupère les coordonnées de translation + # et on les ajoute aux offsets + if transform: + translate = transform.replace("translate(", "").replace(")", "").split(",") + if len(translate) == 2: + child_x_offset += float(translate[0]) + child_y_offset += float(translate[1]) + + # On regarde ensuite la boite englobante de l'enfant + # On met à jour les coordonnées de la boîte englobante en fonction de l'enfant + # x_min, y_min, x_max, y_max. + + if child.tag == "rect": + x = child_x_offset + float(child.get("x", 0)) + y = child_y_offset + float(child.get("y", 0)) + width = float(child.get("width", 0)) + height = float(child.get("height", 0)) + bbox["x_min"] = min(bbox["x_min"], x) + bbox["y_min"] = min(bbox["y_min"], y) + bbox["x_max"] = max(bbox["x_max"], x + width) + bbox["y_max"] = max(bbox["y_max"], y + height) + + if len(child): + child_bbox = _get_bbox(child, child_x_offset, child_y_offset) + bbox["x_min"] = min(bbox["x_min"], child_bbox["x_min"]) + bbox["y_min"] = min(bbox["y_min"], child_bbox["y_min"]) + bbox["x_max"] = max(bbox["x_max"], child_bbox["x_max"]) + bbox["y_max"] = max(bbox["y_max"], child_bbox["y_max"]) + + return bbox + + +def generate_svg(element, fname): + """ + Génère un fichier SVG à partir d'un élément SVG + """ + # On récupère les dimensions de l'élément racine + bbox = _get_bbox(element) + # On définit la taille du SVG en fonction des dimensions de l'élément racine + width = bbox["x_max"] - bbox["x_min"] + 80 + height = bbox["y_max"] - bbox["y_min"] + 80 + + # Création de l'élément racine du SVG + svg = ET.Element( + "svg", + { + "width": str(width), + "height": str(height), + "xmlns": "http://www.w3.org/2000/svg", + "viewBox": f"{bbox['x_min'] - 10} {bbox['y_min'] - 10} {width} {height}", + }, + ) + + # Création du motif de la flèche pour les liens + # (définition d'un marqueur pour les flèches) + defs = ET.SubElement(svg, "defs") + marker = ET.SubElement( + defs, + "marker", + { + "id": "arrowhead", + "markerWidth": "10", + "markerHeight": "7", + "refX": "10", + "refY": "3.5", + "orient": "auto", + }, + ) + ET.SubElement(marker, "polygon", {"points": "0 0, 10 3.5, 0 7"}) + + # Ajout de l'élément principal à l'élément racine + svg.append(element) + + # Écriture du fichier SVG + tree = ET.ElementTree(svg) + tree.write(fname, encoding="utf-8", xml_declaration=True) + + +def parse_query_doc(doc): + """ + renvoie un dictionnaire {param: } (ex: {assiduite_id : }) + + La doc doit contenir des lignes de la forme: + + QUERY + ----- + param: + param1: + param2: + + Dès qu'une ligne ne respecte pas ce format (voir regex dans la fonction), on arrête de parser + Attention, la ligne ----- doit être collée contre QUERY et contre le premier paramètre + """ + # Récupérer les lignes de la doc + lines = [line.strip() for line in doc.split("\n")] + # On cherche la ligne "QUERY" et on vérifie que la ligne suivante est "-----" + # Si ce n'est pas le cas, on renvoie un dictionnaire vide + try: + query_index = lines.index("QUERY") + if lines[query_index + 1] != "-----": + return {} + except ValueError: + return {} + # On récupère les lignes de la doc qui correspondent à la query (enfin on espère) + query_lines = lines[query_index + 2 :] + + query = {} + regex = re.compile(r"^(\w+):(<.+>)$") + for line in query_lines: + # On verifie que la ligne respecte le format attendu + # Si ce n'est pas le cas, on arrête de parser + parts = regex.match(line) + if not parts: + break + # On récupère le paramètre et son type:nom + param, type_nom_param = parts.groups() + # On ajoute le paramètre au dictionnaire + query[param] = type_nom_param + + return query + + +if __name__ == "__main__": + # Exemple d'utilisation de la classe Token + # Exemple simple de création d'un arbre de Token + + root = Token("api") + child1 = Token("assiduites", leaf=True) + child1.func_name = "assiduites_get" + child2 = Token("count") + child22 = Token("all") + child23 = Token( + "query", + query={ + "etat": "", + "moduleimpl_id": "", + "count": "", + "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")