CLI switch to prod server (#2016)
This commit is contained in:
parent
53566c2adf
commit
81053618c9
@ -190,6 +190,11 @@ class Config(Base):
|
||||
# The rxdeploy url.
|
||||
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.
|
||||
username: Optional[str] = None
|
||||
|
||||
|
@ -66,6 +66,8 @@ class Config(Base):
|
||||
event_namespace: Optional[str]
|
||||
frontend_packages: List[str]
|
||||
rxdeploy_url: Optional[str]
|
||||
cp_backend_url: str
|
||||
cp_web_url: str
|
||||
username: Optional[str]
|
||||
|
||||
def __init__(
|
||||
@ -90,6 +92,8 @@ class Config(Base):
|
||||
event_namespace: Optional[str] = None,
|
||||
frontend_packages: Optional[List[str]] = None,
|
||||
rxdeploy_url: Optional[str] = None,
|
||||
cp_backend_url: Optional[str] = None,
|
||||
cp_web_url: Optional[str] = None,
|
||||
username: Optional[str] = None,
|
||||
**kwargs
|
||||
) -> None: ...
|
||||
|
@ -10,40 +10,15 @@ class Hosting:
|
||||
# The hosting config json file
|
||||
HOSTING_JSON = os.path.join(Reflex.DIR, "hosting_v0.json")
|
||||
# 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
|
||||
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.
|
||||
WEB_AUTH_RETRIES = 60
|
||||
# The time to sleep between requests to check if for authentication completion. In seconds.
|
||||
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.
|
||||
BACKEND_POLL_RETRIES = 30
|
||||
# The time to wait for the frontend to come up after user initiates deployment. In seconds.
|
||||
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
|
||||
|
@ -2,7 +2,6 @@
|
||||
|
||||
import asyncio
|
||||
import atexit
|
||||
import contextlib
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
@ -276,6 +275,11 @@ def export(
|
||||
help="The directory to export the zip files to.",
|
||||
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(
|
||||
console._LOG_LEVEL, help="The log level to use."
|
||||
),
|
||||
@ -306,6 +310,7 @@ def export(
|
||||
zip=zipping,
|
||||
zip_dest_dir=zip_dest_dir,
|
||||
deploy_url=config.deploy_url,
|
||||
backend_exclude_sqlite_db_files=backend_exclude_sqlite_db_files,
|
||||
)
|
||||
|
||||
# Post a telemetry event.
|
||||
@ -322,39 +327,19 @@ def login(
|
||||
# Set the log level.
|
||||
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 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:
|
||||
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)
|
||||
|
||||
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()
|
||||
@ -368,7 +353,7 @@ def logout(
|
||||
|
||||
hosting.log_out_on_browser()
|
||||
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()
|
||||
@ -483,6 +468,10 @@ def deploy(
|
||||
None,
|
||||
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(
|
||||
config.loglevel, help="The log level to use."
|
||||
),
|
||||
@ -569,6 +558,7 @@ def deploy(
|
||||
zipping=True,
|
||||
zip_dest_dir=tmp_dir,
|
||||
loglevel=loglevel,
|
||||
backend_exclude_sqlite_db_files=backend_exclude_sqlite_db_files,
|
||||
)
|
||||
except ImportError as ie:
|
||||
console.error(
|
||||
@ -654,7 +644,7 @@ def deploy(
|
||||
)
|
||||
return
|
||||
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
|
||||
|
||||
|
||||
@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(
|
||||
deployments_cli,
|
||||
|
@ -60,7 +60,7 @@ def _zip(
|
||||
target: str,
|
||||
root_dir: str,
|
||||
exclude_venv_dirs: bool,
|
||||
exclude_sqlite_db_files: bool,
|
||||
exclude_sqlite_db_files: bool = True,
|
||||
dirs_to_exclude: set[str] | None = None,
|
||||
files_to_exclude: set[str] | None = None,
|
||||
) -> None:
|
||||
@ -127,6 +127,7 @@ def export(
|
||||
zip: bool = False,
|
||||
zip_dest_dir: str = os.getcwd(),
|
||||
deploy_url: str | None = None,
|
||||
backend_exclude_sqlite_db_files: bool = True,
|
||||
):
|
||||
"""Export the app for deployment.
|
||||
|
||||
@ -136,6 +137,7 @@ def export(
|
||||
zip: Whether to zip the app.
|
||||
zip_dest_dir: The destination directory for created zip files (if any)
|
||||
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.
|
||||
path_ops.rm(constants.Dirs.WEB_STATIC)
|
||||
@ -183,7 +185,6 @@ def export(
|
||||
root_dir=".web/_static",
|
||||
files_to_exclude=files_to_exclude,
|
||||
exclude_venv_dirs=False,
|
||||
exclude_sqlite_db_files=False,
|
||||
)
|
||||
if backend:
|
||||
_zip(
|
||||
@ -195,7 +196,7 @@ def export(
|
||||
dirs_to_exclude={"assets", "__pycache__"},
|
||||
files_to_exclude=files_to_exclude,
|
||||
exclude_venv_dirs=True,
|
||||
exclude_sqlite_db_files=True,
|
||||
exclude_sqlite_db_files=backend_exclude_sqlite_db_files,
|
||||
)
|
||||
|
||||
|
||||
|
@ -19,32 +19,57 @@ from pydantic import Field, ValidationError, root_validator
|
||||
|
||||
from reflex import constants
|
||||
from reflex.base import Base
|
||||
from reflex.config import get_config
|
||||
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]:
|
||||
"""Fetch the access token from the existing config if applicable.
|
||||
|
||||
Raises:
|
||||
Exception: if runs into any issues, file not exist, ill-formatted, etc.
|
||||
|
||||
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...")
|
||||
access_token = invitation_code = ""
|
||||
try:
|
||||
with open(constants.Hosting.HOSTING_JSON, "r") as 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:
|
||||
console.debug(
|
||||
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):
|
||||
@ -59,9 +84,9 @@ def validate_token(token: str):
|
||||
"""
|
||||
try:
|
||||
response = httpx.post(
|
||||
constants.Hosting.POST_VALIDATE_ME_ENDPOINT,
|
||||
POST_VALIDATE_ME_ENDPOINT,
|
||||
headers=authorization_header(token),
|
||||
timeout=constants.Hosting.HTTP_REQUEST_TIMEOUT,
|
||||
timeout=HTTP_REQUEST_TIMEOUT,
|
||||
)
|
||||
if response.status_code == HTTPStatus.FORBIDDEN:
|
||||
raise ValueError
|
||||
@ -80,14 +105,22 @@ def validate_token(token: str):
|
||||
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):
|
||||
hosting_config = {}
|
||||
try:
|
||||
with open(constants.Hosting.HOSTING_JSON, "w") as config_file:
|
||||
hosting_config = json.load(config_file)
|
||||
del hosting_config["access_token"]
|
||||
if include_invitation_code:
|
||||
del hosting_config["code"]
|
||||
json.dump(hosting_config, config_file)
|
||||
except Exception as ex:
|
||||
# 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):
|
||||
"""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:
|
||||
token: The access token to save.
|
||||
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}
|
||||
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.
|
||||
|
||||
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
|
||||
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]:
|
||||
@ -157,6 +179,18 @@ def authorization_header(token: str) -> dict[str, str]:
|
||||
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):
|
||||
"""The params/settings returned from the prepare endpoint
|
||||
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.
|
||||
"""
|
||||
# Check if the user is authenticated
|
||||
if not (token := authenticated_token()):
|
||||
if not (token := requires_authenticated()):
|
||||
raise Exception("not authenticated")
|
||||
try:
|
||||
response = httpx.post(
|
||||
constants.Hosting.POST_DEPLOYMENTS_PREPARE_ENDPOINT,
|
||||
POST_DEPLOYMENTS_PREPARE_ENDPOINT,
|
||||
headers=authorization_header(token),
|
||||
json=DeploymentsPreparePostParam(
|
||||
app_name=app_name, key=key, frontend_hostname=frontend_hostname
|
||||
).dict(exclude_none=True),
|
||||
timeout=constants.Hosting.HTTP_REQUEST_TIMEOUT,
|
||||
timeout=HTTP_REQUEST_TIMEOUT,
|
||||
)
|
||||
|
||||
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.
|
||||
"""
|
||||
# Check if the user is authenticated
|
||||
if not (token := authenticated_token()):
|
||||
if not (token := requires_authenticated()):
|
||||
raise Exception("not authenticated")
|
||||
|
||||
try:
|
||||
@ -407,11 +441,15 @@ def deploy(
|
||||
("files", (backend_file_name, backend_file)),
|
||||
]
|
||||
response = httpx.post(
|
||||
constants.Hosting.POST_DEPLOYMENTS_ENDPOINT,
|
||||
POST_DEPLOYMENTS_ENDPOINT,
|
||||
headers=authorization_header(token),
|
||||
data=params.dict(exclude_none=True),
|
||||
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_json = response.json()
|
||||
return DeploymentPostResponse(
|
||||
@ -430,6 +468,9 @@ def deploy(
|
||||
except (KeyError, ValidationError) as kve:
|
||||
console.debug(f"Post params or server response format unexpected: {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:
|
||||
console.debug(f"Unable to deploy due to internal errors: {ex}.")
|
||||
raise Exception("internal errors") from ex
|
||||
@ -477,17 +518,17 @@ def list_deployments(
|
||||
Returns:
|
||||
The list of deployments if successful, None otherwise.
|
||||
"""
|
||||
if not (token := authenticated_token()):
|
||||
if not (token := requires_authenticated()):
|
||||
raise Exception("not authenticated")
|
||||
|
||||
params = DeploymentsGetParam(app_name=app_name)
|
||||
|
||||
try:
|
||||
response = httpx.get(
|
||||
constants.Hosting.GET_DEPLOYMENTS_ENDPOINT,
|
||||
GET_DEPLOYMENTS_ENDPOINT,
|
||||
headers=authorization_header(token),
|
||||
params=params.dict(exclude_none=True),
|
||||
timeout=constants.Hosting.HTTP_REQUEST_TIMEOUT,
|
||||
timeout=HTTP_REQUEST_TIMEOUT,
|
||||
)
|
||||
response.raise_for_status()
|
||||
return [
|
||||
@ -523,34 +564,30 @@ def fetch_token(request_id: str) -> tuple[str, str]:
|
||||
Args:
|
||||
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:
|
||||
The access token if it exists, None otherwise.
|
||||
"""
|
||||
access_token = invitation_code = ""
|
||||
try:
|
||||
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()
|
||||
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:
|
||||
console.debug(f"Unable to fetch token due to request error: {re}")
|
||||
raise Exception("request timeout") from re
|
||||
except httpx.HTTPError as he:
|
||||
console.debug(f"Unable to fetch token due to {he}")
|
||||
raise Exception("not found") from he
|
||||
except json.JSONDecodeError as jde:
|
||||
console.debug(f"Server did not respond with valid json: {jde}")
|
||||
raise Exception("internal errors") from jde
|
||||
except KeyError as 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}")
|
||||
raise Exception("internal errors") from ex
|
||||
|
||||
return access_token, invitation_code
|
||||
|
||||
|
||||
def poll_backend(backend_url: str) -> bool:
|
||||
@ -564,9 +601,7 @@ def poll_backend(backend_url: str) -> bool:
|
||||
"""
|
||||
try:
|
||||
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()
|
||||
return True
|
||||
except httpx.HTTPError:
|
||||
@ -584,9 +619,7 @@ def poll_frontend(frontend_url: str) -> bool:
|
||||
"""
|
||||
try:
|
||||
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()
|
||||
return True
|
||||
except httpx.HTTPError:
|
||||
@ -610,16 +643,16 @@ def delete_deployment(key: str):
|
||||
ValueError: If the key is not provided.
|
||||
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")
|
||||
if not key:
|
||||
raise ValueError("Valid key is required for the delete.")
|
||||
|
||||
try:
|
||||
response = httpx.delete(
|
||||
f"{constants.Hosting.DELETE_DEPLOYMENTS_ENDPOINT}/{key}",
|
||||
f"{DELETE_DEPLOYMENTS_ENDPOINT}/{key}",
|
||||
headers=authorization_header(token),
|
||||
timeout=constants.Hosting.HTTP_REQUEST_TIMEOUT,
|
||||
timeout=HTTP_REQUEST_TIMEOUT,
|
||||
)
|
||||
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."
|
||||
)
|
||||
|
||||
if not (token := authenticated_token()):
|
||||
if not (token := requires_authenticated()):
|
||||
raise Exception("not authenticated")
|
||||
|
||||
try:
|
||||
response = httpx.get(
|
||||
f"{constants.Hosting.GET_DEPLOYMENT_STATUS_ENDPOINT}/{key}/status",
|
||||
f"{GET_DEPLOYMENT_STATUS_ENDPOINT}/{key}/status",
|
||||
headers=authorization_header(token),
|
||||
timeout=constants.Hosting.HTTP_REQUEST_TIMEOUT,
|
||||
timeout=HTTP_REQUEST_TIMEOUT,
|
||||
)
|
||||
response.raise_for_status()
|
||||
response_json = response.json()
|
||||
@ -769,12 +802,12 @@ async def get_logs(
|
||||
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")
|
||||
if not key:
|
||||
raise ValueError("Valid key is required for querying logs.")
|
||||
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}")
|
||||
if from_iso_timestamp is not None:
|
||||
logs_endpoint += (
|
||||
@ -786,18 +819,22 @@ async def get_logs(
|
||||
row_json = json.loads(await ws.recv())
|
||||
console.debug(f"Server responded with logs: {row_json}")
|
||||
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:
|
||||
console.debug("Server responded, no new logs, this is normal")
|
||||
except Exception as ex:
|
||||
console.debug(f"Unable to get more deployment logs due to {ex}.")
|
||||
console.print("Log server disconnected ...")
|
||||
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(
|
||||
invitation_code: str | None,
|
||||
) -> tuple[str | None, str | None]:
|
||||
invitation_code: str,
|
||||
) -> str:
|
||||
"""Open the browser to authenticate the user.
|
||||
|
||||
Args:
|
||||
invitation_code: The invitation code if it exists.
|
||||
|
||||
Raises:
|
||||
SystemExit: If the browser cannot be opened.
|
||||
|
||||
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
|
||||
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 ..."):
|
||||
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:
|
||||
@ -853,23 +890,21 @@ def validate_token_with_retries(access_token: str) -> bool:
|
||||
Args:
|
||||
access_token: The access token to validate.
|
||||
|
||||
Raises:
|
||||
SystemExit: If the token is confirmed invalid by server.
|
||||
|
||||
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 ..."):
|
||||
for _ in range(constants.Hosting.WEB_AUTH_RETRIES):
|
||||
try:
|
||||
validate_token(access_token)
|
||||
return True
|
||||
except ValueError as ve:
|
||||
except ValueError:
|
||||
console.error(f"Access denied")
|
||||
delete_token_from_config()
|
||||
raise SystemExit("Access denied") from ve
|
||||
break
|
||||
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)
|
||||
return False
|
||||
|
||||
@ -957,20 +992,17 @@ def process_envs(envs: list[str]) -> dict[str, str]:
|
||||
|
||||
|
||||
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
|
||||
invitation_code = None
|
||||
with contextlib.suppress(Exception):
|
||||
_, invitation_code = get_existing_access_token()
|
||||
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:
|
||||
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")
|
||||
|
||||
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}")
|
||||
_ws = websockets.connect(logs_endpoint) # type: ignore
|
||||
async with _ws as ws:
|
||||
# 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())
|
||||
console.debug(f"Server responded with: {row_json}")
|
||||
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(
|
||||
msg in row_json["message"].lower()
|
||||
for msg in constants.Hosting.END_OF_DEPLOYMENT_MESSAGES
|
||||
for msg in END_OF_DEPLOYMENT_MESSAGES
|
||||
):
|
||||
console.debug(
|
||||
"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():
|
||||
"""Wait for server to pick up the request. Right now is just sleep."""
|
||||
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)
|
||||
|
||||
|
||||
@ -1040,11 +1072,11 @@ def interactive_prompt_for_envs() -> list[str]:
|
||||
"""
|
||||
envs = []
|
||||
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 ...")
|
||||
while not envs_finished:
|
||||
env_key = console.ask(env_key_prompt)
|
||||
env_key_prompt = " env name (enter to finish)"
|
||||
if not env_key:
|
||||
envs_finished = True
|
||||
if envs:
|
||||
@ -1053,6 +1085,34 @@ def interactive_prompt_for_envs() -> list[str]:
|
||||
console.print("No envs added. Continuing ...")
|
||||
break
|
||||
# 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}")
|
||||
env_count += 1
|
||||
env_key_prompt = f" * env-{env_count} name (enter to skip)"
|
||||
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 []
|
||||
|
@ -10,74 +10,34 @@ from reflex.utils.hosting import DeploymentPrepInfo
|
||||
runner = CliRunner()
|
||||
|
||||
|
||||
def test_login_success(mocker):
|
||||
mock_get_existing_access_token = mocker.patch(
|
||||
"reflex.utils.hosting.get_existing_access_token",
|
||||
def test_login_success_existing_token(mocker):
|
||||
mocker.patch(
|
||||
"reflex.utils.hosting.authenticated_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_on_browser(mocker):
|
||||
mocker.patch(
|
||||
"reflex.utils.hosting.get_existing_access_token",
|
||||
return_value=("fake-token", "fake-code"),
|
||||
"reflex.utils.hosting.authenticated_token",
|
||||
return_value=("", "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"])
|
||||
assert result.exit_code == 1
|
||||
# Make sure the invalid token delete is performed
|
||||
mock_delete_token_from_config.assert_called_once()
|
||||
|
||||
|
||||
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
|
||||
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"
|
||||
)
|
||||
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"])
|
||||
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.
|
||||
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"])
|
||||
assert result.exit_code == 1
|
||||
|
||||
|
@ -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)))
|
||||
token, code = hosting.get_existing_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):
|
||||
@ -28,35 +28,37 @@ def test_get_existing_access_token_and_invitation_code(mocker):
|
||||
|
||||
def test_no_existing_access_token(mocker):
|
||||
# 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):
|
||||
# Config file not exist
|
||||
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):
|
||||
# Config file is empty
|
||||
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):
|
||||
# Config file content is not valid json
|
||||
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):
|
||||
@ -124,20 +126,21 @@ def test_save_access_code_but_none_invitation_code_to_config(mocker):
|
||||
|
||||
def test_authenticated_token_success(mocker):
|
||||
access_token = "fake_token"
|
||||
invitation_code = "fake_code"
|
||||
mocker.patch(
|
||||
"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):
|
||||
mocker.patch(
|
||||
"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):
|
||||
@ -145,30 +148,30 @@ def test_maybe_authenticated_token_is_invalid(mocker):
|
||||
"reflex.utils.hosting.get_existing_access_token",
|
||||
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):
|
||||
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:
|
||||
hosting.prepare_deploy("fake-app")
|
||||
assert ex.value == "Not authenticated"
|
||||
|
||||
|
||||
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))
|
||||
with pytest.raises(Exception):
|
||||
hosting.prepare_deploy("fake-app")
|
||||
|
||||
|
||||
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(
|
||||
"httpx.post",
|
||||
return_value=Mock(
|
||||
@ -190,7 +193,9 @@ def test_prepare_deploy_success(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(
|
||||
"httpx.post",
|
||||
@ -224,14 +229,13 @@ def test_validate_token_with_retries_failed(mocker):
|
||||
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(
|
||||
"reflex.utils.hosting.validate_token", side_effect=ValueError
|
||||
)
|
||||
mock_delete_token = mocker.patch("reflex.utils.hosting.delete_token_from_config")
|
||||
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_delete_token.call_count == 1
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user