Skip to main content

podpack-pdf

Two PDF utilities packaged as an installable Flask app:

  • PDF Booklet Maker — imposes an A4 document four-up onto A4 sheets so that each group of eight pages forms a signature, returned as a zip of the odd and even sides for duplex printing.
  • PDF Page Splitter — explodes a PDF into one file per page, returned as a zip.

Extracted from holdenweb.com with its history intact. It answers to two contracts and requires neither: a plain Flask blueprint, and a podpack app.

Install

uv add podpack-pdf

The distribution is podpack-pdf; the module, and the app's name everywhere podpack needs one, is podpack_pdf.


As a plain Flask blueprint

from flask import Flask
from podpack_pdf import pdf_blueprint

app = Flask(__name__)
app.config["SECRET_KEY"] = "..."          # required: the forms are CSRF-protected
app.register_blueprint(pdf_blueprint, url_prefix="/pdf/")

That yields three endpoints under the prefix you chose:

Route Purpose
/ Index listing both tools
/booklet Upload a PDF, receive a zip of imposed odd/even sides
/pagezip Upload a PDF, receive a zip of one file per page

Here the mount point is yours: the blueprint is relocatable, so mount it at /tools/pdf or anywhere else and its internal links follow, because they are generated with url_for.

Discovery

[project.entry-points."holdenweb.apps"]
pdf = "podpack_pdf:pdf_blueprint"

A host site can enumerate its installed apps rather than hard-coding imports:

from importlib.metadata import entry_points

for entry_point in entry_points(group="holdenweb.apps"):
    app.register_blueprint(entry_point.load(), url_prefix=f"/{entry_point.name}/")

The entry point resolves to the blueprint itself, not to a bespoke register() callable, so the contract is expressed in Flask's own vocabulary. The entry-point name is only a mount hint for that loop; it is not the app's name.

Setup hooks

Anything needed at registration time goes through Flask's own deferred- registration hook, which is the equivalent of Django's AppConfig.ready. This package uses it on itself, to settle its two config keys:

@pdf_blueprint.record_once
def _defaults(state):
    state.app.config.setdefault("PODPACK_PDF_BASE_TEMPLATE", STANDALONE_LAYOUT)
    state.app.config.setdefault("PODPACK_PDF_MAX_PAGES", DEFAULT_MAX_PAGES)

As a podpack app

Add the package's import name to the site's config file and restart:

[site]
apps = ["podpack_pdf"]

[apps.pdf]
max_pages = 200

There is no second step. podpack imports the package, reads its module-level site_app, and takes the mount point, the nav entry and the config namespace from it:

site_app = SiteApp(
    blueprint=pdf_blueprint,
    url_prefix="/pdf",
    nav=(Section("PDF tools", "pdf.root_page"),),
    init=_init,
)

The app's name is not declared: podpack derives it from the blueprint's own name, so pdf — the template namespace, the config section, the data directory — follows from Blueprint("pdf", ...) in views.py.

url_prefix is what this app asks for, not what it is entitled to. A site that wants these pages somewhere else in its address space says so, and the nav entry follows without either side restating it — a Section names an endpoint, so podpack resolves it with url_for as the chrome renders:

[site.mounts]
pdf = "/tools/pdf"

That lives under [site], not in [apps.pdf], because it is the site's policy rather than this package's configuration — app_config() returns only what this app is meant to read, and where it was mounted is not among it.

So the mount point is the host's under either contract; only the way of saying so differs — an argument to register_blueprint there, a line of config here.

This app's name is its blueprint's name: podpack derives it, so Blueprint("pdf", …) in views.py is what decides the template namespace, the data directory and the [apps.pdf] config section. There is nothing to keep in step by hand.

podpack is an optional import here — it is on no package index, and this package's first contract is to need no framework at all. Where it is absent, podpack_pdf.site_app is None and everything else works unchanged.


How one package serves both

Plain Flask podpack
Discovery holdenweb.apps entry point, or a direct import import name in the site's apps list
What is discovered the Blueprint site_app: SiteApp
Mount point the host's argument to register_blueprint [site.mounts] pdf, defaulting to the app's own
Setup hook pdf_blueprint.record_once SiteApp.init
Page layout pdf/standalone.html, shipped here the site's base.html
Configuration app.config["PODPACK_PDF_…"] [apps.pdf] in the site's TOML
Navigation the host's business nav=(Section(…),)

