The sillo command: every command a Sillo project gets, where they come from, and how to add your own.
The Console
Section titled “The Console”A project created from the starter ships no console file. sillo finds the
application and derives its commands from it.
uv run sillowith no arguments prints everything below.
Where the commands come from
Section titled “Where the commands come from”The application already says what the project has, so nothing is configured
twice. sillo imports it and reads it:
| On the application | What it brings |
|---|---|
setup_record(app, …) on app.state["record"] | db:* and user:* |
setup_scheduler(app) on app.state["scheduler"] | schedule:* |
AuthenticationMiddleware(user_model=…) | which model accounts are created in |
app.add_command(…) | the project’s own commands |
A project with no database gets no db:*. There is nothing to migrate. The
queue commands are always offered, because a queue needs no setup to inspect.
The starter wires the first three in app/bootstrap.py, so all of it is
available as soon as dependencies are installed.
How the application is found
Section titled “How the application is found”In order: the SILLO_APP environment variable, then [tool.sillo] app in
pyproject.toml, then app.main:app, main:app, app:app. The starter’s
layout matches the first conventional name, so it needs no configuration.
# pyproject.toml — only if your application lives somewhere unusual[tool.sillo]app = "src/myapp/server.py:application"Outside a project none of those resolve and only version, serve and
routes are offered.
Always run it through uv run
Section titled “Always run it through uv run”uv run sillo db:migrate # correctsillo db:migrate # depends on what is activatedA virtual environment activated in a parent directory shadows the project’s
own, and the sillo it finds there is usually older than the project needs.
uv run avoids the question entirely.
Database
Section titled “Database”uv run sillo db:migrate [--target] [--fake]uv run sillo db:make [name] [--apply]uv run sillo db:plan [--target]uv run sillo db:rollback <target> [--fake] [-f]uv run sillo db:statusuv run sillo db:sql <migration> [--backward]uv run sillo db:initdb:migrate
Section titled “db:migrate”Applies every pending migration, listing them first.
$ uv run sillo db:migrate • + models.0001_initial
✓ Applied 1 migration.Nothing pending says so and changes nothing:
$ uv run sillo db:migrateNothing pending.--fake records migrations as applied without running their SQL. That is for
adopting a schema that already exists (tables created before the project had
migrations) not for skipping one that fails.
db:make
Section titled “db:make”Writes a migration describing the difference between the models and the last migration.
uv run sillo db:make add_posts # write ituv run sillo db:make add_posts --apply # write and apply✓ Migration written. Review it, then: sillo db:migrateWhen the models already match, nothing is written and it says so rather than reporting a success for a file that does not exist:
No model changes to record.db:plan
Section titled “db:plan”What db:migrate would do, without doing it. Worth running before a
deployment.
db:status
Section titled “db:status”Whether the database is up to date.
$ uv run sillo db:status app models pending 0
✓ Up to date.db:rollback
Section titled “db:rollback” USAGE sillo db:rollback <TARGET> [options]
ARGUMENTS TARGET Migration to stop at, or 'zero'
OPTIONS --fake Record the rollback without running it -f, --force Skip the confirmationThere is no implicit “one step back”: name the migration to stop at. zero
unapplies everything, which drops the tables those migrations made, so it asks
you to type zero back before it does. Without a terminal it refuses rather
than assuming yes, which is what stops an unattended run from dropping a
schema.
uv run sillo user:admin <email> [username]uv run sillo user:create <email> <username> [--admin]uv run sillo user:list [-l] [--offset] [--staff]uv run sillo user:show <identifier>uv run sillo user:password <identifier>uv run sillo user:active <identifier> [--off]uv run sillo user:staff <identifier> [--revoke]The account is created in the model the application authenticates against. The
starter’s database/models/user.py, because app/bootstrap.py passes it to
AuthenticationMiddleware.
user:admin and user:create
Section titled “user:admin and user:create”$ uv run sillo user:admin ada@example.com adaPassword: ••••••••Confirm: ••••••••✓ Created ada@example.com. Sign in at /admin/The password is read from a hidden prompt. With no terminal (CI, a container
build) it comes from SILLO_PASSWORD instead, and with neither the command
fails and says so rather than blocking on a prompt nobody can answer.
SILLO_PASSWORD='…' uv run sillo user:admin ci@example.com ciuser:admin omits the username when you do: it defaults to the mailbox, so
ada@example.com becomes ada.
user:list
Section titled “user:list”$ uv run sillo user:list id email username admin active ── ─────────────── ──────── ───── ────── 1 ada@example.com ada yes yes
1 shown--staff narrows it to accounts with admin access. -l limits the rows.
user:active and user:staff
Section titled “user:active and user:staff”Deactivating is the reversible alternative to deleting: credentials stop working immediately and the rows referencing the user stay valid.
uv run sillo user:active ada@example.com --off # cannot sign inuv run sillo user:active ada@example.com # can againuv run sillo user:staff ada@example.com # grant admin accessuv run sillo user:staff ada@example.com --revokeThe identifier is an email address or a username, and matches deactivated accounts too. An account you cannot find is one you could never turn back on.
Processes
Section titled “Processes”queue:work
Section titled “queue:work” OPTIONS -q, --queue Queue to consume. Repeatable, highest priority first -c, --concurrency Jobs at once [4] --timeout Seconds one job may run [60.0] --max-jobs Restart after this many jobs. 0 is unlimited [0]QUEUE_URL=redis://localhost:6379 uv run sillo queue:workuv run sillo queue:work --queue urgent --queue defaultQueues are consumed in the order named, so the first is drained before the second is looked at.
Without a redis:// URL the queue lives in this process, so nothing a web
process dispatches ever reaches it. The command says so rather than sitting at
zero looking healthy.
queue:list and queue:failed
Section titled “queue:list and queue:failed”$ uv run sillo queue:list queue waiting ─────── ─────── default 0queue:failed lists jobs that exhausted their retries, queue:forget <id>
drops one, and queue:flush drops them all. The failed-job record is in memory
unless you bind a durable one, and queue:failed reports that distinction
rather than printing “no failures” at somebody about to stop looking.
schedule:run and schedule:list
Section titled “schedule:run and schedule:list”$ uv run sillo schedule:list name trigger status runs last run ───── ───────── ────── ──── ──────── prune 0 3 * * * active 0 —
$ uv run sillo schedule:runschedule:pause <id> and schedule:resume <id> stop and restart one task.
These need a scheduler, which setup_scheduler(app) puts on app.state. The
starter has that behind the commented-out _register_work(application) in
app/bootstrap.py; until you uncomment it, the schedule commands say there is
no scheduler bound rather than reporting an empty one.
serve and routes
Section titled “serve and routes”uv run uvicorn app:app --reload # developmentuv run uvicorn app:app -p 9000uv run sillo routes # every route, method and handleruv run sillo routes -m post # only POSTBoth default to the application sillo already found, so neither needs an
import string.
A fresh clone has no migration yet
Section titled “A fresh clone has no migration yet”db:migrate applies what exists. On a brand-new project there is nothing to
apply, so the first run is two commands:
sillo db:init # create the migrations packagesillo db:make initial # write the first migration from the modelssillo db:migrate # apply itAfter that, sillo db:migrate on its own is all you need, and sillo-start
runs the first two for you when it creates the project.
Adding your own
Section titled “Adding your own”Register a command on the application and it appears in the same listing, grouped by the part of its name before the colon.
from sillo.console import Argument, Command, Option
app = create_app()
@app.add_commandclass Backfill(Command): """Fill in slugs for posts written before the column existed."""
name = "posts:backfill" help = "Backfill post slugs"
arguments = [ Argument("since", default=None, help="Only posts after this date"), Option("batch", type=int, default=100, help="Rows per batch"), ]
async def handle(self): from database.models.post import Post
query = Post.filter(slug=None) if self.argument("since"): query = query.filter(created_at__gte=self.argument("since"))
total = 0 for post in await query.limit(self.option("batch")): post.slug = slugify(post.title) await post.save() total += 1
self.success(f"Backfilled {total} posts.")uv run sillo posts:backfilluv run sillo posts:backfill 2024-01-01 --batch 500For something short, the decorator form skips the class:
@app.command("cache:clear", help="Drop every cached entry")async def clear(command): await cache.flush() command.success("Cache cleared.")Commands registered on the application are added last, so a name you choose
overrides a built-in one of the same name. sillo.console documents the
parameter types, the output helpers and the interactive prompts in full. See
Console Commands.
Errors and output
Section titled “Errors and output”Exit codes are what a script would expect: 0 for success, 2 for a usage
error, 1 for a command that failed, 130 for a cancelled prompt.
$ uv run sillo db:rollback✗ missing argument <TARGET> Usage: sillo db:rollback <TARGET> [options]Colour is dropped when the output is not a terminal, so a log file gets plain
text. NO_COLOR=1 turns it off everywhere and FORCE_COLOR=1 keeps it through
a pipe.
Related
Section titled “Related”- Console Commands: the toolkit underneath, in full
- Database: what the migration commands act on
- Background Work: the queue and scheduler
- Deployment: running these in production