reflex.py 20 KB

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