The two coexist because the podpack half is a config translator, not a second code path. Both hosts settle the same two keys before the first request, and the views, forms and templates read only those.

Templates, and the layout ladder

All templates ship namespaced under templates/pdf/, so nothing can collide with a host site's own template names. Every page extends pdf/base.html, which is one line: it extends whatever the host has said should wrap it.

a site's own templates/pdf/base.html   shadows this package's entirely
podpack                                   "base.html" -- the site's chrome
plain Flask                               pdf/standalone.html, shipped here

A host of either kind overrides any template here — including the whole page layout — by placing a file at the same path in its own template folder. Flask searches the application's templates before any blueprint's, so no configuration is involved. To wrap these pages in a plain-Flask site's furniture, that is a one-line file:

{# templates/pdf/base.html in the host site #}
{% extends "site-base.html" %}

or a single config key, set before the blueprint is registered:

app.config["PODPACK_PDF_BASE_TEMPLATE"] = "site-base.html"

Standalone mode does not go looking for a base.html of its own accord. A host opts in, by one of those two routes. Adopting an unrelated layout automatically would reparent these pages onto blocks that may not match and context this package cannot supply — and a Jinja block that no ancestor renders is dropped in silence, so the failure would have no error message.

For the same reason this package ships no template called base.html: Flask searches every blueprint's templates for a name the application does not supply, so one here would become the site-wide base for a podpack site and reparent every other installed app. A test pins that.

Which blocks a child may fill: content and title. Both known layouts define them. scripts exists only in standalone.html and vanishes without warning under podpack.

A note on upload size

A podpack site's [limits] max_upload_bytes becomes Flask's MAX_CONTENT_LENGTH. podpack's lab config sets it to 1 MiB, which will reject most real PDFs with a 413 before this app ever sees them. Raise it there.

Separately, max_pages bounds the splitter: it builds its zip entirely in memory, one member per page, so an unbounded document is a way to exhaust the process rather than a document that cannot be read. Over the limit it declines with a message instead of trying.


Development

uv sync
uv run pytest

That runs the standalone suite: it registers the blueprint on a bare Flask app with no framework of any kind, pushes real reportlab-generated PDFs through both tools, and asserts that mounting at a non-default prefix, overriding the layout, and refusing an oversized document all work. If it passes, the package is genuinely self-contained.

The podpack conformance suite installs the app into a real podpack site and checks the other contract. It needs no setting up:

uv sync
uv run pytest

podpack is a dev dependency, sourced from its repository — never a real one, because an app must not pin the framework version of the site installing it. It used to be absent from the lock altogether, opted into by hand with uv pip install -e ../podpack, because podpack was a sibling working tree and any declaration would have made uv lock fail on a machine that had not checked it out. A git source needs a network rather than a neighbour, so that restriction is gone.

To look at the pages without a host site of any kind:

uv run python devserver.py     # http://127.0.0.1:8459/pdf/

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

podpack_pdf-0.2.2.tar.gz (11.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

podpack_pdf-0.2.2-py3-none-any.whl (15.4 kB view details)

Uploaded Python 3

File details

Details for the file podpack_pdf-0.2.2.tar.gz.

File metadata

  • Download URL: podpack_pdf-0.2.2.tar.gz
  • Upload date:
  • Size: 11.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.12.2 {"installer":{"name":"uv","version":"0.12.2","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for podpack_pdf-0.2.2.tar.gz
Algorithm Hash digest
SHA256 5a2e17fe703affecdbf16650c6d8769efad42452e9465d3efa97e3dd40354dba
MD5 7ec774be4a0cd20d8a34274153ca6d68
BLAKE2b-256 4468fcfed8389dbf7e9d3d7c874abad4aecf84dc5a5e04fa9147ad83b16c3902

See more details on using hashes here.

File details

Details for the file podpack_pdf-0.2.2-py3-none-any.whl.

File metadata

  • Download URL: podpack_pdf-0.2.2-py3-none-any.whl
  • Upload date:
  • Size: 15.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.12.2 {"installer":{"name":"uv","version":"0.12.2","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for podpack_pdf-0.2.2-py3-none-any.whl
Algorithm Hash digest
SHA256 deb14adfbbf1c67ef2ecfea4d46aa46f6f42defc18523d49b66003e6448f1f2c
MD5 cbddbd205c4fc770a23c7c9d85e84b6f
BLAKE2b-256 d42e9175d77f49b6765a4464ada626f83337c9f7206731d38071bc5840598da3

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.2.2 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page