Explorar el Código

CLI switch to prod server (#2016)

Martin Xu hace 1 año
padre
commit
81053618c9
Se han modificado 8 ficheros con 288 adiciones y 268 borrados
  1. 5 0
      reflex/config.py
  2. 4 0
      reflex/config.pyi
  3. 3 28
      reflex/constants/hosting.py
  4. 41 30
      reflex/reflex.py
  5. 4 3
      reflex/utils/build.py
  6. 182 122
      reflex/utils/hosting.py
  7. 12 52
      tests/test_reflex.py
  8. 37 33
      tests/utils/test_hosting.py

+ 5 - 0
reflex/config.py

@@ -190,6 +190,11 @@ class Config(Base):
     # The rxdeploy url.
     # The rxdeploy url.
     rxdeploy_url: Optional[str] = None
     rxdeploy_url: Optional[str] = None
 
 
+    # The hosting service backend URL.
+    cp_backend_url: str = constants.Hosting.CP_BACKEND_URL
+    # The hosting service frontend URL.
+    cp_web_url: str = constants.Hosting.CP_WEB_URL
+
     # The username.
     # The username.
     username: Optional[str] = None
     username: Optional[str] = None
 
 

+ 4 - 0
reflex/config.pyi

@@ -66,6 +66,8 @@ class Config(Base):
     event_namespace: Optional[str]
     event_namespace: Optional[str]
     frontend_packages: List[str]
     frontend_packages: List[str]
     rxdeploy_url: Optional[str]
     rxdeploy_url: Optional[str]
+    cp_backend_url: str
+    cp_web_url: str
     username: Optional[str]
     username: Optional[str]
 
 
     def __init__(
     def __init__(
@@ -90,6 +92,8 @@ class Config(Base):
         event_namespace: Optional[str] = None,
         event_namespace: Optional[str] = None,
         frontend_packages: Optional[List[str]] = None,
         frontend_packages: Optional[List[str]] = None,
         rxdeploy_url: Optional[str] = None,
         rxdeploy_url: Optional[str] = None,
+        cp_backend_url: Optional[str] = None,
+        cp_web_url: Optional[str] = None,
         username: Optional[str] = None,
         username: Optional[str] = None,
         **kwargs
         **kwargs
     ) -> None: ...
     ) -> None: ...

+ 3 - 28
reflex/constants/hosting.py

@@ -10,40 +10,15 @@ class Hosting:
     # The hosting config json file
     # The hosting config json file
     HOSTING_JSON = os.path.join(Reflex.DIR, "hosting_v0.json")
     HOSTING_JSON = os.path.join(Reflex.DIR, "hosting_v0.json")
     # The hosting service backend URL
     # The hosting service backend URL
-    CP_BACKEND_URL = "https://rxcp-dev-control-plane.fly.dev"
+    CP_BACKEND_URL = "https://rxcp-prod-control-plane.fly.dev"
     # The hosting service webpage URL
     # The hosting service webpage URL
-    CP_WEB_URL = "https://control-plane.dev.reflexcorp.run"
-    # Endpoint to create or update a deployment
-    POST_DEPLOYMENTS_ENDPOINT = f"{CP_BACKEND_URL}/deployments"
-    # Endpoint to get all deployments for the user
-    GET_DEPLOYMENTS_ENDPOINT = f"{CP_BACKEND_URL}/deployments"
-    # Endpoint to fetch information from backend in preparation of a deployment
-    POST_DEPLOYMENTS_PREPARE_ENDPOINT = f"{CP_BACKEND_URL}/deployments/prepare"
-    # Endpoint to authenticate current user
-    POST_VALIDATE_ME_ENDPOINT = f"{CP_BACKEND_URL}/authenticate/me"
-    # Endpoint to fetch a login token after user completes authentication on web
-    FETCH_TOKEN_ENDPOINT = f"{CP_BACKEND_URL}/authenticate"
-    # Endpoint to delete a deployment
-    DELETE_DEPLOYMENTS_ENDPOINT = f"{CP_BACKEND_URL}/deployments"
-    # Endpoint to get deployment status
-    GET_DEPLOYMENT_STATUS_ENDPOINT = f"{CP_BACKEND_URL}/deployments"
-    # Websocket endpoint to stream logs of a deployment
-    DEPLOYMENT_LOGS_ENDPOINT = f'{CP_BACKEND_URL.replace("http", "ws")}/deployments'
+    CP_WEB_URL = "https://control-plane.reflex.run"
+
     # The number of times to try and wait for the user to complete web authentication.
     # The number of times to try and wait for the user to complete web authentication.
     WEB_AUTH_RETRIES = 60
     WEB_AUTH_RETRIES = 60
     # The time to sleep between requests to check if for authentication completion. In seconds.
     # The time to sleep between requests to check if for authentication completion. In seconds.
     WEB_AUTH_SLEEP_DURATION = 5
     WEB_AUTH_SLEEP_DURATION = 5
-    # The expected number of milestones
-    MILESTONES_COUNT = 6
-    # Expected server response time to new deployment request. In seconds.
-    DEPLOYMENT_PICKUP_DELAY = 30
     # The time to wait for the backend to come up after user initiates deployment. In seconds.
     # The time to wait for the backend to come up after user initiates deployment. In seconds.
     BACKEND_POLL_RETRIES = 30
     BACKEND_POLL_RETRIES = 30
     # The time to wait for the frontend to come up after user initiates deployment. In seconds.
     # The time to wait for the frontend to come up after user initiates deployment. In seconds.
     FRONTEND_POLL_RETRIES = 30
     FRONTEND_POLL_RETRIES = 30
-    # End of deployment workflow message. Used to determine if it is the last message from server.
-    END_OF_DEPLOYMENT_MESSAGES = ["deploy success", "deploy failed"]
-    # How many iterations to try and print the deployment event messages from server during deployment.
-    DEPLOYMENT_EVENT_MESSAGES_RETRIES = 30
-    # Timeout limit for http requests
-    HTTP_REQUEST_TIMEOUT = 5  # seconds

+ 41 - 30
reflex/reflex.py

@@ -2,7 +2,6 @@
 
 
 import asyncio
 import asyncio
 import atexit
 import atexit
-import contextlib
 import json
 import json
 import os
 import os
 import shutil
 import shutil
@@ -276,6 +275,11 @@ def export(
         help="The directory to export the zip files to.",
         help="The directory to export the zip files to.",
         show_default=False,
         show_default=False,
     ),
     ),
+    backend_exclude_sqlite_db_files: bool = typer.Option(
+        True,
+        help="Whether to exclude sqlite db files when exporting backend.",
+        show_default=False,
+    ),
     loglevel: constants.LogLevel = typer.Option(
     loglevel: constants.LogLevel = typer.Option(
         console._LOG_LEVEL, help="The log level to use."
         console._LOG_LEVEL, help="The log level to use."
     ),
     ),
@@ -306,6 +310,7 @@ def export(
         zip=zipping,
         zip=zipping,
         zip_dest_dir=zip_dest_dir,
         zip_dest_dir=zip_dest_dir,
         deploy_url=config.deploy_url,
         deploy_url=config.deploy_url,
+        backend_exclude_sqlite_db_files=backend_exclude_sqlite_db_files,
     )
     )
 
 
     # Post a telemetry event.
     # Post a telemetry event.
