Source code for sktime_mcp.tools.save_data

"""
Data persistence tool for sktime MCP.

Saves the data behind a handle to a local file in CSV, Parquet, or JSON format.
"""

import logging
from pathlib import Path
from typing import Any

import pandas as pd

from sktime_mcp.runtime.executor import get_executor

logger = logging.getLogger(__name__)

# Supported output formats and their pandas writer methods
_FORMAT_WRITERS = {
    "csv": "to_csv",
    "parquet": "to_parquet",
    "json": "to_json",
}


[docs] def save_data_tool( data_handle: str, path: str, format: str = "csv", ) -> dict[str, Any]: """Persist the data behind a handle to a local file. Supports CSV, Parquet, and JSON output formats. The target directory is created automatically if it does not exist. Parameters ---------- data_handle : str Handle ID of the data to save (from load_data_source, split_data, etc.). path : str Destination file path (e.g. "/tmp/forecast_output.csv"). Note that the file extension is not used to infer the format; use the `format` argument instead. format : str, default="csv" Output format. Must be one of: "csv", "parquet", or "json". Returns ------- dict Dictionary containing success status and path information: - ``"success"`` (bool) -- True if the data was written successfully, False otherwise. - ``"saved_path"`` (str) -- Absolute path to the written file. - ``"format"`` (str) -- The format used to write the file. - ``"rows"`` (int) -- Number of rows written to the file. - ``"error"`` (str, optional) -- Error message if "success" is False. """ executor = get_executor() # --- validation -------------------------------------------------------- if data_handle not in executor._data_handles: return { "success": False, "error": f"Data handle '{data_handle}' not found", "available_handles": list(executor._data_handles.keys()), } fmt = format.lower() if fmt not in _FORMAT_WRITERS: return { "success": False, "error": f"Unsupported format '{format}'. Choose from: {list(_FORMAT_WRITERS.keys())}", } data_info = executor._data_handles[data_handle] y = data_info["y"] X = data_info.get("X") try: # Combine y and X into a single DataFrame for export if isinstance(y, pd.Series): df = y.to_frame(name=y.name if y.name else "target") elif isinstance(y, pd.DataFrame): df = y.copy() else: # Best-effort: wrap in a DataFrame df = pd.DataFrame(y, columns=["target"]) if X is not None and isinstance(X, pd.DataFrame): df = pd.concat([df, X], axis=1) # Ensure target directory exists abs_path = Path(path).resolve() abs_path.parent.mkdir(parents=True, exist_ok=True) # Write writer = getattr(df, _FORMAT_WRITERS[fmt]) if fmt == "json": writer(str(abs_path), orient="records", date_format="iso", indent=2) elif fmt == "parquet": writer(str(abs_path)) else: # CSV — include the index as a time column writer(str(abs_path)) return { "success": True, "saved_path": str(abs_path), "format": fmt, "rows": len(df), } except Exception as e: logger.exception("Error saving data") return { "success": False, "error": str(e), "error_type": type(e).__name__, }