reflex.py 18 KB

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