exstruct 0.2.3__tar.gz → 0.2.21__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: exstruct
3
- Version: 0.2.3
3
+ Version: 0.2.21
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
@@ -60,12 +60,12 @@ Description-Content-Type: text/markdown
60
60
 
61
61
  ![ExStruct Image](/docs/assets/icon.webp)
62
62
 
63
- ExStruct reads Excel workbooks and outputs structured data (cells, table candidates, shapes, charts, print areas/views, hyperlinks) as JSON by default, with optional YAML/TOON formats. It targets both COM/Excel environments (rich extraction) and non-COM environments (cells + table candidates + print areas), with tunable detection heuristics and multiple output modes to fit LLM/RAG pipelines.
63
+ ExStruct reads Excel workbooks and outputs structured data (tables, shapes, charts, hyperlinks) as JSON by default, with optional YAML/TOON formats. It targets both COM/Excel environments (rich extraction) and non-COM environments (cells + table candidates), with tunable detection heuristics and multiple output modes to fit LLM/RAG pipelines.
64
64
 
65
65
  ## Features
66
66
 
67
- - **Excel → Structured JSON**: cells, shapes, charts, table candidates, and print areas/views per sheet.
68
- - **Output modes**: `light` (cells + table candidates + print areas; no COM, shapes/charts empty), `standard` (texted shapes + arrows, charts, print areas), `verbose` (all shapes with width/height, charts with size, print areas). Verbose also emits cell hyperlinks. Size output is flag-controlled.
67
+ - **Excel → Structured JSON**: cells, shapes, charts, and table candidates per sheet.
68
+ - **Output modes**: `light` (cells + table candidates only), `standard` (texted shapes + arrows, charts), `verbose` (all shapes with width/height). Verbose also emits cell hyperlinks.
69
69
  - **Formats**: JSON (compact by default, `--pretty` available), YAML, TOON (optional dependencies).
70
70
  - **Table detection tuning**: adjust heuristics at runtime via API.
71
71
  - **CLI rendering** (Excel required): optional PDF and per-sheet PNGs.
@@ -85,8 +85,7 @@ Optional extras:
85
85
  - All extras at once: `pip install exstruct[yaml,toon,render]`
86
86
 
87
87
  Platform note:
88
-
89
- - Full extraction (shapes/charts) targets Windows + Excel (COM via xlwings). On other platforms, use `mode=light` to get cells + `table_candidates`.
88
+ - Full extraction (shapes/charts) targets Windows + Excel (COM via xlwings). On other platforms, use `mode=light` to get cells + `table_candidates` safely.
90
89
 
91
90
  ## Quick Start (CLI)
92
91
 
@@ -96,7 +95,6 @@ exstruct input.xlsx -o out.json --pretty # pretty JSON to a file
96
95
  exstruct input.xlsx --format yaml # YAML (needs pyyaml)
97
96
  exstruct input.xlsx --format toon # TOON (needs python-toon)
98
97
  exstruct input.xlsx --sheets-dir sheets/ # split per sheet in chosen format
99
- exstruct input.xlsx --print-areas-dir areas/ # split per print area (if any)
100
98
  exstruct input.xlsx --mode light # cells + table candidates only
101
99
  exstruct input.xlsx --pdf --image # PDF and PNGs (Excel required)
