pc.py 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. """Pynecone CLI to create, run, and deploy apps."""
  2. import os
  3. import platform
  4. import threading
  5. from pathlib import Path
  6. import httpx
  7. import typer
  8. from pynecone import constants
  9. from pynecone.config import get_config
  10. from pynecone.utils import build, console, exec, prerequisites, processes, telemetry
  11. # Create the app.
  12. cli = typer.Typer()
  13. @cli.command()
  14. def version():
  15. """Get the Pynecone version."""
  16. console.print(constants.VERSION)
  17. @cli.command()
  18. def init(
  19. name: str = typer.Option(None, help="Name of the app to be initialized."),
  20. template: constants.Template = typer.Option(
  21. constants.Template.DEFAULT, help="Template to use for the app."
  22. ),
  23. ):
  24. """Initialize a new Pynecone app in the current directory."""
  25. app_name = prerequisites.get_default_app_name() if name is None else name
  26. # Make sure they don't name the app "pynecone".
  27. if app_name == constants.MODULE_NAME:
  28. console.print(
  29. f"[red]The app directory cannot be named [bold]{constants.MODULE_NAME}."
  30. )
  31. raise typer.Exit()
  32. console.rule(f"[bold]Initializing {app_name}")
  33. # Set up the web directory.
  34. prerequisites.validate_and_install_bun()
  35. prerequisites.initialize_web_directory()
  36. # Set up the app directory, only if the config doesn't exist.
  37. if not os.path.exists(constants.CONFIG_FILE):
  38. prerequisites.create_config(app_name)
  39. prerequisites.initialize_app_directory(app_name, template)
  40. build.set_pynecone_project_hash()
  41. telemetry.send("init", get_config().telemetry_enabled)
  42. else:
  43. build.set_pynecone_project_hash()
  44. telemetry.send("reinit", get_config().telemetry_enabled)
  45. # Initialize the .gitignore.
  46. prerequisites.initialize_gitignore()
  47. # Finish initializing the app.
  48. console.log(f"[bold green]Finished Initializing: {app_name}")
  49. @cli.command()
  50. def run(
  51. env: constants.Env = typer.Option(
  52. constants.Env.DEV, help="The environment to run the app in."
  53. ),
  54. frontend: bool = typer.Option(
  55. False, "--frontend-only", help="Execute only frontend."
  56. ),
  57. backend: bool = typer.Option(False, "--backend-only", help="Execute only backend."),
  58. loglevel: constants.LogLevel = typer.Option(
  59. constants.LogLevel.ERROR, help="The log level to use."
  60. ),
  61. frontend_port: str = typer.Option(None, help="Specify a different frontend port."),
  62. backend_port: str = typer.Option(None, help="Specify a different backend port."),
  63. backend_host: str = typer.Option(None, help="Specify the backend host."),
  64. ):
  65. """Run the app in the current directory."""
  66. if platform.system() == "Windows":
  67. console.print(
  68. "[yellow][WARNING] We strongly advise you to use Windows Subsystem for Linux (WSL) for optimal performance when using Pynecone. Due to compatibility issues with one of our dependencies, Bun, you may experience slower performance on Windows. By using WSL, you can expect to see a significant speed increase."
  69. )
  70. # Set ports as os env variables to take precedence over config and
  71. # .env variables(if override_os_envs flag in config is set to False).
  72. build.set_os_env(
  73. frontend_port=frontend_port,
  74. backend_port=backend_port,
  75. backend_host=backend_host,
  76. )
  77. frontend_port = (
  78. get_config().frontend_port if frontend_port is None else frontend_port
  79. )
  80. backend_port = get_config().backend_port if backend_port is None else backend_port
  81. backend_host = get_config().backend_host if backend_host is None else backend_host
  82. # If no --frontend-only and no --backend-only, then turn on frontend and backend both
  83. if not frontend and not backend:
  84. frontend = True
  85. backend = True
  86. # If something is running on the ports, ask the user if they want to kill or change it.
  87. if frontend and processes.is_process_on_port(frontend_port):
  88. frontend_port = processes.change_or_terminate_port(frontend_port, "frontend")
  89. if backend and processes.is_process_on_port(backend_port):
  90. backend_port = processes.change_or_terminate_port(backend_port, "backend")
  91. # Check that the app is initialized.
  92. if frontend and not prerequisites.is_initialized():
  93. console.print(
  94. "[red]The app is not initialized. Run [bold]pc init[/bold] first."
  95. )
  96. raise typer.Exit()
  97. # Check that the template is up to date.
  98. if frontend and not prerequisites.is_latest_template():
  99. console.print(
  100. "[red]The base app template has updated. Run [bold]pc init[/bold] again."
  101. )
  102. raise typer.Exit()
  103. # Get the app module.
  104. console.rule("[bold]Starting Pynecone App")
  105. app = prerequisites.get_app()
  106. # Get the frontend and backend commands, based on the environment.
  107. frontend_cmd = backend_cmd = None
  108. if env == constants.Env.DEV:
  109. frontend_cmd, backend_cmd = exec.run_frontend, exec.run_backend
  110. if env == constants.Env.PROD:
  111. frontend_cmd, backend_cmd = exec.run_frontend_prod, exec.run_backend_prod
  112. assert frontend_cmd and backend_cmd, "Invalid env"
  113. # Post a telemetry event.
  114. telemetry.send(f"run-{env.value}", get_config().telemetry_enabled)
  115. # Run the frontend and backend.
  116. if backend:
  117. threading.Thread(
  118. target=backend_cmd,
  119. args=(app.__name__, backend_host, backend_port, loglevel),
  120. ).start()
  121. if frontend:
  122. threading.Thread(
  123. target=frontend_cmd, args=(app.app, Path.cwd(), frontend_port)
  124. ).start()
  125. @cli.command()
  126. def deploy(dry_run: bool = typer.Option(False, help="Whether to run a dry run.")):
  127. """Deploy the app to the Pynecone hosting service."""
  128. # Get the app config.
  129. config = get_config()
  130. config.api_url = prerequisites.get_production_backend_url()
  131. # Check if the deploy url is set.
  132. if config.pcdeploy_url is None:
  133. typer.echo("This feature is coming soon!")
  134. return
  135. # Compile the app in production mode.
  136. typer.echo("Compiling production app")
  137. app = prerequisites.get_app().app
  138. build.export_app(app, zip=True, deploy_url=config.deploy_url)
  139. # Exit early if this is a dry run.
  140. if dry_run:
  141. return
  142. # Deploy the app.
  143. data = {"userId": config.username, "projectId": config.app_name}
  144. original_response = httpx.get(config.pcdeploy_url, params=data)
  145. response = original_response.json()
  146. frontend = response["frontend_resources_url"]
  147. backend = response["backend_resources_url"]
  148. # Upload the frontend and backend.
  149. with open(constants.FRONTEND_ZIP, "rb") as f:
  150. httpx.put(frontend, data=f) # type: ignore
  151. with open(constants.BACKEND_ZIP, "rb") as f:
  152. httpx.put(backend, data=f) # type: ignore
  153. @cli.command()
  154. def export(
  155. zipping: bool = typer.Option(
  156. True, "--no-zip", help="Disable zip for backend and frontend exports."
  157. ),
  158. frontend: bool = typer.Option(
  159. True, "--backend-only", help="Export only backend.", show_default=False
  160. ),
  161. backend: bool = typer.Option(
  162. True, "--frontend-only", help="Export only frontend.", show_default=False
  163. ),
  164. for_pc_deploy: bool = typer.Option(
  165. False,
  166. "--for-pc-deploy",
  167. help="Whether export the app for Pynecone Deploy Service.",
  168. ),
  169. ):
  170. """Export the app to a zip file."""
  171. config = get_config()
  172. if for_pc_deploy:
  173. # Get the app config and modify the api_url base on username and app_name.
  174. config.api_url = prerequisites.get_production_backend_url()
  175. # Compile the app in production mode and export it.
  176. console.rule("[bold]Compiling production app and preparing for export.")
  177. app = prerequisites.get_app().app
  178. build.export_app(
  179. app,
  180. backend=backend,
  181. frontend=frontend,
  182. zip=zipping,
  183. deploy_url=config.deploy_url,
  184. )
  185. # Post a telemetry event.
  186. telemetry.send("export", get_config().telemetry_enabled)
  187. if zipping:
  188. console.rule(
  189. """Backend & Frontend compiled. See [green bold]backend.zip[/green bold]
  190. and [green bold]frontend.zip[/green bold]."""
  191. )
  192. else:
  193. console.rule(
  194. """Backend & Frontend compiled. See [green bold]app[/green bold]
  195. and [green bold].web/_static[/green bold] directories."""
  196. )
  197. main = cli
  198. if __name__ == "__main__":
  199. main()