Skip to content

The hare command

The hare command (installed as a console script) reads its config from -c/--config — either module.VARIABLE (a dotted path to a config value, e.g. settings.HARE_ORM) or the path of a .json/.yml/.yaml file — and without it from the HARE_ORM environment variable or [tool.hare].hare_orm in pyproject.toml, which hold the same type of value. It is what HareConfig.load() takes. hare -V/--version prints hare's version.

Command Purpose
hare init [app_labels...] Create a migrations package for the given (or all) apps.
hare shell IPython shell (pip install hare-orm[ipython]) with top-level await, and Hare, hare (the HareContext), apps and every model already in scope — each model also as <app_label>_<ModelName>, which tells apart two apps' models of one name.
hare makemigrations [app_labels...] [--empty] [--merge] [--check] [--dry-run] [-n/--name NAME] [--rename OLD=NEW]... [--no-input] Generates migrations from the changes to the models — see hare makemigrations.
hare squashmigrations APP_LABEL [START] END [--squashed-name NAME] Squashes a run of an app's migrations into one that replaces them — see hare squashmigrations.
hare migrate [app_label] [migration] [--fake] [--dry-run] [--lock-timeout SECONDS] Applies or unapplies migrations up to a given one — see hare migrate.
hare history [app_labels...] List migrations already applied (from the database), grouped by connection/app.
hare heads [app_labels...] List head migrations on disk (not from the database).
hare inspectdb [tables...] [--connection ALIAS] [--schema SCHEMA] Writes the models of an existing schema — see Models from an existing database.
hare dbshell [--connection ALIAS] Runs the database's own interactive client on a connection — see hare dbshell.
hare checkmigrations [app_labels...] Checks the migrations migrate would apply for risks to a database in use — see hare checkmigrations.
hare sqlmigrate APP_LABEL MIGRATION_NAME [--backward] Print a migration's SQL without running it; --backward prints the rollback. An atomic migration on a database that rolls back DDL (PostgreSQL, SQLite) is wrapped in BEGIN;/COMMIT;. Any migration on disk is printed, a squashed one and those it replaces alike, each on the state of its dependencies.
hare drift [app_labels...] [--connection ALIAS] [--schema SCHEMA] [--journal-table NAME ...] Compares a connection's live database with the models — see hare drift.
hare distributed-recover --coordinator ALIAS [--finish] [--older-than SECONDS] Reports or finishes Transactions.distributed() prepared transactions that never resolved — see hare distributed-recover.
hare stubs [--output DIRECTORY] [--relation-depth N] [--check] Writes the stubs pyright and Pylance check the models with — see hare stubs.

hare makemigrations

Generate migrations by diffing model state; an app label given twice counts once. --empty needs an app label and creates a blank migration depending on the current heads. --merge needs an app label and writes one migration joining the app's forked history (two heads); without it a forked history is an error. --dry-run prints what would be written without writing; --check does the same and exits 1 when there is anything to write (neither combines with --empty). -n/--name renames every migration generated in the run, dependencies between them included. A removed field or model declared exactly like an added one is asked about in the terminal — Was field 'title' of shop.Item renamed to 'name'? [y/N]; --rename answers beforehand (repeatable: --rename shop.Item.title=name, --rename shop.Item=Product, --rename blog.Tag=catalog.Tag for a model moved keeping its table — see Renames); --no-input, or input not from a terminal, asks nothing — only the --rename answers rename anything. An answer that renames nothing — a misspelt name, a model renamed while its fields change — fails the command, and nothing is written: the field or model it meant would be removed and added, its rows dropped. Without an explicit migrations module, an app whose models live in a top-level models.py (no package) gets a top-level migrations package next to it. Two apps that would end up with the same migrations package (two such single-file apps in one directory, or the same explicit migrations value) are refused with an error by every migration command — set a distinct migrations module for each (e.g. "migrations_billing").

hare squashmigrations

Squash an app's migrations from START (its first without it) to END — each a name or a unique prefix — into one that replaces them: a database that applied none or all of them runs it in their place, one that applied only some runs the rest of them first. See Squashing migrations.

hare migrate

Apply migrations forward or backward, whichever the current database state calls for: a migration after the applied ones applies up to it, one before them unapplies back to it, and zero unapplies every migration of the app (hare migrate models zero). migration may be a unique prefix of the name (0002 for 0002_add_email), as for sqlmigrate; a name of no migration, or one several begin with, exits with code 2 here, in sqlmigrate and in squashmigrations, as an unknown app_label does. Without migration the app goes to its latest; without app_label every app does. --fake records history without running SQL; --dry-run prints the plan only and writes nothing to the database, not even the migrations table. --lock-timeout fails a migration whose statement waits longer for a lock another session holds (see Lock timeout); a value outside the range a transaction's lock_timeout takes exits with code 2.

hare dbshell