102
100
  ```
@@ -114,7 +112,7 @@ set_table_detection_params(table_score_threshold=0.3, density_min=0.04)
114
112
  wb = extract("input.xlsx", mode="standard")
115
113
  export(wb, Path("out.json"), pretty=False) # compact JSON
116
114
 
117
- # Model helpers: iterate, index, and serialize directly
115
+ # Model helpers: iterate, index, and serialize directly from the models
118
116
  first_sheet = wb["Sheet1"] # __getitem__ access
119
117
  for name, sheet in wb: # __iter__ yields (name, SheetData)
120
118
  print(name, len(sheet.rows))
@@ -122,23 +120,19 @@ wb.save("out.json", pretty=True) # WorkbookData → file (by extension)
122
120
  first_sheet.save("sheet.json") # SheetData → file (by extension)
123
121
  print(first_sheet.to_yaml()) # YAML text (requires pyyaml)
124
122
 
125
- # ExStructEngine: per-instance options for extraction/output
126
- from exstruct import ExStructEngine, StructOptions, OutputOptions
127
-
128
- engine = ExStructEngine(
129
- options=StructOptions(mode="verbose"), # verbose includes hyperlinks by default
130
- output=OutputOptions(include_shapes=False, pretty=True),
131
- )
132
- wb2 = engine.extract("input.xlsx")
133
- engine.export(wb2, Path("out_filtered.json")) # drops shapes via OutputOptions
134
-
135
- # Enable hyperlinks in other modes
136
- engine_links = ExStructEngine(options=StructOptions(mode="standard", include_cell_links=True))
137
- with_links = engine_links.extract("input.xlsx")
138
-
139
- # Export per print area (if print areas exist)
140
- from exstruct import export_print_areas_as
141
- export_print_areas_as(wb, "areas", fmt="json", pretty=True)
123
+ # ExStructEngine: per-instance options for extraction/output
124
+ from exstruct import ExStructEngine, StructOptions, OutputOptions
125
+
126
+ engine = ExStructEngine(
127
+ options=StructOptions(mode="verbose"), # verbose includes hyperlinks by default
128
+ output=OutputOptions(include_shapes=False, pretty=True),
129
+ )
130
+ wb2 = engine.extract("input.xlsx")
131
+ engine.export(wb2, Path("out_filtered.json")) # drops shapes via OutputOptions
132
+
133
+ # Enable hyperlinks in other modes
134
+ engine_links = ExStructEngine(options=StructOptions(mode="standard", include_cell_links=True))
135
+ with_links = engine_links.extract("input.xlsx")
142
136
  ```
143
137
 
144
138
  **Note (non-COM environments):** If Excel COM is unavailable, extraction still runs and returns cells + `table_candidates`; `shapes`/`charts` will be empty.
