eptm_dashboard/.venv/lib/python3.12/site-packages/reflex/utils/prerequisites.py

700 lines
21 KiB
Python

"""Everything related to fetching or initializing build prerequisites."""
from __future__ import annotations
import contextlib
import importlib
import importlib.metadata
import inspect
import json
import random
import re
import sys
import typing
from datetime import datetime
from os import getcwd
from pathlib import Path
from types import ModuleType
from typing import NamedTuple
from packaging import version
from reflex_base import constants
from reflex_base.config import Config, get_config
from reflex_base.constants.base import RunningMode
from reflex_base.environment import environment
from reflex_base.utils.decorator import once
from reflex import model
from reflex.utils import console, net, path_ops
from reflex.utils.misc import get_module_path
if typing.TYPE_CHECKING:
from redis import Redis as RedisSync
from redis.asyncio import Redis
from reflex.app import App
class AppInfo(NamedTuple):
"""A tuple containing the app instance and module."""
app: App
module: ModuleType
def get_web_dir() -> Path:
"""Get the working directory for the frontend.
Can be overridden with REFLEX_WEB_WORKDIR.
Returns:
The working directory.
"""
return environment.REFLEX_WEB_WORKDIR.get()
def get_states_dir() -> Path:
"""Get the working directory for the states.
Can be overridden with REFLEX_STATES_WORKDIR.
Returns:
The working directory.
"""
return environment.REFLEX_STATES_WORKDIR.get()
def get_backend_dir() -> Path:
"""Get the working directory for the backend.
Returns:
The working directory.
"""
return get_web_dir() / constants.Dirs.BACKEND
def check_latest_package_version(package_name: str):
"""Check if the latest version of the package is installed.
Args:
package_name: The name of the package.
"""
if environment.REFLEX_CHECK_LATEST_VERSION.get() is False:
return
try:
console.debug(f"Checking for the latest version of {package_name}...")
# Get the latest version from PyPI
current_version = importlib.metadata.version(package_name)
url = f"https://pypi.org/pypi/{package_name}/json"
response = net.get(url, timeout=2)
latest_version = response.json()["info"]["version"]
console.debug(f"Latest version of {package_name}: {latest_version}")
if get_or_set_last_reflex_version_check_datetime():
# Versions were already checked and saved in reflex.json, no need to warn again
return
if version.parse(current_version) < version.parse(latest_version):
# Show a warning when the host version is older than PyPI version
console.warn(
f"Your version ({current_version}) of {package_name} is out of date. Upgrade to {latest_version} with 'pip install {package_name} --upgrade'"
)
except Exception:
console.debug(f"Failed to check for the latest version of {package_name}.")
def get_or_set_last_reflex_version_check_datetime():
"""Get the last time a check was made for the latest reflex version.
This is typically useful for cases where the host reflex version is
less than that on Pypi.
Returns:
The last version check datetime.
"""
reflex_json_file = get_web_dir() / constants.Reflex.JSON
if not reflex_json_file.exists():
return None
# Open and read the file
data = json.loads(reflex_json_file.read_text())
last_version_check_datetime = data.get("last_version_check_datetime")
if not last_version_check_datetime:
data.update({"last_version_check_datetime": str(datetime.now())})
path_ops.update_json_file(reflex_json_file, data)
return last_version_check_datetime
def set_last_reflex_run_time():
"""Set the last Reflex run time."""
path_ops.update_json_file(
get_web_dir() / constants.Reflex.JSON,
{"last_reflex_run_datetime": str(datetime.now())},
)
@once
def windows_check_onedrive_in_path() -> bool:
"""For windows, check if oneDrive is present in the project dir path.
Returns:
If oneDrive is in the path of the project directory.
"""
return "onedrive" in str(Path.cwd()).lower()
def _check_app_name(config: Config):
"""Check if the app name is valid and matches the folder structure.
Args:
config: The config object.
Raises:
RuntimeError: If the app name is not set, folder doesn't exist, or doesn't match config.
ModuleNotFoundError: If the app_name is not importable (i.e., not a valid Python package, folder structure being wrong).
"""
if not config.app_name:
msg = (
"Cannot get the app module because `app_name` is not set in rxconfig! "
"If this error occurs in a reflex test case, ensure that `get_app` is mocked."
)
raise RuntimeError(msg)
from reflex.utils.misc import with_cwd_in_syspath
with with_cwd_in_syspath():
module_path = get_module_path(config.module)
if module_path is None:
msg = f"Module {config.module} not found. "
if config.app_module_import is not None:
msg += f"Ensure app_module_import='{config.app_module_import}' in rxconfig.py matches your folder structure."
else:
msg += f"Ensure app_name='{config.app_name}' in rxconfig.py matches your folder structure."
raise ModuleNotFoundError(msg)
config._app_name_is_valid = True
def get_app(reload: bool = False) -> ModuleType:
"""Get the app module based on the default config.
Args:
reload: Re-import the app module from disk
Returns:
The app based on the default config.
Raises:
Exception: If an error occurs while getting the app module.
"""
from reflex.utils import telemetry
try:
config = get_config()
# Avoid hitting disk when the app name has already been validated in this process.
if not config._app_name_is_valid:
_check_app_name(config)
module = config.module
sys.path.insert(0, getcwd()) # noqa: PTH109
app = (
__import__(module, fromlist=(constants.CompileVars.APP,))
if not config.app_module
else config.app_module
)
if reload:
from reflex.page import DECORATED_PAGES
from reflex.state import reload_state_module
# Reset rx.State subclasses to avoid conflict when reloading.
reload_state_module(module=module)
DECORATED_PAGES.clear()
# Reload the app module.
importlib.reload(app)
except Exception as ex:
telemetry.send_error(ex, context="frontend")
raise
else:
return app
def get_and_validate_app(
reload: bool = False, check_if_schema_up_to_date: bool = False
) -> AppInfo:
"""Get the app instance based on the default config and validate it.
Args:
reload: Re-import the app module from disk
check_if_schema_up_to_date: If True, check if the schema is up to date.
Returns:
The app instance and the app module.
Raises:
RuntimeError: If the app instance is not an instance of rx.App.
"""
from reflex.app import App
app_module = get_app(reload=reload)
app = getattr(app_module, constants.CompileVars.APP)
if not isinstance(app, App):
msg = "The app instance in the specified app_module_import in rxconfig must be an instance of rx.App."
raise RuntimeError(msg)
if check_if_schema_up_to_date:
check_schema_up_to_date()
return AppInfo(app=app, module=app_module)
def get_compiled_app(
reload: bool = False,
prerender_routes: bool = False,
dry_run: bool = False,
check_if_schema_up_to_date: bool = False,
use_rich: bool = True,
) -> ModuleType:
"""Get the app module based on the default config after first compiling it.
Args:
reload: Re-import the app module from disk
prerender_routes: Whether to prerender routes.
dry_run: If True, do not write the compiled app to disk.
check_if_schema_up_to_date: If True, check if the schema is up to date.
use_rich: Whether to use rich progress bars.
Returns:
The compiled app based on the default config.
"""
app, app_module = get_and_validate_app(
reload=reload, check_if_schema_up_to_date=check_if_schema_up_to_date
)
app._compile(prerender_routes=prerender_routes, dry_run=dry_run, use_rich=use_rich)
return app_module
def _can_colorize() -> bool:
"""Check if the output can be colorized.
Copied from _colorize.can_colorize.
https://raw.githubusercontent.com/python/cpython/refs/heads/main/Lib/_colorize.py
Returns:
If the output can be colorized
"""
import io
import os
def _safe_getenv(k: str, fallback: str | None = None) -> str | None:
"""Exception-safe environment retrieval. See gh-128636.
Args:
k: The environment variable key.
fallback: The fallback value if the environment variable is not set.
Returns:
The value of the environment variable or the fallback value.
"""
try:
return os.environ.get(k, fallback)
except Exception:
return fallback
file = sys.stdout
if not sys.flags.ignore_environment:
if _safe_getenv("PYTHON_COLORS") == "0":
return False
if _safe_getenv("PYTHON_COLORS") == "1":
return True
if _safe_getenv("NO_COLOR"):
return False
if _safe_getenv("FORCE_COLOR"):
return True
if _safe_getenv("TERM") == "dumb":
return False
if not hasattr(file, "fileno"):
return False
if sys.platform == "win32":
try:
import nt
if not nt._supports_virtual_terminal(): # pyright: ignore[reportAttributeAccessIssue]
return False
except (ImportError, AttributeError):
return False
try:
return os.isatty(file.fileno())
except io.UnsupportedOperation:
return hasattr(file, "isatty") and file.isatty()
def compile_or_validate_app(
compile: bool = False,
check_if_schema_up_to_date: bool = False,
prerender_routes: bool = False,
) -> bool:
"""Compile or validate the app module based on the default config.
Args:
compile: Whether to compile the app.
check_if_schema_up_to_date: If True, check if the schema is up to date.
prerender_routes: Whether to prerender routes.
Returns:
True if the app was successfully compiled or validated, False otherwise.
"""
try:
if compile:
get_compiled_app(
check_if_schema_up_to_date=check_if_schema_up_to_date,
prerender_routes=prerender_routes,
)
else:
get_and_validate_app(check_if_schema_up_to_date=check_if_schema_up_to_date)
except Exception as e:
import traceback
try:
colorize = _can_colorize()
traceback.print_exception(e, colorize=colorize) # pyright: ignore[reportCallIssue]
except Exception:
traceback.print_exception(e)
return False
else:
return True
def get_redis() -> Redis | None:
"""Get the asynchronous redis client.
Returns:
The asynchronous redis client.
"""
try:
from redis.asyncio import Redis
from redis.exceptions import RedisError
except ImportError:
console.debug("Redis package not installed.")
return None
if (redis_url := parse_redis_url()) is not None:
return Redis.from_url(
redis_url,
retry_on_error=[RedisError],
)
return None
def get_redis_sync() -> RedisSync | None:
"""Get the synchronous redis client.
Returns:
The synchronous redis client.
"""
try:
from redis import Redis as RedisSync
from redis.exceptions import RedisError
except ImportError:
console.debug("Redis package not installed.")
return None
if (redis_url := parse_redis_url()) is not None:
return RedisSync.from_url(
redis_url,
retry_on_error=[RedisError],
)
return None
def parse_redis_url() -> str | None:
"""Parse the REFLEX_REDIS_URL in config if applicable.
Returns:
If url is non-empty, return the URL as it is.
Raises:
ValueError: If the REFLEX_REDIS_URL is not a supported scheme.
"""
config = get_config()
if not config.redis_url:
return None
if not config.redis_url.startswith(("redis://", "rediss://", "unix://")):
msg = "REFLEX_REDIS_URL must start with 'redis://', 'rediss://', or 'unix://'."
raise ValueError(msg)
return config.redis_url
async def get_redis_status() -> dict[str, bool | None]:
"""Checks the status of the Redis connection.
Attempts to connect to Redis and send a ping command to verify connectivity.
Returns:
The status of the Redis connection.
"""
try:
status = True
redis_client = get_redis()
if redis_client is not None:
ping_command = redis_client.ping()
if inspect.isawaitable(ping_command):
await ping_command
else:
status = None
except Exception as exc:
status = False
console.error(
f"Redis health check failed: {exc} (subsequent errors will not be logged)",
dedupe=True,
)
return {"redis": status}
def validate_app_name(app_name: str | None = None) -> str:
"""Validate the app name.
The default app name is the name of the current directory.
Args:
app_name: the name passed by user during reflex init
Returns:
The app name after validation.
Raises:
SystemExit: if the app directory name is reflex or if the name is not standard for a python package name.
"""
app_name = app_name or Path.cwd().name.replace("-", "_")
# Make sure the app is not named "reflex".
if app_name.lower() == constants.Reflex.MODULE_NAME:
console.error(
f"The app directory cannot be named [bold]{constants.Reflex.MODULE_NAME}[/bold]."
)
raise SystemExit(1)
# Make sure the app name is standard for a python package name.
if not re.match(r"^[a-zA-Z][a-zA-Z0-9_]*$", app_name):
console.error(
"The app directory name must start with a letter and can contain letters, numbers, and underscores."
)
raise SystemExit(1)
return app_name
def get_project_hash(raise_on_fail: bool = False) -> int | None:
"""Get the project hash from the reflex.json file if the file exists.
Args:
raise_on_fail: Whether to raise an error if the file does not exist.
Returns:
project_hash: The app hash.
"""
json_file = get_web_dir() / constants.Reflex.JSON
if not json_file.exists() and not raise_on_fail:
return None
data = json.loads(json_file.read_text())
return data.get("project_hash")
def check_running_mode(frontend: bool, backend: bool) -> RunningMode:
"""Check if the app is running in frontend or backend mode.
Args:
frontend: Whether to run the frontend of the app.
backend: Whether to run the backend of the app.
Returns:
The running modes.
"""
if not frontend and not backend:
return RunningMode.FULLSTACK
if frontend and not backend:
return RunningMode.FRONTEND_ONLY
if not frontend and backend:
return RunningMode.BACKEND_ONLY
return RunningMode.FULLSTACK
def assert_in_reflex_dir():
"""Assert that the current working directory is the reflex directory.
Raises:
SystemExit: If the current working directory is not the reflex directory.
"""
if not constants.Config.FILE.exists():
console.error(
f"[cyan]{constants.Config.FILE}[/cyan] not found. Move to the root folder of your project, or run [bold]{constants.Reflex.MODULE_NAME} init[/bold] to start a new project."
)
raise SystemExit(1)
def needs_reinit() -> bool:
"""Check if an app needs to be reinitialized.
Returns:
Whether the app needs to be reinitialized.
"""
# Make sure the .reflex directory exists.
if not environment.REFLEX_DIR.get().exists():
return True
# Make sure the .web directory exists in frontend mode.
if not get_web_dir().exists():
return True
if not _is_app_compiled_with_same_reflex_version():
return True
if constants.IS_WINDOWS:
console.warn(
"""Windows Subsystem for Linux (WSL) is recommended for improving initial install times."""
)
if windows_check_onedrive_in_path():
console.warn(
"Creating project directories in OneDrive may lead to performance issues. For optimal performance, It is recommended to avoid using OneDrive for your reflex app."
)
# No need to reinitialize if the app is already initialized.
return False
def _is_app_compiled_with_same_reflex_version() -> bool:
json_file = get_web_dir() / constants.Reflex.JSON
if not json_file.exists():
return False
app_version = json.loads(json_file.read_text()).get("version")
return app_version == constants.Reflex.VERSION
def ensure_reflex_installation_id() -> int | None:
"""Ensures that a reflex distinct id has been generated and stored in the reflex directory.
Returns:
Distinct id.
"""
try:
console.debug("Ensuring reflex installation id.")
initialize_reflex_user_directory()
installation_id_file = environment.REFLEX_DIR.get() / "installation_id"
installation_id = None
if installation_id_file.exists():
with contextlib.suppress(Exception):
installation_id = int(installation_id_file.read_text())
# If anything goes wrong at all... just regenerate.
# Like what? Examples:
# - file not exists
# - file not readable
# - content not parseable as an int
if installation_id is None:
installation_id = random.getrandbits(128)
installation_id_file.write_text(str(installation_id))
except Exception as e:
console.debug(f"Failed to ensure reflex installation id: {e}")
return None
else:
# If we get here, installation_id is definitely set
return installation_id
def initialize_reflex_user_directory():
"""Initialize the reflex user directory."""
console.debug(f"Creating {environment.REFLEX_DIR.get()}")
# Create the reflex directory.
path_ops.mkdir(environment.REFLEX_DIR.get())
def initialize_frontend_dependencies():
"""Initialize all the frontend dependencies."""
from reflex.utils.frontend_skeleton import initialize_web_directory
from reflex.utils.js_runtimes import install_bun, validate_frontend_dependencies
# validate dependencies before install
console.debug("Validating frontend dependencies.")
validate_frontend_dependencies()
# Install the frontend dependencies.
console.debug("Installing or validating bun.")
install_bun()
# Set up the web directory.
initialize_web_directory()
def check_db_used() -> bool:
"""Check if the database is used.
Returns:
True if the database is used.
"""
return bool(get_config().db_url)
def check_redis_used() -> bool:
"""Check if Redis is used.
Returns:
True if Redis is used.
"""
return bool(get_config().redis_url)
def check_db_initialized() -> bool:
"""Check if the database migrations are initialized.
Returns:
True if alembic is initialized (or if database is not used).
"""
if (
get_config().db_url is not None
and not environment.ALEMBIC_CONFIG.get().exists()
):
console.error(
"Database is not initialized. Run [bold]reflex db init[/bold] first.",
dedupe=True,
)
return False
return True
def check_schema_up_to_date():
"""Check if the sqlmodel metadata matches the current database schema."""
if get_config().db_url is None or not environment.ALEMBIC_CONFIG.get().exists():
return
with model.get_engine().connect() as connection:
from alembic.util.exc import CommandError
try:
if model.alembic_autogenerate(
connection=connection,
write_migration_scripts=False,
):
console.error(
"Detected database schema changes. Run [bold]reflex db makemigrations[/bold] "
"to generate migration scripts.",
)
except CommandError as command_error:
if "Target database is not up to date." in str(command_error):
console.error(
f"{command_error} Run [bold]reflex db migrate[/bold] to update database."
)
@once
def get_user_tier():
"""Get the current user's tier.
Returns:
The current user's tier.
"""
from reflex_cli.v2.utils import hosting
authenticated_token = hosting.authenticated_token()
return (
authenticated_token[1].get("tier", "").lower()
if authenticated_token[0]
else "anonymous"
)