exstruct 0.3.5__tar.gz → 0.3.6__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 (31) hide show
  1. {exstruct-0.3.5 → exstruct-0.3.6}/PKG-INFO +1 -1
  2. {exstruct-0.3.5 → exstruct-0.3.6}/pyproject.toml +1 -1
  3. exstruct-0.3.6/src/exstruct/render/__init__.py +239 -0
  4. exstruct-0.3.5/src/exstruct/render/__init__.py +0 -114
  5. {exstruct-0.3.5 → exstruct-0.3.6}/LICENSE +0 -0
  6. {exstruct-0.3.5 → exstruct-0.3.6}/README.md +0 -0
  7. {exstruct-0.3.5 → exstruct-0.3.6}/src/exstruct/__init__.py +0 -0
  8. {exstruct-0.3.5 → exstruct-0.3.6}/src/exstruct/cli/availability.py +0 -0
  9. {exstruct-0.3.5 → exstruct-0.3.6}/src/exstruct/cli/main.py +0 -0
  10. {exstruct-0.3.5 → exstruct-0.3.6}/src/exstruct/core/__init__.py +0 -0
  11. {exstruct-0.3.5 → exstruct-0.3.6}/src/exstruct/core/backends/__init__.py +0 -0
  12. {exstruct-0.3.5 → exstruct-0.3.6}/src/exstruct/core/backends/base.py +0 -0
  13. {exstruct-0.3.5 → exstruct-0.3.6}/src/exstruct/core/backends/com_backend.py +0 -0
  14. {exstruct-0.3.5 → exstruct-0.3.6}/src/exstruct/core/backends/openpyxl_backend.py +0 -0
  15. {exstruct-0.3.5 → exstruct-0.3.6}/src/exstruct/core/cells.py +0 -0
  16. {exstruct-0.3.5 → exstruct-0.3.6}/src/exstruct/core/charts.py +0 -0
  17. {exstruct-0.3.5 → exstruct-0.3.6}/src/exstruct/core/integrate.py +0 -0
  18. {exstruct-0.3.5 → exstruct-0.3.6}/src/exstruct/core/logging_utils.py +0 -0
  19. {exstruct-0.3.5 → exstruct-0.3.6}/src/exstruct/core/modeling.py +0 -0
  20. {exstruct-0.3.5 → exstruct-0.3.6}/src/exstruct/core/pipeline.py +0 -0
  21. {exstruct-0.3.5 → exstruct-0.3.6}/src/exstruct/core/ranges.py +0 -0
  22. {exstruct-0.3.5 → exstruct-0.3.6}/src/exstruct/core/shapes.py +0 -0
  23. {exstruct-0.3.5 → exstruct-0.3.6}/src/exstruct/core/workbook.py +0 -0
  24. {exstruct-0.3.5 → exstruct-0.3.6}/src/exstruct/engine.py +0 -0
  25. {exstruct-0.3.5 → exstruct-0.3.6}/src/exstruct/errors.py +0 -0
  26. {exstruct-0.3.5 → exstruct-0.3.6}/src/exstruct/io/__init__.py +0 -0
  27. {exstruct-0.3.5 → exstruct-0.3.6}/src/exstruct/io/serialize.py +0 -0
  28. {exstruct-0.3.5 → exstruct-0.3.6}/src/exstruct/models/__init__.py +0 -0
  29. {exstruct-0.3.5 → exstruct-0.3.6}/src/exstruct/models/maps.py +0 -0
  30. {exstruct-0.3.5 → exstruct-0.3.6}/src/exstruct/models/types.py +0 -0
  31. {exstruct-0.3.5 → exstruct-0.3.6}/src/exstruct/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: exstruct
