Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/reflex-hosting-cli/news/6866.misc.md
Original file line number Diff line number Diff line change
@@ -1 +1 @@
The hosting CLI's forked console module and `LogLevel` enum are now shims over `reflex-base` (new dependency), and its logging goes through standard python `logging`. Debug output renders purple (was blue), errors go to stderr (was stdout), and success messages are hidden at `--loglevel warning`.
The hosting CLI's logging goes through standard python `logging`. On reflex 0.9 and up it shares the `reflex-base` console and `LogLevel`; on earlier reflex, where `reflex-base` is not installed, the CLI renders the same output itself. Debug output renders purple (was blue), errors go to stderr (was stdout), and success messages are hidden at `--loglevel warning`.
4 changes: 0 additions & 4 deletions packages/reflex-hosting-cli/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,9 @@ dependencies = [
"httpx >=0.25.1,<1.0",
"packaging >=24.2",
"platformdirs >=3.10.0,<5.0",
"reflex-base >= 0.9.8.post19.dev0",
"rich >=13,<16",
]

[tool.uv.sources]
reflex-base = { workspace = true }

[tool.hatch.version]
source = "uv-dynamic-versioning"

Expand Down
15 changes: 14 additions & 1 deletion packages/reflex-hosting-cli/src/reflex_cli/constants/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,22 @@
from __future__ import annotations

from types import SimpleNamespace
from typing import TYPE_CHECKING

from platformdirs import PlatformDirs
from reflex_base.constants.base import LogLevel as LogLevel

if TYPE_CHECKING:
# The two enums are interchangeable, so the shared one is what gets
# type-checked; reflex_cli.constants.log_level is checked on its own.
from reflex_base.constants.base import LogLevel as LogLevel
else:
try:
# reflex-base only exists from reflex 0.9 on, and the hosting CLI
# supports older reflex too, so its LogLevel is shared when available
# and forked otherwise.
from reflex_base.constants.base import LogLevel as LogLevel
except ImportError:
from reflex_cli.constants.log_level import LogLevel as LogLevel


class Reflex(SimpleNamespace):
Expand Down
115 changes: 115 additions & 0 deletions packages/reflex-hosting-cli/src/reflex_cli/constants/log_level.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
"""The hosting CLI's own LogLevel, used when reflex-base is not installed.

reflex-base only exists from reflex 0.9 on, but the hosting CLI supports older
reflex too. :mod:`reflex_cli.constants.base` prefers the reflex-base enum when
it is importable and falls back to this one otherwise; the two are
interchangeable, with the same members and the same string values.
"""

from __future__ import annotations

import logging
from enum import Enum


class LogLevel(str, Enum):
"""The log levels."""

DEBUG = "debug"
DEFAULT = "default"
INFO = "info"
WARNING = "warning"
ERROR = "error"
CRITICAL = "critical"

@classmethod
def from_string(cls, level: str | None) -> LogLevel | None:
"""Convert a string to a log level.

Args:
level: The log level as a string.

Returns:
The log level, or None if the string names no level.
"""
if not level:
return None
try:
return cls[level.upper()]
except KeyError:
return None

def to_logging_level(self) -> int:
"""Map this level to a stdlib logging level number.

DEFAULT acts as a threshold equivalent to INFO.

Returns:
The stdlib logging level.
"""
return _LOGGING_LEVELS[self]
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.

def subprocess_level(self) -> LogLevel:
"""Return the log level to hand to a subprocess.

Returns:
This level, or WARNING when it is DEFAULT.
"""
return self if self != LogLevel.DEFAULT else LogLevel.WARNING

# The str mixin supplies alphabetical comparisons, so all four operators
# must be overridden to compare by verbosity rank instead.
def __lt__(self, other: LogLevel) -> bool:
"""Compare log levels.

Args:
other: The other log level.

Returns:
True if the log level is less verbose than the other log level.
"""
return _LOG_LEVEL_RANK[self] < _LOG_LEVEL_RANK[other]

def __le__(self, other: LogLevel) -> bool:
"""Compare log levels.

Args:
other: The other log level.

Returns:
True if the log level is less than or equal to the other log level.
"""
return _LOG_LEVEL_RANK[self] <= _LOG_LEVEL_RANK[other]

def __gt__(self, other: LogLevel) -> bool:
"""Compare log levels.

Args:
other: The other log level.

Returns:
True if the log level is more verbose-restrictive than the other.
"""
return _LOG_LEVEL_RANK[self] > _LOG_LEVEL_RANK[other]

def __ge__(self, other: LogLevel) -> bool:
"""Compare log levels.

Args:
other: The other log level.

Returns:
True if the log level is greater than or equal to the other.
"""
return _LOG_LEVEL_RANK[self] >= _LOG_LEVEL_RANK[other]


_LOG_LEVEL_RANK = {level: rank for rank, level in enumerate(LogLevel)}
_LOGGING_LEVELS = {
LogLevel.DEBUG: logging.DEBUG,
LogLevel.DEFAULT: logging.INFO,
LogLevel.INFO: logging.INFO,
LogLevel.WARNING: logging.WARNING,
LogLevel.ERROR: logging.ERROR,
LogLevel.CRITICAL: logging.CRITICAL,
}
156 changes: 133 additions & 23 deletions packages/reflex-hosting-cli/src/reflex_cli/utils/console.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,136 @@
"""Functions to communicate to the user via console (shared with reflex-base)."""
"""Interactive console helpers, shared with reflex-base when it is installed.

Level-gated messages go through :mod:`logging` (see :mod:`reflex_cli.utils.log`);
what remains here are the rich features that are output rather than logging:
prompts, tables, spinners and plain prints.
"""

