Keep every document Postulo holds — rendered CVs and letters, uploaded files — in a Paperless-ngx archive, filed by company, kind and application. https://source.tiagoagueda.com/postulo/postulo
Find a file
Tiago Águeda 973969bb7f
All checks were successful
CI / test (push) Successful in 1m4s
Run Postulo's surface check, and say which core these tests passed against
`tests/test_surface.py` walked this package's source for imports past
`postulo.plugins.api`. So did postulo-apprise's, postulo-helloworld's
and postulo-imap's, and postulo-dav kept a fourth inside `test_dav.py`
-- five near-identical AST walks, and no two agreed about relative
imports or about what counted as reaching past. That is the drift a
published check exists to stop, and postulo/postulo#229 published one:
`postulo.plugins.testing`.

Nothing moves in `src/`. This store has imported only the surface since
its own #2, and now something the core maintains says so.

What stays is what is particular here: that the seven names this store
asks of the surface are on `__all__` rather than merely reachable --
`DocumentMetadata` and `ExternalRef` among them, which are the two #2
found being taken from `postulo.plugins.base`.

The core dependency pointed at `main`, which is a moving target: the
tests passed against whatever that branch was on the day somebody ran
them, and the lock said so while the pin did not. It is `1d6cdd7e0` now,
which is also where `postulo.plugins.testing` starts.

Closes #4

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 17:27:53 +02:00
.forgejo/workflows Run CI in an image that can check the repository out, and gate a tag elsewhere 2026-09-16 11:17:38 +02:00
src/postulo_paperless Run a CI that exists, test the Postulo this is for, and keep the token off the wire 2026-09-16 10:40:21 +02:00
tests Run Postulo's surface check, and say which core these tests passed against 2026-09-21 17:27:53 +02:00
.gitignore Keep every document in Paperless-ngx 2026-09-06 15:01:10 +02:00
LICENSE Keep every document in Paperless-ngx 2026-09-06 15:01:10 +02:00
pyproject.toml Run Postulo's surface check, and say which core these tests passed against 2026-09-21 17:27:53 +02:00
README.md Take the version of the Postulo this is released beside, work against it, and hold the catalogues to the same rule 2026-09-13 06:08:02 +02:00
uv.lock Run Postulo's surface check, and say which core these tests passed against 2026-09-21 17:27:53 +02:00

postulo-paperless

Every document Postulo holds — rendered CVs and letters, the files you uploaded — kept in a Paperless-ngx archive, filed by company, kind and application.

Postulo keeps what you sent, exactly as it left, under its own private media. That stays true: Postulo's copy is the source of truth, and nothing in Postulo depends on Paperless being up. This plugin gives Paperless a copy of each document as it is created, so the archive that already holds your contracts and letters holds your job search too, and a document found there leads back to the application it was used for.

Install

Into the environment of a running Postulo:

uv pip install git+https://source.tiagoagueda.com/postulo/postulo-paperless.git

Restart Postulo. In a container, bake it into the image — see Plugins in the image on Installing Postulo:

docker compose -f docker/compose.yml build \
  --build-arg POSTULO_EXTRA_PACKAGES="git+https://source.tiagoagueda.com/postulo/postulo-paperless.git"

Paperless almost always sits on the same LAN or Compose network as Postulo. Postulo refuses private addresses by default, so the operator sets POSTULO_CONNECTIONS_ALLOW_PRIVATE=true for that; it is the reason that switch exists.

Use

Under Settings → Connections → Add a connection, choose Paperless-ngx:

Field What it is
Paperless address Where you open Paperless, e.g. https://paperless.example.org
API token From your Paperless profile (My profile → API auth token). Stored encrypted, never shown again.
Tag Put on every document from Postulo. Default postulo; blank for none.
Custom field for the application's link A URL field leading back to the application. Default Postulo; blank for none.

Then tick which kinds of document go to the archive — CV, cover letter, certificate, portfolio, reference, other — and press Test: it lists the archive with your token.

From then on every new document is queued and the scheduler sends it on its next pass. Each document shows archived with a link into Paperless, waiting to be sent, or failed with the reason. Send everything on the connection queues what you already had; Send to stores now under a document tries at once.

What lands in Paperless

  • Correspondent ← the company, created if missing.
  • Document type ← the kind: CV, Cover letter, Certificate…
  • Tag ← postulo (or yours).
  • Custom field Postulo (URL) ← the application's address on your instance.
  • Title ← CV — Company — Role — date for what Postulo rendered; an upload keeps its own name, followed by the date.
  • Created ← the day it was sent, or uploaded.

Paperless refuses a document whose checksum it already holds and names the one it has; the plugin reads that as success. So a document is never archived twice, however many times it is offered — which is what makes retrying safe. Consumption is asynchronous: the plugin waits a short while for it, and if Paperless is slower (OCR of a large scan, a busy server) it leaves the document to the next scheduler pass, where the duplicate answer picks it up.

Nothing is ever deleted from Paperless. Withdrawing or declining an application changes the timeline in Postulo and nothing in the archive; an archive is for keeping.

Translations

French and Portuguese ship with the package, in src/postulo_paperless/locale/. Postulo never translates a plugin's strings; a catalogue for every language Postulo offers is in place; fill one and run uv run postulo-messages compile. uv run postulo-messages extract refreshes them from the source, and uv run pytest -m release holds the twenty-four European Union catalogues complete before a release.

Develop

uv sync        # brings in Postulo itself from its repository, for the tests
uv run pytest
uv run ruff check .

The tests run against a Paperless that lives in memory — its consumer, its task queue, its duplicate check — and inside a real Postulo through its registry and scheduler. No request leaves the machine.

Licence

AGPL-3.0-or-later, like Postulo and like Paperless-ngx.