reflex.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632
  1. """Reflex CLI to create, run, and deploy apps."""
  2. from __future__ import annotations
  3. import atexit
  4. from pathlib import Path
  5. import typer
  6. import typer.core
  7. from reflex_cli.v2.deployments import check_version, hosting_cli
  8. from reflex import constants
  9. from reflex.config import environment, get_config
  10. from reflex.custom_components.custom_components import custom_components_cli
  11. from reflex.state import reset_disk_state_manager
  12. from reflex.utils import console, telemetry
  13. # Disable typer+rich integration for help panels
  14. typer.core.rich = None # pyright: ignore [reportPrivateImportUsage]
  15. # Create the app.
  16. cli = typer.Typer(add_completion=False, pretty_exceptions_enable=False)
  17. # Get the config.
  18. config = get_config()
  19. def version(value: bool):
  20. """Get the Reflex version.
  21. Args:
  22. value: Whether the version flag was passed.
  23. Raises:
  24. typer.Exit: If the version flag was passed.
  25. """
  26. if value:
  27. console.print(constants.Reflex.VERSION)
  28. raise typer.Exit()
  29. @cli.callback()
  30. def main(
  31. version: bool = typer.Option(
  32. None,
  33. "-v",
  34. "--version",
  35. callback=version,
  36. help="Get the Reflex version.",
  37. is_eager=True,
  38. ),
  39. ):
  40. """Reflex CLI to create, run, and deploy apps."""
  41. pass
  42. def _init(
  43. name: str,
  44. template: str | None = None,
  45. loglevel: constants.LogLevel = config.loglevel,
  46. ai: bool = False,
  47. ):
  48. """Initialize a new Reflex app in the given directory."""
  49. from reflex.utils import exec, prerequisites
  50. # Set the log level.
  51. console.set_log_level(loglevel)
  52. # Show system info
  53. exec.output_system_info()
  54. # Validate the app name.
  55. app_name = prerequisites.validate_app_name(name)
  56. console.rule(f"[bold]Initializing {app_name}")
  57. # Check prerequisites.
  58. prerequisites.check_latest_package_version(constants.Reflex.MODULE_NAME)
  59. prerequisites.initialize_reflex_user_directory()
  60. prerequisites.ensure_reflex_installation_id()
  61. # Set up the web project.
  62. prerequisites.initialize_frontend_dependencies()
  63. # Initialize the app.
  64. template = prerequisites.initialize_app(app_name, template, ai)
  65. # Initialize the .gitignore.
  66. prerequisites.initialize_gitignore()
  67. # Initialize the requirements.txt.
  68. prerequisites.initialize_requirements_txt()
  69. template_msg = f" using the {template} template" if template else ""
  70. # Finish initializing the app.
  71. console.success(f"Initialized {app_name}{template_msg}")
  72. @cli.command()
  73. def init(
  74. name: str = typer.Option(
  75. None, metavar="APP_NAME", help="The name of the app to initialize."
  76. ),
  77. template: str = typer.Option(
  78. None,
  79. help="The template to initialize the app with.",
  80. ),
  81. loglevel: constants.LogLevel = typer.Option(
  82. config.loglevel, help="The log level to use."
  83. ),
  84. ai: bool = typer.Option(
  85. False,
  86. help="Use AI to create the initial template. Cannot be used with existing app or `--template` option.",
  87. ),
  88. ):
  89. """Initialize a new Reflex app in the current directory."""
  90. _init(name, template, loglevel, ai)
  91. def _run(
  92. env: constants.Env = constants.Env.DEV,
  93. frontend: bool = True,
  94. backend: bool = True,
  95. frontend_port: int | None = None,
  96. backend_port: int | None = None,
  97. backend_host: str = config.backend_host,
  98. loglevel: constants.LogLevel = config.loglevel,
  99. ):
  100. """Run the app in the given directory."""
  101. from reflex.utils import build, exec, prerequisites, processes
  102. # Set the log level.
  103. console.set_log_level(loglevel)
  104. # Set env mode in the environment
  105. environment.REFLEX_ENV_MODE.set(env)
  106. # Show system info
  107. exec.output_system_info()
  108. # If no --frontend-only and no --backend-only, then turn on frontend and backend both
  109. frontend, backend = prerequisites.check_running_mode(frontend, backend)
  110. if not frontend and backend:
  111. _skip_compile()
  112. # Check that the app is initialized.
  113. if prerequisites.needs_reinit(frontend=frontend):
  114. _init(name=config.app_name, loglevel=loglevel)
  115. # Delete the states folder if it exists.
  116. reset_disk_state_manager()
  117. # Find the next available open port if applicable.
  118. if frontend:
  119. auto_increment_frontend = not bool(frontend_port or config.frontend_port)
  120. frontend_port = processes.handle_port(
  121. "frontend",
  122. (
  123. frontend_port
  124. or config.frontend_port
  125. or constants.DefaultPorts.FRONTEND_PORT
  126. ),
  127. auto_increment=auto_increment_frontend,
  128. )
  129. if backend:
  130. auto_increment_backend = not bool(backend_port or config.backend_port)
  131. backend_port = processes.handle_port(
  132. "backend",
  133. (
  134. backend_port
  135. or config.backend_port
  136. or constants.DefaultPorts.BACKEND_PORT
  137. ),
  138. auto_increment=auto_increment_backend,
  139. )
  140. # Apply the new ports to the config.
  141. if frontend_port != config.frontend_port:
  142. config._set_persistent(frontend_port=frontend_port)
  143. if backend_port != config.backend_port:
  144. config._set_persistent(backend_port=backend_port)
  145. # Reload the config to make sure the env vars are persistent.
  146. get_config(reload=True)
  147. console.rule("[bold]Starting Reflex App")
  148. prerequisites.check_latest_package_version(constants.Reflex.MODULE_NAME)
  149. if frontend:
  150. # Get the app module.
  151. prerequisites.get_compiled_app()
  152. # Warn if schema is not up to date.
  153. prerequisites.check_schema_up_to_date()
  154. # Get the frontend and backend commands, based on the environment.
  155. setup_frontend = frontend_cmd = backend_cmd = None
  156. if env == constants.Env.DEV:
  157. setup_frontend, frontend_cmd, backend_cmd = (
  158. build.setup_frontend,
  159. exec.run_frontend,
  160. exec.run_backend,
  161. )
  162. if env == constants.Env.PROD:
  163. setup_frontend, frontend_cmd, backend_cmd = (
  164. build.setup_frontend_prod,
  165. exec.run_frontend_prod,
  166. exec.run_backend_prod,
  167. )
  168. if not setup_frontend or not frontend_cmd or not backend_cmd:
  169. raise ValueError("Invalid env")
  170. # Post a telemetry event.
  171. telemetry.send(f"run-{env.value}")
  172. # Display custom message when there is a keyboard interrupt.
  173. atexit.register(processes.atexit_handler)
  174. # Run the frontend and backend together.
  175. commands = []
  176. # Run the frontend on a separate thread.
  177. if frontend:
  178. setup_frontend(Path.cwd())
  179. commands.append((frontend_cmd, Path.cwd(), frontend_port, backend))
  180. # In prod mode, run the backend on a separate thread.
  181. if backend and env == constants.Env.PROD:
  182. commands.append(
  183. (
  184. backend_cmd,
  185. backend_host,
  186. backend_port,
  187. loglevel.subprocess_level(),
  188. frontend,
  189. )
  190. )
  191. # Start the frontend and backend.
  192. with processes.run_concurrently_context(*commands):
  193. # In dev mode, run the backend on the main thread.
  194. if backend and backend_port and env == constants.Env.DEV:
  195. backend_cmd(
  196. backend_host, int(backend_port), loglevel.subprocess_level(), frontend
  197. )
  198. # The windows uvicorn bug workaround
  199. # https://github.com/reflex-dev/reflex/issues/2335
  200. if constants.IS_WINDOWS and exec.frontend_process:
  201. # Sends SIGTERM in windows
  202. exec.kill(exec.frontend_process.pid)
  203. @cli.command()
  204. def run(
  205. env: constants.Env = typer.Option(
  206. constants.Env.DEV, help="The environment to run the app in."
  207. ),
  208. frontend: bool = typer.Option(
  209. False,
  210. "--frontend-only",
  211. help="Execute only frontend.",
  212. envvar=environment.REFLEX_FRONTEND_ONLY.name,
  213. ),
  214. backend: bool = typer.Option(
  215. False,
  216. "--backend-only",
  217. help="Execute only backend.",
  218. envvar=environment.REFLEX_BACKEND_ONLY.name,
  219. ),
  220. frontend_port: int = typer.Option(
  221. config.frontend_port,
  222. help="Specify a different frontend port.",
  223. envvar=environment.REFLEX_FRONTEND_PORT.name,
  224. ),
  225. backend_port: int = typer.Option(
  226. config.backend_port,
  227. help="Specify a different backend port.",
  228. envvar=environment.REFLEX_BACKEND_PORT.name,
  229. ),
  230. backend_host: str = typer.Option(
  231. config.backend_host, help="Specify the backend host."
  232. ),
  233. loglevel: constants.LogLevel = typer.Option(
  234. config.loglevel, help="The log level to use."
  235. ),
  236. ):
  237. """Run the app in the current directory."""
  238. if frontend and backend:
  239. console.error("Cannot use both --frontend-only and --backend-only options.")
  240. raise typer.Exit(1)
  241. environment.REFLEX_COMPILE_CONTEXT.set(constants.CompileContext.RUN)
  242. environment.REFLEX_BACKEND_ONLY.set(backend)
  243. environment.REFLEX_FRONTEND_ONLY.set(frontend)
  244. _run(env, frontend, backend, frontend_port, backend_port, backend_host, loglevel)
  245. @cli.command()
  246. def export(
  247. zipping: bool = typer.Option(
  248. True, "--no-zip", help="Disable zip for backend and frontend exports."
  249. ),
  250. frontend: bool = typer.Option(
  251. False,
  252. "--frontend-only",
  253. help="Export only frontend.",
  254. show_default=False,
  255. envvar=environment.REFLEX_FRONTEND_ONLY.name,
  256. ),
  257. backend: bool = typer.Option(
  258. False,
  259. "--backend-only",
  260. help="Export only backend.",
  261. show_default=False,
  262. envvar=environment.REFLEX_BACKEND_ONLY.name,
  263. ),
  264. zip_dest_dir: str = typer.Option(
  265. str(Path.cwd()),
  266. help="The directory to export the zip files to.",
  267. show_default=False,
  268. ),
  269. upload_db_file: bool = typer.Option(
  270. False,
  271. help="Whether to exclude sqlite db files when exporting backend.",
  272. hidden=True,
  273. ),
  274. env: constants.Env = typer.Option(
  275. constants.Env.PROD, help="The environment to export the app in."
  276. ),
  277. loglevel: constants.LogLevel = typer.Option(
  278. config.loglevel, help="The log level to use."
  279. ),
  280. ):
  281. """Export the app to a zip file."""
  282. from reflex.utils import export as export_utils
  283. from reflex.utils import prerequisites
  284. environment.REFLEX_COMPILE_CONTEXT.set(constants.CompileContext.EXPORT)
  285. frontend, backend = prerequisites.check_running_mode(frontend, backend)
  286. if prerequisites.needs_reinit(frontend=frontend or not backend):
  287. _init(name=config.app_name, loglevel=loglevel)
  288. export_utils.export(
  289. zipping=zipping,
  290. frontend=frontend,
  291. backend=backend,
  292. zip_dest_dir=zip_dest_dir,
  293. upload_db_file=upload_db_file,
  294. env=env,
  295. loglevel=loglevel.subprocess_level(),
  296. )
  297. @cli.command()
  298. def login(loglevel: constants.LogLevel = typer.Option(config.loglevel)):
  299. """Authenticate with experimental Reflex hosting service."""
  300. from reflex_cli.v2 import cli as hosting_cli
  301. check_version()
  302. validated_info = hosting_cli.login()
  303. if validated_info is not None:
  304. _skip_compile() # Allow running outside of an app dir
  305. telemetry.send("login", user_uuid=validated_info.get("user_id"))
  306. @cli.command()
  307. def logout(
  308. loglevel: constants.LogLevel = typer.Option(
  309. config.loglevel, help="The log level to use."
  310. ),
  311. ):
  312. """Log out of access to Reflex hosting service."""
  313. from reflex_cli.v2.cli import logout
  314. check_version()
  315. logout(loglevel) # pyright: ignore [reportArgumentType]
  316. db_cli = typer.Typer()
  317. script_cli = typer.Typer()
  318. def _skip_compile():
  319. """Skip the compile step."""
  320. environment.REFLEX_SKIP_COMPILE.set(True)
  321. @db_cli.command(name="init")
  322. def db_init():
  323. """Create database schema and migration configuration."""
  324. from reflex import model
  325. from reflex.utils import prerequisites
  326. # Check the database url.
  327. if config.db_url is None:
  328. console.error("db_url is not configured, cannot initialize.")
  329. return
  330. # Check the alembic config.
  331. if environment.ALEMBIC_CONFIG.get().exists():
  332. console.error(
  333. "Database is already initialized. Use "
  334. "[bold]reflex db makemigrations[/bold] to create schema change "
  335. "scripts and [bold]reflex db migrate[/bold] to apply migrations "
  336. "to a new or existing database.",
  337. )
  338. return
  339. # Initialize the database.
  340. _skip_compile()
  341. prerequisites.get_compiled_app()
  342. model.Model.alembic_init()
  343. model.Model.migrate(autogenerate=True)
  344. @db_cli.command()
  345. def migrate():
  346. """Create or update database schema from migration scripts."""
  347. from reflex import model
  348. from reflex.utils import prerequisites
  349. # TODO see if we can use `get_app()` instead (no compile). Would _skip_compile still be needed then?
  350. _skip_compile()
  351. prerequisites.get_compiled_app()
  352. if not prerequisites.check_db_initialized():
  353. return
  354. model.Model.migrate()
  355. prerequisites.check_schema_up_to_date()
  356. @db_cli.command()
  357. def makemigrations(
  358. message: str = typer.Option(
  359. None, help="Human readable identifier for the generated revision."
  360. ),
  361. ):
  362. """Create autogenerated alembic migration scripts."""
  363. from alembic.util.exc import CommandError
  364. from reflex import model
  365. from reflex.utils import prerequisites
  366. # TODO see if we can use `get_app()` instead (no compile). Would _skip_compile still be needed then?
  367. _skip_compile()
  368. prerequisites.get_compiled_app()
  369. if not prerequisites.check_db_initialized():
  370. return
  371. with model.Model.get_db_engine().connect() as connection:
  372. try:
  373. model.Model.alembic_autogenerate(connection=connection, message=message)
  374. except CommandError as command_error:
  375. if "Target database is not up to date." not in str(command_error):
  376. raise
  377. console.error(
  378. f"{command_error} Run [bold]reflex db migrate[/bold] to update database."
  379. )
  380. @cli.command()
  381. def deploy(
  382. app_name: str = typer.Option(
  383. config.app_name,
  384. "--app-name",
  385. help="The name of the App to deploy under.",
  386. ),
  387. app_id: str = typer.Option(
  388. None,
  389. "--app-id",
  390. help="The ID of the App to deploy over.",
  391. ),
  392. regions: list[str] = typer.Option(
  393. [],
  394. "-r",
  395. "--region",
  396. help="The regions to deploy to. `reflex cloud regions` For multiple envs, repeat this option, e.g. --region sjc --region iad",
  397. ),
  398. envs: list[str] = typer.Option(
  399. [],
  400. "--env",
  401. help="The environment variables to set: <key>=<value>. For multiple envs, repeat this option, e.g. --env k1=v2 --env k2=v2.",
  402. ),
  403. vmtype: str | None = typer.Option(
  404. None,
  405. "--vmtype",
  406. help="Vm type id. Run `reflex cloud vmtypes` to get options.",
  407. ),
  408. hostname: str | None = typer.Option(
  409. None,
  410. "--hostname",
  411. help="The hostname of the frontend.",
  412. ),
  413. interactive: bool = typer.Option(
  414. True,
  415. help="Whether to list configuration options and ask for confirmation.",
  416. ),
  417. envfile: str | None = typer.Option(
  418. None,
  419. "--envfile",
  420. help="The path to an env file to use. Will override any envs set manually.",
  421. ),
  422. loglevel: constants.LogLevel = typer.Option(
  423. config.loglevel, help="The log level to use."
  424. ),
  425. project: str | None = typer.Option(
  426. None,
  427. "--project",
  428. help="project id to deploy to",
  429. ),
  430. project_name: str | None = typer.Option(
  431. None,
  432. "--project-name",
  433. help="The name of the project to deploy to.",
  434. ),
  435. token: str | None = typer.Option(
  436. None,
  437. "--token",
  438. help="token to use for auth",
  439. ),
  440. config_path: str | None = typer.Option(
  441. None,
  442. "--config",
  443. help="path to the config file",
  444. ),
  445. ):
  446. """Deploy the app to the Reflex hosting service."""
  447. from reflex_cli.constants.base import LogLevel as HostingLogLevel
  448. from reflex_cli.utils import dependency
  449. from reflex_cli.v2 import cli as hosting_cli
  450. from reflex.utils import export as export_utils
  451. from reflex.utils import prerequisites
  452. check_version()
  453. environment.REFLEX_COMPILE_CONTEXT.set(constants.CompileContext.DEPLOY)
  454. # Set the log level.
  455. console.set_log_level(loglevel)
  456. def convert_reflex_loglevel_to_reflex_cli_loglevel(
  457. loglevel: constants.LogLevel,
  458. ) -> HostingLogLevel:
  459. if loglevel == constants.LogLevel.DEBUG:
  460. return HostingLogLevel.DEBUG
  461. if loglevel == constants.LogLevel.INFO:
  462. return HostingLogLevel.INFO
  463. if loglevel == constants.LogLevel.WARNING:
  464. return HostingLogLevel.WARNING
  465. if loglevel == constants.LogLevel.ERROR:
  466. return HostingLogLevel.ERROR
  467. if loglevel == constants.LogLevel.CRITICAL:
  468. return HostingLogLevel.CRITICAL
  469. return HostingLogLevel.INFO
  470. # Only check requirements if interactive.
  471. # There is user interaction for requirements update.
  472. if interactive:
  473. dependency.check_requirements()
  474. # Check if we are set up.
  475. if prerequisites.needs_reinit(frontend=True):
  476. _init(name=config.app_name, loglevel=loglevel)
  477. prerequisites.check_latest_package_version(constants.ReflexHostingCLI.MODULE_NAME)
  478. hosting_cli.deploy(
  479. app_name=app_name,
  480. app_id=app_id,
  481. export_fn=lambda zip_dest_dir,
  482. api_url,
  483. deploy_url,
  484. frontend,
  485. backend,
  486. zipping: export_utils.export(
  487. zip_dest_dir=zip_dest_dir,
  488. api_url=api_url,
  489. deploy_url=deploy_url,
  490. frontend=frontend,
  491. backend=backend,
  492. zipping=zipping,
  493. loglevel=loglevel.subprocess_level(),
  494. ),
  495. regions=regions,
  496. envs=envs,
  497. vmtype=vmtype,
  498. envfile=envfile,
  499. hostname=hostname,
  500. interactive=interactive,
  501. loglevel=convert_reflex_loglevel_to_reflex_cli_loglevel(loglevel),
  502. token=token,
  503. project=project,
  504. project_name=project_name,
  505. **({"config_path": config_path} if config_path is not None else {}),
  506. )
  507. @cli.command()
  508. def rename(
  509. new_name: str = typer.Argument(..., help="The new name for the app."),
  510. loglevel: constants.LogLevel = typer.Option(
  511. config.loglevel, help="The log level to use."
  512. ),
  513. ):
  514. """Rename the app in the current directory."""
  515. from reflex.utils import prerequisites
  516. prerequisites.validate_app_name(new_name)
  517. prerequisites.rename_app(new_name, loglevel)
  518. cli.add_typer(db_cli, name="db", help="Subcommands for managing the database schema.")
  519. cli.add_typer(script_cli, name="script", help="Subcommands running helper scripts.")
  520. cli.add_typer(
  521. hosting_cli,
  522. name="cloud",
  523. help="Subcommands for managing the reflex cloud.",
  524. )
  525. cli.add_typer(
  526. custom_components_cli,
  527. name="component",
  528. help="Subcommands for creating and publishing Custom Components.",
  529. )
  530. if __name__ == "__main__":
  531. cli()