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 (13)
Showing
with 111 additions and 61 deletions
......@@ -212,7 +212,10 @@ deploy_wheel_to_gitlab:
extends: .deploy_wheel_to_gitlab_template
stage: deploy
<<: *deploy_wheel_needs
rules:
- if: ($CI_COMMIT_BRANCH == "master" || $CI_COMMIT_BRANCH == "develop")
when: on_success
- when: manual
deploy_wheel_to_pypi:
extends: .deploy_wheel_to_pypi_template
......
......@@ -4,7 +4,7 @@ from dataclasses import dataclass
from clive.__private.cli.commands.abc.beekeeper_based_command import BeekeeperCommon
from clive.__private.cli.commands.abc.contextual_cli_command import ContextualCLICommand
from clive.__private.core.beekeeper import Beekeeper
from clive.__private.core.world import TyperWorld, World
from clive.__private.core.world import CLIWorld, World
@dataclass(kw_only=True)
......@@ -23,7 +23,7 @@ class WorldBasedCommand(ContextualCLICommand[World], BeekeeperCommon, ABC):
return self.world.beekeeper
async def _create_context_manager_instance(self) -> World:
return TyperWorld(
return CLIWorld(
profile_name=self.profile_name,
use_beekeeper=self.use_beekeeper,
beekeeper_remote_endpoint=self.beekeeper_remote_url,
......
......@@ -118,7 +118,5 @@ class RemoveKey(WorldBasedCommand):
self.world.profile.keys.remove(key)
async def __remove_key_from_the_beekeeper(self, key: PublicKeyAliased) -> None:
unlock_wrapper = await self.world.commands.unlock(password=self.password)
unlock_wrapper.raise_if_error_occurred()
remove_wrapper = await self.world.commands.remove_key(password=self.password, key_to_remove=key)
remove_wrapper.raise_if_error_occurred()
await self.world.commands.unlock(password=self.password)
await self.world.commands.remove_key(password=self.password, key_to_remove=key)
......@@ -15,7 +15,7 @@ class ShowBalances(WorldBasedCommand):
async def _run(self) -> None:
account = TrackedAccount(name=self.account_name)
(await self.world.commands.update_node_data(accounts=[account])).raise_if_error_occurred()
await self.world.commands.update_node_data(accounts=[account])
table = Table(title=f"Balances of `{self.account_name}` account")
......
......@@ -4,6 +4,7 @@ from clive.__private.cli.clive_typer import CliveTyper
from clive.__private.cli.exceptions import CLIPrettyError, CLIProfileAlreadyExistsError, CLIProfileDoesNotExistsError
from clive.__private.core.commands.abc.command_secured import InvalidPasswordError
from clive.__private.core.commands.unlock import WalletDoesNotExistsError
from clive.__private.core.error_handlers.abc.error_notificator import CannotNotifyError
from clive.__private.storage.service import ProfileAlreadyExistsError, ProfileDoesNotExistsError
from clive.dev import is_in_dev_mode
from clive.exceptions import CommunicationError
......@@ -22,6 +23,10 @@ def register_error_handlers(cli: CliveTyper) -> None:
def handle_communication_error(error: CommunicationError) -> None:
raise CLIPrettyError(str(error), errno.ECOMM) from None
@cli.error_handler(CannotNotifyError)
def handle_cannot_notify_error(error: CannotNotifyError) -> None:
raise CLIPrettyError(error.reason, 1) from None
@cli.error_handler(InvalidPasswordError)
def handle_invalid_password_error(_: InvalidPasswordError) -> None:
raise CLIPrettyError("Invalid password.", errno.EINVAL) from None
......
......@@ -66,7 +66,7 @@ if TYPE_CHECKING:
AnyErrorHandlerContextManager,
)
from clive.__private.core.keys import PrivateKeyAliased, PublicKey, PublicKeyAliased
from clive.__private.core.world import TextualWorld, World
from clive.__private.core.world import CLIWorld, TUIWorld, World
from clive.__private.models import Transaction
from clive.__private.models.schemas import (
Account,
......@@ -469,8 +469,13 @@ class Commands(Generic[WorldT_co]):
return CommandWrapper(command=command, error=error)
class TextualCommands(Commands["TextualWorld"], CliveDOMNode):
def __init__(self, world: TextualWorld) -> None:
class CLICommands(Commands["CLIWorld"]):
def __init__(self, world: CLIWorld) -> None:
super().__init__(world, exception_handlers=[CommunicationFailureNotificator, GeneralErrorNotificator])
class TUICommands(Commands["TUIWorld"], CliveDOMNode):
def __init__(self, world: TUIWorld) -> None:
super().__init__(world, exception_handlers=[CommunicationFailureNotificator, GeneralErrorNotificator])
async def unlock(self, *, password: str, time: timedelta | None = None, permanent: bool = False) -> CommandWrapper:
......
from __future__ import annotations
from typing import Final
CLIVE_ODD_COLUMN_CLASS_NAME: Final[str] = "-odd-column"
CLIVE_EVEN_COLUMN_CLASS_NAME: Final[str] = "-even-column"
"""File with placeholders that are used very often."""
from __future__ import annotations
from typing import Final
......
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING
from clive.__private.core.clive_import import get_clive
from clive.__private.core.error_handlers.abc.error_handler_context_manager import (
......@@ -8,12 +9,45 @@ from clive.__private.core.error_handlers.abc.error_handler_context_manager impor
ExceptionT,
ResultNotAvailable,
)
from clive.__private.logger import logger
if TYPE_CHECKING:
from types import TracebackType
class ErrorNotificatorError(Exception):
"""Base class for exceptions raised by ErrorNotificator."""
class CannotNotifyError(ErrorNotificatorError):
def __init__(self, error: Exception, reason: str) -> None:
self.error = error
self.reason = reason
self.message = f"Error occurred, but no one was notified: {reason}"
super().__init__(self.message)
class ErrorNotificator(ErrorHandlerContextManager[ExceptionT], ABC):
"""A context manager that notifies about errors."""
async def __aexit__(
self,
exc_type: type[BaseException] | None,
exc_val: BaseException | None,
exc_tb: TracebackType | None,
) -> bool:
"""Return false if exception should be re-raised."""
if exc_val is not None and isinstance(exc_val, Exception):
try:
await self.try_to_handle_error(exc_val)
except CannotNotifyError:
# returning false would result in incorrect error being thrown
raise
except Exception: # noqa: BLE001
return False
else:
return True
return False
@abstractmethod
def _determine_message(self, exception: ExceptionT) -> str:
"""Return message to be displayed in notification."""
......@@ -29,7 +63,7 @@ class ErrorNotificator(ErrorHandlerContextManager[ExceptionT], ABC):
self._notify_tui(message)
return
logger.warning(f"Command failed and no one was notified. {message=}")
raise CannotNotifyError(exception, message)
def _notify_tui(self, message: str) -> None:
get_clive().app_instance().notify(message, severity="error")
......@@ -4,7 +4,6 @@ from typing import Final, TypeGuard
from clive.__private.core.clive_import import get_clive
from clive.__private.core.error_handlers.abc.error_notificator import ErrorNotificator
from clive.dev import is_in_dev_mode
from clive.exceptions import CommunicationError, CommunicationTimeoutError
......@@ -27,7 +26,7 @@ class CommunicationFailureNotificator(ErrorNotificator[CommunicationError]):
url = exception.url
if not error_messages:
return cls._get_communication_detailed_error_message(url, "No error details available.")
return cls._get_communication_not_available_message(url)
replaced: list[str] = []
for error_message in error_messages:
......@@ -45,26 +44,23 @@ class CommunicationFailureNotificator(ErrorNotificator[CommunicationError]):
"""
Notify about the error in TUI if it's necessary.
Presents explicit error message always in dev mode for debugging purposes
or if response is available because if there is no response, it indicates general connection issue
and no need to notify user about it multiple times and show request details because that causes a lot of long
and unreadable notifications.
Notifies always only if the error response is available because if there is no response,
it indicates general connection issue and no need to notify user about it multiple times
because that causes a lot of notifications.
"""
error = self.error_ensure
def should_notify_with_explicit_error() -> bool:
return is_in_dev_mode() or error.is_response_available
def is_error_response_available() -> bool:
return self.error_ensure.is_response_available
if should_notify_with_explicit_error():
if is_error_response_available():
super()._notify_tui(message)
return
clive_app = get_clive().app_instance()
notification_content = self._get_communication_not_available_message(error.url)
if clive_app.is_notification_present(notification_content):
if clive_app.is_notification_present(message):
return
super()._notify_tui(notification_content)
super()._notify_tui(message)
@staticmethod
def _get_communication_not_available_message(url: str) -> str:
......
......@@ -8,7 +8,7 @@ from textual.reactive import var
from clive.__private.core.app_state import AppState
from clive.__private.core.beekeeper import Beekeeper
from clive.__private.core.commands.commands import Commands, TextualCommands
from clive.__private.core.commands.commands import CLICommands, Commands, TUICommands
from clive.__private.core.communication import Communication
from clive.__private.core.node.node import Node
from clive.__private.core.profile import Profile
......@@ -140,7 +140,7 @@ class World:
self.app_state.lock()
class TextualWorld(World, ManualReactive):
class TUIWorld(World, ManualReactive):
profile: Profile = var(None) # type: ignore[assignment]
app_state: AppState = var(None) # type: ignore[assignment]
node: Node = var(None) # type: ignore[assignment]
......@@ -163,8 +163,8 @@ class TextualWorld(World, ManualReactive):
return profile
@property
def commands(self) -> TextualCommands:
return cast(TextualCommands, super().commands)
def commands(self) -> TUICommands:
return cast(TUICommands, super().commands)
@property
def is_in_onboarding_mode(self) -> bool:
......@@ -173,8 +173,8 @@ class TextualWorld(World, ManualReactive):
def _is_in_onboarding_mode(self, profile: Profile) -> bool:
return profile.name == Onboarding.ONBOARDING_PROFILE_NAME
def _setup_commands(self) -> TextualCommands:
return TextualCommands(self)
def _setup_commands(self) -> TUICommands:
return TUICommands(self)
def notify_wallet_closing(self) -> None:
super().notify_wallet_closing()
......@@ -186,6 +186,13 @@ class TextualWorld(World, ManualReactive):
self.app.trigger_app_state_watchers()
class TyperWorld(World):
class CLIWorld(World):
@property
def commands(self) -> CLICommands:
return cast(CLICommands, super().commands)
def _setup_commands(self) -> CLICommands:
return CLICommands(self)
def _load_profile(self, profile_name: str | None) -> Profile:
return Profile.load(profile_name, auto_create=False)
......@@ -16,16 +16,15 @@ from textual.notifications import Notification, Notify, SeverityLevel
from textual.reactive import var
from clive.__private.core.constants.terminal import TERMINAL_HEIGHT, TERMINAL_WIDTH
from clive.__private.core.world import TextualWorld
from clive.__private.core.world import TUIWorld
from clive.__private.logger import logger
from clive.__private.settings import safe_settings
from clive.__private.ui.dashboard.dashboard_locked import DashboardLocked
from clive.__private.ui.dashboard.dashboard_unlocked import DashboardUnlocked
from clive.__private.ui.get_css import get_relative_css_path
from clive.__private.ui.help import Help
from clive.__private.ui.manual_reactive import ManualReactive
from clive.__private.ui.onboarding.onboarding import Onboarding
from clive.__private.ui.quit.quit import Quit
from clive.__private.ui.shared.help import Help
from clive.__private.ui.screens.dashboard import DashboardLocked, DashboardUnlocked
from clive.__private.ui.screens.quit import Quit
from clive.exceptions import CommunicationError, ScreenNotFoundError
if TYPE_CHECKING:
......@@ -36,7 +35,7 @@ if TYPE_CHECKING:
from textual.screen import Screen, ScreenResultCallbackType, ScreenResultType
from textual.widget import AwaitMount
from clive.__private.ui.pilot import ClivePilot
from clive.__private.ui.clive_pilot import ClivePilot
UpdateScreenResultT = TypeVar("UpdateScreenResultT")
......@@ -76,7 +75,7 @@ class Clive(App[int], ManualReactive):
is_launched: ClassVar[bool] = False
"""Whether the Clive app is currently launched."""
world: ClassVar[TextualWorld] = None # type: ignore[assignment]
world: ClassVar[TUIWorld] = None # type: ignore[assignment]
notification_history: list[Notification] = var([], init=False) # type: ignore[assignment]
"""A list of all notifications that were displayed."""
......@@ -131,7 +130,7 @@ class Clive(App[int], ManualReactive):
auto_pilot: AutopilotCallbackType | None = None,
) -> int | None:
try:
async with TextualWorld() as world:
async with TUIWorld() as world:
self.__class__.world = world
return await super().run_async(
headless=headless,
......@@ -158,7 +157,7 @@ class Clive(App[int], ManualReactive):
message_hook: Callable[[Message], None] | None = None,
) -> AsyncGenerator[ClivePilot, None]:
try:
async with TextualWorld() as world:
async with TUIWorld() as world:
self.__class__.world = world
async with super().run_test(
headless=headless,
......
......@@ -11,7 +11,7 @@ from textual.screen import Screen, ScreenResultType
from clive.__private.core.clive_import import get_clive
from clive.__private.core.commands.abc.command_in_unlocked import CommandRequiresUnlockedModeError
from clive.__private.logger import logger
from clive.__private.ui.widgets.clive_widget import CliveWidget
from clive.__private.ui.clive_widget import CliveWidget
from clive.exceptions import CliveError
if TYPE_CHECKING:
......@@ -72,7 +72,7 @@ class CliveScreen(Screen[ScreenResultType], CliveWidget):
try:
await func(*args, **kwargs)
except (CommandRequiresUnlockedModeError, OnlyInUnlockedModeError):
from clive.__private.ui.unlock.unlock import Unlock
from clive.__private.ui.screens.unlock.unlock import Unlock
async def _on_unlock_result(*, unlocked: bool) -> None:
if not unlocked:
......
......@@ -12,10 +12,10 @@ if TYPE_CHECKING:
from textual.binding import Binding
from clive.__private.core.app_state import AppState
from clive.__private.core.commands.commands import TextualCommands
from clive.__private.core.commands.commands import TUICommands
from clive.__private.core.node import Node
from clive.__private.core.profile import Profile
from clive.__private.core.world import TextualWorld
from clive.__private.core.world import TUIWorld
class CliveWidget(CliveDOMNode, Widget):
......@@ -26,7 +26,7 @@ class CliveWidget(CliveDOMNode, Widget):
"""
@property
def world(self) -> TextualWorld:
def world(self) -> TUIWorld:
return self.app.world
@property
......@@ -38,7 +38,7 @@ class CliveWidget(CliveDOMNode, Widget):
return self.world.app_state
@property
def commands(self) -> TextualCommands:
def commands(self) -> TUICommands:
return self.world.commands
@property
......
......@@ -10,8 +10,8 @@ from textual.worker import Worker, WorkerState
from clive.__private.abstract_class import AbstractClassMessagePump
from clive.__private.settings import safe_settings
from clive.__private.ui.widgets.clive_screen import CliveScreen
from clive.__private.ui.widgets.clive_widget import CliveWidget
from clive.__private.ui.clive_screen import CliveScreen
from clive.__private.ui.clive_widget import CliveWidget
from clive.exceptions import CliveError
ProviderContentT = TypeVar("ProviderContentT")
......
......@@ -8,12 +8,11 @@ from textual.containers import Container, Horizontal, Vertical
from textual.screen import ModalScreen
from textual.widgets import Static
from clive.__private.ui.account_details.alarms.fix_alarm_info_widget import FixAlarmInfoWidget
from clive.__private.core.constants.tui.class_names import CLIVE_EVEN_COLUMN_CLASS_NAME, CLIVE_ODD_COLUMN_CLASS_NAME
from clive.__private.ui.get_css import get_relative_css_path
from clive.__private.ui.screens.account_details.alarms.fix_alarm_info_widget import FixAlarmInfoWidget
from clive.__private.ui.widgets.buttons.close_button import CloseButton
from clive.__private.ui.widgets.clive_checkerboard_table import (
EVEN_CLASS_NAME,
ODD_CLASS_NAME,
from clive.__private.ui.widgets.clive_basic import (
CliveCheckerboardTable,
CliveCheckerBoardTableCell,
CliveCheckerboardTableRow,
......@@ -25,7 +24,7 @@ if TYPE_CHECKING:
from clive.__private.core.accounts.accounts import TrackedAccount
from clive.__private.core.alarms.alarm import AnyAlarm
from clive.__private.ui.account_details.alarms.alarm_fix_details import AlarmFixDetails
from clive.__private.ui.screens.account_details.alarms.alarm_fix_details import AlarmFixDetails
class AlarmInfoDialogContent(Vertical):
......@@ -41,7 +40,7 @@ class AlarmDataHeader(Horizontal):
def compose(self) -> ComposeResult:
for evenness, column in enumerate(self._columns):
yield Static(column, classes=EVEN_CLASS_NAME if evenness % 2 else ODD_CLASS_NAME)
yield Static(column, classes=CLIVE_EVEN_COLUMN_CLASS_NAME if evenness % 2 else CLIVE_ODD_COLUMN_CLASS_NAME)
class AlarmDataRow(CliveCheckerboardTableRow):
......
......@@ -9,10 +9,10 @@ from textual.screen import ModalScreen
from clive.__private.core.accounts.accounts import Account
from clive.__private.models import Asset
from clive.__private.ui.clive_widget import CliveWidget
from clive.__private.ui.get_css import get_relative_css_path
from clive.__private.ui.operations import HivePowerManagement, Savings, TransferToAccount
from clive.__private.ui.screens.operations import HivePowerManagement, Savings, TransferToAccount
from clive.__private.ui.widgets.buttons.one_line_button import OneLineButton
from clive.__private.ui.widgets.clive_widget import CliveWidget
from clive.__private.ui.widgets.section import Section
if TYPE_CHECKING:
......
......@@ -11,8 +11,8 @@ from textual.widgets import Static
from clive.__private.core.formatters.humanize import humanize_datetime, humanize_hbd_exchange_rate
from clive.__private.settings import safe_settings
from clive.__private.ui.clive_widget import CliveWidget
from clive.__private.ui.get_css import get_relative_css_path
from clive.__private.ui.widgets.clive_widget import CliveWidget
if TYPE_CHECKING:
from textual.app import ComposeResult
......