MooseCP/tools/utils.py
2026-07-25 21:00:51 -07:00

120 lines
3.6 KiB
Python

import time
import datetime
import tomllib
from pathlib import Path
from typing import List, Dict, Any, Optional
class ToolError(Exception):
"""Custom exception for tool-related errors to be caught by the MCP server."""
pass
def load_toml(path: str) -> Dict[str, Any]:
"""Loads a TOML file into a dictionary."""
try:
with open(path, "rb") as f:
return tomllib.load(f)
except Exception as e:
raise ToolError(f"Failed to load config file {path}: {str(e)}")
def format_relative_time(timestamp: float) -> str:
"""Converts a timestamp to a human-readable relative format."""
now = time.time()
diff = now - timestamp
seconds = int(diff)
if seconds < 0:
return "In the future"
if seconds < 60:
return "Just now"
minutes = seconds // 60
if minutes < 60:
return f"{minutes}m ago"
hours = minutes // 60
if hours < 24:
return f"{hours}h ago"
days = hours // 24
if days == 1:
return "Yesterday"
if days < 30:
return f"{days}d ago"
months = days // 30
if months < 12:
return f"{months}mo ago"
dt = datetime.datetime.fromtimestamp(timestamp)
return dt.strftime('%Y-%m-%d')
def get_file_info_list(path: Path, extensions: Optional[tuple] = None) -> List[Dict[str, Any]]:
"""
Gathers basic information about files in a directory.
Optional extensions filter (e.g. ('.png', '.jpg')).
"""
items = []
try:
for entry in path.iterdir():
if extensions and not entry.name.lower().endswith(extensions):
continue
stats = entry.stat()
items.append({
"name": entry.name,
"path": entry,
"mtime": stats.st_mtime,
"size": stats.st_size,
"is_dir": entry.is_dir()
})
except Exception as e:
raise ToolError(f"Error accessing directory {path}: {e}")
return items
def sort_file_list(items: List[Dict[str, Any]], sort_by: str) -> List[Dict[str, Any]]:
"""Sorts items based on the provided key. Directories always come first."""
if sort_by == "mtime":
# Newest first
items.sort(key=lambda x: (not x["is_dir"], -x["mtime"]))
elif sort_by == "size":
# Largest first
items.sort(key=lambda x: (not x["is_dir"], -x["size"]))
else:
# Alphabetical
items.sort(key=lambda x: (not x["is_dir"], x["name"].lower()))
return items
def get_paginated_list(items: List[Any], page: int, page_size: int) -> List[Any]:
"""Returns a slice of the list based on page and page_size."""
start_idx = (page - 1) * page_size
end_idx = start_idx + page_size
return items[start_idx:end_idx]
def parse_indices(indices_str: str) -> List[int]:
"""
Parses a string like '11, 31-33, 44' into a list of 1-based indices.
Preserves the order specified in the string.
"""
indices = []
parts = [p.strip() for p in indices_str.split(',')]
for part in parts:
if not part:
continue
if '-' in part:
try:
start, end = map(int, part.split('-'))
step = 1 if start <= end else -1
for i in range(start, end + step, step):
indices.append(i)
except ValueError:
raise ToolError(f"Invalid range format: {part}")
else:
try:
indices.append(int(part))
except ValueError:
raise ToolError(f"Invalid index format: {part}")
return indices