Explorar o código

click over typer (#5154)

* click over typer

* these are flags

* i missed a few

* fix a few more

* zip

* factor out loglevel debug

* case insensitive for everyone

* do things a bit smarter

* fix pyright

* fix app harness

* use export directly

* uv lock without trailing slash

* last fixes

* unpin weird stuff

* no need to export typer

* that guy too

* restore uv lokc

* uv lock

* precommit silly

* lock

* why did i do that

* factor things out
Khaleel Al-Adhami hai 3 semanas
pai
achega
27bb987f78

+ 2 - 2
pyproject.toml

@@ -32,10 +32,10 @@ dependencies = [
   "python-socketio >=5.12.0,<6.0",
   "python-multipart >=0.0.20,<1.0",
   "redis >=5.2.1,<6.0",
-  "reflex-hosting-cli >=0.1.38",
+  "reflex-hosting-cli >=0.1.43",
   "rich >=13,<15",
   "sqlmodel >=0.0.24,<0.1",
-  "typer >=0.15.2,<1.0",
+  "click >=8",
   "typing_extensions >=4.13.0",
   "wrapt >=1.17.0,<2.0",
 ]

+ 1 - 1
reflex/config.py

@@ -903,7 +903,7 @@ class Config(Base):
         # Set the log level for this process
         env_loglevel = os.environ.get("LOGLEVEL")
         if env_loglevel is not None:
-            env_loglevel = LogLevel(env_loglevel)
+            env_loglevel = LogLevel(env_loglevel.lower())
         if env_loglevel or self.loglevel != LogLevel.DEFAULT:
             console.set_log_level(env_loglevel or self.loglevel)
 

+ 21 - 0
reflex/constants/base.py

@@ -7,6 +7,7 @@ from enum import Enum
 from importlib import metadata
 from pathlib import Path
 from types import SimpleNamespace
+from typing import Literal
 
 from platformdirs import PlatformDirs
 
@@ -219,6 +220,9 @@ class ColorMode(SimpleNamespace):
     SET = "setColorMode"
 
 
+LITERAL_ENV = Literal["dev", "prod"]
+
+
 # Env modes
 class Env(str, Enum):
     """The environment modes."""
@@ -238,6 +242,23 @@ class LogLevel(str, Enum):
     ERROR = "error"
     CRITICAL = "critical"
 
+    @classmethod
+    def from_string(cls, level: str | None) -> LogLevel | None:
+        """Convert a string to a log level.
+
+        Args:
+            level: The log level as a string.
+
+        Returns:
+            The log level.
+        """
+        if not level:
+            return None
+        try:
+            return LogLevel[level.upper()]
+        except KeyError:
+            return None
+
     def __le__(self, other: LogLevel) -> bool:
         """Compare log levels.
 

+ 67 - 64
reflex/custom_components/custom_components.py

@@ -9,17 +9,46 @@ import sys
 from collections import namedtuple
 from contextlib import contextmanager
 from pathlib import Path
+from typing import Any
 
+import click
 import httpx
-import typer
 
 from reflex import constants
-from reflex.config import get_config
 from reflex.constants import CustomComponents
 from reflex.utils import console
 
-custom_components_cli = typer.Typer()
 
+def set_loglevel(ctx: Any, self: Any, value: str | None):
+    """Set the log level.
+
+    Args:
+        ctx: The click context.
+        self: The click command.
+        value: The log level to set.
+    """
+    if value is not None:
+        loglevel = constants.LogLevel.from_string(value)
+        console.set_log_level(loglevel)
+
+
+@click.group
+def custom_components_cli():
+    """CLI for creating custom components."""
+    pass
+
+
+loglevel_option = click.option(
+    "--loglevel",
+    type=click.Choice(
+        [loglevel.value for loglevel in constants.LogLevel],
+        case_sensitive=False,
+    ),
+    callback=set_loglevel,
+    is_eager=True,
+    expose_value=False,
+    help="The log level to use.",
+)
 
 POST_CUSTOM_COMPONENTS_GALLERY_TIMEOUT = 15
 
@@ -163,13 +192,13 @@ def _get_default_library_name_parts() -> list[str]:
             console.error(
                 f"Based on current directory name {current_dir_name}, the library name is {constants.Reflex.MODULE_NAME}. This package already exists. Please use --library-name to specify a different name."
             )
-            raise typer.Exit(code=1)
+            raise click.exceptions.Exit(code=1)
     if not parts:
         # The folder likely has a name not suitable for python paths.
         console.error(
             f"Could not find a valid library name based on the current directory: got {current_dir_name}."
         )
-        raise typer.Exit(code=1)
+        raise click.exceptions.Exit(code=1)
     return parts
 
 
@@ -205,7 +234,7 @@ def _validate_library_name(library_name: str | None) -> NameVariants:
         console.error(
             f"Please use only alphanumeric characters or dashes: got {library_name}"
         )
-        raise typer.Exit(code=1)
+        raise click.exceptions.Exit(code=1)
 
     # If not specified, use the current directory name to form the module name.
     name_parts = (
@@ -277,36 +306,35 @@ def _populate_custom_component_project(name_variants: NameVariants):
 
 
 @custom_components_cli.command(name="init")
+@click.option(
+    "--library-name",
+    default=None,
+    help="The name of your library. On PyPI, package will be published as `reflex-{library-name}`.",
+)
+@click.option(
+    "--install/--no-install",
+    default=True,
+    help="Whether to install package from this local custom component in editable mode.",
+)
+@loglevel_option
 def init(
-    library_name: str | None = typer.Option(
-        None,
-        help="The name of your library. On PyPI, package will be published as `reflex-{library-name}`.",
-    ),
-    install: bool = typer.Option(
-        True,
-        help="Whether to install package from this local custom component in editable mode.",
-    ),
-    loglevel: constants.LogLevel | None = typer.Option(
-        None, help="The log level to use."
-    ),
+    library_name: str | None,
+    install: bool,
 ):
     """Initialize a custom component.
 
     Args:
         library_name: The name of the library.
         install: Whether to install package from this local custom component in editable mode.
-        loglevel: The log level to use.
 
     Raises:
         Exit: If the pyproject.toml already exists.
     """
     from reflex.utils import exec, prerequisites
 
-    console.set_log_level(loglevel or get_config().loglevel)
-
     if CustomComponents.PYPROJECT_TOML.exists():
         console.error(f"A {CustomComponents.PYPROJECT_TOML} already exists. Aborting.")
-        typer.Exit(code=1)
+        click.exceptions.Exit(code=1)
 
     # Show system info.
     exec.output_system_info()
@@ -331,7 +359,7 @@ def init(
         if _pip_install_on_demand(package_name=".", install_args=["-e"]):
             console.info(f"Package {package_name} installed!")
         else:
-            raise typer.Exit(code=1)
+            raise click.exceptions.Exit(code=1)
 
     console.print("[bold]Custom component initialized successfully!")
     console.rule("[bold]Project Summary")
@@ -424,21 +452,13 @@ def _run_build():
     if _run_commands_in_subprocess(cmds):
         console.info("Custom component built successfully!")
     else:
-        raise typer.Exit(code=1)
+        raise click.exceptions.Exit(code=1)
 
 
 @custom_components_cli.command(name="build")
-def build(
-    loglevel: constants.LogLevel | None = typer.Option(
-        None, help="The log level to use."
-    ),
-):
-    """Build a custom component. Must be run from the project root directory where the pyproject.toml is.
-
-    Args:
-        loglevel: The log level to use.
-    """
-    console.set_log_level(loglevel or get_config().loglevel)
+@loglevel_option
+def build():
+    """Build a custom component. Must be run from the project root directory where the pyproject.toml is."""
     _run_build()
 
 
@@ -453,7 +473,7 @@ def publish():
         "The publish command is deprecated. You can use `reflex component build` followed by `twine upload` or a similar publishing command to publish your custom component."
         "\nIf you want to share your custom component with the Reflex community, please use `reflex component share`."
     )
-    raise typer.Exit(code=1)
+    raise click.exceptions.Exit(code=1)
 
 
 def _collect_details_for_gallery():
@@ -472,7 +492,7 @@ def _collect_details_for_gallery():
         console.error(
             "Unable to authenticate with Reflex backend services. Make sure you are logged in."
         )
-        raise typer.Exit(code=1)
+        raise click.exceptions.Exit(code=1)
 
     console.rule("[bold]Custom Component Information")
     params = {}
@@ -502,11 +522,11 @@ def _collect_details_for_gallery():
             console.error(
                 f"{package_name} is owned by another user. Unable to update the information for it."
             )
-            raise typer.Exit(code=1)
+            raise click.exceptions.Exit(code=1)
         response.raise_for_status()
     except httpx.HTTPError as he:
         console.error(f"Unable to complete request due to {he}.")
-        raise typer.Exit(code=1) from he
+        raise click.exceptions.Exit(code=1) from he
 
     files = []
     if (image_file_and_extension := _get_file_from_prompt_in_loop()) is not None:
@@ -541,7 +561,7 @@ def _collect_details_for_gallery():
 
     except httpx.HTTPError as he:
         console.error(f"Unable to complete request due to {he}.")
-        raise typer.Exit(code=1) from he
+        raise click.exceptions.Exit(code=1) from he
 
     console.info("Custom component information successfully shared!")
 
@@ -577,7 +597,7 @@ def _get_file_from_prompt_in_loop() -> tuple[bytes, str] | None:
             image_file = image_file_path.read_bytes()
         except OSError as ose:
             console.error(f"Unable to read the {file_extension} file due to {ose}")
-            raise typer.Exit(code=1) from ose
+            raise click.exceptions.Exit(code=1) from ose
         else:
             return image_file, file_extension
 
@@ -586,38 +606,21 @@ def _get_file_from_prompt_in_loop() -> tuple[bytes, str] | None:
 
 
 @custom_components_cli.command(name="share")
-def share_more_detail(
-    loglevel: constants.LogLevel | None = typer.Option(
-        None, help="The log level to use."
-    ),
-):
-    """Collect more details on the published package for gallery.
-
-    Args:
-        loglevel: The log level to use.
-    """
-    console.set_log_level(loglevel or get_config().loglevel)
-
+@loglevel_option
+def share_more_detail():
+    """Collect more details on the published package for gallery."""
     _collect_details_for_gallery()
 
 
-@custom_components_cli.command()
-def install(
-    loglevel: constants.LogLevel | None = typer.Option(
-        None, help="The log level to use."
-    ),
-):
+@custom_components_cli.command(name="install")
+@loglevel_option
+def install():
     """Install package from this local custom component in editable mode.
 
-    Args:
-        loglevel: The log level to use.
-
     Raises:
         Exit: If unable to install the current directory in editable mode.
     """
-    console.set_log_level(loglevel or get_config().loglevel)
-
     if _pip_install_on_demand(package_name=".", install_args=["-e"]):
         console.info("Package installed successfully!")
     else:
-        raise typer.Exit(code=1)
+        raise click.exceptions.Exit(code=1)

+ 276 - 265
reflex/reflex.py

@@ -3,74 +3,63 @@
 from __future__ import annotations
 
 import atexit
+from importlib.util import find_spec
 from pathlib import Path
 from typing import TYPE_CHECKING
 
-import typer
-import typer.core
+import click
 from reflex_cli.v2.deployments import hosting_cli
 
 from reflex import constants
 from reflex.config import environment, get_config
+from reflex.constants.base import LITERAL_ENV
 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.exec import should_use_granian
 
-# Disable typer+rich integration for help panels
-typer.core.rich = None  # pyright: ignore [reportPrivateImportUsage]
 
-# Create the app.
-cli = typer.Typer(add_completion=False, pretty_exceptions_enable=False)
-
-
-def version(value: bool):
-    """Get the Reflex version.
+def set_loglevel(ctx: click.Context, self: click.Parameter, value: str | None):
+    """Set the log level.
 
     Args:
-        value: Whether the version flag was passed.
-
-    Raises:
-        typer.Exit: If the version flag was passed.
+        ctx: The click context.
+        self: The click command.
+        value: The log level to set.
     """
-    if value:
-        console.print(constants.Reflex.VERSION)
-        raise typer.Exit()
-
-
-@cli.callback()
-def main(
-    version: bool = typer.Option(
-        None,
-        "-v",
-        "--version",
-        callback=version,
-        help="Get the Reflex version.",
-        is_eager=True,
-    ),
-):
+    if value is not None:
+        loglevel = constants.LogLevel.from_string(value)
+        console.set_log_level(loglevel)
+
+
+@click.group
+@click.version_option(constants.Reflex.VERSION, message="%(version)s")
+def cli():
     """Reflex CLI to create, run, and deploy apps."""
     pass
 
 
+loglevel_option = click.option(
+    "--loglevel",
+    type=click.Choice(
+        [loglevel.value for loglevel in constants.LogLevel],
+        case_sensitive=False,
+    ),
+    is_eager=True,
+    callback=set_loglevel,
+    expose_value=False,
+    help="The log level to use.",
+)
+
+
 def _init(
     name: str,
     template: str | None = None,
-    loglevel: constants.LogLevel | None = None,
     ai: bool = False,
 ):
     """Initialize a new Reflex app in the given directory."""
     from reflex.utils import exec, prerequisites
 
-    if loglevel is not None:
-        console.set_log_level(loglevel)
-
-    config = get_config()
-
-    # Set the log level.
-    loglevel = loglevel or config.loglevel
-    console.set_log_level(loglevel)
-
     # Show system info
     exec.output_system_info()
 
@@ -112,24 +101,28 @@ def _init(
 
 
 @cli.command()
+@loglevel_option
+@click.option(
+    "--name",
+    metavar="APP_NAME",
+    help="The name of the app to initialize.",
+)
+@click.option(
+    "--template",
+    help="The template to initialize the app with.",
+)
+@click.option(
+    "--ai",
+    is_flag=True,
+    help="Use AI to create the initial template. Cannot be used with existing app or `--template` option.",
+)
 def init(
-    name: str = typer.Option(
-        None, metavar="APP_NAME", help="The name of the app to initialize."
-    ),
-    template: str = typer.Option(
-        None,
-        help="The template to initialize the app with.",
-    ),
-    loglevel: constants.LogLevel | None = typer.Option(
-        None, help="The log level to use."
-    ),
-    ai: bool = typer.Option(
-        False,
-        help="Use AI to create the initial template. Cannot be used with existing app or `--template` option.",
-    ),
+    name: str,
+    template: str | None,
+    ai: bool,
 ):
     """Initialize a new Reflex app in the current directory."""
-    _init(name, template, loglevel, ai)
+    _init(name, template, ai)
 
 
 def _run(
@@ -139,22 +132,14 @@ def _run(
     frontend_port: int | None = None,
     backend_port: int | None = None,
     backend_host: str | None = None,
-    loglevel: constants.LogLevel | None = None,
 ):
     """Run the app in the given directory."""
     from reflex.utils import build, exec, prerequisites, processes
 
-    if loglevel is not None:
-        console.set_log_level(loglevel)
-
     config = get_config()
 
-    loglevel = loglevel or config.loglevel
     backend_host = backend_host or config.backend_host
 
-    # Set the log level.
-    console.set_log_level(loglevel)
-
     # Set env mode in the environment
     environment.REFLEX_ENV_MODE.set(env)
 
@@ -168,7 +153,7 @@ def _run(
 
     # Check that the app is initialized.
     if prerequisites.needs_reinit(frontend=frontend):
-        _init(name=config.app_name, loglevel=loglevel)
+        _init(name=config.app_name)
 
     # Delete the states folder if it exists.
     reset_disk_state_manager()
@@ -228,7 +213,7 @@ def _run(
     else:
         validation_result = app_task(*args)
     if not validation_result:
-        raise typer.Exit(1)
+        raise click.exceptions.Exit(1)
 
     # Warn if schema is not up to date.
     prerequisites.check_schema_up_to_date()
@@ -248,7 +233,7 @@ def _run(
             exec.run_backend_prod,
         )
     if not setup_frontend or not frontend_cmd or not backend_cmd:
-        raise ValueError("Invalid env")
+        raise ValueError(f"Invalid env: {env}. Must be DEV or PROD.")
 
     # Post a telemetry event.
     telemetry.send(f"run-{env.value}")
@@ -271,7 +256,7 @@ def _run(
                 backend_cmd,
                 backend_host,
                 backend_port,
-                loglevel.subprocess_level(),
+                config.loglevel.subprocess_level(),
                 frontend,
             )
         )
@@ -281,7 +266,10 @@ def _run(
         # In dev mode, run the backend on the main thread.
         if backend and backend_port and env == constants.Env.DEV:
             backend_cmd(
-                backend_host, int(backend_port), loglevel.subprocess_level(), frontend
+                backend_host,
+                int(backend_port),
+                config.loglevel.subprocess_level(),
+                frontend,
             )
             # The windows uvicorn bug workaround
             # https://github.com/reflex-dev/reflex/issues/2335
@@ -291,94 +279,122 @@ def _run(
 
 
 @cli.command()
+@loglevel_option
+@click.option(
+    "--env",
+    type=click.Choice([e.value for e in constants.Env], case_sensitive=False),
+    default=constants.Env.DEV.value,
+    help="The environment to run the app in.",
+)
+@click.option(
+    "--frontend-only",
+    is_flag=True,
+    show_default=False,
+    help="Execute only frontend.",
+    envvar=environment.REFLEX_FRONTEND_ONLY.name,
+)
+@click.option(
+    "--backend-only",
+    is_flag=True,
+    show_default=False,
+    help="Execute only backend.",
+    envvar=environment.REFLEX_BACKEND_ONLY.name,
+)
+@click.option(
+    "--frontend-port",
+    type=int,
+    help="Specify a different frontend port.",
+    envvar=environment.REFLEX_FRONTEND_PORT.name,
+)
+@click.option(
+    "--backend-port",
+    type=int,
+    help="Specify a different backend port.",
+    envvar=environment.REFLEX_BACKEND_PORT.name,
+)
+@click.option(
+    "--backend-host",
+    help="Specify the backend host.",
+)
 def run(
-    env: constants.Env = typer.Option(
-        constants.Env.DEV, help="The environment to run the app in."
-    ),
-    frontend: bool = typer.Option(
-        False,
-        "--frontend-only",
-        help="Execute only frontend.",
-        envvar=environment.REFLEX_FRONTEND_ONLY.name,
-    ),
-    backend: bool = typer.Option(
-        False,
-        "--backend-only",
-        help="Execute only backend.",
-        envvar=environment.REFLEX_BACKEND_ONLY.name,
-    ),
-    frontend_port: int | None = typer.Option(
-        None,
-        help="Specify a different frontend port.",
-        envvar=environment.REFLEX_FRONTEND_PORT.name,
-    ),
-    backend_port: int | None = typer.Option(
-        None,
-        help="Specify a different backend port.",
-        envvar=environment.REFLEX_BACKEND_PORT.name,
-    ),
-    backend_host: str | None = typer.Option(None, help="Specify the backend host."),
-    loglevel: constants.LogLevel | None = typer.Option(
-        None, help="The log level to use."
-    ),
+    env: LITERAL_ENV,
+    frontend_only: bool,
+    backend_only: bool,
+    frontend_port: int | None,
+    backend_port: int | None,
+    backend_host: str | None,
 ):
     """Run the app in the current directory."""
-    if frontend and backend:
+    if frontend_only and backend_only:
         console.error("Cannot use both --frontend-only and --backend-only options.")
-        raise typer.Exit(1)
-
-    if loglevel is not None:
-        console.set_log_level(loglevel)
+        raise click.exceptions.Exit(1)
 
     config = get_config()
 
     frontend_port = frontend_port or config.frontend_port
     backend_port = backend_port or config.backend_port
     backend_host = backend_host or config.backend_host
-    loglevel = loglevel or config.loglevel
 
     environment.REFLEX_COMPILE_CONTEXT.set(constants.CompileContext.RUN)
-    environment.REFLEX_BACKEND_ONLY.set(backend)
-    environment.REFLEX_FRONTEND_ONLY.set(frontend)
-
-    _run(env, frontend, backend, frontend_port, backend_port, backend_host, loglevel)
+    environment.REFLEX_BACKEND_ONLY.set(backend_only)
+    environment.REFLEX_FRONTEND_ONLY.set(frontend_only)
+
+    _run(
+        constants.Env.DEV if env == constants.Env.DEV else constants.Env.PROD,
+        frontend_only,
+        backend_only,
+        frontend_port,
+        backend_port,
+        backend_host,
+    )
 
 
 @cli.command()
+@loglevel_option
+@click.option(
+    "--zip/--no-zip",
+    default=True,
+    help="Whether to zip the backend and frontend exports.",
+)
+@click.option(
+    "--frontend-only",
+    is_flag=True,
+    show_default=False,
+    envvar=environment.REFLEX_FRONTEND_ONLY.name,
+    help="Export only frontend.",
+)
+@click.option(
+    "--backend-only",
+    is_flag=True,
+    show_default=False,
+    envvar=environment.REFLEX_BACKEND_ONLY.name,
+    help="Export only backend.",
+)
+@click.option(
+    "--zip-dest-dir",
+    default=str(Path.cwd()),
+    help="The directory to export the zip files to.",
+    show_default=False,
+)
+@click.option(
+    "--upload-db-file",
+    is_flag=True,
+    help="Whether to exclude sqlite db files when exporting backend.",
+    hidden=True,
+)
+@click.option(
+    "--env",
+    type=click.Choice([e.value for e in constants.Env], case_sensitive=False),
+    default=constants.Env.PROD.value,
+    help="The environment to export the app in.",
+)
 def export(
-    zipping: bool = typer.Option(
-        True, "--no-zip", help="Disable zip for backend and frontend exports."
-    ),
-    frontend: bool = typer.Option(
-        False,
-        "--frontend-only",
-        help="Export only frontend.",
-        show_default=False,
-        envvar=environment.REFLEX_FRONTEND_ONLY.name,
-    ),
-    backend: bool = typer.Option(
-        False,
-        "--backend-only",
-        help="Export only backend.",
-        show_default=False,
-        envvar=environment.REFLEX_BACKEND_ONLY.name,
-    ),
-    zip_dest_dir: str = typer.Option(
-        str(Path.cwd()),
-        help="The directory to export the zip files to.",
-        show_default=False,
-    ),
-    upload_db_file: bool = typer.Option(
-        False,
-        help="Whether to exclude sqlite db files when exporting backend.",
-        hidden=True,
-    ),
-    env: constants.Env = typer.Option(
-        constants.Env.PROD, help="The environment to export the app in."
-    ),
-    loglevel: constants.LogLevel | None = typer.Option(
-        None, help="The log level to use."
-    ),
+    zip: bool,
+    frontend_only: bool,
+    backend_only: bool,
+    zip_dest_dir: str,
+    upload_db_file: bool,
+    env: LITERAL_ENV,
 ):
     """Export the app to a zip file."""
     from reflex.utils import export as export_utils
@@ -386,35 +402,33 @@ def export(
 
     environment.REFLEX_COMPILE_CONTEXT.set(constants.CompileContext.EXPORT)
 
-    frontend, backend = prerequisites.check_running_mode(frontend, backend)
+    frontend_only, backend_only = prerequisites.check_running_mode(
+        frontend_only, backend_only
+    )
 
-    loglevel = loglevel or get_config().loglevel
-    console.set_log_level(loglevel)
+    config = get_config()
 
-    if prerequisites.needs_reinit(frontend=frontend or not backend):
-        _init(name=get_config().app_name, loglevel=loglevel)
+    if prerequisites.needs_reinit(frontend=frontend_only or not backend_only):
+        _init(name=config.app_name)
 
     export_utils.export(
-        zipping=zipping,
-        frontend=frontend,
-        backend=backend,
+        zipping=zip,
+        frontend=frontend_only,
+        backend=backend_only,
         zip_dest_dir=zip_dest_dir,
         upload_db_file=upload_db_file,
-        env=env,
-        loglevel=loglevel.subprocess_level(),
+        env=constants.Env.DEV if env == constants.Env.DEV else constants.Env.PROD,
+        loglevel=config.loglevel.subprocess_level(),
     )
 
 
 @cli.command()
-def login(loglevel: constants.LogLevel | None = typer.Option(None)):
+@loglevel_option
+def login():
     """Authenticate with experimental Reflex hosting service."""
     from reflex_cli.v2 import cli as hosting_cli
     from reflex_cli.v2.deployments import check_version
 
-    loglevel = loglevel or get_config().loglevel
-
-    console.set_log_level(loglevel)
-
     check_version()
 
     validated_info = hosting_cli.login()
@@ -424,24 +438,27 @@ def login(loglevel: constants.LogLevel | None = typer.Option(None)):
 
 
 @cli.command()
-def logout(
-    loglevel: constants.LogLevel | None = typer.Option(
-        None, help="The log level to use."
-    ),
-):
+@loglevel_option
+def logout():
     """Log out of access to Reflex hosting service."""
     from reflex_cli.v2.cli import logout
     from reflex_cli.v2.deployments import check_version
 
     check_version()
 
-    loglevel = loglevel or get_config().loglevel
+    logout(_convert_reflex_loglevel_to_reflex_cli_loglevel(get_config().loglevel))
 
-    logout(_convert_reflex_loglevel_to_reflex_cli_loglevel(loglevel))
 
+@click.group
+def db_cli():
+    """Subcommands for managing the database schema."""
+    pass
 
-db_cli = typer.Typer()
-script_cli = typer.Typer()
+
+@click.group
+def script_cli():
+    """Subcommands for running helper scripts."""
+    pass
 
 
 def _skip_compile():
@@ -495,11 +512,11 @@ def migrate():
 
 
 @db_cli.command()
-def makemigrations(
-    message: str = typer.Option(
-        None, help="Human readable identifier for the generated revision."
-    ),
-):
+@click.option(
+    "--message",
+    help="Human readable identifier for the generated revision.",
+)
+def makemigrations(message: str | None):
     """Create autogenerated alembic migration scripts."""
     from alembic.util.exc import CommandError
 
@@ -523,70 +540,74 @@ def makemigrations(
 
 
 @cli.command()
+@loglevel_option
+@click.option(
+    "--app-name",
+    help="The name of the app to deploy.",
+)
+@click.option(
+    "--app-id",
+    help="The ID of the app to deploy.",
+)
+@click.option(
+    "-r",
+    "--region",
+    multiple=True,
+    help="The regions to deploy to. `reflex cloud regions` For multiple envs, repeat this option, e.g. --region sjc --region iad",
+)
+@click.option(
+    "--env",
+    multiple=True,
+    help="The environment variables to set: <key>=<value>. For multiple envs, repeat this option, e.g. --env k1=v2 --env k2=v2.",
+)
+@click.option(
+    "--vmtype",
+    help="Vm type id. Run `reflex cloud vmtypes` to get options.",
+)
+@click.option(
+    "--hostname",
+    help="The hostname of the frontend.",
+)
+@click.option(
+    "--interactive",
+    is_flag=True,
+    default=True,
+    help="Whether to list configuration options and ask for confirmation.",
+)
+@click.option(
+    "--envfile",
+    help="The path to an env file to use. Will override any envs set manually.",
+)
+@click.option(
+    "--project",
+    help="project id to deploy to",
+)
+@click.option(
+    "--project-name",
+    help="The name of the project to deploy to.",
+)
+@click.option(
+    "--token",
+    help="token to use for auth",
+)
+@click.option(
+    "--config-path",
+    "--config",
+    help="path to the config file",
+)
 def deploy(
-    app_name: str | None = typer.Option(
-        None,
-        "--app-name",
-        help="The name of the App to deploy under.",
-    ),
-    app_id: str = typer.Option(
-        None,
-        "--app-id",
-        help="The ID of the App to deploy over.",
-    ),
-    regions: list[str] = typer.Option(
-        [],
-        "-r",
-        "--region",
-        help="The regions to deploy to. `reflex cloud regions` For multiple envs, repeat this option, e.g. --region sjc --region iad",
-    ),
-    envs: list[str] = typer.Option(
-        [],
-        "--env",
-        help="The environment variables to set: <key>=<value>. For multiple envs, repeat this option, e.g. --env k1=v2 --env k2=v2.",
-    ),
-    vmtype: str | None = typer.Option(
-        None,
-        "--vmtype",
-        help="Vm type id. Run `reflex cloud vmtypes` to get options.",
-    ),
-    hostname: str | None = typer.Option(
-        None,
-        "--hostname",
-        help="The hostname of the frontend.",
-    ),
-    interactive: bool = typer.Option(
-        True,
-        help="Whether to list configuration options and ask for confirmation.",
-    ),
-    envfile: str | None = typer.Option(
-        None,
-        "--envfile",
-        help="The path to an env file to use. Will override any envs set manually.",
-    ),
-    loglevel: constants.LogLevel | None = typer.Option(
-        None, help="The log level to use."
-    ),
-    project: str | None = typer.Option(
-        None,
-        "--project",
-        help="project id to deploy to",
-    ),
-    project_name: str | None = typer.Option(
-        None,
-        "--project-name",
-        help="The name of the project to deploy to.",
-    ),
-    token: str | None = typer.Option(
-        None,
-        "--token",
-        help="token to use for auth",
-    ),
-    config_path: str | None = typer.Option(
-        None,
-        "--config",
-        help="path to the config file",
-    ),
+    app_name: str | None,
+    app_id: str | None,
+    region: tuple[str, ...],
+    env: tuple[str],
+    vmtype: str | None,
+    hostname: str | None,
+    interactive: bool,
+    envfile: str | None,
+    project: str | None,
+    project_name: str | None,
+    token: str | None,
+    config_path: str | None,
 ):
     """Deploy the app to the Reflex hosting service."""
     from reflex_cli.utils import dependency
@@ -596,21 +617,14 @@ def deploy(
     from reflex.utils import export as export_utils
     from reflex.utils import prerequisites
 
-    if loglevel is not None:
-        console.set_log_level(loglevel)
-
     config = get_config()
 
-    loglevel = loglevel or config.loglevel
     app_name = app_name or config.app_name
 
     check_version()
 
     environment.REFLEX_COMPILE_CONTEXT.set(constants.CompileContext.DEPLOY)
 
-    # Set the log level.
-    console.set_log_level(loglevel)
-
     # Only check requirements if interactive.
     # There is user interaction for requirements update.
     if interactive:
@@ -618,7 +632,7 @@ def deploy(
 
     # Check if we are set up.
     if prerequisites.needs_reinit(frontend=True):
-        _init(name=config.app_name, loglevel=loglevel)
+        _init(name=config.app_name)
     prerequisites.check_latest_package_version(constants.ReflexHostingCLI.MODULE_NAME)
 
     hosting_cli.deploy(
@@ -638,17 +652,17 @@ def deploy(
                 frontend=frontend,
                 backend=backend,
                 zipping=zipping,
-                loglevel=loglevel.subprocess_level(),
+                loglevel=config.loglevel.subprocess_level(),
                 upload_db_file=upload_db,
             )
         ),
-        regions=regions,
-        envs=envs,
+        regions=list(region),
+        envs=list(env),
         vmtype=vmtype,
         envfile=envfile,
         hostname=hostname,
         interactive=interactive,
-        loglevel=_convert_reflex_loglevel_to_reflex_cli_loglevel(loglevel),
+        loglevel=_convert_reflex_loglevel_to_reflex_cli_loglevel(config.loglevel),
         token=token,
         project=project,
         project_name=project_name,
@@ -657,19 +671,14 @@ def deploy(
 
 
 @cli.command()
-def rename(
-    new_name: str = typer.Argument(..., help="The new name for the app."),
-    loglevel: constants.LogLevel | None = typer.Option(
-        None, help="The log level to use."
-    ),
-):
+@loglevel_option
+@click.argument("new_name")
+def rename(new_name: str):
     """Rename the app in the current directory."""
     from reflex.utils import prerequisites
 
-    loglevel = loglevel or get_config().loglevel
-
     prerequisites.validate_app_name(new_name)
-    prerequisites.rename_app(new_name, loglevel)
+    prerequisites.rename_app(new_name, get_config().loglevel)
 
 
 if TYPE_CHECKING:
@@ -702,18 +711,20 @@ def _convert_reflex_loglevel_to_reflex_cli_loglevel(
     return HostingLogLevel.INFO
 
 
-cli.add_typer(db_cli, name="db", help="Subcommands for managing the database schema.")
-cli.add_typer(script_cli, name="script", help="Subcommands running helper scripts.")
-cli.add_typer(
-    hosting_cli,  # pyright: ignore [reportArgumentType]
-    name="cloud",
-    help="Subcommands for managing the reflex cloud.",
-)
-cli.add_typer(
-    custom_components_cli,
-    name="component",
-    help="Subcommands for creating and publishing Custom Components.",
-)
+if find_spec("typer"):
+    import typer  # pyright: ignore[reportMissingImports]
+
+    if isinstance(hosting_cli, typer.Typer):
+        hosting_cli_command = typer.main.get_command(hosting_cli)
+    else:
+        hosting_cli_command = hosting_cli
+else:
+    hosting_cli_command = hosting_cli
+
+cli.add_command(hosting_cli_command, name="cloud")
+cli.add_command(db_cli, name="db")
+cli.add_command(script_cli, name="script")
+cli.add_command(custom_components_cli, name="component")
 
 if __name__ == "__main__":
     cli()

+ 10 - 3
reflex/testing.py

@@ -34,7 +34,7 @@ import reflex.utils.format
 import reflex.utils.prerequisites
 import reflex.utils.processes
 from reflex.components.component import CustomComponent
-from reflex.config import environment
+from reflex.config import environment, get_config
 from reflex.state import (
     BaseState,
     StateManager,
@@ -44,6 +44,7 @@ from reflex.state import (
     reload_state_module,
 )
 from reflex.utils import console
+from reflex.utils.export import export
 
 try:
     from selenium import webdriver
@@ -252,11 +253,11 @@ class AppHarness:
                     self._get_source_from_app_source(self.app_source),
                 ]
             )
+            get_config().loglevel = reflex.constants.LogLevel.INFO
             with chdir(self.app_path):
                 reflex.reflex._init(
                     name=self.app_name,
                     template=reflex.constants.Templates.DEFAULT,
-                    loglevel=reflex.constants.LogLevel.INFO,
                 )
                 self.app_module_path.write_text(source_code)
         else:
@@ -933,7 +934,13 @@ class AppHarnessProd(AppHarness):
             config.api_url = "http://{}:{}".format(
                 *self._poll_for_servers().getsockname(),
             )
-            reflex.reflex.export(
+
+            get_config().loglevel = reflex.constants.LogLevel.INFO
+
+            if reflex.utils.prerequisites.needs_reinit(frontend=True):
+                reflex.reflex._init(name=get_config().app_name)
+
+            export(
                 zipping=False,
                 frontend=True,
                 backend=False,

+ 4 - 3
reflex/utils/console.py

@@ -47,7 +47,7 @@ _EMITTED_LOGS = set()
 _EMITTED_PRINTS = set()
 
 
-def set_log_level(log_level: LogLevel):
+def set_log_level(log_level: LogLevel | None):
     """Set the log level.
 
     Args:
@@ -56,6 +56,8 @@ def set_log_level(log_level: LogLevel):
     Raises:
         TypeError: If the log level is a string.
     """
+    if log_level is None:
+        return
     if not isinstance(log_level, LogLevel):
         raise TypeError(
             f"log_level must be a LogLevel enum value, got {log_level} of type {type(log_level)} instead."
@@ -193,13 +195,12 @@ def warn(msg: str, dedupe: bool = False, **kwargs):
 
 def _get_first_non_framework_frame() -> FrameType | None:
     import click
-    import typer
     import typing_extensions
 
     import reflex as rx
 
     # Exclude utility modules that should never be the source of deprecated reflex usage.
-    exclude_modules = [click, rx, typer, typing_extensions]
+    exclude_modules = [click, rx, typing_extensions]
     exclude_roots = [
         p.parent.resolve() if (p := Path(file)).name == "__init__.py" else p.resolve()
         for m in exclude_modules

+ 27 - 27
reflex/utils/prerequisites.py

@@ -27,8 +27,8 @@ from types import ModuleType
 from typing import NamedTuple
 from urllib.parse import urlparse
 
+import click
 import httpx
-import typer
 from alembic.util.exc import CommandError
 from packaging import version
 from redis import Redis as RedisSync
@@ -517,7 +517,7 @@ def compile_or_validate_app(compile: bool = False) -> bool:
         else:
             validate_app()
     except Exception as e:
-        if isinstance(e, typer.Exit):
+        if isinstance(e, click.exceptions.Exit):
             return False
 
         import traceback
@@ -621,14 +621,14 @@ def validate_app_name(app_name: str | None = None) -> str:
         console.error(
             f"The app directory cannot be named [bold]{constants.Reflex.MODULE_NAME}[/bold]."
         )
-        raise typer.Exit(1)
+        raise click.exceptions.Exit(1)
 
     # Make sure the app name is standard for a python package name.
     if not re.match(r"^[a-zA-Z][a-zA-Z0-9_]*$", app_name):
         console.error(
             "The app directory name must start with a letter and can contain letters, numbers, and underscores."
         )
-        raise typer.Exit(1)
+        raise click.exceptions.Exit(1)
 
     return app_name
 
@@ -687,7 +687,7 @@ def rename_app(new_app_name: str, loglevel: constants.LogLevel):
         console.error(
             "No rxconfig.py found. Make sure you are in the root directory of your app."
         )
-        raise typer.Exit(1)
+        raise click.exceptions.Exit(1)
 
     sys.path.insert(0, str(Path.cwd()))
 
@@ -695,11 +695,11 @@ def rename_app(new_app_name: str, loglevel: constants.LogLevel):
     module_path = importlib.util.find_spec(config.module)
     if module_path is None:
         console.error(f"Could not find module {config.module}.")
-        raise typer.Exit(1)
+        raise click.exceptions.Exit(1)
 
     if not module_path.origin:
         console.error(f"Could not find origin for module {config.module}.")
-        raise typer.Exit(1)
+        raise click.exceptions.Exit(1)
     console.info(f"Renaming app directory to {new_app_name}.")
     process_directory(
         Path.cwd(),
@@ -862,7 +862,7 @@ def initialize_requirements_txt() -> bool:
             continue
         except Exception as e:
             console.error(f"Failed to read {requirements_file_path}.")
-            raise typer.Exit(1) from e
+            raise click.exceptions.Exit(1) from e
     else:
         return False
 
@@ -907,7 +907,7 @@ def initialize_app_directory(
             console.error(
                 f"Only {template_name=} should be provided, got {template_code_dir_name=}, {template_dir=}."
             )
-            raise typer.Exit(1)
+            raise click.exceptions.Exit(1)
         template_code_dir_name = constants.Templates.Dirs.CODE
         template_dir = Path(constants.Templates.Dirs.BASE, "apps", template_name)
     else:
@@ -915,7 +915,7 @@ def initialize_app_directory(
             console.error(
                 f"For `{template_name}` template, `template_code_dir_name` and `template_dir` should both be provided."
             )
-            raise typer.Exit(1)
+            raise click.exceptions.Exit(1)
 
     console.debug(f"Using {template_name=} {template_dir=} {template_code_dir_name=}.")
 
@@ -1147,7 +1147,7 @@ def download_and_run(url: str, *args, show_status: bool = False, **env):
         console.error(
             f"Failed to download bun install script. You can install or update bun manually from https://bun.sh \n{e}"
         )
-        raise typer.Exit(1) from None
+        raise click.exceptions.Exit(1) from None
 
     # Save the script to a temporary file.
     script = Path(tempfile.NamedTemporaryFile().name)
@@ -1381,7 +1381,7 @@ def needs_reinit(frontend: bool = True) -> bool:
         console.error(
             f"[cyan]{constants.Config.FILE}[/cyan] not found. Move to the root folder of your project, or run [bold]{constants.Reflex.MODULE_NAME} init[/bold] to start a new project."
         )
-        raise typer.Exit(1)
+        raise click.exceptions.Exit(1)
 
     # Don't need to reinit if not running in frontend mode.
     if not frontend:
@@ -1446,7 +1446,7 @@ def validate_bun(bun_path: Path | None = None):
             console.error(
                 "Failed to obtain bun version. Make sure the specified bun path in your config is correct."
             )
-            raise typer.Exit(1)
+            raise click.exceptions.Exit(1)
         elif bun_version < version.parse(constants.Bun.MIN_VERSION):
             console.warn(
                 f"Reflex requires bun version {constants.Bun.MIN_VERSION} or higher to run, but the detected version is "
@@ -1468,14 +1468,14 @@ def validate_frontend_dependencies(init: bool = True):
         try:
             get_js_package_executor(raise_on_none=True)
         except FileNotFoundError as e:
-            raise typer.Exit(1) from e
+            raise click.exceptions.Exit(1) from e
 
     if prefer_npm_over_bun() and not check_node_version():
         node_version = get_node_version()
         console.error(
             f"Reflex requires node version {constants.Node.MIN_VERSION} or higher to run, but the detected version is {node_version}",
         )
-        raise typer.Exit(1)
+        raise click.exceptions.Exit(1)
 
 
 def ensure_reflex_installation_id() -> int | None:
@@ -1622,17 +1622,17 @@ def prompt_for_template_options(templates: list[Template]) -> str:
 
     if not template:
         console.error("No template selected.")
-        raise typer.Exit(1)
+        raise click.exceptions.Exit(1)
 
     try:
         template_index = int(template)
     except ValueError:
         console.error("Invalid template selected.")
-        raise typer.Exit(1) from None
+        raise click.exceptions.Exit(1) from None
 
     if template_index < 0 or template_index >= len(templates):
         console.error("Invalid template selected.")
-        raise typer.Exit(1)
+        raise click.exceptions.Exit(1)
 
     # Return the template.
     return templates[template_index].name
@@ -1712,7 +1712,7 @@ def create_config_init_app_from_remote_template(app_name: str, template_url: str
         temp_dir = tempfile.mkdtemp()
     except OSError as ose:
         console.error(f"Failed to create temp directory for download: {ose}")
-        raise typer.Exit(1) from ose
+        raise click.exceptions.Exit(1) from ose
 
     # Use httpx GET with redirects to download the zip file.
     zip_file_path: Path = Path(temp_dir) / "template.zip"
@@ -1723,20 +1723,20 @@ def create_config_init_app_from_remote_template(app_name: str, template_url: str
         response.raise_for_status()
     except httpx.HTTPError as he:
         console.error(f"Failed to download the template: {he}")
-        raise typer.Exit(1) from he
+        raise click.exceptions.Exit(1) from he
     try:
         zip_file_path.write_bytes(response.content)
         console.debug(f"Downloaded the zip to {zip_file_path}")
     except OSError as ose:
         console.error(f"Unable to write the downloaded zip to disk {ose}")
-        raise typer.Exit(1) from ose
+        raise click.exceptions.Exit(1) from ose
 
     # Create a temp directory for the zip extraction.
     try:
         unzip_dir = Path(tempfile.mkdtemp())
     except OSError as ose:
         console.error(f"Failed to create temp directory for extracting zip: {ose}")
-        raise typer.Exit(1) from ose
+        raise click.exceptions.Exit(1) from ose
 
     try:
         zipfile.ZipFile(zip_file_path).extractall(path=unzip_dir)
@@ -1744,11 +1744,11 @@ def create_config_init_app_from_remote_template(app_name: str, template_url: str
         # repo-name-branch/**/*, so we need to remove the top level directory.
     except Exception as uze:
         console.error(f"Failed to unzip the template: {uze}")
-        raise typer.Exit(1) from uze
+        raise click.exceptions.Exit(1) from uze
 
     if len(subdirs := list(unzip_dir.iterdir())) != 1:
         console.error(f"Expected one directory in the zip, found {subdirs}")
-        raise typer.Exit(1)
+        raise click.exceptions.Exit(1)
 
     template_dir = unzip_dir / subdirs[0]
     console.debug(f"Template folder is located at {template_dir}")
@@ -1810,7 +1810,7 @@ def validate_and_create_app_using_remote_template(
             console.print(
                 f"Please use `reflex login` to access the '{template}' template."
             )
-            raise typer.Exit(3)
+            raise click.exceptions.Exit(3)
 
         template_url = templates[template].code_url
     else:
@@ -1821,7 +1821,7 @@ def validate_and_create_app_using_remote_template(
             template_url = f"https://github.com/{path}/archive/main.zip"
         else:
             console.error(f"Template `{template}` not found or invalid.")
-            raise typer.Exit(1)
+            raise click.exceptions.Exit(1)
 
     if template_url is None:
         return
@@ -1888,7 +1888,7 @@ def initialize_app(app_name: str, template: str | None = None) -> str | None:
             console.print(
                 f"Go to the templates page ({constants.Templates.REFLEX_TEMPLATES_URL}) and copy the command to init with a template."
             )
-            raise typer.Exit(0)
+            raise click.exceptions.Exit(0)
 
     # If the blank template is selected, create a blank app.
     if template in (constants.Templates.DEFAULT,):

+ 6 - 6
reflex/utils/processes.py

@@ -13,8 +13,8 @@ from concurrent import futures
 from pathlib import Path
 from typing import Any, Literal, overload
 
+import click
 import psutil
-import typer
 from redis.exceptions import RedisError
 from rich.progress import Progress
 
@@ -48,7 +48,7 @@ def get_num_workers() -> int:
         redis_client.ping()
     except RedisError as re:
         console.error(f"Unable to connect to Redis: {re}")
-        raise typer.Exit(1) from re
+        raise click.exceptions.Exit(1) from re
     return (os.cpu_count() or 1) * 2 + 1
 
 
@@ -141,7 +141,7 @@ def handle_port(service_name: str, port: int, auto_increment: bool) -> int:
         console.error(
             f"{service_name.capitalize()} port: {port} is already in use by PID: {process.pid}."
         )
-        raise typer.Exit()
+        raise click.exceptions.Exit()
 
 
 @overload
@@ -186,7 +186,7 @@ def new_process(
     non_empty_args = list(filter(None, args)) if isinstance(args, list) else [args]
     if isinstance(args, list) and len(non_empty_args) != len(args):
         console.error(f"Invalid command: {args}")
-        raise typer.Exit(1)
+        raise click.exceptions.Exit(1)
 
     path_env: str = os.environ.get("PATH", "")
 
@@ -345,7 +345,7 @@ def stream_logs(
                 "NPM_CONFIG_REGISTRY environment variable. If TLS is the issue, and you know what "
                 "you are doing, you can disable it by setting the SSL_NO_VERIFY environment variable."
             )
-            raise typer.Exit(1)
+            raise click.exceptions.Exit(1)
         for set_of_logs in (*prior_logs, tuple(logs)):
             for line in set_of_logs:
                 console.error(line, end="")
@@ -353,7 +353,7 @@ def stream_logs(
         if analytics_enabled:
             telemetry.send("error", context=message)
         console.error("Run with [bold]--loglevel debug [/bold] for the full log.")
-        raise typer.Exit(1)
+        raise click.exceptions.Exit(1)
 
 
 def show_logs(message: str, process: subprocess.Popen):

+ 2 - 2
tests/units/test_prerequisites.py

@@ -5,7 +5,7 @@ import tempfile
 from pathlib import Path
 
 import pytest
-from typer.testing import CliRunner
+from click.testing import CliRunner
 
 from reflex.config import Config
 from reflex.reflex import cli
@@ -279,7 +279,7 @@ app.add_page(index)
     with chdir(temp_directory / "foo"):
         result = runner.invoke(cli, ["rename", "bar"])
 
-    assert result.exit_code == 0
+    assert result.exit_code == 0, result.output
     assert (foo_dir / "rxconfig.py").read_text() == (
         """
 import reflex as rx

+ 5 - 5
tests/units/utils/test_utils.py

@@ -5,8 +5,8 @@ from functools import cached_property
 from pathlib import Path
 from typing import Any, ClassVar, List, Literal, NoReturn  # noqa: UP035
 
+import click
 import pytest
-import typer
 from packaging import version
 
 from reflex import constants
@@ -180,7 +180,7 @@ def test_validate_none_bun_path(mocker):
         mocker: Pytest mocker object.
     """
     mocker.patch("reflex.utils.path_ops.get_bun_path", return_value=None)
-    # with pytest.raises(typer.Exit):
+    # with pytest.raises(click.exceptions.Exit):
     prerequisites.validate_bun()
 
 
@@ -198,7 +198,7 @@ def test_validate_invalid_bun_path(
     mocker.patch("reflex.utils.path_ops.samefile", return_value=False)
     mocker.patch("reflex.utils.prerequisites.get_bun_version", return_value=None)
 
-    with pytest.raises(typer.Exit):
+    with pytest.raises(click.exceptions.Exit):
         prerequisites.validate_bun()
 
 
@@ -464,10 +464,10 @@ def test_validate_app_name(tmp_path, mocker):
 
     mocker.patch("reflex.utils.prerequisites.os.getcwd", return_value=str(reflex))
 
-    with pytest.raises(typer.Exit):
+    with pytest.raises(click.exceptions.Exit):
         prerequisites.validate_app_name()
 
-    with pytest.raises(typer.Exit):
+    with pytest.raises(click.exceptions.Exit):
         prerequisites.validate_app_name(app_name="1_test")
 
 

+ 3 - 27
uv.lock

@@ -1514,6 +1514,7 @@ version = "0.7.9.dev1"
 source = { editable = "." }
 dependencies = [
     { name = "alembic" },
+    { name = "click" },
     { name = "fastapi" },
     { name = "granian", extra = ["reload"] },
     { name = "httpx" },
@@ -1528,7 +1529,6 @@ dependencies = [
     { name = "reflex-hosting-cli" },
     { name = "rich" },
     { name = "sqlmodel" },
-    { name = "typer" },
     { name = "typing-extensions" },
     { name = "wrapt" },
 ]
@@ -1567,6 +1567,7 @@ dev = [
 [package.metadata]
 requires-dist = [
     { name = "alembic", specifier = ">=1.15.2,<2.0" },
+    { name = "click", specifier = ">=8" },
     { name = "fastapi", specifier = ">=0.115.0" },
     { name = "granian", extras = ["reload"], specifier = ">=2.2.5" },
     { name = "httpx", specifier = ">=0.28.0,<1.0" },
@@ -1578,10 +1579,9 @@ requires-dist = [
     { name = "python-multipart", specifier = ">=0.0.20,<1.0" },
     { name = "python-socketio", specifier = ">=5.12.0,<6.0" },
     { name = "redis", specifier = ">=5.2.1,<6.0" },
-    { name = "reflex-hosting-cli", specifier = ">=0.1.38" },
+    { name = "reflex-hosting-cli", specifier = ">=0.1.43" },
     { name = "rich", specifier = ">=13,<15" },
     { name = "sqlmodel", specifier = ">=0.0.24,<0.1" },
-    { name = "typer", specifier = ">=0.15.2,<1.0" },
     { name = "typing-extensions", specifier = ">=4.13.0" },
     { name = "wrapt", specifier = ">=1.17.0,<2.0" },
 ]
@@ -1706,15 +1706,6 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/32/53/212db779d2481b0a8428365960596f8d5a4d482ae12c441d0507fd54aaf2/selenium-4.31.0-py3-none-any.whl", hash = "sha256:7b8b8d5e424d7133cb7aa656263b19ac505ec26d65c0f921a696e7e2c5ccd95b", size = 9350584, upload_time = "2025-04-05T00:43:04.04Z" },
 ]
 
-[[package]]
-name = "shellingham"
-version = "1.5.4"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload_time = "2023-10-24T04:13:40.426Z" }
-wheels = [
-    { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload_time = "2023-10-24T04:13:38.866Z" },
-]
-
 [[package]]
 name = "simple-websocket"
 version = "1.1.0"
@@ -1937,21 +1928,6 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/70/7d/a2271b98b833680561ab3fcd60ab682478dc4f7cc023fab24991601ac8ac/trove_classifiers-2025.4.11.15-py3-none-any.whl", hash = "sha256:e7d98983f004df35293caf954bdfe944b139eb402677a97115450e320f0bd855", size = 13710, upload_time = "2025-04-11T15:13:16.152Z" },
 ]
 
-[[package]]
-name = "typer"
-version = "0.15.2"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
-    { name = "click" },
-    { name = "rich" },
-    { name = "shellingham" },
-    { name = "typing-extensions" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/8b/6f/3991f0f1c7fcb2df31aef28e0594d8d54b05393a0e4e34c65e475c2a5d41/typer-0.15.2.tar.gz", hash = "sha256:ab2fab47533a813c49fe1f16b1a370fd5819099c00b119e0633df65f22144ba5", size = 100711, upload_time = "2025-02-27T19:17:34.807Z" }
-wheels = [
-    { url = "https://files.pythonhosted.org/packages/7f/fc/5b29fea8cee020515ca82cc68e3b8e1e34bb19a3535ad854cac9257b414c/typer-0.15.2-py3-none-any.whl", hash = "sha256:46a499c6107d645a9c13f7ee46c5d5096cae6f5fc57dd11eccbbb9ae3e44ddfc", size = 45061, upload_time = "2025-02-27T19:17:32.111Z" },
-]
-
 [[package]]
 name = "typing-extensions"
 version = "4.13.2"