Skip to content

History and network states

History is implemented directly by Network; there is no separate NetworkHistory class. Mutating decorated methods save deep node/edge snapshots as NetworkState objects. Undo, redo, checkout, and subsequent mutations form a branching tree.

For structured topology explanations, provenance, pruning gaps, and JSON export/recovery, see Interaction curation.

net.connect_nodes()
states = net.list_states()
net.undo()
net.redo()
html = net.history_html()

For routine inspection, avoid materializing complete history:

status = net.history_status()
count = net.state_count
page = net.list_states(offset=0, limit=25, detail="summary")
details = net.get_state(page[0]["id"]) if page else None

history_status() returns pruned_state_count and working_state_matches_current. It compares the working tables with the current snapshot, including nested provenance, without traversing older snapshots. It costs work proportional to the current network, not the retained history. state_count is the number of retained states.

list_states(*, offset=0, limit=None, detail="full") slices IDs before copying metadata. The default result is unchanged. Summary entries retain id, parent_ids, children_ids and current; their metadata contains method, label, ancestry_gaps and transition. The summary transition includes explanation_status and counts, with nodes/edges counts for added, removed and changed. Evidence, parameters and inclusion explanations are available through get_state. Summary entries omit the repeated pruned_state_ids list; use history_status for the count and history_dict for exact IDs. All returned mutable values are independent copies. For legacy snapshots without a recorded table delta, that table's counts are None, rather than invented zeroes.

Offsets and limits must be non-negative integers (booleans are rejected); limit=None selects the remaining states, and zero returns an empty list. Unknown detail modes and invalid pagination values raise ValueError. Pages use creation order; they are not frozen cursors across concurrent edits.

MCP summaries can use the page's parent/child IDs directly, together with state_count and history_status. Calling history_graph() or history_dict() as well would reintroduce full-history work.

Network history methods

Network

Network(initial_nodes: list[str] = None, sif_file=None, resources=None, *, database_name=None)

A molecular interaction network.

The `Network` object is the central organizing component of the `neko`
module. It is the subject of all operations implemented here, including
topological algorithms, graph analysis, network visualization and
integration of database knowledge.

Args: initial_nodes: A list of initial nodes to be added to the network. sif_file: A SIF (Simple Interaction Format) file to load the network from. resources: A pandas DataFrame containing the resources database.

Methods:

Source code in neko/core/network.py
def __init__(
        self,
        initial_nodes: list[str] = None,
        sif_file=None,
        resources=None,
        *,
        database_name=None,
    ):

    self._init_args = locals()
    del self._init_args['self']
    self.nodes = pd.DataFrame(columns=["Genesymbol", "Uniprot", "Type"])
    self.edges = pd.DataFrame(columns=["source", "target", "Type", "Effect", "References"])
    # Internal object-based storage
    self._node_objs = set()  # Set of Node objects
    self._edge_objs = set()  # Set of Edge objects
    self.initial_nodes = initial_nodes
    self._ontology = Ontology()
    # --- NetworkState history tracking ---
    self._states: dict[int, NetworkState] = {}
    self._state_metadata: dict[int, dict] = {}
    self._state_log: list[int] = []
    self._state_counter: int = 0
    self._current_state_id: Optional[int] = None
    self._root_state_id: Optional[int] = None
    self._auto_state_depth: int = 0
    self._history_enabled: bool = True
    self._max_history: Optional[int] = None
    self._pruned_state_ids = []
    self._transition_context = None
    self._resources_available = True
    self._is_initializing = True
    self._populate()
    self._is_initializing = False

Attributes

current_state_id property
current_state_id: Optional[int]
state_count property
state_count: int

Number of retained states, without materializing their metadata.

root_state_id property
root_state_id: Optional[int]

Methods:

save_state
save_state(metadata: Optional[dict] = None) -> int

Persist the current network snapshot and return its state id.

Source code in neko/core/network.py
def save_state(self, metadata: Optional[dict] = None) -> int:
    """Persist the current network snapshot and return its state id."""

    parent_id = self._current_state_id
    state = self._build_state(metadata, parent_id)
    state_id = state.state_id
    self._states[state_id] = state
    self._state_metadata[state_id] = state.metadata
    self._state_log.append(state_id)

    if parent_id is None:
        self._root_state_id = state_id
    else:
        parent_state = self._states[parent_id]
        parent_state.add_child(state_id)

    self._current_state_id = state_id
    self._state_counter += 1
    self._enforce_max_history()
    return state_id
set_max_history
set_max_history(max_states: Optional[int]) -> None

Set the maximum number of stored history states (None disables pruning).

