licos-dev-sdk 0.4.0__tar.gz → 0.4.1__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.
- {licos_dev_sdk-0.4.0 → licos_dev_sdk-0.4.1}/PKG-INFO +1 -1
- {licos_dev_sdk-0.4.0 → licos_dev_sdk-0.4.1}/pyproject.toml +1 -1
- {licos_dev_sdk-0.4.0 → licos_dev_sdk-0.4.1}/src/licos_dev_sdk/document_table_export.py +109 -34
- {licos_dev_sdk-0.4.0 → licos_dev_sdk-0.4.1}/tests/test_document_table_export.py +68 -0
- {licos_dev_sdk-0.4.0 → licos_dev_sdk-0.4.1}/.gitignore +0 -0
- {licos_dev_sdk-0.4.0 → licos_dev_sdk-0.4.1}/src/licos_dev_sdk/__init__.py +0 -0
- {licos_dev_sdk-0.4.0 → licos_dev_sdk-0.4.1}/src/licos_dev_sdk/_utils.py +0 -0
- {licos_dev_sdk-0.4.0 → licos_dev_sdk-0.4.1}/src/licos_dev_sdk/archive.py +0 -0
- {licos_dev_sdk-0.4.0 → licos_dev_sdk-0.4.1}/src/licos_dev_sdk/chart.py +0 -0
- {licos_dev_sdk-0.4.0 → licos_dev_sdk-0.4.1}/src/licos_dev_sdk/data.py +0 -0
- {licos_dev_sdk-0.4.0 → licos_dev_sdk-0.4.1}/src/licos_dev_sdk/diagram.py +0 -0
- {licos_dev_sdk-0.4.0 → licos_dev_sdk-0.4.1}/src/licos_dev_sdk/document.py +0 -0
- {licos_dev_sdk-0.4.0 → licos_dev_sdk-0.4.1}/src/licos_dev_sdk/document_common.py +0 -0
- {licos_dev_sdk-0.4.0 → licos_dev_sdk-0.4.1}/src/licos_dev_sdk/document_docx.py +0 -0
- {licos_dev_sdk-0.4.0 → licos_dev_sdk-0.4.1}/src/licos_dev_sdk/document_docx_edit.py +0 -0
- {licos_dev_sdk-0.4.0 → licos_dev_sdk-0.4.1}/src/licos_dev_sdk/document_docx_layout.py +0 -0
- {licos_dev_sdk-0.4.0 → licos_dev_sdk-0.4.1}/src/licos_dev_sdk/document_docx_table.py +0 -0
- {licos_dev_sdk-0.4.0 → licos_dev_sdk-0.4.1}/src/licos_dev_sdk/document_read.py +0 -0
- {licos_dev_sdk-0.4.0 → licos_dev_sdk-0.4.1}/src/licos_dev_sdk/image.py +0 -0
- {licos_dev_sdk-0.4.0 → licos_dev_sdk-0.4.1}/src/licos_dev_sdk/model.py +0 -0
- {licos_dev_sdk-0.4.0 → licos_dev_sdk-0.4.1}/src/licos_dev_sdk/observability.py +0 -0
- {licos_dev_sdk-0.4.0 → licos_dev_sdk-0.4.1}/src/licos_dev_sdk/pdf_inspect.py +0 -0
- {licos_dev_sdk-0.4.0 → licos_dev_sdk-0.4.1}/src/licos_dev_sdk/ppt_master_runtime.py +0 -0
- {licos_dev_sdk-0.4.0 → licos_dev_sdk-0.4.1}/src/licos_dev_sdk/presentation.py +0 -0
- {licos_dev_sdk-0.4.0 → licos_dev_sdk-0.4.1}/src/licos_dev_sdk/presentation_canvas.py +0 -0
- {licos_dev_sdk-0.4.0 → licos_dev_sdk-0.4.1}/src/licos_dev_sdk/presentation_inspect.py +0 -0
- {licos_dev_sdk-0.4.0 → licos_dev_sdk-0.4.1}/src/licos_dev_sdk/spreadsheet.py +0 -0
- {licos_dev_sdk-0.4.0 → licos_dev_sdk-0.4.1}/src/licos_dev_sdk/spreadsheet_edit.py +0 -0
- {licos_dev_sdk-0.4.0 → licos_dev_sdk-0.4.1}/src/licos_dev_sdk/spreadsheet_html.py +0 -0
- {licos_dev_sdk-0.4.0 → licos_dev_sdk-0.4.1}/src/licos_dev_sdk/spreadsheet_inspect.py +0 -0
- {licos_dev_sdk-0.4.0 → licos_dev_sdk-0.4.1}/src/licos_dev_sdk/web.py +0 -0
- {licos_dev_sdk-0.4.0 → licos_dev_sdk-0.4.1}/tests/test_document_read.py +0 -0
- {licos_dev_sdk-0.4.0 → licos_dev_sdk-0.4.1}/tests/test_document_spreadsheet.py +0 -0
- {licos_dev_sdk-0.4.0 → licos_dev_sdk-0.4.1}/tests/test_docx_table_edit.py +0 -0
- {licos_dev_sdk-0.4.0 → licos_dev_sdk-0.4.1}/tests/test_model.py +0 -0
- {licos_dev_sdk-0.4.0 → licos_dev_sdk-0.4.1}/tests/test_observability.py +0 -0
- {licos_dev_sdk-0.4.0 → licos_dev_sdk-0.4.1}/tests/test_output_paths.py +0 -0
- {licos_dev_sdk-0.4.0 → licos_dev_sdk-0.4.1}/tests/test_ppt_master_runtime.py +0 -0
- {licos_dev_sdk-0.4.0 → licos_dev_sdk-0.4.1}/tests/test_presentation.py +0 -0
- {licos_dev_sdk-0.4.0 → licos_dev_sdk-0.4.1}/tests/test_spreadsheet_html_edit.py +0 -0
|
@@ -60,34 +60,55 @@ def extract_docx_table_columns(
|
|
|
60
60
|
previous_table_number: int | None = None
|
|
61
61
|
|
|
62
62
|
for table_number, table in enumerate(tables, start=1):
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
local_header = _find_table_header_layout(
|
|
64
|
+
table,
|
|
65
|
+
normalized,
|
|
66
|
+
table_number=table_number,
|
|
66
67
|
)
|
|
67
|
-
|
|
68
|
-
|
|
68
|
+
layout = local_header or header
|
|
69
|
+
mapped_columns = (
|
|
70
|
+
tuple(start for start, _span in layout.columns)
|
|
71
|
+
if local_header is not None
|
|
72
|
+
else tuple(
|
|
73
|
+
_scale_column(start, span, header.column_count, table.column_count)
|
|
74
|
+
for start, span in header.columns
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
if local_header is not None:
|
|
78
|
+
sequence_column = local_header.sequence_column
|
|
79
|
+
elif header.sequence_column is not None:
|
|
80
|
+
sequence_column = _scale_column(
|
|
69
81
|
header.sequence_column,
|
|
70
82
|
1,
|
|
71
83
|
header.column_count,
|
|
72
84
|
table.column_count,
|
|
73
85
|
)
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
)
|
|
86
|
+
else:
|
|
87
|
+
sequence_column = None
|
|
77
88
|
|
|
78
89
|
for row in table.rows:
|
|
79
|
-
if
|
|
90
|
+
if local_header is not None and row.number <= local_header.row_number:
|
|
80
91
|
continue
|
|
81
|
-
if _is_header_row(row, normalized):
|
|
92
|
+
if _is_header_row(table, row, normalized):
|
|
82
93
|
continue
|
|
83
94
|
|
|
84
|
-
values = tuple(_text_at(row, column) for column in mapped_columns)
|
|
95
|
+
values = tuple(_text_at(table, row, column) for column in mapped_columns)
|
|
85
96
|
if not any(values):
|
|
86
97
|
continue
|
|
87
98
|
|
|
88
|
-
sequence =
|
|
99
|
+
sequence = (
|
|
100
|
+
_text_at(table, row, sequence_column)
|
|
101
|
+
if sequence_column is not None
|
|
102
|
+
else ""
|
|
103
|
+
)
|
|
89
104
|
populated = sum(bool(value) for value in values)
|
|
90
|
-
if
|
|
105
|
+
if _is_structural_heading(table, row, mapped_columns):
|
|
106
|
+
continue
|
|
107
|
+
if (
|
|
108
|
+
populated == 1
|
|
109
|
+
and sequence_column is not None
|
|
110
|
+
and not _is_numeric_sequence(sequence)
|
|
111
|
+
):
|
|
91
112
|
if rows and previous_table_number == table_number and values[0]:
|
|
92
113
|
rows[-1][selected[0]] = _join_continuation(rows[-1][selected[0]], values[0])
|
|
93
114
|
continue
|
|
@@ -105,29 +126,56 @@ def _find_header_layout(
|
|
|
105
126
|
normalized_columns: tuple[str, ...],
|
|
106
127
|
) -> _HeaderLayout:
|
|
107
128
|
for table_number, table in enumerate(tables, start=1):
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
row_number=row.number,
|
|
116
|
-
column_count=table.column_count,
|
|
117
|
-
columns=tuple((cell.column, cell.col_span) for cell in matched if cell is not None),
|
|
118
|
-
sequence_column=sequence_column,
|
|
119
|
-
)
|
|
129
|
+
layout = _find_table_header_layout(
|
|
130
|
+
table,
|
|
131
|
+
normalized_columns,
|
|
132
|
+
table_number=table_number,
|
|
133
|
+
)
|
|
134
|
+
if layout is not None:
|
|
135
|
+
return layout
|
|
120
136
|
display = ", ".join(normalized_columns)
|
|
121
137
|
raise ValueError(f"no DOCX table header row contains all requested columns: {display}")
|
|
122
138
|
|
|
123
139
|
|
|
124
|
-
def
|
|
125
|
-
|
|
140
|
+
def _find_table_header_layout(
|
|
141
|
+
table: TableModel,
|
|
142
|
+
normalized_columns: tuple[str, ...],
|
|
143
|
+
*,
|
|
144
|
+
table_number: int,
|
|
145
|
+
) -> _HeaderLayout | None:
|
|
146
|
+
for row in table.rows:
|
|
147
|
+
cells = {
|
|
148
|
+
_normalize_header(cell.text): cell
|
|
149
|
+
for cell in _visible_cells(table, row.number)
|
|
150
|
+
if cell.text.strip()
|
|
151
|
+
}
|
|
152
|
+
matched = [cells.get(column) for column in normalized_columns]
|
|
153
|
+
if all(cell is not None for cell in matched):
|
|
154
|
+
return _HeaderLayout(
|
|
155
|
+
table_number=table_number,
|
|
156
|
+
row_number=row.number,
|
|
157
|
+
column_count=table.column_count,
|
|
158
|
+
columns=tuple((cell.column, cell.col_span) for cell in matched if cell is not None),
|
|
159
|
+
sequence_column=_sequence_column(table, row.number),
|
|
160
|
+
)
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _is_header_row(
|
|
165
|
+
table: TableModel,
|
|
166
|
+
row: TableRow,
|
|
167
|
+
normalized_columns: tuple[str, ...],
|
|
168
|
+
) -> bool:
|
|
169
|
+
available = {
|
|
170
|
+
_normalize_header(cell.text)
|
|
171
|
+
for cell in _visible_cells(table, row.number)
|
|
172
|
+
if cell.text.strip()
|
|
173
|
+
}
|
|
126
174
|
return all(column in available for column in normalized_columns)
|
|
127
175
|
|
|
128
176
|
|
|
129
|
-
def _sequence_column(
|
|
130
|
-
for cell in
|
|
177
|
+
def _sequence_column(table: TableModel, row_number: int) -> int | None:
|
|
178
|
+
for cell in _visible_cells(table, row_number):
|
|
131
179
|
if _normalize_header(cell.text) in {"序号", "编号"}:
|
|
132
180
|
return cell.column
|
|
133
181
|
return None
|
|
@@ -148,13 +196,40 @@ def _scale_column(
|
|
|
148
196
|
return min(max(mapped, 1), target_count)
|
|
149
197
|
|
|
150
198
|
|
|
151
|
-
def _text_at(row: TableRow, column: int | None) -> str:
|
|
199
|
+
def _text_at(table: TableModel, row: TableRow, column: int | None) -> str:
|
|
152
200
|
if column is None:
|
|
153
201
|
return ""
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
202
|
+
cell = table.cell_at(row.number, column)
|
|
203
|
+
return cell.text.strip() if cell is not None else ""
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _visible_cells(table: TableModel, row_number: int) -> list[TableCell]:
|
|
207
|
+
cells: list[TableCell] = []
|
|
208
|
+
seen: set[int] = set()
|
|
209
|
+
for column in range(1, table.column_count + 1):
|
|
210
|
+
cell = table.cell_at(row_number, column)
|
|
211
|
+
if cell is not None and id(cell) not in seen:
|
|
212
|
+
cells.append(cell)
|
|
213
|
+
seen.add(id(cell))
|
|
214
|
+
return cells
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _is_structural_heading(
|
|
218
|
+
table: TableModel,
|
|
219
|
+
row: TableRow,
|
|
220
|
+
mapped_columns: tuple[int, ...],
|
|
221
|
+
) -> bool:
|
|
222
|
+
nonempty = [cell for cell in _visible_cells(table, row.number) if cell.text.strip()]
|
|
223
|
+
if len(nonempty) != 1:
|
|
224
|
+
return False
|
|
225
|
+
cell = nonempty[0]
|
|
226
|
+
selected_coverage = sum(
|
|
227
|
+
cell.column <= column < cell.column + cell.col_span
|
|
228
|
+
for column in mapped_columns
|
|
229
|
+
)
|
|
230
|
+
return cell.col_span > 1 and (
|
|
231
|
+
selected_coverage > 1 or cell.col_span * 2 >= table.column_count
|
|
232
|
+
)
|
|
158
233
|
|
|
159
234
|
|
|
160
235
|
def _normalize_header(value: str) -> str:
|
|
@@ -84,6 +84,74 @@ def test_create_xlsx_from_docx_writes_native_workbook(tmp_path: Path):
|
|
|
84
84
|
workbook.close()
|
|
85
85
|
|
|
86
86
|
|
|
87
|
+
def test_extract_docx_columns_without_sequence_preserves_sparse_rows(tmp_path: Path):
|
|
88
|
+
source = tmp_path / "without-sequence.docx"
|
|
89
|
+
document = Document()
|
|
90
|
+
table = document.add_table(rows=5, cols=3)
|
|
91
|
+
for column, value in enumerate(["名称", "单位", "数量"]):
|
|
92
|
+
table.cell(0, column).text = value
|
|
93
|
+
for row, values in enumerate(
|
|
94
|
+
[
|
|
95
|
+
["泵", "台", "2"],
|
|
96
|
+
["备用泵", "", ""],
|
|
97
|
+
["阀门", "个", ""],
|
|
98
|
+
["液位计", "", "1"],
|
|
99
|
+
],
|
|
100
|
+
start=1,
|
|
101
|
+
):
|
|
102
|
+
for column, value in enumerate(values):
|
|
103
|
+
table.cell(row, column).text = value
|
|
104
|
+
document.save(source)
|
|
105
|
+
|
|
106
|
+
rows = extract_docx_table_columns(source, ["名称", "单位", "数量"])
|
|
107
|
+
|
|
108
|
+
assert rows == [
|
|
109
|
+
{"名称": "泵", "单位": "台", "数量": "2"},
|
|
110
|
+
{"名称": "备用泵", "单位": "", "数量": ""},
|
|
111
|
+
{"名称": "阀门", "单位": "个", "数量": ""},
|
|
112
|
+
{"名称": "液位计", "单位": "", "数量": "1"},
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_extract_docx_columns_without_sequence_skips_merged_heading(tmp_path: Path):
|
|
117
|
+
source = tmp_path / "merged-heading.docx"
|
|
118
|
+
document = Document()
|
|
119
|
+
table = document.add_table(rows=3, cols=3)
|
|
120
|
+
for column, value in enumerate(["名称", "单位", "数量"]):
|
|
121
|
+
table.cell(0, column).text = value
|
|
122
|
+
table.cell(1, 0).merge(table.cell(1, 2)).text = "泵类设备"
|
|
123
|
+
for column, value in enumerate(["循环泵", "台", "2"]):
|
|
124
|
+
table.cell(2, column).text = value
|
|
125
|
+
document.save(source)
|
|
126
|
+
|
|
127
|
+
rows = extract_docx_table_columns(source, ["名称", "单位", "数量"])
|
|
128
|
+
|
|
129
|
+
assert rows == [{"名称": "循环泵", "单位": "台", "数量": "2"}]
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def test_extract_docx_columns_handles_multilevel_header_and_vertical_data_merge(tmp_path: Path):
|
|
133
|
+
source = tmp_path / "multilevel-header.docx"
|
|
134
|
+
document = Document()
|
|
135
|
+
table = document.add_table(rows=4, cols=3)
|
|
136
|
+
table.cell(0, 0).merge(table.cell(1, 0)).text = "名称"
|
|
137
|
+
table.cell(0, 1).merge(table.cell(0, 2)).text = "属性"
|
|
138
|
+
table.cell(1, 1).text = "单位"
|
|
139
|
+
table.cell(1, 2).text = "数量"
|
|
140
|
+
table.cell(2, 0).text = "主泵"
|
|
141
|
+
table.cell(2, 1).merge(table.cell(3, 1)).text = "台"
|
|
142
|
+
table.cell(2, 2).text = "2"
|
|
143
|
+
table.cell(3, 0).text = "备用泵"
|
|
144
|
+
table.cell(3, 2).text = "1"
|
|
145
|
+
document.save(source)
|
|
146
|
+
|
|
147
|
+
rows = extract_docx_table_columns(source, ["名称", "单位", "数量"])
|
|
148
|
+
|
|
149
|
+
assert rows == [
|
|
150
|
+
{"名称": "主泵", "单位": "台", "数量": "2"},
|
|
151
|
+
{"名称": "备用泵", "单位": "台", "数量": "1"},
|
|
152
|
+
]
|
|
153
|
+
|
|
154
|
+
|
|
87
155
|
def test_extract_docx_columns_requires_a_complete_header(tmp_path: Path):
|
|
88
156
|
source = tmp_path / "source.docx"
|
|
89
157
|
document = Document()
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|