Skip to content

memory.gexf

memory.gexf

Minimal, safe GEXF (Graph Exchange XML Format) serializer.

A tiny, dependency-free builder used by both the memory tree exporter (:mod:memory.export) and the zettelkasten exporter (:mod:zettelkasten.export). It lives in memory/ deliberately: the memory subsystem must never import zettelkasten (the dependency is strictly one-way), so the shared helper sits on the side that both may depend on.

Safety

The builder only ever constructs an XML document with :mod:xml.etree.ElementTree — it never parses untrusted XML, so there is no XXE / external-entity / entity-expansion surface here. Text is emitted through ElementTree's own attribute/character escaping, so arbitrary note titles/tags (which are DATA, never markup) cannot break out of the document. Any caller that needs to read GEXF back should parse it with a hardened parser.

build_gexf

build_gexf(nodes: Iterable[tuple[str, str, Mapping[str, object]]], edges: Iterable[tuple[str, str, str, object]], *, node_attributes: Sequence[str] = (), directed: bool = True) -> str

Serialize nodes/edges into a GEXF 1.2 document string.

Parameters:

Name Type Description Default
nodes Iterable[tuple[str, str, Mapping[str, object]]]

iterable of (id, label, attrs) where attrs maps an attribute name (which MUST appear in node_attributes) to a value. Empty/None attribute values are skipped.

required
edges Iterable[tuple[str, str, str, object]]

iterable of (source, target, label, weight). label (e.g. a relation type) and weight (e.g. a confidence) are optional — pass ""/None to omit them from the emitted <edge>.

required
node_attributes Sequence[str]

ordered attribute names declared once in the GEXF <attributes class="node"> block; their column ids are assigned in this order.

()
directed bool

whether edges are directed (default) or undirected.

True

Returns:

Type Description
str

A UTF-8 XML document (with declaration) as a str.

Source code in memory/gexf.py
def build_gexf(
    nodes: Iterable[tuple[str, str, Mapping[str, object]]],
    edges: Iterable[tuple[str, str, str, object]],
    *,
    node_attributes: Sequence[str] = (),
    directed: bool = True,
) -> str:
    """Serialize ``nodes``/``edges`` into a GEXF 1.2 document string.

    Args:
        nodes: iterable of ``(id, label, attrs)`` where ``attrs`` maps an
            attribute name (which MUST appear in ``node_attributes``) to a value.
            Empty/``None`` attribute values are skipped.
        edges: iterable of ``(source, target, label, weight)``. ``label`` (e.g. a
            relation type) and ``weight`` (e.g. a confidence) are optional — pass
            ``""``/``None`` to omit them from the emitted ``<edge>``.
        node_attributes: ordered attribute names declared once in the GEXF
            ``<attributes class="node">`` block; their column ids are assigned in
            this order.
        directed: whether edges are directed (default) or undirected.

    Returns:
        A UTF-8 XML document (with declaration) as a ``str``.
    """
    attr_index = {name: str(i) for i, name in enumerate(node_attributes)}

    gexf = ET.Element("gexf", {"xmlns": GEXF_NS, "version": "1.2"})
    graph_el = ET.SubElement(
        gexf,
        "graph",
        {"mode": "static", "defaultedgetype": "directed" if directed else "undirected"},
    )

    if node_attributes:
        attrs_el = ET.SubElement(graph_el, "attributes", {"class": "node"})
        for name in node_attributes:
            ET.SubElement(
                attrs_el,
                "attribute",
                {"id": attr_index[name], "title": name, "type": "string"},
            )

    nodes_el = ET.SubElement(graph_el, "nodes")
    for node_id, label, attrs in nodes:
        node_el = ET.SubElement(
            nodes_el, "node", {"id": str(node_id), "label": str(label)}
        )
        pairs = [
            (name, value)
            for name, value in (attrs or {}).items()
            if name in attr_index and value not in (None, "")
        ]
        if pairs:
            av_el = ET.SubElement(node_el, "attvalues")
            for name, value in pairs:
                ET.SubElement(
                    av_el,
                    "attvalue",
                    {"for": attr_index[name], "value": str(value)},
                )

    edges_el = ET.SubElement(graph_el, "edges")
    for i, (source, target, label, weight) in enumerate(edges):
        edge_attrs = {"id": str(i), "source": str(source), "target": str(target)}
        if label:
            edge_attrs["label"] = str(label)
        if weight is not None and weight != "":
            edge_attrs["weight"] = str(weight)
        ET.SubElement(edges_el, "edge", edge_attrs)

    return ET.tostring(gexf, encoding="unicode", xml_declaration=True)