Source code in neko/core/network.py
def set_max_history(self, max_states: Optional[int]) -> None:
    """Set the maximum number of stored history states (None disables pruning)."""

    if max_states is None:
        self._max_history = None
    else:
        max_states = int(max_states)
        if max_states < 2:
            max_states = 2
        self._max_history = max_states
    self._enforce_max_history()
set_history_tracking
set_history_tracking(enabled: bool) -> None

Globally enable or disable automatic state capture.

Source code in neko/core/network.py
def set_history_tracking(self, enabled: bool) -> None:
    """Globally enable or disable automatic state capture."""

    self._history_enabled = bool(enabled)
suspend_history
suspend_history()

Temporarily suspend automatic state capture within the context.

Source code in neko/core/network.py
@contextmanager
def suspend_history(self):
    """Temporarily suspend automatic state capture within the context."""

    previous = self._history_enabled
    self._history_enabled = False
    try:
        yield
    finally:
        self._history_enabled = previous
list_states
list_states(*, offset=0, limit=None, detail='full') -> list

Return a creation-ordered page; defaults preserve the full listing.

Summary metadata includes labels, explanation status and change counts, without copying evidence, parameters or inclusion explanations.

Source code in neko/core/network.py
def list_states(self, *, offset=0, limit=None, detail="full") -> list:
    """Return a creation-ordered page; defaults preserve the full listing.

    Summary metadata includes labels, explanation status and change counts,
    without copying evidence, parameters or inclusion explanations.
    """
    if type(offset) is not int or offset < 0:
        raise ValueError("offset must be a non-negative integer.")
    if limit is not None and (type(limit) is not int or limit < 0):
        raise ValueError("limit must be None or a non-negative integer.")
    if detail not in ("full", "summary"):
        raise ValueError("detail must be 'full' or 'summary'.")
    stop = None if limit is None else offset + limit
    result = []
    for state_id in self._state_log[offset:stop]:
        metadata = self._state_metadata.get(state_id, {})
        if detail == "summary":
            transition = metadata.get("transition", {})
            metadata = {
                "method": metadata.get("method"),
                "label": metadata.get("label"),
                "ancestry_gaps": list(metadata.get("ancestry_gaps", [])),
                "transition": {
                    "explanation_status": transition.get("explanation_status", "unavailable"),
                    "counts": {
                        table: {kind: len(transition.get(table, {}).get(kind, []))
                                for kind in ("added", "removed", "changed")}
                        if table in transition else None
                        for table in ("nodes", "edges")
                    },
                },
            }
        entry = {
            "id": state_id, "metadata": copy.deepcopy(metadata),
            "parent_ids": list(self._states[state_id].parent_ids),
            "children_ids": list(self._states[state_id].children_ids),
            "current": state_id == self.current_state_id,
        }
        if detail == "full":
            entry["pruned_state_ids"] = list(self._pruned_state_ids)
        result.append(entry)
    return result
checkout
checkout(state_id: int) -> None

Restore the network to a previously saved state.

Source code in neko/core/network.py
def checkout(self, state_id: int) -> None:
    """Restore the network to a previously saved state."""

    state = self._get_state(state_id)
    self.nodes = _history.copy_frame(state.nodes)
    self.edges = _history.copy_frame(state.edges)
    self.initial_nodes = copy.deepcopy(state.metadata.get("initial_nodes", self.initial_nodes))
    self._database_name = state.metadata.get("database_name", self.database_name)
    self._current_state_id = state_id
    self.sync_nodes_from_df()
    self.sync_edges_from_df()
restore_state
restore_state(state_id: Optional[int] = None) -> None

Backward compatible wrapper around :meth:checkout.

Source code in neko/core/network.py
def restore_state(self, state_id: Optional[int] = None) -> None:
    """Backward compatible wrapper around :meth:`checkout`."""

    target = state_id if state_id is not None else self._current_state_id
    if target is None:
        raise ValueError("No saved states to restore.")
    self.checkout(self._resolve_state_id(target))
undo
undo() -> None

Move to the parent state if available.

Source code in neko/core/network.py
def undo(self) -> None:
    """Move to the parent state if available."""

    if self._current_state_id is None:
        return
    parents = self._get_state(self._current_state_id).parent_ids
    if not parents:
        return
    self.checkout(parents[-1])
redo
redo(state_id: Optional[int] = None) -> None

Move to a child state. If more than one child exists, a specific state id is required.

Source code in neko/core/network.py
def redo(self, state_id: Optional[int] = None) -> None:
    """Move to a child state. If more than one child exists, a specific state id is required."""

    if self._current_state_id is None:
        return
    children = self._get_state(self._current_state_id).children_ids
    if not children:
        return
    target = state_id
    if target is None:
        if len(children) != 1:
            raise ValueError("Multiple branches available; specify a target state id.")
        target = children[0]
    elif target not in children:
        raise ValueError(f"State {target} is not a child of {self._current_state_id}.")
    self.checkout(target)
