"""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" )