eptm_dashboard/.venv/lib/python3.12/site-packages/reflex_cli/v2/apps.py

813 lines
26 KiB
Python

"""App commands for the Reflex Cloud CLI."""
from __future__ import annotations
import json
import click
from reflex_cli import constants
from reflex_cli.core.config import Config
from reflex_cli.utils import console
from reflex_cli.utils.exceptions import (
ConfigInvalidFieldValueError,
GetAppError,
NotAuthenticatedError,
ResponseError,
ScaleAppError,
ScaleParamError,
ScaleTypeError,
)
@click.group()
def apps_cli():
"""Commands for managing apps."""
@apps_cli.command(name="history")
@click.argument("app_id", required=False)
@click.option("--app-name", help="The name of the application.")
@click.option("--token", help="The authentication token.")
@click.option(
"--loglevel",
type=click.Choice([level.value for level in constants.LogLevel]),
default=constants.LogLevel.INFO.value,
help="The log level to use.",
)
@click.option(
"--json/--no-json",
"-j",
"as_json",
is_flag=True,
help="Whether to output the result in json format.",
)
@click.option(
"--interactive/--no-interactive",
"-i",
is_flag=True,
default=True,
help="Whether to use interactive mode.",
)
def app_history(
app_id: str | None,
app_name: str | None,
token: str | None,
loglevel: str,
as_json: bool,
interactive: bool,
):
"""Retrieve the deployment history for a given application."""
from reflex_cli.utils import hosting
console.set_log_level(loglevel)
try:
authenticated_client = hosting.get_authenticated_client(
token=token, interactive=interactive
)
if not app_id:
config = hosting.read_config()
if config:
app_id = config.appid
if not isinstance(app_id, (str, type(None))):
console.error(
"app_id must be a string or None. Please check your config file."
)
raise click.exceptions.Exit(1)
if app_name is not None and app_id is None:
result = hosting.search_app(
app_name=app_name,
project_id=None,
client=authenticated_client,
interactive=interactive,
)
app_id = result.get("id") if result else None
if not app_id:
console.error("No valid app_id or app_name provided.")
raise click.exceptions.Exit(1)
history = hosting.get_app_history(app_id=app_id, client=authenticated_client)
if as_json:
console.print(json.dumps(history))
return
if history:
headers = list(history[0].keys())
table = [
[str(value) for value in deployment.values()] for deployment in history
]
console.print_table(table, headers=headers)
else:
console.print(str(history))
except NotAuthenticatedError as err:
console.error("You are not authenticated. Run `reflex login` to authenticate.")
raise click.exceptions.Exit(1) from err
@apps_cli.command("build-logs")
@click.argument("deployment_id", required=True)
@click.option("--token", help="The authentication token.")
@click.option(
"--interactive/--no-interactive",
"-i",
is_flag=True,
default=True,
help="Whether to use interactive mode.",
)
def deployment_build_logs(
deployment_id: str,
token: str | None,
interactive: bool,
):
"""Retrieve the build logs for a specific deployment."""
from reflex_cli.utils import hosting
try:
authenticated_client = hosting.get_authenticated_client(
token=token, interactive=interactive
)
logs = hosting.get_deployment_build_logs(
deployment_id=deployment_id, client=authenticated_client
)
console.print(logs)
except NotAuthenticatedError as err:
console.error("You are not authenticated. Run `reflex login` to authenticate.")
raise click.exceptions.Exit(1) from err
@apps_cli.command(name="status")
@click.argument("deployment_id", required=True)
@click.option(
"--watch/--no-watch", is_flag=True, help="Whether to continuously watch the status."
)
@click.option("--token", help="The authentication token.")
@click.option(
"--loglevel",
type=click.Choice([level.value for level in constants.LogLevel]),
default=constants.LogLevel.INFO.value,
help="The log level to use.",
)
@click.option(
"--interactive/--no-interactive",
"-i",
is_flag=True,
default=True,
help="Whether to use interactive mode.",
)
def deployment_status(
deployment_id: str,
watch: bool,
token: str | None,
loglevel: str,
interactive: bool,
):
"""Retrieve the status of a specific deployment."""
from reflex_cli.utils import hosting
console.set_log_level(loglevel)
try:
authenticated_client = hosting.get_authenticated_client(
token=token, interactive=interactive
)
if watch:
status = hosting.watch_deployment_status(
deployment_id=deployment_id, client=authenticated_client
)
if status is False:
raise click.exceptions.Exit(1)
else:
status = hosting.get_deployment_status(
deployment_id=deployment_id, client=authenticated_client
)
console.error(status) if "failed" in status else console.print(status)
except NotAuthenticatedError as err:
console.error("You are not authenticated. Run `reflex login` to authenticate.")
raise click.exceptions.Exit(1) from err
@apps_cli.command(name="stop")
@click.argument("app_id", required=False)
@click.option("--app-name", help="The name of the application.")
@click.option("--token", help="The authentication token.")
@click.option(
"--loglevel",
type=click.Choice([level.value for level in constants.LogLevel]),
default=constants.LogLevel.INFO.value,
help="The log level to use.",
)
@click.option(
"--interactive/--no-interactive",
"-i",
is_flag=True,
default=True,
help="Whether to use interactive mode.",
)
def stop_app(
app_id: str | None,
app_name: str | None,
token: str | None,
loglevel: str,
interactive: bool,
):
"""Stop a running application."""
from reflex_cli.utils import hosting
console.set_log_level(loglevel)
try:
authenticated_client = hosting.get_authenticated_client(
token=token, interactive=interactive
)
if not app_id:
config = hosting.read_config()
if config:
app_id = config.appid
if not isinstance(app_id, (str, type(None))):
console.error(
"app_id must be a string or None. Please check your config file."
)
raise click.exceptions.Exit(1)
if app_name is not None and app_id is None:
app_result = hosting.search_app(
app_name=app_name,
project_id=None,
client=authenticated_client,
interactive=interactive,
)
app_id = app_result.get("id") if app_result else None
if not app_id:
console.error("No valid app_id or app_name provided.")
raise click.exceptions.Exit(1)
result = hosting.stop_app(app_id=app_id, client=authenticated_client)
if result:
console.error(result) if "failed" in result else console.success(result)
except NotAuthenticatedError as err:
console.error("You are not authenticated. Run `reflex login` to authenticate.")
raise click.exceptions.Exit(1) from err
@apps_cli.command(name="start")
@click.argument("app_id", required=False)
@click.option("--app-name", help="The name of the application.")
@click.option("--token", help="The authentication token.")
@click.option(
"--loglevel",
type=click.Choice([level.value for level in constants.LogLevel]),
default=constants.LogLevel.INFO.value,
help="The log level to use.",
)
@click.option(
"--interactive/--no-interactive",
"-i",
is_flag=True,
default=True,
help="Whether to use interactive mode.",
)
def start_app(
app_id: str | None,
app_name: str | None,
token: str | None,
loglevel: str,
interactive: bool,
):
"""Start a stopped application."""
from reflex_cli.utils import hosting
console.set_log_level(loglevel)
try:
authenticated_client = hosting.get_authenticated_client(
token=token, interactive=interactive
)
if not app_id:
config = hosting.read_config()
if config:
app_id = config.appid
if not isinstance(app_id, (str, type(None))):
console.error(
"app_id must be a string or None. Please check your config file."
)
raise click.exceptions.Exit(1)
if app_name is not None and app_id is None:
app_result = hosting.search_app(
app_name=app_name,
project_id=None,
client=authenticated_client,
interactive=interactive,
)
app_id = app_result.get("id") if app_result else None
if not app_id:
console.error("No valid app_id or app_name provided.")
raise click.exceptions.Exit(1)
result = hosting.start_app(app_id=app_id, client=authenticated_client)
if result:
console.error(result) if "failed" in result else console.success(result)
except NotAuthenticatedError as err:
console.error("You are not authenticated. Run `reflex login` to authenticate.")
raise click.exceptions.Exit(1) from err
@apps_cli.command(name="delete")
@click.argument("app_id", required=False)
@click.option("--app-name", help="The name of the application.")
@click.option("--token", help="The authentication token.")
@click.option(
"--loglevel",
type=click.Choice([level.value for level in constants.LogLevel]),
default=constants.LogLevel.INFO.value,
help="The log level to use.",
)
@click.option(
"--interactive/--no-interactive",
"-i",
is_flag=True,
default=True,
help="Whether to use interactive mode.",
)
def delete_app(
app_id: str | None,
app_name: str | None,
token: str | None,
loglevel: str,
interactive: bool,
):
"""Delete an application."""
from reflex_cli.utils import hosting
console.set_log_level(loglevel)
try:
authenticated_client = hosting.get_authenticated_client(
token=token, interactive=interactive
)
if not app_id:
config = hosting.read_config()
if config:
app_id = config.appid
if not isinstance(app_id, (str, type(None))):
console.error(
"app_id must be a string or None. Please check your config file."
)
raise click.exceptions.Exit(1)
app_name_from_search = None
if app_name is not None and app_id is None:
app_result = hosting.search_app(
app_name=app_name,
project_id=None,
client=authenticated_client,
interactive=interactive,
)
if not app_result:
console.warn(f"App '{app_name}' not found.")
raise click.exceptions.Exit(1)
app_id = app_result.get("id") if app_result else None
app_name_from_search = app_result.get("name") if app_result else app_name
if app_name_from_search is None and app_id:
try:
app_result = hosting.get_app(
client=authenticated_client,
app_id=app_id,
)
except GetAppError:
console.warn(f"No application found with ID '{app_id}'")
return
if not app_result:
console.warn(f"App with ID '{app_id}' not found.")
raise click.exceptions.Exit(0)
if not app_id:
console.error("No valid app_id or app_name provided.")
raise click.exceptions.Exit(1)
if interactive:
app_name_display = "Unknown"
if app_name_from_search is not None:
app_name_display = app_name_from_search
elif app_name is not None:
app_name_display = app_name
else:
try:
app_details = hosting.get_app(
app_id=app_id, client=authenticated_client
)
app_name_display = app_details.get("name", "Unknown")
except Exception:
app_name_display = "Unknown"
app_id_display = app_id
if (
console.ask(
f"Are you sure you want to delete app '{app_name_display}' (ID: {app_id_display})?",
choices=["y", "n"],
default="n",
)
!= "y"
):
console.info("Deletion cancelled.")
return
result = hosting.delete_app(app_id=app_id, client=authenticated_client)
if result:
console.warn(result)
except NotAuthenticatedError as err:
console.error("You are not authenticated. Run `reflex login` to authenticate.")
raise click.exceptions.Exit(1) from err
@apps_cli.command(name="logs")
@click.argument("app_id", required=False)
@click.option("--app-name", help="The name of the application.")
@click.option("--token", help="The authentication token.")
@click.option("--offset", type=int, help="The offset in seconds from the current time.")
@click.option("--start", type=int, help="The start time in Unix epoch format.")
@click.option("--end", type=int, help="The end time in Unix epoch format.")
@click.option(
"--loglevel",
type=click.Choice([level.value for level in constants.LogLevel]),
default=constants.LogLevel.INFO.value,
help="The log level to use.",
)
@click.option(
"--interactive/--no-interactive",
"-i",
is_flag=True,
default=True,
help="Whether to use interactive mode.",
)
@click.option("--cursor", type=str, help="The cursor for pagination.")
@click.option("--pretty", type=bool, help="Use pretty printing for logs.")
@click.option(
"--follow", type=bool, default=True, help="Asks to continue to query logs."
)
def app_logs(
app_id: str | None,
app_name: str | None,
token: str | None,
offset: int | None,
start: int | None,
end: int | None,
loglevel: str,
interactive: bool,
cursor: str | None = None,
pretty: bool = False,
follow: bool = True,
):
"""Retrieve logs for a given application."""
import pprint
from reflex_cli.utils import hosting
console.set_log_level(loglevel)
try:
authenticated_client = hosting.get_authenticated_client(
token=token, interactive=interactive
)
if not app_id:
config = hosting.read_config()
if config:
app_id = config.appid
if not isinstance(app_id, (str, type(None))):
console.error(
"app_id must be a string or None. Please check your config file."
)
raise click.exceptions.Exit(1)
if app_name is not None and app_id is None:
app_result = hosting.search_app(
app_name=app_name,
project_id=None,
client=authenticated_client,
interactive=interactive,
)
app_id = app_result.get("id") if app_result else None
if not app_id:
console.error("No valid app_id or app_name provided.")
raise click.exceptions.Exit(1)
if offset is None and start is None and end is None:
offset = 3600
if not offset and not (start and end):
console.error("must provide both start and end")
raise click.exceptions.Exit(1)
while True:
console.debug(f"fetching logs with cursor: {cursor}")
result = hosting.get_app_logs(
app_id=app_id,
offset=offset,
start=start,
end=end,
client=authenticated_client,
cursor=cursor,
)
if not isinstance(result, list):
console.warn("Unable to retrieve logs.")
return
if len(result) == 2 and isinstance(result[1], str):
cursor = result[1]
result = result[0]
else:
cursor = None
if not result:
console.warn("No logs found for the specified criteria.")
return
result.reverse()
for log in result:
if pretty:
log = pprint.pformat(log, indent=2)
console.info(log)
if not (interactive and follow):
return
from rich.prompt import Prompt
prompt = Prompt.ask(
"Press Enter to fetch next 100 logs or type 'exit' to quit",
default="",
show_default=False,
)
if prompt.lower() == "exit":
console.info("Exiting log retrieval.")
return
except ResponseError as err:
console.error(f"Error retrieving logs: {err}")
raise click.exceptions.Exit(1) from err
except NotAuthenticatedError as err:
console.error("You are not authenticated. Run `reflex login` to authenticate.")
raise click.exceptions.Exit(1) from err
@apps_cli.command(name="list")
@click.option("--project", "project_id", help="The project ID to filter deployments.")
@click.option("--project-name", help="The name of the project.")
@click.option("--token", help="The authentication token.")
@click.option(
"--loglevel",
type=click.Choice([level.value for level in constants.LogLevel]),
default=constants.LogLevel.INFO.value,
help="The log level to use.",
)
@click.option(
"--json/--no-json",
"-j",
"as_json",
is_flag=True,
help="Whether to output the result in JSON format.",
)
@click.option(
"--interactive/--no-interactive",
is_flag=True,
default=True,
help="Whether to list configuration options and ask for confirmation.",
)
def list_apps(
project_id: str | None,
project_name: str | None,
token: str | None,
loglevel: str,
as_json: bool,
interactive: bool,
):
"""List all the hosted deployments of the authenticated user. Will exit if unable to list deployments."""
from reflex_cli.utils import hosting
console.set_log_level(loglevel)
try:
authenticated_client = hosting.get_authenticated_client(
token=token, interactive=interactive
)
if project_name and not project_id:
result = hosting.search_project(
project_name, client=authenticated_client, interactive=interactive
)
project_id = result.get("id") if result else None
if project_id is None:
project_id = hosting.get_selected_project()
if project_id is not None and not as_json:
try:
project = hosting.get_project(project_id, client=authenticated_client)
console.info(
f"Listing apps for project '{project['name']}' ({project_id})"
)
except Exception:
pass
deployments = hosting.list_apps(project=project_id, client=authenticated_client)
except NotAuthenticatedError as err:
console.error("You are not authenticated. Run `reflex login` to authenticate.")
raise click.exceptions.Exit(1) from err
except Exception as ex:
console.error("Unable to list deployments")
raise click.exceptions.Exit(1) from ex
if as_json:
console.print(json.dumps(deployments))
return
if deployments:
headers = list(deployments[0].keys())
table = [
[str(value) for value in deployment.values()] for deployment in deployments
]
console.print_table(table, headers=headers)
else:
console.print(str(deployments))
@apps_cli.command(name="scale")
@click.argument("app_id", required=False)
@click.option("--app-name", help="The name of the app.")
@click.option("--vmtype", help="The virtual machine type to scale to.")
@click.option("--regions", "-r", multiple=True, help="Region to scale the app to.")
@click.option("--token", help="The authentication token.")
@click.option(
"--loglevel",
type=click.Choice([level.value for level in constants.LogLevel]),
default=constants.LogLevel.INFO.value,
help="The log level to use.",
)
@click.option("--scale-type", help="The type of scaling.")
@click.option(
"--interactive/--no-interactive",
"-i",
is_flag=True,
default=True,
help="Whether to use interactive mode.",
)
def scale_app(
app_id: str | None,
app_name: str | None,
vmtype: str | None,
regions: tuple[str, ...],
token: str | None,
loglevel: str,
scale_type: str | None,
interactive: bool,
):
"""Scale an application by changing the VM type or adding/removing regions."""
from reflex_cli.utils import hosting
console.set_log_level(loglevel)
try:
authenticated_client = hosting.get_authenticated_client(
token=token, interactive=interactive
)
if not app_id:
config = hosting.read_config()
if config:
app_id = config.appid
if not isinstance(app_id, (str, type(None))):
console.error(
"app_id must be a string or None. Please check your config file."
)
raise click.exceptions.Exit(1)
cli_args = hosting.ScaleAppCliArgs.create(
regions=list(regions), vm_type=vmtype, scale_type=scale_type
)
config = Config.from_yaml_or_toml_or_default().with_overrides(
vmtype=cli_args.vm_type,
regions=cli_args.regions,
)
if not config.exists() and not cli_args.is_valid:
console.error(
"specify either --vmtype or --regions or add them to the cloud.yml or pyproject.toml file"
)
raise click.exceptions.Exit(1)
if config.exists() and cli_args.is_valid:
console.warn(
"CLI arguments will override the values in the cloud.yml or pyproject.toml file."
)
scale_params = hosting.ScaleParams.from_config(config).set_type_from_cli_args(
cli_args
)
# If app_name is provided, find the app_id
if app_name is not None and app_id is None:
app_result = hosting.search_app(
app_name=app_name,
project_id=None,
client=authenticated_client,
interactive=interactive,
)
app_id = app_result.get("id") if app_result else None
if not app_id:
console.error("No valid app_id or app_name provided.")
raise click.exceptions.Exit(1)
hosting.scale_app(
app_id=app_id, scale_params=scale_params, client=authenticated_client
)
console.success("Successfully scaled the app.")
except NotAuthenticatedError as err:
console.error("You are not authenticated. Run `reflex login` to authenticate.")
raise click.exceptions.Exit(1) from err
except (
ScaleAppError,
ResponseError,
ConfigInvalidFieldValueError,
ScaleTypeError,
ScaleParamError,
) as err:
console.error(err.args[0])
raise click.exceptions.Exit(1) from err
@apps_cli.command(name="inspect")
@click.argument("app_id", required=False)
@click.option("--token", help="The authentication token.")
@click.option(
"--loglevel",
type=click.Choice([level.value for level in constants.LogLevel]),
default=constants.LogLevel.INFO.value,
help="The log level to use.",
)
@click.option(
"--json/--no-json",
"-j",
"as_json",
is_flag=True,
help="Whether to output the result in JSON format.",
)
@click.option(
"--interactive/--no-interactive",
"-i",
is_flag=True,
default=True,
help="Whether to use interactive mode.",
)
def inspect_app(
app_id: str | None,
token: str | None,
loglevel: str,
as_json: bool,
interactive: bool,
):
"""Retrieve detailed information about a specific application."""
from reflex_cli.utils import hosting
console.set_log_level(loglevel)
try:
authenticated_client = hosting.get_authenticated_client(
token=token, interactive=interactive
)
if not app_id:
config = hosting.read_config()
if config:
app_id = config.appid
if not isinstance(app_id, (str, type(None))):
console.error(
"app_id must be a string or None. Please check your config file."
)
raise click.exceptions.Exit(1)
if not app_id:
console.error(
"No valid app_id provided or found in cloud.yml or pyproject.toml."
)
raise click.exceptions.Exit(1)
app_info = hosting.get_app(app_id=app_id, client=authenticated_client)
if as_json:
console.print(json.dumps(app_info))
return
if app_info:
if isinstance(app_info, dict):
headers = list(app_info.keys())
values = [[str(value) for value in app_info.values()]]
console.print_table(values, headers=headers)
else:
console.print(str(app_info))
else:
console.print("No app information found.")
except NotAuthenticatedError as err:
console.error("You are not authenticated. Run `reflex login` to authenticate.")
raise click.exceptions.Exit(1) from err