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 (24)
Showing
with 1051 additions and 18 deletions
......@@ -193,7 +193,6 @@ class WitnessesDataRetrieval(CommandDataRetrieval[HarvestedDataRaw, SanitizedDat
witness_data = self.__create_witness_data(witness, data)
witnesses[witness.owner] = witness_data
assert len(witnesses) == self.search_by_pattern_limit
return witnesses
def __create_witness_data(self, witness: Witness, data: SanitizedData, *, rank: int | None = None) -> WitnessData:
......
from __future__ import annotations
from typing import TYPE_CHECKING
from clive.models import Asset
if TYPE_CHECKING:
from clive.models.aliased import DynamicGlobalProperties
def hive_to_vests(amount: int | Asset.Hive, gdpo: DynamicGlobalProperties) -> Asset.Vests:
if isinstance(amount, Asset.Hive):
amount = int(amount.amount)
return Asset.Vests(
amount=int(amount * int(gdpo.total_vesting_shares.amount) / int(gdpo.total_vesting_fund_hive.amount))
)
......@@ -14,13 +14,8 @@ from clive.__private.ui.operations.operation_base_screen import OperationBaseScr
from clive.__private.ui.widgets.clive_tabbed_content import CliveTabbedContent
if TYPE_CHECKING:
from typing import Final
from textual.app import ComposeResult
WITNESSES_TAB_LABEL: Final[str] = "Witnesses"
PROPOSALS_TAB_NAME: Final[str] = "Proposals"
class Governance(OperationBaseScreen):
CSS_PATH = [
......@@ -31,18 +26,19 @@ class Governance(OperationBaseScreen):
def create_left_panel(self) -> ComposeResult:
with WitnessesDataProvider(paused=True), ProposalsDataProvider(paused=True), CliveTabbedContent():
yield Proxy("Proxy")
yield Witnesses(WITNESSES_TAB_LABEL)
yield Proposals(PROPOSALS_TAB_NAME)
yield Witnesses("Witnesses")
yield Proposals("Proposals")
@on(CliveTabbedContent.TabActivated)
def change_provider_status(self, event: CliveTabbedContent.TabActivated) -> None:
witnesses_provider = self.query_one(WitnessesDataProvider)
proposals_provider = self.query_one(ProposalsDataProvider)
if str(event.tab.label) == PROPOSALS_TAB_NAME:
active_pane = event.tabbed_content.active_pane
if isinstance(active_pane, Proposals):
witnesses_provider.pause()
proposals_provider.restart()
elif str(event.tab.label) == WITNESSES_TAB_LABEL:
elif isinstance(active_pane, Witnesses):
proposals_provider.pause()
witnesses_provider.restart()
else:
......
from __future__ import annotations
from typing import TYPE_CHECKING
from clive.__private.core.hive_to_vests import hive_to_vests
from clive.__private.ui.widgets.dynamic_label import DynamicLabel
from clive.models import Asset
if TYPE_CHECKING:
from clive.__private.core.commands.data_retrieval.hive_power_data import HivePowerData
from clive.__private.ui.data_providers.hive_power_data_provider import HivePowerDataProvider
class HpVestsFactor(DynamicLabel):
DEFAULT_CSS = """
HpVestsFactor {
width: 1fr;
height: 1;
margin-bottom: 1;
align: center middle;
background: $warning;
color: $text;
}
"""
def __init__(self, provider: HivePowerDataProvider):
super().__init__(
obj_to_watch=provider, attribute_name="_content", callback=self._get_hp_vests_factor, init=False
)
def _get_hp_vests_factor(self, content: HivePowerData) -> str:
factor = hive_to_vests(1000, content.gdpo)
return f"HP is calculated to VESTS with the factor: 1.000 HP -> {Asset.pretty_amount(factor)} VESTS"
......@@ -23,6 +23,7 @@ from clive.__private.ui.operations.hive_power_management.delegate_hive_power.del
)
from clive.__private.ui.operations.hive_power_management.power_down.power_down import PowerDown
from clive.__private.ui.operations.hive_power_management.power_up.power_up import PowerUp
from clive.__private.ui.operations.hive_power_management.withdraw_routes.withdraw_routes import WithdrawRoutes
from clive.__private.ui.operations.operation_base_screen import OperationBaseScreen
from clive.__private.ui.widgets.big_title import BigTitle
from clive.__private.ui.widgets.clive_data_table import CliveDataTable
......@@ -33,6 +34,7 @@ if TYPE_CHECKING:
POWER_UP_TAB_LABEL: Final[str] = "Power up"
POWER_DOWN_TAB_LABEL: Final[str] = "Power down"
WITHDRAW_ROUTES_TAB_LABEL: Final[str] = "Withdraw routes"
DELEGATE_HIVE_POWER_LABEL: Final[str] = "Delegate"
......@@ -59,4 +61,5 @@ class HivePowerManagement(OperationBaseScreen):
with CliveTabbedContent():
yield PowerUp(POWER_UP_TAB_LABEL)
yield PowerDown(POWER_DOWN_TAB_LABEL)
yield WithdrawRoutes(WITHDRAW_ROUTES_TAB_LABEL)
yield DelegateHivePower(DELEGATE_HIVE_POWER_LABEL)
......@@ -2,23 +2,187 @@ from __future__ import annotations
from typing import TYPE_CHECKING
from textual.widgets import TabPane
from textual import on
from textual.containers import Horizontal, ScrollableContainer
from textual.widgets import Input, Pretty, Static, TabPane
from clive.__private.ui.widgets.clive_widget import CliveWidget
from clive.__private.core.formatters.humanize import humanize_datetime
from clive.__private.core.hive_to_vests import hive_to_vests
from clive.__private.ui.data_providers.hive_power_data_provider import HivePowerDataProvider
from clive.__private.ui.get_css import get_css_from_relative_path
from clive.__private.ui.operations.bindings.operation_action_bindings import OperationActionBindings
from clive.__private.ui.operations.hive_power_management.common_hive_power.hp_vests_factor import HpVestsFactor
from clive.__private.ui.operations.operation_summary.cancel_power_down import CancelPowerDown
from clive.__private.ui.widgets.clive_button import CliveButton
from clive.__private.ui.widgets.clive_checkerboard_table import (
EVEN_STYLE,
ODD_STYLE,
CliveCheckerboardTable,
CliveCheckerBoardTableCell,
CliveCheckerboardTableRow,
)
from clive.__private.ui.widgets.currency_selector.currency_selector_hp_vests import CurrencySelectorHpVests
from clive.__private.ui.widgets.generous_button import GenerousButton
from clive.__private.ui.widgets.inputs.hp_vests_amount_input import HPVestsAmountInput
from clive.models import Asset
from schemas.operations import WithdrawVestingOperation
if TYPE_CHECKING:
from datetime import datetime
from rich.text import TextType
from textual.app import ComposeResult
from textual.widget import Widget
from clive.__private.core.commands.data_retrieval.hive_power_data import HivePowerData
class PlaceTaker(Static):
pass
class ScrollablePart(ScrollableContainer):
pass
class WithdrawRoutesDisplay(Pretty):
"""Widget used just to inform user to which account has withdrawal route and how much % it is."""
class PendingPowerDownHeader(Horizontal):
def compose(self) -> ComposeResult:
yield Static("Next power down", classes=EVEN_STYLE)
yield Static("Power down(HP)", classes=ODD_STYLE)
yield Static("Power down(VESTS)", classes=EVEN_STYLE)
yield PlaceTaker()
class PendingPowerDown(CliveCheckerboardTable):
def __init__(self) -> None:
super().__init__(
Static("Pending Power down", id="pending-power-down-title"), PendingPowerDownHeader(), dynamic=True
)
def create_dynamic_rows(self, content: HivePowerData) -> list[Widget]:
if humanize_datetime(content.next_vesting_withdrawal) == "never":
return [Static("No pending power down", id="no-pending-power-down-info")]
return [
CliveCheckerboardTableRow(
CliveCheckerBoardTableCell(humanize_datetime(content.next_vesting_withdrawal), evenness="odd"),
CliveCheckerBoardTableCell(Asset.pretty_amount(content.next_power_down.hp_balance), evenness="even"),
CliveCheckerBoardTableCell(Asset.pretty_amount(content.next_power_down.vests_balance), evenness="odd"),
CliveButton("Cancel", variant="error"),
)
]
@on(CliveButton.Pressed)
def push_operation_summary_screen(self) -> None:
self.app.push_screen(
CancelPowerDown(self.provider.content.next_vesting_withdrawal, self.provider.content.next_power_down)
)
@property
def provider(self) -> HivePowerDataProvider:
return self.app.query_one(HivePowerDataProvider)
@property
def creature_to_reconstruction_check(self) -> datetime:
return self.provider.content.next_vesting_withdrawal
class PowerDown(TabPane, CliveWidget):
class PowerDown(TabPane, OperationActionBindings):
"""TabPane with all content about power down."""
def __init__(self, title: TextType):
DEFAULT_CSS = get_css_from_relative_path(__file__)
def __init__(self, title: TextType) -> None:
"""
Initialize a TabPane.
Initialize the PowerDown tab-pane.
Args:
----
title: Title of the TabPane (will be displayed in a tab label).
"""
super().__init__(title=title)
self._shares_input = HPVestsAmountInput()
self._instalment_display = Static("", id="instalment-display")
self._instalment_display.display = False
def compose(self) -> ComposeResult:
with ScrollablePart():
yield Static("Power down corresponds to a `withdraw vesting` operation", id="operation-name-info")
yield HpVestsFactor(self.provider)
with Horizontal(id="input-with-button"):
yield self._shares_input
yield GenerousButton(self._shares_input, self._get_shares_balance) # type: ignore[arg-type]
yield self._instalment_display
yield Static("Your withdraw routes", id="withdraw-routes-title")
yield WithdrawRoutesDisplay({})
yield PendingPowerDown()
def on_mount(self) -> None:
self.watch(self.provider, "_content", self._update_withdraw_routes, init=False)
def _get_shares_balance(self) -> Asset.Hive | Asset.Vests:
if self._shares_input.selected_asset_type is Asset.Hive:
return self.provider.content.owned_balance.hp_balance
return self.provider.content.owned_balance.vests_balance
@on(Input.Changed)
def calculate_one_withdrawal(self) -> None:
"""The withdrawal is divided into 13 parts - calculate and inform the user of the amount of one of them."""
shares_input = self._shares_input.value_or_none()
if shares_input is None:
self._instalment_display.display = False
self._instalment_display.update("")
return
one_withdrawal = shares_input / 13
self._instalment_display.update(
f"The withdrawal will be divided into 13 parts, one of which is: {Asset.pretty_amount(one_withdrawal)}"
)
self._instalment_display.display = True
@on(CurrencySelectorHpVests.Changed)
def shares_type_changed(self) -> None:
"""Clear input when shares type was changed and hide factor display when vests selected."""
self._shares_input.input.value = ""
if self._shares_input.selected_asset_type is Asset.Vests:
self.query_one(HpVestsFactor).display = False
return
self.query_one(HpVestsFactor).display = True
def _create_operation(self) -> WithdrawVestingOperation | None:
asset = self._shares_input.value_or_none()
if asset is None:
return None
if isinstance(asset, Asset.Vests):
return WithdrawVestingOperation(account=self.working_account, vesting_shares=asset)
hp_to_vests = hive_to_vests(asset, self.provider.content.gdpo)
# If the user has passed an amount in `HP` - convert it to `VESTS`. The operation is performed using VESTS.
return WithdrawVestingOperation(account=self.working_account, vesting_shares=hp_to_vests)
def _update_withdraw_routes(self, content: HivePowerData) -> None:
"""Update withdraw routes pretty widget."""
if not content.withdraw_routes:
self.query_one(WithdrawRoutesDisplay).update("You have no withdraw routes")
return
withdraw_routes = {}
for withdraw_route in content.withdraw_routes:
withdraw_routes[withdraw_route.to_account] = f"{withdraw_route.percent / 100}%"
self.query_one(WithdrawRoutesDisplay).update(withdraw_routes)
@property
def provider(self) -> HivePowerDataProvider:
return self.app.query_one(HivePowerDataProvider)
@property
def working_account(self) -> str:
return self.app.world.profile_data.working_account.name
$info-color: $accent;
CliveButton {
width: 1fr;
}
WithdrawRoutesDisplay {
content-align: center middle;
width: 1fr;
}
#withdraw-routes-title {
text-style: bold;
margin-top: 2;
background: $info-color;
width: 1fr;
text-align: center;
}
HPVestsAmountInput {
width: 5fr;
}
#input-with-button {
background: $panel;
padding: 2 4;
height: auto;
}
#instalment-display {
text-style: bold;
background: $info-color;
height: 1;
text-align: center;
}
#operation-name-info {
text-style: bold;
margin-bottom: 1;
background: $info-color;
width: 1fr;
height: 1;
text-align: center;
}
/* Pending power down */
#pending-power-down-title {
text-style: bold;
margin-top: 2;
background: $primary;
width: 1fr;
height: 1;
text-align: center;
}
#no-pending-power-down-info {
text-style: bold;
background: $info-color;
width: 1fr;
height: 1;
text-align: center;
}
PendingPowerDown {
height: auto;
}
PendingPowerDownHeader {
height: 1;
}
PendingPowerDownHeader Static {
text-style: bold;
width: 1fr;
text-align: center;
}
from __future__ import annotations
from typing import TYPE_CHECKING
from textual import on
from textual.containers import Horizontal, ScrollableContainer, Vertical
from textual.widgets import Checkbox, Static, TabPane
from clive.__private.ui.data_providers.hive_power_data_provider import HivePowerDataProvider
from clive.__private.ui.get_css import get_css_from_relative_path
from clive.__private.ui.operations.bindings import OperationActionBindings
from clive.__private.ui.operations.operation_summary.remove_withdraw_vesting_route import RemoveWithdrawVestingRoute
from clive.__private.ui.widgets.clive_button import CliveButton
from clive.__private.ui.widgets.clive_checkerboard_table import (
EVEN_STYLE,
ODD_STYLE,
CliveCheckerboardTable,
CliveCheckerBoardTableCell,
CliveCheckerboardTableRow,
)
from clive.__private.ui.widgets.inputs.account_name_input import AccountNameInput
from clive.__private.ui.widgets.inputs.clive_validated_input import CliveValidatedInput
from clive.__private.ui.widgets.inputs.percent_input import PercentInput
from schemas.operations import SetWithdrawVestingRouteOperation
if TYPE_CHECKING:
from rich.text import TextType
from textual.app import ComposeResult
from textual.widget import Widget
from clive.__private.core.commands.data_retrieval.hive_power_data import HivePowerData
from schemas.apis.database_api.fundaments_of_reponses import WithdrawVestingRoutesFundament as WithdrawRouteSchema
class PlaceTaker(Static):
pass
class WithdrawRoutesHeader(Horizontal):
def compose(self) -> ComposeResult:
yield Static("To", classes=ODD_STYLE)
yield Static("Percent", classes=EVEN_STYLE)
yield Static("Auto vest", classes=ODD_STYLE)
yield PlaceTaker()
class WithdrawRoute(CliveCheckerboardTableRow):
"""Row of the `WithdrawRoutesTable`."""
def __init__(self, withdraw_route: WithdrawRouteSchema, evenness: str = "odd") -> None:
"""
Initialize the WithdrawRoute row.
Args:
----
withdraw_route: Withdraw route data to display.
evenness: Evenness of the row.
"""
super().__init__(
CliveCheckerBoardTableCell(withdraw_route.to_account, evenness="odd" if evenness == "odd" else "even"),
CliveCheckerBoardTableCell(
f"{withdraw_route.percent / 100} %", evenness="even" if evenness == "odd" else "odd"
),
CliveCheckerBoardTableCell(f"{withdraw_route.auto_vest}", evenness="odd" if evenness == "odd" else "even"),
CliveButton("Remove", classes="remove-withdraw-route-button", variant="error"),
)
self._withdraw_route = withdraw_route
@on(CliveButton.Pressed, ".remove-withdraw-route-button")
def push_operation_summary_screen(self) -> None:
self.app.push_screen(RemoveWithdrawVestingRoute(self._withdraw_route))
class WithdrawRoutesTable(CliveCheckerboardTable):
"""Table with WithdrawRoutes."""
def __init__(self) -> None:
super().__init__(
Static("Current withdraw routes", id="withdraw-routes-table-title"), WithdrawRoutesHeader(), dynamic=True
)
def create_dynamic_rows(self, content: HivePowerData) -> list[Widget]:
if len(content.withdraw_routes) == 0:
return [Static("You have no withdraw routes", id="no-withdraw-routes-info")]
withdraw_routes: list[Widget] = []
for evenness, withdraw_route in enumerate(content.withdraw_routes):
withdraw_routes.append(WithdrawRoute(withdraw_route, "even" if evenness % 2 == 0 else "odd"))
return withdraw_routes
@property
def provider(self) -> HivePowerDataProvider:
return self.app.query_one(HivePowerDataProvider)
@property
def creature_to_reconstruction_check(self) -> list[WithdrawRouteSchema]:
return self.provider.content.withdraw_routes
@property
def working_account(self) -> str:
return self.app.world.profile_data.working_account.name
class WithdrawRoutes(TabPane, OperationActionBindings):
"""TabPane with all content about setting withdraw routes."""
DEFAULT_CSS = get_css_from_relative_path(__file__)
def __init__(self, title: TextType):
"""
Initialize a TabPane.
Args:
----
title: Title of the TabPane (will be displayed in a tab label).
"""
super().__init__(title=title)
self._account_input = AccountNameInput()
self._percent_input = PercentInput("Percent")
self._auto_vest_checkbox = Checkbox("Auto vest")
def compose(self) -> ComposeResult:
with ScrollableContainer():
yield Static("Set withdraw route", id="set-withdraw-route-title")
with Vertical(id="inputs-container"):
yield self._account_input
with Horizontal(id="input-with-checkbox"):
yield self._percent_input
yield self._auto_vest_checkbox
yield WithdrawRoutesTable()
def _create_operation(self) -> SetWithdrawVestingRouteOperation | None:
CliveValidatedInput.validate_many(self._account_input, self._percent_input)
return SetWithdrawVestingRouteOperation(
from_account=self.working_account,
to_account=self._account_input.value_or_error,
percent=self._percent_input.value_or_error * 100,
auto_vest=self._auto_vest_checkbox.value,
)
@property
def working_account(self) -> str:
return self.app.world.profile_data.working_account.name
WithdrawRoutes {
height: 1fr;
}
WithdrawRoutes Static {
text-style: bold;
width: 1fr;
text-align: center;
}
/* Set withdraw route */
#set-withdraw-route-title {
background: $primary;
width: 1fr;
height: 1;
}
#inputs-container {
background: $panel;
padding: 2 4;
height: auto;
}
#input-with-checkbox {
margin-top: 1;
height: auto;
}
#input-with-checkbox IntegerInput {
width: 1fr;
}
#input-with-checkbox Checkbox {
width: 17;
}
AccountNameInput KnownAccount {
margin-left: 3;
}
/* Withdraw routes table */
WithdrawRoutesHeader {
height: auto;
}
WithdrawRoutesTable {
margin-top: 1;
height: auto;
}
#no-withdraw-routes-info {
background: $primary-darken-1;
height: auto;
}
#withdraw-routes-table-title {
background: $primary;
width: 1fr;
}
WithdrawRoute {
height: auto;
}
WithdrawRoute .remove-withdraw-route-button {
background: $error-darken-1;
width: 1fr;
}
from __future__ import annotations
from typing import TYPE_CHECKING, ClassVar
from clive.__private.core.formatters.humanize import humanize_datetime
from clive.__private.ui.operations.operation_summary.operation_summary import OperationSummary
from clive.__private.ui.widgets.inputs.labelized_input import LabelizedInput
from clive.models import Asset
from schemas.operations import WithdrawVestingOperation
if TYPE_CHECKING:
from datetime import datetime
from textual.app import ComposeResult
from clive.__private.core.commands.data_retrieval.hive_power_data import SharesBalance
class CancelPowerDown(OperationSummary):
BIG_TITLE: ClassVar[str] = "Cancel power down"
def __init__(self, next_power_down_date: datetime, next_power_down: SharesBalance) -> None:
super().__init__()
self._next_power_down_date = next_power_down_date
self._next_power_down = next_power_down
def content(self) -> ComposeResult:
yield LabelizedInput("Next power down", humanize_datetime(self._next_power_down_date))
yield LabelizedInput("Power down(HP)", Asset.pretty_amount(self._next_power_down.hp_balance))
yield LabelizedInput("Power down(VESTS)", Asset.pretty_amount(self._next_power_down.vests_balance))
def _create_operation(self) -> WithdrawVestingOperation:
return WithdrawVestingOperation(
account=self.working_account,
vesting_shares=Asset.vests(0),
)
@property
def working_account(self) -> str:
return self.app.world.profile_data.working_account.name
from __future__ import annotations
from typing import TYPE_CHECKING, ClassVar, Final
from clive.__private.ui.operations.operation_summary.operation_summary import OperationSummary
from clive.__private.ui.widgets.inputs.labelized_input import LabelizedInput
from schemas.operations import SetWithdrawVestingRouteOperation
if TYPE_CHECKING:
from textual.app import ComposeResult
from schemas.apis.database_api.fundaments_of_reponses import WithdrawVestingRoutesFundament as WithdrawRoute
WITHDRAW_ROUTE_REMOVE_PERCENT: Final[int] = 0
class RemoveWithdrawVestingRoute(OperationSummary):
"""Screen to remove withdraw vesting route."""
BIG_TITLE: ClassVar[str] = "Remove withdraw route"
def __init__(self, withdraw_route: WithdrawRoute) -> None:
super().__init__()
self._withdraw_route = withdraw_route
def content(self) -> ComposeResult:
yield LabelizedInput("From account", self.working_account)
yield LabelizedInput("To account", self._withdraw_route.to_account)
yield LabelizedInput("Percent", str(self._withdraw_route.percent / 100))
yield LabelizedInput("Auto vest", str(self._withdraw_route.auto_vest))
def _create_operation(self) -> SetWithdrawVestingRouteOperation | None:
return SetWithdrawVestingRouteOperation(
from_account=self.working_account,
to_account=self._withdraw_route.to_account,
auto_vest=self._withdraw_route.auto_vest,
percent=WITHDRAW_ROUTE_REMOVE_PERCENT,
)
@property
def working_account(self) -> str:
return self.app.world.profile_data.working_account.name
......@@ -192,7 +192,6 @@ class TransactionSummaryCommon(BaseScreen):
)
).result_or_raise
except CommandRequiresActiveModeError:
self.notify("Active mode is required for this action.", severity="warning")
raise # reraise so try_again_after_activation decorator can handle it
except Exception as error: # noqa: BLE001
self.notify(f"Transaction save failed. Reason: {error}", severity="error")
......@@ -224,7 +223,6 @@ class TransactionSummaryCommon(BaseScreen):
)
).raise_if_error_occurred()
except CommandRequiresActiveModeError:
self.notify("Active mode is required for this action.", severity="warning")
raise # reraise so try_again_after_activation decorator can handle it
except Exception as error: # noqa: BLE001
self.notify(f"Transaction broadcast failed! Reason: {error}", severity="error")
......
from __future__ import annotations
from typing import TYPE_CHECKING, Any, ClassVar, Final
from textual.containers import Vertical
from textual.widgets import Static
from clive.__private.ui.widgets.clive_widget import CliveWidget
from clive.exceptions import CliveError
if TYPE_CHECKING:
from textual.app import ComposeResult
from textual.widget import Widget
from clive.__private.ui.widgets.clive_button import CliveButton
ODD_STYLE: Final[str] = "OddColumn"
EVEN_STYLE: Final[str] = "EvenColumn"
class CliveCheckerboardTableError(CliveError):
pass
class InvalidDynamicDefinedError(CliveCheckerboardTableError):
MESSAGE = """
You are trying to create a dynamic checkerboard table without overriding the `provider` property.
Replace it or set the `dynamic` parameter to False if you want to create a static table.
"""
def __init__(self) -> None:
super().__init__(self.MESSAGE)
class PlaceTaker(Static):
pass
class CliveCheckerBoardTableCell(Vertical):
"""3 - row cell of the table. Use `Static` instead if you only want single row."""
DEFAULT_CSS = """
CliveCheckerBoardTableCell {
width: 1fr;
}
CliveCheckerBoardTableCell Static {
text-style: bold;
text-align: center;
}
"""
def __init__(self, text: str, evenness: str = "odd", id_: str | None = None, classes: str | None = None) -> None:
"""
Initialise the checkerboard table cell.
Args:
----
text: Text to be displayed in the cell.
evenness: Evenness of the cell.
id_: The ID of the widget in the DOM.
classes: The CSS classes for the widget.
"""
super().__init__(id=id_, classes=f"{classes} {ODD_STYLE if evenness == 'odd' else EVEN_STYLE}".lstrip())
self._text = text
def compose(self) -> ComposeResult:
yield PlaceTaker()
yield Static(self._text)
yield PlaceTaker()
class CliveCheckerboardTableRow(CliveWidget):
"""Row with checkerboard columns."""
DEFAULT_CSS = """
CliveCheckerboardTableRow {
layout: horizontal;
height: auto;
}
"""
def __init__(self, *cells: CliveCheckerBoardTableCell | Static | CliveButton):
super().__init__()
self._cells = cells
def compose(self) -> ComposeResult:
yield from self._cells
class CliveCheckerboardTable(CliveWidget):
"""
Table that displays checkerboard rows.
Dynamic usage
-------------
1. Set `dynamic` to True in `__init__`
2. Override `provider` property
3. Override `creature_to_reconstruction_check`
4. Override `create_dynamic_rows`.
Static usage
------------
1. Override `create_static_rows`
"""
DEFAULT_CSS = """
CliveCheckerboardTable {
layout: vertical;
}
CliveCheckerboardTable Vertical {
height: auto;
}
CliveCheckerboardTable .OddColumn {
background: $primary-background-darken-2;
}
CliveCheckerboardTable .EvenColumn {
background: $primary-background-darken-1;
}
CliveCheckerboardTable #loading-static {
text-align: center;
text-style: bold;
}
"""
ROWS_CONTAINER_ID: ClassVar[str] = "container-with-rows"
"""Id of the container in which the `_mount_new_rows` method should mount the rows."""
def __init__(self, title: Widget, header: Widget, dynamic: bool = False):
super().__init__()
self._title = title
self._header = header
self._dynamic = dynamic
self._is_after_first_rebuilt = False
self._reconstruction_checker: Any = None
"""Used to check whether the data has changed since the last refresh and whether the rows should be rebuilt."""
def compose(self) -> ComposeResult:
yield self._title
yield self._header
with Vertical(id=self.ROWS_CONTAINER_ID):
yield Static("Loading...", id="loading-static")
if not self._dynamic:
self._mount_static_rows()
def on_mount(self) -> None:
if self._dynamic:
self.watch(self.provider, "_content", self._mount_dynamic_rows, init=False)
def _mount_static_rows(self) -> None:
"""Mount rows created in static mood (dynamic = False)."""
self.query_one(f"#{self.ROWS_CONTAINER_ID}").query("*").remove()
self.mount_all(self.create_static_rows())
def _mount_dynamic_rows(self, content: Any) -> None:
"""New rows are mounted when the data to be displayed has been changed or when there is a first sync."""
if not self._dynamic:
raise InvalidDynamicDefinedError
if self.creature_to_reconstruction_check != self._reconstruction_checker or not self._is_after_first_rebuilt:
self._rows_rebuilt()
self._reconstruction_checker = self.creature_to_reconstruction_check
with self.app.batch_update():
rows_container = self.query_one(f"#{self.ROWS_CONTAINER_ID}")
rows_container.query("*").remove()
new_rows = self.create_dynamic_rows(content)
rows_container.mount_all(new_rows)
return
def create_dynamic_rows(self, content: Any) -> list[Widget]: # noqa: ARG002
"""
Override if dynamic is set to True.
Raises
------
InvalidDynamicDefinedError: When dynamic has been set to `True` without overriding the method.
"""
if self._dynamic:
raise InvalidDynamicDefinedError
return [Static("Define create_dynamic_rows method !")]
def create_static_rows(self) -> list[Widget]:
"""Override if dynamic is set to False."""
return [Static("Define create_static_rows method !")]
def _rows_rebuilt(self) -> None:
self._is_after_first_rebuilt = True
@property
def creature_to_reconstruction_check(self) -> Any:
"""
Must be overridden by the inheritance class.
A property containing a model/variable whose change will cause the table to be rebuilt.
Examples
--------
return self.provider.content.pending_transfers.
Raises
------
InvalidDynamicDefinedError: When dynamic has been set to `True` without overriding the property.
"""
if self._dynamic:
raise InvalidDynamicDefinedError
@property
def provider(self) -> Any:
"""
Must be overridden by the inheritance class.
Raises
------
InvalidDynamicDefinedError: When dynamic has been set to `True` without overriding the property.
"""
if self._dynamic:
raise InvalidDynamicDefinedError
......@@ -61,11 +61,12 @@ class CliveScreen(Screen[ScreenResultType], CliveWidget):
async def _on_activation_result(value: bool) -> None:
if not value:
app_.notify("Aborted. Active mode is required for this action.", severity="warning")
app_.notify("Aborted. Active mode was required for this action.", severity="warning")
return
await func(*args, **kwargs)
app_.notify("This action requires active mode. Please activate...")
await app_.push_screen(Activate(activation_result_callback=_on_activation_result))
return wrapper
......
from __future__ import annotations
from clive.__private.ui.widgets.currency_selector.currency_selector_base import CurrencySelectorBase
from clive.models import Asset
from clive.models.asset import AssetFactoryHolder
class CurrencySelectorHpVests(CurrencySelectorBase[Asset.Hive | Asset.Vests]):
@staticmethod
def _create_selectable() -> dict[str, AssetFactoryHolder[Asset.Hive | Asset.Vests]]:
return {
"HP": AssetFactoryHolder(asset_cls=Asset.Hive, asset_factory=Asset.hive),
"VESTS": AssetFactoryHolder(asset_cls=Asset.Vests, asset_factory=Asset.vests),
}
from __future__ import annotations
from typing import TYPE_CHECKING
from textual import on
from textual.containers import Horizontal, Vertical
from clive.__private.ui.widgets.currency_selector.currency_selector_hp_vests import CurrencySelectorHpVests
from clive.__private.ui.widgets.inputs.clive_validated_input import (
CliveValidatedInput,
)
from clive.__private.validators.asset_amount_validator import AssetAmountValidator
from clive.models import Asset
if TYPE_CHECKING:
from collections.abc import Iterable
from textual.app import ComposeResult
from textual.widgets._input import InputValidationOn
class HPVestsAmountInput(CliveValidatedInput[Asset.VotingT]):
"""An input for HP/VESTS amount."""
DEFAULT_CSS = """
HPVestsAmountInput {
height: auto;
Vertical {
height: auto;
Horizontal {
height: auto;
CliveInput {
width: 1fr;
}
CurrencySelectorHpVests {
width: 14;
}
}
}
}
"""
def __init__(
self,
title: str = "Amount",
value: str | float | None = None,
placeholder: str | None = None,
*,
always_show_title: bool = False,
include_title_in_placeholder_when_blurred: bool = True,
show_invalid_reasons: bool = True,
required: bool = True,
validate_on: Iterable[InputValidationOn] | None = None,
valid_empty: bool = False,
id: str | None = None, # noqa: A002
classes: str | None = None,
disabled: bool = False,
) -> None:
"""
Initialize the widget.
Args difference from `CliveValidatedInput`:
----
placeholder: If not provided, placeholder will be dynamically generated based on the asset type.
"""
self._currency_selector = CurrencySelectorHpVests()
default_asset_type = self._currency_selector.default_asset_cls
default_asset_precision = default_asset_type.get_asset_information().precision
super().__init__(
title=title,
value=str(value) if value is not None else None,
placeholder=self._get_dynamic_placeholder(default_asset_precision),
always_show_title=always_show_title,
include_title_in_placeholder_when_blurred=include_title_in_placeholder_when_blurred,
show_invalid_reasons=show_invalid_reasons,
required=required,
restrict=self._create_restriction(default_asset_precision),
type="number",
validators=[AssetAmountValidator(default_asset_type)],
validate_on=validate_on,
valid_empty=valid_empty,
id=id,
classes=classes,
disabled=disabled,
)
self._dynamic_placeholder = placeholder is None
@property
def _value(self) -> Asset.Hive | Asset.Vests:
"""
Return the value of the input as a HIVE(HP)/VESTS asset.
Probably you want to use other `value_` properties instead.
Raises
------
AssetAmountInvalidFormatError: Raised when given amount is in invalid format.
"""
return self._currency_selector.create_asset(self.value_raw)
@property
def selected_asset_type(self) -> type[Asset.Hive | Asset.Vests]:
return self._currency_selector.asset_cls
@property
def selected_asset_precision(self) -> int:
return self.selected_asset_type.get_asset_information().precision
def compose(self) -> ComposeResult:
with Vertical():
with Horizontal():
yield self.input
yield self._currency_selector
yield self.pretty
@on(CurrencySelectorHpVests.Changed)
def _asset_changed(self) -> None:
# update placeholder
if self._dynamic_placeholder:
self.input.set_unmodified_placeholder(self._get_dynamic_placeholder(self.selected_asset_precision))
# update input restrict
self.input.restrict = self._create_restriction(self.selected_asset_precision)
# update asset amount validator
self.input.validators = [
validator for validator in self.input.validators if not isinstance(validator, AssetAmountValidator)
]
self.input.validators.append(AssetAmountValidator(self.selected_asset_type))
# need to revalidate the input (possible to switch from higher precision to lower precision)
self.input.validate(self.input.value)
def _create_restriction(self, precision: int) -> str:
precision_digits = f"{{0,{precision}}}"
return rf"\d*\.?\d{precision_digits}"
def _get_dynamic_placeholder(self, precision: int) -> str:
max_allowed_precision = 9
assert precision >= 0, f"Precision must be non-negative, got {precision}"
assert precision <= max_allowed_precision, f"Precision must be at most {max_allowed_precision}, got {precision}"
numbers = "123456789"
return f"e.g.: 1.{numbers[:precision]}"
from __future__ import annotations
from typing import TYPE_CHECKING
from clive.__private.ui.widgets.inputs.integer_input import IntegerInput
from clive.__private.validators.percent_validator import PercentValidator
if TYPE_CHECKING:
from collections.abc import Iterable
from textual.widgets._input import InputValidationOn
class PercentInput(IntegerInput):
"""An input for a values between 1 and 100."""
def __init__(
self,
title: str,
value: str | int | None = None,
*,
always_show_title: bool = False,
include_title_in_placeholder_when_blurred: bool = True,
show_invalid_reasons: bool = True,
required: bool = True,
validate_on: Iterable[InputValidationOn] | None = None,
valid_empty: bool = False,
id: str | None = None, # noqa: A002
classes: str | None = None,
disabled: bool = False,
) -> None:
super().__init__(
title=title,
value=str(value) if value is not None else None,
always_show_title=always_show_title,
include_title_in_placeholder_when_blurred=include_title_in_placeholder_when_blurred,
show_invalid_reasons=show_invalid_reasons,
required=required,
validators=PercentValidator(),
validate_on=validate_on,
valid_empty=valid_empty,
id=id,
classes=classes,
disabled=disabled,
)
from __future__ import annotations
from typing import Final
from textual.validation import ValidationResult, Validator
class PercentValidator(Validator):
INVALID_PERCENT_FAILURE_DESCRIPTION: str = "Invalid percent value."
MIN_PERCENT_VALUE: Final[int] = 1
MAX_PERCENT_VALUE: Final[int] = 100
def validate(self, value: str) -> ValidationResult:
if not value:
return self.failure(self.INVALID_PERCENT_FAILURE_DESCRIPTION, value)
if self.MIN_PERCENT_VALUE <= int(value) <= self.MAX_PERCENT_VALUE:
return self.success()
return self.failure(self.INVALID_PERCENT_FAILURE_DESCRIPTION, value)
......@@ -48,6 +48,7 @@ class Asset:
Hbd: TypeAlias = AssetHbdHF26
Vests: TypeAlias = AssetVestsHF26
LiquidT: TypeAlias = Hive | Hbd
VotingT: TypeAlias = Hive | Vests
AnyT: TypeAlias = Hive | Hbd | Vests
@classmethod
......