exstruct 0.3.6__tar.gz → 0.3.7__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. {exstruct-0.3.6 → exstruct-0.3.7}/PKG-INFO +3 -2
  2. {exstruct-0.3.6 → exstruct-0.3.7}/README.md +2 -1
  3. {exstruct-0.3.6 → exstruct-0.3.7}/pyproject.toml +16 -20
  4. {exstruct-0.3.6 → exstruct-0.3.7}/src/exstruct/__init__.py +7 -19
  5. {exstruct-0.3.6 → exstruct-0.3.7}/src/exstruct/core/backends/base.py +9 -1
  6. {exstruct-0.3.6 → exstruct-0.3.7}/src/exstruct/core/backends/com_backend.py +33 -8
  7. {exstruct-0.3.6 → exstruct-0.3.7}/src/exstruct/core/backends/openpyxl_backend.py +22 -4
  8. {exstruct-0.3.6 → exstruct-0.3.7}/src/exstruct/core/cells.py +181 -23
  9. exstruct-0.3.7/src/exstruct/core/integrate.py +62 -0
  10. {exstruct-0.3.6 → exstruct-0.3.7}/src/exstruct/core/modeling.py +3 -0
  11. {exstruct-0.3.6 → exstruct-0.3.7}/src/exstruct/core/pipeline.py +201 -56
  12. {exstruct-0.3.6 → exstruct-0.3.7}/src/exstruct/core/workbook.py +9 -6
  13. {exstruct-0.3.6 → exstruct-0.3.7}/src/exstruct/engine.py +30 -8
  14. {exstruct-0.3.6 → exstruct-0.3.7}/src/exstruct/models/__init__.py +7 -0
  15. exstruct-0.3.7/src/exstruct/render/__init__.py +636 -0
  16. exstruct-0.3.6/src/exstruct/core/integrate.py +0 -58
  17. exstruct-0.3.6/src/exstruct/render/__init__.py +0 -239
  18. {exstruct-0.3.6 → exstruct-0.3.7}/LICENSE +0 -0
  19. {exstruct-0.3.6 → exstruct-0.3.7}/src/exstruct/cli/availability.py +0 -0
  20. {exstruct-0.3.6 → exstruct-0.3.7}/src/exstruct/cli/main.py +0 -0
  21. {exstruct-0.3.6 → exstruct-0.3.7}/src/exstruct/core/__init__.py +0 -0
  22. {exstruct-0.3.6 → exstruct-0.3.7}/src/exstruct/core/backends/__init__.py +0 -0
  23. {exstruct-0.3.6 → exstruct-0.3.7}/src/exstruct/core/charts.py +0 -0
  24. {exstruct-0.3.6 → exstruct-0.3.7}/src/exstruct/core/logging_utils.py +0 -0
  25. {exstruct-0.3.6 → exstruct-0.3.7}/src/exstruct/core/ranges.py +0 -0
  26. {exstruct-0.3.6 → exstruct-0.3.7}/src/exstruct/core/shapes.py +0 -0
  27. {exstruct-0.3.6 → exstruct-0.3.7}/src/exstruct/errors.py +0 -0
  28. {exstruct-0.3.6 → exstruct-0.3.7}/src/exstruct/io/__init__.py +0 -0
  29. {exstruct-0.3.6 → exstruct-0.3.7}/src/exstruct/io/serialize.py +0 -0
  30. {exstruct-0.3.6 → exstruct-0.3.7}/src/exstruct/models/maps.py +0 -0
  31. {exstruct-0.3.6 → exstruct-0.3.7}/src/exstruct/models/types.py +0 -0
  32. {exstruct-0.3.6 → exstruct-0.3.7}/src/exstruct/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: exstruct
3
- Version: 0.3.6
3
+ Version: 0.3.7
4
4
  Summary: Excel to structured JSON (tables, shapes, charts) for LLM/RAG pipelines
5
5
  Keywords: excel,structure,data,exstruct
6
6
  Author: harumiWeb
