- Python 100%
|
All checks were successful
CI / test (push) Successful in 1m7s
`tests/test_surface.py` said *"Copy this file into your own plugin; the only line to change is PACKAGE"*, and the README said the same in one word: *copy it*. Four plugins did. That is how five near-identical AST walks ended up in five repositories, no two agreeing about relative imports or about what counted as reaching past -- and a check that disagrees with the core's is a check that passes while the plugin is broken. postulo/postulo#229 publishes the walk as `postulo.plugins.testing`, so this file calls it. Nothing moves in `src/`: this plugin has only ever imported the surface. What is left is the two things a plugin author actually has to write -- the call over their own package, and the names they ask of the surface asserted against `api.__all__`, because `__all__` is what the promise is made about. This is the plugin people are told to copy, so what it shows should be the check the core maintains rather than a snapshot of it. The docstring now says not to copy the file, and says why. The core dev dependency had no ref at all, so these tests passed against whatever `main` was that day. It is `1d6cdd7e0`, which is also where `postulo.plugins.testing` starts. Closes #3 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| .forgejo/workflows | ||
| src/postulo_helloworld | ||
| tests | ||
| .gitignore | ||
| LICENSE | ||
| pyproject.toml | ||
| README.md | ||
| uv.lock | ||
postulo-helloworld
The smallest Postulo plugin there is. Copy it to start your own.
Postulo puts an interface wherever a choice could reasonably vary — where a posting is read from, how a person is told about things — and its own implementations are plugins that happen to ship in the box. A plugin written by somebody else is installed with one command and no change to Postulo. This repository is one such plugin, deliberately tiny, with everything a real one needs already in place: two kinds of plugin, translations of its own, tests that run it inside a real Postulo, and a CI workflow.
What is in the box
| Path | What it is |
|---|---|
src/postulo_helloworld/source.py |
A capture source: four names (name, version, can_handle, parse), no base class. Reads a posting out of a tiny page format and returns JobPostingData. |
src/postulo_helloworld/notifier.py |
A notifier, one of the connected plugins: declares the fields it needs, Postulo draws the form, and send() appends each notification to a file. The person names the file; the operator names the directory, and file_for() explains at length why it is that way round. |
src/postulo_helloworld/locale/ |
This plugin's own translations, French and Portuguese. Postulo never translates a plugin's strings; every plugin holds its locale. |
tests/ |
Runs both plugins through Postulo's registry, exactly as an instance would, and checks the French comes from this package. tests/test_surface.py fails on an import that reaches past postulo.plugins.api, by calling the core's own postulo.plugins.testing — do that rather than copying this file. tests/settings.py is Postulo's test configuration, borrowed. |
pyproject.toml |
The entry points that make the plugins appear, and nothing else: a plugin has no dependencies of its own, it runs inside Postulo's environment. |
Try it
Into the environment of a running Postulo:
uv pip install git+https://source.tiagoagueda.com/postulo/postulo-helloworld.git
Restart Postulo. Capture a posting from any address on hello.example and the source
answers; under Settings → Connections → Add, Hello world is offered, asks for a
file name, and its Test button writes a line. That is the whole installation;
uninstalling the package removes both.
The lines go to data/helloworld/ beside Postulo's own data, or wherever
POSTULO_HELLOWORLD_DIR points. Nothing a person types moves that: the field is a name,
never a path.
Make it yours
-
Copy the repository and rename
postulo_helloworldto your package. -
Keep or delete either plugin; a package may hold one kind or several.
-
Change the entry points in
pyproject.tomlto point at your classes. The groups arepostulo.sources,postulo.notifiers,postulo.storesandpostulo.syncs. -
Wrap every string a person reads in
gettext/gettext_lazy, then:uv run postulo-messages extract # a catalogue for every language Postulo offers uv run postulo-messages compile # writes the .mo files, no gettext neededPostulo's own catalogue tool, installed with Postulo as a dev dependency. Commit the
.mofiles: they ship with the package. English is the source; French and European Portuguese are filled while working, anduv run pytest -m releaseholds the twenty-four European Union catalogues complete before a release. -
Run the tests:
uv sync # brings in Postulo itself from its repository, for the tests uv run pytest
The contract in full — every field of JobPostingData, how sources are chosen, what a
connected plugin's config_fields() and test() must do, how to add a settings section —
is in Postulo's
docs/PLUGINS.md.
Rules worth keeping
- Leave a field empty rather than guessing. A blank box on the review screen invites typing; a confidently wrong value has to be noticed first.
- Return
Nonewhen the page is not yours. Postulo tries the next source. - A person names a thing; the operator names the place. A connection's settings are ordinary user input handed to you with the server's privileges attached, so a field that takes a path lets any account on the instance reach any file the process may write. Take a name, refuse a separator rather than cleaning it out, and put the directory in the environment.
- Match the host, not the address.
"board.example" in urlalso says yes tohttps://evil.test/?board.example, and a source that says yes is parsing a stranger's page ahead of Postulo's own parsers. - Import
postulo.plugins.apiand nothing else from Postulo. Everything else is internals that may be renamed in a patch release;tests/test_surface.pyholds you to it. - Use
postulo.plugins.http.client()for every request a connected plugin makes. It carries Postulo's timeouts, user agent and destination policy. - Raise on failure in
send(). Postulo logs it and shows it on the connection; it never fails whatever caused the notification. - Translate your own strings. Ship
locale/; Postulo reads it when it loads you.
Licence
MIT, so that copying it into your own plugin is a decision you never have to think about. Postulo itself is AGPL-3.0-or-later.