compare_states
compare_states(state_a: int, state_b: int) -> dict

Compare two states identified by their ids.

Source code in neko/core/network.py
def compare_states(self, state_a: int, state_b: int) -> dict:
    """Compare two states identified by their ids."""

    resolved_a = self._resolve_state_id(state_a)
    resolved_b = self._resolve_state_id(state_b)
    state1 = self._get_state(resolved_a)
    state2 = self._get_state(resolved_b)
    nodes1 = self._state_node_labels(state1)
    nodes2 = self._state_node_labels(state2)
    edges1 = self._state_edge_signatures(state1)
    edges2 = self._state_edge_signatures(state2)
    diff = {
        "added_nodes": list(nodes2 - nodes1),
        "removed_nodes": list(nodes1 - nodes2),
        "added_edges": list(edges2 - edges1),
        "removed_edges": list(edges1 - edges2),
        "node_changes": _history.delta(state1.nodes, state2.nodes, ["Uniprot", "Genesymbol"]),
        "edge_changes": _history.delta(state1.edges, state2.edges, ["source", "target"]),
    }
    return diff
describe_history
describe_history() -> None

Pretty-print the branching history tree.

Source code in neko/core/network.py
def describe_history(self) -> None:
    """Pretty-print the branching history tree."""

    if not self._states:
        print("<no states recorded>")
        return

    def _label(state: NetworkState) -> str:
        meta = state.metadata or {}
        label = meta.get("label") or meta.get("description")
        if label:
            return str(label)
        meta = {key: value for key, value in meta.items() if key not in _history.INTERNAL_METADATA_KEYS}
        if meta:
            return str(meta)
        return ""

    def _walk(state_id: int, depth: int) -> None:
        state = self._get_state(state_id)
        indent = "  " * depth
        label = _label(state)
        suffix = f" - {label}" if label else ""
        print(f"{indent}State {state_id}{suffix}")
        for child_id in state.children_ids:
            _walk(child_id, depth + 1)

    _walk(self._root_state_id, 0)
describe_states
describe_states() -> None

Backward compatible alias for :meth:describe_history.

Source code in neko/core/network.py
def describe_states(self) -> None:
    """Backward compatible alias for :meth:`describe_history`."""

    self.describe_history()
history_graph
history_graph()

Return a networkx DiGraph representing the state transitions.

Source code in neko/core/network.py
def history_graph(self):
    """Return a networkx DiGraph representing the state transitions."""

    from .._visual.history import build_history_graph

    return build_history_graph(self)
history_digraph
history_digraph(include_metadata: bool = True)

Return a Graphviz digraph for the history.

Source code in neko/core/network.py
def history_digraph(self, include_metadata: bool = True):
    """Return a Graphviz digraph for the history."""

    from .._visual.history import history_digraph

    return history_digraph(self, include_metadata=include_metadata)
history_html
history_html(include_metadata: bool = True, div_class: str = 'neko-history-graph') -> str

Return an HTML snippet embedding the history graph.

Source code in neko/core/network.py
def history_html(self, include_metadata: bool = True, div_class: str = "neko-history-graph") -> str:
    """Return an HTML snippet embedding the history graph."""

    from .._visual.history import history_html

    return history_html(self, include_metadata=include_metadata, div_class=div_class)
export_history
export_history(path, *, pretty=False)

Write all retained states, working state, provenance and ancestry.

Source code in neko/core/network.py
def export_history(self, path, *, pretty=False):
    """Write all retained states, working state, provenance and ancestry."""
    return _history.write_json(
        path, self.history_dict(), pretty=pretty, _normalized=True,
    )
from_history classmethod
from_history(path, resources=None)

Restore stored history offline; resources are needed for growth.

Source code in neko/core/network.py
@classmethod
def from_history(cls, path, resources=None):
    """Restore stored history offline; resources are needed for growth."""
    return _history.load_history(cls, path, resources)
history_dict
history_dict() -> dict

Return the independent JSON-compatible payload used by export_history.

Source code in neko/core/network.py
def history_dict(self) -> dict:
    """Return the independent JSON-compatible payload used by export_history."""
    return _history.history_payload(self)
history_status
history_status() -> dict

Inspect pruning and working changes without serializing past states.

Source code in neko/core/network.py
def history_status(self) -> dict:
    """Inspect pruning and working changes without serializing past states."""
    return {
        "pruned_state_count": len(self._pruned_state_ids),
        "working_state_matches_current": _history.working_state_matches_current(self),
    }
get_state
get_state(state_id: Optional[int] = None) -> dict

Read one exact saved state without checkout or mutable internal references.