@@ -66,7 +66,8 @@ ExStruct reads Excel workbooks and outputs structured data (cells, table candida
66
66
  ## Features
67
67
 
68
68
  - **Excel → Structured JSON**: cells, shapes, charts, smartart, table candidates, print areas/views, and auto page-break areas per sheet.
69
- - **Output modes**: `light` (cells + table candidates + print areas; no COM, shapes/charts empty), `standard` (texted shapes + arrows, charts, smartart, merged cell ranges, print areas), `verbose` (all shapes with width/height, charts with size, merged cell ranges, print areas). Verbose also emits cell hyperlinks and `colors_map`. Size output is flag-controlled.
69
+ - **Output modes**: `light` (cells + table candidates + print areas; no COM, shapes/charts empty), `standard` (texted shapes + arrows, charts, smartart, merged cell ranges, print areas), `verbose` (all shapes with width/height, charts with size, merged cell ranges, print areas). Verbose also emits cell hyperlinks and `colors_map`. Size output is flag-controlled.
70
+ - **Formula map extraction**: emits `formulas_map` (formula string -> cell coordinates) via openpyxl/COM; enabled by default in `verbose` or via `include_formulas_map`.
70
71
  - **Auto page-break export (COM only)**: capture Excel-computed auto page breaks and write per-area JSON/YAML/TOON when requested (CLI option appears only when COM is available).
71
72
  - **Formats**: JSON (compact by default, `--pretty` available), YAML, TOON (optional dependencies).
72
73
  - **Table detection tuning**: adjust heuristics at runtime via API.
@@ -11,7 +11,8 @@ ExStruct reads Excel workbooks and outputs structured data (cells, table candida
11
11
  ## Features
12
12
 
13
13
  - **Excel → Structured JSON**: cells, shapes, charts, smartart, table candidates, print areas/views, and auto page-break areas per sheet.
14
- - **Output modes**: `light` (cells + table candidates + print areas; no COM, shapes/charts empty), `standard` (texted shapes + arrows, charts, smartart, merged cell ranges, print areas), `verbose` (all shapes with width/height, charts with size, merged cell ranges, print areas). Verbose also emits cell hyperlinks and `colors_map`. Size output is flag-controlled.
14
+ - **Output modes**: `light` (cells + table candidates + print areas; no COM, shapes/charts empty), `standard` (texted shapes + arrows, charts, smartart, merged cell ranges, print areas), `verbose` (all shapes with width/height, charts with size, merged cell ranges, print areas). Verbose also emits cell hyperlinks and `colors_map`. Size output is flag-controlled.
15
+ - **Formula map extraction**: emits `formulas_map` (formula string -> cell coordinates) via openpyxl/COM; enabled by default in `verbose` or via `include_formulas_map`.
15
16
  - **Auto page-break export (COM only)**: capture Excel-computed auto page breaks and write per-area JSON/YAML/TOON when requested (CLI option appears only when COM is available).
16
17
  - **Formats**: JSON (compact by default, `--pretty` available), YAML, TOON (optional dependencies).
17
18
  - **Table detection tuning**: adjust heuristics at runtime via API.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "exstruct"
3
- version = "0.3.6"
3
+ version = "0.3.7"
4
4
  description = "Excel to structured JSON (tables, shapes, charts) for LLM/RAG pipelines"
5
5
  readme = "README.md"
6
6
  license = { file = "LICENSE" }
@@ -60,7 +60,10 @@ omit = [
60
60
  [tool.ruff]
61
61
  target-version = "py311"
62
62
  src = ["exstruct"]
63
+ fix = true
63
64
 
65
+ # 静的解析ルール
66
+ [tool.ruff.lint]
64
67
  select = [
65
68
  "E", # pycodestyle errors
66
69
  "W", # pycodestyle warnings
@@ -75,43 +78,36 @@ select = [
75
78
  ]
76
79
 
77
80
  ignore = [
78
- "E501", # 行長は許容(Excel JSON は長くなりがち)
79
- "B008", # Pydantic の default_factory を誤検知するため
80
- "ANN101", # self に型を要求されてしまうため
81
- "ANN102", # cls も同様
81
+ "E501", # 長い行は許容(Excel JSON は長くなりがち)
82
+ "B008", # Pydantic の default_factory を使用するため
83
+ "ANN101", # self の型注釈は省略可能
84
+ "ANN102", # cls の型注釈は省略可能
82
85
  ]
83
86
 
84
- fix = true
85
-
86
- # 型ヒントのスタイル
87
- [tool.ruff.lint]
88
- extend-select = ["ANN"]
89
-
90
- # import の並び替え設定
91
- [tool.ruff.isort]
87
+ # import の並び順
88
+ [tool.ruff.lint.isort]
92
89
  combine-as-imports = true
93
90
  known-first-party = ["exstruct"]
94
91
  force-sort-within-sections = true
95
92
 
96
- # 複雑度チェック(関数の最大複雑度)
97
- [tool.ruff.mccabe]
93
+ # 複雑度の最大値
94
+ [tool.ruff.lint.mccabe]
98
95
  max-complexity = 12
99
96
 
100
- [tool.ruff.per-file-ignores]
97
+ [tool.ruff.lint.per-file-ignores]
101
98
  "tests/**/*.py" = ["N802", "N803", "N806"]
102
99
 
103
-
104
100
  [tool.mypy]
105
101
  packages = ["exstruct"]
106
102
  python_version = "3.11"
107
103
 
108
- # 外部ライブラリは一切チェックしない
104
+ # 外部ライブラリの型情報がない場合は無視
109
105
  ignore_missing_imports = true
110
106
 
111
- # 自作コードは厳密にチェックする
107
+ # 厳格モードを有効化
112
108
  strict = true
113
109
 
114
- # Pydantic v2 向け
110
+ # Pydantic v2 対応
115
111
  plugins = ["pydantic.mypy"]
116
112
 
117
113
  [tool.pytest.ini_options]
@@ -90,36 +90,24 @@ ExtractionMode = Literal["light", "standard", "verbose"]
90
90
 
91
91
  def extract(file_path: str | Path, mode: ExtractionMode = "standard") -> WorkbookData:
92
92
  """
93
- Extract an Excel workbook into WorkbookData.
93
+ Extracts an Excel workbook into a WorkbookData structure.
94
94
 
95
- Args:
96
- file_path: Path to .xlsx/.xlsm/.xls.
97
- mode: "light" / "standard" / "verbose"
98
- - light: cells + table detection only (no COM, shapes/charts empty). Print areas via openpyxl.
99
- - standard: texted shapes + arrows + charts (COM if available), print areas included. Shape/chart size is kept but hidden by default in output.
100
- - verbose: all shapes (including textless) with size, charts with size, and colors_map.
95
+ Parameters:
96
+ file_path (str | Path): Path to the workbook file (.xlsx, .xlsm, .xls).
97
+ mode (ExtractionMode): Extraction detail level. "light" includes cells and table detection only (no COM, shapes/charts empty; print areas via openpyxl). "standard" includes texted shapes, arrows, charts (COM if available) and print areas. "verbose" also includes shape/chart sizes, cell link map, colors map, and formulas map.
101
98
 
102
99
  Returns:
103
- WorkbookData containing sheets, rows, shapes, charts, and print areas.
104
-
105
- Raises:
106
- ValueError: If an invalid mode is provided.
107
-
108
- Examples:
109
- Extract with hyperlinks (verbose) and inspect table candidates:
110
-
111
- >>> from exstruct import extract
112
- >>> wb = extract("input.xlsx", mode="verbose")
113
- >>> wb.sheets["Sheet1"].table_candidates
114
- ['A1:B5']
100
+ WorkbookData: Parsed workbook representation containing sheets, rows, shapes, charts, and print areas.
115
101
  """
116
102
  include_links = True if mode == "verbose" else False
117
103
  include_colors_map = True if mode == "verbose" else None
104
+ include_formulas_map = True if mode == "verbose" else None
118
105
  engine = ExStructEngine(
119
106
  options=StructOptions(
120
107
  mode=mode,
121
108
  include_cell_links=include_links,
122
109
  include_colors_map=include_colors_map,
110
+ include_formulas_map=include_formulas_map,
123
111
  )
124
112
  )
125
113
  return engine.extract(file_path, mode=mode)
@@ -4,7 +4,7 @@ from dataclasses import dataclass
4
4
  from typing import Protocol
5
5
 
6
6
  from ...models import CellRow, PrintArea
7
- from ..cells import MergedCellRange, WorkbookColorsMap
7
+ from ..cells import MergedCellRange, WorkbookColorsMap, WorkbookFormulasMap
8
8
 
9
9
  CellData = dict[str, list[CellRow]]
10
10
  PrintAreaData = dict[str, list[PrintArea]]
@@ -40,3 +40,11 @@ class Backend(Protocol):
40
40
 
41
41
  def extract_merged_cells(self) -> MergedCellData:
42
42
  """Extract merged cell ranges from the workbook."""
43
+
44
+ def extract_formulas_map(self) -> WorkbookFormulasMap | None:
45
+ """
46
+ Retrieve the workbook's formulas organized by worksheet.
47
+
48
+ Returns:
49
+ WorkbookFormulasMap | None: A mapping of worksheet identifiers to their formulas, or `None` if the backend cannot provide a formulas map.
50
+ """
@@ -9,7 +9,12 @@ from typing import Any, cast
9
9
  import xlwings as xw
10
10
 
11
11
  from ...models import PrintArea
12
- from ..cells import WorkbookColorsMap, extract_sheet_colors_map_com
12
+ from ..cells import (
13
+ WorkbookColorsMap,
14
+ WorkbookFormulasMap,
15
+ extract_sheet_colors_map_com,
16
+ extract_sheet_formulas_map_com,
17
+ )
13
18
  from ..ranges import parse_range_zero_based
14
19
  from .base import MergedCellData, PrintAreaData
15
20
 
@@ -58,14 +63,15 @@ class ComBackend:
58
63
  def extract_colors_map(
59
64
  self, *, include_default_background: bool, ignore_colors: set[str] | None
60
65
  ) -> WorkbookColorsMap | None:
61
- """Extract colors_map via COM; logs and skips on failure.
66
+ """
67
+ Extract a workbook colors map using the Excel COM API.
62
68
 
63
- Args:
64
- include_default_background: Whether to include default backgrounds.
65
- ignore_colors: Optional set of color keys to ignore.
69
+ Parameters:
70
+ include_default_background (bool): Include the workbook's default background color in the resulting map.
71
+ ignore_colors (set[str] | None): Optional set of color keys to exclude from the map.
66
72
 
67
73
  Returns:
68
- WorkbookColorsMap or None when extraction fails.
74
+ WorkbookColorsMap | None: A mapping of workbook color definitions when extraction succeeds, or `None` if COM extraction fails.
69
75
  """
70
76
  try:
71
77
  return extract_sheet_colors_map_com(
@@ -80,11 +86,30 @@ class ComBackend:
80
86
  )
81
87
  return None
82
88
 
89
+ def extract_formulas_map(self) -> WorkbookFormulasMap | None:
90
+ """
91
+ Extracts the workbook's formulas map using COM.
92
+
93
+ Returns:
94
+ WorkbookFormulasMap or None: The extracted formulas map, or `None` if extraction failed.
95
+ """
96
+ try:
97
+ return extract_sheet_formulas_map_com(self.workbook)
98
+ except Exception as exc:
99
+ logger.warning(
100
+ "COM formula map extraction failed; skipping formulas_map. (%r)",
101
+ exc,
102
+ )
103
+ return None
104
+
83
105
  def extract_auto_page_breaks(self) -> PrintAreaData:
84
- """Compute auto page-break rectangles per sheet using Excel COM.
106
+ """
107
+ Compute auto page-break rectangles for each worksheet using Excel COM.
108
+
109
+ For each sheet, determine the sheet's print area (PageSetup.PrintArea or the used range) and split it into sub-rectangles along Excel's horizontal and vertical page breaks; parts that reference a different sheet are ignored. If extraction for a sheet fails, the sheet is skipped and a warning is logged.
85
110
 
86
111
  Returns:
87
- Mapping of sheet name to auto page-break areas.
112
+ Mapping from sheet name to a list of PrintArea entries. Each PrintArea describes a rectangular region with `r1` and `r2` as 1-based row indices and `c1` and `c2` as 0-based column indices.
88
113
  """
89
114
  results: PrintAreaData = {}
90
115
  for sheet in self.workbook.sheets:
@@ -9,10 +9,12 @@ from pathlib import Path
9
9
  from ...models import PrintArea
10
10
  from ..cells import (
11
11
  WorkbookColorsMap,
12
+ WorkbookFormulasMap,
12
13
  detect_tables_openpyxl,
13
14
  extract_sheet_cells,
14
15
  extract_sheet_cells_with_links,
15
16
  extract_sheet_colors_map,
17
+ extract_sheet_formulas_map,
16
18
  extract_sheet_merged_cells,
17
19
  )
18
20
  from ..ranges import parse_range_zero_based
@@ -99,14 +101,30 @@ class OpenpyxlBackend:
99
101
  except Exception:
100
102
  return {}
101
103
 
104
+ def extract_formulas_map(self) -> WorkbookFormulasMap | None:
105
+ """
106
+ Extract a mapping of workbook formulas for each sheet.
107
+
108
+ Returns:
109
+ WorkbookFormulasMap | None: A mapping from sheet name to its formulas, or `None` if extraction fails.
110
+ """
111
+ try:
112
+ return extract_sheet_formulas_map(self.file_path)
113
+ except Exception as exc:
114
+ logger.warning(
115
+ "Formula map extraction failed; skipping formulas_map. (%r)", exc
116
+ )
117
+ return None
118
+
102
119
  def detect_tables(self, sheet_name: str) -> list[str]:
103
- """Detect table candidates for a single sheet.
120
+ """
121
+ Detects table candidate ranges within the specified worksheet.
104
122
 
105
- Args:
106
- sheet_name: Target worksheet name.
123
+ Parameters:
124
+ sheet_name (str): Name of the worksheet to analyze for table candidates.
107
125
 
108
126
  Returns:
109
- List of table candidate ranges.
127
+ list[str]: Detected table candidate ranges as A1-style range strings; empty list if none are found or detection fails.
110
128
  """
111
129
  try:
112
130
  return detect_tables_openpyxl(self.file_path, sheet_name)
@@ -56,13 +56,41 @@ class WorkbookColorsMap:
56
56
  sheets: dict[str, SheetColorsMap]
57
57
 
58
58
  def get_sheet(self, sheet_name: str) -> SheetColorsMap | None:
59
- """Return the colors map for a sheet if available.
59
+ """
60
+ Retrieve the SheetColorsMap for a worksheet by name.
60
61
 
61
- Args:
62
- sheet_name: Target worksheet name.
62
+ Parameters:
63
+ sheet_name (str): Name of the worksheet to retrieve.
63
64
 
64
65
  Returns:
65
- SheetColorsMap for the sheet, or None if missing.
66
+ SheetColorsMap | None: The sheet's color map if present, `None` otherwise.
67
+ """
68
+ return self.sheets.get(sheet_name)
69
+
70
+
71
+ @dataclass(frozen=True)
72
+ class SheetFormulasMap:
73
+ """Formula map for a single worksheet."""
74
+
75
+ sheet_name: str
76
+ formulas_map: dict[str, list[tuple[int, int]]]
77
+
78
+
79
+ @dataclass(frozen=True)
80
+ class WorkbookFormulasMap:
81
+ """Formula maps for all worksheets in a workbook."""
82
+
83
+ sheets: dict[str, SheetFormulasMap]
84
+
85
+ def get_sheet(self, sheet_name: str) -> SheetFormulasMap | None:
86
+ """
87
+ Retrieve the formulas map for a worksheet.
88
+
89
+ Parameters:
90
+ sheet_name (str): Name of the worksheet to look up.
91
+
92
+ Returns:
93
+ SheetFormulasMap | None: The sheet's formulas map if present, `None` if the worksheet is not found.
66
94
  """
67
95
  return self.sheets.get(sheet_name)
68
96
 
@@ -102,22 +130,79 @@ def extract_sheet_colors_map(
102
130
  return WorkbookColorsMap(sheets=sheets)
103
131
 
104
132
 
133
+ def extract_sheet_formulas_map(file_path: Path) -> WorkbookFormulasMap:
134
+ """
135
+ Extract normalized formula strings from every worksheet in the workbook.
136
+
137
+ Parameters:
138
+ file_path (Path): Path to the Excel workbook to read.
139
+
140
+ Returns:
141
+ WorkbookFormulasMap: Mapping of sheet names to SheetFormulasMap objects. Each SheetFormulasMap contains a mapping from normalized formula strings (each beginning with "=") to a list of cell coordinates (row, column) where that formula occurs.
142
+ """
143
+ sheets: dict[str, SheetFormulasMap] = {}
144
+ with openpyxl_workbook(file_path, data_only=False, read_only=False) as wb:
145
+ for ws in wb.worksheets:
146
+ sheet_map = _extract_sheet_formulas(ws)
147
+ sheets[ws.title] = sheet_map
148
+ return WorkbookFormulasMap(sheets=sheets)
149
+
150
+
151
+ def extract_sheet_formulas_map_com(workbook: xw.Book) -> WorkbookFormulasMap:
152
+ """
153
+ Collects and normalizes formulas from every worksheet in an xlwings workbook into per-sheet mappings.
154
+
155
+ Parameters:
156
+ workbook: xlwings Book instance whose sheets will be scanned for formulas.
157
+
158
+ Returns:
159
+ WorkbookFormulasMap: maps sheet names to SheetFormulasMap objects. Each SheetFormulasMap.formulas_map maps a normalized formula string (consistent representation, e.g., beginning with "=") to a list of (row, column) tuples where row is 1-based and column is 0-based.
160
+ """
161
+ sheets: dict[str, SheetFormulasMap] = {}
162
+ for sheet in workbook.sheets:
163
+ formulas_map: dict[str, list[tuple[int, int]]] = {}
164
+ used = sheet.used_range
165
+ start_row = int(getattr(used, "row", 1))
166
+ start_col = int(getattr(used, "column", 1))
167
+ max_row = used.last_cell.row
168
+ max_col = used.last_cell.column
169
+ if max_row <= 0 or max_col <= 0:
170
+ sheets[sheet.name] = SheetFormulasMap(
171
+ sheet_name=sheet.name, formulas_map=formulas_map
172
+ )
173
+ continue
174
+ rng = sheet.range((start_row, start_col), (max_row, max_col))
175
+ matrix = _normalize_matrix(rng.formula)
176
+ for r_offset, row in enumerate(matrix):
177
+ for c_offset, value in enumerate(row):
178
+ normalized = _normalize_formula_from_com(value)
179
+ if normalized is None:
180
+ continue
181
+ row_index = start_row + r_offset
182
+ col_index = start_col + c_offset - 1
183
+ formulas_map.setdefault(normalized, []).append((row_index, col_index))
184
+ sheets[sheet.name] = SheetFormulasMap(
185
+ sheet_name=sheet.name, formulas_map=formulas_map
186
+ )
187
+ return WorkbookFormulasMap(sheets=sheets)
188
+
189
+
105
190
  def extract_sheet_colors_map_com(
106
191
  workbook: xw.Book,
107
192
  *,
108
193
  include_default_background: bool,
109
194
  ignore_colors: set[str] | None,
110
195
  ) -> WorkbookColorsMap:
111
- """Extract background colors for each worksheet via COM display formats.
196
+ """
197
+ Extract per-sheet background color maps using the workbook's COM/display-format interfaces.
112
198
 
113
- Args:
114
- workbook: xlwings workbook instance.
115
- include_default_background: Whether to include default (white) backgrounds
116
- within the used range.
117
- ignore_colors: Optional set of color keys to ignore.
199
+ Parameters:
200
+ workbook (xw.Book): xlwings workbook whose sheets will be inspected.
201
+ include_default_background (bool): If true, include default background colors (e.g., white) for cells inside each sheet's used range.
202
+ ignore_colors (set[str] | None): Optional set of normalized color keys to exclude from results.
118
203
 
119
204
  Returns:
120
- WorkbookColorsMap containing per-sheet color maps.
205
+ WorkbookColorsMap: Mapping of sheet names to SheetColorsMap containing detected background color positions for each worksheet.
121
206
  """
122
207
  _prepare_workbook_for_display_format(workbook)
123
208
  sheets: dict[str, SheetColorsMap] = {}
@@ -133,15 +218,16 @@ def extract_sheet_colors_map_com(
133
218
  def _extract_sheet_colors(
134
219
  ws: Worksheet, include_default_background: bool, ignore_colors: set[str] | None
135
220
  ) -> SheetColorsMap:
136
- """Extract background colors for a single worksheet.
221
+ """
222
+ Extract the background color locations present on a single worksheet.
137
223
 
138
- Args:
139
- ws: Target worksheet.
140
- include_default_background: Whether to include default (white) backgrounds.
141
- ignore_colors: Optional set of color keys to ignore.
224
+ Parameters:
225
+ ws (Worksheet): Worksheet to scan.
226
+ include_default_background (bool): If true, treat cells with the workbook default/background color as having a color key.
227
+ ignore_colors (set[str] | None): Optional set of color keys to ignore (keys are normalized before comparison).
142
228
 
143
229
  Returns:
144
- SheetColorsMap for the worksheet.
230
+ SheetColorsMap: Mapping from normalized color key to a list of cell coordinates where that color appears. Coordinates are tuples (row, col) where `row` is 1-based and `col` is 0-based.
145
231
  """
146
232
  min_row, min_col, max_row, max_col = _get_used_range_bounds(ws)
147
233
  colors_map: dict[str, list[tuple[int, int]]] = {}
@@ -165,18 +251,90 @@ def _extract_sheet_colors(
165
251
  return SheetColorsMap(sheet_name=ws.title, colors_map=colors_map)
166
252
 
167
253
 
254
+ def _extract_sheet_formulas(ws: Worksheet) -> SheetFormulasMap:
255
+ """
256
+ Collect normalized formula strings from a worksheet and group their cell coordinates.
257
+
258
+ Parameters:
259
+ ws (Worksheet): Worksheet to scan for formulas.
260
+
261
+ Returns:
262
+ SheetFormulasMap: container with the sheet's name and a mapping from each normalized formula string (prefixed with "=") to a list of cell coordinates as (row, zero-based-column).
263
+ """
264
+ min_row, min_col, max_row, max_col = _get_used_range_bounds(ws)
265
+ formulas_map: dict[str, list[tuple[int, int]]] = {}
266
+ if min_row > max_row or min_col > max_col:
267
+ return SheetFormulasMap(sheet_name=ws.title, formulas_map=formulas_map)
268
+
269
+ for row in ws.iter_rows(
270
+ min_row=min_row, max_row=max_row, min_col=min_col, max_col=max_col
271
+ ):
272
+ for cell in row:
273
+ if getattr(cell, "data_type", None) != "f":
274
+ continue
275
+ normalized = _normalize_formula_value(getattr(cell, "value", None))
276
+ if normalized is None:
277
+ continue
278
+ formulas_map.setdefault(normalized, []).append((cell.row, cell.col_idx - 1))
279
+ return SheetFormulasMap(sheet_name=ws.title, formulas_map=formulas_map)
280
+
281
+
282
+ def _normalize_formula_value(value: object) -> str | None:
283
+ """Normalize a formula string for openpyxl cells.
284
+
285
+ Args:
286
+ value: Raw cell value.
287
+
288
+ Returns:
289
+ Formula string with leading "=", or None when empty.
290
+ """
291
+ if value is None:
292
+ return None
293
+ array_text = getattr(value, "text", None)
294
+ if array_text is not None:
295
+ text = str(array_text)
296
+ else:
297
+ text = str(value)
298
+ if text == "":
299
+ return None
300
+ if not text.startswith("="):
301
+ return f"={text}"
302
+ return text
303
+
304
+
305
+ def _normalize_formula_from_com(value: object) -> str | None:
306
+ """
307
+ Normalize a COM-returned cell formula into a string that begins with '='.
308
+
309
+ Parameters:
310
+ value (object): Raw value returned from COM for a cell's formula.
311
+
312
+ Returns:
313
+ str | None: The input string if it is non-empty and starts with '=', `None` otherwise.
314
+ """
315
+ if value is None or not isinstance(value, str):
316
+ return None
317
+ text = value
318
+ if text == "":
319
+ return None
320
+ if not text.startswith("="):
321
+ return None
322
+ return text
323
+
324
+
168
325
  def _extract_sheet_colors_com(
169
326
  sheet: xw.Sheet, include_default_background: bool, ignore_colors: set[str] | None
170
327
  ) -> SheetColorsMap:
171
- """Extract background colors for a single worksheet via COM.
328
+ """
329
+ Extract per-sheet background color mapping using COM/DisplayFormat.
172
330
 
173
- Args:
174
- sheet: Target worksheet.
175
- include_default_background: Whether to include default (white) backgrounds.
176
- ignore_colors: Optional set of color keys to ignore.
331
+ Parameters:
332
+ sheet (xw.Sheet): xlwings sheet object to inspect.
333
+ include_default_background (bool): If True, include cells whose background is the workbook default color.
334
+ ignore_colors (set[str] | None): Optional set of normalized color keys to exclude from the result.
177
335
 
178
336
  Returns:
179
- SheetColorsMap for the worksheet.
337
+ SheetColorsMap: Mapping from normalized color key (hex/theme/index canonical form) to a list of cell coordinates where that color appears. Each coordinate is a tuple (row, col) where `row` is the worksheet row number (1-based) and `col` is the zero-based column index.
180
338
  """
181
339
  colors_map: dict[str, list[tuple[int, int]]] = {}
182
340
  used = sheet.used_range
@@ -0,0 +1,62 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Literal
5
+
6
+ from ..models import WorkbookData
7
+ from .pipeline import resolve_extraction_inputs, run_extraction_pipeline
8
+
9
+
10
+ def extract_workbook( # noqa: C901
11
+ file_path: str | Path,
12
+ mode: Literal["light", "standard", "verbose"] = "standard",
13
+ *,
14
+ include_cell_links: bool | None = None,
15
+ include_print_areas: bool | None = None,
16
+ include_auto_page_breaks: bool = False,
17
+ include_colors_map: bool | None = None,
18
+ include_default_background: bool = False,
19
+ ignore_colors: set[str] | None = None,
20
+ include_formulas_map: bool | None = None,
21
+ include_merged_cells: bool | None = None,
22
+ include_merged_values_in_rows: bool = True,
23
+ ) -> WorkbookData:
24
+ """
25
+ Extract a workbook into a structured WorkbookData representation.
26
+
27
+ May fall back to cells+tables extraction if Excel COM automation is unavailable.
28
+
29
+ Parameters:
30
+ file_path (str | Path): Path to the workbook file.
31
+ mode (Literal['light', 'standard', 'verbose']): Extraction mode that controls detail level.
32
+ include_cell_links (bool | None): Include cell hyperlinks; `None` uses mode defaults.
33
+ include_print_areas (bool | None): Include print areas; `None` defaults to True.
34
+ include_auto_page_breaks (bool): Include automatic page break information.
35
+ include_colors_map (bool | None): Include a colors map; `None` uses mode defaults.
36
+ include_default_background (bool): Include default background color when present.
37
+ ignore_colors (set[str] | None): Set of color keys to ignore during color mapping.
38
+ include_formulas_map (bool | None): Include a map of cell formulas; `None` uses mode defaults.
39
+ include_merged_cells (bool | None): Include merged cell ranges; `None` uses mode defaults.
40
+ include_merged_values_in_rows (bool): Preserve merged cell values in row-wise output.
41
+
42
+ Returns:
43
+ WorkbookData: The extracted workbook representation.
44
+
45
+ Raises:
46
+ ValueError: If `mode` is not one of "light", "standard", or "verbose".
47
+ """
48
+ inputs = resolve_extraction_inputs(
49
+ file_path,
50
+ mode=mode,
51
+ include_cell_links=include_cell_links,
52
+ include_print_areas=include_print_areas,
53
+ include_auto_page_breaks=include_auto_page_breaks,
54
+ include_colors_map=include_colors_map,
55
+ include_default_background=include_default_background,
56
+ ignore_colors=ignore_colors,
57
+ include_formulas_map=include_formulas_map,
58
+ include_merged_cells=include_merged_cells,
59
+ include_merged_values_in_rows=include_merged_values_in_rows,
60
+ )
61
+ result = run_extraction_pipeline(inputs)
62
+ return result.workbook
@@ -27,6 +27,7 @@ class SheetRawData:
27
27
  table_candidates: Detected table ranges.
28
28
  print_areas: Extracted print areas.
29
29
  auto_print_areas: Extracted auto page-break areas.
30
+ formulas_map: Mapping of formula strings to (row, column) positions.
30
31
  colors_map: Mapping of color keys to (row, column) positions.
31
32
  merged_cells: Extracted merged cell ranges.
32
33
  """
@@ -37,6 +38,7 @@ class SheetRawData:
37
38
  table_candidates: list[str]
38
39
  print_areas: list[PrintArea]
39
40
  auto_print_areas: list[PrintArea]
41
+ formulas_map: dict[str, list[tuple[int, int]]]
40
42
  colors_map: dict[str, list[tuple[int, int]]]
41
43
  merged_cells: list[MergedCellRange]
42
44
 
@@ -70,6 +72,7 @@ def build_sheet_data(raw: SheetRawData) -> SheetData:
70
72
  table_candidates=raw.table_candidates,
71
73
  print_areas=raw.print_areas,
72
74
  auto_print_areas=raw.auto_print_areas,
75
+ formulas_map=raw.formulas_map,
73
76
  colors_map=raw.colors_map,
74
77
  merged_cells=_build_merged_cells(raw.merged_cells),
75
78
  )