aerich/aerich/cli.py
Waket Zheng 5d9adbdb54
chore: improve type hints (#432)
* chore: improve type hints

* chore: set `warn_unused_ignores` true for mypy

* refactor: use function to compare tortoise version

* refactor: change function name
2025-03-04 14:52:12 +08:00

283 lines
9.0 KiB
Python

from __future__ import annotations
import os
from pathlib import Path
from typing import cast
import asyncclick as click
from asyncclick import Context, UsageError
from aerich import Command
from aerich._compat import imports_tomlkit, tomllib
from aerich.enums import Color
from aerich.exceptions import DowngradeError
from aerich.utils import add_src_path, get_tortoise_config
from aerich.version import __version__
CONFIG_DEFAULT_VALUES = {
"src_folder": ".",
}
@click.group(context_settings={"help_option_names": ["-h", "--help"]})
@click.version_option(__version__, "-V", "--version")
@click.option(
"-c",
"--config",
default="pyproject.toml",
show_default=True,
help="Config file.",
)
@click.option("--app", required=False, help="Tortoise-ORM app name.")
@click.pass_context
async def cli(ctx: Context, config, app) -> None:
ctx.ensure_object(dict)
ctx.obj["config_file"] = config
invoked_subcommand = ctx.invoked_subcommand
if invoked_subcommand != "init":
config_path = Path(config)
if not config_path.exists():
raise UsageError(
"You need to run `aerich init` first to create the config file.", ctx=ctx
)
content = config_path.read_text("utf-8")
doc: dict = tomllib.loads(content)
try:
tool = cast("dict[str, str]", doc["tool"]["aerich"])
location = tool["location"]
tortoise_orm = tool["tortoise_orm"]
src_folder = tool.get("src_folder", CONFIG_DEFAULT_VALUES["src_folder"])
except KeyError as e:
raise UsageError(
"You need run `aerich init` again when upgrading to aerich 0.6.0+."
) from e
add_src_path(src_folder)
tortoise_config = get_tortoise_config(ctx, tortoise_orm)
if not app:
try:
apps_config = cast(dict, tortoise_config["apps"])
except KeyError:
raise UsageError('Config must define "apps" section')
app = list(apps_config.keys())[0]
command = Command(tortoise_config=tortoise_config, app=app, location=location)
ctx.obj["command"] = command
if invoked_subcommand != "init-db":
if not Path(location, app).exists():
raise UsageError(
"You need to run `aerich init-db` first to initialize the database.", ctx=ctx
)
await command.init()
@cli.command(help="Generate a migration file for the current state of the models.")
@click.option("--name", default="update", show_default=True, help="Migration name.")
@click.option("--empty", default=False, is_flag=True, help="Generate an empty migration file.")
@click.pass_context
async def migrate(ctx: Context, name, empty) -> None:
command = ctx.obj["command"]
ret = await command.migrate(name, empty)
if not ret:
return click.secho("No changes detected", fg=Color.yellow)
click.secho(f"Success creating migration file {ret}", fg=Color.green)
@cli.command(help="Upgrade to specified migration version.")
@click.option(
"--in-transaction",
"-i",
default=True,
type=bool,
help="Make migrations in a single transaction or not. Can be helpful for large migrations or creating concurrent indexes.",
)
@click.option(
"--fake",
default=False,
is_flag=True,
help="Mark migrations as run without actually running them.",
)
@click.pass_context
async def upgrade(ctx: Context, in_transaction: bool, fake: bool) -> None:
command = ctx.obj["command"]
migrated = await command.upgrade(run_in_transaction=in_transaction, fake=fake)
if not migrated:
click.secho("No upgrade items found", fg=Color.yellow)
else:
for version_file in migrated:
if fake:
click.echo(
f"Upgrading to {version_file}... " + click.style("FAKED", fg=Color.green)
)
else:
click.secho(f"Success upgrading to {version_file}", fg=Color.green)
@cli.command(help="Downgrade to specified version.")
@click.option(
"-v",
"--version",
default=-1,
type=int,
show_default=False,
help="Specified version, default to last migration.",
)
@click.option(
"-d",
"--delete",
is_flag=True,
default=False,
show_default=True,
help="Also delete the migration files.",
)
@click.option(
"--fake",
default=False,
is_flag=True,
help="Mark migrations as run without actually running them.",
)
@click.pass_context
@click.confirmation_option(
prompt="Downgrade is dangerous: you might lose your data! Are you sure?",
)
async def downgrade(ctx: Context, version: int, delete: bool, fake: bool) -> None:
command = ctx.obj["command"]
try:
files = await command.downgrade(version, delete, fake=fake)
except DowngradeError as e:
return click.secho(str(e), fg=Color.yellow)
for file in files:
if fake:
click.echo(f"Downgrading to {file}... " + click.style("FAKED", fg=Color.green))
else:
click.secho(f"Success downgrading to {file}", fg=Color.green)
@cli.command(help="Show currently available heads (unapplied migrations).")
@click.pass_context
async def heads(ctx: Context) -> None:
command = ctx.obj["command"]
head_list = await command.heads()
if not head_list:
return click.secho("No available heads.", fg=Color.green)
for version in head_list:
click.secho(version, fg=Color.green)
@cli.command(help="List all migrations.")
@click.pass_context
async def history(ctx: Context) -> None:
command = ctx.obj["command"]
versions = await command.history()
if not versions:
return click.secho("No migrations created yet.", fg=Color.green)
for version in versions:
click.secho(version, fg=Color.green)
def _write_config(config_path, doc, table) -> None:
tomlkit = imports_tomlkit()
try:
doc["tool"]["aerich"] = table
except KeyError:
doc["tool"] = {"aerich": table}
config_path.write_text(tomlkit.dumps(doc))
@cli.command(help="Initialize aerich config and create migrations folder.")
@click.option(
"-t",
"--tortoise-orm",
required=True,
help="Tortoise-ORM config dict location, like `settings.TORTOISE_ORM`.",
)
@click.option(
"--location",
default="./migrations",
show_default=True,
help="Migrations folder.",
)
@click.option(
"-s",
"--src_folder",
default=CONFIG_DEFAULT_VALUES["src_folder"],
show_default=False,
help="Folder of the source, relative to the project root.",
)
@click.pass_context
async def init(ctx: Context, tortoise_orm, location, src_folder) -> None:
config_file = ctx.obj["config_file"]
if os.path.isabs(src_folder):
src_folder = os.path.relpath(os.getcwd(), src_folder)
# Add ./ so it's clear that this is relative path
if not src_folder.startswith("./"):
src_folder = "./" + src_folder
# check that we can find the configuration, if not we can fail before the config file gets created
add_src_path(src_folder)
get_tortoise_config(ctx, tortoise_orm)
config_path = Path(config_file)
content = config_path.read_text("utf-8") if config_path.exists() else "[tool.aerich]"
doc: dict = tomllib.loads(content)
table = {"tortoise_orm": tortoise_orm, "location": location, "src_folder": src_folder}
if (aerich_config := doc.get("tool", {}).get("aerich")) and all(
aerich_config.get(k) == v for k, v in table.items()
):
click.echo(f"Aerich config {config_file} already inited.")
else:
_write_config(config_path, doc, table)
click.secho(f"Success writing aerich config to {config_file}", fg=Color.green)
Path(location).mkdir(parents=True, exist_ok=True)
click.secho(f"Success creating migrations folder {location}", fg=Color.green)
@cli.command(help="Generate schema and generate app migration folder.")
@click.option(
"-s",
"--safe",
type=bool,
is_flag=True,
default=True,
help="Create tables only when they do not already exist.",
show_default=True,
)
@click.pass_context
async def init_db(ctx: Context, safe: bool) -> None:
command = ctx.obj["command"]
app = command.app
dirname = Path(command.location, app)
try:
await command.init_db(safe)
click.secho(f"Success creating app migration folder {dirname}", fg=Color.green)
click.secho(f'Success generating initial migration file for app "{app}"', fg=Color.green)
except FileExistsError:
return click.secho(
f"App {app} is already initialized. Delete {dirname} and try again.", fg=Color.yellow
)
@cli.command(help="Prints the current database tables to stdout as Tortoise-ORM models.")
@click.option(
"-t",
"--table",
help="Which tables to inspect.",
multiple=True,
required=False,
)
@click.pass_context
async def inspectdb(ctx: Context, table: list[str]) -> None:
command = ctx.obj["command"]
ret = await command.inspectdb(table)
click.secho(ret)
def main() -> None:
cli()
if __name__ == "__main__":
main()