exstruct 0.2.71__tar.gz → 0.2.80__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.
- {exstruct-0.2.71 → exstruct-0.2.80}/PKG-INFO +14 -1
- {exstruct-0.2.71 → exstruct-0.2.80}/README.md +13 -0
- {exstruct-0.2.71 → exstruct-0.2.80}/pyproject.toml +1 -1
- {exstruct-0.2.71 → exstruct-0.2.80}/src/exstruct/core/shapes.py +109 -67
- {exstruct-0.2.71 → exstruct-0.2.80}/src/exstruct/models/__init__.py +15 -0
- {exstruct-0.2.71 → exstruct-0.2.80}/LICENSE +0 -0
- {exstruct-0.2.71 → exstruct-0.2.80}/src/exstruct/__init__.py +0 -0
- {exstruct-0.2.71 → exstruct-0.2.80}/src/exstruct/cli/availability.py +0 -0
- {exstruct-0.2.71 → exstruct-0.2.80}/src/exstruct/cli/main.py +0 -0
- {exstruct-0.2.71 → exstruct-0.2.80}/src/exstruct/core/__init__.py +0 -0
- {exstruct-0.2.71 → exstruct-0.2.80}/src/exstruct/core/cells.py +0 -0
- {exstruct-0.2.71 → exstruct-0.2.80}/src/exstruct/core/charts.py +0 -0
- {exstruct-0.2.71 → exstruct-0.2.80}/src/exstruct/core/integrate.py +0 -0
- {exstruct-0.2.71 → exstruct-0.2.80}/src/exstruct/engine.py +0 -0
- {exstruct-0.2.71 → exstruct-0.2.80}/src/exstruct/errors.py +0 -0
- {exstruct-0.2.71 → exstruct-0.2.80}/src/exstruct/io/__init__.py +0 -0
- {exstruct-0.2.71 → exstruct-0.2.80}/src/exstruct/models/maps.py +0 -0
- {exstruct-0.2.71 → exstruct-0.2.80}/src/exstruct/models/types.py +0 -0
- {exstruct-0.2.71 → exstruct-0.2.80}/src/exstruct/py.typed +0 -0
- {exstruct-0.2.71 → exstruct-0.2.80}/src/exstruct/render/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: exstruct
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.80
|
|
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
|
|
@@ -405,6 +405,18 @@ In short, **exstruct = “an engine that converts Excel into a format AI can und
|
|
|
405
405
|
- Default JSON is compact to reduce tokens; use `--pretty` or `pretty=True` when readability matters.
|
|
406
406
|
- Field `table_candidates` replaces `tables`; adjust downstream consumers accordingly.
|
|
407
407
|
|
|
408
|
+
## Enterprise Use
|
|
409
|
+
|
|
410
|
+
ExStruct is used primarily as a **library**, not a service.
|
|
411
|
+
|
|
412
|
+
- No official support or SLA is provided
|
|
413
|
+
- Long-term stability is prioritized over rapid feature growth
|
|
414
|
+
- Forking and internal modification are expected in enterprise use
|
|
415
|
+
|
|
416
|
+
This project is suitable for teams that:
|
|
417
|
+
- need transparency over black-box tools
|
|
418
|
+
- are comfortable maintaining internal forks if necessary
|
|
419
|
+
|
|
408
420
|
## Print Areas and Auto Page Breaks (PrintArea / PrintAreaView)
|
|
409
421
|
|
|
410
422
|
- `SheetData.print_areas` holds print areas (cell coordinates) in light/standard/verbose.
|
|
@@ -420,3 +432,4 @@ BSD-3-Clause. See `LICENSE` for details.
|
|
|
420
432
|
## Documentation
|
|
421
433
|
|
|
422
434
|
- API Reference (GitHub Pages): https://harumiweb.github.io/exstruct/
|
|
435
|
+
- JSON Schemas: see `schemas/` (one file per model); regenerate via `python scripts/gen_json_schema.py`.
|
|
@@ -350,6 +350,18 @@ In short, **exstruct = “an engine that converts Excel into a format AI can und
|
|
|
350
350
|
- Default JSON is compact to reduce tokens; use `--pretty` or `pretty=True` when readability matters.
|
|
351
351
|
- Field `table_candidates` replaces `tables`; adjust downstream consumers accordingly.
|
|
352
352
|
|
|
353
|
+
## Enterprise Use
|
|
354
|
+
|
|
355
|
+
ExStruct is used primarily as a **library**, not a service.
|
|
356
|
+
|
|
357
|
+
- No official support or SLA is provided
|
|
358
|
+
- Long-term stability is prioritized over rapid feature growth
|
|
359
|
+
- Forking and internal modification are expected in enterprise use
|
|
360
|
+
|
|
361
|
+
This project is suitable for teams that:
|
|
362
|
+
- need transparency over black-box tools
|
|
363
|
+
- are comfortable maintaining internal forks if necessary
|
|
364
|
+
|
|
353
365
|
## Print Areas and Auto Page Breaks (PrintArea / PrintAreaView)
|
|
354
366
|
|
|
355
367
|
- `SheetData.print_areas` holds print areas (cell coordinates) in light/standard/verbose.
|
|
@@ -365,3 +377,4 @@ BSD-3-Clause. See `LICENSE` for details.
|
|
|
365
377
|
## Documentation
|
|
366
378
|
|
|
367
379
|
- API Reference (GitHub Pages): https://harumiweb.github.io/exstruct/
|
|
380
|
+
- JSON Schemas: see `schemas/` (one file per model); regenerate via `python scripts/gen_json_schema.py`.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from collections.abc import Iterator
|
|
4
|
-
import math
|
|
5
|
-
from typing import SupportsInt, cast
|
|
3
|
+
from collections.abc import Iterator
|
|
4
|
+
import math
|
|
5
|
+
from typing import SupportsInt, cast
|
|
6
6
|
|
|
7
7
|
import xlwings as xw
|
|
8
8
|
from xlwings import Book
|
|
@@ -23,54 +23,54 @@ def angle_to_compass(angle: float) -> str:
|
|
|
23
23
|
return dirs[idx]
|
|
24
24
|
|
|
25
25
|
|
|
26
|
-
def coord_to_cell_by_edges(
|
|
27
|
-
row_edges: list[float], col_edges: list[float], x: float, y: float
|
|
28
|
-
) -> str | None:
|
|
29
|
-
"""Estimate cell address from coordinates and cumulative edges; return None if out of range."""
|
|
30
|
-
|
|
31
|
-
def find_index(edges: list[float], pos: float) -> int | None:
|
|
32
|
-
for i in range(1, len(edges)):
|
|
33
|
-
if edges[i - 1] <= pos < edges[i]:
|
|
34
|
-
return i
|
|
35
|
-
return None
|
|
26
|
+
def coord_to_cell_by_edges(
|
|
27
|
+
row_edges: list[float], col_edges: list[float], x: float, y: float
|
|
28
|
+
) -> str | None:
|
|
29
|
+
"""Estimate cell address from coordinates and cumulative edges; return None if out of range."""
|
|
30
|
+
|
|
31
|
+
def find_index(edges: list[float], pos: float) -> int | None:
|
|
32
|
+
for i in range(1, len(edges)):
|
|
33
|
+
if edges[i - 1] <= pos < edges[i]:
|
|
34
|
+
return i
|
|
35
|
+
return None
|
|
36
36
|
|
|
37
37
|
r = find_index(row_edges, y)
|
|
38
38
|
c = find_index(col_edges, x)
|
|
39
39
|
if r is None or c is None:
|
|
40
40
|
return None
|
|
41
|
-
return f"{xw.utils.col_name(c)}{r}"
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def has_arrow(style_val: object) -> bool:
|
|
45
|
-
"""Return True if Excel arrow style value indicates an arrowhead."""
|
|
46
|
-
try:
|
|
47
|
-
v = int(cast(SupportsInt, style_val))
|
|
48
|
-
return v != 0
|
|
49
|
-
except Exception:
|
|
50
|
-
return False
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def iter_shapes_recursive(shp: xw.Shape) -> Iterator[xw.Shape]:
|
|
54
|
-
"""Yield shapes recursively, including group children."""
|
|
55
|
-
yield shp
|
|
56
|
-
try:
|
|
57
|
-
if shp.api.Type == 6:
|
|
58
|
-
items = shp.api.GroupItems
|
|
59
|
-
for i in range(1, items.Count + 1):
|
|
60
|
-
inner = items.Item(i)
|
|
61
|
-
try:
|
|
62
|
-
name = inner.Name
|
|
63
|
-
xl_shape = shp.parent.shapes[name]
|
|
64
|
-
except Exception:
|
|
65
|
-
xl_shape = None
|
|
66
|
-
|
|
67
|
-
if xl_shape is not None:
|
|
68
|
-
yield from iter_shapes_recursive(xl_shape)
|
|
69
|
-
except Exception:
|
|
70
|
-
pass
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
def _should_include_shape(
|
|
41
|
+
return f"{xw.utils.col_name(c)}{r}"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def has_arrow(style_val: object) -> bool:
|
|
45
|
+
"""Return True if Excel arrow style value indicates an arrowhead."""
|
|
46
|
+
try:
|
|
47
|
+
v = int(cast(SupportsInt, style_val))
|
|
48
|
+
return v != 0
|
|
49
|
+
except Exception:
|
|
50
|
+
return False
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def iter_shapes_recursive(shp: xw.Shape) -> Iterator[xw.Shape]:
|
|
54
|
+
"""Yield shapes recursively, including group children."""
|
|
55
|
+
yield shp
|
|
56
|
+
try:
|
|
57
|
+
if shp.api.Type == 6:
|
|
58
|
+
items = shp.api.GroupItems
|
|
59
|
+
for i in range(1, items.Count + 1):
|
|
60
|
+
inner = items.Item(i)
|
|
61
|
+
try:
|
|
62
|
+
name = inner.Name
|
|
63
|
+
xl_shape = shp.parent.shapes[name]
|
|
64
|
+
except Exception:
|
|
65
|
+
xl_shape = None
|
|
66
|
+
|
|
67
|
+
if xl_shape is not None:
|
|
68
|
+
yield from iter_shapes_recursive(xl_shape)
|
|
69
|
+
except Exception:
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _should_include_shape(
|
|
74
74
|
*,
|
|
75
75
|
text: str,
|
|
76
76
|
shape_type_num: int | None,
|
|
@@ -108,13 +108,16 @@ def _should_include_shape(
|
|
|
108
108
|
return True
|
|
109
109
|
|
|
110
110
|
|
|
111
|
-
def get_shapes_with_position( # noqa: C901
|
|
112
|
-
workbook: Book, mode: str = "standard"
|
|
113
|
-
) -> dict[str, list[Shape]]:
|
|
111
|
+
def get_shapes_with_position( # noqa: C901
|
|
112
|
+
workbook: Book, mode: str = "standard"
|
|
113
|
+
) -> dict[str, list[Shape]]:
|
|
114
114
|
"""Scan shapes in a workbook and return per-sheet Shape lists with position info."""
|
|
115
115
|
shape_data: dict[str, list[Shape]] = {}
|
|
116
116
|
for sheet in workbook.sheets:
|
|
117
117
|
shapes: list[Shape] = []
|
|
118
|
+
excel_names: list[tuple[str, int]] = []
|
|
119
|
+
node_index = 0
|
|
120
|
+
pending_connections: list[tuple[Shape, str | None, str | None]] = []
|
|
118
121
|
for root in sheet.shapes:
|
|
119
122
|
for shp in iter_shapes_recursive(root):
|
|
120
123
|
try:
|
|
@@ -168,7 +171,29 @@ def get_shapes_with_position( # noqa: C901
|
|
|
168
171
|
else (shape_type_str or shape_name or "Unknown")
|
|
169
172
|
)
|
|
170
173
|
|
|
174
|
+
is_relationship_geom = False
|
|
175
|
+
if type_num in (3, 9):
|
|
176
|
+
is_relationship_geom = True
|
|
177
|
+
if autoshape_type_str and (
|
|
178
|
+
"Arrow" in autoshape_type_str or "Connector" in autoshape_type_str
|
|
179
|
+
):
|
|
180
|
+
is_relationship_geom = True
|
|
181
|
+
if shape_type_str and (
|
|
182
|
+
"Connector" in shape_type_str or shape_type_str in ("Line", "ConnectLine")
|
|
183
|
+
):
|
|
184
|
+
is_relationship_geom = True
|
|
185
|
+
if shape_name and ("Connector" in shape_name or "Line" in shape_name):
|
|
186
|
+
is_relationship_geom = True
|
|
187
|
+
|
|
188
|
+
shape_id = None
|
|
189
|
+
if not is_relationship_geom:
|
|
190
|
+
node_index += 1
|
|
191
|
+
shape_id = node_index
|
|
192
|
+
|
|
193
|
+
excel_name = shape_name if isinstance(shape_name, str) else None
|
|
194
|
+
|
|
171
195
|
shape_obj = Shape(
|
|
196
|
+
id=shape_id,
|
|
172
197
|
text=text,
|
|
173
198
|
l=int(shp.left),
|
|
174
199
|
t=int(shp.top),
|
|
@@ -180,25 +205,12 @@ def get_shapes_with_position( # noqa: C901
|
|
|
180
205
|
else None,
|
|
181
206
|
type=type_label,
|
|
182
207
|
)
|
|
208
|
+
if excel_name:
|
|
209
|
+
if shape_id is not None:
|
|
210
|
+
excel_names.append((excel_name, shape_id))
|
|
183
211
|
try:
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
is_relationship_geom = True
|
|
187
|
-
if autoshape_type_str and (
|
|
188
|
-
"Arrow" in autoshape_type_str
|
|
189
|
-
or "Connector" in autoshape_type_str
|
|
190
|
-
):
|
|
191
|
-
is_relationship_geom = True
|
|
192
|
-
if shape_type_str and (
|
|
193
|
-
"Connector" in shape_type_str
|
|
194
|
-
or shape_type_str in ("Line", "ConnectLine")
|
|
195
|
-
):
|
|
196
|
-
is_relationship_geom = True
|
|
197
|
-
if shape_name and (
|
|
198
|
-
"Connector" in shape_name or "Line" in shape_name
|
|
199
|
-
):
|
|
200
|
-
is_relationship_geom = True
|
|
201
|
-
|
|
212
|
+
begin_name: str | None = None
|
|
213
|
+
end_name: str | None = None
|
|
202
214
|
if is_relationship_geom:
|
|
203
215
|
angle = compute_line_angle_deg(
|
|
204
216
|
float(shp.width), float(shp.height)
|
|
@@ -217,6 +229,28 @@ def get_shapes_with_position( # noqa: C901
|
|
|
217
229
|
shape_obj.end_arrow_style = end_style
|
|
218
230
|
except Exception:
|
|
219
231
|
pass
|
|
232
|
+
# Connector begin/end connected shapes (if this shape is a connector).
|
|
233
|
+
try:
|
|
234
|
+
connector = shp.api.ConnectorFormat
|
|
235
|
+
try:
|
|
236
|
+
begin_shape = connector.BeginConnectedShape
|
|
237
|
+
if begin_shape is not None:
|
|
238
|
+
name = getattr(begin_shape, "Name", None)
|
|
239
|
+
if isinstance(name, str):
|
|
240
|
+
begin_name = name
|
|
241
|
+
except Exception:
|
|
242
|
+
pass
|
|
243
|
+
try:
|
|
244
|
+
end_shape = connector.EndConnectedShape
|
|
245
|
+
if end_shape is not None:
|
|
246
|
+
name = getattr(end_shape, "Name", None)
|
|
247
|
+
if isinstance(name, str):
|
|
248
|
+
end_name = name
|
|
249
|
+
except Exception:
|
|
250
|
+
pass
|
|
251
|
+
except Exception:
|
|
252
|
+
# Not a connector or ConnectorFormat is unavailable.
|
|
253
|
+
pass
|
|
220
254
|
elif type_num == 1 and (
|
|
221
255
|
autoshape_type_str and "Arrow" in autoshape_type_str
|
|
222
256
|
):
|
|
@@ -228,6 +262,14 @@ def get_shapes_with_position( # noqa: C901
|
|
|
228
262
|
pass
|
|
229
263
|
except Exception:
|
|
230
264
|
pass
|
|
265
|
+
pending_connections.append((shape_obj, begin_name, end_name))
|
|
231
266
|
shapes.append(shape_obj)
|
|
267
|
+
if pending_connections:
|
|
268
|
+
name_to_id = {name: sid for name, sid in excel_names}
|
|
269
|
+
for shape_obj, begin_name, end_name in pending_connections:
|
|
270
|
+
if begin_name:
|
|
271
|
+
shape_obj.begin_id = name_to_id.get(begin_name)
|
|
272
|
+
if end_name:
|
|
273
|
+
shape_obj.end_id = name_to_id.get(end_name)
|
|
232
274
|
shape_data[sheet.name] = shapes
|
|
233
275
|
return shape_data
|
|
@@ -11,6 +11,9 @@ from pydantic import BaseModel, Field
|
|
|
11
11
|
class Shape(BaseModel):
|
|
12
12
|
"""Shape metadata (position, size, text, and styling)."""
|
|
13
13
|
|
|
14
|
+
id: int | None = Field(
|
|
15
|
+
default=None, description="Sequential shape id within the sheet (if applicable)."
|
|
16
|
+
)
|
|
14
17
|
text: str = Field(description="Visible text content of the shape.")
|
|
15
18
|
l: int = Field(description="Left offset (Excel units).") # noqa: E741
|
|
16
19
|
t: int = Field(description="Top offset (Excel units).")
|
|
@@ -26,6 +29,18 @@ class Shape(BaseModel):
|
|
|
26
29
|
end_arrow_style: int | None = Field(
|
|
27
30
|
default=None, description="Arrow style enum for the end of a connector."
|
|
28
31
|
)
|
|
32
|
+
begin_id: int | None = Field(
|
|
33
|
+
default=None,
|
|
34
|
+
description=(
|
|
35
|
+
"Shape id at the start of a connector (ConnectorFormat.BeginConnectedShape)."
|
|
36
|
+
),
|
|
37
|
+
)
|
|
38
|
+
end_id: int | None = Field(
|
|
39
|
+
default=None,
|
|
40
|
+
description=(
|
|
41
|
+
"Shape id at the end of a connector (ConnectorFormat.EndConnectedShape)."
|
|
42
|
+
),
|
|
43
|
+
)
|
|
29
44
|
direction: Literal["E", "SE", "S", "SW", "W", "NW", "N", "NE"] | None = Field(
|
|
30
45
|
default=None, description="Connector direction (compass heading)."
|
|
31
46
|
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|