@@ -158,11 +152,11 @@ set_table_detection_params(
158
152
 
159
153
  Use higher thresholds to reduce false positives; lower them if true tables are missed.
160
154
 
161
- ## Output Modes
162
-
163
- - **light**: cells + table candidates (no COM needed).
164
- - **standard**: texted shapes + arrows, charts (COM if available), table candidates. Hyperlinks are off unless `include_cell_links=True`.
165
- - **verbose**: all shapes (with width/height), charts, table candidates, and cell hyperlinks.
155
+ ## Output Modes
156
+
157
+ - **light**: cells + table candidates (no COM needed).
158
+ - **standard**: texted shapes + arrows, charts (COM if available), table candidates. Hyperlinks are off unless `include_cell_links=True`.
159
+ - **verbose**: all shapes (with width/height), charts, table candidates, and cell hyperlinks.
166
160
 
167
161
  ## Error Handling / Fallbacks
168
162
 
@@ -191,7 +185,6 @@ To show how well exstruct can structure Excel, we parse a workbook that combines
191
185
  (Screenshot below is the actual sample Excel sheet)
192
186
  ![Sample Excel](/docs/assets/demo_sheet.png)
193
187
  Sample workbook: `sample/sample.xlsx`
194
- Sample workbook: `sample/sample.xlsx`
195
188
 
196
189
  ### 1. Input: Excel Sheet Overview
197
190
 
@@ -378,12 +371,6 @@ In short, **exstruct = “an engine that converts Excel into a format AI can und
378
371
  - Default JSON is compact to reduce tokens; use `--pretty` or `pretty=True` when readability matters.
379
372
  - Field `table_candidates` replaces `tables`; adjust downstream consumers accordingly.
380
373
 
381
- ## Print Areas (PrintArea / PrintAreaView)
382
-
383
- - `SheetData.print_areas` holds print areas (cell coordinates) in light/standard/verbose.
384
- - Use `export_print_areas_as(...)` or CLI `--print-areas-dir` to write one file per print area (nothing is written if none exist).
385
- - `PrintAreaView` includes rows and table candidates inside the area, plus shapes/charts that overlap the area (size-less shapes are treated as points). `normalize=True` rebases row/col indices to the area origin.
386
-
387
374
  ## License
388
375
 
389
376
  BSD-3-Clause. See `LICENSE` for details.
@@ -391,3 +378,18 @@ BSD-3-Clause. See `LICENSE` for details.
391
378
  ## Documentation
392
379
 
393
380
  - API Reference (GitHub Pages): https://harumiweb.github.io/exstruct/
381
+ # Engine option cheat sheet
382
+
383
+ | Option class | Field | Meaning |
384
+ | -------------- | ------------------- | ------- |
385
+ | StructOptions | mode | "light"/"standard"/"verbose" |
386
+ | | table_params | Dict passed to `set_table_detection_params` (table_score_threshold, density_min, coverage_min, min_nonempty_cells) |
387
+ | | include_cell_links | Include cell hyperlinks in `rows[*].links` (None -> auto: verbose=True, others=False) |
388
+ | OutputOptions | fmt | Default format ("json"/"yaml"/"yml"/"toon") |
389
+ | | pretty / indent | Pretty-print JSON and control indent |
390
+ | | include_rows | Include rows (False to drop) |
391
+ | | include_shapes | Include shapes |
392
+ | | include_charts | Include charts |
393
+ | | include_tables | Include table_candidates |
394
+ | | sheets_dir | Optional directory for per-sheet exports |
395
+ | | stream | Default stream when output_path is None |
@@ -4,12 +4,12 @@
4
4
 
5
5
  ![ExStruct Image](/docs/assets/icon.webp)
6
6
 
7
- ExStruct reads Excel workbooks and outputs structured data (cells, table candidates, shapes, charts, print areas/views, hyperlinks) as JSON by default, with optional YAML/TOON formats. It targets both COM/Excel environments (rich extraction) and non-COM environments (cells + table candidates + print areas), with tunable detection heuristics and multiple output modes to fit LLM/RAG pipelines.
7
+ ExStruct reads Excel workbooks and outputs structured data (tables, shapes, charts, hyperlinks) as JSON by default, with optional YAML/TOON formats. It targets both COM/Excel environments (rich extraction) and non-COM environments (cells + table candidates), with tunable detection heuristics and multiple output modes to fit LLM/RAG pipelines.
8
8
 
9
9
  ## Features
10
10
 
11
- - **Excel → Structured JSON**: cells, shapes, charts, table candidates, and print areas/views per sheet.
12
- - **Output modes**: `light` (cells + table candidates + print areas; no COM, shapes/charts empty), `standard` (texted shapes + arrows, charts, print areas), `verbose` (all shapes with width/height, charts with size, print areas). Verbose also emits cell hyperlinks. Size output is flag-controlled.
11
+ - **Excel → Structured JSON**: cells, shapes, charts, and table candidates per sheet.
12
+ - **Output modes**: `light` (cells + table candidates only), `standard` (texted shapes + arrows, charts), `verbose` (all shapes with width/height). Verbose also emits cell hyperlinks.
13
13
  - **Formats**: JSON (compact by default, `--pretty` available), YAML, TOON (optional dependencies).
14
14
  - **Table detection tuning**: adjust heuristics at runtime via API.
15
15
  - **CLI rendering** (Excel required): optional PDF and per-sheet PNGs.
@@ -29,8 +29,7 @@ Optional extras:
29
29
  - All extras at once: `pip install exstruct[yaml,toon,render]`
30
30
 
31
31
  Platform note:
32
-
33
- - Full extraction (shapes/charts) targets Windows + Excel (COM via xlwings). On other platforms, use `mode=light` to get cells + `table_candidates`.
32
+ - Full extraction (shapes/charts) targets Windows + Excel (COM via xlwings). On other platforms, use `mode=light` to get cells + `table_candidates` safely.
34
33
 
35
34
  ## Quick Start (CLI)
36
35
 
@@ -40,7 +39,6 @@ exstruct input.xlsx -o out.json --pretty # pretty JSON to a file
40
39
  exstruct input.xlsx --format yaml # YAML (needs pyyaml)
41
40
  exstruct input.xlsx --format toon # TOON (needs python-toon)
42
41
  exstruct input.xlsx --sheets-dir sheets/ # split per sheet in chosen format
43
- exstruct input.xlsx --print-areas-dir areas/ # split per print area (if any)
44
42
  exstruct input.xlsx --mode light # cells + table candidates only
45
43
  exstruct input.xlsx --pdf --image # PDF and PNGs (Excel required)
46
44
  ```
@@ -58,7 +56,7 @@ set_table_detection_params(table_score_threshold=0.3, density_min=0.04)
58
56
  wb = extract("input.xlsx", mode="standard")
59
57
  export(wb, Path("out.json"), pretty=False) # compact JSON
60
58
 
61
- # Model helpers: iterate, index, and serialize directly
59
+ # Model helpers: iterate, index, and serialize directly from the models
62
60
  first_sheet = wb["Sheet1"] # __getitem__ access
63
61
  for name, sheet in wb: # __iter__ yields (name, SheetData)
64
62
  print(name, len(sheet.rows))
@@ -66,23 +64,19 @@ wb.save("out.json", pretty=True) # WorkbookData → file (by extension)
66
64
  first_sheet.save("sheet.json") # SheetData → file (by extension)
67
65
  print(first_sheet.to_yaml()) # YAML text (requires pyyaml)
68
66
 
69
- # ExStructEngine: per-instance options for extraction/output
70
- from exstruct import ExStructEngine, StructOptions, OutputOptions
71
-
72
- engine = ExStructEngine(
73
- options=StructOptions(mode="verbose"), # verbose includes hyperlinks by default
74
- output=OutputOptions(include_shapes=False, pretty=True),
75
- )
76
- wb2 = engine.extract("input.xlsx")
77
- engine.export(wb2, Path("out_filtered.json")) # drops shapes via OutputOptions
78
-
79
- # Enable hyperlinks in other modes
80
- engine_links = ExStructEngine(options=StructOptions(mode="standard", include_cell_links=True))
81
- with_links = engine_links.extract("input.xlsx")
82
-
83
- # Export per print area (if print areas exist)
84
- from exstruct import export_print_areas_as
85
- export_print_areas_as(wb, "areas", fmt="json", pretty=True)
67
+ # ExStructEngine: per-instance options for extraction/output
68
+ from exstruct import ExStructEngine, StructOptions, OutputOptions
69
+
70
+ engine = ExStructEngine(
71
+ options=StructOptions(mode="verbose"), # verbose includes hyperlinks by default
72
+ output=OutputOptions(include_shapes=False, pretty=True),
73
+ )
74
+ wb2 = engine.extract("input.xlsx")
75
+ engine.export(wb2, Path("out_filtered.json")) # drops shapes via OutputOptions
76
+
77
+ # Enable hyperlinks in other modes
78
+ engine_links = ExStructEngine(options=StructOptions(mode="standard", include_cell_links=True))
79
+ with_links = engine_links.extract("input.xlsx")
86
80
  ```
87
81
 
88
82
  **Note (non-COM environments):** If Excel COM is unavailable, extraction still runs and returns cells + `table_candidates`; `shapes`/`charts` will be empty.
@@ -102,11 +96,11 @@ set_table_detection_params(
102
96
 
103
97
  Use higher thresholds to reduce false positives; lower them if true tables are missed.
104
98
 
105
- ## Output Modes
106
-
107
- - **light**: cells + table candidates (no COM needed).
108
- - **standard**: texted shapes + arrows, charts (COM if available), table candidates. Hyperlinks are off unless `include_cell_links=True`.
109
- - **verbose**: all shapes (with width/height), charts, table candidates, and cell hyperlinks.
99
+ ## Output Modes
100
+
101
+ - **light**: cells + table candidates (no COM needed).
102
+ - **standard**: texted shapes + arrows, charts (COM if available), table candidates. Hyperlinks are off unless `include_cell_links=True`.
103
+ - **verbose**: all shapes (with width/height), charts, table candidates, and cell hyperlinks.
110
104
 
111
105
  ## Error Handling / Fallbacks
112
106
 
@@ -135,7 +129,6 @@ To show how well exstruct can structure Excel, we parse a workbook that combines
135
129
  (Screenshot below is the actual sample Excel sheet)
136
130
  ![Sample Excel](/docs/assets/demo_sheet.png)
137
131
  Sample workbook: `sample/sample.xlsx`
138
- Sample workbook: `sample/sample.xlsx`
139
132
 
140
133
  ### 1. Input: Excel Sheet Overview
141
134
 
@@ -322,12 +315,6 @@ In short, **exstruct = “an engine that converts Excel into a format AI can und
322
315
  - Default JSON is compact to reduce tokens; use `--pretty` or `pretty=True` when readability matters.
323
316
  - Field `table_candidates` replaces `tables`; adjust downstream consumers accordingly.
324
317
 
325
- ## Print Areas (PrintArea / PrintAreaView)
326
-
327
- - `SheetData.print_areas` holds print areas (cell coordinates) in light/standard/verbose.
328
- - Use `export_print_areas_as(...)` or CLI `--print-areas-dir` to write one file per print area (nothing is written if none exist).
329
- - `PrintAreaView` includes rows and table candidates inside the area, plus shapes/charts that overlap the area (size-less shapes are treated as points). `normalize=True` rebases row/col indices to the area origin.
330
-
331
318
  ## License
332
319
 
333
320
  BSD-3-Clause. See `LICENSE` for details.
@@ -335,3 +322,18 @@ BSD-3-Clause. See `LICENSE` for details.
335
322
  ## Documentation
336
323
 
337
324
  - API Reference (GitHub Pages): https://harumiweb.github.io/exstruct/
325
+ # Engine option cheat sheet
326
+
327
+ | Option class | Field | Meaning |
328
+ | -------------- | ------------------- | ------- |
329
+ | StructOptions | mode | "light"/"standard"/"verbose" |
330
+ | | table_params | Dict passed to `set_table_detection_params` (table_score_threshold, density_min, coverage_min, min_nonempty_cells) |
331
+ | | include_cell_links | Include cell hyperlinks in `rows[*].links` (None -> auto: verbose=True, others=False) |
332
+ | OutputOptions | fmt | Default format ("json"/"yaml"/"yml"/"toon") |
333
+ | | pretty / indent | Pretty-print JSON and control indent |
334
+ | | include_rows | Include rows (False to drop) |
335
+ | | include_shapes | Include shapes |
336
+ | | include_charts | Include charts |
337
+ | | include_tables | Include table_candidates |
338
+ | | sheets_dir | Optional directory for per-sheet exports |
339
+ | | stream | Default stream when output_path is None |
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "exstruct"
3
- version = "0.2.3"
3
+ version = "0.2.21"
4
4
  description = "Excel to structured JSON (tables, shapes, charts) for LLM/RAG pipelines"
5
5
  readme = "README.md"
6
6
  license = { file = "LICENSE" }
@@ -0,0 +1,122 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Literal, Optional, TextIO
5
+
6
+ from .core.integrate import extract_workbook
7
+ from .core.cells import set_table_detection_params
8
+ from .engine import ExStructEngine, OutputOptions, StructOptions
9
+ from .io import save_as_json, save_as_toon, save_as_yaml, save_sheets, serialize_workbook
10
+ from .models import CellRow, Chart, ChartSeries, Shape, SheetData, WorkbookData
11
+ from .render import export_pdf, export_sheet_images
12
+
13
+ __all__ = [
14
+ "extract",
15
+ "export",
16
+ "export_sheets",
17
+ "export_pdf",
18
+ "export_sheet_images",
19
+ "process_excel",
20
+ "ExtractionMode",
21
+ "CellRow",
22
+ "Shape",
23
+ "ChartSeries",
24
+ "Chart",
25
+ "SheetData",
26
+ "WorkbookData",
27
+ "set_table_detection_params",
28
+ "ExStructEngine",
29
+ "StructOptions",
30
+ "OutputOptions",
31
+ ]
32
+
33
+
34
+ ExtractionMode = Literal["light", "standard", "verbose"]
35
+
36
+
37
+ def extract(file_path: str | Path, mode: ExtractionMode = "standard") -> WorkbookData:
38
+ """Extract workbook semantic structure and return WorkbookData."""
39
+ include_links = True if mode == "verbose" else False
40
+ engine = ExStructEngine(options=StructOptions(mode=mode, include_cell_links=include_links))
41
+ return engine.extract(file_path, mode=mode)
42
+
43
+
44
+ def export(
45
+ data: WorkbookData,
46
+ path: str | Path,
47
+ fmt: Optional[Literal["json", "yaml", "yml", "toon"]] = None,
48
+ *,
49
+ pretty: bool = False,
50
+ indent: int | None = None,
51
+ ) -> None:
52
+ """Export WorkbookData to supported file formats (json/yaml/toon)."""
53
+ dest = Path(path)
54
+ format_hint = (fmt or dest.suffix.lstrip(".") or "json").lower()
55
+ match format_hint:
56
+ case "json":
57
+ save_as_json(data, dest, pretty=pretty, indent=indent)
58
+ case "yaml" | "yml":
59
+ save_as_yaml(data, dest)
60
+ case "toon":
61
+ save_as_toon(data, dest)
62
+ case _:
63
+ raise ValueError(f"Unsupported export format: {format_hint}")
64
+
65
+
66
+ def export_sheets(data: WorkbookData, dir_path: str | Path) -> dict[str, Path]:
67
+ """
68
+ Export each sheet as a JSON file (book_name + SheetData) into a directory.
69
+ Returns a mapping of sheet name to written path.
70
+ """
71
+ return save_sheets(data, Path(dir_path), fmt="json")
72
+
73
+
74
+ def export_sheets_as(
75
+ data: WorkbookData,
76
+ dir_path: str | Path,
77
+ fmt: Literal["json", "yaml", "yml", "toon"] = "json",
78
+ *,
79
+ pretty: bool = False,
80
+ indent: int | None = None,
81
+ ) -> dict[str, Path]:
82
+ """
83
+ Export each sheet in the given format (json/yaml/toon), including book_name and SheetData; returns sheet name → path map.
84
+ """
85
+ return save_sheets(data, Path(dir_path), fmt=fmt, pretty=pretty, indent=indent)
86
+
87
+
88
+ def process_excel(
89
+ file_path: Path,
90
+ output_path: Path | None = None,
91
+ out_fmt: str = "json",
92
+ image: bool = False,
93
+ pdf: bool = False,
94
+ dpi: int = 72,
95
+ mode: ExtractionMode = "standard",
96
+ pretty: bool = False,
97
+ indent: int | None = None,
98
+ sheets_dir: Path | None = None,
99
+ stream: TextIO | None = None,
100
+ ) -> None:
101
+ """
102
+ Convenience wrapper for CLI: export workbook and optionally PDF/PNG images (Excel required for rendering).
103
+ - If output_path is None, writes the serialized workbook to stdout (or provided stream).
104
+ - If sheets_dir is given, also writes per-sheet files into that directory.
105
+ """
106
+ engine = ExStructEngine(
107
+ options=StructOptions(mode=mode),
108
+ output=OutputOptions(fmt=out_fmt, pretty=pretty, indent=indent, sheets_dir=sheets_dir, stream=stream),
109
+ )
110
+ engine.process(
111
+ file_path=file_path,
112
+ output_path=output_path,
113
+ out_fmt=out_fmt,
114
+ image=image,
115
+ pdf=pdf,
116
+ dpi=dpi,
117
+ mode=mode,
118
+ pretty=pretty,
119
+ indent=indent,
120
+ sheets_dir=sheets_dir,
121
+ stream=stream,
122
+ )
@@ -57,11 +57,6 @@ def build_parser() -> argparse.ArgumentParser:
57
57
  type=Path,
58
58
  help="Optional directory to write one file per sheet (format follows --format).",
59
59
  )
60
- parser.add_argument(
61
- "--print-areas-dir",
62
- type=Path,
63
- help="Optional directory to write one file per print area (format follows --format).",
64
- )
65
60
  return parser
66
61
 
67
62
 
@@ -85,7 +80,6 @@ def main(argv: list[str] | None = None) -> int:
85
80
  mode=args.mode,
86
81
  pretty=args.pretty,
87
82
  sheets_dir=args.sheets_dir,
88
- print_areas_dir=args.print_areas_dir,
89
83
  )
