diff --git a/scodoc.py b/scodoc.py
index 1b5b8eccf..148916875 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,10 @@ def generate_ens_calendars(): # generate-ens-calendars
from tools.edt import edt_ens
edt_ens.generate_ens_calendars()
+
+
+@app.cli.command()
+@with_appcontext
+def gen_api_map():
+ """Show the API map"""
+ tools.gen_api_map(app)
diff --git a/tools/__init__.py b/tools/__init__.py
index da9214bfa..c7c13e6bd 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 000000000..5f002d3fe
--- /dev/null
+++ b/tools/create_api_map.py
@@ -0,0 +1,422 @@
+import xml.etree.ElementTree as ET
+
+
+class COLORS:
+ BLUE = "rgb(114,159,207)"
+ GREEN = "rgb(165,214,165)"
+ PINK = "rgb(230,156,190)"
+ GREY = "rgb(224,224,224)"
+ ORANGE = "rgb(253,191,111)"
+
+
+class Token:
+ 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
+
+ def add_child(self, child):
+ self.children.append(child)
+
+ def find_child(self, name):
+ for child in self.children:
+ if child.name == name:
+ return child
+ return None
+
+ def __repr__(self, level=0):
+ ret = "\t" * level + f"({self.name})\n"
+ for child in self.children:
+ ret += child.__repr__(level + 1)
+ return ret
+
+ def is_leaf(self):
+ return len(self.children) == 0
+
+ def get_height(self, y_step):
+ # 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
+
+ # La hauteur totale est la somme de la hauteur des enfants et des éléments de la query
+ return children_height + query_height + y_step
+
+ def to_svg_group(
+ self,
+ x_offset=0,
+ y_offset=0,
+ x_step=150,
+ y_step=50,
+ parent_coords=None,
+ parent_children_nb=0,
+ ):
+ group = ET.Element("g")
+ color = COLORS.BLUE
+ if self.force_leaf or self.is_leaf():
+ if self.method == "GET":
+ color = COLORS.GREEN
+ elif self.method == "POST":
+ color = COLORS.PINK
+
+ element = _create_svg_element(self.name, color)
+ element.set("transform", f"translate({x_offset}, {y_offset})")
+ current_start_coords, current_end_coords = _get_anchor_coords(
+ element, x_offset, y_offset
+ )
+ group.append(element)
+
+ # Add an arrow from the parent element to the current element
+ if parent_coords and parent_children_nb > 1:
+ arrow = _create_arrow(parent_coords, current_start_coords)
+ group.append(arrow)
+
+ 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)
+ 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)
+ current_end_coords = _get_anchor_coords(group, 0, 0)[1]
+
+ query_y_offset = y_offset
+ ampersand_start_coords = None
+ ampersand_end_coords = None
+ for key, value in self.query.items():
+ # =
+ translate_x = x_offset + x_step
+
+ #
+ param_el = _create_svg_element(key, COLORS.BLUE)
+ param_el.set(
+ "transform",
+ f"translate({translate_x}, {query_y_offset})",
+ )
+ group.append(param_el)
+
+ # add Arrow from "query" to element
+ coords = (
+ current_end_coords,
+ _get_anchor_coords(param_el, translate_x, query_y_offset)[0],
+ )
+ group.append(_create_arrow(*coords))
+
+ # =
+ equal_el = _create_svg_element("=", COLORS.GREY)
+ translate_x = x_offset + x_step + _get_element_width(param_el)
+ equal_el.set(
+ "transform",
+ f"translate({translate_x}, {query_y_offset})",
+ )
+ group.append(equal_el)
+
+ #
+ value_el = _create_svg_element(value, COLORS.GREEN)
+ 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})",
+ )
+ group.append(value_el)
+ if len(self.query) == 1:
+ continue
+ ampersand_group = _create_svg_element("&", "rgb(224,224,224)")
+ 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})",
+ )
+ group.append(ampersand_group)
+
+ # Track the start and end coordinates of the ampersands
+ if ampersand_start_coords is None:
+ ampersand_start_coords = _get_anchor_coords(
+ ampersand_group, translate_x, query_y_offset
+ )[1]
+ ampersand_end_coords = _get_anchor_coords(
+ ampersand_group, translate_x, query_y_offset
+ )[1]
+ # Draw line connecting all ampersands
+ if ampersand_start_coords and ampersand_end_coords and len(self.query) > 1:
+ line = _create_line(ampersand_start_coords, ampersand_end_coords)
+ group.append(line)
+
+ query_y_offset += y_step
+
+ y_offset = query_y_offset
+
+ current_y_offset = y_offset
+
+ for child in self.children:
+ rel_x_offset = x_offset + _get_group_width(group)
+ if len(self.children) > 1:
+ rel_x_offset += x_step
+ 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),
+ )
+ group.append(child_group)
+ current_y_offset += child.get_height(y_step)
+
+ return group
+
+
+def _create_line(start_coords, end_coords):
+ start_x, start_y = start_coords
+ end_x, end_y = end_coords
+
+ path_data = f"M {start_x},{start_y} L {start_x + 20},{start_y} L {start_x + 20},{end_y} L {end_x},{end_y}"
+
+ path = ET.Element(
+ "path",
+ {
+ "d": path_data,
+ "style": "stroke:black;stroke-width:2;fill:none",
+ },
+ )
+ return path
+
+
+def _create_svg_element(text, color="rgb(230,156,190)"):
+ # Dimensions and styling
+ padding = 5
+ font_size = 16
+ rect_height = 30
+ rect_x = 10
+ rect_y = 20
+
+ # Estimate the text width
+ text_width = (
+ len(text) * font_size * 0.6
+ ) # Estimate based on average character width
+ rect_width = text_width + padding * 2
+
+ # Create the SVG group element
+ group = ET.Element("g")
+
+ # Create the 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",
+ },
+ )
+
+ # Create the text element
+ text_element = ET.SubElement(
+ group,
+ "text",
+ {
+ "x": str(rect_x + padding),
+ "y": str(
+ rect_y + rect_height / 2 + font_size / 2.5
+ ), # Adjust to vertically center the text
+ "font-family": "Courier New, monospace",
+ "font-size": str(font_size),
+ "fill": "black",
+ "style": "white-space: pre;",
+ },
+ )
+ text_element.text = text
+
+ return group
+
+
+def _get_anchor_coords(element, x_offset, y_offset):
+ bbox = _get_bbox(element, x_offset, y_offset)
+ startX = bbox["x_min"]
+ endX = bbox["x_max"]
+ y = bbox["y_min"] + (bbox["y_max"] - bbox["y_min"]) / 2
+ return (startX, y), (endX, y)
+
+
+def _create_arrow(start_coords, end_coords):
+ start_x, start_y = start_coords
+ end_x, end_y = end_coords
+ mid_x = (start_x + end_x) / 2
+
+ path_data = (
+ f"M {start_x},{start_y} L {mid_x},{start_y} L {mid_x},{end_y} L {end_x},{end_y}"
+ )
+
+ path = ET.Element(
+ "path",
+ {
+ "d": path_data,
+ "style": "stroke:black;stroke-width:2;fill:none",
+ "marker-end": "url(#arrowhead)",
+ },
+ )
+ return path
+
+
+def _get_element_width(element):
+ rect = element.find("rect")
+ if rect is not None:
+ return float(rect.get("width", 0))
+ return 0
+
+
+def _get_group_width(group):
+ return sum(_get_element_width(child) for child in group)
+
+
+def gen_api_map(app):
+ api_map = Token("")
+ for rule in app.url_map.iter_rules():
+ # On ne garde que les routes de l'API / APIWEB
+ if not rule.endpoint.lower().startswith("api"):
+ continue
+
+ segments = rule.rule.strip("/").split("/")
+ current_token = api_map
+
+ for i, segment in enumerate(segments):
+ # Check if the segment already exists in the current level
+ child = current_token.find_child(segment)
+ if child is None:
+ # If it's the last segment and it has query parameters
+ if i == len(segments) - 1 and segment == "query":
+ child = Token(
+ segment,
+ leaf=True,
+ )
+ # TODO Parse QUERY doc
+ else:
+ child = Token(segment)
+ method: str = "POST" if "POST" in rule.methods else "GET"
+ child.method = method
+ current_token.add_child(child)
+ current_token = child
+
+ # Mark the last segment as a leaf node if it's not already marked
+ if not current_token.is_leaf():
+ current_token.force_leaf = True
+
+ generate_svg(api_map.to_svg_group(), "/tmp/api_map.svg")
+
+
+def _get_bbox(element, x_offset=0, y_offset=0):
+ # Helper function to calculate the bounding box of an SVG element
+ bbox = {
+ "x_min": float("inf"),
+ "y_min": float("inf"),
+ "x_max": float("-inf"),
+ "y_max": float("-inf"),
+ }
+
+ for child in element:
+ transform = child.get("transform")
+ child_x_offset = x_offset
+ child_y_offset = y_offset
+
+ if transform:
+ translate = transform.replace("translate(", "").replace(")", "").split(",")
+ if len(translate) == 2:
+ child_x_offset += float(translate[0])
+ child_y_offset += float(translate[1])
+
+ 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):
+ bbox = _get_bbox(element)
+ width = bbox["x_max"] - bbox["x_min"] + 20 # Add some padding
+ height = bbox["y_max"] - bbox["y_min"] + 20 # Add some padding
+
+ 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}",
+ },
+ )
+
+ # Define the marker for the arrowhead
+ 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"})
+
+ svg.append(element)
+
+ tree = ET.ElementTree(svg)
+ tree.write(fname, encoding="utf-8", xml_declaration=True)
+
+
+if __name__ == "__main__":
+ root = Token("api")
+ child1 = Token("assiduites", leaf=True)
+ child2 = Token("count")
+ child22 = Token("all")
+ child23 = Token(
+ "query", query={"param1": "value1", "param2": "value2", "param3": "value3"}
+ )
+ child3 = Token("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")