Source code for gxformat2.cytoscape.models

"""Pydantic models for Cytoscape.js workflow visualization elements."""

from __future__ import annotations

from typing import Literal

from pydantic import BaseModel, Field


[docs] class CytoscapePosition(BaseModel): """Cytoscape node position (x/y coordinates).""" x: int = Field(default=0) y: int = Field(default=0)
[docs] class CytoscapeNodeData(BaseModel): """Data payload for a Cytoscape node (input or step).""" id: str label: str doc: str | None = Field(default=None) tool_id: str | None = Field(default=None) step_type: str = Field(default="tool") repo_link: str | None = Field(default=None)
[docs] class CytoscapeEdgeData(BaseModel): """Data payload for a Cytoscape edge (step connection).""" id: str source: str target: str input: str output: str | None = Field(default=None)
[docs] class CytoscapeNode(BaseModel): """A Cytoscape.js node element.""" group: Literal["nodes"] = "nodes" data: CytoscapeNodeData classes: list[str] = Field(default_factory=list) # Present for ``preset`` and ``topological`` layouts; omitted for hint-only # layouts (``dagre``, ``breadthfirst``, ``grid``, ``cose``, ``random``) so # the runtime renderer is responsible for placement. position: CytoscapePosition | None = Field(default=None)
[docs] class CytoscapeEdge(BaseModel): """A Cytoscape.js edge element.""" group: Literal["edges"] = "edges" data: CytoscapeEdgeData
[docs] class CytoscapeLayout(BaseModel): """Layout hint emitted on non-default layouts.""" name: str
[docs] class CytoscapeElements(BaseModel): """Complete set of Cytoscape.js elements for a workflow visualization.""" nodes: list[CytoscapeNode] = Field(default_factory=list) edges: list[CytoscapeEdge] = Field(default_factory=list) # Present only when the builder was invoked with a non-``preset`` layout. # Carried out-of-band so ``to_list()`` keeps the flat-list contract. layout: CytoscapeLayout | None = Field(default=None)
[docs] def to_list(self) -> list[dict]: """Serialize to the flat list-of-dicts format Cytoscape.js expects.""" elements: list[dict] = [] for node in self.nodes: # Drop ``position`` only when it's None (hint-only layouts). We # avoid ``exclude_none`` because it would also strip nested nulls # like ``tool_id: null``, breaking byte-parity for the default flow. if node.position is None: elements.append(node.model_dump(exclude={"position"})) else: elements.append(node.model_dump()) for edge in self.edges: elements.append(edge.model_dump()) return elements
[docs] def to_dict(self) -> dict: """Serialize as ``{"elements": [...], "layout": {...}}`` wrapper. Used by the CLI when ``--layout`` is non-default so the layout hint travels alongside the elements. """ result: dict = {"elements": self.to_list()} if self.layout is not None: result["layout"] = self.layout.model_dump() return result