"""Writer for Archi (.archimate) XML format.
Exports pyArchimate models and views to .archimate files (Archi tool format).
Handles XML structure, folder organization, elements, relationships, and views.
"""
# ruff: noqa: N999 # legacy module name preserved for API compatibility
import os
import sys
import zipfile
from typing import cast as _cast
from lxml import etree as et
from lxml.etree import _Element
try:
from ..constants import (
ARCHI_CATEGORY as archi_category, # noqa: N811 # alias matches public API export and fallback import
)
from ..element import set_id
from ..enums import ArchiType
from ..helpers.logging import log
from ..model import Model
from ..view import View
except ImportError:
sys.path.insert(0, "..")
from pyArchimate import ArchiType, Model, View, archi_category, log, set_id # type: ignore[no-redef,attr-defined]
__mod__ = __name__.split(".")[len(__name__.split(".")) - 1]
__location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__)))
def _create_folders(root: _Element) -> dict[str, _Element]:
f_strategy = et.SubElement(root, "folder", name="Strategy", id=set_id(), type="strategy")
f_business = et.SubElement(root, "folder", name="Business", id=set_id(), type="business")
f_application = et.SubElement(root, "folder", name="Application", id=set_id(), type="application")
f_technology = et.SubElement(root, "folder", name="Technology", id=set_id(), type="technology")
f_motivation = et.SubElement(root, "folder", name="Motivation", id=set_id(), type="motivation")
f_implementation = et.SubElement(
root, "folder", name="Implementation", id=set_id(), type="implementation_migration"
)
f_other = et.SubElement(root, "folder", name="Other", id=set_id(), type="other")
f_relations = et.SubElement(root, "folder", name="Relations", id=set_id(), type="relations")
f_views = et.SubElement(root, "folder", name="Views", id=set_id(), type="diagrams")
return {
"/Strategy": f_strategy,
"/Business": f_business,
"/Application": f_application,
"/Technology": f_technology,
"/Motivation": f_motivation,
"/Implementation & Migration": f_implementation,
"/Other": f_other,
"/Physical": f_technology,
"/Technology & Physical": f_technology,
"/Relations": f_relations,
"/Views": f_views,
"/Junction": f_other,
}
def _is_same_top_folder(folder_str: str, cat: str) -> bool:
top_folder = folder_str.split("/", 2)[1] if folder_str.startswith("/") else folder_str.split("/", 1)[0]
if top_folder == cat:
return True
if cat == "Technology" and top_folder in {"Technology & Physical", "Physical"}:
return True
if cat == "Implementation & Migration" and top_folder == "Implementation":
return True
return False
def _get_folder(folders: dict[str, _Element], folder_str: str) -> _Element:
paths = folder_str.split("/")[1:]
first_folder = "/" + paths[0]
if first_folder not in folders:
log.warning(f"Unknown folder category '{first_folder}', using /Other as parent")
prev_f = folders["/Other"]
else:
prev_f = folders[first_folder]
cur_path = ""
f = None
for p in paths:
cur_path += "/" + p
f = folders[cur_path] if cur_path in folders else None
if f is None:
f = et.SubElement(prev_f, "folder", name=p, id=set_id())
folders[cur_path] = f
prev_f = f
return _cast(_Element, f)
def _resolve_folder_path(obj_folder: str | None, cat: str) -> str:
if obj_folder is None:
return "/" + cat
if _is_same_top_folder(obj_folder, cat):
return obj_folder
return "/" + cat + obj_folder
def _write_element_metadata(e: _Element, elem: object, elem_type: str) -> None:
name = getattr(elem, "name", None)
if name is not None:
e.set("name", name)
desc = getattr(elem, "desc", None)
if desc is not None:
doc = et.SubElement(e, "documentation")
doc.text = desc
for k, v in getattr(elem, "props", {}).items():
et.SubElement(e, "property", key=k, value=str(v))
if elem_type == "Junction":
junction_type = getattr(elem, "junction_type", None)
if junction_type is not None:
e.set("type", junction_type)
et.SubElement(e, "property", key="junctionType", value=junction_type)
visual_style = getattr(elem, "_visual_style", {})
for key in ["fillColor", "lineColor", "lineWidth", "transparency"]:
if key in visual_style:
et.SubElement(e, "property", key=key, value=str(visual_style[key]))
for slug in getattr(elem, "viewpoints", []):
et.SubElement(e, "property", key="viewpoint", value=slug)
profile_id = getattr(elem, "profile_id", None)
if profile_id is not None:
e.set("profiles", profile_id)
def _write_element(folders: dict[str, _Element], elem: object, xsi: et.QName) -> None:
elem_type = getattr(elem, "type", "")
cat = archi_category[elem_type].split("-")[0]
if cat == "Junction":
cat = "Other"
elif cat == "Physical":
cat = "Technology"
folder_path = _resolve_folder_path(getattr(elem, "folder", None), cat)
folder = _get_folder(folders, folder_path)
attrs = {str(xsi): "archimate:" + elem_type, "id": getattr(elem, "uuid", "")}
e = et.SubElement(folder, "element", attrs)
parent_uuid = getattr(elem, "_parent_uuid", None)
if parent_uuid:
e.set("parentId", parent_uuid)
_write_element_metadata(e, elem, elem_type)
def _write_relationship(folders: dict[str, _Element], rel: object, xsi: et.QName) -> None:
rel_folder = _resolve_folder_path(getattr(rel, "folder", None), "Relations")
folder = _get_folder(folders, rel_folder)
source = getattr(rel, "source", None)
target = getattr(rel, "target", None)
assert source is not None and target is not None
rel_type = getattr(rel, "type", "")
r = et.SubElement(
folder,
"element",
{
str(xsi): "archimate:" + rel_type + "Relationship",
"id": getattr(rel, "uuid", ""),
"source": source.uuid,
"target": target.uuid,
},
)
name = getattr(rel, "name", None)
if name is not None:
r.set("name", name)
access_type = getattr(rel, "access_type", None)
if access_type == "Read":
r.set("accessType", "1")
elif access_type == "ReadWrite":
r.set("accessType", "3")
elif access_type == "Access":
r.set("accessType", "2")
is_directed = getattr(rel, "is_directed", None)
if is_directed is not None:
r.set("directed", str(is_directed).lower())
influence_strength = getattr(rel, "influence_strength", None)
if influence_strength is not None:
r.set("strength", influence_strength)
desc = getattr(rel, "desc", None)
if desc is not None:
doc = et.SubElement(r, "documentation")
doc.text = desc
for k, v in getattr(rel, "props", {}).items():
et.SubElement(r, "property", key=k, value=str(v))
profile_id = getattr(rel, "profile_id", None)
if profile_id is not None:
r.set("profiles", profile_id)
def _write_connection(child: _Element, conn: object, xsi: et.QName) -> None: # noqa: C901
conn_source = getattr(conn, "source", None)
conn_target = getattr(conn, "target", None)
if conn_source is None or conn_target is None:
log.debug(f"Skipping connection {getattr(conn, 'uuid', '?')}: missing source or target node")
return
c = et.SubElement(
child,
"sourceConnection",
{
str(xsi): "archimate:Connection",
"id": getattr(conn, "uuid", ""),
"lineWidth": "1",
"source": conn_source.uuid,
"target": conn_target.uuid,
"archimateRelationship": getattr(conn, "ref", ""),
},
)
show_label = getattr(conn, "show_label", True)
et.SubElement(c, "feature", name="nameVisible", value="true" if show_label else "false")
line_width = getattr(conn, "line_width", None)
if line_width is not None:
c.set("lineWidth", str(line_width))
font_name = getattr(conn, "font_name", None)
font_size = getattr(conn, "font_size", None)
if font_name is not None and font_size is not None:
c.set("font", f"1|{font_name}|{int(font_size)}|0|WINDOWS|1|0|0|0|0|0|0|0|0|1|0|0|0|0|{font_name}")
font_color = getattr(conn, "font_color", None)
if font_color is not None:
c.set("fontColor", font_color.lower())
line_color = getattr(conn, "line_color", None)
if line_color is not None:
c.set("lineColor", line_color.lower())
text_position = getattr(conn, "text_position", None)
if text_position is not None:
c.set("textPosition", text_position)
for bp in getattr(conn, "bendpoints", []):
et.SubElement(
c,
"bendpoint",
startX=str(round(bp.x - conn_source.cx)),
startY=str(round(bp.y - conn_source.cy)),
endX=str(round(bp.x - conn_target.cx)),
endY=str(round(bp.y - conn_target.cy)),
)
def _set_node_visual_attrs(child: _Element, node: object) -> None:
_opacity = getattr(node, "opacity", 100)
font_name = getattr(node, "font_name", None)
font_size = getattr(node, "font_size", None)
if font_name is not None and font_size is not None:
size = str(float(font_size))
child.set("font", f"1|{font_name}|{size}|0|WINDOWS|1|0|0|0|0|0|0|0|0|1|0|0|0|0|{font_name}")
font_color = getattr(node, "font_color", None)
if font_color is not None:
child.set("fontColor", font_color.lower())
line_color = getattr(node, "line_color", None)
if line_color is not None:
child.set("lineColor", line_color.lower())
fill_color = getattr(node, "fill_color", None)
if fill_color is not None:
child.set("fillColor", fill_color.lower())
if str(_opacity) != "100":
child.set("alpha", str(int(255 * int(_opacity) / 100)))
lc_opacity = getattr(node, "lc_opacity", 100)
if str(lc_opacity) != "100":
et.SubElement(child, "feature", name="lineAlpha", value=str(int(255 * int(lc_opacity) / 100)))
image_path = getattr(node, "image_path", None)
if image_path is not None:
child.set("imagePath", image_path)
image_position = getattr(node, "image_position", None)
if image_position is not None:
child.set("imagePosition", str(image_position))
image_type = getattr(node, "image_type", None)
if image_type is not None:
child.set("type", str(image_type))
def _set_node_features(child: _Element, node: object) -> None:
label_expression = getattr(node, "label_expression", None)
if label_expression is not None:
et.SubElement(child, "feature", name="labelExpression", value=label_expression)
icon_color = getattr(node, "icon_color", None)
if icon_color is not None:
et.SubElement(child, "feature", name="iconColor", value=icon_color)
gradient = getattr(node, "gradient", None)
if gradient is not None:
et.SubElement(child, "feature", name="gradient", value=gradient)
image_source = getattr(node, "image_source", False)
if image_source:
et.SubElement(child, "feature", name="imageSource", value="1")
def _set_node_cat_content(child: _Element, node: object, xsi: et.QName) -> None:
cat = getattr(node, "cat", "Element")
node_ref = getattr(node, "ref", None)
node_label = getattr(node, "label", None)
if cat == "Element":
child.set("archimateElement", node_ref or "")
elif cat == "Container":
child.set(str(xsi), "archimate:Group")
child.set("name", node_label or "")
elif cat == "Label":
child.set(str(xsi), "archimate:Note")
content = et.SubElement(child, "content")
content.text = node_label
elif cat == "Model":
child.set(str(xsi), "archimate:DiagramModelReference")
child.set("model", node_ref or "")
text_alignment = getattr(node, "text_alignment", None)
if text_alignment is not None:
child.set("textAlignment", text_alignment)
text_position = getattr(node, "text_position", None)
if text_position is not None:
child.set("textPosition", text_position)
border_type = getattr(node, "border_type", None)
if border_type is not None:
child.set("borderType", border_type)
def _add_node(parent: object, parent_tag: _Element, node: object, xsi: et.QName) -> list[str]:
child = et.SubElement(
parent_tag,
"child",
{
str(xsi): "archimate:DiagramObject",
"id": getattr(node, "uuid", ""),
},
)
_set_node_visual_attrs(child, node)
_set_node_features(child, node)
_set_node_cat_content(child, node, xsi)
node_type = getattr(node, "type", None)
fill_color = getattr(node, "fill_color", None)
if node_type == ArchiType.Grouping and fill_color is None:
child.set("alpha", "0")
node_x = getattr(node, "x", 0)
node_y = getattr(node, "y", 0)
if isinstance(parent, View):
et.SubElement(
child,
"bounds",
x=str(node_x),
y=str(node_y),
width=str(getattr(node, "w", 120)),
height=str(getattr(node, "h", 55)),
)
else:
parent_x = getattr(parent, "x", 0)
parent_y = getattr(parent, "y", 0)
et.SubElement(
child,
"bounds",
x=str(node_x - parent_x),
y=str(node_y - parent_y),
width=str(getattr(node, "w", 120)),
height=str(getattr(node, "h", 55)),
)
for conn in getattr(node, "out_conns", lambda: [])():
_write_connection(child, conn, xsi)
targets: list[str] = [conn.uuid for conn in getattr(node, "in_conns", lambda: [])()]
for sub_n in getattr(node, "nodes", []):
_add_node(node, child, sub_n, xsi)
if targets:
# Preserve insertion order while avoiding duplicates
child.set("targetConnections", " ".join(dict.fromkeys(targets)))
return targets
def _write_view_element(view_folder: _Element, view: object, xsi: et.QName) -> None:
e = et.SubElement(
view_folder,
"element",
{
str(xsi): "archimate:ArchimateDiagramModel",
"name": getattr(view, "name", ""),
"id": getattr(view, "uuid", ""),
},
)
# Write primary viewpoint as an XML attribute on the view element
primary_vp = getattr(view, "primary_viewpoint", None)
if primary_vp is not None:
e.set("viewpoint", primary_vp)
for n in getattr(view, "nodes", []):
_add_node(view, e, n, xsi)
view_desc = getattr(view, "desc", None)
if view_desc is not None:
doc = et.SubElement(e, "documentation")
doc.text = view_desc
for k, v in getattr(view, "props", {}).items():
et.SubElement(e, "property", key=k, value=str(v))
def _write_model_metadata(root: _Element, model: Model) -> None:
root.set("name", model.name or "")
if model.desc is not None:
doc = et.SubElement(root, "purpose")
doc.text = model.desc
for k, v in model.props.items():
et.SubElement(root, "property", key=k, value=str(v))
for p in model.profiles:
et.SubElement(root, "profile", name=p.name, id=p.uuid, conceptType=p.concept)
[docs]
def archi_writer(model: Model, file_path: str) -> str:
"""
Write a Model to Archi (.archimate) XML format.
:param model: the model to write
:param file_path: output file path
"""
xml = b"""<?xml version="1.0" encoding="UTF-8"?>
<archimate:model xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:archimate="http://www.archimatetool.com/archimate" name="(new model)" id="id-2b0c639b388044d09709ceaaadbcf40f" version="4.9.0">
</archimate:model>
"""
root = et.fromstring(xml)
xsi_url = "http://www.w3.org/2001/XMLSchema-instance" # NOSONAR
xsi = et.QName(xsi_url, "type")
folders = _create_folders(root)
for elem in model.elements:
_write_element(folders, elem, xsi)
for rel in model.relationships:
_write_relationship(folders, rel, xsi)
for view in model.views:
view_folder = _get_folder(folders, _resolve_folder_path(view.folder, "Views"))
_write_view_element(view_folder, view, xsi)
_write_model_metadata(root, model)
xml_str = et.tostring(root, encoding="UTF-8", pretty_print=True)
if file_path is not None:
try:
# Check if writing to .archimate format (ZIP archive)
if file_path.endswith(".archimate"):
# Create ZIP archive with model.xml + images
with zipfile.ZipFile(file_path, "w", zipfile.ZIP_DEFLATED) as zf:
# Write XML at root of archive
zf.writestr("model.xml", xml_str)
# Write images if they exist
for img_filename, img_bytes in model._images_dict.items():
zf.writestr(img_filename, img_bytes)
else:
# Write plain XML for .xml format
with open(file_path, "wb") as fd:
fd.write(xml_str)
except OSError:
log.error(f'{__mod__}.write: Cannot write to file "{file_path}')
return xml_str.decode()