CLI integration#
Muffin ships a muffin command that loads your application and calls commands
registered on it.
$ muffin path.to.your.module --help
$ muffin path.to.your.module:app_object_name --help
The argument is an import path: either a module that exposes app or a dotted
path to the application object itself. The working directory is added to
sys.path, so the command works from the project root.
Global options:
--config settings.local: setMUFFIN_CONFIGbefore the app is imported--aiolib asyncio|trio|curio: pick the async library for the command, shown only when more than one is installed--version: print the Muffin version
Built-in commands#
run#
Start the application with uvicorn. Host and port default to localhost and
5000.
$ muffin your.module run --host 0.0.0.0 --port 8000
LOG_CONFIG from the application configuration is passed to uvicorn, so
logging behaves the same under the CLI and under a standalone server.
shell#
Start an interactive shell with the application in scope.
$ muffin your.module shell
$ muffin your.module shell --no-ipython
The shell is IPython when it is installed, and the standard code.interact
otherwise; --no-ipython forces the latter. The context comes from
MANAGE_SHELL: by default it holds app, run, lifespan and one
entry per registered plugin.
Change the context with a mapping or a callable:
app.manage.shell({"app": app, "db": app.plugins["db"]})
Custom commands#
Register a command with app.manage. The function name becomes the command
name, with underscores turned into dashes:
@app.manage
async def hello(name, upper=False):
"""Say hello to someone.
:param name: Your name
:param upper: Use uppercase
"""
greetings = f"Hello {name}!"
if upper:
greetings = greetings.upper()
print(greetings)
$ muffin example hello mike --upper
HELLO MIKE!
$ muffin example hello --help
Say hello to someone.
positional arguments:
name Your name
options:
-h, --help show this help message and exit
--upper Use uppercase
Arguments are generated from the signature and the docstring:
parameters without a default become positional arguments
booldefaults become--flag(defaultFalse) or--no-flag(defaultTrue)other defaults become optional arguments with the default shown in the help
list defaults accumulate: pass the option once per value
annotations are used as the argument type;
*argscollects the rest
The first line of the docstring becomes the command description; :param
name: lines become the argument help. Pass name= to set the command name
explicitly:
@app.manage(name="say-hello")
async def hello(name):
...
Async commands run inside the application’s async library. Pass
lifespan=True to run one inside the application lifespan, which is what you
want when the command needs plugin connections:
@app.manage(lifespan=True)
async def cleanup():
"""Drop expired sessions."""
await app.plugins["db"].cleanup()
Uncaught exceptions from a command are logged and turn into a non-zero exit code, so commands are safe to use in scripts.