table-stitcher 0.4.4__tar.gz → 0.5.0__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.
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/.github/workflows/ci.yml +4 -2
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/CHANGELOG.md +35 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/PKG-INFO +2 -2
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/pyproject.toml +5 -2
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/src/table_stitcher/__init__.py +12 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/src/table_stitcher/adapters/docling.py +62 -7
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/src/table_stitcher/models.py +12 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/test_docling_adapter.py +153 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/test_public_api.py +53 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/.claude/settings.json +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/.github/dependabot.yml +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/.github/pull_request_template.md +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/.github/workflows/release.yml +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/.github/workflows/upstream-smoke.yml +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/.gitignore +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/.pre-commit-config.yaml +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/CONTRIBUTING.md +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/LICENSE +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/README.md +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/SECURITY.md +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/examples/basic_pipeline.py +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/examples/system_controller.py +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/scripts/regenerate_docling_snapshots.py +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/scripts/release_gate.sh +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/src/table_stitcher/adapters/README.md +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/src/table_stitcher/adapters/__init__.py +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/src/table_stitcher/adapters/base.py +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/src/table_stitcher/merger.py +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/src/table_stitcher/py.typed +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/README.md +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/__init__.py +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/fixtures/tablemeta/headerless-width-drift.yaml +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/__init__.py +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/_tools/__init__.py +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/_tools/regenerate_expected.py +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/conftest.py +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/_synth/__init__.py +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/_synth/generate.py +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/distinct-tables-no-merge/.gitkeep +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/distinct-tables-no-merge/kaop-study-mixed-3pg.pt2.docling.json +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/distinct-tables-no-merge/kaop-study-mixed-3pg.pt2.expected.yaml +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/distinct-tables-no-merge/kaop-study-mixed-3pg.pt2.pdf +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/distinct-tables-no-merge/lab-panels-3pg.corp.docling.json +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/distinct-tables-no-merge/lab-panels-3pg.corp.expected.yaml +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/distinct-tables-no-merge/lab-panels-3pg.corp.pdf +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/false-merge/category-rows-thematic-2pg.corp.docling.json +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/false-merge/category-rows-thematic-2pg.corp.expected.yaml +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/false-merge/category-rows-thematic-2pg.corp.pdf +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/headerless-continuation/15-page-druglist.corp.docling.json +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/headerless-continuation/15-page-druglist.corp.expected.yaml +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/headerless-continuation/15-page-druglist.corp.pdf +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/headerless-continuation/gene-symbols-6pg.pt2.docling.json +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/headerless-continuation/gene-symbols-6pg.pt2.expected.yaml +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/headerless-continuation/gene-symbols-6pg.pt2.pdf +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/headerless-continuation/rct-study-table-5pg.pt2.docling.json +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/headerless-continuation/rct-study-table-5pg.pt2.expected.yaml +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/headerless-continuation/rct-study-table-5pg.pt2.pdf +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/headerless-continuation/rrt-outcomes-2pg.pt2.docling.json +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/headerless-continuation/rrt-outcomes-2pg.pt2.expected.yaml +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/headerless-continuation/rrt-outcomes-2pg.pt2.pdf +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/headerless-continuation/uveitis-case-series-5pg.pt2.docling.json +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/headerless-continuation/uveitis-case-series-5pg.pt2.expected.yaml +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/headerless-continuation/uveitis-case-series-5pg.pt2.pdf +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/inconsistent-header-detection/covid-misc-labs-4pg.pt2.docling.json +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/inconsistent-header-detection/covid-misc-labs-4pg.pt2.expected.yaml +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/inconsistent-header-detection/covid-misc-labs-4pg.pt2.pdf +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/inconsistent-header-detection/lit-review-3pg.pt2.docling.json +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/inconsistent-header-detection/lit-review-3pg.pt2.expected.yaml +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/inconsistent-header-detection/lit-review-3pg.pt2.pdf +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/inconsistent-header-detection/retirement-portfolio.corp.docling.json +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/inconsistent-header-detection/retirement-portfolio.corp.expected.yaml +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/inconsistent-header-detection/retirement-portfolio.corp.pdf +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/loose-header-layout/.gitkeep +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/loose-header-layout/biological-process-2pg.pt2.docling.json +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/loose-header-layout/biological-process-2pg.pt2.expected.yaml +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/loose-header-layout/biological-process-2pg.pt2.pdf +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/loose-header-layout/symptoms-mediators-4pg.pt2.docling.json +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/loose-header-layout/symptoms-mediators-4pg.pt2.expected.yaml +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/loose-header-layout/symptoms-mediators-4pg.pt2.pdf +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/multilingual/corporate-history-2pg.edinet.docling.json +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/multilingual/corporate-history-2pg.edinet.expected.yaml +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/multilingual/corporate-history-2pg.edinet.pdf +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/multilingual/subsidiaries-4pg.edinet.docling.json +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/multilingual/subsidiaries-4pg.edinet.expected.yaml +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/multilingual/subsidiaries-4pg.edinet.pdf +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/orphan-pair/.gitkeep +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/orphan-pair/varicose-veins-new-table-header-7pg.pt2.docling.json +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/orphan-pair/varicose-veins-new-table-header-7pg.pt2.expected.yaml +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/orphan-pair/varicose-veins-new-table-header-7pg.pt2.pdf +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/page-gap-too-large/unrelated-tables-gap4.synth.docling.json +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/page-gap-too-large/unrelated-tables-gap4.synth.expected.yaml +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/page-gap-too-large/unrelated-tables-gap4.synth.pdf +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/repeated-header/4-page-substance-list.corp.docling.json +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/repeated-header/4-page-substance-list.corp.expected.yaml +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/repeated-header/4-page-substance-list.corp.pdf +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/repeated-header/cell-markers-4pg.pt2.docling.json +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/repeated-header/cell-markers-4pg.pt2.expected.yaml +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/repeated-header/cell-markers-4pg.pt2.pdf +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/repeated-header/challenge-categories-2pg.pt2.docling.json +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/repeated-header/challenge-categories-2pg.pt2.expected.yaml +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/repeated-header/challenge-categories-2pg.pt2.pdf +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/repeated-header/fungal-taxonomy-4pg.pt2.docling.json +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/repeated-header/fungal-taxonomy-4pg.pt2.expected.yaml +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/repeated-header/fungal-taxonomy-4pg.pt2.pdf +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/repeated-header/rowspan-insurance-payout.corp.docling.json +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/repeated-header/rowspan-insurance-payout.corp.expected.yaml +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/repeated-header/rowspan-insurance-payout.corp.pdf +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/repeated-header/search-strategies-2pg.pt2.docling.json +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/repeated-header/search-strategies-2pg.pt2.expected.yaml +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/repeated-header/search-strategies-2pg.pt2.pdf +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/repeated-header/study-sample-7pg.pt2.docling.json +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/repeated-header/study-sample-7pg.pt2.expected.yaml +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/repeated-header/study-sample-7pg.pt2.pdf +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/repeated-header/themes-exemplars-3pg.pt2.docling.json +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/repeated-header/themes-exemplars-3pg.pt2.expected.yaml +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/repeated-header/themes-exemplars-3pg.pt2.pdf +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/simple-continuation/sample-table.corp.docling.json +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/simple-continuation/sample-table.corp.expected.yaml +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/simple-continuation/sample-table.corp.pdf +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/spillover/note-overflow.synth.docling.json +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/spillover/note-overflow.synth.expected.yaml +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/spillover/note-overflow.synth.pdf +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/width-drift/.gitkeep +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/width-drift/abx-literature-review-7pg.pt2.docling.json +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/width-drift/abx-literature-review-7pg.pt2.expected.yaml +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/width-drift/abx-literature-review-7pg.pt2.pdf +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/test_fixtures.py +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/test_intervening_content_guard.py +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/test_merger.py +0 -0
- {table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/test_tablemeta_fixtures.py +0 -0
|
@@ -19,11 +19,13 @@ jobs:
|
|
|
19
19
|
python-version: "3.12"
|
|
20
20
|
cache: pip
|
|
21
21
|
- name: install ruff
|
|
22
|
-
#
|
|
22
|
+
# Keep this pin in sync with the dev extra in pyproject.toml so local
|
|
23
|
+
# + CI run the same version. Exact pin because ruff format output
|
|
24
|
+
# changes between releases and would break lint on untouched files.
|
|
23
25
|
# No need to install the full dev tree (docling, reportlab) just to lint.
|
|
24
26
|
run: |
|
|
25
27
|
python -m pip install --upgrade pip
|
|
26
|
-
pip install "ruff
|
|
28
|
+
pip install "ruff==0.16.2"
|
|
27
29
|
- name: ruff check
|
|
28
30
|
run: ruff check .
|
|
29
31
|
- name: ruff format --check
|
|
@@ -7,6 +7,41 @@ the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.5.0] — 2026-08-23
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **Cell bounding boxes were dropped on multi-page merge**
|
|
15
|
+
(`adapters/docling.py`). When N per-page fragments merged into one logical
|
|
16
|
+
table, every `TableCell` in the injected result came back with `bbox=None`,
|
|
17
|
+
breaking word-level grounding for multi-page tables. `_reemit_body_row`
|
|
18
|
+
rebuilt each untouched body row's cells without copying the source cell's
|
|
19
|
+
`bbox`; it is now carried across, so untouched body rows — the large
|
|
20
|
+
majority on a clean multi-page table — keep their geometry. Header rows
|
|
21
|
+
reused from the anchor already preserved theirs; rows the merger transformed
|
|
22
|
+
(stitched continuations, folded overflow) genuinely have none and stay
|
|
23
|
+
`bbox=None`. The FIFO alignment between repeated identical-text rows and
|
|
24
|
+
their source occurrences is now a documented invariant: it is what guarantees
|
|
25
|
+
a repeated row gets its own page's boxes, not a sibling's.
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- **Row-level page association for restored geometry** (`models.py`,
|
|
30
|
+
`adapters/docling.py`). A merged table's `prov` lists all N source pages, but
|
|
31
|
+
pages share one coordinate space, so a restored `bbox` alone cannot say which
|
|
32
|
+
page it belongs to. `LogicalTable.row_pages` now maps grid row index (header
|
|
33
|
+
rows included) to the resolved `page_no` the row's cell boxes are valid on —
|
|
34
|
+
resolved page numbers rather than prov indices, so the map survives
|
|
35
|
+
downstream prov manipulation. A missing key means the row has no single
|
|
36
|
+
source page (transformed rows, which carry no geometry). Recorded during
|
|
37
|
+
injection, the only point where each row's source fragment is known; a
|
|
38
|
+
consumer cannot reconstruct the mapping after the fact.
|
|
39
|
+
- **`TableStitcher.last_logical_tables`** exposes the `LogicalTable` results of
|
|
40
|
+
the most recent `stitch()` call — previously discarded after injection,
|
|
41
|
+
which would have left `row_pages` unreachable through the public API.
|
|
42
|
+
Consumers needing the map should instantiate `TableStitcher` directly; the
|
|
43
|
+
`stitch_tables()` convenience function does not expose the stitcher instance.
|
|
44
|
+
|
|
10
45
|
## [0.4.4] — 2026-08-13
|
|
11
46
|
|
|
12
47
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: table-stitcher
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Reassemble tables split across page boundaries in PDF extraction
|
|
5
5
|
Project-URL: Homepage, https://github.com/pebbleroad/table-stitcher
|
|
6
6
|
Project-URL: Repository, https://github.com/pebbleroad/table-stitcher
|
|
@@ -29,7 +29,7 @@ Requires-Dist: pre-commit>=3.0; extra == 'dev'
|
|
|
29
29
|
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
30
30
|
Requires-Dist: pyyaml>=6.0; extra == 'dev'
|
|
31
31
|
Requires-Dist: reportlab>=4.0; extra == 'dev'
|
|
32
|
-
Requires-Dist: ruff
|
|
32
|
+
Requires-Dist: ruff==0.16.2; extra == 'dev'
|
|
33
33
|
Requires-Dist: twine>=5.0; extra == 'dev'
|
|
34
34
|
Provides-Extra: docling
|
|
35
35
|
Requires-Dist: docling-core<3,>=2.50; extra == 'docling'
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "table-stitcher"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.5.0"
|
|
8
8
|
description = "Reassemble tables split across page boundaries in PDF extraction"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -40,7 +40,10 @@ dev = [
|
|
|
40
40
|
"docling-core>=2.50,<3",
|
|
41
41
|
"pyyaml>=6.0",
|
|
42
42
|
"reportlab>=4.0",
|
|
43
|
-
|
|
43
|
+
# Exact pin: ruff format output changes between releases (0.16 started
|
|
44
|
+
# formatting markdown code blocks), so a range lets a new ruff release
|
|
45
|
+
# break lint on untouched files. CI installs this same pin.
|
|
46
|
+
"ruff==0.16.2",
|
|
44
47
|
"build>=1.0",
|
|
45
48
|
"twine>=5.0",
|
|
46
49
|
"pre-commit>=3.0",
|
|
@@ -77,6 +77,12 @@ class TableStitcher:
|
|
|
77
77
|
self.logger = logging.getLogger("table_stitcher")
|
|
78
78
|
self.adapter = adapter
|
|
79
79
|
self.config = config or MultiPageConfig()
|
|
80
|
+
self.last_logical_tables: list[LogicalTable] = []
|
|
81
|
+
"""
|
|
82
|
+
The ``LogicalTable`` results of the most recent ``stitch()`` call,
|
|
83
|
+
including per-row page associations (``LogicalTable.row_pages``)
|
|
84
|
+
populated during injection. Reset at the start of every ``stitch()``.
|
|
85
|
+
"""
|
|
80
86
|
self._validate_config()
|
|
81
87
|
|
|
82
88
|
# -------------------------------------------------------------------------
|
|
@@ -164,6 +170,8 @@ class TableStitcher:
|
|
|
164
170
|
Returns:
|
|
165
171
|
The document with merged tables.
|
|
166
172
|
"""
|
|
173
|
+
self.last_logical_tables = []
|
|
174
|
+
|
|
167
175
|
if doc is None:
|
|
168
176
|
if raise_on_error:
|
|
169
177
|
raise StitchingError("Input document is None")
|
|
@@ -213,6 +221,10 @@ class TableStitcher:
|
|
|
213
221
|
raise StitchingError(f"Failed to merge tables: {e}") from e
|
|
214
222
|
return doc
|
|
215
223
|
|
|
224
|
+
# Exposed for consumers needing per-table merge results — notably
|
|
225
|
+
# LogicalTable.row_pages, which injection fills in below.
|
|
226
|
+
self.last_logical_tables = logical_tables
|
|
227
|
+
|
|
216
228
|
multi_page_tables = [lt for lt in logical_tables if len(lt.pages) > 1]
|
|
217
229
|
|
|
218
230
|
if not multi_page_tables:
|
|
@@ -223,6 +223,18 @@ def _extract_y_bounds_from_prov(prov_list: list[Any]) -> Optional[tuple[float, f
|
|
|
223
223
|
return None
|
|
224
224
|
|
|
225
225
|
|
|
226
|
+
def _first_page_no(prov: Any) -> Optional[int]:
|
|
227
|
+
"""Resolved page number of the first prov entry carrying one, else None."""
|
|
228
|
+
if prov is None:
|
|
229
|
+
return None
|
|
230
|
+
entries = prov if isinstance(prov, list) else [prov]
|
|
231
|
+
for p in entries:
|
|
232
|
+
page_no = getattr(p, "page_no", None)
|
|
233
|
+
if page_no is not None:
|
|
234
|
+
return page_no
|
|
235
|
+
return None
|
|
236
|
+
|
|
237
|
+
|
|
226
238
|
def _resolve_page_height(prov_list: list[Any], doc: Any, fallback: float = 842.0) -> float:
|
|
227
239
|
"""
|
|
228
240
|
Look up the actual page height for the first prov entry from the document.
|
|
@@ -421,7 +433,7 @@ def _extract_original_header_rows(
|
|
|
421
433
|
|
|
422
434
|
def _index_member_rows(
|
|
423
435
|
member_data: list[Optional[TableData]],
|
|
424
|
-
) -> dict[tuple, list[list[TableCell]]]:
|
|
436
|
+
) -> dict[tuple, list[tuple[int, list[TableCell]]]]:
|
|
425
437
|
"""
|
|
426
438
|
Index every member fragment's original grid rows by their expanded
|
|
427
439
|
text-vector, for col_span reconstruction during injection.
|
|
@@ -440,17 +452,24 @@ def _index_member_rows(
|
|
|
440
452
|
across the value columns. (The anchor's own header rows are reconstructed
|
|
441
453
|
separately and never looked up here.) Values are buckets because a row can
|
|
442
454
|
legitimately repeat; identical text-vectors imply identical span structure,
|
|
443
|
-
|
|
455
|
+
but NOT identical geometry — re-emitted cells carry their source bbox, so
|
|
456
|
+
occurrences are not interchangeable. Buckets are filled in fragment (page)
|
|
457
|
+
order and popped FIFO, which aligns the k-th DataFrame occurrence with the
|
|
458
|
+
k-th source occurrence because the merged DataFrame preserves fragment
|
|
459
|
+
concatenation order.
|
|
460
|
+
|
|
461
|
+
Bucket entries are ``(fragment_idx, row)`` pairs so the re-emitted row's
|
|
462
|
+
page association can be recorded (``fragment_idx`` indexes ``member_data``).
|
|
444
463
|
"""
|
|
445
|
-
index: dict[tuple, list[list[TableCell]]] = {}
|
|
446
|
-
for data in member_data:
|
|
464
|
+
index: dict[tuple, list[tuple[int, list[TableCell]]]] = {}
|
|
465
|
+
for frag_idx, data in enumerate(member_data):
|
|
447
466
|
if not data or not data.grid:
|
|
448
467
|
continue
|
|
449
468
|
for row in data.grid:
|
|
450
469
|
if not row:
|
|
451
470
|
continue
|
|
452
471
|
key = tuple((getattr(c, "text", "") or "") if c else "" for c in row)
|
|
453
|
-
index.setdefault(key, []).append(row)
|
|
472
|
+
index.setdefault(key, []).append((frag_idx, row))
|
|
454
473
|
return index
|
|
455
474
|
|
|
456
475
|
|
|
@@ -458,7 +477,9 @@ def _reemit_body_row(
|
|
|
458
477
|
orig_row: list[TableCell], table_row_idx: int, has_row_headers: bool
|
|
459
478
|
) -> tuple[list[TableCell], list[TableCell]]:
|
|
460
479
|
"""
|
|
461
|
-
Re-emit an original grid body row at a new row offset, preserving col_span
|
|
480
|
+
Re-emit an original grid body row at a new row offset, preserving col_span
|
|
481
|
+
and the source cell's bbox (page-local coordinates; see the merged table's
|
|
482
|
+
prov for page association).
|
|
462
483
|
|
|
463
484
|
Returns ``(grid_row, distinct_cells)`` where ``grid_row`` repeats each
|
|
464
485
|
spanning cell across the columns it covers (Docling grid convention) and
|
|
@@ -480,6 +501,7 @@ def _reemit_body_row(
|
|
|
480
501
|
col_span = getattr(cell, "col_span", 1) or 1
|
|
481
502
|
new_cell = TableCell(
|
|
482
503
|
text=getattr(cell, "text", "") or "",
|
|
504
|
+
bbox=getattr(cell, "bbox", None),
|
|
483
505
|
row_span=1,
|
|
484
506
|
col_span=col_span,
|
|
485
507
|
column_header=False,
|
|
@@ -539,6 +561,8 @@ def _dataframe_to_docling_data(
|
|
|
539
561
|
df: pd.DataFrame,
|
|
540
562
|
original_data: Optional[TableData] = None,
|
|
541
563
|
member_data: Optional[list[Optional[TableData]]] = None,
|
|
564
|
+
member_pages: Optional[list[Optional[int]]] = None,
|
|
565
|
+
row_pages_out: Optional[dict[int, int]] = None,
|
|
542
566
|
) -> TableData:
|
|
543
567
|
"""
|
|
544
568
|
Converts a pandas DataFrame back into Docling's TableData structure.
|
|
@@ -554,6 +578,13 @@ def _dataframe_to_docling_data(
|
|
|
554
578
|
re-emitted from their original grid cells, preserving col_span. Rows the
|
|
555
579
|
merger transformed (stitched continuations, folded overflow) fall back to a
|
|
556
580
|
flat 1x1 rebuild from the DataFrame.
|
|
581
|
+
|
|
582
|
+
When ``row_pages_out`` is provided (with ``member_pages`` aligned
|
|
583
|
+
index-for-index with ``member_data``), it is filled with the page
|
|
584
|
+
association for rows that carry geometry: grid row index -> resolved
|
|
585
|
+
page_no. Header rows reused from the anchor map to the anchor's page
|
|
586
|
+
(``member_pages[0]``); re-emitted body rows map to their source fragment's
|
|
587
|
+
page; flat-rebuilt rows get no entry (see ``LogicalTable.row_pages``).
|
|
557
588
|
"""
|
|
558
589
|
if df.empty:
|
|
559
590
|
cols = list(df.columns) if len(df.columns) > 0 else ["Column_0"]
|
|
@@ -588,6 +619,13 @@ def _dataframe_to_docling_data(
|
|
|
588
619
|
num_header_rows = len(orig_header_rows)
|
|
589
620
|
grid: list[list[TableCell]] = list(orig_header_rows)
|
|
590
621
|
table_cells: list[TableCell] = list(orig_header_cells)
|
|
622
|
+
# Reused header cells keep the anchor's geometry, so they map to the
|
|
623
|
+
# anchor's page (member_pages[0] — the anchor is always members[0]).
|
|
624
|
+
if row_pages_out is not None and member_pages:
|
|
625
|
+
anchor_page = member_pages[0]
|
|
626
|
+
if anchor_page is not None:
|
|
627
|
+
for h_idx in range(num_header_rows):
|
|
628
|
+
row_pages_out[h_idx] = anchor_page
|
|
591
629
|
else:
|
|
592
630
|
# Fall back to building flat 1x1 header from DataFrame columns
|
|
593
631
|
num_header_rows = 1
|
|
@@ -639,7 +677,7 @@ def _dataframe_to_docling_data(
|
|
|
639
677
|
# adjacent values are never fused.
|
|
640
678
|
bucket = body_index.get(tuple(row_vals))
|
|
641
679
|
if bucket:
|
|
642
|
-
orig_row = bucket.pop(0)
|
|
680
|
+
frag_idx, orig_row = bucket.pop(0)
|
|
643
681
|
if header_sigs and _is_reprinted_header(orig_row, header_sigs):
|
|
644
682
|
# Reprinted header from a continuation page — already present as
|
|
645
683
|
# the header block; drop it instead of duplicating into the body.
|
|
@@ -648,6 +686,10 @@ def _dataframe_to_docling_data(
|
|
|
648
686
|
grid_row, distinct = _reemit_body_row(orig_row, table_row_idx, has_row_headers)
|
|
649
687
|
grid.append(grid_row)
|
|
650
688
|
table_cells.extend(distinct)
|
|
689
|
+
if row_pages_out is not None and member_pages:
|
|
690
|
+
page = member_pages[frag_idx] if frag_idx < len(member_pages) else None
|
|
691
|
+
if page is not None:
|
|
692
|
+
row_pages_out[table_row_idx] = page
|
|
651
693
|
emitted += 1
|
|
652
694
|
continue
|
|
653
695
|
|
|
@@ -1051,12 +1093,25 @@ class DoclingAdapter:
|
|
|
1051
1093
|
member_data = [
|
|
1052
1094
|
table_snapshots[m]["data"] for m in lt.members if m in table_snapshots
|
|
1053
1095
|
]
|
|
1096
|
+
# Page per fragment, same filter as member_data so indices stay
|
|
1097
|
+
# aligned. Feeds LogicalTable.row_pages: resolved page_no rather
|
|
1098
|
+
# than a prov index, so the map survives downstream prov
|
|
1099
|
+
# manipulation by consumers.
|
|
1100
|
+
member_pages = [
|
|
1101
|
+
_first_page_no(table_snapshots[m]["prov"])
|
|
1102
|
+
for m in lt.members
|
|
1103
|
+
if m in table_snapshots
|
|
1104
|
+
]
|
|
1054
1105
|
|
|
1106
|
+
row_pages: dict[int, int] = {}
|
|
1055
1107
|
anchor_table.data = _dataframe_to_docling_data(
|
|
1056
1108
|
lt.df,
|
|
1057
1109
|
original_data=original_data,
|
|
1058
1110
|
member_data=member_data,
|
|
1111
|
+
member_pages=member_pages,
|
|
1112
|
+
row_pages_out=row_pages,
|
|
1059
1113
|
)
|
|
1114
|
+
lt.row_pages = row_pages
|
|
1060
1115
|
|
|
1061
1116
|
for satellite_idx in lt.members[1:]:
|
|
1062
1117
|
satellite_table = doc.tables[satellite_idx]
|
|
@@ -163,3 +163,15 @@ class LogicalTable:
|
|
|
163
163
|
merge_reason: str = ""
|
|
164
164
|
merge_traces: list[MergeTrace] = field(default_factory=list)
|
|
165
165
|
warnings: list[str] = field(default_factory=list)
|
|
166
|
+
row_pages: dict[int, int] = field(default_factory=dict)
|
|
167
|
+
"""
|
|
168
|
+
Page association for the merged table's cell geometry, keyed by grid row
|
|
169
|
+
index (an index into the injected ``TableData.grid``, header rows
|
|
170
|
+
included). The value is the resolved ``page_no`` the row's cell bboxes
|
|
171
|
+
(when present) are valid on. A missing key means the row has no single
|
|
172
|
+
source page: it was transformed by the merger (stitched continuation,
|
|
173
|
+
folded overflow — which can legitimately span two pages) and carries no
|
|
174
|
+
geometry, or its source fragment exposed no page number. Header rows
|
|
175
|
+
preserved from the anchor map to the anchor's page. Populated during
|
|
176
|
+
injection; empty for tables that were not merged.
|
|
177
|
+
"""
|
|
@@ -3,6 +3,7 @@ Tests for the Docling adapter — DataFrame conversion, injection, and pruning.
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
from types import SimpleNamespace
|
|
6
|
+
from typing import Optional
|
|
6
7
|
|
|
7
8
|
import pandas as pd
|
|
8
9
|
import pytest
|
|
@@ -388,6 +389,158 @@ class TestBodySpanPreservation:
|
|
|
388
389
|
assert all(cell.col_span == 1 for cell in td.grid[1])
|
|
389
390
|
|
|
390
391
|
|
|
392
|
+
class TestBboxPreservation:
|
|
393
|
+
"""Re-emitted body rows must carry their source cells' bounding boxes.
|
|
394
|
+
|
|
395
|
+
The merge rebuilds the anchor's TableData from the merged DataFrame; rows
|
|
396
|
+
the merger left untouched are re-emitted from the original grid cells.
|
|
397
|
+
Dropping bbox there loses word-level grounding for every multi-page table.
|
|
398
|
+
"""
|
|
399
|
+
|
|
400
|
+
@staticmethod
|
|
401
|
+
def _cell(text, r, c, *, header=False, bbox=None):
|
|
402
|
+
return TableCell(
|
|
403
|
+
text=text,
|
|
404
|
+
bbox=bbox,
|
|
405
|
+
row_span=1,
|
|
406
|
+
col_span=1,
|
|
407
|
+
column_header=header,
|
|
408
|
+
row_header=False,
|
|
409
|
+
start_row_offset_idx=r,
|
|
410
|
+
end_row_offset_idx=r + 1,
|
|
411
|
+
start_col_offset_idx=c,
|
|
412
|
+
end_col_offset_idx=c + 1,
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
def _fragment_with_bboxes(self) -> TableData:
|
|
416
|
+
from docling_core.types.doc import BoundingBox
|
|
417
|
+
|
|
418
|
+
c = self._cell
|
|
419
|
+
h = [c("Name", 0, 0, header=True), c("Value", 0, 1, header=True)]
|
|
420
|
+
r1 = [
|
|
421
|
+
c("alpha", 1, 0, bbox=BoundingBox(l=10, t=100, r=60, b=110)),
|
|
422
|
+
c("1", 1, 1, bbox=BoundingBox(l=70, t=100, r=90, b=110)),
|
|
423
|
+
]
|
|
424
|
+
return TableData(num_rows=2, num_cols=2, table_cells=h + r1, grid=[h, r1])
|
|
425
|
+
|
|
426
|
+
def test_reemitted_rows_keep_bbox(self):
|
|
427
|
+
original = self._fragment_with_bboxes()
|
|
428
|
+
merged_df = pd.DataFrame([["alpha", "1"]], columns=["Name", "Value"])
|
|
429
|
+
|
|
430
|
+
td = _dataframe_to_docling_data(merged_df, original_data=original, member_data=[original])
|
|
431
|
+
|
|
432
|
+
body_row = td.grid[1]
|
|
433
|
+
assert body_row[0].bbox is not None
|
|
434
|
+
assert body_row[0].bbox.l == 10 and body_row[0].bbox.t == 100
|
|
435
|
+
assert body_row[1].bbox is not None
|
|
436
|
+
assert body_row[1].bbox.l == 70
|
|
437
|
+
|
|
438
|
+
def test_flat_rebuild_rows_have_no_bbox(self):
|
|
439
|
+
"""Transformed rows (no member-row match) have no geometry to carry."""
|
|
440
|
+
original = self._fragment_with_bboxes()
|
|
441
|
+
merged_df = pd.DataFrame([["alpha (stitched)", "1"]], columns=["Name", "Value"])
|
|
442
|
+
|
|
443
|
+
td = _dataframe_to_docling_data(merged_df, original_data=original, member_data=[original])
|
|
444
|
+
|
|
445
|
+
assert all(cell.bbox is None for cell in td.grid[1])
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
class TestRowPageMap:
|
|
449
|
+
"""row_pages: grid row index -> resolved page_no for restored geometry.
|
|
450
|
+
|
|
451
|
+
A merged table's prov lists all N source pages, but pages share one
|
|
452
|
+
coordinate space, so a restored bbox is ambiguous without a per-row page
|
|
453
|
+
association. The map is recorded at merge time (the only point where each
|
|
454
|
+
row's source fragment is known) and keyed by grid row index — resolved
|
|
455
|
+
page_no, not prov index, so it survives downstream prov manipulation.
|
|
456
|
+
A missing key is the honest signal for a transformed row.
|
|
457
|
+
"""
|
|
458
|
+
|
|
459
|
+
@staticmethod
|
|
460
|
+
def _cell(text, r, c, *, header=False):
|
|
461
|
+
return TableCell(
|
|
462
|
+
text=text,
|
|
463
|
+
row_span=1,
|
|
464
|
+
col_span=1,
|
|
465
|
+
column_header=header,
|
|
466
|
+
row_header=False,
|
|
467
|
+
start_row_offset_idx=r,
|
|
468
|
+
end_row_offset_idx=r + 1,
|
|
469
|
+
start_col_offset_idx=c,
|
|
470
|
+
end_col_offset_idx=c + 1,
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
def _one_col_fragment(self, texts: list[str], *, header: Optional[str] = None) -> TableData:
|
|
474
|
+
rows = []
|
|
475
|
+
r = 0
|
|
476
|
+
if header is not None:
|
|
477
|
+
rows.append([self._cell(header, 0, 0, header=True)])
|
|
478
|
+
r = 1
|
|
479
|
+
for i, t in enumerate(texts):
|
|
480
|
+
rows.append([self._cell(t, r + i, 0)])
|
|
481
|
+
flat = [c for row in rows for c in row]
|
|
482
|
+
return TableData(num_rows=len(rows), num_cols=1, table_cells=flat, grid=rows)
|
|
483
|
+
|
|
484
|
+
def test_rebuild_maps_rows_to_source_pages(self):
|
|
485
|
+
anchor = self._one_col_fragment(["alpha"], header="Name")
|
|
486
|
+
satellite = self._one_col_fragment(["beta"])
|
|
487
|
+
merged_df = pd.DataFrame([["alpha"], ["beta"], ["gamma (stitched)"]], columns=["Name"])
|
|
488
|
+
|
|
489
|
+
row_pages: dict[int, int] = {}
|
|
490
|
+
_dataframe_to_docling_data(
|
|
491
|
+
merged_df,
|
|
492
|
+
original_data=anchor,
|
|
493
|
+
member_data=[anchor, satellite],
|
|
494
|
+
member_pages=[1, 2],
|
|
495
|
+
row_pages_out=row_pages,
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
# Grid: row 0 = anchor header, row 1 = alpha (page 1),
|
|
499
|
+
# row 2 = beta (page 2), row 3 = transformed -> no key.
|
|
500
|
+
assert row_pages == {0: 1, 1: 1, 2: 2}
|
|
501
|
+
|
|
502
|
+
def test_unknown_member_page_yields_no_key(self):
|
|
503
|
+
anchor = self._one_col_fragment(["alpha"], header="Name")
|
|
504
|
+
satellite = self._one_col_fragment(["beta"])
|
|
505
|
+
merged_df = pd.DataFrame([["alpha"], ["beta"]], columns=["Name"])
|
|
506
|
+
|
|
507
|
+
row_pages: dict[int, int] = {}
|
|
508
|
+
_dataframe_to_docling_data(
|
|
509
|
+
merged_df,
|
|
510
|
+
original_data=anchor,
|
|
511
|
+
member_data=[anchor, satellite],
|
|
512
|
+
member_pages=[1, None],
|
|
513
|
+
row_pages_out=row_pages,
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
assert row_pages == {0: 1, 1: 1} # beta's fragment has no page -> no key
|
|
517
|
+
|
|
518
|
+
def test_inject_populates_logical_table_row_pages(self):
|
|
519
|
+
doc = _build_doc_with_tables(2)
|
|
520
|
+
doc.tables[0].prov = [SimpleNamespace(page_no=1, bbox=None)]
|
|
521
|
+
doc.tables[1].prov = [SimpleNamespace(page_no=2, bbox=None)]
|
|
522
|
+
|
|
523
|
+
# Rows V0 / V1 match the fragments' original body rows untouched.
|
|
524
|
+
merged_df = pd.DataFrame({"H0": ["V0", "V1"]})
|
|
525
|
+
lt = LogicalTable(0, [0, 1], [1, 2], merged_df)
|
|
526
|
+
|
|
527
|
+
adapter = DoclingAdapter()
|
|
528
|
+
adapter.inject(doc, [lt])
|
|
529
|
+
|
|
530
|
+
# Grid row 0 = anchor header (page 1), row 1 = V0 (page 1),
|
|
531
|
+
# row 2 = V1 (page 2).
|
|
532
|
+
assert lt.row_pages == {0: 1, 1: 1, 2: 2}
|
|
533
|
+
|
|
534
|
+
def test_single_member_table_has_empty_row_pages(self):
|
|
535
|
+
doc = _build_doc_with_tables(1)
|
|
536
|
+
lt = LogicalTable(0, [0], [1], pd.DataFrame({"H0": ["V0"]}))
|
|
537
|
+
|
|
538
|
+
adapter = DoclingAdapter()
|
|
539
|
+
adapter.inject(doc, [lt])
|
|
540
|
+
|
|
541
|
+
assert lt.row_pages == {}
|
|
542
|
+
|
|
543
|
+
|
|
391
544
|
class TestReprintedHeaderDedup:
|
|
392
545
|
"""Reprinted continuation-page headers are dropped from the injected body,
|
|
393
546
|
but column_header-flagged rows that don't match the header (Docling
|
|
@@ -81,3 +81,56 @@ def test_table_stitcher_class_signature():
|
|
|
81
81
|
|
|
82
82
|
def test_stitching_error_is_exception():
|
|
83
83
|
assert issubclass(table_stitcher.StitchingError, Exception)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_last_logical_tables_exposed_after_stitch():
|
|
87
|
+
"""`TableStitcher.last_logical_tables` surfaces the merge results.
|
|
88
|
+
|
|
89
|
+
`stitch()` returns only the document, but grounding consumers need the
|
|
90
|
+
per-table merge results — notably `LogicalTable.row_pages`, filled in
|
|
91
|
+
during injection. The stitcher keeps its most recent results reachable.
|
|
92
|
+
"""
|
|
93
|
+
import pandas as pd
|
|
94
|
+
|
|
95
|
+
from table_stitcher.merger import is_numeric_like_colnames, tokenize
|
|
96
|
+
from table_stitcher.models import TableMeta
|
|
97
|
+
|
|
98
|
+
def _meta(idx: int, start_page: int, *, headerless: bool) -> TableMeta:
|
|
99
|
+
df = pd.DataFrame({"Name": ["Alice"], "Age": ["30"]})
|
|
100
|
+
return TableMeta(
|
|
101
|
+
idx=idx,
|
|
102
|
+
df=df,
|
|
103
|
+
start_page=start_page,
|
|
104
|
+
pages=[start_page],
|
|
105
|
+
width=df.shape[1],
|
|
106
|
+
header_tokens=tokenize(" ".join(str(c) for c in df.columns)),
|
|
107
|
+
first_row_tokens=tokenize(" ".join(str(x) for x in df.iloc[0].tolist())),
|
|
108
|
+
raw_columns=[str(c) for c in df.columns],
|
|
109
|
+
vert_center=None,
|
|
110
|
+
vert_top=None,
|
|
111
|
+
vert_bottom=None,
|
|
112
|
+
is_header_orphan=False,
|
|
113
|
+
is_data_orphan=False,
|
|
114
|
+
numeric_like_cols=is_numeric_like_colnames([str(c) for c in df.columns]),
|
|
115
|
+
row_count=df.shape[0],
|
|
116
|
+
is_headerless=headerless,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
class _StubAdapter:
|
|
120
|
+
def extract(self, doc, config):
|
|
121
|
+
return [_meta(0, 1, headerless=False), _meta(1, 2, headerless=True)]
|
|
122
|
+
|
|
123
|
+
def inject(self, doc, logical_tables):
|
|
124
|
+
for lt in logical_tables:
|
|
125
|
+
lt.row_pages = {0: 1}
|
|
126
|
+
return doc
|
|
127
|
+
|
|
128
|
+
stitcher = table_stitcher.TableStitcher(_StubAdapter())
|
|
129
|
+
assert stitcher.last_logical_tables == []
|
|
130
|
+
|
|
131
|
+
stitcher.stitch(object())
|
|
132
|
+
|
|
133
|
+
assert len(stitcher.last_logical_tables) == 1
|
|
134
|
+
lt = stitcher.last_logical_tables[0]
|
|
135
|
+
assert lt.members == [0, 1]
|
|
136
|
+
assert lt.row_pages == {0: 1}
|
|
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
|
{table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/fixtures/tablemeta/headerless-width-drift.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/_tools/regenerate_expected.py
RENAMED
|
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
|
|
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
|
{table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/orphan-pair/.gitkeep
RENAMED
|
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
|
|
File without changes
|
{table_stitcher-0.4.4 → table_stitcher-0.5.0}/tests/integration/fixtures/width-drift/.gitkeep
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|