Writing a plugin¶
The plugin API (the Plugin base class, the hooks, the models, the
exceptions, the context) is FOCA's: read FOCA's plugins
page (docs/plugins.md in
the FOCA repository) first. This page describes the conventions of this
repository; the example plugin
generic.example.noop
shows them all and is the template of new plugins.
Start from the template¶
Open an issue for the plugin first (what it does, its phases, its data transfers), then create it from the template:
python scripts/new_plugin.py tes routing distance
uv sync # adds the plugin to `uv.lock` and installs it
scripts/new_plugin.py <group> <subgroup> <name> copies
plugins/generic/example/noop/ to plugins/<group>/<subgroup>/<name>/,
renames the plugin ID, the distribution, the import path and the class
(e.g., Distance, with DistanceConfig and DistanceParams), removes the
lines that keep the example off PyPI, and writes a new README and changelog
with TODOs. The copy passes the checks at once; then implement the plugin
and fill in the TODOs. Commit uv.lock with it.
Names¶
| Item | Convention | Example |
|---|---|---|
| Directory | plugins/<group>/<subgroup>/<name>/ |
plugins/tes/routing/distance/ |
| Plugin ID (entry point name) | <group>.<subgroup>.<name> |
tes.routing.distance |
| Distribution | foca-plugin-<group>-<subgroup>-<name> |
foca-plugin-tes-routing-distance |
| Import path | foca_plugins.<group>.<subgroup>.<name> (_ for -) |
foca_plugins.tes.routing.distance |
| Entry point group | foca.plugins |
|
| Release tag | <distribution>-v<version> |
foca-plugin-tes-routing-distance-v1.2.0 |
- Groups follow the gateway API:
generic(any API,apis = frozenset({"*"})),tes,wes. Subgroups name the function, e.g.,routing,policy,data,identity(seeplugins/README.md). Subgroups and names are lowercase, with digits and single hyphens, and have no part likev1(it would make release tags ambiguous). The distribution names of all plugins must differ (e.g., not bothtes/a-b/candtes/a/b-c). - Unprefixed IDs (
<group>.<subgroup>.<name>) are reserved for this repository; plugins of other repositories use a prefix of their own, e.g.,org.example.tes.routing.cost. FOCA's built-in plugins arefoca.<subgroup>.<name>, those of apps, e.g.,protes.<subgroup>.<name>. foca_plugins,foca_plugins.<group>andfoca_plugins.<group>.<subgroup>are namespace packages (PEP 420): never add an__init__.pyto them, or installed plugins hide each other. The plugin's own package (.../<name>/) has one, and apy.typed.
python -m scripts.checks check (a pre-commit hook and part of CI's
lint job) checks these conventions, and that each package ships the
repository's LICENSE.
Package contents¶
plugins/<group>/<subgroup>/<name>/
pyproject.toml metadata, dependencies, entry point; static version
src/foca_plugins/<group>/<subgroup>/<name>/__init__.py, py.typed
tests/ unit tests and a pipeline test through FOCA's engine
README.md ID, phases, maturity, data transfers, configuration
CHANGELOG.md per version, in the sections of the release notes
LICENSE a copy of the repository's LICENSE
- Dependencies:
focawith the supported range (>=X,<Y), plus what the plugin imports itself (e.g.,pydantic). Keep them few: every dependency ends up in the gateway's image. Development tools are shared, in the rootpyproject.toml(dependency groupdev); the tool configuration (ruff, mypy, pytest, coverage) is shared there, too. - README: keep the summary between the
summarymarkers: the plugin catalogue shows it. Document every setting (Config) and parameter (Params), the phases, what the plugin records (annotations), and its data transfers to third parties. - Metadata of the class:
apis,maturity(experimentaluntil it has proven itself, thenstable; a string in the class body, which the checks read to place a released plugin in the meta distribution's[all]or[experimental]),capabilities(generic descriptors, not package names) anddata_transfers.
Rules for plugin code¶
In addition to FOCA's rules (plain data and an explicit context; one
instance serves all requests from several threads; idempotent setup()):
- No web framework: don't import Flask, Werkzeug, Connexion or
Starlette, nor FOCA's framework glue
foca.plugins.adapter. The ast-grep ruleno-framework-importsrefuses them inplugins/*/*/*/src/, so that plugins run unchanged on FOCA 1.0. - No personal data in annotations (
ctx.record()); keep them small. - Outbound calls go through
ctx.http(the gateway's guarded client) where the app provides one, withinctx.remaining(); never disable TLS verification (a semgrep rule refuses it). - Settings models forbid unknown fields (
extra="forbid"), so that typos in configurations fail at startup.
Tests and checks¶
Every package keeps 100% coverage of its src/. Run the checks of one
plugin from the repository root, as CI does:
uv run ruff check plugins/tes/routing/distance
uv run ruff format --check plugins/tes/routing/distance
uv run ast-grep scan plugins/tes/routing/distance
uv run python -m scripts.packages mypy plugins/tes/routing/distance
uv run coverage run --source plugins/tes/routing/distance/src \
-m pytest plugins/tes/routing/distance
uv run coverage report
The template's tests show both kinds of tests: unit tests that call the
hooks with a Context of their own, and a pipeline test that loads the
plugin through its installed entry point with FOCA's PluginHost, as
gateway apps do. FOCA's plugin test kit (foca.plugins.testing, with
contract tests) will replace parts of them once it is released.
Compatibility¶
CI tests each plugin with the lowest and highest Python versions that FOCA
supports and the default one (3.11, 3.13, 3.15), and with the lowest and
highest supported FOCA versions. Until FOCA 0.18.0 is released, that is
FOCA's dev branch only, from Git (tool.uv.sources in the root
pyproject.toml); the published distributions name FOCA by version only.
Once 0.18.0 is released, the lower bound of every plugin becomes >=0.18.0
and CI tests 0.18.0, too.
Changes and releases¶
Commit messages and pull request titles use the plugin's path as their
scope, e.g., feat(tes/routing/distance): add region filter. Add each
user-visible change to the plugin's CHANGELOG.md under ## Unreleased.
See Releasing.