reflex.py 16 KB

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