Add non-interactive mode to redbot-setup (#5448)

* Simplify `redbot-setup backup` thanks to Click 8.0

* Add some of the missing type hints

* Fix unnecessary new lines in `redbot-setup` and `redbot-setup delete`

* Add default value for storage backend

* Add non-interactive mode to `redbot-setup`
This commit is contained in:
jack1142 2021-12-31 02:08:18 +01:00 committed by GitHub
parent ff7c146b62
commit 8cc004f70f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -55,10 +55,13 @@ def save_config(name, data, remove=False):
json.dump(_config, fs, indent=4)
def get_data_dir(instance_name: str):
def get_data_dir(*, instance_name: str, data_path: Optional[Path], interactive: bool) -> str:
if data_path is not None:
return str(data_path.resolve())
data_path = Path(appdir.user_data_dir) / "data" / instance_name
if not interactive:
return str(data_path.resolve())
print()
print(
"We've attempted to figure out a sane default data location which is printed below."
" If you don't want to change this default please press [ENTER],"
@ -99,15 +102,23 @@ def get_data_dir(instance_name: str):
return str(data_path.resolve())
def get_storage_type():
storage_dict = {1: "JSON", 2: "PostgreSQL"}
def get_storage_type(backend: Optional[str], *, interactive: bool):
if backend:
return get_target_backend(backend)
if not interactive:
return BackendType.JSON
storage_dict = {1: BackendType.JSON, 2: BackendType.POSTGRES}
storage = None
while storage is None:
print()
print("Please choose your storage backend (if you're unsure, just choose 1).")
print("Please choose your storage backend.")
print("1. JSON (file storage, requires no database).")
print("2. PostgreSQL (Requires a database server)")
print("If you're unsure, press [ENTER] to use the recommended default - JSON.")
storage = input("> ")
if not storage:
return BackendType.JSON
try:
storage = int(storage)
except ValueError:
@ -115,11 +126,20 @@ def get_storage_type():
else:
if storage not in storage_dict:
storage = None
return storage
return storage_dict[storage]
def get_name() -> str:
name = ""
def get_name(name: str) -> str:
INSTANCE_NAME_RE = re.compile(r"[A-Za-z0-9_\.\-]*")
if name:
if INSTANCE_NAME_RE.fullmatch(name) is None:
print(
"ERROR: Instance names can only include characters A-z, numbers, "
"underscores (_) and periods (.)."
)
sys.exit(1)
return name
while len(name) == 0:
print(
"Please enter a name for your instance,"
@ -128,7 +148,7 @@ def get_name() -> str:
" A-z, numbers, underscores (_) and periods (.)."
)
name = input("> ")
if re.fullmatch(r"[A-Za-z0-9_\.\-]*", name) is None:
if INSTANCE_NAME_RE.fullmatch(name) is None:
print(
"ERROR: Instance names can only include characters A-z, numbers, "
"underscores (_) and periods (.)."
@ -144,54 +164,84 @@ def get_name() -> str:
return name
def basic_setup():
def basic_setup(
*,
name: str,
data_path: Optional[Path],
backend: Optional[str],
interactive: bool,
overwrite_existing_instance: bool,
):
"""
Creates the data storage folder.
:return:
"""
if not interactive and not name:
print(
"Providing instance name through --instance-name is required"
" when using non-interactive mode."
)
sys.exit(1)
print(
"Hello! Before we begin, we need to gather some initial information for the new instance."
if interactive:
print(
"Hello! Before we begin, we need to gather some initial information"
" for the new instance."
)
name = get_name(name)
default_data_dir = get_data_dir(
instance_name=name, data_path=data_path, interactive=interactive
)
name = get_name()
default_data_dir = get_data_dir(name)
default_dirs = deepcopy(data_manager.basic_config_default)
default_dirs["DATA_PATH"] = default_data_dir
storage = get_storage_type()
storage_type = get_storage_type(backend, interactive=interactive)
storage_dict = {1: BackendType.JSON, 2: BackendType.POSTGRES}
storage_type: BackendType = storage_dict.get(storage, BackendType.JSON)
default_dirs["STORAGE_TYPE"] = storage_type.value
driver_cls = drivers.get_driver_class(storage_type)
default_dirs["STORAGE_DETAILS"] = driver_cls.get_config_details()
if name in instance_data:
print(
"WARNING: An instance already exists with this name. "
"Continuing will overwrite the existing instance config."
)
if not click.confirm("Are you absolutely certain you want to continue?", default=False):
print("Not continuing")
sys.exit(0)
if overwrite_existing_instance:
pass
elif interactive:
print(
"WARNING: An instance already exists with this name. "
"Continuing will overwrite the existing instance config."
)
if not click.confirm(
"Are you absolutely certain you want to continue?", default=False
):
print("Not continuing")
sys.exit(0)
else:
print(
"An instance with this name already exists.\n"
"If you want to remove the existing instance and replace it with this one,"
" run this command with --overwrite-existing-instance flag."
)
sys.exit(1)
save_config(name, default_dirs)
print()
print(
"Your basic configuration has been saved. Please run `redbot <name>` to"
" continue your setup process and to run the bot.\n\n"
"First time? Read the quickstart guide:\n"
"https://docs.discord.red/en/stable/getting_started.html"
)
if interactive:
print()
print(
f"Your basic configuration has been saved. Please run `redbot {name}` to"
" continue your setup process and to run the bot.\n\n"
"First time? Read the quickstart guide:\n"
"https://docs.discord.red/en/stable/getting_started.html"
)
else:
print("Your basic configuration has been saved.")
def get_current_backend(instance) -> BackendType:
def get_current_backend(instance: str) -> BackendType:
return BackendType(instance_data[instance]["STORAGE_TYPE"])
def get_target_backend(backend) -> BackendType:
def get_target_backend(backend: str) -> BackendType:
if backend == "json":
return BackendType.JSON
elif backend == "postgres":
@ -234,13 +284,13 @@ async def create_backup(instance: str, destination_folder: Path = Path.home()) -
async def remove_instance(
instance,
instance: str,
interactive: bool = False,
delete_data: Optional[bool] = None,
_create_backup: Optional[bool] = None,
drop_db: Optional[bool] = None,
remove_datapath: Optional[bool] = None,
):
) -> None:
data_manager.load_basic_configuration(instance)
backend = get_current_backend(instance)
@ -277,10 +327,10 @@ async def remove_instance(
safe_delete(data_path)
save_config(instance, {}, remove=True)
print("The instance {} has been removed\n".format(instance))
print("The instance {} has been removed.".format(instance))
async def remove_instance_interaction():
async def remove_instance_interaction() -> None:
if not instance_list:
print("No instances have been set up!")
return
@ -303,8 +353,54 @@ async def remove_instance_interaction():
@click.group(invoke_without_command=True)
@click.option("--debug", type=bool)
@click.option(
"--no-prompt",
"interactive",
type=bool,
is_flag=True,
default=True,
help=(
"Don't ask for user input during the process (non-interactive mode)."
" This makes `--instance-name` required."
),
)
@click.option(
"--instance-name",
type=str,
default="",
help="Name of the new instance. Required if --no-prompt is passed.",
)
@click.option(
"--data-path",
type=click.Path(exists=False, dir_okay=True, file_okay=False, writable=True, path_type=Path),
default=None,
help=(
"Data path of the new instance. If this option and --no-prompt are omitted,"
" you will be asked for this."
),
)
@click.option(
"--backend",
type=click.Choice(["json", "postgres"]),
default=None,
help=(
"Choose a backend type for the new instance."
" If this option is omitted, you will be asked for this."
" Defaults to JSON in non-interactive mode.\n"
"Note: Choosing PostgreSQL will prevent the setup from being completely non-interactive."
),
)
@click.option("--overwrite-existing-instance", type=bool, is_flag=True)
@click.pass_context
def cli(ctx, debug):
def cli(
ctx: click.Context,
debug: bool,
interactive: bool,
instance_name: str,
data_path: Optional[Path],
backend: Optional[str],
overwrite_existing_instance: bool,
) -> None:
"""Create a new instance."""
level = logging.DEBUG if debug else logging.INFO
base_logger = logging.getLogger("red")
@ -317,7 +413,13 @@ def cli(ctx, debug):
base_logger.addHandler(stdout_handler)
if ctx.invoked_subcommand is None:
basic_setup()
basic_setup(
name=instance_name,
data_path=data_path,
backend=backend,
overwrite_existing_instance=overwrite_existing_instance,
interactive=interactive,
)
@cli.command()
@ -376,7 +478,7 @@ def delete(
_create_backup: Optional[bool],
drop_db: Optional[bool],
remove_datapath: Optional[bool],
):
) -> None:
"""Removes an instance."""
asyncio.run(
remove_instance(
@ -388,7 +490,7 @@ def delete(
@cli.command()
@click.argument("instance", type=click.Choice(instance_list), metavar="<INSTANCE_NAME>")
@click.argument("backend", type=click.Choice(["json", "postgres"]))
def convert(instance, backend):
def convert(instance: str, backend: str) -> None:
"""Convert data backend of an instance."""
current_backend = get_current_backend(instance)
target = get_target_backend(backend)
@ -418,13 +520,13 @@ def convert(instance, backend):
@click.argument(
"destination_folder",
type=click.Path(
exists=False, dir_okay=True, file_okay=False, resolve_path=True, writable=True
dir_okay=True, file_okay=False, resolve_path=True, writable=True, path_type=Path
),
default=Path.home(),
)
def backup(instance: str, destination_folder: Union[str, Path]) -> None:
def backup(instance: str, destination_folder: Path) -> None:
"""Backup instance's data."""
asyncio.run(create_backup(instance, Path(destination_folder)))
asyncio.run(create_backup(instance, destination_folder))
def run_cli():