reflex.py 20 KB

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