mirror of
https://github.com/haris-musa/excel-mcp-server.git
synced 2025-12-08 17:12:41 +08:00
250 lines
9.2 KiB
Python
250 lines
9.2 KiB
Python
import logging
|
|
from typing import Any, Dict
|
|
|
|
from openpyxl.styles import (
|
|
PatternFill, Border, Side, Alignment, Protection, Font,
|
|
Color
|
|
)
|
|
from openpyxl.formatting.rule import (
|
|
ColorScaleRule, DataBarRule, IconSetRule,
|
|
FormulaRule, CellIsRule
|
|
)
|
|
|
|
from .workbook import get_or_create_workbook
|
|
from .cell_utils import parse_cell_range, validate_cell_reference
|
|
from .exceptions import ValidationError, FormattingError
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
def format_range(
|
|
filepath: str,
|
|
sheet_name: str,
|
|
start_cell: str,
|
|
end_cell: str = None,
|
|
bold: bool = False,
|
|
italic: bool = False,
|
|
underline: bool = False,
|
|
font_size: int = None,
|
|
font_color: str = None,
|
|
bg_color: str = None,
|
|
border_style: str = None,
|
|
border_color: str = None,
|
|
number_format: str = None,
|
|
alignment: str = None,
|
|
wrap_text: bool = False,
|
|
merge_cells: bool = False,
|
|
protection: Dict[str, Any] = None,
|
|
conditional_format: Dict[str, Any] = None
|
|
) -> Dict[str, Any]:
|
|
"""Apply formatting to a range of cells.
|
|
|
|
This function handles all Excel formatting operations including:
|
|
- Font properties (bold, italic, size, color, etc.)
|
|
- Cell fill/background color
|
|
- Borders (style and color)
|
|
- Number formatting
|
|
- Alignment and text wrapping
|
|
- Cell merging
|
|
- Protection
|
|
- Conditional formatting
|
|
|
|
Args:
|
|
filepath: Path to Excel file
|
|
sheet_name: Name of worksheet
|
|
start_cell: Starting cell reference
|
|
end_cell: Optional ending cell reference
|
|
bold: Whether to make text bold
|
|
italic: Whether to make text italic
|
|
underline: Whether to underline text
|
|
font_size: Font size in points
|
|
font_color: Font color (hex code)
|
|
bg_color: Background color (hex code)
|
|
border_style: Border style (thin, medium, thick, double)
|
|
border_color: Border color (hex code)
|
|
number_format: Excel number format string
|
|
alignment: Text alignment (left, center, right, justify)
|
|
wrap_text: Whether to wrap text
|
|
merge_cells: Whether to merge the range
|
|
protection: Cell protection settings
|
|
conditional_format: Conditional formatting rules
|
|
|
|
Returns:
|
|
Dictionary with operation status
|
|
"""
|
|
try:
|
|
# Validate cell references
|
|
if not validate_cell_reference(start_cell):
|
|
raise ValidationError(f"Invalid start cell reference: {start_cell}")
|
|
|
|
if end_cell and not validate_cell_reference(end_cell):
|
|
raise ValidationError(f"Invalid end cell reference: {end_cell}")
|
|
|
|
wb = get_or_create_workbook(filepath)
|
|
if sheet_name not in wb.sheetnames:
|
|
raise ValidationError(f"Sheet '{sheet_name}' not found")
|
|
|
|
sheet = wb[sheet_name]
|
|
|
|
# Get cell range coordinates
|
|
try:
|
|
start_row, start_col, end_row, end_col = parse_cell_range(start_cell, end_cell)
|
|
except ValueError as e:
|
|
raise ValidationError(f"Invalid cell range: {str(e)}")
|
|
|
|
# If no end cell specified, use start cell coordinates
|
|
if end_row is None:
|
|
end_row = start_row
|
|
if end_col is None:
|
|
end_col = start_col
|
|
|
|
# Apply font formatting
|
|
font_args = {
|
|
"bold": bold,
|
|
"italic": italic,
|
|
"underline": 'single' if underline else None,
|
|
}
|
|
if font_size is not None:
|
|
font_args["size"] = font_size
|
|
if font_color is not None:
|
|
try:
|
|
# Ensure color has FF prefix for full opacity
|
|
font_color = font_color if font_color.startswith('FF') else f'FF{font_color}'
|
|
font_args["color"] = Color(rgb=font_color)
|
|
except ValueError as e:
|
|
raise FormattingError(f"Invalid font color: {str(e)}")
|
|
font = Font(**font_args)
|
|
|
|
# Apply fill
|
|
fill = None
|
|
if bg_color is not None:
|
|
try:
|
|
# Ensure color has FF prefix for full opacity
|
|
bg_color = bg_color if bg_color.startswith('FF') else f'FF{bg_color}'
|
|
fill = PatternFill(
|
|
start_color=Color(rgb=bg_color),
|
|
end_color=Color(rgb=bg_color),
|
|
fill_type='solid'
|
|
)
|
|
except ValueError as e:
|
|
raise FormattingError(f"Invalid background color: {str(e)}")
|
|
|
|
# Apply borders
|
|
border = None
|
|
if border_style is not None:
|
|
try:
|
|
border_color = border_color if border_color else "000000"
|
|
border_color = border_color if border_color.startswith('FF') else f'FF{border_color}'
|
|
side = Side(
|
|
style=border_style,
|
|
color=Color(rgb=border_color)
|
|
)
|
|
border = Border(
|
|
left=side,
|
|
right=side,
|
|
top=side,
|
|
bottom=side
|
|
)
|
|
except ValueError as e:
|
|
raise FormattingError(f"Invalid border settings: {str(e)}")
|
|
|
|
# Apply alignment
|
|
align = None
|
|
if alignment is not None or wrap_text:
|
|
try:
|
|
align = Alignment(
|
|
horizontal=alignment,
|
|
vertical='center',
|
|
wrap_text=wrap_text
|
|
)
|
|
except ValueError as e:
|
|
raise FormattingError(f"Invalid alignment settings: {str(e)}")
|
|
|
|
# Apply protection
|
|
protect = None
|
|
if protection is not None:
|
|
try:
|
|
protect = Protection(**protection)
|
|
except ValueError as e:
|
|
raise FormattingError(f"Invalid protection settings: {str(e)}")
|
|
|
|
# Apply formatting to range
|
|
for row in range(start_row, end_row + 1):
|
|
for col in range(start_col, end_col + 1):
|
|
cell = sheet.cell(row=row, column=col)
|
|
cell.font = font
|
|
if fill is not None:
|
|
cell.fill = fill
|
|
if border is not None:
|
|
cell.border = border
|
|
if align is not None:
|
|
cell.alignment = align
|
|
if protect is not None:
|
|
cell.protection = protect
|
|
if number_format is not None:
|
|
cell.number_format = number_format
|
|
|
|
# Merge cells if requested
|
|
if merge_cells and end_cell:
|
|
try:
|
|
range_str = f"{start_cell}:{end_cell}"
|
|
sheet.merge_cells(range_str)
|
|
except ValueError as e:
|
|
raise FormattingError(f"Failed to merge cells: {str(e)}")
|
|
|
|
# Apply conditional formatting
|
|
if conditional_format is not None:
|
|
range_str = f"{start_cell}:{end_cell}" if end_cell else start_cell
|
|
rule_type = conditional_format.get('type')
|
|
if not rule_type:
|
|
raise FormattingError("Conditional format type not specified")
|
|
|
|
params = conditional_format.get('params', {})
|
|
|
|
# Handle fill parameter for cell_is rule
|
|
if rule_type == 'cell_is' and 'fill' in params:
|
|
fill_params = params['fill']
|
|
if isinstance(fill_params, dict):
|
|
try:
|
|
fill_color = fill_params.get('fgColor', 'FFC7CE') # Default to light red
|
|
fill_color = fill_color if fill_color.startswith('FF') else f'FF{fill_color}'
|
|
params['fill'] = PatternFill(
|
|
start_color=fill_color,
|
|
end_color=fill_color,
|
|
fill_type='solid'
|
|
)
|
|
except ValueError as e:
|
|
raise FormattingError(f"Invalid conditional format fill color: {str(e)}")
|
|
|
|
try:
|
|
if rule_type == 'color_scale':
|
|
rule = ColorScaleRule(**params)
|
|
elif rule_type == 'data_bar':
|
|
rule = DataBarRule(**params)
|
|
elif rule_type == 'icon_set':
|
|
rule = IconSetRule(**params)
|
|
elif rule_type == 'formula':
|
|
rule = FormulaRule(**params)
|
|
elif rule_type == 'cell_is':
|
|
rule = CellIsRule(**params)
|
|
else:
|
|
raise FormattingError(f"Invalid conditional format type: {rule_type}")
|
|
|
|
sheet.conditional_formatting.add(range_str, rule)
|
|
except Exception as e:
|
|
raise FormattingError(f"Failed to apply conditional formatting: {str(e)}")
|
|
|
|
wb.save(filepath)
|
|
|
|
range_str = f"{start_cell}:{end_cell}" if end_cell else start_cell
|
|
return {
|
|
"message": f"Applied formatting to range {range_str}",
|
|
"range": range_str
|
|
}
|
|
|
|
except (ValidationError, FormattingError) as e:
|
|
logger.error(str(e))
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Failed to apply formatting: {e}")
|
|
raise FormattingError(str(e))
|