Datagrunt 5.3.2: PDF Stream Line Assembly & CI Workflow Deduplication

November 9, 2026by Martin Graham

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

  1. PDF Stream Line Assembly & Isolation (#476, #480) — Introduces _OpenLine and bounded neighbor checks in src/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.
  2. Version Bump Serialization & Tag Check (#483) — Adds concurrency controls (concurrency.group) to bump-version.yml and HEAD release tag checking to prevent back-to-back squash merges from racing tag creation or executing duplicate version bumps.
  3. Deduplicated Post-Merge Push Builds (#483) — Skips redundant Python compatibility matrices, wheel matrix builds, and advisory dependency audit runs on post-merge push events to main, saving GitHub Actions runner resources while maintaining full PR status check coverage.
  4. PyPI Release Guard (#483) — Ensures release artifact builds in publish-to-pypi.yml only trigger upon successful completion of predecessor workflow runs.
  5. Action Runner Upgrades (#468) — Bumps actions/setup-python to version 7.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: false

The 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: true

PyPI 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 _OpenLine class, 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 and already_tagged HEAD 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: Upgraded actions/setup-python to v7.1.0.

Upgrading to 5.3.2

To update to Datagrunt 5.3.2 using uv:

uv pip install --upgrade datagrunt

Or with standard pip:

pip install --upgrade datagrunt