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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: exstruct
3
- Version: 0.2.71
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,6 +1,6 @@
1
1
  [project]
2
2
  name = "exstruct"
3
- version = "0.2.71"
3
+ version = "0.2.80"
4
4
  description = "Excel to structured JSON (tables, shapes, charts) for LLM/RAG pipelines"
5
5
  readme = "README.md"
6
6
  license = { file = "LICENSE" }
@@ -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
- is_relationship_geom = False
185
- if type_num in (3, 9):
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