Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,17 @@ jobs:
output/reference-env/bin/python -I tools/verify_quickstarts.py
- name: Verify the downloadable Node invoice and guide
run: python tools/verify_node_examples.py
- name: Verify the downloadable Python web starter and actual HTTP servers
run: python tools/verify_python_web_starter.py
- uses: actions/upload-artifact@v4
if: always()
with:
name: python-web-starter-verification
path: |
output/python-web-verification/*.json
output/python-web-verification/*.log
output/python-web-verification/checks
retention-days: 14
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'
Expand Down
Binary file added docs/assets/python-web/invoice.pdf
Binary file not shown.
Binary file added docs/assets/python-web/invoice.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/python-web/project.zip
Binary file not shown.
151 changes: 151 additions & 0 deletions docs/assets/python-web/source.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
{
"schema": "fullbleed.python-web-starter.v1",
"engineVersion": "2.5.6",
"sourceRepository": "https://github.com/fullbleed-engine/fullbleed-official",
"sourceCommit": "76769ccd58426717c10cf0d988838182c203382f",
"files": [
{
"path": ".gitignore",
"bytes": 28,
"sha256": "d3b4beec83000bfbd6f81222b601d367412f30ef10bf55a6bc51b275bdfd905f"
},
{
"path": "LICENSE",
"bytes": 1079,
"sha256": "36a2115938f6e68cf0d87eccff7a6a3e57c17017e909cbd640d51aff1fa0c5a9"
},
{
"path": "README.md",
"bytes": 5145,
"sha256": "b70edbd2649dbf4ea64ebeedaba6522ef8b484c02309febfde22be8babd99b78"
},
{
"path": "check_examples.py",
"bytes": 6388,
"sha256": "2e675d4b0a1d55cba31fa0dd8d4f1eb6ce779af32ade71b90c2a0b2c63988628"
},
{
"path": "check_http.py",
"bytes": 4400,
"sha256": "740ae2957d1651b17642201df800019eec8521a1f9829c99379449399c98a043"
},
{
"path": "demo.py",
"bytes": 394,
"sha256": "82eec29da66e5edfb5460cf0c8ea9d5595f499e26b5688c8c61f07ae92a24fb3"
},
{
"path": "django_app.py",
"bytes": 1631,
"sha256": "90707de21765ba8f72e22e271b6fd0a3a9513b2416056472d01efdc27f7e8f16"
},
{
"path": "fastapi_app.py",
"bytes": 1081,
"sha256": "c5d94fedf92142755995066be17338da6edbaa7a9b37cc8cd376f64dd47a1202"
},
{
"path": "flask_app.py",
"bytes": 808,
"sha256": "8195fb6020b1a0c5e53026099860973965a1c643b4c319676fa39047cee023d7"
},
{
"path": "fonts/.gitattributes",
"bytes": 120,
"sha256": "5de00bfac958cb296380be2135dbbc986dd1f25103ea0f6495231c61affa9c72"
},
{
"path": "fonts/BebasNeue-OFL.txt",
"bytes": 4337,
"sha256": "72082f6cb4d04be2ecf7cc7d9e1e7d73787f0af8a5a278a47cade70c16b78341"
},
{
"path": "fonts/BebasNeue-Regular.ttf",
"bytes": 61400,
"sha256": "08e4623805102d819f58601e46e345648846075e363b2ceb23313c2d1c83ec73"
},
{
"path": "fonts/DMSerifDisplay-Italic.ttf",
"bytes": 71208,
"sha256": "df74c0ac387baeaeb0fe4f2324e1668e6a3ed8c09cd9796fe162c71753e19e45"
},
{
"path": "fonts/DMSerifDisplay-OFL.txt",
"bytes": 4605,
"sha256": "a3e5cdd67d4571dd0a24fcc968de0efde7ae97ef752daf0906e4767619dd7231"
},
{
"path": "fonts/DMSerifDisplay-Regular.ttf",
"bytes": 76580,
"sha256": "8cc3643535edf039aa5d95440a8542735e9197e4f4b8d9303e980fefbf5ab616"
},
{
"path": "fonts/sources.json",
"bytes": 1592,
"sha256": "01c23f8e23d039ef6a9945183ff98fd0022975227ae949de4f1669f06ad18db7"
},
{
"path": "invoice.py",
"bytes": 2868,
"sha256": "45b7ca7f4fd9aa4201aacb295d9b8f03b772244325d0935bb6205806d6b862d6"
},
{
"path": "requirements-check.txt",
"bytes": 183,
"sha256": "cbfe62b7f173711ea4ba4c165a684ddb2c70c9f7954ab7dac3980977e0a59659"
},
{
"path": "requirements-django.txt",
"bytes": 15,
"sha256": "56cbd1c0a43496327d9af1bf231abd19bb1b0e2f0a14734319fe3108dda90b13"
},
{
"path": "requirements-fastapi.txt",
"bytes": 33,
"sha256": "5feb6a8b0c09ed30d831399662a75525ea956cd35c0311ecfa178f87d5a25941"
},
{
"path": "requirements-flask.txt",
"bytes": 13,
"sha256": "35fde835d2cdf4ccbfb67738c7e5d2f8829fd441d3cbcb34b1f4c948a0a11cd4"
},
{
"path": "static/index.html",
"bytes": 4187,
"sha256": "acfd0db1efab50b1229ad937e7bd3075a3b7146de0dfddb33247d0b85bc429a3"
},
{
"path": "static/invoice.png",
"bytes": 130289,
"sha256": "a38d23651c355d1b87bc16fa37fe9121a377a12e6d515b4cd1871750c2241175"
},
{
"path": "templates/invoice.css",
"bytes": 3546,
"sha256": "cda22b39ab1f7a1eeac7cd76d162b05cd3568c2430c099123ba30a2fbd58a296"
},
{
"path": "templates/invoice.html",
"bytes": 2299,
"sha256": "c8c522d0f983c9917587083e5472af39c515c447b33ebf3edf2fafcf2f21673e"
}
],
"checkedAt": "2026-10-04T23:24:23.430920+00:00",
"assets": [
{
"file": "project.zip",
"bytes": 230094,
"sha256": "6bcd44c551aeace4e658bf96b7528ea7237d386dfebd52de5d48bbdab4712f1c"
},
{
"file": "invoice.pdf",
"bytes": 91537,
"sha256": "2e908a21e5f9ac5b1fa761d678833c62f3acc772ba7162d8da1b900f861320e6"
},
{
"file": "invoice.png",
"bytes": 130289,
"sha256": "a38d23651c355d1b87bc16fa37fe9121a377a12e6d515b4cd1871750c2241175"
}
]
}
94 changes: 71 additions & 23 deletions docs/guides/web-frameworks.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,38 +6,53 @@ description: Serve downloadable PDF invoices from FastAPI, Flask, or Django with

Fullbleed returns PDF bytes from Python data and HTML/CSS. Pass those bytes to
your framework's response object to serve an invoice, report, or statement.
The examples below generate the same one-page invoice and send it as a download.
The starter below generates a designed one-page invoice and sends it as a
download. It includes a browser page, editable HTML/CSS, and the same document
renderer behind three small framework adapters.

[Get the complete examples](https://github.com/fullbleed-engine/fullbleed-official/tree/master/examples/web_frameworks){ .md-button .md-button--primary }
[Try a PDF in your browser](../getting-started/notebook.md){ .md-button }
[Download the Python starter](../assets/python-web/project.zip){ .md-button .md-button--primary }
[Open the sample PDF](../assets/python-web/invoice.pdf){ .md-button }

[![Northstar Studio invoice with large green typography, three line items, and an orange-accented total panel.](../assets/python-web/invoice.png)](../assets/python-web/invoice.pdf)

The [source record and download hashes](../assets/python-web/source.json) identify
the exact source files, fonts, package version, and generated sample. This is a
fictional integration example; connect your application's authorized record
lookup before serving real invoices.

## Set up the examples

Use a Python 3.10–3.14 virtual environment, then get the example source:
Use Python 3.10–3.14. Extract the starter, open its `fullbleed-python-invoice`
directory, and create a virtual environment:

```bash
git clone https://github.com/fullbleed-engine/fullbleed-official.git
cd fullbleed-official/examples/web_frameworks
python -m venv .venv
```

Activate it with `source .venv/bin/activate` on macOS/Linux or
`.venv\Scripts\Activate.ps1` in Windows PowerShell. A repository checkout also
works: the [source directory](https://github.com/fullbleed-engine/fullbleed-official/tree/master/examples/web_frameworks)
is `examples/web_frameworks`.

Choose one framework below. Each requirement file installs that framework;
`fullbleed` remains a separate package. The examples are checked with Fullbleed
2.4.0, FastAPI 0.142.2, Flask 3.1.3, and Django 5.2.17.
2.5.6, FastAPI 0.142.2, Flask 3.1.3, and Django 5.2.17.

Each app serves this fictional record:

```text
http://127.0.0.1:8000/invoices/INV-1042.pdf
```

Open that URL after starting the app. The browser downloads `invoice.pdf`, which
Open `http://127.0.0.1:8000/` after starting the app and select **Download invoice**.
The browser downloads `invoice.pdf`, which
contains three line items and a total of **USD 1,870.00**. An unknown invoice ID
returns HTTP 404. Stop the server before trying another framework on the same port.

## FastAPI

```bash
python -m pip install fullbleed -r requirements-fastapi.txt
python -m pip install fullbleed==2.5.6 -r requirements-fastapi.txt
python -m uvicorn fastapi_app:app --host 127.0.0.1 --port 8000
```

Expand Down Expand Up @@ -73,7 +88,7 @@ finishes. See [FastAPI's concurrency documentation](https://fastapi.tiangolo.com
## Flask

```bash
python -m pip install fullbleed -r requirements-flask.txt
python -m pip install fullbleed==2.5.6 -r requirements-flask.txt
python -m flask --app flask_app run --host 127.0.0.1 --port 8000
```

Expand Down Expand Up @@ -101,7 +116,7 @@ for response bodies, media types, and headers.
## Django

```bash
python -m pip install fullbleed -r requirements-django.txt
python -m pip install fullbleed==2.5.6 -r requirements-django.txt
python django_app.py runserver 127.0.0.1:8000 --noreload
```

Expand Down Expand Up @@ -134,10 +149,29 @@ for response construction and headers.
## The shared document renderer

[`invoice.py`](https://github.com/fullbleed-engine/fullbleed-official/blob/master/examples/web_frameworks/invoice.py)
contains the fictional record lookup, HTML/CSS template, and `render_invoice()`.
contains the fictional record lookup and `render_invoice()`.
It calculates line totals with `Decimal`, escapes text before inserting it into
HTML, and embeds the Inter font shipped with Fullbleed. A new `PdfEngine` renders
each request into memory.
HTML, and registers the Inter font shipped with Fullbleed alongside the included
DM Serif Display and Bebas Neue families. A new `PdfEngine` renders each request
into memory. Font licenses and pinned source hashes are included in `fonts/`.

## Edit the HTML and CSS

Change `templates/invoice.html` for document structure and
`templates/invoice.css` for typography, spacing, columns, and color. The renderer
reads those files for each request, so a new download uses your edits without
restarting the server. Its Python `string.Template` placeholders include
`$customer`, `$number`, `$rows`, and `$total`; write a literal dollar sign as `$$`.
Text values are escaped before substitution and are not interpreted again as
template markup.

The included layout is a fixed one-page A4 sample for three line items. Adapt
its geometry and pagination before using longer records, and review the final
PDF. This starter leaves tax, discounts, invoice numbering, payment collection,
and application authentication to your application. `static/index.html` is the
local demo page; replace it with your application's interface when integrating.

## Return a private PDF attachment

The response headers are shared too:

Expand All @@ -154,22 +188,36 @@ it does not need JSON or base64 encoding, and requests do not share output files

Replace `load_invoice()` with your application's authorized record lookup. It
returns an invoice number, customer, date strings, and items with descriptions,
integer quantities, and unit-price strings. Keep access checks before rendering.
This sample omits tax and discounts.
integer quantities, unit-price strings, and an optional item `detail`.
Keep access checks before rendering.

## Verify and deploy your workflow

From the repository root, with Fullbleed installed:
From the extracted starter's directory, with Fullbleed installed:

```bash
python -m pip install -r requirements-check.txt
python check_examples.py --out output/check
```

The check exercises each framework's test client and starts the documented
local servers in turn. It verifies actual HTTP downloads, an independent PDF
reader's text and totals, page count, embedded fonts, headers, 404/405 responses,
and matching simultaneous downloads. It also checks font/license hashes,
literal markup and placeholder-like customer text, and the saved preview.
Servers stop when the checks finish.

Review `output/check/verification.json`, its PDFs, and the PNG preview after a
layout change. To refresh the download page's saved preview, run:

```bash
python -m pip install -r examples/web_frameworks/requirements-check.txt
python examples/web_frameworks/check_examples.py --out target/web-framework-check
python check_examples.py --out output/check --update-preview
```

The check exercises each framework's test client, saves its PDF response, and
verifies the expected text and total, page count, embedded font, HTTP headers,
404 behavior, and repeated output bytes. It also emits a PNG preview and
`verification.json`. Review the preview after changing the document's layout.
The browser page shows a saved preview of the included invoice. Its **Download
invoice** link always renders the current HTML/CSS. Verification covers these
synthetic fixtures and local servers; it does not establish production capacity
or PDF standards conformance.

The launch commands above run local development servers. Use your framework's
deployment setup for a public app. For large jobs, render in your job queue and
Expand Down
64 changes: 64 additions & 0 deletions tools/verify_python_web_starter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""Install and exercise the exact downloadable Python web starter in isolation."""
from datetime import datetime, timezone
import hashlib
import json
import os
from pathlib import Path
import subprocess
import sys
import zipfile

ROOT = Path(__file__).resolve().parents[1]
ASSETS = ROOT / "docs/assets/python-web"
OUT = ROOT / "output/python-web-verification"


def main():
OUT.mkdir(parents=True, exist_ok=False)
source = json.loads((ASSETS / "source.json").read_text(encoding="utf-8"))
assert source["engineVersion"] == "2.5.6"
for item in source["assets"]:
data = (ASSETS / item["file"]).read_bytes()
assert len(data) == item["bytes"]
assert hashlib.sha256(data).hexdigest() == item["sha256"], item["file"]
unpacked = OUT / "project with spaces"
with zipfile.ZipFile(ASSETS / "project.zip") as zipped:
assert zipped.testzip() is None
for item in zipped.infolist():
assert (unpacked / item.filename).resolve().is_relative_to(unpacked.resolve())
zipped.extractall(unpacked)
project = unpacked / "fullbleed-python-invoice"
manifest = json.loads((project / "MANIFEST.json").read_text(encoding="utf-8"))
assert manifest["sourceCommit"] == source["sourceCommit"]
assert manifest["files"] == source["files"]
for item in manifest["files"]:
data = (project / item["path"]).read_bytes()
assert hashlib.sha256(data).hexdigest() == item["sha256"], item["path"]
venv = OUT / "venv"
subprocess.run([sys.executable, "-m", "venv", str(venv)], check=True)
python = venv / ("Scripts/python.exe" if os.name == "nt" else "bin/python")
commands = [
("install", [str(python), "-m", "pip", "install", "--disable-pip-version-check",
"fullbleed==2.5.6", "-r", "requirements-check.txt"]),
("verify", [str(python), "check_examples.py", "--out", str(OUT / "checks")]),
]
for name, command in commands:
with (OUT / (name + ".log")).open("w", encoding="utf-8") as log:
result = subprocess.run(command, cwd=project, stdout=log, stderr=subprocess.STDOUT, timeout=240)
if result.returncode:
raise RuntimeError(f"{name} failed; inspect {OUT / (name + '.log')}")
for framework in ["fastapi", "flask", "django"]:
for suffix in ["", "-http"]:
assert (OUT / "checks" / f"{framework}{suffix}.pdf").read_bytes() == (ASSETS / "invoice.pdf").read_bytes()
assert (OUT / "checks/preview/invoice_page1.png").read_bytes() == (ASSETS / "invoice.png").read_bytes()
report = {"ok": True, "checkedAt": datetime.now(timezone.utc).isoformat(),
"sourceCommit": source["sourceCommit"], "engineVersion": "2.5.6",
"manifestFilesVerified": len(manifest["files"]),
"frameworkClientsPassed": 3, "actualHttpServersPassed": 3,
"pdfAndPreviewMatchPublishedAssets": True,
"scope": "Isolated ZIP install and local HTTP verification; no production deployment claim."}
(OUT / "verification.json").write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8")
print(json.dumps(report))


if __name__ == "__main__": main()
Loading