From c37a92aa5ca7a0f2713664c53c617f74f4de5f25 Mon Sep 17 00:00:00 2001
From: Iziram <matthias.hartmann@iziram.fr>
Date: Thu, 20 Jun 2024 15:48:11 +0200
Subject: [PATCH] WIP : gen_api_map (no query)

---
 scodoc.py               |  11 +-
 tools/__init__.py       |   1 +
 tools/create_api_map.py | 422 ++++++++++++++++++++++++++++++++++++++++
 3 files changed, 431 insertions(+), 3 deletions(-)
 create mode 100644 tools/create_api_map.py

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():
+            # <param>=<value>
+            translate_x = x_offset + x_step
+
+            # <param>
+            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>
+            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")
-- 
GitLab