mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-11-06 11:18:54 -05:00
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:
parent
ff7c146b62
commit
8cc004f70f
192
redbot/setup.py
192
redbot/setup.py
@ -55,10 +55,13 @@ def save_config(name, data, remove=False):
|
|||||||
json.dump(_config, fs, indent=4)
|
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
|
data_path = Path(appdir.user_data_dir) / "data" / instance_name
|
||||||
|
if not interactive:
|
||||||
|
return str(data_path.resolve())
|
||||||
|
|
||||||
print()
|
|
||||||
print(
|
print(
|
||||||
"We've attempted to figure out a sane default data location which is printed below."
|
"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],"
|
" 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())
|
return str(data_path.resolve())
|
||||||
|
|
||||||
|
|
||||||
def get_storage_type():
|
def get_storage_type(backend: Optional[str], *, interactive: bool):
|
||||||
storage_dict = {1: "JSON", 2: "PostgreSQL"}
|
if backend:
|
||||||
|
return get_target_backend(backend)
|
||||||
|
if not interactive:
|
||||||
|
return BackendType.JSON
|
||||||
|
storage_dict = {1: BackendType.JSON, 2: BackendType.POSTGRES}
|
||||||
storage = None
|
storage = None
|
||||||
while storage is None:
|
while storage is None:
|
||||||
print()
|
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("1. JSON (file storage, requires no database).")
|
||||||
print("2. PostgreSQL (Requires a database server)")
|
print("2. PostgreSQL (Requires a database server)")
|
||||||
|
print("If you're unsure, press [ENTER] to use the recommended default - JSON.")
|
||||||
|
|
||||||
storage = input("> ")
|
storage = input("> ")
|
||||||
|
if not storage:
|
||||||
|
return BackendType.JSON
|
||||||
try:
|
try:
|
||||||
storage = int(storage)
|
storage = int(storage)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
@ -115,11 +126,20 @@ def get_storage_type():
|
|||||||
else:
|
else:
|
||||||
if storage not in storage_dict:
|
if storage not in storage_dict:
|
||||||
storage = None
|
storage = None
|
||||||
return storage
|
return storage_dict[storage]
|
||||||
|
|
||||||
|
|
||||||
def get_name() -> str:
|
def get_name(name: str) -> str:
|
||||||
name = ""
|
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:
|
while len(name) == 0:
|
||||||
print(
|
print(
|
||||||
"Please enter a name for your instance,"
|
"Please enter a name for your instance,"
|
||||||
@ -128,7 +148,7 @@ def get_name() -> str:
|
|||||||
" A-z, numbers, underscores (_) and periods (.)."
|
" A-z, numbers, underscores (_) and periods (.)."
|
||||||
)
|
)
|
||||||
name = input("> ")
|
name = input("> ")
|
||||||
if re.fullmatch(r"[A-Za-z0-9_\.\-]*", name) is None:
|
if INSTANCE_NAME_RE.fullmatch(name) is None:
|
||||||
print(
|
print(
|
||||||
"ERROR: Instance names can only include characters A-z, numbers, "
|
"ERROR: Instance names can only include characters A-z, numbers, "
|
||||||
"underscores (_) and periods (.)."
|
"underscores (_) and periods (.)."
|
||||||
@ -144,54 +164,84 @@ def get_name() -> str:
|
|||||||
return name
|
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.
|
Creates the data storage folder.
|
||||||
:return:
|
: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(
|
if interactive:
|
||||||
"Hello! Before we begin, we need to gather some initial information for the new instance."
|
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 = deepcopy(data_manager.basic_config_default)
|
||||||
default_dirs["DATA_PATH"] = default_data_dir
|
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
|
default_dirs["STORAGE_TYPE"] = storage_type.value
|
||||||
driver_cls = drivers.get_driver_class(storage_type)
|
driver_cls = drivers.get_driver_class(storage_type)
|
||||||
default_dirs["STORAGE_DETAILS"] = driver_cls.get_config_details()
|
default_dirs["STORAGE_DETAILS"] = driver_cls.get_config_details()
|
||||||
|
|
||||||
if name in instance_data:
|
if name in instance_data:
|
||||||
print(
|
if overwrite_existing_instance:
|
||||||
"WARNING: An instance already exists with this name. "
|
pass
|
||||||
"Continuing will overwrite the existing instance config."
|
elif interactive:
|
||||||
)
|
print(
|
||||||
if not click.confirm("Are you absolutely certain you want to continue?", default=False):
|
"WARNING: An instance already exists with this name. "
|
||||||
print("Not continuing")
|
"Continuing will overwrite the existing instance config."
|
||||||
sys.exit(0)
|
)
|
||||||
|
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)
|
save_config(name, default_dirs)
|
||||||
|
|
||||||
print()
|
if interactive:
|
||||||
print(
|
print()
|
||||||
"Your basic configuration has been saved. Please run `redbot <name>` to"
|
print(
|
||||||
" continue your setup process and to run the bot.\n\n"
|
f"Your basic configuration has been saved. Please run `redbot {name}` to"
|
||||||
"First time? Read the quickstart guide:\n"
|
" continue your setup process and to run the bot.\n\n"
|
||||||
"https://docs.discord.red/en/stable/getting_started.html"
|
"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"])
|
return BackendType(instance_data[instance]["STORAGE_TYPE"])
|
||||||
|
|
||||||
|
|
||||||
def get_target_backend(backend) -> BackendType:
|
def get_target_backend(backend: str) -> BackendType:
|
||||||
if backend == "json":
|
if backend == "json":
|
||||||
return BackendType.JSON
|
return BackendType.JSON
|
||||||
elif backend == "postgres":
|
elif backend == "postgres":
|
||||||
@ -234,13 +284,13 @@ async def create_backup(instance: str, destination_folder: Path = Path.home()) -
|
|||||||
|
|
||||||
|
|
||||||
async def remove_instance(
|
async def remove_instance(
|
||||||
instance,
|
instance: str,
|
||||||
interactive: bool = False,
|
interactive: bool = False,
|
||||||
delete_data: Optional[bool] = None,
|
delete_data: Optional[bool] = None,
|
||||||
_create_backup: Optional[bool] = None,
|
_create_backup: Optional[bool] = None,
|
||||||
drop_db: Optional[bool] = None,
|
drop_db: Optional[bool] = None,
|
||||||
remove_datapath: Optional[bool] = None,
|
remove_datapath: Optional[bool] = None,
|
||||||
):
|
) -> None:
|
||||||
data_manager.load_basic_configuration(instance)
|
data_manager.load_basic_configuration(instance)
|
||||||
backend = get_current_backend(instance)
|
backend = get_current_backend(instance)
|
||||||
|
|
||||||
@ -277,10 +327,10 @@ async def remove_instance(
|
|||||||
safe_delete(data_path)
|
safe_delete(data_path)
|
||||||
|
|
||||||
save_config(instance, {}, remove=True)
|
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:
|
if not instance_list:
|
||||||
print("No instances have been set up!")
|
print("No instances have been set up!")
|
||||||
return
|
return
|
||||||
@ -303,8 +353,54 @@ async def remove_instance_interaction():
|
|||||||
|
|
||||||
@click.group(invoke_without_command=True)
|
@click.group(invoke_without_command=True)
|
||||||
@click.option("--debug", type=bool)
|
@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
|
@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."""
|
"""Create a new instance."""
|
||||||
level = logging.DEBUG if debug else logging.INFO
|
level = logging.DEBUG if debug else logging.INFO
|
||||||
base_logger = logging.getLogger("red")
|
base_logger = logging.getLogger("red")
|
||||||
@ -317,7 +413,13 @@ def cli(ctx, debug):
|
|||||||
base_logger.addHandler(stdout_handler)
|
base_logger.addHandler(stdout_handler)
|
||||||
|
|
||||||
if ctx.invoked_subcommand is None:
|
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()
|
@cli.command()
|
||||||
@ -376,7 +478,7 @@ def delete(
|
|||||||
_create_backup: Optional[bool],
|
_create_backup: Optional[bool],
|
||||||
drop_db: Optional[bool],
|
drop_db: Optional[bool],
|
||||||
remove_datapath: Optional[bool],
|
remove_datapath: Optional[bool],
|
||||||
):
|
) -> None:
|
||||||
"""Removes an instance."""
|
"""Removes an instance."""
|
||||||
asyncio.run(
|
asyncio.run(
|
||||||
remove_instance(
|
remove_instance(
|
||||||
@ -388,7 +490,7 @@ def delete(
|
|||||||
@cli.command()
|
@cli.command()
|
||||||
@click.argument("instance", type=click.Choice(instance_list), metavar="<INSTANCE_NAME>")
|
@click.argument("instance", type=click.Choice(instance_list), metavar="<INSTANCE_NAME>")
|
||||||
@click.argument("backend", type=click.Choice(["json", "postgres"]))
|
@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."""
|
"""Convert data backend of an instance."""
|
||||||
current_backend = get_current_backend(instance)
|
current_backend = get_current_backend(instance)
|
||||||
target = get_target_backend(backend)
|
target = get_target_backend(backend)
|
||||||
@ -418,13 +520,13 @@ def convert(instance, backend):
|
|||||||
@click.argument(
|
@click.argument(
|
||||||
"destination_folder",
|
"destination_folder",
|
||||||
type=click.Path(
|
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(),
|
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."""
|
"""Backup instance's data."""
|
||||||
asyncio.run(create_backup(instance, Path(destination_folder)))
|
asyncio.run(create_backup(instance, destination_folder))
|
||||||
|
|
||||||
|
|
||||||
def run_cli():
|
def run_cli():
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user