Run the database's own interactive client on a connection (default: the first one configured): psql for PostgreSQL, sqlite3 for a SQLite file, clickhouse-client for ClickHouse. The password goes to the client through its environment (PGPASSWORD, CLICKHOUSE_PASSWORD), never among the arguments; a connection with a password_provider passes the provider's password. PostgreSQL's TLS settings, schema and application_name go along too (PGSSLMODE, PGSSLROOTCERT, PGOPTIONS, PGAPPNAME). Exits with the client's exit code. An in-memory SQLite database, a dialect that names no client, or a client not on PATH is refused with an error.

hare checkmigrations

Check the migrations migrate would apply against the database: operations that lock or rewrite a large table, or break the code still running during a deployment (renamed and dropped columns and tables, raw SQL), each printed with how to make the change safely. Exits 1 while a risk isn't listed in its migration's safety_exemptions. A table is large from the config's migrations.safety.large_table_rows rows. makemigrations prints the same warnings for the migrations it writes, without reading the database. See Checking migrations.

hare drift

Compare the live database of a connection (default: the first one configured) against current model state of the given apps (default: every app) that use it — tables/columns present in the database but not in any migration, and operations that would be needed to reconcile the rest. Exits 1 if anything is found. Meta.managed = False models are skipped entirely, same as makemigrations. --schema picks the schema swept for untracked tables — an attached database on SQLite, a database of the server on ClickHouse (default: the connection's current schema). hare's own bookkeeping tables (hare_migrations, Transactions.distributed()'s hare_distributed_decisions) are never reported as untracked, nor is a journal named by --journal-table (a MigrationRecorder(table_name=...)'s), nor are the tables of a PostgreSQL extension (PostGIS's spatial_ref_sys) and SpatiaLite's spatial metadata. See Detecting drift for what is compared and how.

hare distributed-recover

Report (or, with --finish, actually finish) Transactions.distributed() prepared transactions that never resolved. --coordinator (required) is the connection alias holding the hare_distributed_decisions decision log. --finish issues COMMIT PREPARED/ROLLBACK PREPARED instead of only reporting. --older-than ignores anything younger than that many seconds, since it may still be mid-flight (default: 300); 0 ignores nothing. Exits 1 if any stale prepared transaction was found (report mode) or left unresolved after --finish.

hare stubs

Write the stubs pyright and Pylance type-check the modules of models with, into typings/ by default. --relation-depth (0 to 5, default 2) is how many relations a filter key crosses at most; --check writes nothing and exits 1 when a stub is missing or outdated. See pyright and Pylance.

hare -c settings.HARE_ORM makemigrations models -n add_book_isbn
hare -c settings.HARE_ORM migrate models 0003_add_index --dry-run
hare -c settings.HARE_ORM migrate models 0002_add_book_isbn   # back to 0002: unapplies 0003
hare -c settings.HARE_ORM migrate models zero                  # unapplies every migration of the app
hare -c settings.HARE_ORM sqlmigrate models 0003_add_index
hare -c settings.HARE_ORM inspectdb --connection default --schema public books authors

Exit codes: 0 success, 2 usage error (CLIUsageError, printed to stderr), 1 any other failure (CLIError), 130 stopped by Ctrl+C (hare: interrupted printed to stderr; the running command is cancelled, a migration running in a transaction rolled back).

Adding your own commands

Installed packages and your project can add subcommands to hare without touching hare. A command is a CLICommand subclass:

# library/cli.py
from hare.cli.plugins import CLICommand, CLIError, CommandContext


class ExportCommand(CLICommand):
    name = "export"                   # `hare export`
    help = "Export loans to CSV."     # shown in `hare --help`

    def add_arguments(self, parser):
        parser.add_argument("--since")

    async def run(self, cli_context, args):
        config = CommandContext.load_config(cli_context)        # the same -c/--config config
        async with CommandContext.hare_cli_context(config):
            ...                                         # models and connections are ready here
        return 0                                        # exit code; None also means 0

cli_context is a CLIContext (hare.cli.plugins) holding the global option config — the -c/--config value (module.VARIABLE or a file path), None when not given: then CommandContext.load_config(cli_context) locates the config through the environment or pyproject.toml, like every built-in command.

run() raising CLIUsageError exits with code 2, CLIError with code 1, the message printed to stderr — the same as built-in commands. Any other hare exception (ConfigurationError, DBConnectionError, ...) also exits with code 1 and prints its type and message instead of a traceback; so does a built-in command whose configuration can't be loaded (an unknown engine, a bad port, ...). An exception of another type (a RuntimeError, a KeyError — a bug of the command) exits with code 1 and prints its full traceback, to find the bug by. The module defining a command is imported on every hare run, so keep it light and import heavy dependencies inside run().

Two ways to register a command:

  • An installed package declares an entry point in the hare.cli group of its own pyproject.toml; the command appears once the package is installed:
[project.entry-points."hare.cli"]
export = "library.cli:ExportCommand"
  • The project itself lists commands in its config, next to connections and apps (Hare.init() ignores this section — only the CLI reads it):
HARE_ORM = {
    "connections": {...},
    "apps": {...},
    "cli": {"commands": ["library.cli:ExportCommand"]},
}

A command whose name is already taken — by a built-in command or an earlier plugin — is skipped with a warning naming both sources; built-in commands always win. A plugin that fails to import or to declare its arguments is skipped with a warning too, and every other command keeps working.