reflex.py 21 KB

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