336 lines
11 KiB
Python
336 lines
11 KiB
Python
"""Reflex CLI to create, run, and deploy apps."""
|
|
|
|
import os
|
|
import platform
|
|
import signal
|
|
import threading
|
|
from pathlib import Path
|
|
|
|
import httpx
|
|
import typer
|
|
from alembic.util.exc import CommandError
|
|
|
|
from reflex import constants, model
|
|
from reflex.config import get_config
|
|
from reflex.utils import build, console, exec, prerequisites, processes, telemetry
|
|
|
|
# Create the app.
|
|
cli = typer.Typer()
|
|
|
|
|
|
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.VERSION)
|
|
raise typer.Exit()
|
|
|
|
|
|
@cli.callback()
|
|
def main(
|
|
version: bool = typer.Option(
|
|
None,
|
|
"--version",
|
|
"-v",
|
|
callback=version,
|
|
help="Get the Reflex version.",
|
|
is_eager=True,
|
|
),
|
|
):
|
|
"""Reflex CLI global configuration."""
|
|
pass
|
|
|
|
|
|
@cli.command()
|
|
def init(
|
|
name: str = typer.Option(None, help="Name of the app to be initialized."),
|
|
template: constants.Template = typer.Option(
|
|
constants.Template.DEFAULT, help="Template to use for the app."
|
|
),
|
|
):
|
|
"""Initialize a new Reflex app in the current directory."""
|
|
# Get the app name.
|
|
app_name = prerequisites.get_default_app_name() if name is None else name
|
|
console.rule(f"[bold]Initializing {app_name}")
|
|
|
|
# Set up the web project.
|
|
prerequisites.initialize_frontend_dependencies()
|
|
|
|
# Migrate Pynecone projects to Reflex.
|
|
prerequisites.migrate_to_reflex()
|
|
|
|
# Set up the app directory, only if the config doesn't exist.
|
|
if not os.path.exists(constants.CONFIG_FILE):
|
|
prerequisites.create_config(app_name)
|
|
prerequisites.initialize_app_directory(app_name, template)
|
|
build.set_reflex_project_hash()
|
|
telemetry.send("init", get_config().telemetry_enabled)
|
|
else:
|
|
telemetry.send("reinit", get_config().telemetry_enabled)
|
|
|
|
# Initialize the .gitignore.
|
|
prerequisites.initialize_gitignore()
|
|
|
|
# Finish initializing the app.
|
|
console.log(f"[bold green]Finished Initializing: {app_name}")
|
|
|
|
|
|
@cli.command()
|
|
def run(
|
|
env: constants.Env = typer.Option(
|
|
get_config().env, 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."),
|
|
loglevel: constants.LogLevel = typer.Option(
|
|
constants.LogLevel.ERROR, help="The log level to use."
|
|
),
|
|
frontend_port: str = typer.Option(None, help="Specify a different frontend port."),
|
|
backend_port: str = typer.Option(None, help="Specify a different backend port."),
|
|
backend_host: str = typer.Option(None, help="Specify the backend host."),
|
|
):
|
|
"""Run the app in the current directory."""
|
|
if platform.system() == "Windows":
|
|
console.print(
|
|
"[yellow][WARNING] We strongly advise using Windows Subsystem for Linux (WSL) for optimal performance with reflex."
|
|
)
|
|
# Set ports as os env variables to take precedence over config and
|
|
# .env variables(if override_os_envs flag in config is set to False).
|
|
build.set_os_env(
|
|
frontend_port=frontend_port,
|
|
backend_port=backend_port,
|
|
backend_host=backend_host,
|
|
)
|
|
|
|
frontend_port = (
|
|
get_config().frontend_port if frontend_port is None else frontend_port
|
|
)
|
|
backend_port = get_config().backend_port if backend_port is None else backend_port
|
|
backend_host = get_config().backend_host if backend_host is None else backend_host
|
|
|
|
# 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 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")
|
|
|
|
# Check that the app is initialized.
|
|
if frontend and not prerequisites.is_initialized():
|
|
console.print(
|
|
"[red]The app is not initialized. Run [bold]reflex init[/bold] first."
|
|
)
|
|
raise typer.Exit()
|
|
|
|
# Check that the template is up to date.
|
|
if frontend and not prerequisites.is_latest_template():
|
|
console.print(
|
|
"[red]The base app template has updated. Run [bold]reflex init[/bold] again."
|
|
)
|
|
raise typer.Exit()
|
|
|
|
# Get the app module.
|
|
console.rule("[bold]Starting Reflex App")
|
|
app = prerequisites.get_app()
|
|
|
|
# Check the admin dashboard settings.
|
|
prerequisites.check_admin_settings()
|
|
|
|
# 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}", get_config().telemetry_enabled)
|
|
|
|
# Run the frontend and backend.
|
|
if frontend:
|
|
setup_frontend(Path.cwd(), loglevel)
|
|
threading.Thread(
|
|
target=frontend_cmd, args=(Path.cwd(), frontend_port, loglevel)
|
|
).start()
|
|
if backend:
|
|
threading.Thread(
|
|
target=backend_cmd,
|
|
args=(app.__name__, backend_host, backend_port, loglevel),
|
|
).start()
|
|
|
|
# Display custom message when there is a keyboard interrupt.
|
|
signal.signal(signal.SIGINT, processes.catch_keyboard_interrupt)
|
|
|
|
|
|
@cli.command()
|
|
def deploy(dry_run: bool = typer.Option(False, help="Whether to run a dry run.")):
|
|
"""Deploy the app to the Reflex hosting service."""
|
|
# Get the app config.
|
|
config = get_config()
|
|
config.api_url = prerequisites.get_production_backend_url()
|
|
|
|
# Check if the deploy url is set.
|
|
if config.rxdeploy_url is None:
|
|
typer.echo("This feature is coming soon!")
|
|
return
|
|
|
|
# Compile the app in production mode.
|
|
typer.echo("Compiling production app")
|
|
export(for_reflex_deploy=True)
|
|
|
|
# Exit early if this is a dry run.
|
|
if dry_run:
|
|
return
|
|
|
|
# Deploy the app.
|
|
data = {"userId": config.username, "projectId": config.app_name}
|
|
original_response = httpx.get(config.rxdeploy_url, params=data)
|
|
response = original_response.json()
|
|
frontend = response["frontend_resources_url"]
|
|
backend = response["backend_resources_url"]
|
|
|
|
# Upload the frontend and backend.
|
|
with open(constants.FRONTEND_ZIP, "rb") as f:
|
|
httpx.put(frontend, data=f) # type: ignore
|
|
|
|
with open(constants.BACKEND_ZIP, "rb") as f:
|
|
httpx.put(backend, data=f) # type: ignore
|
|
|
|
|
|
@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
|
|
),
|
|
for_reflex_deploy: bool = typer.Option(
|
|
False,
|
|
"--for-reflex-deploy",
|
|
help="Whether export the app for Reflex Deploy Service.",
|
|
),
|
|
):
|
|
"""Export the app to a zip file."""
|
|
config = get_config()
|
|
|
|
if for_reflex_deploy:
|
|
# Get the app config and modify the api_url base on username and app_name.
|
|
config.api_url = prerequisites.get_production_backend_url()
|
|
|
|
# Compile the app in production mode and export it.
|
|
console.rule("[bold]Compiling production app and preparing for export.")
|
|
|
|
if frontend:
|
|
# ensure module can be imported and app.compile() is called
|
|
prerequisites.get_app()
|
|
# set up .web directory and install frontend dependencies
|
|
build.setup_frontend(Path.cwd())
|
|
|
|
build.export_app(
|
|
backend=backend,
|
|
frontend=frontend,
|
|
zip=zipping,
|
|
deploy_url=config.deploy_url,
|
|
)
|
|
|
|
# Post a telemetry event.
|
|
telemetry.send("export", get_config().telemetry_enabled)
|
|
|
|
if zipping:
|
|
console.rule(
|
|
"""Backend & Frontend compiled. See [green bold]backend.zip[/green bold]
|
|
and [green bold]frontend.zip[/green bold]."""
|
|
)
|
|
else:
|
|
console.rule(
|
|
"""Backend & Frontend compiled. See [green bold]app[/green bold]
|
|
and [green bold].web/_static[/green bold] directories."""
|
|
)
|
|
|
|
|
|
db_cli = typer.Typer()
|
|
|
|
|
|
@db_cli.command(name="init")
|
|
def db_init():
|
|
"""Create database schema and migration configuration."""
|
|
if get_config().db_url is None:
|
|
console.print("[red]db_url is not configured, cannot initialize.")
|
|
if Path(constants.ALEMBIC_CONFIG).exists():
|
|
console.print(
|
|
"[red]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
|
|
prerequisites.get_app()
|
|
model.Model.alembic_init()
|
|
model.Model.migrate(autogenerate=True)
|
|
|
|
|
|
@db_cli.command()
|
|
def migrate():
|
|
"""Create or update database schema from migration scripts."""
|
|
prerequisites.get_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."""
|
|
prerequisites.get_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.print(
|
|
f"[red]{command_error} Run [bold]reflex db migrate[/bold] to update database."
|
|
)
|
|
|
|
|
|
cli.add_typer(db_cli, name="db", help="Subcommands for managing the database schema.")
|
|
|
|
if __name__ == "__main__":
|
|
cli()
|