946 lines
28 KiB
Python
946 lines
28 KiB
Python
"""Reflex CLI to create, run, and deploy apps."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from importlib.util import find_spec
|
|
from pathlib import Path
|
|
from typing import TYPE_CHECKING
|
|
|
|
import click
|
|
from reflex_base import constants
|
|
from reflex_base.config import get_config
|
|
from reflex_base.environment import environment
|
|
from reflex_base.utils import console
|
|
from reflex_cli.v2.deployments import hosting_cli
|
|
|
|
from reflex.custom_components.custom_components import custom_components_cli
|
|
|
|
if TYPE_CHECKING:
|
|
from reflex_base.constants.base import LITERAL_ENV
|
|
from reflex_cli.constants.base import LogLevel as HostingLogLevel
|
|
|
|
|
|
def set_loglevel(ctx: click.Context, self: click.Parameter, value: str | None):
|
|
"""Set the log level.
|
|
|
|
Args:
|
|
ctx: The click context.
|
|
self: The click command.
|
|
value: The log level to set.
|
|
"""
|
|
if value is not None:
|
|
loglevel = constants.LogLevel.from_string(value)
|
|
console.set_log_level(loglevel)
|
|
|
|
|
|
@click.group
|
|
@click.version_option(constants.Reflex.VERSION, message="%(version)s")
|
|
def cli():
|
|
"""Reflex CLI to create, run, and deploy apps."""
|
|
|
|
|
|
loglevel_option = click.option(
|
|
"--loglevel",
|
|
"--log-level",
|
|
"loglevel",
|
|
type=click.Choice(
|
|
[loglevel.value for loglevel in constants.LogLevel],
|
|
case_sensitive=False,
|
|
),
|
|
is_eager=True,
|
|
callback=set_loglevel,
|
|
expose_value=False,
|
|
help="The log level to use.",
|
|
)
|
|
|
|
|
|
def _init(
|
|
name: str,
|
|
template: str | None = None,
|
|
ai: bool = False,
|
|
):
|
|
"""Initialize a new Reflex app in the given directory."""
|
|
from reflex.utils import exec, frontend_skeleton, prerequisites, templates
|
|
|
|
# Show system info
|
|
exec.output_system_info()
|
|
|
|
if ai:
|
|
from reflex.utils.redir import reflex_build_redirect
|
|
|
|
reflex_build_redirect()
|
|
return
|
|
|
|
# Validate the app name.
|
|
app_name = prerequisites.validate_app_name(name)
|
|
console.rule(f"[bold]Initializing {app_name}")
|
|
|
|
# Check prerequisites.
|
|
prerequisites.check_latest_package_version(constants.Reflex.MODULE_NAME)
|
|
prerequisites.initialize_reflex_user_directory()
|
|
prerequisites.ensure_reflex_installation_id()
|
|
|
|
# Set up the web project.
|
|
prerequisites.initialize_frontend_dependencies()
|
|
|
|
# Initialize the app.
|
|
template = templates.initialize_app(app_name, template)
|
|
|
|
# Initialize the .gitignore.
|
|
frontend_skeleton.initialize_gitignore()
|
|
|
|
template_msg = f" using the {template} template" if template else ""
|
|
if Path(constants.PyprojectToml.FILE).exists():
|
|
needs_user_manual_update = False
|
|
next_steps = " Run `uv run reflex run` to start the app."
|
|
else:
|
|
needs_user_manual_update = frontend_skeleton.initialize_requirements_txt()
|
|
next_steps = " Install dependencies from `requirements.txt` with `uv pip install -r requirements.txt` (or your preferred installer) before running `uv run reflex run`."
|
|
manual_update = (
|
|
f" Make sure to add `{constants.RequirementsTxt.DEFAULTS_STUB + constants.Reflex.VERSION}` to your requirements.txt file."
|
|
if needs_user_manual_update
|
|
else ""
|
|
)
|
|
|
|
# Finish initializing the app.
|
|
console.success(f"Initialized {app_name}{template_msg}.{manual_update}{next_steps}")
|
|
|
|
|
|
@cli.command()
|
|
@loglevel_option
|
|
@click.option(
|
|
"--name",
|
|
metavar="APP_NAME",
|
|
help="The name of the app to initialize.",
|
|
)
|
|
@click.option(
|
|
"--template",
|
|
help="The template to initialize the app with.",
|
|
)
|
|
@click.option(
|
|
"--ai",
|
|
is_flag=True,
|
|
help="Use AI to create the initial template. Cannot be used with existing app or `--template` option.",
|
|
)
|
|
def init(
|
|
name: str,
|
|
template: str | None,
|
|
ai: bool,
|
|
):
|
|
"""Initialize a new Reflex app in the current directory."""
|
|
_init(name, template, ai)
|
|
|
|
|
|
def _compile_app(*, avoid_dirty_check: bool = True):
|
|
from reflex.utils import exec, prerequisites
|
|
|
|
app_task = prerequisites.compile_or_validate_app
|
|
args = (True,)
|
|
kwargs = {
|
|
"check_if_schema_up_to_date": True,
|
|
"prerender_routes": exec.should_prerender_routes(),
|
|
}
|
|
|
|
# Granian fails if the app is already imported.
|
|
if exec.should_use_granian() and avoid_dirty_check:
|
|
import concurrent.futures
|
|
|
|
with concurrent.futures.ProcessPoolExecutor(max_workers=1) as executor:
|
|
compile_future = executor.submit(app_task, *args, **kwargs)
|
|
return_result = compile_future.result()
|
|
else:
|
|
return_result = app_task(*args, **kwargs)
|
|
|
|
if not return_result:
|
|
raise SystemExit(1)
|
|
|
|
|
|
def _run_dev(
|
|
running_mode: constants.RunningMode,
|
|
frontend_port: int | None,
|
|
backend_port: int | None,
|
|
backend_host: str,
|
|
):
|
|
"""Run the app in development mode."""
|
|
import atexit
|
|
|
|
from reflex.utils import build, exec, processes, telemetry
|
|
|
|
config = get_config()
|
|
|
|
if frontend_port:
|
|
config._set_persistent(frontend_port=frontend_port)
|
|
if backend_port:
|
|
config._set_persistent(backend_port=backend_port)
|
|
|
|
if running_mode.has_frontend():
|
|
_compile_app()
|
|
|
|
# Post a telemetry event.
|
|
telemetry.send("run-dev")
|
|
|
|
# Display custom message when there is a keyboard interrupt.
|
|
atexit.register(processes.atexit_handler)
|
|
|
|
# Run the frontend and backend together.
|
|
commands = []
|
|
|
|
# Run the frontend on a separate thread.
|
|
if running_mode.has_frontend():
|
|
build.setup_frontend(Path.cwd())
|
|
commands.append((
|
|
exec.run_frontend,
|
|
Path.cwd(),
|
|
frontend_port,
|
|
running_mode.has_backend(),
|
|
))
|
|
|
|
# Start the frontend and backend.
|
|
with processes.run_concurrently_context(*commands):
|
|
# In dev mode, run the backend on the main thread.
|
|
if running_mode.has_backend() and backend_port:
|
|
exec.run_backend(
|
|
backend_host,
|
|
int(backend_port),
|
|
config.loglevel.subprocess_level(),
|
|
running_mode.has_frontend(),
|
|
)
|
|
# The windows uvicorn bug workaround
|
|
# https://github.com/reflex-dev/reflex/issues/2335
|
|
if constants.IS_WINDOWS and exec.frontend_process:
|
|
# Sends SIGTERM in windows
|
|
exec.kill(exec.frontend_process.pid)
|
|
|
|
|
|
def _run_prod(running_mode: constants.RunningMode, port: int, host: str):
|
|
import atexit
|
|
|
|
from reflex.utils import build, exec, processes, telemetry
|
|
|
|
config = get_config()
|
|
|
|
config._set_persistent(frontend_port=port, backend_port=port)
|
|
|
|
if running_mode.has_frontend():
|
|
# Get the app module.
|
|
_compile_app(avoid_dirty_check=False)
|
|
build.setup_frontend_prod(Path.cwd())
|
|
|
|
_skip_compile()
|
|
|
|
# Post a telemetry event.
|
|
telemetry.send("run-prod")
|
|
|
|
# Display custom message when there is a keyboard interrupt.
|
|
atexit.register(processes.atexit_handler)
|
|
|
|
exec.notify_app_running()
|
|
exec.notify_frontend(
|
|
f"http://{host}:{port}",
|
|
backend_present=running_mode.has_backend(),
|
|
)
|
|
if running_mode.has_backend():
|
|
exec.run_backend_prod(
|
|
host, port, config.loglevel.subprocess_level(), running_mode.has_frontend()
|
|
)
|
|
else:
|
|
exec.run_frontend_prod(host, port)
|
|
|
|
|
|
def _run(
|
|
*,
|
|
env: constants.Env = constants.Env.DEV,
|
|
running_mode: constants.RunningMode = constants.RunningMode.FULLSTACK,
|
|
frontend_port: int | None = None,
|
|
backend_port: int | None = None,
|
|
backend_host: str | None = None,
|
|
):
|
|
"""Run the app in the given directory."""
|
|
from reflex.istate.manager import reset_disk_state_manager
|
|
from reflex.utils import exec, prerequisites, processes
|
|
|
|
if frontend_port and not running_mode.has_frontend():
|
|
console.error("Cannot specify --frontend-port when not running frontend.")
|
|
raise SystemExit(1)
|
|
if backend_port and not running_mode.has_backend():
|
|
console.error("Cannot specify --backend-port when not running backend.")
|
|
raise SystemExit(1)
|
|
if (
|
|
env == constants.Env.PROD
|
|
and frontend_port
|
|
and backend_port
|
|
and frontend_port != backend_port
|
|
):
|
|
console.error("In production, frontend and backend must run on the same port.")
|
|
raise SystemExit(1)
|
|
|
|
config = get_config()
|
|
|
|
backend_host = backend_host or config.backend_host
|
|
|
|
# Set env mode in the environment
|
|
environment.REFLEX_ENV_MODE.set(env)
|
|
|
|
# Show system info
|
|
exec.output_system_info()
|
|
|
|
if running_mode == constants.RunningMode.BACKEND_ONLY:
|
|
_skip_compile()
|
|
|
|
prerequisites.assert_in_reflex_dir()
|
|
|
|
# Check that the app is initialized.
|
|
if running_mode.has_frontend() and prerequisites.needs_reinit():
|
|
_init(name=config.app_name)
|
|
|
|
# Delete the states folder if it exists.
|
|
reset_disk_state_manager()
|
|
|
|
# Apply the new ports and host to the config.
|
|
if frontend_port != config.frontend_port:
|
|
config._set_persistent(frontend_port=frontend_port)
|
|
if backend_port != config.backend_port:
|
|
config._set_persistent(backend_port=backend_port)
|
|
if backend_host != config.backend_host:
|
|
config._set_persistent(backend_host=backend_host)
|
|
|
|
# Reload the config to make sure the env vars are persistent.
|
|
get_config(reload=True)
|
|
|
|
console.rule("[bold]Starting Reflex App")
|
|
|
|
prerequisites.check_latest_package_version(constants.Reflex.MODULE_NAME)
|
|
|
|
if env == constants.Env.DEV:
|
|
# Find the next available open port if applicable.
|
|
if running_mode.has_frontend():
|
|
auto_increment_frontend = not bool(frontend_port or config.frontend_port)
|
|
frontend_port = processes.handle_port(
|
|
"frontend",
|
|
(
|
|
frontend_port
|
|
or config.frontend_port
|
|
or constants.DefaultPorts.FRONTEND_PORT
|
|
),
|
|
auto_increment=auto_increment_frontend,
|
|
)
|
|
|
|
if running_mode.has_backend():
|
|
auto_increment_backend = not bool(backend_port or config.backend_port)
|
|
|
|
backend_port = processes.handle_port(
|
|
"backend",
|
|
(
|
|
backend_port
|
|
or config.backend_port
|
|
or constants.DefaultPorts.BACKEND_PORT
|
|
),
|
|
auto_increment=auto_increment_backend,
|
|
)
|
|
|
|
_run_dev(running_mode, frontend_port, backend_port, backend_host)
|
|
else:
|
|
if running_mode == constants.RunningMode.BACKEND_ONLY:
|
|
requested_port = backend_port or config.backend_port
|
|
fallback_port = constants.DefaultPorts.BACKEND_PORT
|
|
elif running_mode == constants.RunningMode.FRONTEND_ONLY:
|
|
requested_port = frontend_port or config.frontend_port
|
|
fallback_port = constants.DefaultPorts.FRONTEND_PORT
|
|
else:
|
|
requested_port = (
|
|
frontend_port
|
|
or backend_port
|
|
or config.frontend_port
|
|
or config.backend_port
|
|
)
|
|
fallback_port = constants.DefaultPorts.FRONTEND_PORT
|
|
|
|
port = processes.handle_port(
|
|
service_name=running_mode.name.lower(),
|
|
port=requested_port or fallback_port,
|
|
auto_increment=requested_port is None,
|
|
)
|
|
|
|
_run_prod(running_mode, port, backend_host)
|
|
|
|
|
|
@cli.command()
|
|
@loglevel_option
|
|
@click.option(
|
|
"--env",
|
|
type=click.Choice([e.value for e in constants.Env], case_sensitive=False),
|
|
default=constants.Env.DEV.value,
|
|
help="The environment to run the app in.",
|
|
)
|
|
@click.option(
|
|
"--frontend-only",
|
|
is_flag=True,
|
|
show_default=False,
|
|
help="Execute only frontend.",
|
|
envvar=environment.REFLEX_FRONTEND_ONLY.name,
|
|
)
|
|
@click.option(
|
|
"--backend-only",
|
|
is_flag=True,
|
|
show_default=False,
|
|
help="Execute only backend.",
|
|
envvar=environment.REFLEX_BACKEND_ONLY.name,
|
|
)
|
|
@click.option(
|
|
"--frontend-port",
|
|
type=int,
|
|
help="Specify a different frontend port.",
|
|
envvar=environment.REFLEX_FRONTEND_PORT.name,
|
|
)
|
|
@click.option(
|
|
"--backend-port",
|
|
type=int,
|
|
help="Specify a different backend port.",
|
|
envvar=environment.REFLEX_BACKEND_PORT.name,
|
|
)
|
|
@click.option(
|
|
"--backend-host",
|
|
help="Specify the backend host.",
|
|
)
|
|
@click.option(
|
|
"--single-port",
|
|
is_flag=True,
|
|
help="Run both frontend and backend on the same port.",
|
|
default=False,
|
|
)
|
|
def run(
|
|
env: LITERAL_ENV,
|
|
frontend_only: bool,
|
|
backend_only: bool,
|
|
frontend_port: int | None,
|
|
backend_port: int | None,
|
|
backend_host: str | None,
|
|
single_port: bool,
|
|
):
|
|
"""Run the app in the current directory."""
|
|
from reflex.utils import prerequisites
|
|
|
|
if frontend_only and backend_only:
|
|
console.error("Cannot use both --frontend-only and --backend-only options.")
|
|
raise SystemExit(1)
|
|
|
|
if single_port:
|
|
if env != constants.Env.PROD:
|
|
console.error("--single-port can only be used with --env=PROD.")
|
|
raise SystemExit(1)
|
|
if frontend_only or backend_only:
|
|
console.error(
|
|
"Cannot use --single-port with --frontend-only or --backend-only."
|
|
)
|
|
raise SystemExit(1)
|
|
if frontend_port and backend_port and frontend_port != backend_port:
|
|
console.error(
|
|
"Cannot specify different ports for frontend and backend when using --single-port."
|
|
)
|
|
raise SystemExit(1)
|
|
|
|
config = get_config()
|
|
|
|
frontend_port = frontend_port or config.frontend_port
|
|
backend_port = backend_port or config.backend_port
|
|
backend_host = backend_host or config.backend_host
|
|
|
|
environment.REFLEX_COMPILE_CONTEXT.set(constants.CompileContext.RUN)
|
|
environment.REFLEX_BACKEND_ONLY.set(backend_only)
|
|
environment.REFLEX_FRONTEND_ONLY.set(frontend_only)
|
|
|
|
running_mode = prerequisites.check_running_mode(frontend_only, backend_only)
|
|
|
|
_run(
|
|
env=constants.Env.DEV if env == constants.Env.DEV else constants.Env.PROD,
|
|
running_mode=running_mode,
|
|
frontend_port=frontend_port,
|
|
backend_port=backend_port,
|
|
backend_host=backend_host,
|
|
)
|
|
|
|
|
|
@cli.command()
|
|
@loglevel_option
|
|
@click.option(
|
|
"--dry",
|
|
is_flag=True,
|
|
default=False,
|
|
help="Run the command without making any changes.",
|
|
)
|
|
@click.option(
|
|
"--rich/--no-rich",
|
|
default=True,
|
|
is_flag=True,
|
|
help="Whether to use rich progress bars.",
|
|
)
|
|
def compile(dry: bool, rich: bool):
|
|
"""Compile the app in the current directory."""
|
|
import time
|
|
|
|
from reflex.utils import prerequisites
|
|
|
|
# Check the app.
|
|
if prerequisites.needs_reinit():
|
|
_init(name=get_config().app_name)
|
|
get_config(reload=True)
|
|
starting_time = time.monotonic()
|
|
prerequisites.get_compiled_app(dry_run=dry, use_rich=rich)
|
|
elapsed_time = time.monotonic() - starting_time
|
|
console.success(f"App compiled successfully in {elapsed_time:.3f} seconds.")
|
|
|
|
|
|
@cli.command()
|
|
@loglevel_option
|
|
@click.option(
|
|
"--zip/--no-zip",
|
|
default=True,
|
|
is_flag=True,
|
|
help="Whether to zip the backend and frontend exports.",
|
|
)
|
|
@click.option(
|
|
"--frontend-only",
|
|
is_flag=True,
|
|
show_default=False,
|
|
envvar=environment.REFLEX_FRONTEND_ONLY.name,
|
|
help="Export only frontend.",
|
|
)
|
|
@click.option(
|
|
"--backend-only",
|
|
is_flag=True,
|
|
show_default=False,
|
|
envvar=environment.REFLEX_BACKEND_ONLY.name,
|
|
help="Export only backend.",
|
|
)
|
|
@click.option(
|
|
"--zip-dest-dir",
|
|
default=str(Path.cwd()),
|
|
help="The directory to export the zip files to.",
|
|
show_default=False,
|
|
)
|
|
@click.option(
|
|
"--upload-db-file",
|
|
is_flag=True,
|
|
help="Whether to exclude sqlite db files when exporting backend.",
|
|
hidden=True,
|
|
)
|
|
@click.option(
|
|
"--env",
|
|
type=click.Choice([e.value for e in constants.Env], case_sensitive=False),
|
|
default=constants.Env.PROD.value,
|
|
help="The environment to export the app in.",
|
|
)
|
|
@click.option(
|
|
"--exclude-from-backend",
|
|
"backend_excluded_dirs",
|
|
multiple=True,
|
|
type=click.Path(exists=True, path_type=Path, resolve_path=True),
|
|
help="Files or directories to exclude from the backend zip. Can be used multiple times.",
|
|
)
|
|
@click.option(
|
|
"--server-side-rendering/--no-server-side-rendering",
|
|
"--ssr/--no-ssr",
|
|
"ssr",
|
|
default=True,
|
|
is_flag=True,
|
|
help="Whether to enable server side rendering for the frontend.",
|
|
)
|
|
def export(
|
|
zip: bool,
|
|
frontend_only: bool,
|
|
backend_only: bool,
|
|
zip_dest_dir: str,
|
|
upload_db_file: bool,
|
|
env: LITERAL_ENV,
|
|
backend_excluded_dirs: tuple[Path, ...] = (),
|
|
ssr: bool = True,
|
|
):
|
|
"""Export the app to a zip file."""
|
|
from reflex.utils import export as export_utils
|
|
from reflex.utils import prerequisites
|
|
|
|
if not environment.REFLEX_SSR.is_set():
|
|
environment.REFLEX_SSR.set(ssr)
|
|
elif environment.REFLEX_SSR.get() != ssr:
|
|
ssr = environment.REFLEX_SSR.get()
|
|
|
|
environment.REFLEX_COMPILE_CONTEXT.set(constants.CompileContext.EXPORT)
|
|
|
|
running_mode = prerequisites.check_running_mode(frontend_only, backend_only)
|
|
|
|
config = get_config()
|
|
|
|
prerequisites.assert_in_reflex_dir()
|
|
|
|
if running_mode.has_frontend() and prerequisites.needs_reinit():
|
|
_init(name=config.app_name)
|
|
|
|
export_utils.export(
|
|
zipping=zip,
|
|
frontend=running_mode.has_frontend(),
|
|
backend=running_mode.has_backend(),
|
|
zip_dest_dir=zip_dest_dir,
|
|
upload_db_file=upload_db_file,
|
|
env=constants.Env.DEV if env == constants.Env.DEV else constants.Env.PROD,
|
|
loglevel=config.loglevel.subprocess_level(),
|
|
backend_excluded_dirs=backend_excluded_dirs,
|
|
prerender_routes=ssr,
|
|
)
|
|
|
|
|
|
@cli.command()
|
|
@loglevel_option
|
|
def login():
|
|
"""Authenticate with experimental Reflex hosting service."""
|
|
from reflex_cli.v2 import cli as hosting_cli
|
|
from reflex_cli.v2.deployments import check_version
|
|
|
|
check_version()
|
|
|
|
validated_info = hosting_cli.login()
|
|
if validated_info is not None:
|
|
_skip_compile() # Allow running outside of an app dir
|
|
from reflex.utils import telemetry
|
|
|
|
telemetry.send("login", user_uuid=validated_info.get("user_id"))
|
|
|
|
|
|
@cli.command()
|
|
@loglevel_option
|
|
def logout():
|
|
"""Log out of access to Reflex hosting service."""
|
|
from reflex_cli.v2.cli import logout
|
|
from reflex_cli.v2.deployments import check_version
|
|
|
|
check_version()
|
|
|
|
logout(_convert_reflex_loglevel_to_reflex_cli_loglevel(get_config().loglevel))
|
|
|
|
|
|
@click.group
|
|
def db_cli():
|
|
"""Subcommands for managing the database schema."""
|
|
|
|
|
|
@click.group
|
|
def script_cli():
|
|
"""Subcommands for running helper scripts."""
|
|
|
|
|
|
def _skip_compile():
|
|
"""Skip the compile step."""
|
|
environment.REFLEX_SKIP_COMPILE.set(True)
|
|
|
|
|
|
@db_cli.command(name="init")
|
|
def db_init():
|
|
"""Create database schema and migration configuration."""
|
|
from reflex import model
|
|
from reflex.utils import prerequisites
|
|
|
|
config = get_config()
|
|
|
|
# Check the database url.
|
|
if config.db_url is None:
|
|
console.error("db_url is not configured, cannot initialize.")
|
|
return
|
|
|
|
# Check the alembic config.
|
|
if environment.ALEMBIC_CONFIG.get().exists():
|
|
console.error(
|
|
"Database is already initialized. Use "
|
|
"[bold]reflex db makemigrations[/bold] to create schema change "
|
|
"scripts and [bold]reflex db migrate[/bold] to apply migrations "
|
|
"to a new or existing database.",
|
|
)
|
|
return
|
|
|
|
# Initialize the database.
|
|
_skip_compile()
|
|
prerequisites.get_compiled_app()
|
|
model.alembic_init()
|
|
model.migrate(autogenerate=True)
|
|
|
|
|
|
@db_cli.command()
|
|
def migrate():
|
|
"""Create or update database schema from migration scripts."""
|
|
from reflex import model
|
|
from reflex.utils import prerequisites
|
|
|
|
prerequisites.get_app()
|
|
if not prerequisites.check_db_initialized():
|
|
return
|
|
model.migrate()
|
|
prerequisites.check_schema_up_to_date()
|
|
|
|
|
|
@db_cli.command()
|
|
def status():
|
|
"""Check the status of the database schema."""
|
|
from reflex.model import format_revision, get_migration_history
|
|
from reflex.utils import prerequisites
|
|
|
|
prerequisites.get_app()
|
|
if not prerequisites.check_db_initialized():
|
|
console.info(
|
|
"Database is not initialized. Run [bold]reflex db init[/bold] to initialize."
|
|
)
|
|
return
|
|
|
|
# Run alembic check command and display output
|
|
import reflex_base.config
|
|
|
|
config = reflex_base.config.get_config()
|
|
console.print(f"[bold]\\[{config.db_url}][/bold]")
|
|
|
|
# Get migration history using Model method
|
|
current_rev, revisions = get_migration_history()
|
|
if current_rev is None and not revisions:
|
|
return
|
|
|
|
current_reached_ref = [current_rev is None]
|
|
|
|
# Show migration history in chronological order
|
|
console.print("<base>")
|
|
for rev in revisions:
|
|
# Format and print the revision
|
|
console.print(format_revision(rev, current_rev, current_reached_ref))
|
|
|
|
|
|
@db_cli.command()
|
|
@click.option(
|
|
"--message",
|
|
help="Human readable identifier for the generated revision.",
|
|
)
|
|
def makemigrations(message: str | None):
|
|
"""Create autogenerated alembic migration scripts."""
|
|
from alembic.util.exc import CommandError
|
|
|
|
from reflex import model
|
|
from reflex.utils import prerequisites
|
|
|
|
# TODO see if we can use `get_app()` instead (no compile). Would _skip_compile still be needed then?
|
|
_skip_compile()
|
|
prerequisites.get_compiled_app()
|
|
if not prerequisites.check_db_initialized():
|
|
return
|
|
with model.get_engine().connect() as connection:
|
|
try:
|
|
model.alembic_autogenerate(connection=connection, message=message)
|
|
except CommandError as command_error:
|
|
if "Target database is not up to date." not in str(command_error):
|
|
raise
|
|
console.error(
|
|
f"{command_error} Run [bold]reflex db migrate[/bold] to update database."
|
|
)
|
|
|
|
|
|
@cli.command()
|
|
@loglevel_option
|
|
@click.option(
|
|
"--app-name",
|
|
help="The name of the app to deploy.",
|
|
)
|
|
@click.option(
|
|
"--app-id",
|
|
help="The ID of the app to deploy.",
|
|
)
|
|
@click.option(
|
|
"-r",
|
|
"--region",
|
|
multiple=True,
|
|
help="The regions to deploy to. `reflex cloud regions` For multiple envs, repeat this option, e.g. --region sjc --region iad",
|
|
)
|
|
@click.option(
|
|
"--env",
|
|
multiple=True,
|
|
help="The environment variables to set: <key>=<value>. For multiple envs, repeat this option, e.g. --env k1=v2 --env k2=v2.",
|
|
)
|
|
@click.option(
|
|
"--vmtype",
|
|
help="Vm type id. Run `reflex cloud vmtypes` to get options.",
|
|
)
|
|
@click.option(
|
|
"--hostname",
|
|
help="The hostname of the frontend.",
|
|
)
|
|
@click.option(
|
|
"--interactive/--no-interactive",
|
|
is_flag=True,
|
|
default=True,
|
|
help="Whether to list configuration options and ask for confirmation.",
|
|
)
|
|
@click.option(
|
|
"--envfile",
|
|
help="The path to an env file to use. Will override any envs set manually.",
|
|
)
|
|
@click.option(
|
|
"--project",
|
|
help="project id to deploy to",
|
|
)
|
|
@click.option(
|
|
"--project-name",
|
|
help="The name of the project to deploy to.",
|
|
)
|
|
@click.option(
|
|
"--token",
|
|
help="token to use for auth",
|
|
)
|
|
@click.option(
|
|
"--config-path",
|
|
"--config",
|
|
help="path to the config file",
|
|
)
|
|
@click.option(
|
|
"--exclude-from-backend",
|
|
"backend_excluded_dirs",
|
|
multiple=True,
|
|
type=click.Path(exists=True, path_type=Path, resolve_path=True),
|
|
help="Files or directories to exclude from the backend zip. Can be used multiple times.",
|
|
)
|
|
@click.option(
|
|
"--server-side-rendering/--no-server-side-rendering",
|
|
"--ssr/--no-ssr",
|
|
"ssr",
|
|
default=True,
|
|
is_flag=True,
|
|
help="Whether to enable server side rendering for the frontend.",
|
|
)
|
|
def deploy(
|
|
app_name: str | None,
|
|
app_id: str | None,
|
|
region: tuple[str, ...],
|
|
env: tuple[str],
|
|
vmtype: str | None,
|
|
hostname: str | None,
|
|
interactive: bool,
|
|
envfile: str | None,
|
|
project: str | None,
|
|
project_name: str | None,
|
|
token: str | None,
|
|
config_path: str | None,
|
|
backend_excluded_dirs: tuple[Path, ...] = (),
|
|
ssr: bool = True,
|
|
):
|
|
"""Deploy the app to the Reflex hosting service."""
|
|
from reflex_cli.utils import dependency
|
|
from reflex_cli.v2 import cli as hosting_cli
|
|
from reflex_cli.v2.deployments import check_version
|
|
|
|
from reflex.utils import export as export_utils
|
|
from reflex.utils import prerequisites
|
|
|
|
config = get_config()
|
|
|
|
app_name = app_name or config.app_name
|
|
|
|
check_version()
|
|
|
|
environment.REFLEX_COMPILE_CONTEXT.set(constants.CompileContext.DEPLOY)
|
|
|
|
if not environment.REFLEX_SSR.is_set():
|
|
environment.REFLEX_SSR.set(ssr)
|
|
elif environment.REFLEX_SSR.get() != ssr:
|
|
ssr = environment.REFLEX_SSR.get()
|
|
|
|
# Only check requirements if interactive.
|
|
# There is user interaction for requirements update.
|
|
if interactive:
|
|
dependency.check_requirements()
|
|
|
|
prerequisites.assert_in_reflex_dir()
|
|
|
|
# Check if we are set up.
|
|
if prerequisites.needs_reinit():
|
|
_init(name=config.app_name)
|
|
prerequisites.check_latest_package_version(constants.ReflexHostingCLI.MODULE_NAME)
|
|
|
|
hosting_cli.deploy(
|
|
app_name=app_name,
|
|
app_id=app_id,
|
|
export_fn=(
|
|
lambda zip_dest_dir, api_url, deploy_url, frontend, backend, upload_db, zipping: (
|
|
export_utils.export(
|
|
zip_dest_dir=zip_dest_dir,
|
|
api_url=api_url,
|
|
deploy_url=deploy_url,
|
|
frontend=frontend,
|
|
backend=backend,
|
|
zipping=zipping,
|
|
loglevel=config.loglevel.subprocess_level(),
|
|
upload_db_file=upload_db,
|
|
backend_excluded_dirs=backend_excluded_dirs,
|
|
prerender_routes=ssr,
|
|
)
|
|
)
|
|
),
|
|
regions=list(region),
|
|
envs=list(env),
|
|
vmtype=vmtype,
|
|
envfile=envfile,
|
|
hostname=hostname,
|
|
interactive=interactive,
|
|
loglevel=_convert_reflex_loglevel_to_reflex_cli_loglevel(config.loglevel),
|
|
token=token,
|
|
project=project,
|
|
project_name=project_name,
|
|
**({"config_path": config_path} if config_path is not None else {}),
|
|
)
|
|
|
|
|
|
@cli.command()
|
|
@loglevel_option
|
|
@click.argument("new_name")
|
|
def rename(new_name: str):
|
|
"""Rename the app in the current directory."""
|
|
from reflex.utils import prerequisites
|
|
from reflex.utils.rename import rename_app
|
|
|
|
prerequisites.validate_app_name(new_name)
|
|
rename_app(new_name, get_config().loglevel)
|
|
|
|
|
|
def _convert_reflex_loglevel_to_reflex_cli_loglevel(
|
|
loglevel: constants.LogLevel,
|
|
) -> HostingLogLevel:
|
|
"""Convert a Reflex log level to a Reflex CLI log level.
|
|
|
|
Args:
|
|
loglevel: The Reflex log level to convert.
|
|
|
|
Returns:
|
|
The converted Reflex CLI log level.
|
|
"""
|
|
from reflex_cli.constants.base import LogLevel as HostingLogLevel
|
|
|
|
if loglevel == constants.LogLevel.DEBUG:
|
|
return HostingLogLevel.DEBUG
|
|
if loglevel == constants.LogLevel.INFO:
|
|
return HostingLogLevel.INFO
|
|
if loglevel == constants.LogLevel.WARNING:
|
|
return HostingLogLevel.WARNING
|
|
if loglevel == constants.LogLevel.ERROR:
|
|
return HostingLogLevel.ERROR
|
|
if loglevel == constants.LogLevel.CRITICAL:
|
|
return HostingLogLevel.CRITICAL
|
|
return HostingLogLevel.INFO
|
|
|
|
|
|
if find_spec("typer") and find_spec("typer.main"):
|
|
import typer # pyright: ignore[reportMissingImports]
|
|
|
|
if isinstance(hosting_cli, typer.Typer):
|
|
hosting_cli_command = typer.main.get_command(hosting_cli)
|
|
else:
|
|
hosting_cli_command = hosting_cli
|
|
else:
|
|
hosting_cli_command = hosting_cli
|
|
|
|
cli.add_command(hosting_cli_command, name="cloud")
|
|
cli.add_command(db_cli, name="db")
|
|
cli.add_command(script_cli, name="script")
|
|
cli.add_command(custom_components_cli, name="component")
|
|
|
|
if __name__ == "__main__":
|
|
cli()
|