From 1b42eea1edcba5bfb1b8f613886a259359c36b29 Mon Sep 17 00:00:00 2001 From: Elijah Date: Wed, 13 Nov 2024 15:37:19 +0000 Subject: [PATCH 01/12] Rework Init workflow --- reflex/constants/base.py | 4 + reflex/reflex.py | 24 +----- reflex/utils/prerequisites.py | 139 ++++++++++++++++++++++++++-------- 3 files changed, 112 insertions(+), 55 deletions(-) diff --git a/reflex/constants/base.py b/reflex/constants/base.py index 798ac7dc68..caf3f24024 100644 --- a/reflex/constants/base.py +++ b/reflex/constants/base.py @@ -97,6 +97,10 @@ class Templates(SimpleNamespace): # The default template DEFAULT = "blank" + AI = "ai" + + CHOOSE_TEMPLATES = "choose templates" + # The reflex.build frontend host REFLEX_BUILD_FRONTEND = "https://flexgen.reflex.run" diff --git a/reflex/reflex.py b/reflex/reflex.py index b0aa510904..69f2b02716 100644 --- a/reflex/reflex.py +++ b/reflex/reflex.py @@ -89,30 +89,8 @@ def _init( # Set up the web project. prerequisites.initialize_frontend_dependencies() - # Integrate with reflex.build. - generation_hash = None - if ai: - if template is None: - # If AI is requested and no template specified, redirect the user to reflex.build. - generation_hash = redir.reflex_build_redirect() - elif prerequisites.is_generation_hash(template): - # Otherwise treat the template as a generation hash. - generation_hash = template - else: - console.error( - "Cannot use `--template` option with `--ai` option. Please remove `--template` option." - ) - raise typer.Exit(2) - template = constants.Templates.DEFAULT - # Initialize the app. - template = prerequisites.initialize_app(app_name, template) - - # If a reflex.build generation hash is available, download the code and apply it to the main module. - if generation_hash: - prerequisites.initialize_main_module_index_from_generation( - app_name, generation_hash=generation_hash - ) + template = prerequisites.initialize_app(app_name, template, ai) # Initialize the .gitignore. prerequisites.initialize_gitignore() diff --git a/reflex/utils/prerequisites.py b/reflex/utils/prerequisites.py index b2b3b7f3b2..25b5addaca 100644 --- a/reflex/utils/prerequisites.py +++ b/reflex/utils/prerequisites.py @@ -34,7 +34,7 @@ from reflex import constants, model from reflex.compiler import templates from reflex.config import Config, environment, get_config -from reflex.utils import console, net, path_ops, processes +from reflex.utils import console, net, path_ops, processes, redir from reflex.utils.exceptions import ( GeneratedCodeHasNoFunctionDefs, raise_system_package_missing_error, @@ -218,10 +218,10 @@ def get_install_package_manager(on_failure_return_none: bool = False) -> str | N The path to the package manager. """ if ( - constants.IS_WINDOWS - and not is_windows_bun_supported() - or windows_check_onedrive_in_path() - or windows_npm_escape_hatch() + constants.IS_WINDOWS + and not is_windows_bun_supported() + or windows_check_onedrive_in_path() + or windows_npm_escape_hatch() ): return get_package_manager(on_failure_return_none) return str(get_config().bun_path) @@ -443,8 +443,8 @@ def create_config(app_name: str): def initialize_gitignore( - gitignore_file: Path = constants.GitIgnore.FILE, - files_to_ignore: set[str] | list[str] = constants.GitIgnore.DEFAULTS, + gitignore_file: Path = constants.GitIgnore.FILE, + files_to_ignore: set[str] | list[str] = constants.GitIgnore.DEFAULTS, ): """Initialize the template .gitignore file. @@ -510,10 +510,10 @@ def initialize_requirements_txt(): def initialize_app_directory( - app_name: str, - template_name: str = constants.Templates.DEFAULT, - template_code_dir_name: str | None = None, - template_dir: Path | None = None, + app_name: str, + template_name: str = constants.Templates.DEFAULT, + template_code_dir_name: str | None = None, + template_dir: Path | None = None, ): """Initialize the app directory on reflex init. @@ -687,7 +687,7 @@ def update_next_config(export=False, transpile_packages: Optional[List[str]] = N def _update_next_config( - config: Config, export: bool = False, transpile_packages: Optional[List[str]] = None + config: Config, export: bool = False, transpile_packages: Optional[List[str]] = None ): next_config = { "basePath": config.frontend_path or "", @@ -844,7 +844,7 @@ def install_bun(): # Skip if bun is already installed. if Path(get_config().bun_path).exists() and get_bun_version() == version.parse( - constants.Bun.VERSION + constants.Bun.VERSION ): console.debug("Skipping bun installation as it is already installed.") return @@ -943,16 +943,16 @@ def install_frontend_packages(packages: set[str], config: Config): fallback_command = ( get_package_manager(on_failure_return_none=True) if ( - not constants.IS_WINDOWS - or constants.IS_WINDOWS - and is_windows_bun_supported() - and not windows_check_onedrive_in_path() + not constants.IS_WINDOWS + or constants.IS_WINDOWS + and is_windows_bun_supported() + and not windows_check_onedrive_in_path() ) else None ) install_package_manager = ( - get_install_package_manager(on_failure_return_none=True) or fallback_command + get_install_package_manager(on_failure_return_none=True) or fallback_command ) if install_package_manager is None: @@ -1178,8 +1178,8 @@ def check_db_initialized() -> bool: True if alembic is initialized (or if database is not used). """ if ( - get_config().db_url is not None - and not environment.ALEMBIC_CONFIG.get().exists() + get_config().db_url is not None + and not environment.ALEMBIC_CONFIG.get().exists() ): console.error( "Database is not initialized. Run [bold]reflex db init[/bold] first." @@ -1195,8 +1195,8 @@ def check_schema_up_to_date(): with model.Model.get_db_engine().connect() as connection: try: if model.Model.alembic_autogenerate( - connection=connection, - write_migration_scripts=False, + connection=connection, + write_migration_scripts=False, ): console.error( "Detected database schema changes. Run [bold]reflex db makemigrations[/bold] " @@ -1378,7 +1378,31 @@ def create_config_init_app_from_remote_template(app_name: str, template_url: str shutil.rmtree(unzip_dir) -def initialize_app(app_name: str, template: str | None = None) -> str | None: +def prompt_templates(templates: dict[str, Template]) -> str: + while True: + console.print("visit https://reflex.dev/templates for the complete list of templates.") + answer = console.ask("Enter a valid template name", show_choices=False) + if not answer in templates: + console.error("Invalid template name. Please try again.") + else: + return answer + + +def use_ai_generation(template: str | None = None) -> str: + if template is None: + # If AI is requested and no template specified, redirect the user to reflex.build. + return redir.reflex_build_redirect() + elif is_generation_hash(template): + # Otherwise treat the template as a generation hash. + return template + else: + console.error( + "Cannot use `--template` option with `--ai` option. Please remove `--template` option." + ) + raise typer.Exit(2) + + +def initialize_app(app_name: str, template: str | None = None, ai: bool = False) -> str | None: """Initialize the app either from a remote template or a blank app. If the config file exists, it is considered as reinit. Args: @@ -1399,10 +1423,15 @@ def initialize_app(app_name: str, template: str | None = None) -> str | None: telemetry.send("reinit") return + generation_hash = None + if ai: + generation_hash = use_ai_generation(template) + template = constants.Templates.AI + templates: dict[str, Template] = {} # Don't fetch app templates if the user directly asked for DEFAULT. - if template is None or (template != constants.Templates.DEFAULT): + if template is not None and (template != constants.Templates.DEFAULT or template != constants.Templates.AI): try: # Get the available templates templates = fetch_app_templates(constants.Reflex.VERSION) @@ -1414,14 +1443,32 @@ def initialize_app(app_name: str, template: str | None = None) -> str | None: finally: template = template or constants.Templates.DEFAULT + if template is None: + + template = prompt_for_template(get_init_cli_options()) + if template == constants.Templates.AI: + generation_hash = use_ai_generation() + elif template == constants.Templates.CHOOSE_TEMPLATES: + try: + # Get the available templates + templates = fetch_app_templates(constants.Reflex.VERSION) + # default to the blank template if no templates are available + template = prompt_templates(templates) if len(templates) > 0 else constants.Templates.DEFAULT + except Exception as e: + console.warn("Failed to fetch templates. Falling back to default template.") + console.debug(f"Error while fetching templates: {e}") + template = constants.Templates.DEFAULT + + else: + console.error("Invalid option selected.") + raise typer.Exit(2) + # If the blank template is selected, create a blank app. - if template == constants.Templates.DEFAULT: + if template == constants.Templates.DEFAULT or template == constants.Templates.AI: # Default app creation behavior: a blank app. create_config(app_name) initialize_app_directory(app_name) else: - # Fetch App templates from the backend server. - console.debug(f"Available templates: {templates}") # If user selects a template, it needs to exist. if template in templates: @@ -1444,9 +1491,37 @@ def initialize_app(app_name: str, template: str | None = None) -> str | None: ) telemetry.send("init", template=template) + # If a reflex.build generation hash is available, download the code and apply it to the main module. + if generation_hash: + initialize_main_module_index_from_generation( + app_name, generation_hash=generation_hash + ) + return template +def fetch_and_prompt_for_templates(template: str | None, templates: dict[str, Template]) -> str: + """Fetches available templates and prompts the user if template is not specified.""" + try: + templates = fetch_app_templates(constants.Reflex.VERSION) + if not template and templates: + template = prompt_for_template(list(templates.values())) + except Exception as e: + console.warn("Failed to fetch templates. Falling back to default template.") + console.debug(f"Error while fetching templates: {e}") + return template or constants.Templates.DEFAULT + + +def get_init_cli_options() -> list[Template]: + return [ + Template(name=constants.Templates.DEFAULT, description="A blank Reflex app.", demo_url="", code_url=""), + Template(name=constants.Templates.AI, description="Generate a template using AI(Flexgen)", demo_url="https://flexgen.reflex.run", + code_url=""), + Template(name=constants.Templates.CHOOSE_TEMPLATES, description="Choose an existing template.", demo_url="https://reflex.dev/templates", + code_url=""), + ] + + def initialize_main_module_index_from_generation(app_name: str, generation_hash: str): """Overwrite the `index` function in the main module with reflex.build generated code. @@ -1588,11 +1663,11 @@ def is_windows_bun_supported() -> bool: """ cpu_info = get_cpu_info() return ( - constants.IS_WINDOWS - and cpu_info is not None - and cpu_info.address_width == 64 - and cpu_info.model_name is not None - and "ARM" not in cpu_info.model_name + constants.IS_WINDOWS + and cpu_info is not None + and cpu_info.address_width == 64 + and cpu_info.model_name is not None + and "ARM" not in cpu_info.model_name ) From aa4570175e3d882834863d1254407079e205ef42 Mon Sep 17 00:00:00 2001 From: Elijah Date: Wed, 13 Nov 2024 15:40:37 +0000 Subject: [PATCH 02/12] minor format --- reflex/utils/prerequisites.py | 103 +++++++++++++++++++++------------- 1 file changed, 64 insertions(+), 39 deletions(-) diff --git a/reflex/utils/prerequisites.py b/reflex/utils/prerequisites.py index 25b5addaca..59a7bf6c4c 100644 --- a/reflex/utils/prerequisites.py +++ b/reflex/utils/prerequisites.py @@ -218,10 +218,10 @@ def get_install_package_manager(on_failure_return_none: bool = False) -> str | N The path to the package manager. """ if ( - constants.IS_WINDOWS - and not is_windows_bun_supported() - or windows_check_onedrive_in_path() - or windows_npm_escape_hatch() + constants.IS_WINDOWS + and not is_windows_bun_supported() + or windows_check_onedrive_in_path() + or windows_npm_escape_hatch() ): return get_package_manager(on_failure_return_none) return str(get_config().bun_path) @@ -443,8 +443,8 @@ def create_config(app_name: str): def initialize_gitignore( - gitignore_file: Path = constants.GitIgnore.FILE, - files_to_ignore: set[str] | list[str] = constants.GitIgnore.DEFAULTS, + gitignore_file: Path = constants.GitIgnore.FILE, + files_to_ignore: set[str] | list[str] = constants.GitIgnore.DEFAULTS, ): """Initialize the template .gitignore file. @@ -510,10 +510,10 @@ def initialize_requirements_txt(): def initialize_app_directory( - app_name: str, - template_name: str = constants.Templates.DEFAULT, - template_code_dir_name: str | None = None, - template_dir: Path | None = None, + app_name: str, + template_name: str = constants.Templates.DEFAULT, + template_code_dir_name: str | None = None, + template_dir: Path | None = None, ): """Initialize the app directory on reflex init. @@ -687,7 +687,7 @@ def update_next_config(export=False, transpile_packages: Optional[List[str]] = N def _update_next_config( - config: Config, export: bool = False, transpile_packages: Optional[List[str]] = None + config: Config, export: bool = False, transpile_packages: Optional[List[str]] = None ): next_config = { "basePath": config.frontend_path or "", @@ -844,7 +844,7 @@ def install_bun(): # Skip if bun is already installed. if Path(get_config().bun_path).exists() and get_bun_version() == version.parse( - constants.Bun.VERSION + constants.Bun.VERSION ): console.debug("Skipping bun installation as it is already installed.") return @@ -943,16 +943,16 @@ def install_frontend_packages(packages: set[str], config: Config): fallback_command = ( get_package_manager(on_failure_return_none=True) if ( - not constants.IS_WINDOWS - or constants.IS_WINDOWS - and is_windows_bun_supported() - and not windows_check_onedrive_in_path() + not constants.IS_WINDOWS + or constants.IS_WINDOWS + and is_windows_bun_supported() + and not windows_check_onedrive_in_path() ) else None ) install_package_manager = ( - get_install_package_manager(on_failure_return_none=True) or fallback_command + get_install_package_manager(on_failure_return_none=True) or fallback_command ) if install_package_manager is None: @@ -1178,8 +1178,8 @@ def check_db_initialized() -> bool: True if alembic is initialized (or if database is not used). """ if ( - get_config().db_url is not None - and not environment.ALEMBIC_CONFIG.get().exists() + get_config().db_url is not None + and not environment.ALEMBIC_CONFIG.get().exists() ): console.error( "Database is not initialized. Run [bold]reflex db init[/bold] first." @@ -1195,8 +1195,8 @@ def check_schema_up_to_date(): with model.Model.get_db_engine().connect() as connection: try: if model.Model.alembic_autogenerate( - connection=connection, - write_migration_scripts=False, + connection=connection, + write_migration_scripts=False, ): console.error( "Detected database schema changes. Run [bold]reflex db makemigrations[/bold] " @@ -1380,7 +1380,9 @@ def create_config_init_app_from_remote_template(app_name: str, template_url: str def prompt_templates(templates: dict[str, Template]) -> str: while True: - console.print("visit https://reflex.dev/templates for the complete list of templates.") + console.print( + "visit https://reflex.dev/templates for the complete list of templates." + ) answer = console.ask("Enter a valid template name", show_choices=False) if not answer in templates: console.error("Invalid template name. Please try again.") @@ -1402,7 +1404,9 @@ def use_ai_generation(template: str | None = None) -> str: raise typer.Exit(2) -def initialize_app(app_name: str, template: str | None = None, ai: bool = False) -> str | None: +def initialize_app( + app_name: str, template: str | None = None, ai: bool = False +) -> str | None: """Initialize the app either from a remote template or a blank app. If the config file exists, it is considered as reinit. Args: @@ -1431,7 +1435,9 @@ def initialize_app(app_name: str, template: str | None = None, ai: bool = False) templates: dict[str, Template] = {} # Don't fetch app templates if the user directly asked for DEFAULT. - if template is not None and (template != constants.Templates.DEFAULT or template != constants.Templates.AI): + if template is not None and ( + template != constants.Templates.DEFAULT or template != constants.Templates.AI + ): try: # Get the available templates templates = fetch_app_templates(constants.Reflex.VERSION) @@ -1444,7 +1450,6 @@ def initialize_app(app_name: str, template: str | None = None, ai: bool = False) template = template or constants.Templates.DEFAULT if template is None: - template = prompt_for_template(get_init_cli_options()) if template == constants.Templates.AI: generation_hash = use_ai_generation() @@ -1453,9 +1458,15 @@ def initialize_app(app_name: str, template: str | None = None, ai: bool = False) # Get the available templates templates = fetch_app_templates(constants.Reflex.VERSION) # default to the blank template if no templates are available - template = prompt_templates(templates) if len(templates) > 0 else constants.Templates.DEFAULT + template = ( + prompt_templates(templates) + if len(templates) > 0 + else constants.Templates.DEFAULT + ) except Exception as e: - console.warn("Failed to fetch templates. Falling back to default template.") + console.warn( + "Failed to fetch templates. Falling back to default template." + ) console.debug(f"Error while fetching templates: {e}") template = constants.Templates.DEFAULT @@ -1469,7 +1480,6 @@ def initialize_app(app_name: str, template: str | None = None, ai: bool = False) create_config(app_name) initialize_app_directory(app_name) else: - # If user selects a template, it needs to exist. if template in templates: template_url = templates[template].code_url @@ -1500,7 +1510,9 @@ def initialize_app(app_name: str, template: str | None = None, ai: bool = False) return template -def fetch_and_prompt_for_templates(template: str | None, templates: dict[str, Template]) -> str: +def fetch_and_prompt_for_templates( + template: str | None, templates: dict[str, Template] +) -> str: """Fetches available templates and prompts the user if template is not specified.""" try: templates = fetch_app_templates(constants.Reflex.VERSION) @@ -1514,11 +1526,24 @@ def fetch_and_prompt_for_templates(template: str | None, templates: dict[str, Te def get_init_cli_options() -> list[Template]: return [ - Template(name=constants.Templates.DEFAULT, description="A blank Reflex app.", demo_url="", code_url=""), - Template(name=constants.Templates.AI, description="Generate a template using AI(Flexgen)", demo_url="https://flexgen.reflex.run", - code_url=""), - Template(name=constants.Templates.CHOOSE_TEMPLATES, description="Choose an existing template.", demo_url="https://reflex.dev/templates", - code_url=""), + Template( + name=constants.Templates.DEFAULT, + description="A blank Reflex app.", + demo_url="", + code_url="", + ), + Template( + name=constants.Templates.AI, + description="Generate a template using AI(Flexgen)", + demo_url="https://flexgen.reflex.run", + code_url="", + ), + Template( + name=constants.Templates.CHOOSE_TEMPLATES, + description="Choose an existing template.", + demo_url="https://reflex.dev/templates", + code_url="", + ), ] @@ -1663,11 +1688,11 @@ def is_windows_bun_supported() -> bool: """ cpu_info = get_cpu_info() return ( - constants.IS_WINDOWS - and cpu_info is not None - and cpu_info.address_width == 64 - and cpu_info.model_name is not None - and "ARM" not in cpu_info.model_name + constants.IS_WINDOWS + and cpu_info is not None + and cpu_info.address_width == 64 + and cpu_info.model_name is not None + and "ARM" not in cpu_info.model_name ) From 5c7c0a49ded5b0d40eab5192da19e33f7acc9639 Mon Sep 17 00:00:00 2001 From: Elijah Date: Wed, 13 Nov 2024 17:12:05 +0000 Subject: [PATCH 03/12] refactor --- reflex/reflex.py | 4 +- reflex/utils/prerequisites.py | 210 +++++++++++++++++++++------------- 2 files changed, 132 insertions(+), 82 deletions(-) diff --git a/reflex/reflex.py b/reflex/reflex.py index 69f2b02716..1f92e32548 100644 --- a/reflex/reflex.py +++ b/reflex/reflex.py @@ -17,7 +17,7 @@ from reflex.config import environment, get_config from reflex.custom_components.custom_components import custom_components_cli from reflex.state import reset_disk_state_manager -from reflex.utils import console, redir, telemetry +from reflex.utils import console, telemetry # Disable typer+rich integration for help panels typer.core.rich = False # type: ignore @@ -98,7 +98,7 @@ def _init( # Initialize the requirements.txt. prerequisites.initialize_requirements_txt() - template_msg = "" if template else f" using the {template} template" + template_msg = f" using the {template} template" if template else "" # Finish initializing the app. console.success(f"Initialized {app_name}{template_msg}") diff --git a/reflex/utils/prerequisites.py b/reflex/utils/prerequisites.py index 59a7bf6c4c..d86619a782 100644 --- a/reflex/utils/prerequisites.py +++ b/reflex/utils/prerequisites.py @@ -1209,7 +1209,7 @@ def check_schema_up_to_date(): ) -def prompt_for_template(templates: list[Template]) -> str: +def prompt_for_template_options(templates: list[Template]) -> str: """Prompt the user to specify a template. Args: @@ -1378,19 +1378,83 @@ def create_config_init_app_from_remote_template(app_name: str, template_url: str shutil.rmtree(unzip_dir) -def prompt_templates(templates: dict[str, Template]) -> str: +def prompt_for_remote_template_selection(templates: dict[str, Template]) -> str: + """Prompt the user to input a remote template. + + Args: + templates: The available templates. + + Returns: + The selected template. + + Raises: + Exit: If the user does not input a valid template. + """ while True: console.print( - "visit https://reflex.dev/templates for the complete list of templates." + "Visit https://reflex.dev/templates for the complete list of templates." + ) + selected_template = console.ask( + "Enter a valid template name", show_choices=False ) - answer = console.ask("Enter a valid template name", show_choices=False) - if not answer in templates: + if selected_template not in templates: console.error("Invalid template name. Please try again.") else: - return answer + return selected_template + + +def initialize_default_app(app_name: str): + """Initialize the default app. + + Args: + app_name: The name of the app. + """ + create_config(app_name) + initialize_app_directory(app_name) + + +def validate_and_create_app_using_remote_template(app_name, template, templates): + """Validate and create an app using a remote template. + + Args: + app_name: The name of the app. + template: The name of the template. + templates: The available templates. + + Raises: + Exit: If the template is not found. + """ + # If user selects a template, it needs to exist. + if template in templates: + template_url = templates[template].code_url + else: + # Check if the template is a github repo. + if template.startswith("https://github.com"): + template_url = f"{template.strip('/').replace('.git', '')}/archive/main.zip" + else: + console.error(f"Template `{template}` not found.") + raise typer.Exit(1) + if template_url is None: + return + + create_config_init_app_from_remote_template( + app_name=app_name, template_url=template_url + ) -def use_ai_generation(template: str | None = None) -> str: + +def generate_template_using_ai(template: str | None = None) -> str: + """Generate a template using AI(Flexgen). + + Args: + template: The name of the template. + + Returns: + The generation hash. + + Raises: + Exit: If the template and ai flags are used. + """ if template is None: # If AI is requested and no template specified, redirect the user to reflex.build. return redir.reflex_build_redirect() @@ -1404,6 +1468,42 @@ def use_ai_generation(template: str | None = None) -> str: raise typer.Exit(2) +def fetch_and_prompt_with_remote_templates( + template: str, show_prompt: bool = True +) -> tuple[str, dict[str, Template]]: + """Fetch the available remote templates and prompt the user for an input. + + Args: + template: The name of the template. + show_prompt: Whether to show the prompt. + + Returns: + The selected template and the available templates. + """ + available_templates = {} + + try: + # Get the available templates + available_templates = fetch_app_templates(constants.Reflex.VERSION) + + if not show_prompt and template in available_templates: + return template, available_templates + + if not show_prompt and (template not in templates): + console.error(f"{template} is not a valid template name.") + + template = ( + prompt_for_remote_template_selection(available_templates) + if available_templates + else constants.Templates.DEFAULT + ) + except Exception as e: + console.warn("Failed to fetch templates. Falling back to default template.") + console.debug(f"Error while fetching templates: {e}") + + return (template or constants.Templates.DEFAULT), available_templates + + def initialize_app( app_name: str, template: str | None = None, ai: bool = False ) -> str | None: @@ -1412,6 +1512,7 @@ def initialize_app( Args: app_name: The name of the app. template: The name of the template to use. + ai: Whether to use AI to generate the template. Raises: Exit: If template is directly provided in the command flag and is invalid. @@ -1429,102 +1530,51 @@ def initialize_app( generation_hash = None if ai: - generation_hash = use_ai_generation(template) - template = constants.Templates.AI + generation_hash = generate_template_using_ai(template) + template = constants.Templates.DEFAULT templates: dict[str, Template] = {} # Don't fetch app templates if the user directly asked for DEFAULT. - if template is not None and ( - template != constants.Templates.DEFAULT or template != constants.Templates.AI - ): - try: - # Get the available templates - templates = fetch_app_templates(constants.Reflex.VERSION) - if template is None and len(templates) > 0: - template = prompt_for_template(list(templates.values())) - except Exception as e: - console.warn("Failed to fetch templates. Falling back to default template.") - console.debug(f"Error while fetching templates: {e}") - finally: - template = template or constants.Templates.DEFAULT + if template is not None and (template not in (constants.Templates.DEFAULT,)): + template, templates = fetch_and_prompt_with_remote_templates( + template, show_prompt=False + ) if template is None: - template = prompt_for_template(get_init_cli_options()) + template = prompt_for_template_options(get_init_cli_prompt_options()) if template == constants.Templates.AI: - generation_hash = use_ai_generation() + generation_hash = generate_template_using_ai() + # change to the default to allow creation of default app + template = constants.Templates.DEFAULT elif template == constants.Templates.CHOOSE_TEMPLATES: - try: - # Get the available templates - templates = fetch_app_templates(constants.Reflex.VERSION) - # default to the blank template if no templates are available - template = ( - prompt_templates(templates) - if len(templates) > 0 - else constants.Templates.DEFAULT - ) - except Exception as e: - console.warn( - "Failed to fetch templates. Falling back to default template." - ) - console.debug(f"Error while fetching templates: {e}") - template = constants.Templates.DEFAULT - - else: - console.error("Invalid option selected.") - raise typer.Exit(2) + template, templates = fetch_and_prompt_with_remote_templates(template) # If the blank template is selected, create a blank app. - if template == constants.Templates.DEFAULT or template == constants.Templates.AI: + if template in (constants.Templates.DEFAULT,): # Default app creation behavior: a blank app. - create_config(app_name) - initialize_app_directory(app_name) + initialize_default_app(app_name) else: - # If user selects a template, it needs to exist. - if template in templates: - template_url = templates[template].code_url - else: - # Check if the template is a github repo. - if template.startswith("https://github.com"): - template_url = ( - f"{template.strip('/').replace('.git', '')}/archive/main.zip" - ) - else: - console.error(f"Template `{template}` not found.") - raise typer.Exit(1) - - if template_url is None: - return - - create_config_init_app_from_remote_template( - app_name=app_name, template_url=template_url + validate_and_create_app_using_remote_template( + app_name=app_name, template=template, templates=templates ) - telemetry.send("init", template=template) # If a reflex.build generation hash is available, download the code and apply it to the main module. if generation_hash: initialize_main_module_index_from_generation( app_name, generation_hash=generation_hash ) + telemetry.send("init", template=template) return template -def fetch_and_prompt_for_templates( - template: str | None, templates: dict[str, Template] -) -> str: - """Fetches available templates and prompts the user if template is not specified.""" - try: - templates = fetch_app_templates(constants.Reflex.VERSION) - if not template and templates: - template = prompt_for_template(list(templates.values())) - except Exception as e: - console.warn("Failed to fetch templates. Falling back to default template.") - console.debug(f"Error while fetching templates: {e}") - return template or constants.Templates.DEFAULT - +def get_init_cli_prompt_options() -> list[Template]: + """Get the CLI options for initializing a Reflex app. -def get_init_cli_options() -> list[Template]: + Returns: + The CLI options. + """ return [ Template( name=constants.Templates.DEFAULT, @@ -1534,7 +1584,7 @@ def get_init_cli_options() -> list[Template]: ), Template( name=constants.Templates.AI, - description="Generate a template using AI(Flexgen)", + description="Generate a template using AI (Flexgen)", demo_url="https://flexgen.reflex.run", code_url="", ), From a96c4c56379516e4258516ca18c3d8dc8f5f7493 Mon Sep 17 00:00:00 2001 From: Elijah Date: Wed, 13 Nov 2024 17:27:35 +0000 Subject: [PATCH 04/12] add comments --- reflex/constants/base.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/reflex/constants/base.py b/reflex/constants/base.py index caf3f24024..7d51b757b3 100644 --- a/reflex/constants/base.py +++ b/reflex/constants/base.py @@ -97,8 +97,10 @@ class Templates(SimpleNamespace): # The default template DEFAULT = "blank" + # The AI template AI = "ai" + # The option for the user to choose a remote template. CHOOSE_TEMPLATES = "choose templates" # The reflex.build frontend host From 57029471926c215db684d12e814f2e52eb750d2f Mon Sep 17 00:00:00 2001 From: Elijah Date: Wed, 13 Nov 2024 17:54:14 +0000 Subject: [PATCH 05/12] fix pyright alongside some improvements --- reflex/constants/base.py | 2 +- reflex/utils/prerequisites.py | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/reflex/constants/base.py b/reflex/constants/base.py index 7d51b757b3..b50ce50aca 100644 --- a/reflex/constants/base.py +++ b/reflex/constants/base.py @@ -101,7 +101,7 @@ class Templates(SimpleNamespace): AI = "ai" # The option for the user to choose a remote template. - CHOOSE_TEMPLATES = "choose templates" + CHOOSE_TEMPLATES = "choose-templates" # The reflex.build frontend host REFLEX_BUILD_FRONTEND = "https://flexgen.reflex.run" diff --git a/reflex/utils/prerequisites.py b/reflex/utils/prerequisites.py index d86619a782..8c7923116d 100644 --- a/reflex/utils/prerequisites.py +++ b/reflex/utils/prerequisites.py @@ -1223,7 +1223,9 @@ def prompt_for_template_options(templates: list[Template]) -> str: # Prompt the user to select a template. id_to_name = { - str(idx): f"{template.name} ({template.demo_url}) - {template.description}" + str( + idx + ): f"{template.name.replace('_', ' ').replace('-', ' ')} ({template.demo_url}) - {template.description}" for idx, template in enumerate(templates) } for id in range(len(id_to_name)): @@ -1489,8 +1491,8 @@ def fetch_and_prompt_with_remote_templates( if not show_prompt and template in available_templates: return template, available_templates - if not show_prompt and (template not in templates): - console.error(f"{template} is not a valid template name.") + if not show_prompt and (template not in available_templates): + console.error(f"{template!r} is not a valid template name.") template = ( prompt_for_remote_template_selection(available_templates) From 9e1906d7749f4379c871466e6f2e763fedb625fe Mon Sep 17 00:00:00 2001 From: Elijah Date: Wed, 13 Nov 2024 17:57:09 +0000 Subject: [PATCH 06/12] add demolink for blank template --- reflex/utils/prerequisites.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reflex/utils/prerequisites.py b/reflex/utils/prerequisites.py index 8c7923116d..714e4cafdd 100644 --- a/reflex/utils/prerequisites.py +++ b/reflex/utils/prerequisites.py @@ -1581,7 +1581,7 @@ def get_init_cli_prompt_options() -> list[Template]: Template( name=constants.Templates.DEFAULT, description="A blank Reflex app.", - demo_url="", + demo_url="https://blank-template.reflex.run", code_url="", ), Template( From 6587103ed155d646f2820b6e0ce08c9cb6343364 Mon Sep 17 00:00:00 2001 From: Elijah Date: Wed, 13 Nov 2024 18:01:16 +0000 Subject: [PATCH 07/12] fix darglint --- reflex/utils/prerequisites.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/reflex/utils/prerequisites.py b/reflex/utils/prerequisites.py index 714e4cafdd..de15067759 100644 --- a/reflex/utils/prerequisites.py +++ b/reflex/utils/prerequisites.py @@ -1388,9 +1388,6 @@ def prompt_for_remote_template_selection(templates: dict[str, Template]) -> str: Returns: The selected template. - - Raises: - Exit: If the user does not input a valid template. """ while True: console.print( @@ -1516,9 +1513,6 @@ def initialize_app( template: The name of the template to use. ai: Whether to use AI to generate the template. - Raises: - Exit: If template is directly provided in the command flag and is invalid. - Returns: The name of the template. """ From 511ff8a33055c98a7393a30cc18fc8806e8ff7d2 Mon Sep 17 00:00:00 2001 From: Elijah Date: Thu, 14 Nov 2024 16:19:49 +0000 Subject: [PATCH 08/12] Add more templates and keep template name in kebab case --- reflex/constants/base.py | 6 ++++++ reflex/utils/format.py | 13 ++++++++++++ reflex/utils/prerequisites.py | 39 +++++++++++++++++++++++++++++------ 3 files changed, 52 insertions(+), 6 deletions(-) diff --git a/reflex/constants/base.py b/reflex/constants/base.py index b50ce50aca..3266043c5a 100644 --- a/reflex/constants/base.py +++ b/reflex/constants/base.py @@ -103,6 +103,12 @@ class Templates(SimpleNamespace): # The option for the user to choose a remote template. CHOOSE_TEMPLATES = "choose-templates" + # The URL to find reflex templates. + REFLEX_TEMPLATES_URL = "https://reflex.dev/templates" + + # Demo url for the default template. + DEFAULT_TEMPLATE_URL = "https://blank-template.reflex.run" + # The reflex.build frontend host REFLEX_BUILD_FRONTEND = "https://flexgen.reflex.run" diff --git a/reflex/utils/format.py b/reflex/utils/format.py index 1b3d1740fe..42ba5a4d5a 100644 --- a/reflex/utils/format.py +++ b/reflex/utils/format.py @@ -766,3 +766,16 @@ def format_data_editor_cell(cell: Any): "kind": Var(_js_expr="GridCellKind.Text"), "data": cell, } + + +def format_template_name(name: str) -> str: + """Format the template name of remote templates obtained during reflex init. + + Args: + name: The name of the template. + + Returns: + The formatted template name. + """ + formatted_name = to_kebab_case(name) + return formatted_name.removesuffix("-app").removesuffix("-template") diff --git a/reflex/utils/prerequisites.py b/reflex/utils/prerequisites.py index de15067759..6f55bec503 100644 --- a/reflex/utils/prerequisites.py +++ b/reflex/utils/prerequisites.py @@ -39,7 +39,7 @@ GeneratedCodeHasNoFunctionDefs, raise_system_package_missing_error, ) -from reflex.utils.format import format_library_name +from reflex.utils.format import format_library_name, format_template_name from reflex.utils.registry import _get_npm_registry CURRENTLY_INSTALLING_NODE = False @@ -1242,6 +1242,30 @@ def prompt_for_template_options(templates: list[Template]) -> str: return templates[int(template)].name +def get_other_templates() -> list[dict[str, str]]: + """Get other templates not included in the templates repo. + + Returns: + List of other templates. + """ + return [ + { + "name": "llamaindex", + "description": "A minimal chat app using LLamaIndex.", + "demo_url": "https://frontend-gold-orca.dev.reflexcorp.run/", + "code_url": "https://github.com/reflex-dev/reflex-llamaindex-template/archive/refs/heads/main.zip", + "hidden": False, + }, + { + "name": "chat", + "description": "A chat app with a dark theme.", + "demo_url": "https://chat.reflex.run", + "code_url": "https://github.com/reflex-dev/reflex-chat/archive/refs/heads/main.zip", + "hidden": False, + }, + ] + + def fetch_app_templates(version: str) -> dict[str, Template]: """Fetch a dict of templates from the templates repo using github API. @@ -1288,9 +1312,12 @@ def get_release_by_tag(tag: str) -> dict | None: ), None, ) + # include other templates not in the templates repo. + templates_data.extend(get_other_templates()) filtered_templates = {} for tp in templates_data: + tp["name"] = format_template_name(tp["name"]) if tp["hidden"] or tp["code_url"] is None: continue known_fields = set(f.name for f in dataclasses.fields(Template)) @@ -1391,7 +1418,7 @@ def prompt_for_remote_template_selection(templates: dict[str, Template]) -> str: """ while True: console.print( - "Visit https://reflex.dev/templates for the complete list of templates." + f"Visit {constants.Templates.REFLEX_TEMPLATES_URL} for the complete list of templates." ) selected_template = console.ask( "Enter a valid template name", show_choices=False @@ -1484,7 +1511,7 @@ def fetch_and_prompt_with_remote_templates( try: # Get the available templates available_templates = fetch_app_templates(constants.Reflex.VERSION) - + console.info(available_templates) if not show_prompt and template in available_templates: return template, available_templates @@ -1575,19 +1602,19 @@ def get_init_cli_prompt_options() -> list[Template]: Template( name=constants.Templates.DEFAULT, description="A blank Reflex app.", - demo_url="https://blank-template.reflex.run", + demo_url=constants.Templates.DEFAULT_TEMPLATE_URL, code_url="", ), Template( name=constants.Templates.AI, description="Generate a template using AI (Flexgen)", - demo_url="https://flexgen.reflex.run", + demo_url=constants.Templates.REFLEX_BUILD_FRONTEND, code_url="", ), Template( name=constants.Templates.CHOOSE_TEMPLATES, description="Choose an existing template.", - demo_url="https://reflex.dev/templates", + demo_url=constants.Templates.REFLEX_TEMPLATES_URL, code_url="", ), ] From 52cb331b0416d024cc891a7ce092efbed7c6a881 Mon Sep 17 00:00:00 2001 From: Elijah Date: Thu, 14 Nov 2024 21:54:45 +0000 Subject: [PATCH 09/12] revert getting other templates since we'll use the submodules approach --- reflex/utils/prerequisites.py | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/reflex/utils/prerequisites.py b/reflex/utils/prerequisites.py index 6f55bec503..db4453d4ae 100644 --- a/reflex/utils/prerequisites.py +++ b/reflex/utils/prerequisites.py @@ -1242,30 +1242,6 @@ def prompt_for_template_options(templates: list[Template]) -> str: return templates[int(template)].name -def get_other_templates() -> list[dict[str, str]]: - """Get other templates not included in the templates repo. - - Returns: - List of other templates. - """ - return [ - { - "name": "llamaindex", - "description": "A minimal chat app using LLamaIndex.", - "demo_url": "https://frontend-gold-orca.dev.reflexcorp.run/", - "code_url": "https://github.com/reflex-dev/reflex-llamaindex-template/archive/refs/heads/main.zip", - "hidden": False, - }, - { - "name": "chat", - "description": "A chat app with a dark theme.", - "demo_url": "https://chat.reflex.run", - "code_url": "https://github.com/reflex-dev/reflex-chat/archive/refs/heads/main.zip", - "hidden": False, - }, - ] - - def fetch_app_templates(version: str) -> dict[str, Template]: """Fetch a dict of templates from the templates repo using github API. @@ -1312,8 +1288,6 @@ def get_release_by_tag(tag: str) -> dict | None: ), None, ) - # include other templates not in the templates repo. - templates_data.extend(get_other_templates()) filtered_templates = {} for tp in templates_data: From 3135a311510514a0cc210d6cf0ae7d8aabe182e9 Mon Sep 17 00:00:00 2001 From: Elijah Date: Fri, 15 Nov 2024 14:37:34 +0000 Subject: [PATCH 10/12] remove debug statement --- reflex/utils/prerequisites.py | 1 - 1 file changed, 1 deletion(-) diff --git a/reflex/utils/prerequisites.py b/reflex/utils/prerequisites.py index db4453d4ae..8f74e3e564 100644 --- a/reflex/utils/prerequisites.py +++ b/reflex/utils/prerequisites.py @@ -1485,7 +1485,6 @@ def fetch_and_prompt_with_remote_templates( try: # Get the available templates available_templates = fetch_app_templates(constants.Reflex.VERSION) - console.info(available_templates) if not show_prompt and template in available_templates: return template, available_templates From 128d6d103c58305451480c52d0172edc01aaedd5 Mon Sep 17 00:00:00 2001 From: Elijah Date: Fri, 15 Nov 2024 19:16:38 +0000 Subject: [PATCH 11/12] Improvements based on standup comments --- reflex/utils/format.py | 13 ------------- reflex/utils/prerequisites.py | 14 ++++++++------ 2 files changed, 8 insertions(+), 19 deletions(-) diff --git a/reflex/utils/format.py b/reflex/utils/format.py index 42ba5a4d5a..1b3d1740fe 100644 --- a/reflex/utils/format.py +++ b/reflex/utils/format.py @@ -766,16 +766,3 @@ def format_data_editor_cell(cell: Any): "kind": Var(_js_expr="GridCellKind.Text"), "data": cell, } - - -def format_template_name(name: str) -> str: - """Format the template name of remote templates obtained during reflex init. - - Args: - name: The name of the template. - - Returns: - The formatted template name. - """ - formatted_name = to_kebab_case(name) - return formatted_name.removesuffix("-app").removesuffix("-template") diff --git a/reflex/utils/prerequisites.py b/reflex/utils/prerequisites.py index 8f74e3e564..cf087e99a5 100644 --- a/reflex/utils/prerequisites.py +++ b/reflex/utils/prerequisites.py @@ -39,7 +39,7 @@ GeneratedCodeHasNoFunctionDefs, raise_system_package_missing_error, ) -from reflex.utils.format import format_library_name, format_template_name +from reflex.utils.format import format_library_name from reflex.utils.registry import _get_npm_registry CURRENTLY_INSTALLING_NODE = False @@ -1221,11 +1221,14 @@ def prompt_for_template_options(templates: list[Template]) -> str: # Show the user the URLs of each template to preview. console.print("\nGet started with a template:") + def format_demo_url_str(url: str) -> str: + return f" ({url})" if url else "" + # Prompt the user to select a template. id_to_name = { str( idx - ): f"{template.name.replace('_', ' ').replace('-', ' ')} ({template.demo_url}) - {template.description}" + ): f"{template.name.replace('_', ' ').replace('-', ' ')}{format_demo_url_str(template.demo_url)} - {template.description}" for idx, template in enumerate(templates) } for id in range(len(id_to_name)): @@ -1291,7 +1294,6 @@ def get_release_by_tag(tag: str) -> dict | None: filtered_templates = {} for tp in templates_data: - tp["name"] = format_template_name(tp["name"]) if tp["hidden"] or tp["code_url"] is None: continue known_fields = set(f.name for f in dataclasses.fields(Template)) @@ -1580,14 +1582,14 @@ def get_init_cli_prompt_options() -> list[Template]: ), Template( name=constants.Templates.AI, - description="Generate a template using AI (Flexgen)", - demo_url=constants.Templates.REFLEX_BUILD_FRONTEND, + description="Generate a template using AI [Experimental]", + demo_url="", code_url="", ), Template( name=constants.Templates.CHOOSE_TEMPLATES, description="Choose an existing template.", - demo_url=constants.Templates.REFLEX_TEMPLATES_URL, + demo_url="", code_url="", ), ] From 37b19d286ad18ce4ad4eadc59168de0bd65810a7 Mon Sep 17 00:00:00 2001 From: Elijah Date: Fri, 15 Nov 2024 19:27:04 +0000 Subject: [PATCH 12/12] Add redirect logic --- reflex/utils/prerequisites.py | 1 + reflex/utils/redir.py | 17 +++++++++++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/reflex/utils/prerequisites.py b/reflex/utils/prerequisites.py index cf087e99a5..6b03d8dc2a 100644 --- a/reflex/utils/prerequisites.py +++ b/reflex/utils/prerequisites.py @@ -1493,6 +1493,7 @@ def fetch_and_prompt_with_remote_templates( if not show_prompt and (template not in available_templates): console.error(f"{template!r} is not a valid template name.") + redir.open_browser(constants.Templates.REFLEX_TEMPLATES_URL) template = ( prompt_for_remote_template_selection(available_templates) if available_templates diff --git a/reflex/utils/redir.py b/reflex/utils/redir.py index 1dbd989e90..b35cbe26e7 100644 --- a/reflex/utils/redir.py +++ b/reflex/utils/redir.py @@ -10,6 +10,18 @@ from . import console +def open_browser(target_url: str) -> None: + """Open a browser window to target_url. + + Args: + target_url: The URL to open in the browser. + """ + if not webbrowser.open(target_url): + console.warn( + f"Unable to automatically open the browser. Please navigate to {target_url} in your browser." + ) + + def open_browser_and_wait( target_url: str, poll_url: str, interval: int = 2 ) -> httpx.Response: @@ -23,10 +35,7 @@ def open_browser_and_wait( Returns: The response from the poll_url. """ - if not webbrowser.open(target_url): - console.warn( - f"Unable to automatically open the browser. Please navigate to {target_url} in your browser." - ) + open_browser(target_url) console.info("[b]Complete the workflow in the browser to continue.[/b]") while True: try: