table-stitcher 0.4.0__tar.gz → 0.4.2__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.0 → table_stitcher-0.4.2}/CHANGELOG.md +28 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/PKG-INFO +1 -1
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/pyproject.toml +1 -1
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/scripts/release_gate.sh +8 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/src/table_stitcher/__init__.py +8 -1
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/src/table_stitcher/adapters/docling.py +114 -6
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/test_docling_adapter.py +88 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/.claude/settings.json +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/.github/dependabot.yml +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/.github/pull_request_template.md +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/.github/workflows/ci.yml +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/.github/workflows/release.yml +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/.github/workflows/upstream-smoke.yml +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/.gitignore +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/.pre-commit-config.yaml +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/CONTRIBUTING.md +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/LICENSE +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/README.md +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/SECURITY.md +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/examples/basic_pipeline.py +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/examples/system_controller.py +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/scripts/regenerate_docling_snapshots.py +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/src/table_stitcher/adapters/README.md +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/src/table_stitcher/adapters/__init__.py +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/src/table_stitcher/adapters/base.py +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/src/table_stitcher/merger.py +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/src/table_stitcher/models.py +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/src/table_stitcher/py.typed +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/README.md +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/__init__.py +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/fixtures/tablemeta/headerless-width-drift.yaml +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/__init__.py +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/_tools/__init__.py +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/_tools/regenerate_expected.py +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/conftest.py +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/_synth/__init__.py +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/_synth/generate.py +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/distinct-tables-no-merge/.gitkeep +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/distinct-tables-no-merge/kaop-study-mixed-3pg.pt2.docling.json +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/distinct-tables-no-merge/kaop-study-mixed-3pg.pt2.expected.yaml +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/distinct-tables-no-merge/kaop-study-mixed-3pg.pt2.pdf +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/distinct-tables-no-merge/lab-panels-3pg.corp.docling.json +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/distinct-tables-no-merge/lab-panels-3pg.corp.expected.yaml +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/distinct-tables-no-merge/lab-panels-3pg.corp.pdf +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/false-merge/category-rows-thematic-2pg.corp.docling.json +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/false-merge/category-rows-thematic-2pg.corp.expected.yaml +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/false-merge/category-rows-thematic-2pg.corp.pdf +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/headerless-continuation/15-page-druglist.corp.docling.json +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/headerless-continuation/15-page-druglist.corp.expected.yaml +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/headerless-continuation/15-page-druglist.corp.pdf +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/headerless-continuation/gene-symbols-6pg.pt2.docling.json +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/headerless-continuation/gene-symbols-6pg.pt2.expected.yaml +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/headerless-continuation/gene-symbols-6pg.pt2.pdf +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/headerless-continuation/rct-study-table-5pg.pt2.docling.json +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/headerless-continuation/rct-study-table-5pg.pt2.expected.yaml +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/headerless-continuation/rct-study-table-5pg.pt2.pdf +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/headerless-continuation/rrt-outcomes-2pg.pt2.docling.json +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/headerless-continuation/rrt-outcomes-2pg.pt2.expected.yaml +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/headerless-continuation/rrt-outcomes-2pg.pt2.pdf +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/headerless-continuation/uveitis-case-series-5pg.pt2.docling.json +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/headerless-continuation/uveitis-case-series-5pg.pt2.expected.yaml +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/headerless-continuation/uveitis-case-series-5pg.pt2.pdf +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/inconsistent-header-detection/covid-misc-labs-4pg.pt2.docling.json +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/inconsistent-header-detection/covid-misc-labs-4pg.pt2.expected.yaml +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/inconsistent-header-detection/covid-misc-labs-4pg.pt2.pdf +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/inconsistent-header-detection/lit-review-3pg.pt2.docling.json +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/inconsistent-header-detection/lit-review-3pg.pt2.expected.yaml +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/inconsistent-header-detection/lit-review-3pg.pt2.pdf +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/inconsistent-header-detection/retirement-portfolio.corp.docling.json +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/inconsistent-header-detection/retirement-portfolio.corp.expected.yaml +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/inconsistent-header-detection/retirement-portfolio.corp.pdf +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/loose-header-layout/.gitkeep +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/loose-header-layout/biological-process-2pg.pt2.docling.json +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/loose-header-layout/biological-process-2pg.pt2.expected.yaml +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/loose-header-layout/biological-process-2pg.pt2.pdf +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/loose-header-layout/symptoms-mediators-4pg.pt2.docling.json +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/loose-header-layout/symptoms-mediators-4pg.pt2.expected.yaml +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/loose-header-layout/symptoms-mediators-4pg.pt2.pdf +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/multilingual/corporate-history-2pg.edinet.docling.json +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/multilingual/corporate-history-2pg.edinet.expected.yaml +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/multilingual/corporate-history-2pg.edinet.pdf +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/multilingual/subsidiaries-4pg.edinet.docling.json +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/multilingual/subsidiaries-4pg.edinet.expected.yaml +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/multilingual/subsidiaries-4pg.edinet.pdf +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/orphan-pair/.gitkeep +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/orphan-pair/varicose-veins-new-table-header-7pg.pt2.docling.json +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/orphan-pair/varicose-veins-new-table-header-7pg.pt2.expected.yaml +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/orphan-pair/varicose-veins-new-table-header-7pg.pt2.pdf +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/page-gap-too-large/unrelated-tables-gap4.synth.docling.json +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/page-gap-too-large/unrelated-tables-gap4.synth.expected.yaml +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/page-gap-too-large/unrelated-tables-gap4.synth.pdf +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/repeated-header/4-page-substance-list.corp.docling.json +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/repeated-header/4-page-substance-list.corp.expected.yaml +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/repeated-header/4-page-substance-list.corp.pdf +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/repeated-header/cell-markers-4pg.pt2.docling.json +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/repeated-header/cell-markers-4pg.pt2.expected.yaml +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/repeated-header/cell-markers-4pg.pt2.pdf +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/repeated-header/challenge-categories-2pg.pt2.docling.json +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/repeated-header/challenge-categories-2pg.pt2.expected.yaml +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/repeated-header/challenge-categories-2pg.pt2.pdf +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/repeated-header/fungal-taxonomy-4pg.pt2.docling.json +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/repeated-header/fungal-taxonomy-4pg.pt2.expected.yaml +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/repeated-header/fungal-taxonomy-4pg.pt2.pdf +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/repeated-header/rowspan-insurance-payout.corp.docling.json +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/repeated-header/rowspan-insurance-payout.corp.expected.yaml +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/repeated-header/rowspan-insurance-payout.corp.pdf +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/repeated-header/search-strategies-2pg.pt2.docling.json +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/repeated-header/search-strategies-2pg.pt2.expected.yaml +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/repeated-header/search-strategies-2pg.pt2.pdf +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/repeated-header/study-sample-7pg.pt2.docling.json +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/repeated-header/study-sample-7pg.pt2.expected.yaml +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/repeated-header/study-sample-7pg.pt2.pdf +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/repeated-header/themes-exemplars-3pg.pt2.docling.json +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/repeated-header/themes-exemplars-3pg.pt2.expected.yaml +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/repeated-header/themes-exemplars-3pg.pt2.pdf +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/simple-continuation/sample-table.corp.docling.json +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/simple-continuation/sample-table.corp.expected.yaml +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/simple-continuation/sample-table.corp.pdf +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/spillover/note-overflow.synth.docling.json +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/spillover/note-overflow.synth.expected.yaml +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/spillover/note-overflow.synth.pdf +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/width-drift/.gitkeep +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/width-drift/abx-literature-review-7pg.pt2.docling.json +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/width-drift/abx-literature-review-7pg.pt2.expected.yaml +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/fixtures/width-drift/abx-literature-review-7pg.pt2.pdf +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/integration/test_fixtures.py +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/test_intervening_content_guard.py +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/test_merger.py +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/test_public_api.py +0 -0
- {table_stitcher-0.4.0 → table_stitcher-0.4.2}/tests/test_tablemeta_fixtures.py +0 -0
|
@@ -7,6 +7,34 @@ the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.2] — 2026-06-08
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **`__version__` was hardcoded and stale** (`__init__.py`). It read `"0.2.0"`
|
|
15
|
+
regardless of the installed release, since nothing tied it to the version in
|
|
16
|
+
`pyproject.toml`. It is now derived from the installed distribution metadata
|
|
17
|
+
via `importlib.metadata.version("table-stitcher")`, so it always reflects the
|
|
18
|
+
actual release (falling back to `"0.0.0+unknown"` when run from an
|
|
19
|
+
uninstalled source tree). The release gate now also asserts
|
|
20
|
+
`__version__` matches the `pyproject.toml` version, so the two can't drift
|
|
21
|
+
again.
|
|
22
|
+
|
|
23
|
+
## [0.4.1] — 2026-06-08
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- **Spanning body cells duplicated across columns on multi-page merge**
|
|
28
|
+
(`adapters/docling.py`). Docling repeats a `col_span=N` cell's text across
|
|
29
|
+
every column it covers; the merge round-trip rebuilt those as `N` separate
|
|
30
|
+
`col_span=1` cells, leaking a full-width description into every value column
|
|
31
|
+
and displacing the real values (a repeated `col_span` header behaved the same
|
|
32
|
+
way). Injection now matches each merged row back to its source grid row and
|
|
33
|
+
re-emits the original spans; rows the merger transformed (stitched
|
|
34
|
+
continuations, folded overflow) fall back to the flat 1x1 rebuild. The match
|
|
35
|
+
uses the original span metadata, never value equality, so coincidentally-equal
|
|
36
|
+
adjacent values (e.g. two plan columns sharing a cap) stay separate cells.
|
|
37
|
+
|
|
10
38
|
## [0.4.0] — 2026-05-29
|
|
11
39
|
|
|
12
40
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: table-stitcher
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
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
|
|
@@ -69,15 +69,23 @@ else
|
|
|
69
69
|
fi
|
|
70
70
|
|
|
71
71
|
echo "==> Smoke-testing installed wheel outside checkout"
|
|
72
|
+
EXPECTED_VERSION="$(grep -E '^version = ' pyproject.toml | head -1 | sed -E 's/.*"([^"]+)".*/\1/')"
|
|
73
|
+
export EXPECTED_VERSION
|
|
72
74
|
(
|
|
73
75
|
cd "$TMP_ENV"
|
|
74
76
|
"$TMP_ENV/venv/bin/python" - <<'PY'
|
|
77
|
+
import os
|
|
75
78
|
import pathlib
|
|
79
|
+
|
|
76
80
|
import table_stitcher
|
|
77
81
|
|
|
78
82
|
package_path = pathlib.Path(table_stitcher.__file__).resolve()
|
|
83
|
+
expected = os.environ["EXPECTED_VERSION"]
|
|
79
84
|
assert "site-packages" in str(package_path), package_path
|
|
80
85
|
assert table_stitcher.__version__, "missing __version__"
|
|
86
|
+
assert table_stitcher.__version__ == expected, (
|
|
87
|
+
f"__version__ {table_stitcher.__version__!r} != pyproject version {expected!r}"
|
|
88
|
+
)
|
|
81
89
|
assert callable(table_stitcher.stitch_tables)
|
|
82
90
|
print(f"installed {table_stitcher.__version__} from {package_path}")
|
|
83
91
|
PY
|
|
@@ -24,13 +24,20 @@ Usage (custom parser):
|
|
|
24
24
|
|
|
25
25
|
import logging
|
|
26
26
|
import time
|
|
27
|
+
from importlib.metadata import PackageNotFoundError
|
|
28
|
+
from importlib.metadata import version as _pkg_version
|
|
27
29
|
from typing import Any, Optional
|
|
28
30
|
|
|
29
31
|
from .adapters.base import TableStitcherAdapter
|
|
30
32
|
from .merger import merge_multipage_tables
|
|
31
33
|
from .models import LogicalTable, MergeTrace, MultiPageConfig, TableMeta
|
|
32
34
|
|
|
33
|
-
|
|
35
|
+
# Single source of truth: the installed distribution's version (from
|
|
36
|
+
# pyproject.toml). Derived rather than hardcoded so it can never drift.
|
|
37
|
+
try:
|
|
38
|
+
__version__ = _pkg_version("table-stitcher")
|
|
39
|
+
except PackageNotFoundError: # running from a source tree without an install
|
|
40
|
+
__version__ = "0.0.0+unknown"
|
|
34
41
|
__all__ = [
|
|
35
42
|
"stitch_tables",
|
|
36
43
|
"extract_table_meta",
|
|
@@ -419,9 +419,90 @@ def _extract_original_header_rows(
|
|
|
419
419
|
return header_rows, header_cells
|
|
420
420
|
|
|
421
421
|
|
|
422
|
+
def _index_member_rows(
|
|
423
|
+
member_data: list[Optional[TableData]],
|
|
424
|
+
) -> dict[tuple, list[list[TableCell]]]:
|
|
425
|
+
"""
|
|
426
|
+
Index every member fragment's original grid rows by their expanded
|
|
427
|
+
text-vector, for col_span reconstruction during injection.
|
|
428
|
+
|
|
429
|
+
Docling repeats a spanning cell's text across each column it covers, so the
|
|
430
|
+
text-vector of an original grid row equals the DataFrame row that
|
|
431
|
+
``_grid_to_dataframe`` produced for it (for any row the merger left
|
|
432
|
+
untouched). Keying on that vector lets ``_dataframe_to_docling_data``
|
|
433
|
+
recover the original col_span structure instead of flattening every cell to
|
|
434
|
+
1x1 — which duplicates a spanning cell into every column it covered on a
|
|
435
|
+
multi-page merge.
|
|
436
|
+
|
|
437
|
+
All rows are indexed, including header rows: a satellite fragment's repeated
|
|
438
|
+
header (e.g. a ``col_span=6`` banner) arrives in the merged DataFrame as a
|
|
439
|
+
body row, and must match its original spanning cell rather than duplicate
|
|
440
|
+
across the value columns. (The anchor's own header rows are reconstructed
|
|
441
|
+
separately and never looked up here.) Values are buckets because a row can
|
|
442
|
+
legitimately repeat; identical text-vectors imply identical span structure,
|
|
443
|
+
so any occurrence is interchangeable.
|
|
444
|
+
"""
|
|
445
|
+
index: dict[tuple, list[list[TableCell]]] = {}
|
|
446
|
+
for data in member_data:
|
|
447
|
+
if not data or not data.grid:
|
|
448
|
+
continue
|
|
449
|
+
for row in data.grid:
|
|
450
|
+
if not row:
|
|
451
|
+
continue
|
|
452
|
+
key = tuple((getattr(c, "text", "") or "") if c else "" for c in row)
|
|
453
|
+
index.setdefault(key, []).append(row)
|
|
454
|
+
return index
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def _reemit_body_row(
|
|
458
|
+
orig_row: list[TableCell], table_row_idx: int, has_row_headers: bool
|
|
459
|
+
) -> tuple[list[TableCell], list[TableCell]]:
|
|
460
|
+
"""
|
|
461
|
+
Re-emit an original grid body row at a new row offset, preserving col_span.
|
|
462
|
+
|
|
463
|
+
Returns ``(grid_row, distinct_cells)`` where ``grid_row`` repeats each
|
|
464
|
+
spanning cell across the columns it covers (Docling grid convention) and
|
|
465
|
+
``distinct_cells`` lists each origin cell once (for ``table_cells``).
|
|
466
|
+
|
|
467
|
+
row_span is intentionally clamped to 1: the merged DataFrame represents one
|
|
468
|
+
logical row per grid row, so a multi-row body span cannot be expressed
|
|
469
|
+
without desynchronizing the rebuilt grid. (Body row_spans are rare; col_span
|
|
470
|
+
is the case that corrupts multi-page merges.)
|
|
471
|
+
"""
|
|
472
|
+
grid_row: list[Optional[TableCell]] = []
|
|
473
|
+
distinct: list[TableCell] = []
|
|
474
|
+
for c_idx, cell in enumerate(orig_row):
|
|
475
|
+
if cell is None:
|
|
476
|
+
grid_row.append(None)
|
|
477
|
+
continue
|
|
478
|
+
start_col = getattr(cell, "start_col_offset_idx", c_idx)
|
|
479
|
+
if start_col == c_idx:
|
|
480
|
+
col_span = getattr(cell, "col_span", 1) or 1
|
|
481
|
+
new_cell = TableCell(
|
|
482
|
+
text=getattr(cell, "text", "") or "",
|
|
483
|
+
row_span=1,
|
|
484
|
+
col_span=col_span,
|
|
485
|
+
column_header=False,
|
|
486
|
+
row_header=(c_idx == 0 and has_row_headers)
|
|
487
|
+
or bool(getattr(cell, "row_header", False)),
|
|
488
|
+
start_row_offset_idx=table_row_idx,
|
|
489
|
+
end_row_offset_idx=table_row_idx + 1,
|
|
490
|
+
start_col_offset_idx=c_idx,
|
|
491
|
+
end_col_offset_idx=c_idx + col_span,
|
|
492
|
+
)
|
|
493
|
+
distinct.append(new_cell)
|
|
494
|
+
grid_row.append(new_cell)
|
|
495
|
+
else:
|
|
496
|
+
# Continuation column of a span originating to the left: repeat the
|
|
497
|
+
# same cell object, which was already appended at ``start_col``.
|
|
498
|
+
grid_row.append(grid_row[start_col] if start_col < len(grid_row) else None)
|
|
499
|
+
return grid_row, distinct
|
|
500
|
+
|
|
501
|
+
|
|
422
502
|
def _dataframe_to_docling_data(
|
|
423
503
|
df: pd.DataFrame,
|
|
424
504
|
original_data: Optional[TableData] = None,
|
|
505
|
+
member_data: Optional[list[Optional[TableData]]] = None,
|
|
425
506
|
) -> TableData:
|
|
426
507
|
"""
|
|
427
508
|
Converts a pandas DataFrame back into Docling's TableData structure.
|
|
@@ -431,6 +512,12 @@ def _dataframe_to_docling_data(
|
|
|
431
512
|
are preserved exactly. Only the data rows are rebuilt from the DataFrame.
|
|
432
513
|
This prevents the lossy roundtrip that would flatten complex headers into
|
|
433
514
|
simple 1x1 cells.
|
|
515
|
+
|
|
516
|
+
When ``member_data`` (the original ``TableData`` of every fragment in the
|
|
517
|
+
logical table) is provided, body rows the merger left untouched are
|
|
518
|
+
re-emitted from their original grid cells, preserving col_span. Rows the
|
|
519
|
+
merger transformed (stitched continuations, folded overflow) fall back to a
|
|
520
|
+
flat 1x1 rebuild from the DataFrame.
|
|
434
521
|
"""
|
|
435
522
|
if df.empty:
|
|
436
523
|
cols = list(df.columns) if len(df.columns) > 0 else ["Column_0"]
|
|
@@ -499,16 +586,28 @@ def _dataframe_to_docling_data(
|
|
|
499
586
|
break
|
|
500
587
|
|
|
501
588
|
# --- Build data rows from merged DataFrame ---
|
|
589
|
+
# Index member fragments' original body rows so spanning cells survive the
|
|
590
|
+
# round-trip (see _index_member_body_rows).
|
|
591
|
+
body_index = _index_member_rows(member_data) if member_data else {}
|
|
592
|
+
|
|
502
593
|
for i, (_, row) in enumerate(df.iterrows()):
|
|
503
|
-
grid_row: list[TableCell] = []
|
|
504
594
|
table_row_idx = num_header_rows + i
|
|
505
595
|
|
|
506
|
-
for
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
596
|
+
row_vals = ["" if (pd.isna(v) or v is None) else str(v) for v in row]
|
|
597
|
+
|
|
598
|
+
# Re-emit untouched rows from their original grid cells (preserves
|
|
599
|
+
# col_span); only matches when widths align, so coincidentally-equal
|
|
600
|
+
# adjacent values are never fused.
|
|
601
|
+
bucket = body_index.get(tuple(row_vals))
|
|
602
|
+
if bucket:
|
|
603
|
+
orig_row = bucket.pop(0)
|
|
604
|
+
grid_row, distinct = _reemit_body_row(orig_row, table_row_idx, has_row_headers)
|
|
605
|
+
grid.append(grid_row)
|
|
606
|
+
table_cells.extend(distinct)
|
|
607
|
+
continue
|
|
511
608
|
|
|
609
|
+
grid_row: list[TableCell] = []
|
|
610
|
+
for j, text_val in enumerate(row_vals):
|
|
512
611
|
row_header = j == 0 and has_row_headers
|
|
513
612
|
|
|
514
613
|
cell = TableCell(
|
|
@@ -899,9 +998,18 @@ class DoclingAdapter:
|
|
|
899
998
|
|
|
900
999
|
original_data = getattr(anchor_table, "data", None)
|
|
901
1000
|
|
|
1001
|
+
# Original TableData of every fragment, captured in
|
|
1002
|
+
# table_snapshots before any mutation, so injection can recover
|
|
1003
|
+
# each untouched body row's col_span (see
|
|
1004
|
+
# _index_member_body_rows).
|
|
1005
|
+
member_data = [
|
|
1006
|
+
table_snapshots[m]["data"] for m in lt.members if m in table_snapshots
|
|
1007
|
+
]
|
|
1008
|
+
|
|
902
1009
|
anchor_table.data = _dataframe_to_docling_data(
|
|
903
1010
|
lt.df,
|
|
904
1011
|
original_data=original_data,
|
|
1012
|
+
member_data=member_data,
|
|
905
1013
|
)
|
|
906
1014
|
|
|
907
1015
|
for satellite_idx in lt.members[1:]:
|
|
@@ -300,6 +300,94 @@ class TestHeaderPreservation:
|
|
|
300
300
|
assert td.num_rows == 3 # 1 header + 2 data
|
|
301
301
|
|
|
302
302
|
|
|
303
|
+
class TestBodySpanPreservation:
|
|
304
|
+
"""Body col_span cells must survive the merge round-trip, not duplicate.
|
|
305
|
+
|
|
306
|
+
Docling repeats a spanning cell's text across every column it covers, so a
|
|
307
|
+
naive grid -> DataFrame -> grid rebuild flattens a ``col_span=N`` body cell
|
|
308
|
+
into N duplicate ``col_span=1`` cells — leaking a full-width description
|
|
309
|
+
into every value column and displacing the real values. Passing the member
|
|
310
|
+
fragments' original TableData lets injection re-emit the original spans.
|
|
311
|
+
"""
|
|
312
|
+
|
|
313
|
+
@staticmethod
|
|
314
|
+
def _cell(text, r, c, *, col_span=1, header=False):
|
|
315
|
+
return TableCell(
|
|
316
|
+
text=text,
|
|
317
|
+
row_span=1,
|
|
318
|
+
col_span=col_span,
|
|
319
|
+
column_header=header,
|
|
320
|
+
row_header=False,
|
|
321
|
+
start_row_offset_idx=r,
|
|
322
|
+
end_row_offset_idx=r + 1,
|
|
323
|
+
start_col_offset_idx=c,
|
|
324
|
+
end_col_offset_idx=c + col_span,
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
def _fragment(self) -> TableData:
|
|
328
|
+
"""3-col fragment: flat header, a col_span=3 description row, a data row,
|
|
329
|
+
and a row with coincidentally-equal adjacent values (separate cells)."""
|
|
330
|
+
c = self._cell
|
|
331
|
+
# Header row 0
|
|
332
|
+
h = [
|
|
333
|
+
c("Section", 0, 0, header=True),
|
|
334
|
+
c("Plan A", 0, 1, header=True),
|
|
335
|
+
c("Plan B", 0, 2, header=True),
|
|
336
|
+
]
|
|
337
|
+
# Row 1: description spanning all 3 cols — grid repeats the same object.
|
|
338
|
+
desc = c("See important notes below", 1, 0, col_span=3)
|
|
339
|
+
r1 = [desc, desc, desc]
|
|
340
|
+
# Row 2: ordinary data row.
|
|
341
|
+
r2 = [c("1", 2, 0), c("100", 2, 1), c("200", 2, 2)]
|
|
342
|
+
# Row 3: two value columns share a cap value, but are SEPARATE cells.
|
|
343
|
+
r3 = [c("2", 3, 0), c("150", 3, 1), c("150", 3, 2)]
|
|
344
|
+
grid = [h, r1, r2, r3]
|
|
345
|
+
flat = h + [desc, r2[0], r2[1], r2[2], r3[0], r3[1], r3[2]]
|
|
346
|
+
return TableData(num_rows=4, num_cols=3, table_cells=flat, grid=grid)
|
|
347
|
+
|
|
348
|
+
def test_body_colspan_preserved_not_duplicated(self):
|
|
349
|
+
original = self._fragment()
|
|
350
|
+
# The DataFrame as _grid_to_dataframe would produce it: the spanning
|
|
351
|
+
# description duplicated across all three columns.
|
|
352
|
+
merged_df = pd.DataFrame(
|
|
353
|
+
[
|
|
354
|
+
["See important notes below"] * 3,
|
|
355
|
+
["1", "100", "200"],
|
|
356
|
+
["2", "150", "150"],
|
|
357
|
+
],
|
|
358
|
+
columns=["Section", "Plan A", "Plan B"],
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
td = _dataframe_to_docling_data(merged_df, original_data=original, member_data=[original])
|
|
362
|
+
|
|
363
|
+
# Description row: one origin cell with col_span=3, repeated across the
|
|
364
|
+
# grid row — NOT three distinct duplicated cells.
|
|
365
|
+
desc_row = td.grid[1]
|
|
366
|
+
assert desc_row[0].col_span == 3
|
|
367
|
+
assert desc_row[0].text == "See important notes below"
|
|
368
|
+
assert desc_row[1] is desc_row[0] and desc_row[2] is desc_row[0]
|
|
369
|
+
distinct_desc = [
|
|
370
|
+
cell for cell in td.table_cells if cell.text == "See important notes below"
|
|
371
|
+
]
|
|
372
|
+
assert len(distinct_desc) == 1
|
|
373
|
+
|
|
374
|
+
# Coincidentally-equal values stay as two separate col_span=1 cells.
|
|
375
|
+
last_row = td.grid[3]
|
|
376
|
+
assert last_row[1].text == last_row[2].text == "150"
|
|
377
|
+
assert last_row[1].col_span == 1 and last_row[2].col_span == 1
|
|
378
|
+
assert last_row[1] is not last_row[2]
|
|
379
|
+
|
|
380
|
+
def test_without_member_data_falls_back_to_flat(self):
|
|
381
|
+
"""No member_data -> previous behaviour: flat 1x1 body cells."""
|
|
382
|
+
original = self._fragment()
|
|
383
|
+
merged_df = pd.DataFrame(
|
|
384
|
+
[["See important notes below"] * 3],
|
|
385
|
+
columns=["Section", "Plan A", "Plan B"],
|
|
386
|
+
)
|
|
387
|
+
td = _dataframe_to_docling_data(merged_df, original_data=original)
|
|
388
|
+
assert all(cell.col_span == 1 for cell in td.grid[1])
|
|
389
|
+
|
|
390
|
+
|
|
303
391
|
class TestAdapterProtocol:
|
|
304
392
|
"""Verify DoclingAdapter satisfies the protocol."""
|
|
305
393
|
|
|
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.0 → table_stitcher-0.4.2}/tests/fixtures/tablemeta/headerless-width-drift.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{table_stitcher-0.4.0 → table_stitcher-0.4.2}/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.0 → table_stitcher-0.4.2}/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.0 → table_stitcher-0.4.2}/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
|
|
File without changes
|