reflex.py 21 KB

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