reflex.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616
  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 import constants
  12. from reflex.config import environment, get_config
  13. from reflex.custom_components.custom_components import custom_components_cli
  14. from reflex.state import reset_disk_state_manager
  15. from reflex.utils import console, redir, telemetry
  16. # Disable typer+rich integration for help panels
  17. typer.core.rich = False # type: ignore
  18. # Create the app.
  19. try:
  20. cli = typer.Typer(add_completion=False, pretty_exceptions_enable=False)
  21. except TypeError:
  22. # Fallback for older typer versions.
  23. cli = typer.Typer(add_completion=False)
  24. # Get the config.
  25. config = get_config()
  26. def version(value: bool):
  27. """Get the Reflex version.
  28. Args:
  29. value: Whether the version flag was passed.
  30. Raises:
  31. typer.Exit: If the version flag was passed.
  32. """
  33. if value:
  34. console.print(constants.Reflex.VERSION)
  35. raise typer.Exit()
  36. @cli.callback()
  37. def main(
  38. version: bool = typer.Option(
  39. None,
  40. "-v",
  41. "--version",
  42. callback=version,
  43. help="Get the Reflex version.",
  44. is_eager=True,
  45. ),
  46. ):
  47. """Reflex CLI to create, run, and deploy apps."""
  48. pass
  49. def _init(
  50. name: str,
  51. template: str | None = None,
  52. loglevel: constants.LogLevel = config.loglevel,
  53. ai: bool = False,
  54. ):
  55. """Initialize a new Reflex app in the given directory."""
  56. from reflex.utils import exec, prerequisites
  57. # Set the log level.
  58. console.set_log_level(loglevel)
  59. # Show system info
  60. exec.output_system_info()
  61. # Validate the app name.
  62. app_name = prerequisites.validate_app_name(name)
  63. console.rule(f"[bold]Initializing {app_name}")
  64. # Check prerequisites.
  65. prerequisites.check_latest_package_version(constants.Reflex.MODULE_NAME)
  66. prerequisites.initialize_reflex_user_directory()
  67. prerequisites.ensure_reflex_installation_id()
  68. # Set up the web project.
  69. prerequisites.initialize_frontend_dependencies()
  70. # Integrate with reflex.build.
  71. generation_hash = None
  72. if ai:
  73. if template is None:
  74. # If AI is requested and no template specified, redirect the user to reflex.build.
  75. generation_hash = redir.reflex_build_redirect()
  76. elif prerequisites.is_generation_hash(template):
  77. # Otherwise treat the template as a generation hash.
  78. generation_hash = template
  79. else:
  80. console.error(
  81. "Cannot use `--template` option with `--ai` option. Please remove `--template` option."
  82. )
  83. raise typer.Exit(2)
  84. template = constants.Templates.DEFAULT
  85. # Initialize the app.
  86. prerequisites.initialize_app(app_name, template)
  87. # If a reflex.build generation hash is available, download the code and apply it to the main module.
  88. if generation_hash:
  89. prerequisites.initialize_main_module_index_from_generation(
  90. app_name, generation_hash=generation_hash
  91. )
  92. # Initialize the .gitignore.
  93. prerequisites.initialize_gitignore()
  94. # Initialize the requirements.txt.
  95. prerequisites.initialize_requirements_txt()
  96. # Finish initializing the app.
  97. console.success(f"Initialized {app_name}")
  98. @cli.command()
  99. def init(
  100. name: str = typer.Option(
  101. None, metavar="APP_NAME", help="The name of the app to initialize."
  102. ),
  103. template: str = typer.Option(
  104. None,
  105. help="The template to initialize the app with.",
  106. ),
  107. loglevel: constants.LogLevel = typer.Option(
  108. config.loglevel, help="The log level to use."
  109. ),
  110. ai: bool = typer.Option(
  111. False,
  112. help="Use AI to create the initial template. Cannot be used with existing app or `--template` option.",
  113. ),
  114. ):
  115. """Initialize a new Reflex app in the current directory."""
  116. _init(name, template, loglevel, ai)
  117. def _run(
  118. env: constants.Env = constants.Env.DEV,
  119. frontend: bool = True,
  120. backend: bool = True,
  121. frontend_port: str = str(config.frontend_port),
  122. backend_port: str = str(config.backend_port),
  123. backend_host: str = config.backend_host,
  124. loglevel: constants.LogLevel = config.loglevel,
  125. ):
  126. """Run the app in the given directory."""
  127. from reflex.utils import build, exec, prerequisites, processes
  128. # Set the log level.
  129. console.set_log_level(loglevel)
  130. # Set env mode in the environment
  131. os.environ[constants.ENV_MODE_ENV_VAR] = env.value
  132. # Show system info
  133. exec.output_system_info()
  134. # If no --frontend-only and no --backend-only, then turn on frontend and backend both
  135. if not frontend and not backend:
  136. frontend = True
  137. backend = True
  138. if not frontend and backend:
  139. _skip_compile()
  140. # Check that the app is initialized.
  141. if prerequisites.needs_reinit(frontend=frontend):
  142. _init(name=config.app_name, loglevel=loglevel)
  143. # Delete the states folder if it exists.
  144. reset_disk_state_manager()
  145. # Find the next available open port if applicable.
  146. if frontend:
  147. frontend_port = processes.handle_port(
  148. "frontend", frontend_port, str(constants.DefaultPorts.FRONTEND_PORT)
  149. )
  150. if backend:
  151. backend_port = processes.handle_port(
  152. "backend", backend_port, str(constants.DefaultPorts.BACKEND_PORT)
  153. )
  154. # Apply the new ports to the config.
  155. if frontend_port != str(config.frontend_port):
  156. config._set_persistent(frontend_port=frontend_port)
  157. if backend_port != str(config.backend_port):
  158. config._set_persistent(backend_port=backend_port)
  159. # Reload the config to make sure the env vars are persistent.
  160. get_config(reload=True)
  161. console.rule("[bold]Starting Reflex App")
  162. prerequisites.check_latest_package_version(constants.Reflex.MODULE_NAME)
  163. if frontend:
  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 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=constants.ENV_FRONTEND_ONLY_ENV_VAR,
  227. ),
  228. backend: bool = typer.Option(
  229. False,
  230. "--backend-only",
  231. help="Execute only backend.",
  232. envvar=constants.ENV_BACKEND_ONLY_ENV_VAR,
  233. ),
  234. frontend_port: str = typer.Option(
  235. config.frontend_port, help="Specify a different frontend port."
  236. ),
  237. backend_port: str = typer.Option(
  238. config.backend_port, help="Specify a different backend port."
  239. ),
  240. backend_host: str = typer.Option(
  241. config.backend_host, help="Specify the backend host."
  242. ),
  243. loglevel: constants.LogLevel = typer.Option(
  244. config.loglevel, help="The log level to use."
  245. ),
  246. ):
  247. """Run the app in the current directory."""
  248. if frontend and backend:
  249. console.error("Cannot use both --frontend-only and --backend-only options.")
  250. raise typer.Exit(1)
  251. os.environ[constants.ENV_BACKEND_ONLY_ENV_VAR] = str(backend).lower()
  252. os.environ[constants.ENV_FRONTEND_ONLY_ENV_VAR] = str(frontend).lower()
  253. _run(env, frontend, backend, frontend_port, backend_port, backend_host, loglevel)
  254. @cli.command()
  255. def export(
  256. zipping: bool = typer.Option(
  257. True, "--no-zip", help="Disable zip for backend and frontend exports."
  258. ),
  259. frontend: bool = typer.Option(
  260. True, "--backend-only", help="Export only backend.", show_default=False
  261. ),
  262. backend: bool = typer.Option(
  263. True, "--frontend-only", help="Export only frontend.", show_default=False
  264. ),
  265. zip_dest_dir: str = typer.Option(
  266. os.getcwd(),
  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. loglevel: constants.LogLevel = typer.Option(
  276. config.loglevel, help="The log level to use."
  277. ),
  278. ):
  279. """Export the app to a zip file."""
  280. from reflex.utils import export as export_utils
  281. from reflex.utils import prerequisites
  282. if prerequisites.needs_reinit(frontend=True):
  283. _init(name=config.app_name, loglevel=loglevel)
  284. export_utils.export(
  285. zipping=zipping,
  286. frontend=frontend,
  287. backend=backend,
  288. zip_dest_dir=zip_dest_dir,
  289. upload_db_file=upload_db_file,
  290. loglevel=loglevel.subprocess_level(),
  291. )
  292. def _login() -> str:
  293. """Helper function to authenticate with Reflex hosting service."""
  294. from reflex_cli.utils import hosting
  295. access_token, invitation_code = hosting.authenticated_token()
  296. if access_token:
  297. console.print("You already logged in.")
  298. return access_token
  299. # If not already logged in, open a browser window/tab to the login page.
  300. access_token = hosting.authenticate_on_browser(invitation_code)
  301. if not access_token:
  302. console.error(f"Unable to authenticate. Please try again or contact support.")
  303. raise typer.Exit(1)
  304. console.print("Successfully logged in.")
  305. return access_token
  306. @cli.command()
  307. def login(
  308. loglevel: constants.LogLevel = typer.Option(
  309. config.loglevel, help="The log level to use."
  310. ),
  311. ):
  312. """Authenticate with Reflex hosting service."""
  313. # Set the log level.
  314. console.set_log_level(loglevel)
  315. _login()
  316. @cli.command()
  317. def logout(
  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.utils import hosting
  324. console.set_log_level(loglevel)
  325. hosting.log_out_on_browser()
  326. console.debug("Deleting access token from config locally")
  327. hosting.delete_token_from_config(include_invitation_code=True)
  328. db_cli = typer.Typer()
  329. script_cli = typer.Typer()
  330. def _skip_compile():
  331. """Skip the compile step."""
  332. os.environ[constants.SKIP_COMPILE_ENV_VAR] = "yes"
  333. @db_cli.command(name="init")
  334. def db_init():
  335. """Create database schema and migration configuration."""
  336. from reflex import model
  337. from reflex.utils import prerequisites
  338. # Check the database url.
  339. if config.db_url is None:
  340. console.error("db_url is not configured, cannot initialize.")
  341. return
  342. # Check the alembic config.
  343. if environment.ALEMBIC_CONFIG.exists():
  344. console.error(
  345. "Database is already initialized. Use "
  346. "[bold]reflex db makemigrations[/bold] to create schema change "
  347. "scripts and [bold]reflex db migrate[/bold] to apply migrations "
  348. "to a new or existing database.",
  349. )
  350. return
  351. # Initialize the database.
  352. _skip_compile()
  353. prerequisites.get_compiled_app()
  354. model.Model.alembic_init()
  355. model.Model.migrate(autogenerate=True)
  356. @db_cli.command()
  357. def migrate():
  358. """Create or update database schema from migration scripts."""
  359. from reflex import model
  360. from reflex.utils import prerequisites
  361. # TODO see if we can use `get_app()` instead (no compile). Would _skip_compile still be needed then?
  362. _skip_compile()
  363. prerequisites.get_compiled_app()
  364. if not prerequisites.check_db_initialized():
  365. return
  366. model.Model.migrate()
  367. prerequisites.check_schema_up_to_date()
  368. @db_cli.command()
  369. def makemigrations(
  370. message: str = typer.Option(
  371. None, help="Human readable identifier for the generated revision."
  372. ),
  373. ):
  374. """Create autogenerated alembic migration scripts."""
  375. from alembic.util.exc import CommandError
  376. from reflex import model
  377. from reflex.utils import prerequisites
  378. # TODO see if we can use `get_app()` instead (no compile). Would _skip_compile still be needed then?
  379. _skip_compile()
  380. prerequisites.get_compiled_app()
  381. if not prerequisites.check_db_initialized():
  382. return
  383. with model.Model.get_db_engine().connect() as connection:
  384. try:
  385. model.Model.alembic_autogenerate(connection=connection, message=message)
  386. except CommandError as command_error:
  387. if "Target database is not up to date." not in str(command_error):
  388. raise
  389. console.error(
  390. f"{command_error} Run [bold]reflex db migrate[/bold] to update database."
  391. )
  392. @cli.command()
  393. def deploy(
  394. key: Optional[str] = typer.Option(
  395. None,
  396. "-k",
  397. "--deployment-key",
  398. help="The name of the deployment. Domain name safe characters only.",
  399. ),
  400. app_name: str = typer.Option(
  401. config.app_name,
  402. "--app-name",
  403. help="The name of the App to deploy under.",
  404. hidden=True,
  405. ),
  406. regions: List[str] = typer.Option(
  407. list(),
  408. "-r",
  409. "--region",
  410. help="The regions to deploy to.",
  411. ),
  412. envs: List[str] = typer.Option(
  413. list(),
  414. "--env",
  415. help="The environment variables to set: <key>=<value>. For multiple envs, repeat this option, e.g. --env k1=v2 --env k2=v2.",
  416. ),
  417. cpus: Optional[int] = typer.Option(
  418. None, help="The number of CPUs to allocate.", hidden=True
  419. ),
  420. memory_mb: Optional[int] = typer.Option(
  421. None, help="The amount of memory to allocate.", hidden=True
  422. ),
  423. auto_start: Optional[bool] = typer.Option(
  424. None,
  425. help="Whether to auto start the instance.",
  426. hidden=True,
  427. ),
  428. auto_stop: Optional[bool] = typer.Option(
  429. None,
  430. help="Whether to auto stop the instance.",
  431. hidden=True,
  432. ),
  433. frontend_hostname: Optional[str] = typer.Option(
  434. None,
  435. "--frontend-hostname",
  436. help="The hostname of the frontend.",
  437. hidden=True,
  438. ),
  439. interactive: bool = typer.Option(
  440. True,
  441. help="Whether to list configuration options and ask for confirmation.",
  442. ),
  443. with_metrics: Optional[str] = typer.Option(
  444. None,
  445. help="Setting for metrics scraping for the deployment. Setup required in user code.",
  446. hidden=True,
  447. ),
  448. with_tracing: Optional[str] = typer.Option(
  449. None,
  450. help="Setting to export tracing for the deployment. Setup required in user code.",
  451. hidden=True,
  452. ),
  453. upload_db_file: bool = typer.Option(
  454. False,
  455. help="Whether to include local sqlite db files when uploading to hosting service.",
  456. hidden=True,
  457. ),
  458. loglevel: constants.LogLevel = typer.Option(
  459. config.loglevel, help="The log level to use."
  460. ),
  461. ):
  462. """Deploy the app to the Reflex hosting service."""
  463. from reflex_cli import cli as hosting_cli
  464. from reflex.utils import export as export_utils
  465. from reflex.utils import prerequisites
  466. # Set the log level.
  467. console.set_log_level(loglevel)
  468. # Only check requirements if interactive. There is user interaction for requirements update.
  469. if interactive:
  470. dependency.check_requirements()
  471. # Check if we are set up.
  472. if prerequisites.needs_reinit(frontend=True):
  473. _init(name=config.app_name, loglevel=loglevel)
  474. prerequisites.check_latest_package_version(constants.ReflexHostingCLI.MODULE_NAME)
  475. hosting_cli.deploy(
  476. app_name=app_name,
  477. export_fn=lambda zip_dest_dir,
  478. api_url,
  479. deploy_url,
  480. frontend,
  481. backend,
  482. zipping: export_utils.export(
  483. zip_dest_dir=zip_dest_dir,
  484. api_url=api_url,
  485. deploy_url=deploy_url,
  486. frontend=frontend,
  487. backend=backend,
  488. zipping=zipping,
  489. loglevel=loglevel.subprocess_level(),
  490. upload_db_file=upload_db_file,
  491. ),
  492. key=key,
  493. regions=regions,
  494. envs=envs,
  495. cpus=cpus,
  496. memory_mb=memory_mb,
  497. auto_start=auto_start,
  498. auto_stop=auto_stop,
  499. frontend_hostname=frontend_hostname,
  500. interactive=interactive,
  501. with_metrics=with_metrics,
  502. with_tracing=with_tracing,
  503. loglevel=loglevel.subprocess_level(),
  504. )
  505. cli.add_typer(db_cli, name="db", help="Subcommands for managing the database schema.")
  506. cli.add_typer(script_cli, name="script", help="Subcommands running helper scripts.")
  507. cli.add_typer(
  508. deployments_cli,
  509. name="deployments",
  510. help="Subcommands for managing the Deployments.",
  511. )
  512. cli.add_typer(
  513. custom_components_cli,
  514. name="component",
  515. help="Subcommands for creating and publishing Custom Components.",
  516. )
  517. if __name__ == "__main__":
  518. cli()