A starting point for contributing to LabelJetty - getting set up, the project layout, and how to test, both automatically and against a real printer. Pull requests are welcome, especially reports and fixes for printers other than the reference Vretti 420B.
- Get set up
- Project layout
- Running from source
- Testing
- Real-world print tests with the testbench
- Conventions
You need Python 3.11+ and uv. From the repo root:
# Install uv if you don't have it
curl -fsSL https://astral.sh/uv/install.sh | sh
# Install the project + all dependencies (editable), including the dev/test group
uv sync --group devuv run ... always uses the project's virtualenv, so you never activate it manually. Text/markdown
rendering needs the DejaVu Sans font - present on most Linux desktops; on a bare box:
sudo apt-get install -y fonts-dejavu-core.
You do not need a printer, a .env, or network access to develop and run the test suite.
The code is an installable package, labeljetty, under src/. Its structure mirrors
the layered architecture:
src/labeljetty/
├── config.py # one Config (pydantic-settings) from .env / env vars
├── app.py # wires everything together; the `labeljetty` entry point
├── version.py # runtime version resolution (see BUILD.md)
├── testbench.py # the manual hardware CLI (`labeljetty-testbench`)
├── printer/ # connection + library layer - knows TSPL, zero internal deps
│ ├── connection.py # raw USB I/O + device discovery
│ ├── tspl.py # TSPLPrinter: render → TSPL command streams
│ └── render.py # bitmap rendering (pdf/png/text/markdown/barcode/qr)
├── core/ # persistence: SQLModel models, engine, logging
│ ├── db.py # PrintJob / WorkerStatus models
│ └── sqltypes.py # custom SqlJsonText type
├── service/ # the background print worker
│ └── worker.py
├── integrations/ # optional integrations
│ └── homebox.py # Homebox client (v0.26 entities + labelmaker)
└── web/ # interface layer
├── app.py, api.py # FastAPI app + REST API
├── ui.py # web-UI routes (Jinja2 + HTMX)
├── auth.py # require_access seam + providers
├── password.py # pbkdf2 hashing + labeljetty-hash-password CLI
├── templates/ # Jinja2 templates (no build step)
└── static/ # htmx + CSS
Console entry points (defined in pyproject.toml): labeljetty (the
service), labeljetty-testbench (the hardware CLI), labeljetty-hash-password.
Imports are package-qualified: from labeljetty.printer import TSPLPrinter.
Read the Design doc first - it explains the layers and the decisions behind them, which makes the code far easier to navigate.
uv run labeljettyThis starts the REST API, the web UI, and the background worker on http://localhost:8888/.
Copy sample.env to .env and set at least PRINTER_USB first - see the
Setup guide.
Once it's running, the live API docs are the fastest way to explore and exercise the
endpoints while developing: Swagger UI at /docs, ReDoc at
/redoc, and the raw schema at
/openapi.json (all auto-generated by FastAPI). See also
the REST API reference.
SQLITE_PATHandIMAGE_STORAGE_DIRECTORYresolve relative to the current working directory, so run from the repo root (or use absolute paths in.env).
There are two complementary layers: a fully automated suite for everything, and the manual testbench for the one thing we never automate - real printing.
The automated suite (tests/) is hardware-free: it covers the TSPL command builder, rendering,
persistence, the print-service worker, the Homebox client, and every REST + web-UI endpoint. It
needs no .env, printer, or network - tests/conftest.py redirects config at a throwaway temp
directory and fakes the USB/Homebox/worker boundaries before any labeljetty import.
# One-time: install the project + test tooling
uv sync --group dev
# Run the whole suite
uv run python -m pytest
# A single file / test
uv run python -m pytest tests/test_api.py
uv run python -m pytest tests/test_api.py::test_print_text_enqueues
# Keyword filter, quiet, last-failed-first
uv run python -m pytest -k homebox
uv run python -m pytest -q --lf
# With coverage
uv run python -m pytest --cov=labeljetty --cov-report=term-missingCI (.github/workflows/tests.yml) runs the same suite on every push and PR against Python 3.11
and 3.12, with no secrets or services required.
Deep reference: isolation design, the full fixture list, what's covered (and what's deliberately not), and how to write new tests are all in Testing.
src/labeljetty/testbench.py (the labeljetty-testbench
command) drives the TSPLPrinter library directly - either against the real USB printer or in
--dry-run mode, where the generated TSPL is printed to stdout instead of being sent to the
device. It's how you check label positioning/sizing on real hardware, which is intentionally
never automated.
Run everything from the repository root.
# Alignment/ruler test pattern as TSPL to stdout
uv run labeljetty-testbench --dry-run pattern
# The other renderers
uv run labeljetty-testbench --dry-run text "Hello world"
uv run labeljetty-testbench --dry-run markdown "# Title
* one
* two"
uv run labeljetty-testbench --dry-run barcode 12345678 --text "Item 42"
uv run labeljetty-testbench --dry-run qrcode "https://example.com" --text "box 1"
uv run labeljetty-testbench --dry-run pdf /path/to/file.pdf --page 0To eyeball the actual rendered bitmap, monkeypatch printer._render_and_print_image to capture
the PIL image and save it as a PNG.
Global options apply to every subcommand:
| Option | Default | Meaning |
|---|---|---|
--dry-run |
off | Print TSPL to stdout instead of sending to the printer |
--width-mm |
from env | Label width in mm (DEFAULT_LABEL_WIDTH_MM, else 100) |
--height-mm |
from env | Label height in mm (DEFAULT_LABEL_HEIGHT_MM, else 30) |
--dpi |
from env | Printer resolution (DEFAULT_DPI, else 203) |
--usb |
from env | Override the PRINTER_USB selector (ignored in dry-run) |
Per-subcommand options include --font-size / --fit (text & markdown; see
auto-fit), --page (pdf), --type/--text
(barcode), and --ecc/--text (qrcode). Run uv run labeljetty-testbench --help (or
... <command> --help) for the full list.
Drop --dry-run to send to hardware (requires the
printer setup - udev rule + PRINTER_USB):
# Print the alignment pattern on a 57x32 mm label and verify it lands flush to the edges
uv run labeljetty-testbench --width-mm 57 --height-mm 32 pattern
# Query live printer status (head open / paper out / ribbon out / ...)
uv run labeljetty-testbench status
# Diagnose whether this printer answers status queries at all
uv run labeljetty-testbench probe
# Send arbitrary bytes for debugging
uv run labeljetty-testbench raw '1b213f' # hex
uv run labeljetty-testbench raw '~!T' --text # literal textThe pattern command is the quickest way to validate geometry: a correctly configured label
shows the border flush to all four edges, with the millimetre ruler ticks landing on whole
millimetres. If status says "not available", your printer is write-only for status (common on
cheap clones) - printing still works; probe confirms it.
Testing a new printer? This is the path. Run
patternto confirm geometry,probeto see whether status reads work, then the per-type renderers. Please report what you find in an issue or PR - that's how the supported-hardware list grows.
BASE=http://127.0.0.1:8888/api # match your configured host/port
curl -s $BASE/worker/status # worker health
curl -s -X POST $BASE/print/png -F file=@tests/fixtures/label_test.png
curl -s "$BASE/jobs?limit=10" # inspect the queue
curl -s $BASE/printer/status # {reachable, status_supported, status}- Match the surrounding code - naming, comment density, and idiom.
- New tests go in
tests/, namedtest_*.py; they inherit the isolation harness automatically. Anything that would print, hit USB, or call Homebox over the network must be faked - see Testing → Writing new tests. - The version is derived from git tags by
hatch-vcs; there's no version string to bump. Releases and the Docker build are covered in Build & release. - Hardware-only tests (if you add any) are marked
@pytest.mark.hardwareand deselected by default.