90
84
  return 0
91
85
  except Exception as e:
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import logging
4
- from typing import Dict, List, Optional, Literal
4
+ from typing import Dict, List, Optional
5
5
 
6
6
  import xlwings as xw
7
7
 
@@ -166,7 +166,7 @@ def parse_series_formula(formula: str) -> Optional[Dict[str, Optional[str]]]:
166
166
  }
167
167
 
168
168
 
169
- def get_charts(sheet: xw.Sheet, mode: Literal["light", "standard", "verbose"] = "standard") -> List[Chart]:
169
+ def get_charts(sheet: xw.Sheet) -> List[Chart]:
170
170
  """Parse charts in a sheet into Chart models; failed charts carry an error field."""
171
171
  charts: List[Chart] = []
172
172
  for ch in sheet.charts:
@@ -182,14 +182,6 @@ def get_charts(sheet: xw.Sheet, mode: Literal["light", "standard", "verbose"] =
182
182
  chart_type_label = XL_CHART_TYPE_MAP.get(
183
183
  chart_type_num, f"unknown_{chart_type_num}"
184
184
  )
185
- chart_width: Optional[int] = None
186
- chart_height: Optional[int] = None
187
- try:
188
- chart_width = int(ch.width)
189
- chart_height = int(ch.height)
190
- except Exception:
191
- chart_width = None
192
- chart_height = None
193
185
 
194
186
  for s in chart_com.SeriesCollection():
195
187
  parsed = parse_series_formula(getattr(s, "Formula", ""))
@@ -228,8 +220,6 @@ def get_charts(sheet: xw.Sheet, mode: Literal["light", "standard", "verbose"] =
228
220
  title=title,
229
221
  y_axis_title=y_axis_title,
230
222
  y_axis_range=y_axis_range, # type: ignore
231
- w=chart_width,
232
- h=chart_height,
233
223
  series=series_list,
234
224
  l=int(ch.left),
235
225
  t=int(ch.top),
@@ -0,0 +1,140 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Dict, List, Literal
5
+
6
+ import logging
7
+ import os
8
+
9
+ import xlwings as xw
10
+
11
+ from ..models import CellRow, Shape, SheetData, WorkbookData
12
+ from .cells import (
13
+ detect_tables,
14
+ detect_tables_openpyxl,
15
+ extract_sheet_cells,
16
+ extract_sheet_cells_with_links,
17
+ )
18
+ from .charts import get_charts
19
+ from .shapes import get_shapes_with_position
20
+
21
+ logger = logging.getLogger(__name__)
22
+ _ALLOWED_MODES: set[str] = {"light", "standard", "verbose"}
23
+
24
+
25
+ def _find_open_workbook(file_path: Path) -> xw.Book | None:
26
+ """Return an existing workbook if already open in Excel; otherwise None."""
27
+ try:
28
+ for app in xw.apps:
29
+ for wb in app.books:
30
+ try:
31
+ if Path(wb.fullname).resolve() == file_path.resolve():
32
+ return wb
33
+ except Exception:
34
+ continue
35
+ except Exception:
36
+ return None
37
+ return None
38
+
39
+
40
+ def _open_workbook(file_path: Path) -> tuple[xw.Book, bool]:
41
+ """
42
+ Open workbook:
43
+ - If already open, reuse and do not close Excel on exit.
44
+ - Otherwise create invisible Excel (visible=False) and close when done.
45
+ Returns (workbook, should_close_app).
46
+ """
47
+ existing = _find_open_workbook(file_path)
48
+ if existing:
49
+ return existing, False
50
+ app = xw.App(add_book=False, visible=False)
51
+ wb = app.books.open(str(file_path))
52
+ return wb, True
53
+
54
+
55
+ def integrate_sheet_content(
56
+ cell_data: Dict[str, List[CellRow]],
57
+ shape_data: Dict[str, List[Shape]],
58
+ workbook: xw.Book,
59
+ mode: Literal["light", "standard", "verbose"] = "standard",
60
+ ) -> Dict[str, SheetData]:
61
+ """Integrate cells, shapes, charts, and tables into SheetData per sheet."""
62
+ result: Dict[str, SheetData] = {}
63
+ for sheet_name, rows in cell_data.items():
64
+ sheet_shapes = shape_data.get(sheet_name, [])
65
+ sheet = workbook.sheets[sheet_name]
66
+
67
+ sheet_model = SheetData(
68
+ rows=rows,
69
+ shapes=sheet_shapes,
70
+ charts=[] if mode == "light" else get_charts(sheet),
71
+ table_candidates=detect_tables(sheet),
72
+ )
73
+
74
+ result[sheet_name] = sheet_model
75
+ return result
76
+
77
+
78
+ def extract_workbook(
79
+ file_path: Path,
80
+ mode: Literal["light", "standard", "verbose"] = "standard",
81
+ *,
82
+ include_cell_links: bool = False,
83
+ ) -> WorkbookData:
84
+ """Extract workbook and return WorkbookData; fallback to cells+tables if Excel COM is unavailable."""
85
+ if mode not in _ALLOWED_MODES:
86
+ raise ValueError(f"Unsupported mode: {mode}")
87
+
88
+ cell_data = extract_sheet_cells_with_links(file_path) if include_cell_links else extract_sheet_cells(file_path)
89
+
90
+ def _cells_and_tables_only(reason: str) -> WorkbookData:
91
+ sheets: Dict[str, SheetData] = {}
92
+ for sheet_name, rows in cell_data.items():
93
+ try:
94
+ tables = detect_tables_openpyxl(file_path, sheet_name)
95
+ except Exception:
96
+ tables = []
97
+ sheets[sheet_name] = SheetData(
98
+ rows=rows,
99
+ shapes=[],
100
+ charts=[],
101
+ table_candidates=tables,
102
+ )
103
+ logger.warning(
104
+ "%s Falling back to cells+tables only; shapes and charts will be empty.",
105
+ reason,
106
+ )
107
+ return WorkbookData(book_name=file_path.name, sheets=sheets)
108
+
109
+ if mode == "light":
110
+ return _cells_and_tables_only("Light mode selected.")
111
+
112
+ if os.getenv("SKIP_COM_TESTS"):
113
+ return _cells_and_tables_only("SKIP_COM_TESTS is set; skipping COM/xlwings access.")
114
+
115
+ try:
116
+ wb, close_app = _open_workbook(file_path)
117
+ except Exception as e:
118
+ return _cells_and_tables_only(
119
+ f"xlwings/Excel COM is unavailable. ({e!r})"
120
+ )
121
+
122
+ try:
123
+ try:
124
+ shape_data = get_shapes_with_position(wb, mode=mode)
125
+ merged = integrate_sheet_content(cell_data, shape_data, wb, mode=mode)
126
+ return WorkbookData(book_name=file_path.name, sheets=merged)
127
+ except Exception as e:
128
+ logger.warning("Shape extraction failed; falling back to cells+tables. (%r)", e)
129
+ return _cells_and_tables_only(
130
+ f"Shape extraction failed ({e!r})."
131
+ )
132
+ finally:
133
+ # Close only if we created the app to avoid shutting user sessions.
134
+ try:
135
+ if close_app:
136
+ app = wb.app
137
+ wb.close()
138
+ app.quit()
139
+ except Exception:
140
+ pass