django-poli-page
Render Poli Page documents as Django HTTP responses.
About
django-poli-page is a reusable Django app around the official poli-page Python SDK. You add it to INSTALLED_APPS, declare a POLI_PAGE settings dict, and you return PDFs, streamed PDFs, stored-document HTML previews, and presigned-URL redirects from your views with one helper call each. The app validates configuration at startup, exposes a lazy PoliPage singleton, and bridges the SDK's retry and error hooks onto Django signals.
When to use this:
- You render invoices, contracts, statements or reports from Django views and want the right
Content-Disposition,Cache-ControlandContent-Typewithout rewriting them on each route. - You want SDK retries and terminal errors observable through
django.dispatchinstead of bespoke callbacks. - You want
manage.pyto fail loudly when yourPOLI_PAGEsettings are wrong.
When not to:
- You need a templatetag library, a class-based view mixin, or a database model for documents — this app deliberately does not ship those.
Requirements
- Python 3.11+
- Django 4.2 LTS, 5.0, 5.1, 5.2 LTS, or 6.0
- The
poli-pagePython SDK 0.13.x (installed with the app,poli-page>=0.13,<0.14) - A Poli Page API key (
pp_{environment}_…, see API keys)
Install
pip install django-poli-page
This installs the poli-page SDK 0.13.x with it.
Add the app to INSTALLED_APPS and export your key:
# settings.py
INSTALLED_APPS = [
# ...
"django_poli_page",
]
POLI_PAGE = {
"API_KEY": os.environ["POLI_PAGE_API_KEY"],
}
export POLI_PAGE_API_KEY=pp_sandbox_your_key_here
You get a key from the dashboard at app.poli.page under Settings → API Keys. Smoke-test the wiring from the command line:
python manage.py poli_page_render \
--project=getting-started \
--template=welcome \
--data='{"name":"World"}' \
--output=welcome.pdf
API keys
A key belongs to one environment of your organization and reads pp_{environment}_{32 hex characters}. The environment selects which published copy of a project a render uses, so a render input names a project and a template, never a version.
| Environment | Key | Watermark | Counted |
|---|---|---|---|
sandbox (every org) |
pp_sandbox_… |
yes | no |
live (every org, deletable on Pro+) |
pp_live_… |
no | yes |
created on Pro+ (e.g. staging) |
pp_staging_… |
no | yes |
"Counted" means the render counts toward the organization's monthly PDF quota.
Quick start
# views.py
from django_poli_page import client
from django_poli_page.http import pdf_response
def invoice_view(request, invoice_id):
pdf = client.render.pdf({
"project": "invoices",
"template": "default",
"data": {"invoice_id": invoice_id},
})
return pdf_response(pdf, f"invoice-{invoice_id}.pdf")
Configuration
You configure everything through a single POLI_PAGE dict in your settings module. AppConfig.ready() validates the dict at startup and surfaces problems through manage.py check.
| Key | Default | Description |
|---|---|---|
API_KEY |
POLI_PAGE_API_KEY env var |
Your Poli Page key, a non-empty string. A key that does not match pp_{environment}_{32 lowercase hex characters} raises one startup warning, which shows no part of the key, and the app starts anyway: the API decides whether the key is valid. When API_KEY is absent, the same check applies to the POLI_PAGE_API_KEY env var. |
BASE_URL |
SDK default | Override the API host. |
TIMEOUT |
SDK default | Per-request timeout in seconds (0 < value <= 600). |
RETRIES.MAX_ATTEMPTS |
SDK default | Retry budget for transient failures (0-10). |
RETRIES.DELAY_SECONDS |
SDK default | Base delay between retry attempts (0-30). |
ON_RETRY |
signal dispatch | Dotted path to a callable (RetryEvent) -> None. |
ON_ERROR |
signal dispatch | Dotted path to a callable (PoliPageError) -> None. |
HTTP_CLIENT |
SDK default | Dotted path to a factory returning an httpx.Client. |
ERROR_HANDLER |
True |
Install the error handler middleware (see Errors). False leaves MIDDLEWARE untouched. |
# settings.py
POLI_PAGE = {
"API_KEY": os.environ["POLI_PAGE_API_KEY"],
"TIMEOUT": 30,
"RETRIES": {"MAX_ATTEMPTS": 3, "DELAY_SECONDS": 0.5},
}
When ON_RETRY or ON_ERROR is unset, the SDK hooks dispatch through Django signals instead (see API at a glance).
API at a glance
| Symbol | Purpose |
|---|---|
django_poli_page.client |
Lazy PoliPage singleton, built on first attribute access. |
django_poli_page.http.pdf_response |
Wrap PDF bytes in an HttpResponse with PDF headers and disposition. |
django_poli_page.http.pdf_stream_response |
Stream a PDF in a StreamingHttpResponse. Pass the SDK stream as is, pdf_stream_response(client.render.pdf_stream(input)): the render and the download start in the view, so an SDK error reaches the error handler instead of a 200 PDF that has already started. |
django_poli_page.http.preview_response |
Return a stored document's HTML (client.documents.preview(id)) as text/html. |
django_poli_page.http.document_redirect_response |
Redirect (302 or 301) to a document's presigned PDF URL. |
django_poli_page.middleware.PoliPageErrorMiddleware |
Error handler, on by default: answers a PoliPageError as JSON. |
django_poli_page.signals.poli_page_retry |
Sent before each SDK retry sleep with the RetryEvent. |
django_poli_page.signals.poli_page_error |
Sent on terminal SDK failure with the PoliPageError. |
manage.py poli_page_render |
Render a template end-to-end from the CLI; exits non-zero on PoliPageError. |
Errors
Error handler (on by default)
When the app starts, it appends django_poli_page.middleware.PoliPageErrorMiddleware to MIDDLEWARE. A PoliPageError that a view does not catch is answered as JSON:
- the body is the SDK's
error.to_payload(), exactly{"code", "message", "status", "requestId"}; - the HTTP status is that payload's
status: the API's status for an API error,503fornetwork_error,504fortimeout,502forinvalid_response(a 2xx whose body is not JSON, usually from a proxy), and500when the error has none, as forinvalid_optionsandio_error(the body'sstatusis thennull); Content-Type: application/json.
{
"code": "TEMPLATE_NOT_FOUND",
"message": "Template 'does-not-exist' not found in project 'getting-started'.",
"status": 404,
"requestId": "req_..."
}
Other exceptions go on to Django's own handling. The handler is the innermost middleware, so it answers before any other middleware's process_exception. To handle errors yourself, turn it off:
POLI_PAGE = {
"API_KEY": os.environ["POLI_PAGE_API_KEY"],
"ERROR_HANDLER": False,
}
The app adds the middleware once, at startup; listing it yourself in MIDDLEWARE places it where you want it, and it is not added a second time. A later replacement of MIDDLEWARE, such as override_settings(MIDDLEWARE=[...]) or pytest-django's settings.MIDDLEWARE = [...] in a test, drops the handler unless the new list includes "django_poli_page.middleware.PoliPageErrorMiddleware".
For a streamed PDF, hand the SDK stream to pdf_stream_response instead of opening it inside a generator: an error raised once the body is streaming comes after the 200 is sent, and no handler can answer it.
Error types
The response helpers do not catch SDK exceptions. Every SDK error is a PoliPageError, which carries code, status, message, and request_id; the main families you can catch in a view are exported from poli_page:
- API status —
APIStatusErrorand its per-status subclasses, such asBadRequestError(400),AuthenticationError(401),PermissionDeniedError(403),NotFoundError(404),RateLimitError(429) andInternalServerError(5xx). The API answered with an error status. - Network / transport —
APIConnectionError, and its subclassAPITimeoutError. No answer arrived in time. - Invalid response —
InvalidResponseError, codeinvalid_response. A 2xx whose body is not JSON, usually from a proxy or a captive portal; itsstatusis that 2xx. - Local and download — a
PoliPageErrorwith codeinvalid_options(bad input or settings),io_error(render_to_file()could not write the file) orDOWNLOAD_FAILED(the PDF download from its presigned URL failed or timed out).
Catch one in the view when a route needs its own answer:
# views.py
from poli_page import NotFoundError
from django_poli_page import client
from django_poli_page.http import pdf_response
def invoice_view(request, invoice_id):
try:
pdf = client.render.pdf({
"project": "invoices",
"template": "default",
"data": {"invoice_id": invoice_id},
})
except NotFoundError:
pdf = client.render.pdf({"project": "invoices", "template": "fallback", "data": {}})
return pdf_response(pdf, f"invoice-{invoice_id}.pdf")
Example app
A runnable Django 5 project under example-app/ covers every public method of the SDK through django-poli-page, with one route per demo step and an interactive dashboard at /. See example-app/README.md for the route map.
cd example-app
uv sync
export POLI_PAGE_API_KEY=pp_sandbox_your_key
uv run python manage.py runserver
Going further
- Streaming large PDFs through
pdf_stream_response— forthcoming indocs/streaming.md. - Document workflow: create, fetch, thumbnails, presigned-URL redirects — forthcoming in
docs/documents.md. - Signals: wiring
poli_page_retryandpoli_page_errorinto logging or metrics — forthcoming indocs/signals.md. - Swapping the underlying
httpx.Clientfor connection-pool tuning or instrumentation — forthcoming indocs/http-client.md.
Compatibility
| Package version | poli-page SDK |
Django | Python |
|---|---|---|---|
| 0.13.x | 0.13.x | 4.2 LTS, 5.0, 5.1, 5.2 LTS, 6.0 | 3.11, 3.12, 3.13 (Django 6.0: 3.12+) |
The package shares its major.minor with the Poli Page API and requires the poli-page SDK of the same minor (see RELEASING.md). It follows the Django supported-versions policy; releases drop a Django row when upstream marks it end-of-life.
Contributing
Run the checks CI runs from the repository root: uv sync --all-extras, then uv run ruff check ., uv run ruff format --check ., uv run mypy src tests example-app/tests scripts and uv run pytest tests/unit.
Contract tests
example-app/tests/test_contract.py runs the integration cases (kind: "impl") of the Poli Page API contract against the example app: a local server plays the API, the app's SDK points at it through POLI_PAGE["BASE_URL"], and Django's test client checks each answer. CI runs them against the contract version pinned in .github/workflows/ci.yml. Locally:
cd example-app
uv sync
POLI_CONTRACT=<path to contract.json> uv run pytest
Without POLI_CONTRACT the test is skipped (in CI it fails). example-app/tests/contract-skip.json lists the cases allowed to fail, each with its reason.
License
Released under the MIT License.
Metadata
Release files for django-poli-page 0.13.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| django_poli_page-0.13.0.tar.gz | 16.1 kB | With provenance | Details for django_poli_page-0.13.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| django_poli_page-0.13.0-py3-none-any.whl | Python 3 | none | any | Details for django_poli_page-0.13.0-py3-none-any.whl |
Total release size: 33.5 kB
Release files / django_poli_page-0.13.0.tar.gz
| Download URL | django_poli_page-0.13.0.tar.gz |
|---|---|
| Size | 16.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
edc7871e40fe4bc66c8c6fe6981f78d79cc75a8fbfe085ee08204f4ea1616f32
|
|
BLAKE2b-256 checksum How to use checksums |
d02c3023d3a848946b2eb7d6088e05600d38f9154bfb4488b0880798bfffa6ff
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 4, 2026.
Transparency logRelease files / django_poli_page-0.13.0-py3-none-any.whl
| Download URL | django_poli_page-0.13.0-py3-none-any.whl |
|---|---|
| Size | 17.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
97c10c3bbc4ecefd04da56471563b10abfe3facfe42234707a2403235a5b2558
|
|
BLAKE2b-256 checksum How to use checksums |
9bc238c55bf78ec37c215ba42759f718e8ec3d442151e9e8c67c08ed44c2f6bf
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 4, 2026.
Transparency logSecurity for django-poli-page 0.13.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
Both files in this release have been uploaded with provenance metadata.
Attestation for django_poli_page-0.13.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 4, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.