Skip to content

Commit 40c7db1

Browse files
authored
Merge pull request #51 from ncdcdev/feat/add-cell-styles
feat: add include_cell_styles parameter for Excel cell formatting (Issue #43)
2 parents 762a50e + 9d71bc6 commit 40c7db1

6 files changed

Lines changed: 202 additions & 5 deletions

File tree

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ Two authentication methods are supported:
3737
- Read mode: get data from specific sheets/ranges with `sheet` and `cell_range` parameters
3838
- **Automatic header inclusion**: when `cell_range` is specified, frozen rows (headers) are automatically included by default
3939
- Set `include_frozen_rows=False` to get only the specified range
40+
- **Cell style information** (optional): set `include_cell_styles=True` to get background colors, column widths, and row heights
41+
- Default is `False` to minimize token usage
42+
- Useful for identifying highlighted cells, colored headers, or visually emphasized content
4043
- Response includes cell data in `rows` (value and coordinate) and structural information when available
4144
- Structural info: sheet name, dimensions, frozen_rows, frozen_cols, freeze_panes (when present), merged_ranges (when merged cells exist)
4245
- No Excel Services dependency - uses direct file download + openpyxl parsing

README_ja.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ stdioとHTTPの両方のトランスポートに対応しています。
3737
- 読み取りモード: `sheet``cell_range`パラメータで特定シート/範囲を取得
3838
- **ヘッダー自動追加**: `cell_range`指定時、デフォルトで固定行(ヘッダー)を自動的に含める
3939
- `include_frozen_rows=False`を指定すると、指定範囲のみを取得
40+
- **セルスタイル情報**(オプション): `include_cell_styles=True`を指定すると、背景色・列幅・行高さを取得
41+
- デフォルトは`False`でトークン消費を最小化
42+
- 強調表示されたセル、色付きヘッダー、視覚的に強調されたコンテンツの識別に便利
4043
- レスポンスには`rows`内のセルデータ(値と座標)と構造情報(利用可能な場合)を含む
4144
- 構造情報: シート名、dimensions、frozen_rows、frozen_cols、freeze_panes(存在する場合)、merged_ranges(結合セルが存在する場合)
4245
- Excel Services不要 - 直接ファイルダウンロード+openpyxl解析方式

src/server.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,7 @@ def sharepoint_excel(
454454
sheet: str | None = None,
455455
cell_range: str | None = None,
456456
include_frozen_rows: bool = True,
457+
include_cell_styles: bool = False,
457458
ctx: Context | None = None,
458459
) -> str:
459460
"""
@@ -471,6 +472,10 @@ def sharepoint_excel(
471472
include_frozen_rows: cell_range指定時に固定行を自動追加(デフォルト: True)
472473
True: frozen_rowsで指定された行(通常はヘッダー)を自動的に取得
473474
False: 指定されたcell_rangeのみを取得
475+
include_cell_styles: セルの色・サイズ情報(default: false)
476+
色分けされたデータを抽出する場合のみTrueを指定
477+
背景色(fill)、列幅(width)、行高さ(height)を取得
478+
※トークン消費が約20%増加
474479
ctx: FastMCP context (injected automatically)
475480
476481
Returns:
@@ -498,6 +503,7 @@ def sharepoint_excel(
498503
sheet_name=sheet,
499504
cell_range=cell_range,
500505
include_frozen_rows=include_frozen_rows,
506+
include_cell_styles=include_cell_styles,
501507
)
502508

503509
except Exception as e:
@@ -542,6 +548,7 @@ def register_tools():
542548
"frozen at the top of the sheet (typically column headers). "
543549
"Response includes cell data in 'rows' (value and coordinate) and structural information "
544550
"(sheet name, dimensions, frozen_rows, frozen_cols, freeze_panes when present, merged_ranges when merged cells exist). "
551+
"Cell styles (include_cell_styles, default: false): background colors and sizes. Use only for color-coded data extraction. "
545552
"Header detection: Cannot be auto-detected from frozen_rows. "
546553
"ALWAYS read exactly 5 rows for header check: 'A1:Z5' (NOT 'A1:Z50' or more). "
547554
"Prefer 'query' search when possible to locate data first. "

src/sharepoint_excel.py

Lines changed: 74 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ def parse_to_json(
107107
sheet_name: str | None = None,
108108
cell_range: str | None = None,
109109
include_frozen_rows: bool = True,
110+
include_cell_styles: bool = False,
110111
) -> str:
111112
"""
112113
Excelファイルを解析してJSON形式で返す
@@ -118,13 +119,16 @@ def parse_to_json(
118119
include_frozen_rows: cell_range指定時に固定行(ヘッダー)を自動追加
119120
True(デフォルト): frozen_rowsで指定された行を自動的に取得
120121
False: 指定されたcell_rangeのみを取得
122+
include_cell_styles: セルの色・サイズ情報(default: false)
123+
色分けデータ抽出時のみ使用。トークン消費+約20%
121124
122125
Returns:
123126
JSON文字列
124127
- 各セルのデータ: value(値)、coordinate(座標)
125128
- 構造情報: シート名、dimensions(シート全体のセル範囲、例: "A1:D10")
126129
- 構造情報: frozen_rows(固定行数)、frozen_cols(固定列数)
127130
- 条件付き構造情報: freeze_panes(存在する場合)、merged_ranges(結合セルが存在する場合)
131+
- スタイル情報(include_cell_styles=Trueの場合): fill(背景色)、width(列幅)、height(行高さ)
128132
"""
129133
logger.info(
130134
f"Parsing Excel file: {file_path} (sheet={sheet_name}, range={cell_range})"
@@ -205,6 +209,7 @@ def parse_to_json(
205209
sheet,
206210
cell_range,
207211
include_frozen_rows,
212+
include_cell_styles,
208213
)
209214
result["sheets"].append(sheet_data)
210215

@@ -289,9 +294,7 @@ def _scan_sheet(
289294
}
290295
)
291296

292-
def _calculate_header_range(
293-
self, cell_range: str, frozen_rows: int
294-
) -> str | None:
297+
def _calculate_header_range(self, cell_range: str, frozen_rows: int) -> str | None:
295298
"""
296299
セル範囲に対してfrozen_rowsに基づくヘッダー範囲を計算
297300
@@ -393,6 +396,7 @@ def _parse_sheet(
393396
sheet,
394397
cell_range: str | None = None,
395398
include_frozen_rows: bool = True,
399+
include_cell_styles: bool = False,
396400
) -> dict[str, Any]:
397401
"""
398402
シートを解析してdict形式で返す
@@ -401,6 +405,7 @@ def _parse_sheet(
401405
sheet: openpyxl Worksheet
402406
cell_range: セル範囲指定(例: "A1:D10")
403407
include_frozen_rows: cell_range指定時に固定行(ヘッダー)を自動追加
408+
include_cell_styles: セルのスタイル情報を含めるか
404409
405410
Returns:
406411
シートデータのdict
@@ -467,7 +472,9 @@ def _parse_sheet(
467472

468473
# ヘッダー自動追加の場合、マージセルキャッシュにもヘッダー範囲を含める
469474
if include_frozen_rows and frozen_rows > 0:
470-
header_range = self._calculate_header_range(effective_range, frozen_rows)
475+
header_range = self._calculate_header_range(
476+
effective_range, frozen_rows
477+
)
471478
if header_range:
472479
# ヘッダー範囲とデータ範囲を結合した範囲を計算
473480
effective_range_for_merge = self._merge_ranges(
@@ -519,6 +526,19 @@ def _parse_sheet(
519526
if merged_ranges:
520527
sheet_data["merged_ranges"] = merged_ranges
521528

529+
# セルサイズのキャッシュを構築(パフォーマンス最適化)
530+
col_widths: dict[str, float] | None = None
531+
row_heights: dict[int, float] | None = None
532+
if include_cell_styles:
533+
col_widths = {}
534+
row_heights = {}
535+
for col_letter, dim in sheet.column_dimensions.items():
536+
if dim.width:
537+
col_widths[col_letter] = dim.width
538+
for row_num, dim in sheet.row_dimensions.items():
539+
if dim.height:
540+
row_heights[row_num] = dim.height
541+
522542
# データ取得
523543
if cell_range:
524544
# ヘッダー自動追加(include_frozen_rows=Trueの場合)
@@ -530,8 +550,11 @@ def _parse_sheet(
530550
all_rows.extend(
531551
self._parse_rows(
532552
header_rows,
553+
include_cell_styles,
533554
merged_cell_map,
534555
merged_anchor_value_map,
556+
col_widths,
557+
row_heights,
535558
)
536559
)
537560

@@ -541,8 +564,11 @@ def _parse_sheet(
541564
all_rows.extend(
542565
self._parse_rows(
543566
rows_to_process,
567+
include_cell_styles,
544568
merged_cell_map,
545569
merged_anchor_value_map,
570+
col_widths,
571+
row_heights,
546572
)
547573
)
548574

@@ -554,8 +580,11 @@ def _parse_sheet(
554580
all_rows.extend(
555581
self._parse_rows(
556582
rows_to_process,
583+
include_cell_styles,
557584
merged_cell_map,
558585
merged_anchor_value_map,
586+
col_widths,
587+
row_heights,
559588
)
560589
)
561590

@@ -741,16 +770,22 @@ def _expand_axis_range(self, range_str: str) -> str:
741770
def _parse_cell(
742771
self,
743772
cell,
773+
include_cell_styles: bool = False,
744774
merged_cell_map: dict[str, str] | None = None,
745775
merged_anchor_value_map: dict[str, Any] | None = None,
776+
col_widths: dict[str, float] | None = None,
777+
row_heights: dict[int, float] | None = None,
746778
) -> dict[str, Any]:
747779
"""
748780
セルを解析してdict形式で返す
749781
750782
Args:
751783
cell: openpyxl Cell
784+
include_cell_styles: セルのスタイル情報を含めるか(デフォルト: False)
752785
merged_cell_map: マージセル座標からマージ範囲へのマップ(パフォーマンス最適化用)
753786
merged_anchor_value_map: マージ範囲 -> アンカー値 のマップ(結合セルの値埋め用)
787+
col_widths: 列幅のキャッシュ(パフォーマンス最適化用)
788+
row_heights: 行高さのキャッシュ(パフォーマンス最適化用)
754789
755790
Returns:
756791
セルデータのdict
@@ -776,22 +811,53 @@ def _parse_cell(
776811
if anchor_value is not None:
777812
cell_data["value"] = anchor_value
778813

779-
# 書式情報(fill/width/height/data_type)は現在サポートされていません
814+
# スタイル情報(include_cell_styles=Trueの場合のみ)
815+
if include_cell_styles:
816+
# 背景色情報
817+
if cell.fill and cell.fill.patternType:
818+
fill_info = {
819+
"pattern_type": cell.fill.patternType,
820+
}
821+
fg_color = self._color_to_hex(cell.fill.fgColor)
822+
if fg_color:
823+
fill_info["fg_color"] = fg_color
824+
bg_color = self._color_to_hex(cell.fill.bgColor)
825+
if bg_color:
826+
fill_info["bg_color"] = bg_color
827+
cell_data["fill"] = fill_info
828+
829+
# セルサイズ(列幅・行高さ)
830+
# MergedCellの場合は属性が存在しないため、hasattrでチェック
831+
if hasattr(cell, "column_letter") and hasattr(cell, "row"):
832+
if cell.column_letter and cell.row:
833+
# キャッシュから列幅を取得(パフォーマンス最適化)
834+
if col_widths and cell.column_letter in col_widths:
835+
cell_data["width"] = col_widths[cell.column_letter]
836+
# キャッシュから行高さを取得(パフォーマンス最適化)
837+
if row_heights and cell.row in row_heights:
838+
cell_data["height"] = row_heights[cell.row]
839+
780840
return cell_data
781841

782842
def _parse_rows(
783843
self,
784844
rows: tuple[tuple[Cell, ...], ...],
845+
include_cell_styles: bool = False,
785846
merged_cell_map: dict[str, str] | None = None,
786847
merged_anchor_value_map: dict[str, Any] | None = None,
848+
col_widths: dict[str, float] | None = None,
849+
row_heights: dict[int, float] | None = None,
787850
) -> list[list[dict[str, Any]]]:
788851
"""
789852
行データを解析してリスト形式で返す(コード重複削減用ヘルパー)
790853
791854
Args:
792855
rows: 行データのタプル
856+
include_cell_styles: セルのスタイル情報を含めるか
793857
merged_cell_map: マージセル情報
794858
merged_anchor_value_map: マージ範囲 -> アンカー値
859+
col_widths: 列幅のキャッシュ(パフォーマンス最適化用)
860+
row_heights: 行高さのキャッシュ(パフォーマンス最適化用)
795861
796862
Returns:
797863
解析された行データのリスト
@@ -801,8 +867,11 @@ def _parse_rows(
801867
row_data = [
802868
self._parse_cell(
803869
cell,
870+
include_cell_styles,
804871
merged_cell_map,
805872
merged_anchor_value_map,
873+
col_widths,
874+
row_heights,
806875
)
807876
for cell in row
808877
]

tests/test_server.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,7 @@ def test_excel_read_default(
225225
sheet_name=None,
226226
cell_range=None,
227227
include_frozen_rows=True,
228+
include_cell_styles=False,
228229
)
229230

230231
@pytest.mark.unit
@@ -265,6 +266,7 @@ def test_excel_with_sheet_parameter(
265266
sheet_name="Sheet2",
266267
cell_range=None,
267268
include_frozen_rows=True,
269+
include_cell_styles=False,
268270
)
269271

270272
@pytest.mark.unit
@@ -287,6 +289,7 @@ def test_excel_with_cell_range_parameter(
287289
sheet_name="Sheet1",
288290
cell_range="A1:D10",
289291
include_frozen_rows=True,
292+
include_cell_styles=False,
290293
)
291294

292295
@pytest.mark.unit

0 commit comments

Comments
 (0)