reflex.py 21 KB

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