@@ -322,39 +327,19 @@ def login(
     # Set the log level.
     # Set the log level.
     console.set_log_level(loglevel)
     console.set_log_level(loglevel)
 
 
-    # Check if the user is already logged in.
-    # Token is the access token, a JWT token obtained from auth provider
-    # after user completes authentication on web
-    access_token = None
-    # For initial hosting offering, it is by invitation only
-    # The login page is enabled only after a valid invitation code is entered
-    invitation_code = ""
-    using_existing_token = False
-
-    with contextlib.suppress(Exception):
-        access_token, invitation_code = hosting.get_existing_access_token()
-        using_existing_token = True
-        console.debug("Existing token found, proceed to validate")
+    access_token, invitation_code = hosting.authenticated_token()
+    if access_token:
+        console.print("You already logged in.")
+        return
 
 
     # If not already logged in, open a browser window/tab to the login page.
     # If not already logged in, open a browser window/tab to the login page.
-    if not using_existing_token:
-        access_token, invitation_code = hosting.authenticate_on_browser(invitation_code)
+    access_token = hosting.authenticate_on_browser(invitation_code)
 
 
     if not access_token:
     if not access_token:
-        console.error(
-            f"Unable to fetch access token. Please try again or contact support."
-        )
+        console.error(f"Unable to authenticate. Please try again or contact support.")
         raise typer.Exit(1)
         raise typer.Exit(1)
 
 
-    if not hosting.validate_token_with_retries(access_token):
-        console.error(f"Unable to validate token. Please try again or contact support.")
-        raise typer.Exit(1)
-
-    if not using_existing_token:
-        hosting.save_token_to_config(access_token, invitation_code)
-        console.print("Successfully logged in.")
-    else:
-        console.print("You already logged in.")
+    console.print("Successfully logged in.")
 
 
 
 
 @cli.command()
 @cli.command()
@@ -368,7 +353,7 @@ def logout(
 
 
     hosting.log_out_on_browser()
     hosting.log_out_on_browser()
     console.debug("Deleting access token from config locally")
     console.debug("Deleting access token from config locally")
-    hosting.delete_token_from_config()
+    hosting.delete_token_from_config(include_invitation_code=True)
 
 
 
 
 db_cli = typer.Typer()
 db_cli = typer.Typer()
@@ -483,6 +468,10 @@ def deploy(
         None,
         None,
         help="Setting to export tracing for the deployment. Setup required in user code.",
         help="Setting to export tracing for the deployment. Setup required in user code.",
     ),
     ),
+    backend_exclude_sqlite_db_files: bool = typer.Option(
+        True,
+        help="Whether to exclude sqlite db files from the backend export.",
+    ),
     loglevel: constants.LogLevel = typer.Option(
     loglevel: constants.LogLevel = typer.Option(
         config.loglevel, help="The log level to use."
         config.loglevel, help="The log level to use."
     ),
     ),
@@ -569,6 +558,7 @@ def deploy(
             zipping=True,
             zipping=True,
             zip_dest_dir=tmp_dir,
             zip_dest_dir=tmp_dir,
             loglevel=loglevel,
             loglevel=loglevel,
+            backend_exclude_sqlite_db_files=backend_exclude_sqlite_db_files,
         )
         )
     except ImportError as ie:
     except ImportError as ie:
         console.error(
         console.error(
@@ -654,7 +644,7 @@ def deploy(
         )
         )
         return
         return
     console.warn(
     console.warn(
-        "Your deployment is taking unusually long. Check back later on its status: `reflex deployments status`"
+        f"Your deployment is taking unusually long. Check back later on its status: `reflex deployments status {key}`"
     )
     )
 
 
 
 
@@ -797,6 +787,27 @@ def get_deployment_deploy_logs(
         raise typer.Exit(1) from ex
         raise typer.Exit(1) from ex
 
 
 
 
+@deployments_cli.command(name="regions")
+def get_deployment_regions(
+    loglevel: constants.LogLevel = typer.Option(
+        config.loglevel, help="The log level to use."
+    ),
+    as_json: bool = typer.Option(
+        False, "-j", "--json", help="Whether to output the result in json format."
+    ),
+):
+    """List all the regions of the hosting service."""
+    console.set_log_level(loglevel)
+    list_regions_info = hosting.get_regions()
+    if as_json:
+        console.print(json.dumps(list_regions_info))
+        return
+    if list_regions_info:
+        headers = list(list_regions_info[0].keys())
+        table = [list(deployment.values()) for deployment in list_regions_info]
+        console.print(tabulate(table, headers=headers))
+
+
 cli.add_typer(db_cli, name="db", help="Subcommands for managing the database schema.")
 cli.add_typer(db_cli, name="db", help="Subcommands for managing the database schema.")
 cli.add_typer(
 cli.add_typer(
     deployments_cli,
     deployments_cli,

+ 4 - 3
reflex/utils/build.py

@@ -60,7 +60,7 @@ def _zip(
     target: str,
     target: str,
     root_dir: str,
     root_dir: str,
     exclude_venv_dirs: bool,
     exclude_venv_dirs: bool,
-    exclude_sqlite_db_files: bool,
+    exclude_sqlite_db_files: bool = True,
     dirs_to_exclude: set[str] | None = None,
     dirs_to_exclude: set[str] | None = None,
     files_to_exclude: set[str] | None = None,
     files_to_exclude: set[str] | None = None,
 ) -> None:
 ) -> None:
@@ -127,6 +127,7 @@ def export(
     zip: bool = False,
     zip: bool = False,
     zip_dest_dir: str = os.getcwd(),
     zip_dest_dir: str = os.getcwd(),
     deploy_url: str | None = None,
     deploy_url: str | None = None,
+    backend_exclude_sqlite_db_files: bool = True,
 ):
 ):
     """Export the app for deployment.
     """Export the app for deployment.
 
 
@@ -136,6 +137,7 @@ def export(
         zip: Whether to zip the app.
         zip: Whether to zip the app.
         zip_dest_dir: The destination directory for created zip files (if any)
         zip_dest_dir: The destination directory for created zip files (if any)
         deploy_url: The URL of the deployed app.
         deploy_url: The URL of the deployed app.
+        backend_exclude_sqlite_db_files: Whether to exclude sqlite db files from the backend zip.
     """
     """
     # Remove the static folder.
     # Remove the static folder.
     path_ops.rm(constants.Dirs.WEB_STATIC)
     path_ops.rm(constants.Dirs.WEB_STATIC)
@@ -183,7 +185,6 @@ def export(
                 root_dir=".web/_static",
                 root_dir=".web/_static",
                 files_to_exclude=files_to_exclude,
                 files_to_exclude=files_to_exclude,
                 exclude_venv_dirs=False,
                 exclude_venv_dirs=False,
-                exclude_sqlite_db_files=False,
             )
             )
         if backend:
         if backend:
             _zip(
             _zip(
@@ -195,7 +196,7 @@ def export(
                 dirs_to_exclude={"assets", "__pycache__"},
                 dirs_to_exclude={"assets", "__pycache__"},
                 files_to_exclude=files_to_exclude,
                 files_to_exclude=files_to_exclude,
                 exclude_venv_dirs=True,
                 exclude_venv_dirs=True,
-                exclude_sqlite_db_files=True,
+                exclude_sqlite_db_files=backend_exclude_sqlite_db_files,
             )
             )
 
 
 
 

+ 182 - 122
reflex/utils/hosting.py

@@ -19,32 +19,57 @@ from pydantic import Field, ValidationError, root_validator
 
 
 from reflex import constants
 from reflex import constants
 from reflex.base import Base
 from reflex.base import Base
+from reflex.config import get_config
 from reflex.utils import console
 from reflex.utils import console
 
 
+config = get_config()
+# Endpoint to create or update a deployment
+POST_DEPLOYMENTS_ENDPOINT = f"{config.cp_backend_url}/deployments"
+# Endpoint to get all deployments for the user
+GET_DEPLOYMENTS_ENDPOINT = f"{config.cp_backend_url}/deployments"
+# Endpoint to fetch information from backend in preparation of a deployment
+POST_DEPLOYMENTS_PREPARE_ENDPOINT = f"{config.cp_backend_url}/deployments/prepare"
+# Endpoint to authenticate current user
+POST_VALIDATE_ME_ENDPOINT = f"{config.cp_backend_url}/authenticate/me"
+# Endpoint to fetch a login token after user completes authentication on web
+FETCH_TOKEN_ENDPOINT = f"{config.cp_backend_url}/authenticate"
+# Endpoint to delete a deployment
+DELETE_DEPLOYMENTS_ENDPOINT = f"{config.cp_backend_url}/deployments"
+# Endpoint to get deployment status
+GET_DEPLOYMENT_STATUS_ENDPOINT = f"{config.cp_backend_url}/deployments"
+# Public endpoint to get the list of supported regions in hosting service
+GET_REGIONS_ENDPOINT = f"{config.cp_backend_url}/deployments/regions"
+# Websocket endpoint to stream logs of a deployment
+DEPLOYMENT_LOGS_ENDPOINT = f'{config.cp_backend_url.replace("http", "ws")}/deployments'
+# Expected server response time to new deployment request. In seconds.
+DEPLOYMENT_PICKUP_DELAY = 30
+# End of deployment workflow message. Used to determine if it is the last message from server.
+END_OF_DEPLOYMENT_MESSAGES = ["deploy success", "deploy failed"]
+# How many iterations to try and print the deployment event messages from server during deployment.
+DEPLOYMENT_EVENT_MESSAGES_RETRIES = 90
+# Timeout limit for http requests
+HTTP_REQUEST_TIMEOUT = 60  # seconds
+
 
 
 def get_existing_access_token() -> tuple[str, str]:
 def get_existing_access_token() -> tuple[str, str]:
     """Fetch the access token from the existing config if applicable.
     """Fetch the access token from the existing config if applicable.
 
 
-    Raises:
-        Exception: if runs into any issues, file not exist, ill-formatted, etc.
-
     Returns:
     Returns:
-        The access token and optionally the invitation code if valid, otherwise empty string.
+        The access token and the invitation code.
+        If either is not found, return empty string for it instead.
     """
     """
     console.debug("Fetching token from existing config...")
     console.debug("Fetching token from existing config...")
+    access_token = invitation_code = ""
     try:
     try:
         with open(constants.Hosting.HOSTING_JSON, "r") as config_file:
         with open(constants.Hosting.HOSTING_JSON, "r") as config_file:
             hosting_config = json.load(config_file)
             hosting_config = json.load(config_file)
-
-        assert (
-            access_token := hosting_config.get("access_token", "")
-        ), "no access token found or empty token"
-        return access_token, hosting_config.get("code")
+            access_token = hosting_config.get("access_token", "")
+            invitation_code = hosting_config.get("code", "")
     except Exception as ex:
     except Exception as ex:
         console.debug(
         console.debug(
             f"Unable to fetch token from {constants.Hosting.HOSTING_JSON} due to: {ex}"
             f"Unable to fetch token from {constants.Hosting.HOSTING_JSON} due to: {ex}"
         )
         )
-        raise Exception("no existing login found") from ex
+    return access_token, invitation_code
 
 
 
 
 def validate_token(token: str):
 def validate_token(token: str):
@@ -59,9 +84,9 @@ def validate_token(token: str):
     """
     """
     try:
     try:
         response = httpx.post(
         response = httpx.post(
-            constants.Hosting.POST_VALIDATE_ME_ENDPOINT,
+            POST_VALIDATE_ME_ENDPOINT,
             headers=authorization_header(token),
             headers=authorization_header(token),
-            timeout=constants.Hosting.HTTP_REQUEST_TIMEOUT,
+            timeout=HTTP_REQUEST_TIMEOUT,
         )
         )
         if response.status_code == HTTPStatus.FORBIDDEN:
         if response.status_code == HTTPStatus.FORBIDDEN:
             raise ValueError
             raise ValueError
@@ -80,14 +105,22 @@ def validate_token(token: str):
         raise Exception("internal errors") from ex
         raise Exception("internal errors") from ex
 
 
 
 
-def delete_token_from_config():
-    """Delete the invalid token from the config file if applicable."""
+def delete_token_from_config(include_invitation_code: bool = False):
+    """Delete the invalid token from the config file if applicable.
+
+    Args:
+        include_invitation_code:
+            Whether to delete the invitation code as well.
+            When user logs out, we delete the invitation code together.
+    """
     if os.path.exists(constants.Hosting.HOSTING_JSON):
     if os.path.exists(constants.Hosting.HOSTING_JSON):
         hosting_config = {}
         hosting_config = {}
         try:
         try:
             with open(constants.Hosting.HOSTING_JSON, "w") as config_file:
             with open(constants.Hosting.HOSTING_JSON, "w") as config_file:
                 hosting_config = json.load(config_file)
                 hosting_config = json.load(config_file)
                 del hosting_config["access_token"]
                 del hosting_config["access_token"]
+                if include_invitation_code:
+                    del hosting_config["code"]
                 json.dump(hosting_config, config_file)
                 json.dump(hosting_config, config_file)
         except Exception as ex:
         except Exception as ex:
             # Best efforts removing invalid token is OK
             # Best efforts removing invalid token is OK
@@ -97,14 +130,11 @@ def delete_token_from_config():
 
 
 
 
 def save_token_to_config(token: str, code: str | None = None):
 def save_token_to_config(token: str, code: str | None = None):
-    """Cache the token, and optionally invitation code to the config file.
+    """Best efforts cache the token, and optionally invitation code to the config file.
 
 
     Args:
     Args:
         token: The access token to save.
         token: The access token to save.
         code: The invitation code to save if exists.
         code: The invitation code to save if exists.
-
-    Raise:
-        Exception: if runs into any issues, file not exist, etc.
     """
     """
     hosting_config: dict[str, str] = {"access_token": token}
     hosting_config: dict[str, str] = {"access_token": token}
     if code:
     if code:
@@ -118,31 +148,23 @@ def save_token_to_config(token: str, code: str | None = None):
         )
         )
 
 
 
 
-def authenticated_token() -> str | None:
+def authenticated_token() -> tuple[str, str]:
     """Fetch the access token from the existing config if applicable and validate it.
     """Fetch the access token from the existing config if applicable and validate it.
 
 
     Returns:
     Returns:
-        The access token if it is valid, None otherwise.
+        The access token and the invitation code.
+        If either is not found, return empty string for it instead.
     """
     """
     # Check if the user is authenticated
     # Check if the user is authenticated
-    try:
-        token, _ = get_existing_access_token()
-        if not token:
-            console.debug("No token found from the existing config.")
-            return None
-        validate_token(token)
-        return token
-    except Exception as ex:
-        console.debug(f"Unable to validate the token from the existing config: {ex}")
-        try:
-            console.debug("Try to delete the invalid token from config file")
-            with open(constants.Hosting.HOSTING_JSON, "rw") as config_file:
-                hosting_config = json.load(config_file)
-                del hosting_config["access_token"]
-                json.dump(hosting_config, config_file)
-        except Exception as ex:
-            console.debug(f"Unable to delete the invalid token from config file: {ex}")
-        return None
+
+    access_token, invitation_code = get_existing_access_token()
+    if not access_token:
+        console.debug("No access token found from the existing config.")
+        access_token = ""
+    elif not validate_token_with_retries(access_token):
+        access_token = ""
+
+    return access_token, invitation_code
 
 
 
 
 def authorization_header(token: str) -> dict[str, str]:
 def authorization_header(token: str) -> dict[str, str]:
@@ -157,6 +179,18 @@ def authorization_header(token: str) -> dict[str, str]:
     return {"Authorization": f"Bearer {token}"}
     return {"Authorization": f"Bearer {token}"}
 
 
 
 
+def requires_authenticated() -> str:
+    """Check if the user is authenticated.
+
+    Returns:
+        The validated access token or empty string if not authenticated.
+    """
+    access_token, invitation_code = authenticated_token()
+    if access_token:
+        return access_token
+    return authenticate_on_browser(invitation_code)
+
+
 class DeploymentPrepInfo(Base):
 class DeploymentPrepInfo(Base):
     """The params/settings returned from the prepare endpoint
     """The params/settings returned from the prepare endpoint
     including the deployment key and the frontend/backend URLs once deployed.
     including the deployment key and the frontend/backend URLs once deployed.
@@ -246,16 +280,16 @@ def prepare_deploy(
         The response containing the backend URLs if successful, None otherwise.
         The response containing the backend URLs if successful, None otherwise.
     """
     """
     # Check if the user is authenticated
     # Check if the user is authenticated
-    if not (token := authenticated_token()):
+    if not (token := requires_authenticated()):
         raise Exception("not authenticated")
         raise Exception("not authenticated")
     try:
     try:
         response = httpx.post(
         response = httpx.post(
-            constants.Hosting.POST_DEPLOYMENTS_PREPARE_ENDPOINT,
+            POST_DEPLOYMENTS_PREPARE_ENDPOINT,
             headers=authorization_header(token),
             headers=authorization_header(token),
             json=DeploymentsPreparePostParam(
             json=DeploymentsPreparePostParam(
                 app_name=app_name, key=key, frontend_hostname=frontend_hostname
                 app_name=app_name, key=key, frontend_hostname=frontend_hostname
             ).dict(exclude_none=True),
             ).dict(exclude_none=True),
-            timeout=constants.Hosting.HTTP_REQUEST_TIMEOUT,
+            timeout=HTTP_REQUEST_TIMEOUT,
         )
         )
 
 
         response_json = response.json()
         response_json = response.json()
@@ -377,7 +411,7 @@ def deploy(
         The response containing the URL of the site to be deployed if successful, None otherwise.
         The response containing the URL of the site to be deployed if successful, None otherwise.
     """
     """
     # Check if the user is authenticated
     # Check if the user is authenticated
-    if not (token := authenticated_token()):
+    if not (token := requires_authenticated()):
         raise Exception("not authenticated")
         raise Exception("not authenticated")
 
 
     try:
     try:
@@ -407,11 +441,15 @@ def deploy(
                 ("files", (backend_file_name, backend_file)),
                 ("files", (backend_file_name, backend_file)),
             ]
             ]
             response = httpx.post(
             response = httpx.post(
-                constants.Hosting.POST_DEPLOYMENTS_ENDPOINT,
+                POST_DEPLOYMENTS_ENDPOINT,
                 headers=authorization_header(token),
                 headers=authorization_header(token),
                 data=params.dict(exclude_none=True),
                 data=params.dict(exclude_none=True),
                 files=files,
                 files=files,
             )
             )
+        # If the server explicitly states bad request,
+        # display a different error
+        if response.status_code == HTTPStatus.BAD_REQUEST:
+            raise ValueError(response.json()["detail"])
         response.raise_for_status()
         response.raise_for_status()
         response_json = response.json()
         response_json = response.json()
         return DeploymentPostResponse(
         return DeploymentPostResponse(
@@ -430,6 +468,9 @@ def deploy(
     except (KeyError, ValidationError) as kve:
     except (KeyError, ValidationError) as kve:
         console.debug(f"Post params or server response format unexpected: {kve}")
         console.debug(f"Post params or server response format unexpected: {kve}")
         raise Exception("internal errors") from kve
         raise Exception("internal errors") from kve
+    except ValueError as ve:
+        console.debug(f"Unable to deploy due to request error: {ve}")
+        raise Exception("request error") from ve
     except Exception as ex:
     except Exception as ex:
         console.debug(f"Unable to deploy due to internal errors: {ex}.")
         console.debug(f"Unable to deploy due to internal errors: {ex}.")
         raise Exception("internal errors") from ex
         raise Exception("internal errors") from ex
@@ -477,17 +518,17 @@ def list_deployments(
     Returns:
     Returns:
         The list of deployments if successful, None otherwise.
         The list of deployments if successful, None otherwise.
     """
     """
-    if not (token := authenticated_token()):
+    if not (token := requires_authenticated()):
         raise Exception("not authenticated")
         raise Exception("not authenticated")
 
 
     params = DeploymentsGetParam(app_name=app_name)
     params = DeploymentsGetParam(app_name=app_name)
 
 
     try:
     try:
         response = httpx.get(
         response = httpx.get(
-            constants.Hosting.GET_DEPLOYMENTS_ENDPOINT,
+            GET_DEPLOYMENTS_ENDPOINT,
             headers=authorization_header(token),
             headers=authorization_header(token),
             params=params.dict(exclude_none=True),
             params=params.dict(exclude_none=True),
-            timeout=constants.Hosting.HTTP_REQUEST_TIMEOUT,
+            timeout=HTTP_REQUEST_TIMEOUT,
         )
         )
         response.raise_for_status()
         response.raise_for_status()
         return [
         return [
@@ -523,34 +564,30 @@ def fetch_token(request_id: str) -> tuple[str, str]:
     Args:
     Args:
         request_id: The request ID used when the user opens the browser for authentication.
         request_id: The request ID used when the user opens the browser for authentication.
 
 
-    Raises:
-        Exception: For request timeout, failed requests, ill-formed responses, unexpected errors.
-
     Returns:
     Returns:
         The access token if it exists, None otherwise.
         The access token if it exists, None otherwise.
     """
     """
+    access_token = invitation_code = ""
     try:
     try:
         resp = httpx.get(
         resp = httpx.get(
-            f"{constants.Hosting.FETCH_TOKEN_ENDPOINT}/{request_id}",
-            timeout=constants.Hosting.HTTP_REQUEST_TIMEOUT,
+            f"{FETCH_TOKEN_ENDPOINT}/{request_id}",
+            timeout=HTTP_REQUEST_TIMEOUT,
         )
         )
         resp.raise_for_status()
         resp.raise_for_status()
-        return (resp_json := resp.json())["access_token"], resp_json.get("code", "")
+        access_token = (resp_json := resp.json()).get("access_token", "")
+        invitation_code = resp_json.get("code", "")
     except httpx.RequestError as re:
     except httpx.RequestError as re:
         console.debug(f"Unable to fetch token due to request error: {re}")
         console.debug(f"Unable to fetch token due to request error: {re}")
-        raise Exception("request timeout") from re
     except httpx.HTTPError as he:
     except httpx.HTTPError as he:
         console.debug(f"Unable to fetch token due to {he}")
         console.debug(f"Unable to fetch token due to {he}")
-        raise Exception("not found") from he
     except json.JSONDecodeError as jde:
     except json.JSONDecodeError as jde:
         console.debug(f"Server did not respond with valid json: {jde}")
         console.debug(f"Server did not respond with valid json: {jde}")
-        raise Exception("internal errors") from jde
     except KeyError as ke:
     except KeyError as ke:
         console.debug(f"Server response format unexpected: {ke}")
         console.debug(f"Server response format unexpected: {ke}")
-        raise Exception("internal errors") from ke
-    except Exception as ex:
+    except Exception:
         console.debug("Unexpected errors: {ex}")
         console.debug("Unexpected errors: {ex}")
-        raise Exception("internal errors") from ex
+
+    return access_token, invitation_code
 
 
 
 
 def poll_backend(backend_url: str) -> bool:
 def poll_backend(backend_url: str) -> bool:
@@ -564,9 +601,7 @@ def poll_backend(backend_url: str) -> bool:
     """
     """
     try:
     try:
         console.debug(f"Polling backend at {backend_url}")
         console.debug(f"Polling backend at {backend_url}")
-        resp = httpx.get(
-            f"{backend_url}/ping", timeout=constants.Hosting.HTTP_REQUEST_TIMEOUT
-        )
+        resp = httpx.get(f"{backend_url}/ping", timeout=HTTP_REQUEST_TIMEOUT)
         resp.raise_for_status()
         resp.raise_for_status()
         return True
         return True
     except httpx.HTTPError:
     except httpx.HTTPError:
@@ -584,9 +619,7 @@ def poll_frontend(frontend_url: str) -> bool:
     """
     """
     try:
     try:
         console.debug(f"Polling frontend at {frontend_url}")
         console.debug(f"Polling frontend at {frontend_url}")
-        resp = httpx.get(
-            f"{frontend_url}", timeout=constants.Hosting.HTTP_REQUEST_TIMEOUT
-        )
+        resp = httpx.get(f"{frontend_url}", timeout=HTTP_REQUEST_TIMEOUT)
         resp.raise_for_status()
         resp.raise_for_status()
         return True
         return True
     except httpx.HTTPError:
     except httpx.HTTPError:
@@ -610,16 +643,16 @@ def delete_deployment(key: str):
         ValueError: If the key is not provided.
         ValueError: If the key is not provided.
         Exception: If the operation fails. The exception message is the reason.
         Exception: If the operation fails. The exception message is the reason.
     """
     """
-    if not (token := authenticated_token()):
+    if not (token := requires_authenticated()):
         raise Exception("not authenticated")
         raise Exception("not authenticated")
     if not key:
     if not key:
         raise ValueError("Valid key is required for the delete.")
         raise ValueError("Valid key is required for the delete.")
 
 
     try:
     try:
         response = httpx.delete(
         response = httpx.delete(
-            f"{constants.Hosting.DELETE_DEPLOYMENTS_ENDPOINT}/{key}",
+            f"{DELETE_DEPLOYMENTS_ENDPOINT}/{key}",
             headers=authorization_header(token),
             headers=authorization_header(token),
-            timeout=constants.Hosting.HTTP_REQUEST_TIMEOUT,
+            timeout=HTTP_REQUEST_TIMEOUT,
         )
         )
         response.raise_for_status()
         response.raise_for_status()
 
 
@@ -691,14 +724,14 @@ def get_deployment_status(key: str) -> DeploymentStatusResponse:
             "A non empty key is required for querying the deployment status."
             "A non empty key is required for querying the deployment status."
         )
         )
 
 
-    if not (token := authenticated_token()):
+    if not (token := requires_authenticated()):
         raise Exception("not authenticated")
         raise Exception("not authenticated")
 
 
     try:
     try:
         response = httpx.get(
         response = httpx.get(
-            f"{constants.Hosting.GET_DEPLOYMENT_STATUS_ENDPOINT}/{key}/status",
+            f"{GET_DEPLOYMENT_STATUS_ENDPOINT}/{key}/status",
             headers=authorization_header(token),
             headers=authorization_header(token),
-            timeout=constants.Hosting.HTTP_REQUEST_TIMEOUT,
+            timeout=HTTP_REQUEST_TIMEOUT,
         )
         )
         response.raise_for_status()
         response.raise_for_status()
         response_json = response.json()
         response_json = response.json()
@@ -769,12 +802,12 @@ async def get_logs(
         Exception: If the operation fails. The exception message is the reason.
         Exception: If the operation fails. The exception message is the reason.
 
 
     """
     """
-    if not (token := authenticated_token()):
+    if not (token := requires_authenticated()):
         raise Exception("not authenticated")
         raise Exception("not authenticated")
     if not key:
     if not key:
         raise ValueError("Valid key is required for querying logs.")
         raise ValueError("Valid key is required for querying logs.")
     try:
     try:
-        logs_endpoint = f"{constants.Hosting.DEPLOYMENT_LOGS_ENDPOINT}/{key}/logs?access_token={token}&log_type={log_type.value}"
+        logs_endpoint = f"{DEPLOYMENT_LOGS_ENDPOINT}/{key}/logs?access_token={token}&log_type={log_type.value}"
         console.debug(f"log server endpoint: {logs_endpoint}")
         console.debug(f"log server endpoint: {logs_endpoint}")
         if from_iso_timestamp is not None:
         if from_iso_timestamp is not None:
             logs_endpoint += (
             logs_endpoint += (
@@ -786,18 +819,22 @@ async def get_logs(
                 row_json = json.loads(await ws.recv())
                 row_json = json.loads(await ws.recv())
                 console.debug(f"Server responded with logs: {row_json}")
                 console.debug(f"Server responded with logs: {row_json}")
                 if row_json and isinstance(row_json, dict):
                 if row_json and isinstance(row_json, dict):
-                    if "timestamp" in row_json:
-                        row_json["timestamp"] = convert_to_local_time(
-                            row_json["timestamp"]
-                        )
-                    print(" | ".join(row_json.values()))
+                    row_to_print = {}
+                    for k, v in row_json.items():
+                        if v is None:
+                            row_to_print[k] = str(v)
+                        elif k == "timestamp":
+                            row_to_print[k] = convert_to_local_time(v)
+                        else:
+                            row_to_print[k] = v
+                    print(" | ".join(row_to_print.values()))
                 else:
                 else:
                     console.debug("Server responded, no new logs, this is normal")
                     console.debug("Server responded, no new logs, this is normal")
     except Exception as ex:
     except Exception as ex:
         console.debug(f"Unable to get more deployment logs due to {ex}.")
         console.debug(f"Unable to get more deployment logs due to {ex}.")
         console.print("Log server disconnected ...")
         console.print("Log server disconnected ...")
         console.print(
         console.print(
-            "Note that the server has limit to only stream logs for several minutes to conserve resources"
+            "Note that the server has limit to only stream logs for several minutes"
         )
         )
 
 
 
 
@@ -814,37 +851,37 @@ def check_requirements_txt_exist():
 
 
 
 
 def authenticate_on_browser(
 def authenticate_on_browser(
-    invitation_code: str | None,
-) -> tuple[str | None, str | None]:
+    invitation_code: str,
+) -> str:
     """Open the browser to authenticate the user.
     """Open the browser to authenticate the user.
 
 
     Args:
     Args:
         invitation_code: The invitation code if it exists.
         invitation_code: The invitation code if it exists.
 
 
-    Raises:
-        SystemExit: If the browser cannot be opened.
-
     Returns:
     Returns:
-        The access token and invitation if valid, Nones otherwise.
+        The access token if valid, empty otherwise.
     """
     """
-    console.print(f"Opening {constants.Hosting.CP_WEB_URL} ...")
+    console.print(f"Opening {config.cp_web_url} ...")
     request_id = uuid.uuid4().hex
     request_id = uuid.uuid4().hex
-    if not webbrowser.open(
-        f"{constants.Hosting.CP_WEB_URL}?request-id={request_id}&code={invitation_code}"
-    ):
-        console.error(
-            f"Unable to open the browser to authenticate. Please contact support."
+    auth_url = f"{config.cp_web_url}?request-id={request_id}&code={invitation_code}"
+    if not webbrowser.open(auth_url):
+        console.warn(
+            f"Unable to automatically open the browser. Please go to {auth_url} to authenticate."
         )
         )
-        raise SystemExit("Unable to open browser for authentication.")
+    access_token = invitation_code = ""
     with console.status("Waiting for access token ..."):
     with console.status("Waiting for access token ..."):
         for _ in range(constants.Hosting.WEB_AUTH_RETRIES):
         for _ in range(constants.Hosting.WEB_AUTH_RETRIES):
-            try:
-                return fetch_token(request_id)
-            except Exception:
-                pass
-            time.sleep(constants.Hosting.WEB_AUTH_SLEEP_DURATION)
+            access_token, invitation_code = fetch_token(request_id)
+            if access_token:
+                break
+            else:
+                time.sleep(constants.Hosting.WEB_AUTH_SLEEP_DURATION)
 
 
-    return None, None
+    if access_token and validate_token_with_retries(access_token):
+        save_token_to_config(access_token, invitation_code)
+    else:
+        access_token = ""
+    return access_token
 
 
 
 
 def validate_token_with_retries(access_token: str) -> bool:
 def validate_token_with_retries(access_token: str) -> bool:
@@ -853,23 +890,21 @@ def validate_token_with_retries(access_token: str) -> bool:
     Args:
     Args:
         access_token: The access token to validate.
         access_token: The access token to validate.
 
 
-    Raises:
-        SystemExit: If the token is confirmed invalid by server.
-
     Returns:
     Returns:
-        True if the token is valid, False otherwise.
+        True if the token is valid,
+        False if invalid or unable to validate.
     """
     """
     with console.status("Validating access token ..."):
     with console.status("Validating access token ..."):
         for _ in range(constants.Hosting.WEB_AUTH_RETRIES):
         for _ in range(constants.Hosting.WEB_AUTH_RETRIES):
             try:
             try:
                 validate_token(access_token)
                 validate_token(access_token)
                 return True
                 return True
-            except ValueError as ve:
+            except ValueError:
                 console.error(f"Access denied")
                 console.error(f"Access denied")
                 delete_token_from_config()
                 delete_token_from_config()
-                raise SystemExit("Access denied") from ve
+                break
             except Exception as ex:
             except Exception as ex:
-                console.debug(f"Unable to validate token due to: {ex}")
+                console.debug(f"Unable to validate token due to: {ex}, trying again")
                 time.sleep(constants.Hosting.WEB_AUTH_SLEEP_DURATION)
                 time.sleep(constants.Hosting.WEB_AUTH_SLEEP_DURATION)
     return False
     return False
 
 
@@ -957,20 +992,17 @@ def process_envs(envs: list[str]) -> dict[str, str]:
 
 
 
 
 def log_out_on_browser():
 def log_out_on_browser():
-    """Open the browser to authenticate the user.
-
-    Raises:
-        SystemExit: If the browser cannot be opened.
-    """
+    """Open the browser to authenticate the user."""
     # Fetching existing invitation code so user sees the log out page without having to enter it
     # Fetching existing invitation code so user sees the log out page without having to enter it
     invitation_code = None
     invitation_code = None
     with contextlib.suppress(Exception):
     with contextlib.suppress(Exception):
         _, invitation_code = get_existing_access_token()
         _, invitation_code = get_existing_access_token()
         console.debug("Found existing invitation code in config")
         console.debug("Found existing invitation code in config")
-    console.print(f"Opening {constants.Hosting.CP_WEB_URL} ...")
-    if not webbrowser.open(f"{constants.Hosting.CP_WEB_URL}?code={invitation_code}"):
-        raise SystemExit(
-            f"Unable to open the browser to log out. Please contact support."
+        delete_token_from_config()
+    console.print(f"Opening {config.cp_web_url} ...")
+    if not webbrowser.open(f"{config.cp_web_url}?code={invitation_code}"):
+        console.warn(
+            f"Unable to open the browser automatically. Please go to {config.cp_web_url} to log out."
         )
         )
 
 
 
 
@@ -987,16 +1019,16 @@ async def display_deploy_milestones(key: str, from_iso_timestamp: datetime):
     """
     """
     if not key:
     if not key:
         raise ValueError("Non-empty key is required for querying deploy status.")
         raise ValueError("Non-empty key is required for querying deploy status.")
-    if not (token := authenticated_token()):
+    if not (token := requires_authenticated()):
         raise Exception("not authenticated")
         raise Exception("not authenticated")
 
 
     try:
     try:
-        logs_endpoint = f"{constants.Hosting.DEPLOYMENT_LOGS_ENDPOINT}/{key}/logs?access_token={token}&log_type={LogType.DEPLOY_LOG.value}&from_iso_timestamp={from_iso_timestamp.astimezone().isoformat()}"
+        logs_endpoint = f"{DEPLOYMENT_LOGS_ENDPOINT}/{key}/logs?access_token={token}&log_type={LogType.DEPLOY_LOG.value}&from_iso_timestamp={from_iso_timestamp.astimezone().isoformat()}"
         console.debug(f"log server endpoint: {logs_endpoint}")
         console.debug(f"log server endpoint: {logs_endpoint}")
         _ws = websockets.connect(logs_endpoint)  # type: ignore
         _ws = websockets.connect(logs_endpoint)  # type: ignore
         async with _ws as ws:
         async with _ws as ws:
             # Stream back the deploy events reported back from the server
             # Stream back the deploy events reported back from the server
-            for _ in range(constants.Hosting.DEPLOYMENT_EVENT_MESSAGES_RETRIES):
+            for _ in range(DEPLOYMENT_EVENT_MESSAGES_RETRIES):
                 row_json = json.loads(await ws.recv())
                 row_json = json.loads(await ws.recv())
                 console.debug(f"Server responded with: {row_json}")
                 console.debug(f"Server responded with: {row_json}")
                 if row_json and isinstance(row_json, dict):
                 if row_json and isinstance(row_json, dict):
@@ -1011,7 +1043,7 @@ async def display_deploy_milestones(key: str, from_iso_timestamp: datetime):
                     )
                     )
                     if any(
                     if any(
                         msg in row_json["message"].lower()
                         msg in row_json["message"].lower()
-                        for msg in constants.Hosting.END_OF_DEPLOYMENT_MESSAGES
+                        for msg in END_OF_DEPLOYMENT_MESSAGES
                     ):
                     ):
                         console.debug(
                         console.debug(
                             "Received end of deployment message, stop event message streaming"
                             "Received end of deployment message, stop event message streaming"
@@ -1026,9 +1058,9 @@ async def display_deploy_milestones(key: str, from_iso_timestamp: datetime):
 def wait_for_server_to_pick_up_request():
 def wait_for_server_to_pick_up_request():
     """Wait for server to pick up the request. Right now is just sleep."""
     """Wait for server to pick up the request. Right now is just sleep."""
     with console.status(
     with console.status(
-        f"Waiting for server to pick up request ~ {constants.Hosting.DEPLOYMENT_PICKUP_DELAY} seconds ..."
+        f"Waiting for server to pick up request ~ {DEPLOYMENT_PICKUP_DELAY} seconds ..."
     ):
     ):
-        for _ in range(constants.Hosting.DEPLOYMENT_PICKUP_DELAY):
+        for _ in range(DEPLOYMENT_PICKUP_DELAY):
             time.sleep(1)
             time.sleep(1)
 
 
 
 
@@ -1040,11 +1072,11 @@ def interactive_prompt_for_envs() -> list[str]:
     """
     """
     envs = []
     envs = []
     envs_finished = False
     envs_finished = False
-    env_key_prompt = "  Env name (enter to skip)"
+    env_count = 1
+    env_key_prompt = f" * env-{env_count} name (enter to skip)"
     console.print("Environment variables ...")
     console.print("Environment variables ...")
     while not envs_finished:
     while not envs_finished:
         env_key = console.ask(env_key_prompt)
         env_key = console.ask(env_key_prompt)
-        env_key_prompt = "  env name (enter to finish)"
         if not env_key:
         if not env_key:
             envs_finished = True
             envs_finished = True
             if envs:
             if envs:
@@ -1053,6 +1085,34 @@ def interactive_prompt_for_envs() -> list[str]:
                 console.print("No envs added. Continuing ...")
                 console.print("No envs added. Continuing ...")
             break
             break
         # If it possible to have empty values for env, so we do not check here
         # If it possible to have empty values for env, so we do not check here
-        env_value = console.ask("  env value")
+        env_value = console.ask(f"   env-{env_count} value")
         envs.append(f"{env_key}={env_value}")
         envs.append(f"{env_key}={env_value}")
+        env_count += 1
+        env_key_prompt = f" * env-{env_count} name (enter to skip)"
     return envs
     return envs
+
+
+def get_regions() -> list[dict]:
+    """Get the supported regions from the hosting server.
+
+    Returns:
+        A list of dict representation of the region information.
+    """
+    try:
+        response = httpx.get(
+            GET_REGIONS_ENDPOINT,
+            timeout=HTTP_REQUEST_TIMEOUT,
+        )
+        response.raise_for_status()
+        response_json = response.json()
+        assert response_json and isinstance(
+            response_json, list
+        ), "Expect server to return a list "
+        assert not response_json or (
+            response_json[0] is not None and isinstance(response_json[0], dict)
+        ), "Expect return values are dict's"
+
+        return response_json
+    except Exception as ex:
+        console.debug(f"Unable to get regions due to {ex}.")
+        return []

+ 12 - 52
tests/test_reflex.py

@@ -10,74 +10,34 @@ from reflex.utils.hosting import DeploymentPrepInfo
 runner = CliRunner()
 runner = CliRunner()
 
 
 
 
-def test_login_success(mocker):
-    mock_get_existing_access_token = mocker.patch(
-        "reflex.utils.hosting.get_existing_access_token",
-        return_value=("fake-token", "fake-code"),
-    )
-    mock_validate_token = mocker.patch(
-        "reflex.utils.hosting.validate_token_with_retries"
-    )
-    result = runner.invoke(cli, ["login"])
-    assert result.exit_code == 0
-    mock_get_existing_access_token.assert_called_once()
-    mock_validate_token.assert_called_once_with("fake-token")
-
-
-def test_login_existing_token_but_invalid(mocker):
+def test_login_success_existing_token(mocker):
     mocker.patch(
     mocker.patch(
-        "reflex.utils.hosting.get_existing_access_token",
+        "reflex.utils.hosting.authenticated_token",
         return_value=("fake-token", "fake-code"),
         return_value=("fake-token", "fake-code"),
     )
     )
-    mocker.patch(
-        "reflex.utils.hosting.validate_token",
-        side_effect=ValueError("token not valid"),
-    )
-    mock_delete_token_from_config = mocker.patch(
-        "reflex.utils.hosting.delete_token_from_config"
-    )
     result = runner.invoke(cli, ["login"])
     result = runner.invoke(cli, ["login"])
-    assert result.exit_code == 1
-    # Make sure the invalid token delete is performed
-    mock_delete_token_from_config.assert_called_once()
-
+    assert result.exit_code == 0
 
 
-def test_login_no_existing_token_fetched_valid(mocker):
-    # Access token does not exist, but user authenticates successfully on browser.
-    mocker.patch(
-        "reflex.utils.hosting.get_existing_access_token",
-        side_effect=Exception("no token found"),
-    )
 
 
-    # Token is fetched successfully
+def test_login_success_on_browser(mocker):
     mocker.patch(
     mocker.patch(
-        "reflex.utils.hosting.authenticate_on_browser",
-        return_value=("fake-token2", "fake-code2"),
-    )
-    mock_validate_token = mocker.patch(
-        "reflex.utils.hosting.validate_token_with_retries"
+        "reflex.utils.hosting.authenticated_token",
+        return_value=("", "fake-code"),
     )
     )
-    mock_save_token_to_config = mocker.patch(
-        "reflex.utils.hosting.save_token_to_config"
+    mock_authenticate_on_browser = mocker.patch(
+        "reflex.utils.hosting.authenticate_on_browser", return_value="fake-token"
     )
     )
     result = runner.invoke(cli, ["login"])
     result = runner.invoke(cli, ["login"])
     assert result.exit_code == 0
     assert result.exit_code == 0
-    mock_validate_token.assert_called_once_with(
-        "fake-token2",
-    )
-    mock_save_token_to_config.assert_called_once_with("fake-token2", "fake-code2")
+    mock_authenticate_on_browser.assert_called_once_with("fake-code")
 
 
 
 
-def test_login_no_existing_token_fetch_none(mocker):
+def test_login_fail(mocker):
     # Access token does not exist, but user authenticates successfully on browser.
     # Access token does not exist, but user authenticates successfully on browser.
     mocker.patch(
     mocker.patch(
-        "reflex.utils.hosting.get_existing_access_token",
-        side_effect=Exception("no token found"),
-    )
-    # Token is not fetched
-    mocker.patch(
-        "reflex.utils.hosting.authenticate_on_browser", return_value=(None, None)
+        "reflex.utils.hosting.get_existing_access_token", return_value=("", "")
     )
     )
+    mocker.patch("reflex.utils.hosting.authenticate_on_browser", return_value="")
     result = runner.invoke(cli, ["login"])
     result = runner.invoke(cli, ["login"])
     assert result.exit_code == 1
     assert result.exit_code == 1
 
 

+ 37 - 33
tests/utils/test_hosting.py

@@ -14,7 +14,7 @@ def test_get_existing_access_token_and_no_invitation_code(mocker):
     mocker.patch("builtins.open", mock_open(read_data=json.dumps(mock_hosting_config)))
     mocker.patch("builtins.open", mock_open(read_data=json.dumps(mock_hosting_config)))
     token, code = hosting.get_existing_access_token()
     token, code = hosting.get_existing_access_token()
     assert token == mock_hosting_config["access_token"]
     assert token == mock_hosting_config["access_token"]
-    assert code is None
+    assert code == ""
 
 
 
 
 def test_get_existing_access_token_and_invitation_code(mocker):
 def test_get_existing_access_token_and_invitation_code(mocker):
@@ -28,35 +28,37 @@ def test_get_existing_access_token_and_invitation_code(mocker):
 
 
 def test_no_existing_access_token(mocker):
 def test_no_existing_access_token(mocker):
     # Config file does not have access token
     # Config file does not have access token
-    mock_hosting_config = {"code": "fake_code"}
-    mocker.patch("builtins.open", mock_open(read_data=json.dumps(mock_hosting_config)))
-    with pytest.raises(Exception):
-        token, _ = hosting.get_existing_access_token()
-        assert token is None
+    mocker.patch(
+        "builtins.open",
+        mock_open(read_data=json.dumps({"no-token": "here", "no-code": "here"})),
+    )
+    access_token, invitation_code = hosting.get_existing_access_token()
+    assert access_token == ""
+    assert invitation_code == ""
 
 
 
 
 def test_no_config_file(mocker):
 def test_no_config_file(mocker):
     # Config file not exist
     # Config file not exist
     mocker.patch("builtins.open", side_effect=FileNotFoundError)
     mocker.patch("builtins.open", side_effect=FileNotFoundError)
-    with pytest.raises(Exception) as ex:
-        hosting.get_existing_access_token()
-        assert ex.value == "No existing login found"
+    access_token, invitation_code = hosting.get_existing_access_token()
+    assert access_token == ""
+    assert invitation_code == ""
 
 
 
 
 def test_empty_config_file(mocker):
 def test_empty_config_file(mocker):
     # Config file is empty
     # Config file is empty
     mocker.patch("builtins.open", mock_open(read_data=""))
     mocker.patch("builtins.open", mock_open(read_data=""))
-    with pytest.raises(Exception) as ex:
-        hosting.get_existing_access_token()
-        assert ex.value == "No existing login found"
+    access_token, invitation_code = hosting.get_existing_access_token()
+    assert access_token == ""
+    assert invitation_code == ""
 
 
 
 
 def test_invalid_json_config_file(mocker):
 def test_invalid_json_config_file(mocker):
     # Config file content is not valid json
     # Config file content is not valid json
     mocker.patch("builtins.open", mock_open(read_data="im not json content"))
     mocker.patch("builtins.open", mock_open(read_data="im not json content"))
-    with pytest.raises(Exception) as ex:
-        hosting.get_existing_access_token()
-        assert ex.value == "No existing login found"
+    access_token, invitation_code = hosting.get_existing_access_token()
+    assert access_token == ""
+    assert invitation_code == ""
 
 
 
 
 def test_validate_token_success(mocker):
 def test_validate_token_success(mocker):
@@ -124,20 +126,21 @@ def test_save_access_code_but_none_invitation_code_to_config(mocker):
 
 
 def test_authenticated_token_success(mocker):
 def test_authenticated_token_success(mocker):
     access_token = "fake_token"
     access_token = "fake_token"
+    invitation_code = "fake_code"
     mocker.patch(
     mocker.patch(
         "reflex.utils.hosting.get_existing_access_token",
         "reflex.utils.hosting.get_existing_access_token",
-        return_value=(access_token, "fake_code"),
+        return_value=(access_token, invitation_code),
     )
     )
-    mocker.patch("reflex.utils.hosting.validate_token")
-    assert hosting.authenticated_token() == access_token
+    mocker.patch("reflex.utils.hosting.validate_token_with_retries", return_value=True)
+    assert hosting.authenticated_token() == (access_token, invitation_code)
 
 
 
 
 def test_no_authenticated_token(mocker):
 def test_no_authenticated_token(mocker):
     mocker.patch(
     mocker.patch(
         "reflex.utils.hosting.get_existing_access_token",
         "reflex.utils.hosting.get_existing_access_token",
-        return_value=(None, None),
+        return_value=("", "code-does-not-matter"),
     )
     )
-    assert hosting.authenticated_token() is None
+    assert hosting.authenticated_token()[0] == ""
 
 
 
 
 def test_maybe_authenticated_token_is_invalid(mocker):
 def test_maybe_authenticated_token_is_invalid(mocker):
@@ -145,30 +148,30 @@ def test_maybe_authenticated_token_is_invalid(mocker):
         "reflex.utils.hosting.get_existing_access_token",
         "reflex.utils.hosting.get_existing_access_token",
         return_value=("invalid_token", "fake_code"),
         return_value=("invalid_token", "fake_code"),
     )
     )
-    mocker.patch("reflex.utils.hosting.validate_token", side_effect=ValueError)
-    mocker.patch("builtins.open")
-    mocker.patch("json.load")
-    mock_json_dump = mocker.patch("json.dump")
-    assert hosting.authenticated_token() is None
-    mock_json_dump.assert_called_once()
+    mocker.patch("reflex.utils.hosting.validate_token_with_retries", return_value=False)
+    assert hosting.authenticated_token()[0] == ""
 
 
 
 
 def test_prepare_deploy_not_authenticated(mocker):
 def test_prepare_deploy_not_authenticated(mocker):
-    mocker.patch("reflex.utils.hosting.authenticated_token", return_value=None)
+    mocker.patch("reflex.utils.hosting.requires_authenticated", return_value=None)
     with pytest.raises(Exception) as ex:
     with pytest.raises(Exception) as ex:
         hosting.prepare_deploy("fake-app")
         hosting.prepare_deploy("fake-app")
         assert ex.value == "Not authenticated"
         assert ex.value == "Not authenticated"
 
 
 
 
 def test_server_unable_to_prepare_deploy(mocker):
 def test_server_unable_to_prepare_deploy(mocker):
-    mocker.patch("reflex.utils.hosting.authenticated_token", return_value="fake_token")
+    mocker.patch(
+        "reflex.utils.hosting.requires_authenticated", return_value="fake_token"
+    )
     mocker.patch("httpx.post", return_value=httpx.Response(500))
     mocker.patch("httpx.post", return_value=httpx.Response(500))
     with pytest.raises(Exception):
     with pytest.raises(Exception):
         hosting.prepare_deploy("fake-app")
         hosting.prepare_deploy("fake-app")
 
 
 
 
 def test_prepare_deploy_success(mocker):
 def test_prepare_deploy_success(mocker):
-    mocker.patch("reflex.utils.hosting.authenticated_token", return_value="fake_token")
+    mocker.patch(
+        "reflex.utils.hosting.requires_authenticated", return_value="fake_token"
+    )
     mocker.patch(
     mocker.patch(
         "httpx.post",
         "httpx.post",
         return_value=Mock(
         return_value=Mock(
@@ -190,7 +193,9 @@ def test_prepare_deploy_success(mocker):
 
 
 
 
 def test_deploy(mocker):
 def test_deploy(mocker):
-    mocker.patch("reflex.utils.hosting.authenticated_token", return_value="fake_token")
+    mocker.patch(
+        "reflex.utils.hosting.requires_authenticated", return_value="fake_token"
+    )
     mocker.patch("builtins.open")
     mocker.patch("builtins.open")
     mocker.patch(
     mocker.patch(
         "httpx.post",
         "httpx.post",
@@ -224,14 +229,13 @@ def test_validate_token_with_retries_failed(mocker):
     assert mock_delete_token.call_count == 0
     assert mock_delete_token.call_count == 0
 
 
 
 
-def test_validate_token_access_denied(mocker):
+def test_validate_token_with_retries_access_denied(mocker):
     mock_validate_token = mocker.patch(
     mock_validate_token = mocker.patch(
         "reflex.utils.hosting.validate_token", side_effect=ValueError
         "reflex.utils.hosting.validate_token", side_effect=ValueError
     )
     )
     mock_delete_token = mocker.patch("reflex.utils.hosting.delete_token_from_config")
     mock_delete_token = mocker.patch("reflex.utils.hosting.delete_token_from_config")
     mocker.patch("time.sleep")
     mocker.patch("time.sleep")
-    with pytest.raises(SystemExit):
-        hosting.validate_token_with_retries("fake-token")
+    assert hosting.validate_token_with_retries("fake-token") is False
     assert mock_validate_token.call_count == 1
     assert mock_validate_token.call_count == 1
     assert mock_delete_token.call_count == 1
     assert mock_delete_token.call_count == 1