"""Reflex CLI to create, run, and deploy apps.""" from __future__ import annotations import atexit import os import webbrowser from pathlib import Path from typing import List, Optional import typer import typer.core from reflex_cli.deployments import deployments_cli from reflex_cli.utils import dependency from reflex import constants from reflex.config import get_config from reflex.utils import console, telemetry # Disable typer+rich integration for help panels typer.core.rich = False # type: ignore # Create the app. try: cli = typer.Typer(add_completion=False, pretty_exceptions_enable=False) except TypeError: # Fallback for older typer versions. cli = typer.Typer(add_completion=False) # Get the config. config = get_config() def version(value: bool): """Get the Reflex version. Args: value: Whether the version flag was passed. Raises: typer.Exit: If the version flag was passed. """ if value: console.print(constants.Reflex.VERSION) raise typer.Exit() @cli.callback() def main( version: bool = typer.Option( None, "-v", "--version", callback=version, help="Get the Reflex version.", is_eager=True, ), ): """Reflex CLI to create, run, and deploy apps.""" pass def _init( name: str, template: constants.Templates.Kind | None = constants.Templates.Kind.BLANK, loglevel: constants.LogLevel = config.loglevel, ): """Initialize a new Reflex app in the given directory.""" from reflex.utils import exec, prerequisites # Set the log level. console.set_log_level(loglevel) # Show system info exec.output_system_info() # Validate the app name. app_name = prerequisites.validate_app_name(name) console.rule(f"[bold]Initializing {app_name}") prerequisites.check_latest_package_version(constants.Reflex.MODULE_NAME) prerequisites.initialize_reflex_user_directory() prerequisites.ensure_reflex_installation_id() # When upgrading to 0.4, show migration instructions. if prerequisites.should_show_rx_chakra_migration_instructions(): prerequisites.show_rx_chakra_migration_instructions() # Set up the app directory, only if the config doesn't exist. if not os.path.exists(constants.Config.FILE): if template is None: template = prerequisites.prompt_for_template() prerequisites.create_config(app_name) prerequisites.initialize_app_directory(app_name, template) telemetry.send("init") else: telemetry.send("reinit") # Set up the web project. prerequisites.initialize_frontend_dependencies() # Migrate Pynecone projects to Reflex. prerequisites.migrate_to_reflex() # Initialize the .gitignore. prerequisites.initialize_gitignore() # Initialize the requirements.txt. prerequisites.initialize_requirements_txt() # Finish initializing the app. console.success(f"Initialized {app_name}") @cli.command() def init( name: str = typer.Option( None, metavar="APP_NAME", help="The name of the app to initialize." ), template: constants.Templates.Kind = typer.Option( None, help="The template to initialize the app with.", ), loglevel: constants.LogLevel = typer.Option( config.loglevel, help="The log level to use." ), ): """Initialize a new Reflex app in the current directory.""" _init(name, template, loglevel) def _run( env: constants.Env = constants.Env.DEV, frontend: bool = True, backend: bool = True, frontend_port: str = str(config.frontend_port), backend_port: str = str(config.backend_port), backend_host: str = config.backend_host, loglevel: constants.LogLevel = config.loglevel, ): """Run the app in the given directory.""" from reflex.utils import build, exec, prerequisites, processes # Set the log level. console.set_log_level(loglevel) # Set env mode in the environment os.environ["REFLEX_ENV_MODE"] = env.value # Show system info exec.output_system_info() # If no --frontend-only and no --backend-only, then turn on frontend and backend both if not frontend and not backend: frontend = True backend = True if not frontend and backend: _skip_compile() # Check that the app is initialized. prerequisites.check_initialized(frontend=frontend) # If something is running on the ports, ask the user if they want to kill or change it. if frontend and processes.is_process_on_port(frontend_port): frontend_port = processes.change_or_terminate_port(frontend_port, "frontend") if backend and processes.is_process_on_port(backend_port): backend_port = processes.change_or_terminate_port(backend_port, "backend") # Apply the new ports to the config. if frontend_port != str(config.frontend_port): config._set_persistent(frontend_port=frontend_port) if backend_port != str(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) if frontend: prerequisites.update_next_config() # Get the app module. prerequisites.get_compiled_app() # 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, ) assert setup_frontend and frontend_cmd and backend_cmd, "Invalid env" # 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)) # 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)) # Start the frontend and backend. with processes.run_concurrently_context(*commands): # In dev mode, run the backend on the main thread. if backend and env == constants.Env.DEV: backend_cmd(backend_host, int(backend_port)) @cli.command() def run( env: constants.Env = typer.Option( constants.Env.DEV, help="The environment to run the app in." ), frontend: bool = typer.Option( False, "--frontend-only", help="Execute only frontend." ), backend: bool = typer.Option(False, "--backend-only", help="Execute only backend."), frontend_port: str = typer.Option( config.frontend_port, help="Specify a different frontend port." ), backend_port: str = typer.Option( config.backend_port, help="Specify a different backend port." ), backend_host: str = typer.Option( config.backend_host, help="Specify the backend host." ), loglevel: constants.LogLevel = typer.Option( config.loglevel, help="The log level to use." ), ): """Run the app in the current directory.""" _run(env, frontend, backend, frontend_port, backend_port, backend_host, loglevel) @cli.command() def export( zipping: bool = typer.Option( True, "--no-zip", help="Disable zip for backend and frontend exports." ), frontend: bool = typer.Option( True, "--backend-only", help="Export only backend.", show_default=False ), backend: bool = typer.Option( True, "--frontend-only", help="Export only frontend.", show_default=False ), zip_dest_dir: str = typer.Option( os.getcwd(), help="The directory to export the zip files to.", show_default=False, ), upload_db_file: bool = typer.Option( False, help="Whether to exclude sqlite db files when exporting backend.", hidden=True, ), loglevel: constants.LogLevel = typer.Option( config.loglevel, help="The log level to use." ), ): """Export the app to a zip file.""" from reflex.utils import export as export_utils export_utils.export( zipping=zipping, frontend=frontend, backend=backend, zip_dest_dir=zip_dest_dir, upload_db_file=upload_db_file, loglevel=loglevel, ) @cli.command() def login( loglevel: constants.LogLevel = typer.Option( config.loglevel, help="The log level to use." ), ): """Authenticate with Reflex hosting service.""" from reflex_cli.utils import hosting # Set the log level. console.set_log_level(loglevel) access_token, invitation_code = hosting.authenticated_token() if access_token: console.print("You already logged in.") return # If not already logged in, open a browser window/tab to the login page. access_token = hosting.authenticate_on_browser(invitation_code) if not access_token: console.error(f"Unable to authenticate. Please try again or contact support.") raise typer.Exit(1) console.print("Successfully logged in.") @cli.command() def logout( loglevel: constants.LogLevel = typer.Option( config.loglevel, help="The log level to use." ), ): """Log out of access to Reflex hosting service.""" from reflex_cli.utils import hosting console.set_log_level(loglevel) hosting.log_out_on_browser() console.debug("Deleting access token from config locally") hosting.delete_token_from_config(include_invitation_code=True) db_cli = typer.Typer() script_cli = typer.Typer() def _skip_compile(): """Skip the compile step.""" os.environ[constants.SKIP_COMPILE_ENV_VAR] = "yes" @db_cli.command(name="init") def db_init(): """Create database schema and migration configuration.""" from reflex import model from reflex.utils import prerequisites # 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 Path(constants.ALEMBIC_CONFIG).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() def makemigrations( message: str = typer.Option( None, help="Human readable identifier for the generated revision." ), ): """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." ) @script_cli.command( name="keep-chakra", help="Change all rx. references to rx.chakra., to preserve Chakra UI usage.", ) def keep_chakra(): """Change all rx. references to rx.chakra., to preserve Chakra UI usage.""" from reflex.utils import prerequisites prerequisites.migrate_to_rx_chakra() @cli.command() def deploy( key: Optional[str] = typer.Option( None, "-k", "--deployment-key", help="The name of the deployment. Domain name safe characters only.", ), app_name: str = typer.Option( config.app_name, "--app-name", help="The name of the App to deploy under.", hidden=True, ), regions: List[str] = typer.Option( list(), "-r", "--region", help="The regions to deploy to.", ), envs: List[str] = typer.Option( list(), "--env", help="The environment variables to set: =. For multiple envs, repeat this option, e.g. --env k1=v2 --env k2=v2.", ), cpus: Optional[int] = typer.Option( None, help="The number of CPUs to allocate.", hidden=True ), memory_mb: Optional[int] = typer.Option( None, help="The amount of memory to allocate.", hidden=True ), auto_start: Optional[bool] = typer.Option( None, help="Whether to auto start the instance.", hidden=True, ), auto_stop: Optional[bool] = typer.Option( None, help="Whether to auto stop the instance.", hidden=True, ), frontend_hostname: Optional[str] = typer.Option( None, "--frontend-hostname", help="The hostname of the frontend.", hidden=True, ), interactive: bool = typer.Option( True, help="Whether to list configuration options and ask for confirmation.", ), with_metrics: Optional[str] = typer.Option( None, help="Setting for metrics scraping for the deployment. Setup required in user code.", hidden=True, ), with_tracing: Optional[str] = typer.Option( None, help="Setting to export tracing for the deployment. Setup required in user code.", hidden=True, ), upload_db_file: bool = typer.Option( False, help="Whether to include local sqlite db files when uploading to hosting service.", hidden=True, ), loglevel: constants.LogLevel = typer.Option( config.loglevel, help="The log level to use." ), ): """Deploy the app to the Reflex hosting service.""" from reflex_cli import cli as hosting_cli from reflex.utils import export as export_utils from reflex.utils import prerequisites # Set the log level. console.set_log_level(loglevel) # Only check requirements if interactive. There is user interaction for requirements update. if interactive: dependency.check_requirements() # Check if we are set up. prerequisites.check_initialized(frontend=True) prerequisites.check_latest_package_version(constants.ReflexHostingCLI.MODULE_NAME) hosting_cli.deploy( app_name=app_name, export_fn=lambda zip_dest_dir, api_url, deploy_url, frontend, backend, 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=loglevel, upload_db_file=upload_db_file, ), key=key, regions=regions, envs=envs, cpus=cpus, memory_mb=memory_mb, auto_start=auto_start, auto_stop=auto_stop, frontend_hostname=frontend_hostname, interactive=interactive, with_metrics=with_metrics, with_tracing=with_tracing, loglevel=loglevel.value, ) @cli.command() def demo( frontend_port: str = typer.Option( "3001", help="Specify a different frontend port." ), backend_port: str = typer.Option("8001", help="Specify a different backend port."), ): """Run the demo app.""" # Open the demo app in a terminal. webbrowser.open("https://demo.reflex.run") # Later: open the demo app locally. # with tempfile.TemporaryDirectory() as tmp_dir: # os.chdir(tmp_dir) # _init( # name="reflex_demo", # template=constants.Templates.Kind.DEMO, # loglevel=constants.LogLevel.DEBUG, # ) # _run( # frontend_port=frontend_port, # backend_port=backend_port, # loglevel=constants.LogLevel.DEBUG, # ) cli.add_typer(db_cli, name="db", help="Subcommands for managing the database schema.") cli.add_typer(script_cli, name="script", help="Subcommands running helper scripts.") cli.add_typer( deployments_cli, name="deployments", help="Subcommands for managing the Deployments.", ) if __name__ == "__main__": cli()