reflex.py 19 KB

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