aerich/aerich/cli.py

250 lines
7.9 KiB
Python

import os
from pathlib import Path
from typing import Dict, List, cast
import asyncclick as click
import tomlkit
from asyncclick import Context, UsageError
from tomlkit.exceptions import NonExistentKey
from aerich import Command
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()
doc: dict = tomlkit.parse(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 NonExistentKey:
raise UsageError("You need run `aerich init` again when upgrading to aerich 0.6.0+.")
add_src_path(src_folder)
tortoise_config = get_tortoise_config(ctx, tortoise_orm)
if not app:
apps_config = cast(dict, tortoise_config.get("apps"))
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.pass_context
async def upgrade(ctx: Context, in_transaction: bool) -> None:
command = ctx.obj["command"]
migrated = await command.upgrade(run_in_transaction=in_transaction)
if not migrated:
click.secho("No upgrade items found", fg=Color.yellow)
else:
for version_file in migrated:
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.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) -> None:
command = ctx.obj["command"]
try:
files = await command.downgrade(version, delete)
except DowngradeError as e:
return click.secho(str(e), fg=Color.yellow)
for file in files:
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)
@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)
if config_path.exists():
content = config_path.read_text()
else:
content = "[tool.aerich]"
doc: dict = tomlkit.parse(content)
table = tomlkit.table()
table["tortoise_orm"] = tortoise_orm
table["location"] = location
table["src_folder"] = src_folder
doc["tool"]["aerich"] = table
config_path.write_text(tomlkit.dumps(doc))
Path(location).mkdir(parents=True, exist_ok=True)
click.secho(f"Success creating migrations folder {location}", fg=Color.green)
click.secho(f"Success writing aerich config to {config_file}", 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()