diff --git a/scodoc.py b/scodoc.py index 1b5b8ecc..14891687 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 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..5f002d3f --- /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")