exstruct 0.2.61__tar.gz → 0.2.62__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.61
3
+ Version: 0.2.62
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
@@ -55,7 +55,7 @@ Description-Content-Type: text/markdown
55
55
 
56
56
  # ExStruct — Excel Structured Extraction Engine
57
57
 
58
- [![PyPI version](https://badge.fury.io/py/exstruct.svg)](https://pypi.org/project/exstruct/) [![PyPI Downloads](https://static.pepy.tech/personalized-badge/exstruct?period=total&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=downloads)](https://pepy.tech/projects/exstruct) ![Licence: BSD-3-Clause](https://img.shields.io/badge/license-BSD--3--Clause-blue?style=flat-square) [![pytest](https://github.com/harumiWeb/exstruct/actions/workflows/pytest.yml/badge.svg)](https://github.com/harumiWeb/exstruct/actions/workflows/pytest.yml)
58
+ [![PyPI version](https://badge.fury.io/py/exstruct.svg)](https://pypi.org/project/exstruct/) [![PyPI Downloads](https://static.pepy.tech/personalized-badge/exstruct?period=total&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=downloads)](https://pepy.tech/projects/exstruct) ![Licence: BSD-3-Clause](https://img.shields.io/badge/license-BSD--3--Clause-blue?style=flat-square) [![pytest](https://github.com/harumiWeb/exstruct/actions/workflows/pytest.yml/badge.svg)](https://github.com/harumiWeb/exstruct/actions/workflows/pytest.yml) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/e081cb4f634e4175b259eb7c34f54f60)](https://app.codacy.com/gh/harumiWeb/exstruct/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade)
59
59
 
60
60
  ![ExStruct Image](/docs/assets/icon.webp)
61
61
 
@@ -1,6 +1,6 @@
1
1
  # ExStruct — Excel Structured Extraction Engine
2
2
 
3
- [![PyPI version](https://badge.fury.io/py/exstruct.svg)](https://pypi.org/project/exstruct/) [![PyPI Downloads](https://static.pepy.tech/personalized-badge/exstruct?period=total&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=downloads)](https://pepy.tech/projects/exstruct) ![Licence: BSD-3-Clause](https://img.shields.io/badge/license-BSD--3--Clause-blue?style=flat-square) [![pytest](https://github.com/harumiWeb/exstruct/actions/workflows/pytest.yml/badge.svg)](https://github.com/harumiWeb/exstruct/actions/workflows/pytest.yml)
3
+ [![PyPI version](https://badge.fury.io/py/exstruct.svg)](https://pypi.org/project/exstruct/) [![PyPI Downloads](https://static.pepy.tech/personalized-badge/exstruct?period=total&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=downloads)](https://pepy.tech/projects/exstruct) ![Licence: BSD-3-Clause](https://img.shields.io/badge/license-BSD--3--Clause-blue?style=flat-square) [![pytest](https://github.com/harumiWeb/exstruct/actions/workflows/pytest.yml/badge.svg)](https://github.com/harumiWeb/exstruct/actions/workflows/pytest.yml) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/e081cb4f634e4175b259eb7c34f54f60)](https://app.codacy.com/gh/harumiWeb/exstruct/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade)
4
4
 
5
5
  ![ExStruct Image](/docs/assets/icon.webp)
6
6
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "exstruct"
3
- version = "0.2.61"
3
+ version = "0.2.62"
4
4
  description = "Excel to structured JSON (tables, shapes, charts) for LLM/RAG pipelines"
5
5
  readme = "README.md"
6
6
  license = { file = "LICENSE" }
@@ -25,6 +25,7 @@ build-backend = "uv_build"
25
25
  [dependency-groups]
26
26
  dev = [
27
27
  "mkdocs-material>=9.7.0",
28
+ "mkdocstrings-python>=2.0.1",
28
29
  "mypy>=1.19.0",
29
30
  "pre-commit>=4.5.0",
30
31
  "pytest>=9.0.1",
@@ -94,6 +94,20 @@ def extract(file_path: str | Path, mode: ExtractionMode = "standard") -> Workboo
94
94
  - light: cells + table detection only (no COM, shapes/charts empty). Print areas via openpyxl.
95
95
  - standard: texted shapes + arrows + charts (COM if available), print areas included. Shape/chart size is kept but hidden by default in output.
96
96
  - verbose: all shapes (including textless) with size, charts with size.
97
+
98
+ Returns:
99
+ WorkbookData containing sheets, rows, shapes, charts, and print areas.
100
+
101
+ Raises:
102
+ ValueError: If an invalid mode is provided.
103
+
104
+ Examples:
105
+ Extract with hyperlinks (verbose) and inspect table candidates:
106
+
107
+ >>> from exstruct import extract
108
+ >>> wb = extract("input.xlsx", mode="verbose")
109
+ >>> wb.sheets["Sheet1"].table_candidates
110
+ ['A1:B5']
97
111
  """
98
112
  include_links = True if mode == "verbose" else False
99
113
  engine = ExStructEngine(
@@ -119,6 +133,17 @@ def export(
119
133
  fmt: explicitly set format if desired (json/yaml/yml/toon)
120
134
  pretty: pretty-print JSON
121
135
  indent: JSON indent width (defaults to 2 when pretty=True and indent is None)
136
+
137
+ Raises:
138
+ ValueError: If the format is unsupported.
139
+
140
+ Examples:
141
+ Write pretty JSON and YAML (requires pyyaml):
142
+
143
+ >>> from exstruct import export, extract
144
+ >>> wb = extract("input.xlsx")
145
+ >>> export(wb, "out.json", pretty=True)
146
+ >>> export(wb, "out.yaml", fmt="yaml") # doctest: +SKIP
122
147
  """
123
148
  dest = Path(path)
124
149
  format_hint = (fmt or dest.suffix.lstrip(".") or "json").lower()
@@ -139,6 +164,20 @@ def export_sheets(data: WorkbookData, dir_path: str | Path) -> dict[str, Path]:
139
164
 
140
165
  - Payload: {book_name, sheet_name, sheet: SheetData}
141
166
  - Returns: {sheet_name: Path}
167
+
168
+ Args:
169
+ data: WorkbookData to split by sheet.
170
+ dir_path: Output directory.
171
+
172
+ Returns:
173
+ Mapping from sheet name to written JSON path.
174
+
175
+ Examples:
176
+ >>> from exstruct import export_sheets, extract
177
+ >>> wb = extract("input.xlsx")
178
+ >>> paths = export_sheets(wb, "out_sheets")
179
+ >>> "Sheet1" in paths
180
+ True
142
181
  """
143
182
  return save_sheets(data, Path(dir_path), fmt="json")
144
183
 
@@ -151,7 +190,29 @@ def export_sheets_as(
151
190
  pretty: bool = False,
152
191
  indent: int | None = None,
153
192
  ) -> dict[str, Path]:
154
- """Export each sheet in the given format (json/yaml/toon); returns sheet name to path map."""
193
+ """
194
+ Export each sheet in the given format (json/yaml/toon); returns sheet name to path map.
195
+
196
+ Args:
197
+ data: WorkbookData to split by sheet.
198
+ dir_path: Output directory.
199
+ fmt: Output format; inferred defaults to json.
200
+ pretty: Pretty-print JSON.
201
+ indent: JSON indent width (defaults to 2 when pretty=True and indent is None).
202
+
203
+ Returns:
204
+ Mapping from sheet name to written file path.
205
+
206
+ Raises:
207
+ ValueError: If an unsupported format is passed.
208
+
209
+ Examples:
210
+ Export per sheet as YAML (requires pyyaml):
211
+
212
+ >>> from exstruct import export_sheets_as, extract
213
+ >>> wb = extract("input.xlsx")
214
+ >>> _ = export_sheets_as(wb, "out_yaml", fmt="yaml") # doctest: +SKIP
215
+ """
155
216
  return save_sheets(data, Path(dir_path), fmt=fmt, pretty=pretty, indent=indent)
156
217
 
157
218
 
@@ -171,10 +232,21 @@ def export_print_areas_as(
171
232
  data: WorkbookData that contains print areas
172
233
  dir_path: output directory
173
234
  fmt: json/yaml/yml/toon
174
- pretty/indent: JSON formatting options
235
+ pretty: Pretty-print JSON output.
236
+ indent: JSON indent width (defaults to 2 when pretty is True and indent is None).
175
237
  normalize: rebase row/col indices to the print-area origin when True
238
+
176
239
  Returns:
177
240
  dict mapping area key to path (e.g., "Sheet1#1": /.../Sheet1_area1_...json)
241
+
242
+ Examples:
243
+ Export print areas when present:
244
+
245
+ >>> from exstruct import export_print_areas_as, extract
246
+ >>> wb = extract("input.xlsx", mode="standard")
247
+ >>> paths = export_print_areas_as(wb, "areas")
248
+ >>> isinstance(paths, dict)
249
+ True
178
250
  """
179
251
  return save_print_area_views(
180
252
  data,
@@ -202,8 +274,23 @@ def export_auto_page_breaks(
202
274
  data: WorkbookData containing auto_print_areas (COM extraction with auto breaks enabled)
203
275
  dir_path: output directory
204
276
  fmt: json/yaml/yml/toon
205
- pretty/indent: JSON formatting options
277
+ pretty: Pretty-print JSON output.
278
+ indent: JSON indent width (defaults to 2 when pretty is True and indent is None).
206
279
  normalize: rebase row/col indices to the area origin when True
280
+
281
+ Returns:
282
+ dict mapping area key to path (e.g., "Sheet1#1": /.../Sheet1_auto_page1_...json)
283
+
284
+ Raises:
285
+ PrintAreaError: If no auto page-break areas are present.
286
+
287
+ Examples:
288
+ >>> from exstruct import export_auto_page_breaks, extract
289
+ >>> wb = extract("input.xlsx", mode="standard")
290
+ >>> try:
291
+ ... export_auto_page_breaks(wb, "auto_areas")
292
+ ... except PrintAreaError:
293
+ ... pass
207
294
  """
208
295
  if not any(sheet.auto_print_areas for sheet in data.sheets.values()):
209
296
  message = "No auto page-break areas found. Enable COM-based auto page breaks before exporting."
@@ -220,8 +307,8 @@ def export_auto_page_breaks(
220
307
 
221
308
 
222
309
  def process_excel(
223
- file_path: Path,
224
- output_path: Path | None = None,
310
+ file_path: str | Path,
311
+ output_path: str | Path | None = None,
225
312
  out_fmt: str = "json",
226
313
  image: bool = False,
227
314
  pdf: bool = False,
@@ -229,26 +316,44 @@ def process_excel(
229
316
  mode: ExtractionMode = "standard",
230
317
  pretty: bool = False,
231
318
  indent: int | None = None,
232
- sheets_dir: Path | None = None,
233
- print_areas_dir: Path | None = None,
234
- auto_page_breaks_dir: Path | None = None,
319
+ sheets_dir: str | Path | None = None,
320
+ print_areas_dir: str | Path | None = None,
321
+ auto_page_breaks_dir: str | Path | None = None,
235
322
  stream: TextIO | None = None,
236
323
  ) -> None:
237
324
  """
238
- Convenience wrapper: extract → serialize (file or stdout) → optional PDF/PNG.
325
+ Convenience wrapper: extract -> serialize (file or stdout) -> optional PDF/PNG.
239
326
 
240
327
  Args:
241
- file_path: input Excel
242
- output_path: None for stdout; otherwise, write to file
243
- out_fmt: json/yaml/yml/toon
244
- image/pdf: True to also output PNG/PDF (requires Excel + pypdfium2)
245
- dpi: DPI for image output
246
- mode: light/standard/verbose (same meaning as `extract`)
247
- pretty/indent: JSON formatting
248
- sheets_dir: directory to write per-sheet files
249
- print_areas_dir: directory to write per-print-area files
250
- auto_page_breaks_dir: directory to write per-auto-page-break files (COM only)
251
- stream: IO override when output_path is None
328
+ file_path: Input Excel workbook (path string or Path).
329
+ output_path: None for stdout; otherwise, write to file (string or Path).
330
+ out_fmt: json/yaml/yml/toon.
331
+ image: True to also output PNGs (requires Excel + COM + pypdfium2).
332
+ pdf: True to also output PDF (requires Excel + COM + pypdfium2).
333
+ dpi: DPI for image output.
334
+ mode: light/standard/verbose (same meaning as `extract`).
335
+ pretty: Pretty-print JSON.
336
+ indent: JSON indent width.
337
+ sheets_dir: Directory to write per-sheet files (string or Path).
338
+ print_areas_dir: Directory to write per-print-area files (string or Path).
339
+ auto_page_breaks_dir: Directory to write per-auto-page-break files (COM only).
340
+ stream: IO override when output_path is None.
341
+
342
+ Raises:
343
+ ValueError: If an unsupported format or mode is given.
344
+ PrintAreaError: When exporting auto page breaks without available data.
345
+ RenderError: When rendering fails (Excel/COM/pypdfium2 issues).
346
+
347
+ Examples:
348
+ Extract and write JSON to stdout, plus per-sheet files:
349
+
350
+ >>> from pathlib import Path
351
+ >>> from exstruct import process_excel
352
+ >>> process_excel(Path("input.xlsx"), output_path=None, sheets_dir=Path("sheets"))
353
+
354
+ Render PDF only (COM + Excel required):
355
+
356
+ >>> process_excel(Path("input.xlsx"), output_path=Path("out.json"), pdf=True) # doctest: +SKIP
252
357
  """
253
358
  engine = ExStructEngine(
254
359
  options=StructOptions(mode=mode),
@@ -1,97 +1,97 @@
1
- from __future__ import annotations
2
-
3
- import argparse
4
- from pathlib import Path
5
-
6
- from exstruct import process_excel
7
-
8
-
9
- def build_parser() -> argparse.ArgumentParser:
10
- parser = argparse.ArgumentParser(
11
- description="Dev-only CLI stub for ExStruct extraction."
12
- )
13
- parser.add_argument("input", type=Path, help="Excel file (.xlsx/.xlsm/.xls)")
14
- parser.add_argument(
15
- "-o",
16
- "--output",
17
- type=Path,
18
- help="Output path. If omitted, writes to stdout.",
19
- )
20
- parser.add_argument(
21
- "-f",
22
- "--format",
23
- default="json",
24
- choices=["json", "yaml", "yml", "toon"],
25
- help="Export format",
26
- )
27
- parser.add_argument(
28
- "--image",
29
- action="store_true",
30
- help="(placeholder) Render PNG alongside JSON",
31
- )
32
- parser.add_argument(
33
- "--pdf",
34
- action="store_true",
35
- help="(placeholder) Render PDF alongside JSON",
36
- )
37
- parser.add_argument(
38
- "--dpi",
39
- type=int,
40
- default=144,
41
- help="DPI for image rendering (placeholder)",
42
- )
43
- parser.add_argument(
44
- "-m",
45
- "--mode",
46
- default="standard",
47
- choices=["light", "standard", "verbose"],
48
- help="Extraction detail level",
49
- )
50
- parser.add_argument(
51
- "--pretty",
52
- action="store_true",
53
- help="Pretty-print JSON output (indent=2). Default is compact JSON.",
54
- )
55
- parser.add_argument(
56
- "--sheets-dir",
57
- type=Path,
58
- help="Optional directory to write one file per sheet (format follows --format).",
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
- return parser
66
-
67
-
68
- def main(argv: list[str] | None = None) -> int:
69
- parser = build_parser()
70
- args = parser.parse_args(argv)
71
-
72
- input_path: Path = args.input
73
- if not input_path.exists():
74
- print(f"File not found: {input_path}")
75
- return 0
76
-
77
- try:
78
- process_excel(
79
- file_path=input_path,
80
- output_path=args.output,
81
- out_fmt=args.format,
82
- image=args.image,
83
- pdf=args.pdf,
84
- dpi=args.dpi,
85
- mode=args.mode,
86
- pretty=args.pretty,
87
- sheets_dir=args.sheets_dir,
88
- print_areas_dir=args.print_areas_dir,
89
- )
90
- return 0
91
- except Exception as e:
92
- print(f"Error: {e}")
93
- return 1
94
-
95
-
96
- if __name__ == "__main__":
97
- raise SystemExit(main())
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ from pathlib import Path
5
+
6
+ from exstruct import process_excel
7
+
8
+
9
+ def build_parser() -> argparse.ArgumentParser:
10
+ parser = argparse.ArgumentParser(
11
+ description="Dev-only CLI stub for ExStruct extraction."
12
+ )
13
+ parser.add_argument("input", type=Path, help="Excel file (.xlsx/.xlsm/.xls)")
14
+ parser.add_argument(
15
+ "-o",
16
+ "--output",
17
+ type=Path,
18
+ help="Output path. If omitted, writes to stdout.",
19
+ )
20
+ parser.add_argument(
21
+ "-f",
22
+ "--format",
23
+ default="json",
24
+ choices=["json", "yaml", "yml", "toon"],
25
+ help="Export format",
26
+ )
27
+ parser.add_argument(
28
+ "--image",
29
+ action="store_true",
30
+ help="(placeholder) Render PNG alongside JSON",
31
+ )
32
+ parser.add_argument(
33
+ "--pdf",
34
+ action="store_true",
35
+ help="(placeholder) Render PDF alongside JSON",
36
+ )
37
+ parser.add_argument(
38
+ "--dpi",
39
+ type=int,
40
+ default=144,
41
+ help="DPI for image rendering (placeholder)",
42
+ )
43
+ parser.add_argument(
44
+ "-m",
45
+ "--mode",
46
+ default="standard",
47
+ choices=["light", "standard", "verbose"],
48
+ help="Extraction detail level",
49
+ )
50
+ parser.add_argument(
51
+ "--pretty",
52
+ action="store_true",
53
+ help="Pretty-print JSON output (indent=2). Default is compact JSON.",
54
+ )
55
+ parser.add_argument(
56
+ "--sheets-dir",
57
+ type=Path,
58
+ help="Optional directory to write one file per sheet (format follows --format).",
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
+ return parser
66
+
67
+
68
+ def main(argv: list[str] | None = None) -> int:
69
+ parser = build_parser()
70
+ args = parser.parse_args(argv)
71
+
72
+ input_path: Path = args.input
73
+ if not input_path.exists():
74
+ print(f"File not found: {input_path}", flush=True)
75
+ return 0
76
+
77
+ try:
78
+ process_excel(
79
+ file_path=input_path,
80
+ output_path=args.output,
81
+ out_fmt=args.format,
82
+ image=args.image,
83
+ pdf=args.pdf,
84
+ dpi=args.dpi,
85
+ mode=args.mode,
86
+ pretty=args.pretty,
87
+ sheets_dir=args.sheets_dir,
88
+ print_areas_dir=args.print_areas_dir,
89
+ )
90
+ return 0
91
+ except Exception as e:
92
+ print(f"Error: {e}", flush=True)
93
+ return 1
94
+
95
+
96
+ if __name__ == "__main__":
97
+ raise SystemExit(main())
@@ -288,7 +288,7 @@ def integrate_sheet_content(
288
288
 
289
289
 
290
290
  def extract_workbook( # noqa: C901
291
- file_path: Path,
291
+ file_path: str | Path,
292
292
  mode: Literal["light", "standard", "verbose"] = "standard",
293
293
  *,
294
294
  include_cell_links: bool = False,
@@ -299,21 +299,23 @@ def extract_workbook( # noqa: C901
299
299
  if mode not in _ALLOWED_MODES:
300
300
  raise ValueError(f"Unsupported mode: {mode}")
301
301
 
302
+ normalized_file_path = file_path if isinstance(file_path, Path) else Path(file_path)
303
+
302
304
  cell_data = (
303
- extract_sheet_cells_with_links(file_path)
305
+ extract_sheet_cells_with_links(normalized_file_path)
304
306
  if include_cell_links
305
- else extract_sheet_cells(file_path)
307
+ else extract_sheet_cells(normalized_file_path)
306
308
  )
307
309
  print_area_data: dict[str, list[PrintArea]] = {}
308
310
  if include_print_areas:
309
- print_area_data = _extract_print_areas_openpyxl(file_path)
311
+ print_area_data = _extract_print_areas_openpyxl(normalized_file_path)
310
312
  auto_page_break_data: dict[str, list[PrintArea]] = {}
311
313
 
312
314
  def _cells_and_tables_only(reason: str) -> WorkbookData:
313
315
  sheets: dict[str, SheetData] = {}
314
316
  for sheet_name, rows in cell_data.items():
315
317
  try:
316
- tables = detect_tables_openpyxl(file_path, sheet_name)
318
+ tables = detect_tables_openpyxl(normalized_file_path, sheet_name)
317
319
  except Exception:
318
320
  tables = []
319
321
  sheets[sheet_name] = SheetData(
@@ -330,7 +332,7 @@ def extract_workbook( # noqa: C901
330
332
  "%s Falling back to cells+tables only; shapes and charts will be empty.",
331
333
  reason,
332
334
  )
333
- return WorkbookData(book_name=file_path.name, sheets=sheets)
335
+ return WorkbookData(book_name=normalized_file_path.name, sheets=sheets)
334
336
 
335
337
  if mode == "light":
336
338
  return _cells_and_tables_only("Light mode selected.")
@@ -341,7 +343,7 @@ def extract_workbook( # noqa: C901
341
343
  )
342
344
 
343
345
  try:
344
- wb, close_app = _open_workbook(file_path)
346
+ wb, close_app = _open_workbook(normalized_file_path)
345
347
  except Exception as e:
346
348
  return _cells_and_tables_only(f"xlwings/Excel COM is unavailable. ({e!r})")
347
349
 
@@ -369,7 +371,7 @@ def extract_workbook( # noqa: C901
369
371
  if include_auto_page_breaks
370
372
  else None,
371
373
  )
372
- return WorkbookData(book_name=file_path.name, sheets=merged)
374
+ return WorkbookData(book_name=normalized_file_path.name, sheets=merged)
373
375
  except Exception as e:
374
376
  logger.warning(
375
377
  "Shape extraction failed; falling back to cells+tables. (%r)", e