Datagrunt 5.3.2: PDF Stream Line Assembly & CI Workflow Deduplication
Datagrunt 5.3.2 is now available! This patch release resolves a layout extraction issue where tall glyphs or large font elements could cause vertically adjacent text lines to interleave, while optimizing CI automation workflows and release delivery pipelines.
Key Highlights in Datagrunt 5.3.2
- PDF Stream Line Assembly & Isolation (#476, #480) — Introduces
_OpenLineand bounded neighbor checks insrc/datagrunt/pdf/layout.py. A text line now enforces vertical overlap bounds across every neighboring run along the line, ensuring tall characters (such as drop caps, large section numerals, or large ordinal indicators) never pull subsequent lines of text into an interleaved single line. - Version Bump Serialization & Tag Check (#483) — Adds concurrency controls (
concurrency.group) tobump-version.ymland HEAD release tag checking to prevent back-to-back squash merges from racing tag creation or executing duplicate version bumps. - Deduplicated Post-Merge Push Builds (#483) — Skips redundant Python compatibility matrices, wheel matrix builds, and advisory dependency audit runs on post-merge
pushevents tomain, saving GitHub Actions runner resources while maintaining full PR status check coverage. - PyPI Release Guard (#483) — Ensures release artifact builds in
publish-to-pypi.ymlonly trigger upon successful completion of predecessor workflow runs. - Action Runner Upgrades (#468) — Bumps
actions/setup-pythonto version7.1.0.
1. PDF Stream Line Assembly & Line Isolation (#476, #480)
The Layout Interleaving Challenge
In complex PDF documents — such as multi-column game manuals, financial statements, or magazine layouts — pages often feature large decorative drop caps or oversized step numbers (e.g., a 24 pt or 30 pt numeral “1” positioned beside 11 pt paragraph text).
Previously, line assembly in _lines() grouped runs based purely on the running vertical bounds (top and bottom) of the current line. When a tall glyph spanned the vertical range of two adjacent text lines, its bottom coordinate extended the line’s bounding box downward. Consequently, text runs from line 2 that fell under line 1’s earlier runs were incorrectly greedily merged into line 1:
# Before (5.3.1): Words on line 2 were pulled into line 1 beside the tall drop cap
"1st Choose Factions. Each player chooses a faction board and takes all of their pieces."Bounded Line Isolation via _OpenLine
Datagrunt 5.3.2 introduces the _OpenLine class and rewritten candidate line matching logic in src/datagrunt/pdf/layout.py:
- Line Band & Neighbor Overlap Verification (
_OpenLine.fit): A new text run candidate is evaluated against active open lines. A run fits an open line only if it overlaps the line’s overall band and every run beside it along the line by at least_LINE_OVERLAP(45%) of the smaller height. - Candidate Line Selection: If a run fits multiple open lines, Datagrunt selects the line offering the highest vertical overlap share (
share), minimum horizontal gap (gap), and oldest line age. - Strict Complexity Bounds:
MAX_OPEN_LINES = 16: Limits active open lines per pass (corpus analysis shows pages keep at most 5 open lines concurrently).MAX_NEIGHBOUR_CHECKS = 64: Bounds the horizontal neighbor lookups along each line.MAX_INDEXED_RUNS = 1024: Bounds interval indexing per line.
from datagrunt import PDFReader
# Extract PDF containing large section numbers or drop caps
reader = PDFReader("document_with_drop_caps.pdf")
# Line 1 and Line 2 are now cleanly separated despite the 30 pt drop cap
for element in reader.paragraphs:
print(f"[{element.element_id}] {element.text}")With this fix, lines maintain strict vertical separation:
# After (5.3.2): Clean vertical line separation
Line 1: "1st Choose Factions. Each player chooses a faction"
Line 2: "board and takes all of their pieces."2. GitHub Actions Workflow Optimization & Serialization (#483)
Datagrunt 5.3.2 enhances CI/CD efficiency and reliability across four GitHub Actions workflow configurations:
Version Bump Serialization (bump-version.yml)
Back-to-back PR merges could previously cause concurrent bumpver execution steps to race git push or tag creation. Datagrunt 5.3.2 adds explicit workflow concurrency and pre-flight tag detection:
concurrency:
group: bump-version-${{ github.event_name == 'workflow_dispatch' && github.run_id || 'main' }}
cancel-in-progress: falseThe workflow now checks git tag --points-at HEAD before determining the version bump type, cleanly skipping redundant bump execution if the target commit is already tagged.
Post-Merge Push Build Deduplication (ci.yml)
Every pull request in Datagrunt executes full matrix testing (Python 3.11–3.14), native wheel matrix builds, and advisory dependency audits as required status checks. Datagrunt 5.3.2 adds if: github.event_name != 'push' to these jobs in ci.yml, preventing duplicate build runs on post-merge pushes to main.
In addition, ci.yml now cancels superseded runs on rapid PR force-pushes:
concurrency:
group: ci-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: truePyPI Publish Chain Guard (publish-to-pypi.yml)
The PyPI publishing workflow now explicitly verifies workflow_run.conclusion == 'success' prior to building distribution wheels and source distributions (sdist), guaranteeing that release packages are only created from verified green builds.
Summary of Updated Files
src/datagrunt/pdf/layout.py: Introduced_OpenLineclass, bounded neighbor lookups, and line selection scoring in_lines().tests/pdf_tests/test_layout_stream.py&test_layout_lattice.py: Added comprehensive test cases covering tall glyphs, ordinal step indicators, drop caps, and line overlap bounds..github/workflows/bump-version.yml: Added concurrency serialization andalready_taggedHEAD check step..github/workflows/ci.yml: Deduplicated post-merge push jobs and added PR force-push concurrency cancellation..github/workflows/publish-to-pypi.yml: Added workflow run conclusion guard..github/workflows/build.yml: Upgradedactions/setup-pythontov7.1.0.
Upgrading to 5.3.2
To update to Datagrunt 5.3.2 using uv:
uv pip install --upgrade datagruntOr with standard pip:
pip install --upgrade datagrunt