Plugins#
Muffin has a rich ecosystem of plugins:
muffin-jinja2 – Jinja2 templates (asyncio/trio/curio)
muffin-session – Signed cookie-based HTTP sessions
muffin-oauth – OAuth1/OAuth2 integration
muffin-sentry – Sentry error tracking
muffin-peewee-aio – Peewee ORM integration with async support
muffin-babel – Localization with Babel
muffin-databases – Async support for a range of databases
muffin-mongo – MongoDB integration
muffin-redis – Redis integration
muffin-rest – REST helpers
muffin-apiclient – Support for external APIs
muffin-grpc – GRPC support
muffin-kafka – Kafka integration
muffin-admin – Admin interface
muffin-prometheus – Prometheus metrics exporter
See each repo for usage and installation instructions.
Writing a plugin#
A plugin is a class derived from muffin.plugins.BasePlugin. It gets a
configuration of its own, access to the application, and hooks that run with the
application lifespan:
from muffin.plugins import BasePlugin
class Plugin(BasePlugin):
name = "mailer" # config prefix and app.plugins key
defaults = {"disabled": False, "host": "localhost"}
async def startup(self):
self.client = Client(self.cfg.host)
async def shutdown(self):
await self.client.close()
async def send(self, to, subject):
await self.client.send(to, subject)
Register it with the application:
plugin = Plugin(app)
await plugin.send("user@example.com", "Hello")
Plugin(app) binds the plugin, configures it, registers it in app.plugins
and hooks startup/shutdown into the application lifespan. Binding is
skipped, with a warning in the log, when the resolved config has disabled
set. Pass name= to register a second instance of the same plugin type:
primary = Plugin(app, name="mailer_primary")
secondary = Plugin(app, name="mailer_secondary")
Options are resolved in this order: defaults, application config keys with
the {name}_ prefix, keyword arguments, then environment variables
{APP_NAME}_{NAME}_{OPTION}. Details are in Configuration.
A plugin may also provide request middleware by setting middleware to a
callable; it is registered with app.middleware:
class Plugin(BasePlugin):
name = "timing"
async def middleware(self, app, request, receive, send):
response = await app(request, receive, send)
if response is not None:
response.headers["x-powered-by"] = "muffin"
return response
The plugin is usable outside a request, for example in CLI commands or tests:
async with plugin: # calls startup/shutdown
await plugin.send("user@example.com", "Hello")
await plugin.restart() # shutdown, then startup again
Tests get plugin startup for free: the pytest app fixture runs the
application lifespan, which awaits every plugin’s startup before yielding
the application. See Testing.
What setup() does#
Binding is done by setup(app, **options), which is what Plugin(app) calls.
It returns True when the plugin is registered and False when the resolved
configuration disables it. On a disabled plugin nothing is attached: it is not in
app.plugins, its lifespan hooks are not registered, and touching plugin.app
raises PluginNotInstalledError. Code that may run with a
disabled plugin should check the return value or app.plugins.
setup() also accepts name= to register a second instance of the same
plugin type, which is how a project uses one plugin twice (two databases, two
mailers).
Errors#
- class muffin.plugins.PluginError#
Base exception for plugin errors (public API for external plugins).
- class muffin.plugins.PluginNotInstalledError#
Raised when a plugin is not installed.
Both live in muffin.plugins. PluginError is the base class for plugin
failures, so plugin code can raise it instead of inventing a hierarchy;
PluginNotInstalledError is a RuntimeError for the programming error of
using a plugin that was never bound.
Publishing a plugin#
A plugin is a normal Python package. Keep the conventions the ecosystem uses:
name the distribution muffin-<name>, expose Plugin from the package root,
let Plugin(app) work with no extra calls, and make configuration overridable
from {APP}_{PLUGIN}_* environment variables by reading self.cfg instead of
os.environ. A plugin’s own tests can use the pytest app fixture from the host
application, or instantiate the plugin in a test application directly.