pc.py 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. """Pynecone CLI to create, run, and deploy apps."""
  2. import os
  3. from pathlib import Path
  4. import httpx
  5. import typer
  6. from pynecone import constants, utils
  7. from rich.prompt import Prompt
  8. # Create the app.
  9. cli = typer.Typer()
  10. @cli.command()
  11. def version():
  12. """Get the Pynecone version."""
  13. utils.console.print(constants.VERSION)
  14. @cli.command()
  15. def init():
  16. """Initialize a new Pynecone app in the current directory."""
  17. app_name = utils.get_default_app_name()
  18. # Make sure they don't name the app "pynecone".
  19. if app_name == constants.MODULE_NAME:
  20. utils.console.print(
  21. f"[red]The app directory cannot be named [bold]{constants.MODULE_NAME}."
  22. )
  23. raise typer.Exit()
  24. with utils.console.status(f"[bold]Initializing {app_name}"):
  25. # Set up the web directory.
  26. utils.install_bun()
  27. utils.initialize_web_directory()
  28. # Set up the app directory, only if the config doesn't exist.
  29. if not os.path.exists(constants.CONFIG_FILE):
  30. utils.create_config(app_name)
  31. utils.initialize_app_directory(app_name)
  32. # Initialize the .gitignore.
  33. utils.initialize_gitignore()
  34. # Finish initializing the app.
  35. utils.console.log(f"[bold green]Finished Initializing: {app_name}")
  36. @cli.command()
  37. def run(
  38. env: constants.Env = typer.Option(
  39. constants.Env.DEV, help="The environment to run the app in."
  40. ),
  41. frontend: bool = typer.Option(True, help="Whether to run the frontend."),
  42. backend: bool = typer.Option(True, help="Whether to run the backend."),
  43. loglevel: constants.LogLevel = typer.Option(
  44. constants.LogLevel.ERROR, help="The log level to use."
  45. ),
  46. port: str = typer.Option(None, help="Specify a different port."),
  47. ):
  48. """Run the app in the current directory."""
  49. frontend_port = utils.get_config().port if port is None else port
  50. backend_port = utils.get_api_port()
  51. # If something is running on the ports, ask the user if they want to kill or change it.
  52. if utils.is_process_on_port(frontend_port):
  53. frontend_port = utils.change_or_terminate_port(frontend_port, "frontend")
  54. if utils.is_process_on_port(backend_port):
  55. backend_port = utils.change_or_terminate_port(backend_port, "backend")
  56. # Check that the app is initialized.
  57. if frontend and not utils.is_initialized():
  58. utils.console.print(
  59. "[red]The app is not initialized. Run [bold]pc init[/bold] first."
  60. )
  61. raise typer.Exit()
  62. # Check that the template is up to date.
  63. if frontend and not utils.is_latest_template():
  64. utils.console.print(
  65. "[red]The base app template has updated. Run [bold]pc init[/bold] again."
  66. )
  67. raise typer.Exit()
  68. # Get the app module.
  69. utils.console.rule("[bold]Starting Pynecone App")
  70. app = utils.get_app()
  71. # Get the frontend and backend commands, based on the environment.
  72. frontend_cmd = backend_cmd = None
  73. if env == constants.Env.DEV:
  74. frontend_cmd, backend_cmd = utils.run_frontend, utils.run_backend
  75. if env == constants.Env.PROD:
  76. frontend_cmd, backend_cmd = utils.run_frontend_prod, utils.run_backend_prod
  77. assert frontend_cmd and backend_cmd, "Invalid env"
  78. # Run the frontend and backend.
  79. try:
  80. if frontend:
  81. frontend_cmd(app.app, Path.cwd(), frontend_port)
  82. if backend:
  83. backend_cmd(app.__name__, port=int(backend_port), loglevel=loglevel)
  84. finally:
  85. utils.kill_process_on_port(frontend_port)
  86. utils.kill_process_on_port(backend_port)
  87. @cli.command()
  88. def deploy(dry_run: bool = typer.Option(False, help="Whether to run a dry run.")):
  89. """Deploy the app to the Pynecone hosting service."""
  90. # Get the app config.
  91. config = utils.get_config()
  92. config.api_url = utils.get_production_backend_url()
  93. # Check if the deploy url is set.
  94. if config.deploy_url is None:
  95. typer.echo("This feature is coming soon!")
  96. return
  97. # Compile the app in production mode.
  98. typer.echo("Compiling production app")
  99. app = utils.get_app().app
  100. utils.export_app(app, zip=True)
  101. # Exit early if this is a dry run.
  102. if dry_run:
  103. return
  104. # Deploy the app.
  105. data = {"userId": config.username, "projectId": config.app_name}
  106. original_response = httpx.get(config.deploy_url, params=data)
  107. response = original_response.json()
  108. frontend = response["frontend_resources_url"]
  109. backend = response["backend_resources_url"]
  110. # Upload the frontend and backend.
  111. with open(constants.FRONTEND_ZIP, "rb") as f:
  112. response = httpx.put(frontend, data=f) # type: ignore
  113. with open(constants.BACKEND_ZIP, "rb") as f:
  114. response = httpx.put(backend, data=f) # type: ignore
  115. @cli.command()
  116. def export():
  117. """Export the app to a zip file."""
  118. # Get the app config.
  119. config = utils.get_config()
  120. config.api_url = utils.get_production_backend_url()
  121. # Compile the app in production mode and export it.
  122. utils.console.rule("[bold]Compiling production app and preparing for export.")
  123. app = utils.get_app().app
  124. utils.export_app(app, zip=True)
  125. utils.console.rule(
  126. "Backend & Frontend compiled. See [green bold]backend.zip[/green bold] and [green bold]frontend.zip[/green bold]."
  127. )
  128. main = cli
  129. if __name__ == "__main__":
  130. main()