from __future__ import annotations

from reflex_base.constants.base import LogLevel
from reflex_base.utils import log as _log
from reflex_base.utils.console import PoorProgress as PoorProgress
from reflex_base.utils.console import ask as ask
from reflex_base.utils.console import debug as debug
from reflex_base.utils.console import deprecate as deprecate
from reflex_base.utils.console import error as error
from reflex_base.utils.console import info as info
from reflex_base.utils.console import is_debug as is_debug
from reflex_base.utils.console import log as log
from reflex_base.utils.console import print as print
from reflex_base.utils.console import print_table as print_table
from reflex_base.utils.console import progress as progress
from reflex_base.utils.console import rule as rule
from reflex_base.utils.console import set_log_level as _set_log_level
from reflex_base.utils.console import status as status
from reflex_base.utils.console import success as success
from reflex_base.utils.console import timing as timing
from reflex_base.utils.console import warn as warn
from collections.abc import Sequence
from typing import overload

from reflex_cli.constants.base import LogLevel
from reflex_cli.utils.log import HAS_REFLEX_BASE, is_json_mode
from reflex_cli.utils.log import set_log_level as _set_log_level

if HAS_REFLEX_BASE:
from reflex_base.utils.console import ask as ask
from reflex_base.utils.console import print as print
from reflex_base.utils.console import print_table as print_table
from reflex_base.utils.console import progress as progress
from reflex_base.utils.console import rule as rule
from reflex_base.utils.console import status as status
else:
from rich.console import Console, OverflowMethod
from rich.progress import MofNCompleteColumn, Progress, TimeElapsedColumn
from rich.prompt import Prompt
from rich.table import Table

_console = Console(highlight=False)

def print(msg: str, **kwargs):
"""Print a message.

Args:
msg: The message to print.
kwargs: Keyword arguments to pass to the print function.
"""
_console.print(msg, **kwargs)

def print_table(
tabular_data: list[list[str]],
headers: Sequence[str] = (),
overflow: OverflowMethod = "ellipsis",
) -> None:
"""Print a table to the console.

Args:
tabular_data: The data to print in tabular format.
headers: The headers for the table.
overflow: What to do with a cell too wide for its column. The
default cuts it short; pass "fold" for values a user has to
read in full, such as an email or an identifier.
"""
table = Table()

for column in headers:
table.add_column(column, overflow=overflow)

for row in tabular_data:
table.add_row(*row)

_console.print(table)

def rule(title: str, **kwargs):
"""Print a horizontal rule with a title.

Args:
title: The title of the rule.
kwargs: Keyword arguments to pass to the print function.
"""
_console.rule(title, **kwargs)

@overload
def ask(
question: str,
choices: list[str] | None = None,
*,
show_choices: bool = True,
) -> str: ...

@overload
def ask(
question: str,
choices: list[str] | None = None,
default: str = ...,
show_choices: bool = True,
) -> str: ...

def ask(
question: str,
choices: list[str] | None = None,
default: str | None = None,
show_choices: bool = True,
) -> str | None:
"""Ask the user a question, optionally with a list of choices.

Args:
question: The question to ask the user.
choices: A list of choices to select from.
default: The default option selected.
show_choices: Whether to show the choices.

Returns:
A string with the user input.
"""
return Prompt.ask(
question, choices=choices, default=default, show_choices=show_choices
)

def progress():
"""Create a new progress bar.

Returns:
A new progress bar.
"""
return Progress(
*Progress.get_default_columns()[:-1],
MofNCompleteColumn(),
TimeElapsedColumn(),
)

def status(*args, **kwargs):
"""Create a status with a spinner.

Args:
*args: Args to pass to the status.
**kwargs: Kwargs to pass to the status.

Returns:
A new status.
"""
return _console.status(*args, **kwargs)


def set_log_level(log_level: LogLevel | str):
Expand All @@ -38,8 +148,8 @@ def transfer_progress():
"""Create a progress bar measured in bytes rather than in steps.

Lives here rather than beside ``progress`` in reflex-base because only the
deploy upload wants it: a new name over there would raise this package's
reflex-base floor, and the CLI is released on its own schedule.
deploy upload wants it, and the CLI has to render it whether or not
reflex-base is installed.

Returns:
A new progress bar, sized and paced for a file transfer.
Expand All @@ -59,5 +169,5 @@ def transfer_progress():
DownloadColumn(),
TransferSpeedColumn(),
TimeElapsedColumn(),
disable=_log.is_json_mode(),
disable=is_json_mode(),
)
5 changes: 2 additions & 3 deletions packages/reflex-hosting-cli/src/reflex_cli/utils/hosting.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,10 @@
from urllib.parse import urljoin

import click
from reflex_base.utils import log

import reflex_cli.constants as constants
from reflex_cli.core.config import Config, RegionOption
from reflex_cli.utils import console, dependency
from reflex_cli.utils import console, dependency, log
from reflex_cli.utils.dependency import is_valid_url
from reflex_cli.utils.exceptions import (
ArchiveUploadError,
Expand Down Expand Up @@ -2145,7 +2144,7 @@ def upload_archives(
f"could not upload the build to storage: HTTP {ex.response.status_code}"
) from ex
if attempt < UPLOAD_ATTEMPTS - 1:
console.warn("the upload window expired; reserving another one")
logger.warning("the upload window expired; reserving another one")
except httpx.HTTPError as ex:
raise ArchiveUploadError(f"could not upload the build: {ex}") from ex
else:
Expand Down
Loading
Loading