"""Pynecone CLI to create, run, and deploy apps.""" import os from pathlib import Path import httpx import typer from pynecone import constants, utils from rich.prompt import Prompt # Create the app. cli = typer.Typer() @cli.command() def version(): """Get the Pynecone version.""" utils.console.print(constants.VERSION) @cli.command() def init(): """Initialize a new Pynecone app in the current directory.""" app_name = utils.get_default_app_name() # Make sure they don't name the app "pynecone". if app_name == constants.MODULE_NAME: utils.console.print( f"[red]The app directory cannot be named [bold]{constants.MODULE_NAME}." ) raise typer.Exit() with utils.console.status(f"[bold]Initializing {app_name}"): # Set up the web directory. utils.install_bun() utils.initialize_web_directory() # Set up the app directory, only if the config doesn't exist. if not os.path.exists(constants.CONFIG_FILE): utils.create_config(app_name) utils.initialize_app_directory(app_name) # Initialize the .gitignore. utils.initialize_gitignore() # Finish initializing the app. utils.console.log(f"[bold green]Finished Initializing: {app_name}") @cli.command() def run( env: constants.Env = typer.Option( constants.Env.DEV, help="The environment to run the app in." ), frontend: bool = typer.Option(True, help="Whether to run the frontend."), backend: bool = typer.Option(True, help="Whether to run the backend."), loglevel: constants.LogLevel = typer.Option( constants.LogLevel.ERROR, help="The log level to use." ), port: str = typer.Option(None, help="Specify a different port."), ): """Run the app in the current directory.""" frontend_port = utils.get_config().port if port is None else port backend_port = utils.get_api_port() # If something is running on the ports, ask the user if they want to kill or change it. if utils.is_process_on_port(frontend_port): frontend_port = utils.change_or_terminate_port(frontend_port, "frontend") if utils.is_process_on_port(backend_port): backend_port = utils.change_or_terminate_port(backend_port, "backend") # Check that the app is initialized. if frontend and not utils.is_initialized(): utils.console.print( "[red]The app is not initialized. Run [bold]pc init[/bold] first." ) raise typer.Exit() # Check that the template is up to date. if frontend and not utils.is_latest_template(): utils.console.print( "[red]The base app template has updated. Run [bold]pc init[/bold] again." ) raise typer.Exit() # Get the app module. utils.console.rule("[bold]Starting Pynecone App") app = utils.get_app() # Get the frontend and backend commands, based on the environment. frontend_cmd = backend_cmd = None if env == constants.Env.DEV: frontend_cmd, backend_cmd = utils.run_frontend, utils.run_backend if env == constants.Env.PROD: frontend_cmd, backend_cmd = utils.run_frontend_prod, utils.run_backend_prod assert frontend_cmd and backend_cmd, "Invalid env" # Run the frontend and backend. try: if frontend: frontend_cmd(app.app, Path.cwd(), frontend_port) if backend: backend_cmd(app.__name__, port=int(backend_port), loglevel=loglevel) finally: utils.kill_process_on_port(frontend_port) utils.kill_process_on_port(backend_port) @cli.command() def deploy(dry_run: bool = typer.Option(False, help="Whether to run a dry run.")): """Deploy the app to the Pynecone hosting service.""" # Get the app config. config = utils.get_config() config.api_url = utils.get_production_backend_url() # Check if the deploy url is set. if config.deploy_url is None: typer.echo("This feature is coming soon!") return # Compile the app in production mode. typer.echo("Compiling production app") app = utils.get_app().app utils.export_app(app, zip=True) # Exit early if this is a dry run. if dry_run: return # Deploy the app. data = {"userId": config.username, "projectId": config.app_name} original_response = httpx.get(config.deploy_url, params=data) response = original_response.json() frontend = response["frontend_resources_url"] backend = response["backend_resources_url"] # Upload the frontend and backend. with open(constants.FRONTEND_ZIP, "rb") as f: response = httpx.put(frontend, data=f) # type: ignore with open(constants.BACKEND_ZIP, "rb") as f: response = httpx.put(backend, data=f) # type: ignore @cli.command() def export(): """Export the app to a zip file.""" # Get the app config. config = utils.get_config() config.api_url = utils.get_production_backend_url() # Compile the app in production mode and export it. utils.console.rule("[bold]Compiling production app and preparing for export.") app = utils.get_app().app utils.export_app(app, zip=True) utils.console.rule( "Backend & Frontend compiled. See [green bold]backend.zip[/green bold] and [green bold]frontend.zip[/green bold]." ) main = cli if __name__ == "__main__": main()