Source code in neko/core/network.py
def get_state(self, state_id: Optional[int] = None) -> dict:
    """Read one exact saved state without checkout or mutable internal references."""
    state_id = self.current_state_id if state_id is None else state_id
    if type(state_id) is not int:
        raise ValueError("state_id must be an integer.")
    state = self._get_state(state_id)
    return {
        "id": state_id, "current": state_id == self.current_state_id,
        "parent_ids": list(state.parent_ids), "children_ids": list(state.children_ids),
        "nodes": _history.frame_payload(state.nodes), "edges": _history.frame_payload(state.edges),
        "metadata": _history.json_value(state.metadata),
    }

Snapshot value object

NetworkState

NetworkState(nodes: DataFrame, edges: DataFrame, metadata: Optional[Dict[str, Any]] = None, state_id: Optional[int] = None, parent_ids: Optional[List[int]] = None)

Immutable snapshot of a network, with parent/child bookkeeping.

Source code in neko/core/network_state.py
def __init__(
    self,
    nodes: pd.DataFrame,
    edges: pd.DataFrame,
    metadata: Optional[Dict[str, Any]] = None,
    state_id: Optional[int] = None,
    parent_ids: Optional[List[int]] = None,
):
    self.nodes = copy_frame(nodes)
    self.edges = copy_frame(edges)
    self.metadata: Dict[str, Any] = copy.deepcopy(metadata or {})
    self.state_id = state_id
    self.parent_ids: List[int] = list(parent_ids or [])
    self.children_ids: List[int] = []

Rendering helpers

build_history_graph

build_history_graph(network: 'Network') -> nx.DiGraph

Return a directed graph of the network state history.

Source code in neko/_visual/history.py
def build_history_graph(network: "Network") -> nx.DiGraph:
    """Return a directed graph of the network state history."""

    graph = nx.DiGraph()
    graph.graph.update(database_name=network.database_name,
                       current_state_id=network.current_state_id,
                       pruned_state_ids=list(network._pruned_state_ids))
    for state_id in network.list_states():
        # list_states returns dictionaries with id/metadata
        graph.add_node(state_id["id"], **state_id.get("metadata", {}))
    for state_id, state in network._states.items():  # pylint: disable=protected-access
        for child in state.children_ids:
            graph.add_edge(state_id, child)
    return graph

history_digraph

history_digraph(network: 'Network', include_metadata: bool = True) -> Digraph

Render history as a Graphviz Digraph.

Source code in neko/_visual/history.py
def history_digraph(network: "Network", include_metadata: bool = True) -> Digraph:
    """Render history as a Graphviz Digraph."""

    digraph = Digraph()

    for state_id, state in network._states.items():  # pylint: disable=protected-access
        metadata = state.metadata or {}
        label_lines = [f"State {state_id}"]
        if metadata.get("label"):
            label_lines.append(str(metadata["label"]))
        if state_id == network.current_state_id:
            label_lines.append("current")
        if include_metadata and metadata.get("method"):
            args = metadata.get("args", [])
            kwargs = metadata.get("kwargs", {})
            params = ", ".join(args)
            if kwargs:
                kwargs_repr = ", ".join(f"{k}={v}" for k, v in kwargs.items())
                params = ", ".join(filter(None, [params, kwargs_repr]))
            label_lines.append(f"{metadata['method']}({params[:160]})")
        elif include_metadata:
            extras = {
                key: value for key, value in metadata.items()
                if key not in {"label", "method", "args", "kwargs"}
                and (key == "database_name" or key not in INTERNAL_METADATA_KEYS)
            }
            for key, value in extras.items():
                label_lines.append(f"{key}: {value}")
        if metadata.get('ancestry_gaps'):
            label_lines.append('ancestry gap (pruned states)')
        digraph.node(str(state_id), "\n".join(label_lines),
                     URL=f"#neko-state-{state_id}" if include_metadata else "",
                     tooltip=f"State {state_id}: structured explanation")
        for child in state.children_ids:
            digraph.edge(str(state_id), str(child))
    return digraph

history_html

history_html(network: 'Network', include_metadata: bool = True, div_class: str = 'neko-history-graph') -> str

Return an HTML snippet embedding the history graph as SVG.

Source code in neko/_visual/history.py
def history_html(network: "Network", include_metadata: bool = True, div_class: str = "neko-history-graph") -> str:
    """Return an HTML snippet embedding the history graph as SVG."""

    svg = history_digraph(network, include_metadata=include_metadata).pipe(format="svg").decode("utf-8")
    details = ''
    if include_metadata:
        details = ''.join(
            f'<details id="neko-state-{sid}"><summary>State {sid} explanation</summary>'
            f'<pre>{html.escape(json.dumps(json_value(state.metadata), indent=2))}</pre></details>'
            for sid, state in network._states.items()
        )
    return f'<div class="{html.escape(div_class, quote=True)}">{svg}{details}</div>'