import xml.etree.ElementTree as ET
import re
class COLORS:
BLUE = "rgb(114,159,207)"
GREEN = "rgb(165,214,165)"
PINK = "rgb(230,156,190)"
GREY = "rgb(224,224,224)"
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
self.func_name = ""
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 * 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=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.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
)
href = "#" + self.func_name.replace("_", "-")
if self.query:
href += "-query"
question_mark_group = _create_question_mark_group(current_end_coords, href)
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
query_sub_element = ET.Element("g")
for key, value in self.query.items():
sub_group = ET.Element("g")
# =
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})",
)
sub_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],
)
sub_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})",
)
sub_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})",
)
sub_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})",
)
sub_group.append(ampersand_group)
query_y_offset += y_step * 1.33
query_sub_element.append(sub_group)
group.append(query_sub_element)
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)
# add `?` circle a:href to element
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)"):
# 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 _create_question_mark_group(coords, href):
x, y = coords
radius = 10 # Radius of the circle
y -= radius * 2
font_size = 17 # Font size of the question mark
group = ET.Element("g")
# Create the circle
ET.SubElement(
group,
"circle",
{
"cx": str(x),
"cy": str(y),
"r": str(radius),
"fill": COLORS.GREY,
"stroke": "black",
"stroke-width": "2",
},
)
# Create the link element
link = ET.Element("a", {"href": href})
# Create the text element
text_element = ET.SubElement(
link,
"text",
{
"x": str(x + 1),
"y": str(y + font_size / 3), # Adjust to vertically center the text
"text-anchor": "middle", # Center the text horizontally
"font-family": "Arial",
"font-size": str(font_size),
"fill": "black",
},
)
text_element.text = "?"
group.append(link)
return 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:
func = app.view_functions[rule.endpoint]
# If it's the last segment
if i == len(segments) - 1:
child = Token(
segment,
leaf=True,
query=parse_query_doc(func.__doc__ or ""),
)
else:
child = Token(
segment,
)
child.func_name = func.__name__
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")
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):
# 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"] + 80 # Add some padding
height = bbox["y_max"] - bbox["y_min"] + 80 # 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)
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
"""
lines = [line.strip() for line in doc.split("\n")]
try:
query_index = lines.index("QUERY")
if lines[query_index + 1] != "-----":
return {}
except ValueError:
return {}
query_lines = lines[query_index + 2 :]
query = {}
regex = re.compile(r"^(\w+):(<.+>)$")
for line in query_lines:
parts = regex.match(line)
if not parts:
break
param, type_nom_param = parts.groups()
query[param] = type_nom_param
return query
if __name__ == "__main__":
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")