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

479 lines
17 KiB
Python

"""CLI for the hosting service."""
from __future__ import annotations
import dataclasses
import json
import os
import shutil
import tempfile
from collections.abc import Callable
from pathlib import Path
from typing import Any
import click
from packaging import version
from reflex_cli import constants
from reflex_cli.utils import console
from reflex_cli.utils.dependency import extract_domain
def login(
loglevel: constants.LogLevel = constants.LogLevel.INFO,
) -> dict[str, Any]:
"""Authenticate with Reflex hosting service.
Args:
loglevel: The log level to use.
Returns:
Information about the logged in user.
Raises:
SystemExit: If the command fails.
"""
from reflex_cli.utils import hosting
# Set the log level.
console.set_log_level(loglevel)
access_token, validated_info = hosting.authenticated_token()
if access_token:
console.print("You already logged in.")
return validated_info
# If not already logged in, open a browser window/tab to the login page.
access_token, validated_info = hosting.authenticate_on_browser()
if not access_token:
console.error("Unable to authenticate. Please try again or contact support.")
raise SystemExit(1)
console.print("Successfully logged in.")
return validated_info
def logout(
loglevel: constants.LogLevel = constants.LogLevel.INFO,
):
"""Log out of access to Reflex hosting service.
Args:
loglevel: The log level to use.
"""
from reflex_cli.utils import hosting
console.set_log_level(loglevel)
console.debug("Deleting access token from config locally")
hosting.delete_token_from_config()
console.success("Successfully logged out.")
def deploy(
export_fn: Callable[[str, str, str, bool, bool, bool, bool], None]
| Callable[[str, str, str, bool, bool, bool], None],
app_name: str | None = None,
description: str | None = None,
regions: list[str] | None = None,
project: str | None = None,
envs: list[str] | None = None,
vmtype: str | None = None,
hostname: str | None = None,
interactive: bool = True,
envfile: str | None = None,
loglevel: constants.LogLevel = constants.LogLevel.INFO,
token: str | None = None,
config_path: str | None = None,
env: str | None = None,
project_name: str | None = None,
app_id: str | None = None,
**kwargs,
):
"""Deploy the app to the Reflex hosting service.
Args:
app_name: The name of the app.
export_fn: The function from the Reflex main framework to export the app.
description: The app's description.
regions: The regions to deploy to.
project: The project to deploy to.
envs: The environment variables to set.
vmtype: The VM type to allocate.
hostname: The hostname to use for the frontend.
interactive: Whether to use interactive mode.
envfile: The path to an env file to use. Will override any envs set manually.
loglevel: The log level to use.
token: The authentication token.
config_path: The path to the config file.
env: The environment to use for deployment.
project_name: The name of the project.
app_id: The ID of the app.
**kwargs: Additional keyword arguments.
Raises:
Exit: If the command fails.
"""
import httpx
from reflex_cli.utils import hosting
authenticated_client = hosting.get_authenticated_client(
token=token, interactive=interactive
)
# Set the log level.
console.set_log_level(loglevel)
project_id = project
config = {}
config_from_file = hosting.read_config(config_path, env=env)
if config_from_file:
config = dataclasses.asdict(config_from_file)
packages = None
strategy = None
include_db = False
# If a config file is provided, use values from the file that are not provided as arguments.
if config:
if not regions:
regions = config.get("regions", None)
if not vmtype:
vmtype = config.get("vmtype", None)
if not hostname:
hostname = config.get("hostname", None)
if not envfile:
envfile = config.get("envfile", None)
if not project_id:
project_id = config.get("project", None)
if not project_name:
project_name = config.get("projectname", None)
if not app_id:
app_id = config.get("appid", None)
if not isinstance(app_id, (str, type(None))):
console.error(
"app_id must be a string or None. Please check your config file."
)
raise SystemExit(1)
if not packages:
packages = config.get("packages", None)
if not include_db:
include_db = config.get("include_db", False)
if not strategy:
strategy = config.get("strategy", None)
app_name = config.get("name", app_name)
if not isinstance(app_name, (str, type(None))):
console.error(
"app_name must be a string or None. Please check your config file."
)
raise SystemExit(1)
if app_name == "default":
# not sure if this is the best check?
console.error(
"Please set real config values in cloud.yml or pyproject.toml"
)
raise SystemExit(1)
if not description:
description = config.get("description", None)
# resolve the project id from the project name.
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
try:
# check if provided project exists.
if project_id:
hosting.get_project(project_id, client=authenticated_client)
else:
project_id = hosting.get_selected_project()
except httpx.HTTPStatusError as ex:
try:
console.error(ex.response.json().get("detail"))
except json.JSONDecodeError:
console.error(ex.response.text)
raise click.exceptions.Exit(1) from ex
envs = envs or []
if not app_name and not app_id:
console.error(
"Please provide a valid app name or ID for the deployed instance."
)
raise click.exceptions.Exit(1)
try:
if app_name and not app_id:
search_project_id = project_id
if not interactive and not project and not search_project_id:
search_project_id = hosting.get_selected_project()
elif interactive and not project:
search_project_id = None
app = hosting.search_app(
app_name=app_name,
project_id=search_project_id,
client=authenticated_client,
interactive=interactive,
)
else:
app = hosting.get_app(app_id or "", client=authenticated_client)
app_name = app.get("name")
except click.exceptions.Exit:
raise
except Exception as ex:
console.error(f"Deployment failed: {ex}")
raise click.exceptions.Exit(1) from ex
if app and interactive and not project and not app_id:
default_project_id = hosting.get_selected_project()
app_project_id = app.get("project_id")
if app_project_id and (
not default_project_id or app_project_id != default_project_id
):
app_project = hosting.get_project(
app_project_id, client=authenticated_client
)
app_project_name = app_project.get("name", "Unknown")
if (
console.ask(
f"Deploy to app '{app['name']}' in project '{app_project_name}'?",
choices=["y", "n"],
default="y",
)
!= "y"
):
console.info("Deployment cancelled.")
raise click.exceptions.Exit(0)
project_id = app_project_id
if not app and interactive:
if (
console.ask(
f"No app with {app_name or app_id} found. Do you want to create a new app to deploy?",
choices=["y", "n"],
default="y",
)
== "y"
):
# Check if we need confirmation for deploying to non-default project
if not project:
default_project_id = hosting.get_selected_project()
if not default_project_id:
try:
if project_id:
target_project = hosting.get_project(
project_id, client=authenticated_client
)
project_name = target_project.get("name", "Unknown")
else:
token = hosting.get_existing_access_token()
default_project_id = hosting.get_default_project(
authenticated_client
)
if default_project_id:
default_project = hosting.get_project(
default_project_id, client=authenticated_client
)
project_name = default_project.get(
"name", "Default Project"
)
else:
project_name = "Default Project"
except Exception:
project_name = "Unknown"
if (
console.ask(
f"Create and deploy app '{app_name}' in project '{project_name}'?",
choices=["y", "n"],
default="y",
)
!= "y"
):
console.info("Deployment cancelled.")
raise click.exceptions.Exit(0)
elif project_id and project_id != default_project_id:
try:
target_project = hosting.get_project(
project_id, client=authenticated_client
)
project_name = target_project.get("name", "Unknown")
except Exception:
project_name = "Unknown"
if (
console.ask(
f"Create and deploy app '{app_name}' in project '{project_name}'?",
choices=["y", "n"],
default="y",
)
!= "y"
):
console.info("Deployment cancelled.")
raise click.exceptions.Exit(0)
if description is None:
description = console.ask(
"App Description (Enter to skip)",
)
app = hosting.create_app(
app_name=app_name or "",
description=description,
project_id=project_id,
client=authenticated_client,
)
console.info(f"created app. \nName: {app['name']} \nId: {app['id']}")
else:
console.error("Please create an app to deploy.")
raise click.exceptions.Exit(1)
elif not app:
app = hosting.create_app(
app_name=app_name or "",
description=description or "",
project_id=project_id,
client=authenticated_client,
)
console.info(f"created app. \nName: {app['name']} \nId: {app['id']}")
urls = hosting.get_hostname(
app_id=app["id"],
app_name=app["name"],
hostname=hostname,
client=authenticated_client,
)
if "error" in urls:
console.error(urls["error"])
raise click.exceptions.Exit(1)
server_url = os.getenv("REFLEX_OVERRIDE_BACKEND_URL") or urls["server"] # backend
host_url = os.getenv("REFLEX_OVERRIDE_FRONTEND_URL") or urls["hostname"] # frontend
processed_envs = hosting.process_envs(envs) if envs else None
if not app_name:
console.error("Please set an app name.")
raise click.exceptions.Exit(1)
# at this point, if project_id is None, the App should have the correct project_id and
# we should use that going forward to pass validation checks.
project_id = project_id or app.get("project_id")
validation_message = hosting.validate_deployment_args(
app_name=app_name,
app_id=app.get("id"),
project_id=project_id,
regions=regions,
vmtype=vmtype,
hostname=hostname,
client=authenticated_client,
)
if validation_message != "success":
console.error(validation_message)
raise click.exceptions.Exit(1)
if envfile:
try:
from dotenv import dotenv_values # pyright: ignore[reportMissingImports]
processed_envs = dotenv_values(envfile)
except ImportError:
console.error(
"""The `python-dotenv` package is required to load environment variables from a file. Run `pip install "python-dotenv>=1.0.1"`."""
)
# Compile the app in production mode: backend first then frontend.
temporary_dir = tempfile.TemporaryDirectory()
temporary_dir_path = Path(temporary_dir.name)
import importlib.metadata
rx_version = version.parse(importlib.metadata.version("reflex"))
breaking_version = version.parse("0.7.6")
# Try zipping backend first
try:
# Check if the reflex version is >= 0.7.6
if rx_version <= breaking_version:
export_fn(
str(temporary_dir_path),
server_url,
host_url,
False,
True,
True,
) # pyright: ignore[reportCallIssue]
else:
export_fn(
str(temporary_dir_path),
server_url,
host_url,
False,
True,
include_db,
True, # pyright: ignore[reportCallIssue]
)
except Exception as ex:
console.error(f"Unable to export due to: {ex}")
if temporary_dir_path.exists():
shutil.rmtree(temporary_dir_path)
raise click.exceptions.Exit(1) from ex
# Zip frontend
try:
# Check if the reflex version is >= 0.7.6
if rx_version <= breaking_version:
export_fn(str(temporary_dir_path), server_url, host_url, True, False, True) # pyright: ignore[reportCallIssue]
else:
export_fn(
str(temporary_dir_path),
server_url,
host_url,
True,
False,
include_db,
True, # pyright: ignore[reportCallIssue]
)
except ImportError as ie:
console.error(
f"Encountered ImportError, did you install all the dependencies? {ie}"
)
if temporary_dir_path.exists():
shutil.rmtree(temporary_dir_path)
raise click.exceptions.Exit(1) from ie
except Exception as ex:
console.error(f"Unable to export due to: {ex}")
if temporary_dir_path.exists():
shutil.rmtree(temporary_dir_path)
raise click.exceptions.Exit(1) from ex
result = hosting.create_deployment(
app_id=app.get("id"),
app_name=app_name,
project_id=project_id,
regions=regions,
zip_dir=Path(temporary_dir_path),
hostname=extract_domain(host_url) if hostname else None,
vmtype=vmtype,
secrets=processed_envs,
client=authenticated_client,
packages=packages,
strategy=strategy,
)
if "failed" in result:
console.error(result)
raise click.exceptions.Exit(1)
hosting_ui_url = f"{constants.Hosting.HOSTING_SERVICE_UI}/project/{app['project_id']}/app/{app['id']}/"
console.print(
f"deployment progress can now be viewed on the website: {hosting_ui_url}"
)
console.print(
f"you are now safe to exit this command.\nfollow along with the deployment with the following command: \n reflex cloud apps status {result} --watch"
)
status = hosting.watch_deployment_status(result, client=authenticated_client)
if status is False:
raise click.exceptions.Exit(1)