Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • hive/clive
1 result
Show changes
Commits on Source (16)
Showing
with 109 additions and 65 deletions
......@@ -39,7 +39,7 @@ variables:
include:
- project: 'hive/hive'
ref: 8b6cee02a0261b7202f33e4b7aac2f9cea5ec7f2
ref: 8039e32fb584fbf06f4bd0bf5eae3a7b360dae1b
file: '/scripts/ci-helpers/prepare_data_image_job.yml'
- project: 'hive/common-ci-configuration'
ref: d4e29410ea168096e1a822f77c7ce741d9cfb57a
......@@ -64,6 +64,10 @@ verify_poetry_lock_sanity:
lint_code_with_ruff:
stage: static_code_analysis
extends: .lint_code_with_ruff_template
script: # our own script because of newer ruff version
- echo -e "${TXT_BLUE}Linting all sources with Ruff...${TXT_CLEAR}" &&
ruff check ${MAYBE_EXPLICIT_CONFIG} ${PACKAGES_TO_CHECK}
formatting_with_ruff_check:
stage: static_code_analysis
......
......@@ -43,7 +43,7 @@ repos:
- stylelint-config-standard-scss@13.0.0
args: [ "--fix" ]
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: 'v0.4.9'
rev: 'v0.6.5'
hooks:
- id: ruff
name: linting code with Ruff
......
......@@ -121,7 +121,7 @@ class CliveTyper(typer.Typer):
super().command, name=name, common_options=common_options, help=help, epilog=epilog
)
def callback( # type: ignore[override] # noqa: PLR0913
def callback( # type: ignore[override]
self,
name: Optional[str] = Default(None),
common_options: list[type[CommonOptionsBase]] | None = None,
......@@ -149,16 +149,16 @@ class CliveTyper(typer.Typer):
self.__handle_error(error)
sys.exit(exit_code)
except ClickException as error:
except ClickException as click_exception:
# See: `typer/core.py` -> `_main` -> `except click.ClickException as e:`
# If ClickException was raised in the registered error handler, we need to format it like Typer does.
rich_utils.rich_format_error(error)
sys.exit(error.exit_code)
except Exception as error: # noqa: BLE001
rich_utils.rich_format_error(click_exception)
sys.exit(click_exception.exit_code)
except Exception as exception: # noqa: BLE001
# See: `typer/mian.py` -> `Typer.__call__` -> `except Exception as e:`
# If any other exception was raised in the registered error handler, we need to format it like Typer does.
setattr(
error,
exception,
_typer_developer_exception_attr_name,
DeveloperExceptionConfig(
pretty_exceptions_enable=self.pretty_exceptions_enable,
......@@ -166,7 +166,7 @@ class CliveTyper(typer.Typer):
pretty_exceptions_short=self.pretty_exceptions_short,
),
)
raise error from None
raise exception from None
def __get_error_handler(self, error: ExceptionT) -> ErrorHandlingCallback[ExceptionT]:
for type_ in type(error).mro():
......
......@@ -35,7 +35,7 @@ class CommonOptionsBase:
if cls not in cls.instances:
raise CommonOptionInstanceNotAvailableError(cls)
return cls.instances[cls] # type: ignore[return-value]
return cls.instances[cls]
def as_dict(self) -> dict[str, Any]:
return self.__dict__
......@@ -22,7 +22,7 @@ def asyncio_run(awaitable: Awaitable[T]) -> T:
async def await_for_given_awaitable() -> T:
return await awaitable
return thread_pool.submit(asyncio.run, await_for_given_awaitable()).result() # type: ignore[arg-type]
return thread_pool.submit(asyncio.run, await_for_given_awaitable()).result()
async def event_wait(event: asyncio.Event, timeout: float | None = None) -> bool:
......
from __future__ import annotations
from pathlib import Path
from types import UnionType
from types import NoneType, UnionType
from typing import TYPE_CHECKING, get_args
from pydantic import Field
......@@ -72,7 +72,7 @@ class BeekeeperConfig(CliveBaseModel):
config_name, config_value = line.split("=")
member_name = cls.__convert_config_name_to_member_name(config_name)
member_type = fields[member_name].annotation
if isinstance(member_type, UnionType) and get_args(member_type)[-1] == type(None):
if isinstance(member_type, UnionType) and get_args(member_type)[-1] is NoneType:
member_type = get_args(member_type)[0]
setattr(
result,
......@@ -122,7 +122,7 @@ class BeekeeperConfig(CliveBaseModel):
if expected == Url:
return Url.parse(config_value)
if expected == bool:
if expected is bool:
cv_lower = config_value.lower()
if cv_lower == "yes":
return True
......
......@@ -446,9 +446,9 @@ class Commands(Generic[WorldT_co]):
await handler.execute(
command.execute_with_result() if isinstance(command, CommandWithResult) else command.execute(),
)
except Exception as error: # noqa: BLE001
except Exception as exception: # noqa: BLE001
# Try to handle the error with the next exception handler
return await self.__surround_with_exception_handler(command, exception_handlers[1:], error)
return await self.__surround_with_exception_handler(command, exception_handlers[1:], exception)
return self.__create_command_wrapper(command, handler.error)
@overload
......
......@@ -61,7 +61,7 @@ class ProposalsData:
_Orders = Literal["by_total_votes_with_voted_first", "by_total_votes", "by_start_date", "by_end_date", "by_creator"]
_OrderDirections = Literal["ascending", "descending"]
_Statuses = Literal["all", "active", "inactive", "votable", "expired", "inactive"]
_Statuses = Literal["all", "active", "inactive", "votable", "expired"]
@dataclass(kw_only=True)
......
......@@ -107,7 +107,7 @@ class Communication:
self.__timeout_total_secs = before["timeout_total_secs"]
self.__pool_time_secs = before["pool_time_secs"]
def request( # noqa: PLR0913
def request(
self,
url: str,
*,
......@@ -138,7 +138,7 @@ class Communication:
)
)
async def arequest( # noqa: PLR0913
async def arequest(
self,
url: str,
*,
......@@ -167,7 +167,7 @@ class Communication:
pool_time_secs=pool_time_secs,
)
async def __request( # noqa: PLR0913, C901, PLR0915
async def __request( # noqa: C901, PLR0915
self,
url: str,
*,
......
......@@ -171,7 +171,7 @@ def humanize_operation_details(operation: OperationBase) -> str:
value_ = value
# Display assets in legacy format.
if isinstance(value, Asset.AnyT): # type: ignore[arg-type]
if isinstance(value, Asset.AnyT):
value_ = Asset.to_legacy(value)
out += f"{key}='{value_}', "
......
......@@ -253,7 +253,7 @@ class DatabaseApi(Api):
raise NotImplementedError
@Api.method
async def list_proposal_votes( # noqa: PLR0913
async def list_proposal_votes(
self,
*,
start: list[str],
......@@ -265,7 +265,7 @@ class DatabaseApi(Api):
raise NotImplementedError
@Api.method
async def list_proposals( # noqa: PLR0913
async def list_proposals(
self,
*,
start: list[str] | list[int] | list[datetime],
......
from __future__ import annotations
import asyncio
import signal
import sys
import warnings
from clive.__private.core.clive_import import get_clive
def _hide_never_awaited_warnings_in_non_dev_mode() -> None:
from clive.dev import is_in_dev_mode
......@@ -11,10 +15,22 @@ def _hide_never_awaited_warnings_in_non_dev_mode() -> None:
warnings.filterwarnings("ignore", message=".* was never awaited")
async def _shutdown_tui_gracefully() -> None:
app = get_clive().app_instance()
await app.action_quit()
def _handle_close_signals_in_tui() -> None:
loop = asyncio.get_event_loop()
for signal_number in [signal.SIGHUP, signal.SIGINT, signal.SIGQUIT, signal.SIGTERM]:
loop.add_signal_handler(signal_number, lambda: asyncio.create_task(_shutdown_tui_gracefully()))
async def run_tui() -> None:
from clive.__private.before_launch import prepare_before_launch
from clive.__private.ui.app import Clive
_hide_never_awaited_warnings_in_non_dev_mode()
_handle_close_signals_in_tui()
prepare_before_launch()
sys.exit(await Clive.app_instance().run_async())
......@@ -221,7 +221,7 @@ class Clive(App[int], ManualReactive):
return self.__screen_eq(self.screen, screen)
@overload
def push_screen( # type: ignore[overload-overlap]
def push_screen(
self,
screen: Screen[ScreenResultType] | str,
callback: ScreenResultCallbackType[ScreenResultType] | None = None,
......@@ -358,7 +358,7 @@ class Clive(App[int], ManualReactive):
async def _update_alarms_data_asap() -> None:
self._refresh_alarms_data_interval.pause()
while not self.world.profile.accounts.is_tracked_accounts_node_data_available:
while not self.world.profile.accounts.is_tracked_accounts_node_data_available: # noqa: ASYNC110
await asyncio.sleep(0.1)
self.update_alarms_data()
self._refresh_alarms_data_interval.resume()
......
......@@ -19,6 +19,7 @@ if TYPE_CHECKING:
from textual.widget import Widget
from clive.__private.ui.app import Clive
from clive.__private.ui.types import ActiveBindingsMap
......@@ -47,27 +48,35 @@ class CliveScreen(Screen[ScreenResultType], CliveWidget):
"""Provides the ability to control the binding order in the footer."""
return self._sort_bindings(super().active_bindings)
@staticmethod
def prevent_action_when_no_accounts_node_data(func: Callable[P, None]) -> Callable[P, None]:
@wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> None:
app_ = get_clive().app_instance()
if (
not app_.world.profile.accounts.is_tracked_accounts_node_data_available
or not app_.world.profile.accounts.is_tracked_accounts_alarms_data_available
):
logger.debug(f"action {func.__name__} prevented because no node or alarms data is available yet")
app_.notify("Waiting for data...", severity="warning")
return
func(*args, **kwargs)
return wrapper
@classmethod
def prevent_action_when_no_accounts_node_data(
cls, message: str = "Waiting for data..."
) -> Callable[[Callable[P, None]], Callable[P, None]]:
def can_run_condition(app: Clive) -> bool:
accounts = app.world.profile.accounts
return (
accounts.is_tracked_accounts_node_data_available and accounts.is_tracked_accounts_alarms_data_available
)
return cls._create_prevent_decorator(can_run_condition, message)
@classmethod
def prevent_action_when_no_working_account(
cls, message: str = "Cannot perform this action without working account"
) -> Callable[[Callable[P, None]], Callable[P, None]]:
return cls._create_prevent_decorator(lambda app: app.world.profile.accounts.has_working_account, message)
@classmethod
def prevent_action_when_no_tracked_accounts(
cls, message: str = "Cannot perform this action without tracked accounts"
) -> Callable[[Callable[P, None]], Callable[P, None]]:
return cls._create_prevent_decorator(lambda app: app.world.profile.accounts.has_tracked_accounts, message)
@staticmethod
def try_again_after_unlock(func: Callable[P, Awaitable[None]]) -> Callable[P, Awaitable[None]]:
@wraps(func)
async def wrapper(*args: P.args, **kwargs: P.kwargs) -> None:
app_ = get_clive().app_instance()
app = get_clive().app_instance()
try:
await func(*args, **kwargs)
......@@ -76,16 +85,35 @@ class CliveScreen(Screen[ScreenResultType], CliveWidget):
async def _on_unlock_result(*, unlocked: bool) -> None:
if not unlocked:
app_.notify("Aborted. UNLOCKED mode was required for this action.", severity="warning")
app.notify("Aborted. UNLOCKED mode was required for this action.", severity="warning")
return
await func(*args, **kwargs)
app_.notify("This action requires Clive to be in UNLOCKED mode. Please unlock...")
await app_.push_screen(Unlock(unlock_result_callback=_on_unlock_result))
app.notify("This action requires Clive to be in UNLOCKED mode. Please unlock...")
await app.push_screen(Unlock(unlock_result_callback=_on_unlock_result))
return wrapper
@classmethod
def _create_prevent_decorator(
cls, can_run_condition: Callable[[Clive], bool], message: str
) -> Callable[[Callable[P, None]], Callable[P, None]]:
def decorator(func: Callable[P, None]) -> Callable[P, None]:
@wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> None:
app = get_clive().app_instance()
if not can_run_condition(app):
logger.debug(f"Preventing action: {func.__name__} with message of: {message}")
app.notify(message, severity="warning")
return
func(*args, **kwargs)
return wrapper
return decorator
@on(ScreenSuspend)
def _post_suspended(self) -> None:
"""
......
......@@ -168,8 +168,8 @@ class BalanceStatsButton(DynamicOneLineButtonUnfocusable):
return asset_name_to_value[asset_name]
@CliveScreen.prevent_action_when_no_accounts_node_data()
@on(OneLineButton.Pressed, ".balance-button")
@CliveScreen.prevent_action_when_no_accounts_node_data
def push_balance_screen(self) -> None:
if self._balance_type == "liquid":
self.app.push_screen(LiquidNavigationDialog(self._account, asset_type=self._asset_type))
......@@ -228,7 +228,7 @@ class TrackedAccountInfo(Container, TrackedAccountReferencingWidget):
lambda: f"Account update: {humanize_datetime(self._account.data.last_account_update)}",
)
@CliveScreen.prevent_action_when_no_accounts_node_data
@CliveScreen.prevent_action_when_no_accounts_node_data()
@on(OneLineButton.Pressed, "#account-details-button")
def push_account_details_screen(self) -> None:
self.app.push_screen(AccountDetails(self._account))
......@@ -313,20 +313,16 @@ class DashboardBase(BaseScreen):
await accounts_container.query("*").remove()
await accounts_container.mount_all(widgets_to_mount)
@CliveScreen.prevent_action_when_no_accounts_node_data
@CliveScreen.prevent_action_when_no_working_account()
@CliveScreen.prevent_action_when_no_accounts_node_data()
def action_operations(self) -> None:
if not self.has_working_account:
self.notify("Cannot perform operations without working account", severity="warning")
return
self.app.push_screen(Operations())
def action_config(self) -> None:
self.app.push_screen(Config())
@CliveScreen.prevent_action_when_no_tracked_accounts()
def action_switch_working_account(self) -> None:
if not self.profile.accounts.tracked:
self.notify("Cannot switch a working account without any account", severity="warning")
return
self.app.push_screen(SwitchWorkingAccountDialog())
def action_add_account(self) -> None:
......
......@@ -120,10 +120,7 @@ class GovernanceListWidget(Vertical, CliveWidget, Generic[GovernanceDataT], Abst
# When _data is None - still waiting for the response.
return False
if len(self._data) == 0:
return True
return False
return not bool(self._data)
class GovernanceTableRow(Grid, CliveWidget, Generic[GovernanceDataT], AbstractClassMessagePump, can_focus=True):
......
......@@ -79,7 +79,7 @@ class AlarmDisplay(DynamicOneLineButtonUnfocusable):
def _is_in_auto_working_account_mode(self) -> bool:
return isinstance(self._account, AutoUseWorkingAccount)
@CliveScreen.prevent_action_when_no_accounts_node_data
@CliveScreen.prevent_action_when_no_accounts_node_data()
@on(OneLineButton.Pressed, "#alarm-display-button")
def push_account_details_screen(self) -> None:
from clive.__private.ui.screens.account_details.account_details import AccountDetails
......
......@@ -116,7 +116,7 @@ class CliveButton(Button, CliveWidget):
- https://github.com/Textualize/textual/issues/4967
- https://github.com/Textualize/textual/issues/1814
"""
if type(event) == Button.Pressed:
if type(event) is Button.Pressed:
event.stop()
self.post_message(self.Pressed(self))
......
......@@ -31,6 +31,7 @@ class ProxyValidator(AccountNameValidator):
return ValidationResult.merge([super_result] + [validator.validate(value) for validator in validators])
def _validate_set_proxy_to_self(self, value: str) -> bool:
if self.working_account_name is not None and self.working_account_name == value:
return False
return True
if self.working_account_name is None:
return True # Validation is successful when no working account name is set
return self.working_account_name != value
......@@ -4,6 +4,7 @@ set -euo pipefail
SCRIPTPATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )"
if [[ "$EUID" -eq 0 ]]; then
if [[ -z "${CLIVE_UID:-}" ]]; then
echo "Warning: variable CLIVE_UID is not set or set to an empty value." >&2
......@@ -19,7 +20,7 @@ if [[ "$EUID" -eq 0 ]]; then
fi
echo "Respawning entrypoint as user clive"
sudo -HEnu clive /bin/bash "${SCRIPTPATH}/entrypoint.sh" "$@"
exec sudo -HEnu clive /bin/bash "${SCRIPTPATH}/entrypoint.sh" "$@"
exit 0
fi
fi
......@@ -50,6 +51,7 @@ launch_cli() {
clive --install-completion >/dev/null 2>&1
clive beekeeper spawn # Spawn the beekeeper so commands that require it don't have to do it every time
bash
clive beekeeper close
}
INTERACTIVE_CLI_MODE=0
......@@ -86,7 +88,7 @@ source "${PYTHON_VENV_PATH}/bin/activate"
if [ "${TESTNET_MODE}" = "0" ]; then
if [ "${INTERACTIVE_CLI_MODE}" = "0" ]; then
echo "Launching clive in TUI mode on mainnet"
clive
exec clive
else
echo "Launching clive in CLI mode on mainnet"
launch_cli
......@@ -94,7 +96,7 @@ if [ "${TESTNET_MODE}" = "0" ]; then
else
if [ "${INTERACTIVE_CLI_MODE}" = "0" ]; then
echo "Launching clive in TUI mode on testnet"
python3 testnet_node.py
exec python3 testnet_node.py
else
echo "Launching clive in CLI mode on testnet"
......