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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5a2e17fe703affecdbf16650c6d8769efad42452e9465d3efa97e3dd40354dba
|
|
| MD5 |
7ec774be4a0cd20d8a34274153ca6d68
|
|
| BLAKE2b-256 |
4468fcfed8389dbf7e9d3d7c874abad4aecf84dc5a5e04fa9147ad83b16c3902
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
deb14adfbbf1c67ef2ecfea4d46aa46f6f42defc18523d49b66003e6448f1f2c
|
|
| MD5 |
cbddbd205c4fc770a23c7c9d85e84b6f
|
|
| BLAKE2b-256 |
d42e9175d77f49b6765a4464ada626f83337c9f7206731d38071bc5840598da3
|