"""Reflex CLI to create, run, and deploy apps.""" from __future__ import annotations import atexit from importlib.util import find_spec from pathlib import Path from typing import TYPE_CHECKING import click from reflex_cli.v2.deployments import hosting_cli from reflex import constants from reflex.config import environment, get_config from reflex.constants.base import LITERAL_ENV from reflex.custom_components.custom_components import custom_components_cli from reflex.state import reset_disk_state_manager from reflex.utils import console, redir, telemetry from reflex.utils.exec import should_use_granian 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.""" pass loglevel_option = click.option( "--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, prerequisites # Show system info exec.output_system_info() if ai: redir.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 = prerequisites.initialize_app(app_name, template) # Initialize the .gitignore. prerequisites.initialize_gitignore() # Initialize the requirements.txt. needs_user_manual_update = prerequisites.initialize_requirements_txt() template_msg = f" using the {template} template" if template else "" # Finish initializing the app. console.success( f"Initialized {app_name}{template_msg}." + ( f" Make sure to add {constants.RequirementsTxt.DEFAULTS_STUB + constants.Reflex.VERSION} to your requirements.txt or pyproject.toml file." if needs_user_manual_update else "" ) ) @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 _run( env: constants.Env = constants.Env.DEV, frontend: bool = True, backend: bool = True, frontend_port: int | None = None, backend_port: int | None = None, backend_host: str | None = None, ): """Run the app in the given directory.""" from reflex.utils import build, exec, prerequisites, processes 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 no --frontend-only and no --backend-only, then turn on frontend and backend both frontend, backend = prerequisites.check_running_mode(frontend, backend) if not frontend and backend: _skip_compile() prerequisites.assert_in_reflex_dir() # Check that the app is initialized. if frontend and prerequisites.needs_reinit(): _init(name=config.app_name) # Delete the states folder if it exists. reset_disk_state_manager() # Find the next available open port if applicable. if 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 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, ) # Apply the new ports 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) # 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) # Get the app module. app_task = prerequisites.compile_or_validate_app args = (frontend,) # Granian fails if the app is already imported. if should_use_granian(): import concurrent.futures compile_future = concurrent.futures.ProcessPoolExecutor(max_workers=1).submit( app_task, *args, ) validation_result = compile_future.result() else: validation_result = app_task(*args) if not validation_result: raise click.exceptions.Exit(1) # Warn if schema is not up to date. prerequisites.check_schema_up_to_date() # Get the frontend and backend commands, based on the environment. setup_frontend = frontend_cmd = backend_cmd = None if env == constants.Env.DEV: setup_frontend, frontend_cmd, backend_cmd = ( build.setup_frontend, exec.run_frontend, exec.run_backend, ) if env == constants.Env.PROD: setup_frontend, frontend_cmd, backend_cmd = ( build.setup_frontend_prod, exec.run_frontend_prod, exec.run_backend_prod, ) if not setup_frontend or not frontend_cmd or not backend_cmd: raise ValueError(f"Invalid env: {env}. Must be DEV or PROD.") # Post a telemetry event. telemetry.send(f"run-{env.value}") # 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 frontend: setup_frontend(Path.cwd()) commands.append((frontend_cmd, Path.cwd(), frontend_port, backend)) # In prod mode, run the backend on a separate thread. if backend and env == constants.Env.PROD: commands.append( ( backend_cmd, backend_host, backend_port, config.loglevel.subprocess_level(), frontend, ) ) # Start the frontend and backend. with processes.run_concurrently_context(*commands): # In dev mode, run the backend on the main thread. if backend and backend_port and env == constants.Env.DEV: backend_cmd( backend_host, int(backend_port), config.loglevel.subprocess_level(), 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) @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.", ) def run( env: LITERAL_ENV, frontend_only: bool, backend_only: bool, frontend_port: int | None, backend_port: int | None, backend_host: str | None, ): """Run the app in the current directory.""" if frontend_only and backend_only: console.error("Cannot use both --frontend-only and --backend-only options.") raise click.exceptions.Exit(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) _run( constants.Env.DEV if env == constants.Env.DEV else constants.Env.PROD, frontend_only, backend_only, frontend_port, backend_port, backend_host, ) @cli.command() @loglevel_option @click.option( "--dry", is_flag=True, default=False, help="Run the command without making any changes.", ) def compile(dry: 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.compile_app(dry_run=dry) 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.", ) def export( zip: bool, frontend_only: bool, backend_only: bool, zip_dest_dir: str, upload_db_file: bool, env: LITERAL_ENV, ): """Export the app to a zip file.""" from reflex.utils import export as export_utils from reflex.utils import prerequisites environment.REFLEX_COMPILE_CONTEXT.set(constants.CompileContext.EXPORT) should_frontend_run, should_backend_run = prerequisites.check_running_mode( frontend_only, backend_only ) config = get_config() prerequisites.assert_in_reflex_dir() if should_frontend_run and prerequisites.needs_reinit(): _init(name=config.app_name) export_utils.export( zipping=zip, frontend=should_frontend_run, backend=should_backend_run, 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(), ) @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 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.""" pass @click.group def script_cli(): """Subcommands for running helper scripts.""" pass 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.Model.alembic_init() model.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 # 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 model.Model.migrate() prerequisites.check_schema_up_to_date() @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.Model.get_db_engine().connect() as connection: try: model.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: =. 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", ) 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, ): """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) # 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, ) ), 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 prerequisites.validate_app_name(new_name) prerequisites.rename_app(new_name, get_config().loglevel) if TYPE_CHECKING: from reflex_cli.constants.base import LogLevel as HostingLogLevel 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()