3
- Version: 0.3.5
3
+ Version: 0.3.6
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "exstruct"
3
- version = "0.3.5"
3
+ version = "0.3.6"
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,239 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import multiprocessing as mp
5
+ import os
6
+ from pathlib import Path
7
+ import shutil
8
+ import tempfile
9
+ from types import ModuleType
10
+ from typing import Any, cast
11
+
12
+ import xlwings as xw
13
+
14
+ from ..errors import MissingDependencyError, RenderError
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ def _require_excel_app() -> xw.App:
20
+ """Ensure Excel COM is available and return an App; otherwise raise."""
21
+ try:
22
+ app = xw.App(add_book=False, visible=False)
23
+ return app
24
+ except Exception as e:
25
+ raise RenderError(
26
+ "Excel (COM) is not available. Rendering (PDF/image) requires a desktop Excel installation."
27
+ ) from e
28
+
29
+
30
+ def export_pdf(excel_path: str | Path, output_pdf: str | Path) -> list[str]:
31
+ """Export an Excel workbook to PDF via Excel COM and return sheet names in order."""
32
+ normalized_excel_path = Path(excel_path)
33
+ normalized_output_pdf = Path(output_pdf)
34
+ normalized_output_pdf.parent.mkdir(parents=True, exist_ok=True)
35
+
36
+ with tempfile.TemporaryDirectory() as td:
37
+ temp_dir = Path(td)
38
+ temp_xlsx = temp_dir / "book.xlsx"
39
+ temp_pdf = temp_dir / "book.pdf"
40
+
41
+ app: xw.App | None = None
42
+ wb: xw.Book | None = None
43
+ try:
44
+ app = _require_excel_app()
45
+ app.display_alerts = False
46
+ wb = app.books.open(str(normalized_excel_path))
47
+ sheet_names = [s.name for s in wb.sheets]
48
+ wb.api.SaveAs(str(temp_xlsx))
49
+ wb.api.ExportAsFixedFormat(0, str(temp_pdf))
50
+ shutil.copy(temp_pdf, normalized_output_pdf)
51
+ except RenderError:
52
+ raise
53
+ except Exception as exc:
54
+ raise RenderError(
55
+ "Failed to export PDF for "
56
+ f"'{normalized_excel_path}' to '{normalized_output_pdf}'."
57
+ ) from exc
58
+ finally:
59
+ if wb is not None:
60
+ wb.close()
61
+ if app is not None:
62
+ app.quit()
63
+ if not normalized_output_pdf.exists():
64
+ raise RenderError(f"Failed to export PDF to '{normalized_output_pdf}'.")
65
+ return sheet_names
66
+
67
+
68
+ def _require_pdfium() -> ModuleType:
69
+ """Ensure pypdfium2 is installed; otherwise raise with guidance."""
70
+ try:
71
+ import pypdfium2 as pdfium
72
+ except ImportError as e:
73
+ raise MissingDependencyError(
74
+ "Image rendering requires pypdfium2. Install it via `pip install pypdfium2 pillow` or add the 'render' extra."
75
+ ) from e
76
+ return cast(ModuleType, pdfium)
77
+
78
+
79
+ def export_sheet_images(
80
+ excel_path: str | Path, output_dir: str | Path, dpi: int = 144
81
+ ) -> list[Path]:
82
+ """Export each sheet as PNG (via PDF then pypdfium2 rasterization) and return paths in sheet order."""
83
+ normalized_excel_path = Path(excel_path)
84
+ normalized_output_dir = Path(output_dir)
85
+ normalized_output_dir.mkdir(parents=True, exist_ok=True)
86
+ use_subprocess = _use_render_subprocess()
87
+ if not use_subprocess:
88
+ pdfium = cast(Any, _require_pdfium())
89
+ else:
90
+ _require_pdfium()
91
+
92
+ try:
93
+ with tempfile.TemporaryDirectory() as td:
94
+ written: list[Path] = []
95
+ app: xw.App | None = None
96
+ wb: xw.Book | None = None
97
+ try:
98
+ app = _require_excel_app()
99
+ wb = app.books.open(str(normalized_excel_path))
100
+ for sheet_index, sheet in enumerate(wb.sheets):
101
+ sheet_name = sheet.name
102
+ sheet_pdf = Path(td) / f"sheet_{sheet_index + 1:02d}.pdf"
103
+ sheet.api.ExportAsFixedFormat(0, str(sheet_pdf))
104
+ safe_name = _sanitize_sheet_filename(sheet_name)
105
+ if use_subprocess:
106
+ written.extend(
107
+ _render_pdf_pages_subprocess(
108
+ sheet_pdf,
109
+ normalized_output_dir,
110
+ sheet_index,
111
+ safe_name,
112
+ dpi,
113
+ )
114
+ )
115
+ else:
116
+ written.extend(
117
+ _render_pdf_pages_in_process(
118
+ pdfium,
119
+ sheet_pdf,
120
+ normalized_output_dir,
121
+ sheet_index,
122
+ safe_name,
123
+ dpi,
124
+ )
125
+ )
126
+ return written
127
+ finally:
128
+ if wb is not None:
129
+ wb.close()
130
+ if app is not None:
131
+ app.quit()
132
+ except RenderError:
133
+ raise
134
+ except Exception as exc:
135
+ raise RenderError(
136
+ f"Failed to export sheet images to '{normalized_output_dir}'."
137
+ ) from exc
138
+
139
+
140
+ def _sanitize_sheet_filename(name: str) -> str:
141
+ return "".join("_" if c in '\\/:*?"<>|' else c for c in name).strip() or "sheet"
142
+
143
+
144
+ def _use_render_subprocess() -> bool:
145
+ """Return True when PDF->PNG rendering should run in a subprocess."""
146
+ return os.getenv("EXSTRUCT_RENDER_SUBPROCESS", "1").lower() not in {"0", "false"}
147
+
148
+
149
+ def _render_pdf_pages_in_process(
150
+ pdfium: ModuleType,
151
+ pdf_path: Path,
152
+ output_dir: Path,
153
+ sheet_index: int,
154
+ safe_name: str,
155
+ dpi: int,
156
+ ) -> list[Path]:
157
+ """Render PDF pages to PNGs in the current process."""
158
+ scale = dpi / 72.0
159
+ written: list[Path] = []
160
+ with pdfium.PdfDocument(str(pdf_path)) as pdf:
161
+ for page_index in range(len(pdf)):
162
+ page = pdf[page_index]
163
+ bitmap = page.render(scale=scale)
164
+ pil_image = bitmap.to_pil()
165
+ page_suffix = f"_p{page_index + 1:02d}" if page_index > 0 else ""
166
+ img_path = (
167
+ output_dir / f"{sheet_index + 1:02d}_{safe_name}{page_suffix}.png"
168
+ )
169
+ pil_image.save(img_path, format="PNG", dpi=(dpi, dpi))
170
+ written.append(img_path)
171
+ return written
172
+
173
+
174
+ def _render_pdf_pages_subprocess(
175
+ pdf_path: Path,
176
+ output_dir: Path,
177
+ sheet_index: int,
178
+ safe_name: str,
179
+ dpi: int,
180
+ ) -> list[Path]:
181
+ """Render PDF pages to PNGs in a subprocess for memory isolation."""
182
+ ctx = mp.get_context("spawn")
183
+ queue: mp.Queue[dict[str, list[str] | str]] = ctx.Queue()
184
+ process = ctx.Process(
185
+ target=_render_pdf_pages_worker,
186
+ args=(pdf_path, output_dir, sheet_index, safe_name, dpi, queue),
187
+ )
188
+ process.start()
189
+ process.join()
190
+ result = _get_subprocess_result(queue)
191
+ if process.exitcode != 0 or "error" in result:
192
+ message = result.get("error", "subprocess failed")
193
+ raise RenderError(f"Failed to render PDF pages: {message}")
194
+ paths = result.get("paths", [])
195
+ return [Path(path) for path in paths]
196
+
197
+
198
+ def _get_subprocess_result(
199
+ queue: mp.Queue[dict[str, list[str] | str]],
200
+ ) -> dict[str, list[str] | str]:
201
+ """Fetch the worker result from the queue with a timeout."""
202
+ try:
203
+ return queue.get(timeout=5)
204
+ except Exception as exc:
205
+ return {"error": f"subprocess did not return results ({exc})"}
206
+
207
+
208
+ def _render_pdf_pages_worker(
209
+ pdf_path: Path,
210
+ output_dir: Path,
211
+ sheet_index: int,
212
+ safe_name: str,
213
+ dpi: int,
214
+ queue: mp.Queue[dict[str, list[str] | str]],
215
+ ) -> None:
216
+ """Worker process to render PDF pages into PNG files."""
217
+ try:
218
+ import pypdfium2 as pdfium
219
+
220
+ scale = dpi / 72.0
221
+ output_dir.mkdir(parents=True, exist_ok=True)
222
+ written: list[str] = []
223
+ with pdfium.PdfDocument(str(pdf_path)) as pdf:
224
+ for page_index in range(len(pdf)):
225
+ page = pdf[page_index]
226
+ bitmap = page.render(scale=scale)
227
+ pil_image = bitmap.to_pil()
228
+ page_suffix = f"_p{page_index + 1:02d}" if page_index > 0 else ""
229
+ img_path = (
230
+ output_dir / f"{sheet_index + 1:02d}_{safe_name}{page_suffix}.png"
231
+ )
232
+ pil_image.save(img_path, format="PNG", dpi=(dpi, dpi))
233
+ written.append(str(img_path))
234
+ queue.put({"paths": written})
235
+ except Exception as exc:
236
+ queue.put({"error": str(exc)})
237
+
238
+
239
+ __all__ = ["export_pdf", "export_sheet_images"]
@@ -1,114 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import logging
4
- from pathlib import Path
5
- import shutil
6
- import tempfile
7
- from types import ModuleType
8
- from typing import Any, cast
9
-
10
- import xlwings as xw
11
-
12
- from ..errors import MissingDependencyError, RenderError
13
-
14
- logger = logging.getLogger(__name__)
15
-
16
-
17
- def _require_excel_app() -> xw.App:
18
- """Ensure Excel COM is available and return an App; otherwise raise."""
19
- try:
20
- app = xw.App(add_book=False, visible=False)
21
- return app
22
- except Exception as e:
23
- raise RenderError(
24
- "Excel (COM) is not available. Rendering (PDF/image) requires a desktop Excel installation."
25
- ) from e
26
-
27
-
28
- def export_pdf(excel_path: str | Path, output_pdf: str | Path) -> list[str]:
29
- """Export an Excel workbook to PDF via Excel COM and return sheet names in order."""
30
- normalized_excel_path = Path(excel_path)
31
- normalized_output_pdf = Path(output_pdf)
32
- normalized_output_pdf.parent.mkdir(parents=True, exist_ok=True)
33
-
34
- with tempfile.TemporaryDirectory() as td:
35
- temp_dir = Path(td)
36
- temp_xlsx = temp_dir / "book.xlsx"
37
- temp_pdf = temp_dir / "book.pdf"
38
- shutil.copy(normalized_excel_path, temp_xlsx)
39
-
40
- app: xw.App | None = None
41
- wb: xw.Book | None = None
42
- try:
43
- app = _require_excel_app()
44
- wb = app.books.open(str(temp_xlsx))
45
- sheet_names = [s.name for s in wb.sheets]
46
- wb.api.ExportAsFixedFormat(0, str(temp_pdf))
47
- shutil.copy(temp_pdf, normalized_output_pdf)
48
- except RenderError:
49
- raise
50
- except Exception as exc:
51
- raise RenderError(
52
- "Failed to export PDF for "
53
- f"'{normalized_excel_path}' to '{normalized_output_pdf}'."
54
- ) from exc
55
- finally:
56
- if wb is not None:
57
- wb.close()
58
- if app is not None:
59
- app.quit()
60
- if not normalized_output_pdf.exists():
61
- raise RenderError(f"Failed to export PDF to '{normalized_output_pdf}'.")
62
- return sheet_names
63
-
64
-
65
- def _require_pdfium() -> ModuleType:
66
- """Ensure pypdfium2 is installed; otherwise raise with guidance."""
67
- try:
68
- import pypdfium2 as pdfium
69
- except ImportError as e:
70
- raise MissingDependencyError(
71
- "Image rendering requires pypdfium2. Install it via `pip install pypdfium2 pillow` or add the 'render' extra."
72
- ) from e
73
- return cast(ModuleType, pdfium)
74
-
75
-
76
- def export_sheet_images(
77
- excel_path: str | Path, output_dir: str | Path, dpi: int = 144
78
- ) -> list[Path]:
79
- """Export each sheet as PNG (via PDF then pypdfium2 rasterization) and return paths in sheet order."""
80
- pdfium = cast(Any, _require_pdfium())
81
- normalized_excel_path = Path(excel_path)
82
- normalized_output_dir = Path(output_dir)
83
- normalized_output_dir.mkdir(parents=True, exist_ok=True)
84
-
85
- try:
86
- with tempfile.TemporaryDirectory() as td:
87
- tmp_pdf = Path(td) / "book.pdf"
88
- sheet_names = export_pdf(normalized_excel_path, tmp_pdf)
89
-
90
- scale = dpi / 72.0
91
- written: list[Path] = []
92
- with pdfium.PdfDocument(str(tmp_pdf)) as pdf:
93
- for i, sheet_name in enumerate(sheet_names):
94
- page = pdf[i]
95
- bitmap = page.render(scale=scale)
96
- pil_image = bitmap.to_pil()
97
- safe_name = _sanitize_sheet_filename(sheet_name)
98
- img_path = normalized_output_dir / f"{i + 1:02d}_{safe_name}.png"
99
- pil_image.save(img_path, format="PNG", dpi=(dpi, dpi))
100
- written.append(img_path)
101
- return written
102
- except RenderError:
103
- raise
104
- except Exception as exc:
105
- raise RenderError(
106
- f"Failed to export sheet images to '{normalized_output_dir}'."
107
- ) from exc
108
-
109
-
110
- def _sanitize_sheet_filename(name: str) -> str:
111
- return "".join("_" if c in '\\/:*?"<>|' else c for c in name).strip() or "sheet"
112
-
113
-
114
- __all__ = ["export_pdf", "export_sheet_images"]
File without changes
File without changes
File without changes