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 (8)
Showing
with 1357 additions and 150 deletions
......@@ -61,6 +61,7 @@ repos:
rev: v2.2.4
hooks:
- id: codespell
args: ["--skip=clive/__private/core/accounts/bad_accounts_list.txt"]
- repo: https://github.com/lk16/detect-missing-init
rev: v0.1.6
hooks:
......
from __future__ import annotations
from typing import Iterable
from pathlib import Path
from typing import Final, Iterable
from clive.__private.core.accounts.account_container import (
KnownAccountContainer,
......@@ -11,12 +12,30 @@ from clive.__private.core.accounts.exceptions import (
AccountAlreadyExistsError,
AccountNotFoundError,
NoWorkingAccountError,
TryingToAddBadAccountError,
)
def _load_bad_accounts_from_file() -> list[str]:
current_dir = Path(__file__).parent
bad_accounts_file_path = current_dir / "bad_accounts_list.txt"
with bad_accounts_file_path.open() as f:
account_names = []
for account_name in f.readlines():
name = account_name.strip()
Account.validate(name)
account_names.append(name)
return account_names
class AccountManager:
"""Class for storing and managing accounts."""
BAD_ACCOUNT_NAMES: Final[list[str]] = _load_bad_accounts_from_file()
def __init__(
self,
working_account: str | Account | None = None,
......@@ -127,6 +146,11 @@ class AccountManager:
account_name = Account.ensure_account_name(account)
return account_name in [tracked_account.name for tracked_account in self.tracked]
@classmethod
def is_account_bad(cls, account: str | Account) -> bool:
account_name = Account.ensure_account_name(account)
return account_name in cls.BAD_ACCOUNT_NAMES
def set_working_account(self, value: str | Account) -> None:
"""
Set the working account.
......@@ -182,6 +206,7 @@ class AccountManager:
Add accounts to the tracked (working + watched) accounts.
When there's no working account, the first account from the list will be set as working account.
Each tracked account is added to the known list if it doesn't already exist there.
Args:
----
......@@ -191,12 +216,16 @@ class AccountManager:
------
AccountAlreadyExistsError: If any of the accounts already exists in tracked accounts
(either as working or watched).
TryingToAddBadAccountError: If any of the accounts is a bad account.
"""
if not to_add:
return
if not self.has_working_account:
first_account_to_add = to_add[0]
if self.is_account_bad(first_account_to_add):
raise TryingToAddBadAccountError(Account.ensure_account_name(first_account_to_add))
self.set_working_account(first_account_to_add)
if not self.is_account_known(first_account_to_add):
self.known.add(first_account_to_add)
......@@ -208,6 +237,9 @@ class AccountManager:
if self.is_account_working(account):
raise AccountAlreadyExistsError(Account.ensure_account_name(account), "WorkingAccount")
if self.is_account_bad(account):
raise TryingToAddBadAccountError(Account.ensure_account_name(account))
if not self.is_account_known(account):
self.known.add(account)
......
aalpha
aappreciator
abits
acx
aex.com
alha
alhpa
allcoin.com
alpah
alph
alphha
alppha
alueup
apha
aplha
apppreciator
apprceiator
apprciator
apprecaitor
apprecator
apprecciator
appreciaator
appreciaor
appreciaotr
appreciater
appreciatoor
appreciatorr
appreciatr
appreciatro
appreciattor
appreciiator
apprecitaor
apprecitor
appreeciator
appreiator
appreicator
apprreciator
apreciator
aprpeciator
ausbitban
ausbltbank
avlueup
battrex
bbithumb.hot
bbittrex
bblocktrades
bboomerang
bbooster
bbuildawhale
bcex
bellyrub
berniesandlers
bernieslanders
berniestandards
bernlesanders
bettrex
bihtrex
bihtumb.hot
bihumb.hot
biithumb.hot
biitrex
biittrex
bildawhale
binan
binanc
binance
binance.com
binanced
binancee
binances
binanse
binnance
bit-z
bitex
bitfinex
bitfinex.com
bitfinix
bitflip
bithhumb.hot
bithmb.hot
bithmub.hot
bithub.hot
bithubm.hot
bithum.bhot
bithumb
bithumb-deposit
bithumb-exchange
bithumb-pro
bithumb.com
bithumb.hoot
bithumb.hott
bithumb.hto
bithumb.oht
bithumbb.hot
bithummb.hot
bithuumb.hot
bitifinex
bitkrx
bitnaru
bitre
bitreex
bitrex
bitrexx
bitrix
bitrrex
bitrtex
bitrx
bitsane
bitsane.com
bitstamp
bitstamp.net
bitt
bitteex
bitter
bitterex
bitterx
bittex
bitthai
bittr
bittrax
bittre
bittrec
bittrecs
bittrecx
bittred
bittreex
bittrek
bittres
bittresx
bittret
bittrev
bittrex-deposit
bittrex-pro
bittrex.com
bittrexc
bittrexchange
bittrexe
bittrexs
bittrext
bittrexx
bittrexxx
bittrez
bittriex
bittrix
bittrrex
bittrrx
bittrx
bittrxe
bitttec
bitttex
bitttrex
bittylicious
bittylicious.com
bituhmb.hot
biuldawhale
blcktrades
blcoktrades
bleutrade
bleutrade.com
bllocktrades
bloccktrades
block-trades
block-trades-com
blockktrades
blockrade
blockrades
blockrtades
blocktades
blocktardes
blocktraades
blocktrad
blocktraddes
blocktrade
blocktraded
blocktradee
blocktradees
blocktrader
blocktraders
blocktrades-com
blocktrades-info
blocktrades-us
blocktrades.info
blocktradess
blocktradesss
blocktradez
blocktrading
blocktrads
blocktradse
blocktraeds
blocktraes
blocktrdaes
blocktrdes
blocktredes
blocktrrades
blockttrades
bloctkrades
bloctrades
blokctrades
bloktrades
bloocktrades
bocktrades
bolcktrades
bomerang
bomoerang
booemrang
booerang
boomeang
boomearng
boomeerang
boomeraang
boomerag
boomeragn
boomerangg
boomeranng
boomernag
boomerrang
boommerang
boomreang
booomerang
boooster
booser
boosetr
boostar
booste
boosterr
boostr
boostre
boostter
bootser
bosoter
boster
btc-alpha
btc-alpha.com
btc38
btcalpha
btcmarkets
btcmarkets.net
bthumb.hot
btihumb.hot
btitrex
btrex
bttrex
bttrx
buidawhale
buidlawhale
buiildawhale
builadwhale
buildaawhale
buildahale
buildahwale
buildawahle
buildawale
buildawhaale
buildawhael
buildawhalee
buildawhalle
buildawhhale
buildawhile
buildawhlae
buildawwhale
builddawhale
buildwahale
buildwhale
builldawhale
buldawhale
bulidawhale
buobi-pro
buuildawhale
c-cex
c-cex.com
canadiancoconut
ccex
cexio
changellly
changelly.com
changely
cinpayments.net
cionpayments.net
coin-room
coinapyments.net
coinayments.net
coinbas
coinbase
coinbase.com
coinbased
coinegg
coinegg.com
coinpaments.net
coinpamyents.net
coinpayemnts.net
coinpayents.net
coinpaymens.net
coinpaymenst.net
coinpayment
coinpayment.snet
coinpayments
coinpayments.ent
coinpayments.nte
coinpaymetns.net
coinpaymnets.net
coinpaymnts.net
coinpia
coinpyaments.net
coinroom.com
coinsmarkets
coinsmarkets.com
coinspot
coinzest
coipayments.net
coipnayments.net
community-coin
conipayments.net
conpayments.net
coolcoin.com
curi
curied
curies
curing
d-tube
dcrypto8
ddeepcrypto8
decrypto8
deep8
deepccrypto8
deepcripto8
deepcrpto8
deepcrrypto8
deepcrypo8
deepcryppto8
deepcrypt08
deepcrypt8
deepcrypto-8
deepcrypto0
deepcrypto7
deepcrypto88
deepcrypto9
deepcryptoo8
deepcryptos
deepcryptos8
deepcryptto8
deepcryto8
deepcrytpo8
deepcryypto8
deepcypto8
deeppcrypto8
deeprypto8
depcrypto8
donkeypon
edepcrypto8
eepcrypto8
etherdelta
etherdelta.com
exrates
exx.com
feepcrypto8
freeewallet
freewalet.org
freewaller
frewallet
fyrstiken
gatecoin.com
gatehub
gatehub.net
gdax.com
gemini.com
ggopax-deposit
goapx-deposit
goax-deposit
good-kama
goopax-deposit
gopa-deposit
gopa-xdeposit
gopaax-deposit
gopax-ddeposit
gopax-deopsit
gopax-deosit
gopax-depoist
gopax-depoit
gopax-depoosit
gopax-deposiit
gopax-depositt
gopax-depossit
gopax-depost
gopax-deposti
gopax-depposit
gopax-depsit
gopax-depsoit
gopax-dpeosit
gopax-dposit
gopax-edposit
gopax-eposit
gopax-hot
gopaxd-eposit
gopaxx-deposit
goppax-deposit
gopx-deposit
gopxa-deposit
gpax-deposit
gpoax-deposit
gtg.witnesses
herising
hhuobi-pro
hitbtc-deposit
hitbtc-pro
hitbtc.com
hitbtcexchange
hterising
huobbi-pro
huobi-ppro
huobi-pr0
huobi-proo
huobi-prro
huobi.pro
huobii-pro
huobl-pro
huoobi-pro
huuobi-pro
ibthumb.hot
ibttrex
idex.market
imnnowbooster
inance
innowbooster
ithumb.hot
ittrex
ittrexx
kcx
kevinwon
kocostock
koinex
kraken
kraken.com
kucoi
kucoin
kucoinn
kucoins
lapha
lbocktrades
linkcoin
litebit
little-pepper
livecoin.com
livecoinnet
livingroomofsato
localtrade
localtrade.pro
locktrades
lpha
martsteem
mercatox.com
miinnowbooster
minnnowbooster
minnobooster
minnobwooster
minnooboster
minnoowbooster
minnowboooster
minnowbooser
minnowboosetr
minnowboosster
minnowboost
minnowbooste
minnowboosted
minnowboosteer
minnowboosterr
minnowboosters
minnowboostr
minnowboostre
minnowboostter
minnowbootser
minnowbosoter
minnowboster
minnowbuster
minnowhelp
minnowoboster
minnowooster
minnowpooster
minnows
minnowsuport
minnowsupports
minnowwbooster
minnwbooster
minnwobooster
minobooster
minonwbooster
minowbooster
minowboster
minowhelper
minowsupport
mminnowbooster
mmyupbit
mninowbooster
msartsteem
my-upbit
myupbbit
myupbiit
myupbitt
myupblt
myuppbit
myuupbit
myyupbit
neraex
neraex.com
nextgencrypted
nextgencryptos
obomerang
oboster
ocky1
oenledger-dex
oepnledger-dex
ogpax-deposit
oinpayments.net
okex.com
olonie
oloniex
oomerang
oopenledger-dex
ooster
opeenledger-dex
opeledger-dex
opelnedger-dex
openedger-dex
openeldger-dex
openldeger-dex
openldger-dex
openleddger-dex
openledegr-dex
openleder-dex
openledge-dex
openledge-rdex
openledgeer-dex
openledger-ddex
openledger-de
openledger-deex
openledger-dx
openledger-dxe
openledger-edx
openledger-ex
openledger-pro
openledgerd-ex
openledgerdex
openledgerr-dex
openledgr-dex
openledgre-dex
openleedger-dex
openlegder-dex
openleger-dex
openlledger-dex
opennledger-dex
opnledger-dex
oppenledger-dex
opstpromoter
orcky1
ostpromoter
p-funk
paloniex
papreciator
paypals
penledger-dex
pextokens
pfuck
piloniex
plolniex
ploniex
plooniex
poenledger-dex
pokoniex
polaniex
poleniex
poliniex
polionex
pollniex
polloniex
polloniexx
pollonix
polniex
polnoiex
polobiex
poloex
poloiex
poloinex
pololniex
polomiex
polon
poloneex
poloneiex
poloneix
polonex
poloni
poloniax
polonie
poloniec
poloniecs
polonied
poloniee
polonieex
poloniek
polonieks
polonies
poloniet
poloniets
poloniew
poloniex.com
poloniexcold
poloniexcom
poloniexe
poloniexs
poloniext
poloniexwalle
poloniexwallet
poloniexx
poloniexxx
poloniey
poloniez
poloniiex
poloniix
poloniks
poloniox
polonium
polonix
polonixe
polonixx
polonniex
polonoiex
polonox
polonx
polonyex
polooniex
poluniex
pomobot
pononiex
poolniex
pooloniex
pooniex
poooniex
poostpromoter
poponiex
pormobot
posptromoter
posstpromoter
postpormoter
postppromoter
postprmooter
postprmoter
postprommoter
postpromoer
postpromooter
postpromote
postpromoteer
postpromoterr
postpromotor
postpromotre
postpromotter
postpromter
postpromtoer
postproomoter
postproomter
postprooter
postprromoter
postromoter
posttpromoter
potpromoter
potspromoter
ppostpromoter
ppreciator
ppromobot
prmobot
prmoobot
promboot
prombot
prommobot
promobbot
promobo
promoboot
promobott
promobt
promobto
promoobot
promoobt
promoot
proobot
proombot
proomobot
prromobot
pse
psotpromoter
pstpromoter
ptakundamianshow
ptunk
puloniex
qryptos
qryptos.com
randomwhale
randowale
randowhal
randwhale
rcky1
rcoky1
rdex
rduex
rendowhale
roccky1
rock1y
rockky1
rockyy1
rocy1
rocyk1
rokcy1
romobot
roocky1
rpomobot
rrocky1
rrudex
ruddex
rudeex
rudexx
rudx
rudxe
ruedx
ruudex
samrtsteem
sartsteem
scoin
seem
seemit
seemit2
seepcrypto8
setem
setemit
setemit2
shapeshif
shapeshift
smaartsteem
smarrtsteem
smarstteem
smartseem
smartsetem
smartstee
smartsteeem
smartsteemm
smartsteme
smartstteem
smartteem
smatrsteem
smmartsteem
smratsteem
smrtsteem
ssmartsteem
ssteemit2
steampunks
steeemit2
steeimt
steeimt2
steeit
steeit2
steemi2
steemi2t
steemitpay
steemitt2
steemmit2
steempay
steempays
steemt2
steemti2
steemupbit
stemeit
stemeit2
stemit1
stemit2
stemit3
stemit4
stteem
stteemit2
sweetsj
sweetssj
sweetsss
sweetsssjs
sweetssssj
swetsssj
tdax
tdax.com
teamsteam
techno-comanche
technocommander
teemit2
tehrising
terising
thealien
thebiton
theerising
theirsing
theising
theriing
theriising
theriisng
therisig
therisign
therisiing
therisingg
therisinng
therisng
therisnig
therissing
therocktrading
therrising
thersiing
thersing
thherising
threising
thrising
tidex.com
topbtc
tseem
tseemit
tseemit2
ttherising
ttrex
ubildawhale
udex
uhobi-pro
uildawhale
umewhale
umpewhale
underug
uobi-pro
upbbit
upbi
upbit
upbit.com
upbits
upbitt
upblt
upemwhale
upewhale
upm
upmee
upmeewhale
upmehale
upmehwale
upmewahle
upmewhaale
upmewhae
upmewhael
upmewhalee
upmewhalle
upmewhhale
upmewhlae
upmewhle
upmewwhale
upmme
upmmewhale
upmwehale
upmwhale
uppme
uppmee
uppmewhale
uupmewhale
vaalueup
valeup
valeuup
vallueup
valueeup
valuep
valuepu
valueu
valueupp
valuuep
valuueup
valuup
vaueup
vauleup
viabtc
vittrex
vlaueup
vlueup
vvalueup
wallet.bitshares
whaleshare
www.aex.com
www.binance.com
www.bit-z.com
www.bitfinex.com
www.bithumb.com
www.bitstamp.net
www.bittrex.com
www.coinbase.com
www.coinegg.com
www.coolcoin.com
www.exx.com
www.gatecoin.com
www.gatehub.net
www.gdax.com
www.huobi.pro
www.kraken.com
www.livecoin.net
www.okex.com
www.poloniex.com
www.qryptos.com
www.xbtce.com
xbtce.com
ymupbit
yobit
yobit.net
youbit
yunbi
zenieix
......@@ -33,3 +33,10 @@ class AccountAlreadyExistsError(AccountsUpdateError):
super().__init__(f"Account {account_name} already exists in {place}.")
self.account_name = account_name
self.place = place
class TryingToAddBadAccountError(AccountsUpdateError):
"""Raised when trying to add a bad account to tracked accounts."""
def __init__(self, account_name: str) -> None:
super().__init__(f"Trying to add a bad account {account_name} to tracked accounts.")
......@@ -2,19 +2,14 @@ from __future__ import annotations
from typing import TYPE_CHECKING
from textual import on
from textual.binding import Binding
from textual.events import ScreenSuspend
from clive.__private.ui.get_css import get_relative_css_path
from clive.__private.ui.screens.base_screen import BaseScreen
from clive.__private.ui.screens.config.account_management.bad_accounts import BadAccounts
from clive.__private.ui.screens.config.account_management.known_accounts import KnownAccounts
from clive.__private.ui.screens.config.account_management.switch_working_account import SwitchWorkingAccount
from clive.__private.ui.screens.config.account_management.tracked_accounts import TrackedAccounts
from clive.__private.ui.widgets.clive_basic import CliveTabbedContent
from clive.__private.ui.widgets.switch_working_account_container import (
SwitchWorkingAccountContainer,
)
if TYPE_CHECKING:
from textual.app import ComposeResult
......@@ -30,13 +25,8 @@ class AccountManagement(BaseScreen):
def create_main_panel(self) -> ComposeResult:
with CliveTabbedContent():
yield TrackedAccounts()
yield SwitchWorkingAccount()
yield KnownAccounts()
yield BadAccounts()
def on_mount(self) -> None:
self.app.trigger_profile_watchers()
@on(CliveTabbedContent.TabActivated)
@on(ScreenSuspend)
def _confirm_selected_working_account(self) -> None:
self.screen.query_one(SwitchWorkingAccountContainer).confirm_selected_working_account()
from __future__ import annotations
import re
from typing import TYPE_CHECKING, Final, Sequence
from textual import on
from textual.binding import Binding
from textual.containers import Center, Horizontal
from textual.widgets import Static, TabPane
from clive.__private.core.accounts.account_manager import AccountManager
from clive.__private.ui.widgets.buttons.page_switch_buttons import PageDownButton, PageUpButton
from clive.__private.ui.widgets.buttons.search_operation_buttons import ClearButton, SearchButton
from clive.__private.ui.widgets.clive_basic import (
CliveCheckerboardTable,
CliveCheckerBoardTableCell,
CliveCheckerboardTableRow,
)
from clive.__private.ui.widgets.inputs.account_name_pattern_input import AccountNamePatternInput
from clive.__private.ui.widgets.no_content_available import NoContentAvailable
from clive.__private.ui.widgets.scrolling import ScrollablePart
from clive.__private.ui.widgets.section_title import SectionTitle
if TYPE_CHECKING:
from textual.app import ComposeResult
class BadAccountsTable(CliveCheckerboardTable):
"""Table for a bad accounts."""
DEFAULT_CSS = """
BadAccountsTable {
width: 1fr;
#bad-accounts-header {
height: 1;
background: $error;
text-style: bold;
}
#bad-accounts-title {
height: 1;
OneLineButton {
width: 1fr;
min-width: 1;
}
SectionTitle {
width: 3fr;
}
}
Static {
text-align: center;
width: 1fr;
}
}
"""
BINDINGS = [
Binding("pagedown", "next_page", "PgDn"),
Binding("pageup", "previous_page", "PgUp"),
]
MAX_ACCOUNTS_ON_PAGE: Final[int] = 21
FIRST_PAGE_INDEX: Final[int] = 0
def __init__(self) -> None:
self._page_up_button = PageUpButton()
self._page_down_button = PageDownButton()
self._page_up_button.visible = False
super().__init__(
header=Static("Account name", id="bad-accounts-header"),
title=Horizontal(
self._page_up_button, SectionTitle("Bad accounts"), self._page_down_button, id="bad-accounts-title"
),
)
self._current_page_index = self.FIRST_PAGE_INDEX
self._bad_account_names = AccountManager.BAD_ACCOUNT_NAMES
"""Stored in the attribute as it changes in search mode."""
self._last_page_index = self._get_last_page_index()
"""It is not stored as a final value because it can be dynamically changed with the `_accounts_list`."""
def create_static_rows(self) -> Sequence[CliveCheckerboardTableRow]:
start_index = self._current_page_index * self.MAX_ACCOUNTS_ON_PAGE
end_index = start_index + self.MAX_ACCOUNTS_ON_PAGE
return [
CliveCheckerboardTableRow(CliveCheckerBoardTableCell(account))
for account in self._bad_account_names[start_index:end_index]
]
@on(PageUpButton.Pressed)
async def action_previous_page(self) -> None:
if self._current_page_index == self.FIRST_PAGE_INDEX:
return
self._current_page_index -= 1
self._page_down_button.visible = True
if self._current_page_index <= self.FIRST_PAGE_INDEX:
self._page_up_button.visible = False
await self._rebuild_rows()
@on(PageDownButton.Pressed)
async def action_next_page(self) -> None:
if self._current_page_index == self._last_page_index:
return
self._current_page_index += 1
self._page_up_button.visible = True
if self._current_page_index >= self._last_page_index:
self._page_down_button.visible = False
await self._rebuild_rows()
async def set_search_mode(self, pattern: str) -> None:
pattern = rf"^{pattern}"
self._bad_account_names = [
account for account in AccountManager.BAD_ACCOUNT_NAMES if re.match(pattern, account)
]
await self._reset_table()
async def set_full_list_mode(self) -> None:
self._bad_account_names = AccountManager.BAD_ACCOUNT_NAMES
await self._reset_table()
def _get_last_page_index(self) -> int:
return len(self._bad_account_names) // self.MAX_ACCOUNTS_ON_PAGE
async def _reset_table(self) -> None:
self._current_page_index = self.FIRST_PAGE_INDEX
self._last_page_index = self._get_last_page_index()
self._page_up_button.visible = False
self._page_down_button.visible = True
await self._rebuild_rows()
async def _rebuild_rows(self) -> None:
with self.app.batch_update():
await self._remove_rows()
await self._mount_new_rows()
async def _remove_rows(self) -> None:
await self.query(CliveCheckerboardTableRow).remove()
await self.query(NoContentAvailable).remove()
async def _mount_new_rows(self) -> None:
if len(self._bad_account_names) == 0:
await self.mount(NoContentAvailable("No bad accounts found with this pattern"))
return
new_rows = self.create_static_rows()
self._set_evenness_styles(new_rows)
await self.mount_all(new_rows)
class BadAccounts(TabPane):
"""Currently only used to display the list of bad accounts (cannot be modified)."""
DEFAULT_CSS = """
BadAccounts {
Center {
height: auto;
}
#scrollable-center {
max-height: 24;
height: 1fr;
}
ScrollablePart {
width: 40%;
}
#search-controls {
width: 70%;
height: 3;
CliveButton {
margin: 0 1;
}
}
}
"""
TITLE: Final[str] = "Bad accounts"
def __init__(self) -> None:
super().__init__(title=self.TITLE)
def compose(self) -> ComposeResult:
with Center(id="scrollable-center"), ScrollablePart():
yield BadAccountsTable()
with Center(), Horizontal(id="search-controls"):
yield AccountNamePatternInput(required=False, always_show_title=True)
yield SearchButton()
yield ClearButton()
@on(SearchButton.Pressed)
async def search_pattern_in_list(self) -> None:
pattern = self.query_one(AccountNamePatternInput).value_or_none()
if pattern is None:
return
await self.query_one(BadAccountsTable).set_search_mode(pattern)
@on(ClearButton.Pressed)
async def clear_from_searched(self) -> None:
await self.query_one(BadAccountsTable).set_full_list_mode()
self.query_one(AccountNamePatternInput).input.clear()
from __future__ import annotations
from typing import TYPE_CHECKING, Final
from textual.widgets import TabPane
from clive.__private.ui.clive_widget import CliveWidget
from clive.__private.ui.get_css import get_css_from_relative_path
from clive.__private.ui.widgets.scrolling import ScrollablePart
from clive.__private.ui.widgets.switch_working_account_container import (
SwitchWorkingAccountContainer,
)
if TYPE_CHECKING:
from textual.app import ComposeResult
class SwitchWorkingAccount(TabPane, CliveWidget):
"""TabPane used to modify the working account."""
DEFAULT_CSS = get_css_from_relative_path(__file__)
TITLE: Final[str] = "Switch working account"
def __init__(self) -> None:
super().__init__(self.TITLE)
def compose(self) -> ComposeResult:
with ScrollablePart():
yield SwitchWorkingAccountContainer()
SwitchWorkingAccount {
ScrollablePart {
align: center top;
SwitchWorkingAccountContainer {
height: auto;
}
}
}
......@@ -42,17 +42,17 @@ class GovernanceActionRow(Horizontal, AbstractClassMessagePump):
pending: Indicates if the operation with such identifier is already in the cart.
"""
super().__init__(id=self.create_action_row_id(identifier))
self.__identifier = identifier
self.__vote = vote
self.__pending = pending
self._identifier = identifier
self._vote = vote
self._pending = pending
def compose(self) -> ComposeResult:
if self.__pending:
if self._pending:
yield Label("Pending", classes="action-pending action-label")
yield Label(str(self.action_identifier), classes="action-identifier")
return
if self.__vote:
if self._vote:
yield Label("Vote", classes="action-vote action-label")
else:
yield Label("Unvote", classes="action-unvote action-label")
......@@ -60,7 +60,7 @@ class GovernanceActionRow(Horizontal, AbstractClassMessagePump):
@property
def action_identifier(self) -> str:
return self.__identifier
return self._identifier
@staticmethod
@abstractmethod
......@@ -76,9 +76,9 @@ class GovernanceActions(ScrollablePartFocusable, Generic[OperationActionT]):
def __init__(self) -> None:
super().__init__()
self.__actions_to_perform: dict[str, bool] = {}
self._actions_to_perform: dict[str, bool] = {}
"""A dict with action identifier as key and action to pe performed as value"""
self.__actions_votes = 0
self._actions_votes = 0
def compose(self) -> ComposeResult:
yield SectionTitle("Actions to be performed")
......@@ -99,9 +99,9 @@ class GovernanceActions(ScrollablePartFocusable, Generic[OperationActionT]):
await self.mount(self.create_action_row(identifier, vote=vote, pending=pending))
if vote:
self.__actions_votes += 1
self._actions_votes += 1
else:
self.__actions_votes -= 1
self._actions_votes -= 1
if not pending:
self.add_to_actions(identifier, vote=vote)
......@@ -115,25 +115,25 @@ class GovernanceActions(ScrollablePartFocusable, Generic[OperationActionT]):
return
if vote:
self.__actions_votes -= 1
self._actions_votes -= 1
else:
self.__actions_votes += 1
self._actions_votes += 1
self.delete_from_actions(identifier)
def add_to_actions(self, identifier: str, *, vote: bool) -> None:
self.__actions_to_perform[identifier] = vote
self._actions_to_perform[identifier] = vote
def delete_from_actions(self, identifier: str) -> None:
self.__actions_to_perform.pop(identifier)
self._actions_to_perform.pop(identifier)
@property
def actions_votes(self) -> int:
return self.__actions_votes
return self._actions_votes
@property
def actions_to_perform(self) -> dict[str, bool]:
return self.__actions_to_perform
return self._actions_to_perform
def hook_on_row_added(self) -> None:
"""Create any action when an action row is added to the action table."""
......
......@@ -9,9 +9,8 @@ from textual import on
from textual.binding import Binding
from textual.containers import Grid, Vertical
from textual.css.query import NoMatches
from textual.events import Click
from textual.message import Message
from textual.widgets import Label, Static
from textual.widgets import Label
from clive.__private.abstract_class import AbstractClassMessagePump
from clive.__private.core.commands.data_retrieval.proposals_data import Proposal as ProposalData
......@@ -20,6 +19,7 @@ from clive.__private.ui.clive_widget import CliveWidget
from clive.__private.ui.data_providers.abc.data_provider import DataProvider
from clive.__private.ui.get_css import get_css_from_relative_path
from clive.__private.ui.screens.operations.governance_operations.governance_checkbox import GovernanceCheckbox
from clive.__private.ui.widgets.buttons.page_switch_buttons import PageDownButton, PageUpButton
if TYPE_CHECKING:
from textual.app import ComposeResult
......@@ -28,46 +28,22 @@ GovernanceDataT = TypeVar("GovernanceDataT", ProposalData, WitnessData)
GovernanceDataProviderT = TypeVar("GovernanceDataProviderT", bound=DataProvider[Any])
class ArrowUpWidget(Static):
class Clicked(Message):
"""Message send when WitnessCheckbox is clicked."""
def __init__(self) -> None:
super().__init__(renderable="↑ PgUp")
@on(Click)
async def clicked(self) -> None:
self.post_message(self.Clicked())
class ArrowDownWidget(Static):
class Clicked(Message):
"""Message send when WitnessCheckbox is clicked."""
def __init__(self) -> None:
super().__init__(renderable="↓ PgDn")
@on(Click)
async def clicked(self) -> None:
self.post_message(self.Clicked())
class GovernanceListHeader(Grid, CliveWidget, AbstractClassMessagePump):
"""Widget representing the header of a list that allows page switching using PgUp and PgDn."""
def __init__(self) -> None:
super().__init__()
self.arrow_up = ArrowUpWidget()
self.arrow_down = ArrowDownWidget()
self.button_up = PageUpButton()
self.button_down = PageDownButton()
self.arrow_up.visible = False
self.button_up.visible = False
def compose(self) -> ComposeResult:
yield from self.create_additional_headlines()
yield self.arrow_up
yield self.button_up
yield from self.create_custom_columns()
yield self.arrow_down
yield self.button_down
@abstractmethod
def create_custom_columns(self) -> ComposeResult:
......@@ -153,15 +129,15 @@ class GovernanceTableRow(Grid, CliveWidget, Generic[GovernanceDataT], AbstractCl
even: Whether the row is even or odd.
"""
super().__init__()
self.__row_data: GovernanceDataT = row_data
self.__evenness = "even" if even else "odd"
self._row_data: GovernanceDataT = row_data
self._evenness = "even" if even else "odd"
def on_mount(self) -> None:
self.watch(self.governance_checkbox, "disabled", callback=self.dimm_on_disabled_checkbox)
def compose(self) -> ComposeResult:
self.governance_checkbox = GovernanceCheckbox(
is_voted=self.__row_data.voted,
is_voted=self._row_data.voted,
initial_state=self.is_operation_in_cart or self.is_already_in_actions_container,
disabled=bool(self.profile.accounts.working.data.proxy) or self.is_operation_in_cart,
)
......@@ -193,11 +169,11 @@ class GovernanceTableRow(Grid, CliveWidget, Generic[GovernanceDataT], AbstractCl
@property
def row_data(self) -> GovernanceDataT:
return self.__row_data
return self._row_data
@property
def evenness(self) -> str:
return self.__evenness
return self._evenness
@abstractmethod
def create_row_content(self) -> ComposeResult:
......@@ -241,10 +217,10 @@ class GovernanceTable(
def __init__(self) -> None:
super().__init__()
self.__element_index = 0
self._element_index = 0
self.__header = self.create_header()
self.__is_loading = True
self._header = self.create_header()
self._is_loading = True
def compose(self) -> ComposeResult:
yield self.header
......@@ -271,59 +247,59 @@ class GovernanceTable(
self.set_loaded()
async def loading_set(self) -> None:
self.__is_loading = True
self._is_loading = True
with contextlib.suppress(NoMatches):
selected_list = self.query_one(GovernanceListWidget) # type: ignore[type-abstract]
await selected_list.query("*").remove()
await selected_list.mount(Label("Loading..."))
def set_loaded(self) -> None:
self.__is_loading = False
self._is_loading = False
@on(ArrowDownWidget.Clicked)
@on(PageDownButton.Pressed)
async def action_next_page(self) -> None:
if self.__is_loading:
if self._is_loading:
return
# It is used to prevent the user from switching to an empty page by key binding
if self.data_length - self.MAX_ELEMENTS_ON_PAGE <= self.__element_index:
if self.data_length - self.MAX_ELEMENTS_ON_PAGE <= self._element_index:
self.notify("No elements on the next page", severity="warning")
return
self.__element_index += self.MAX_ELEMENTS_ON_PAGE
self._element_index += self.MAX_ELEMENTS_ON_PAGE
self.__header.arrow_up.visible = True
self._header.button_up.visible = True
if self.data_length - self.MAX_ELEMENTS_ON_PAGE <= self.__element_index:
self.__header.arrow_down.visible = False
if self.data_length - self.MAX_ELEMENTS_ON_PAGE <= self._element_index:
self._header.button_down.visible = False
await self.sync_list(focus_first_element=True)
@on(ArrowUpWidget.Clicked)
@on(PageUpButton.Pressed)
async def action_previous_page(self) -> None:
if self.__is_loading:
if self._is_loading:
return
# It is used to prevent the user going to a page with a negative index by key binding
if self.__element_index <= 0:
if self._element_index <= 0:
self.notify("No elements on the previous page", severity="warning")
return
self.__header.arrow_down.visible = True
self._header.button_down.visible = True
self.__element_index -= self.MAX_ELEMENTS_ON_PAGE
self._element_index -= self.MAX_ELEMENTS_ON_PAGE
if self.__element_index <= 0:
self.__header.arrow_up.visible = False
if self._element_index <= 0:
self._header.button_up.visible = False
await self.sync_list(focus_first_element=True)
async def reset_page(self) -> None:
self.__element_index = 0
self.__header.arrow_up.visible = False
self.__header.arrow_down.visible = True
self._element_index = 0
self._header.button_up.visible = False
self._header.button_down.visible = True
if not self.__is_loading:
if not self._is_loading:
await self.sync_list()
@property
......@@ -340,7 +316,7 @@ class GovernanceTable(
if not self.is_data_available:
return None
return self.data[self.__element_index : self.__element_index + self.MAX_ELEMENTS_ON_PAGE]
return self.data[self._element_index : self._element_index + self.MAX_ELEMENTS_ON_PAGE]
@property
def data_length(self) -> int:
......@@ -351,11 +327,11 @@ class GovernanceTable(
@property
def element_index(self) -> int:
return self.__element_index
return self._element_index
@property
def header(self) -> GovernanceListHeader:
return self.__header
return self._header
@property
def is_proxy_set(self) -> bool:
......
......@@ -32,7 +32,7 @@ from clive.__private.ui.screens.operations.governance_operations.common_governan
GovernanceTable,
GovernanceTableRow,
)
from clive.__private.ui.widgets.buttons.clive_button import CliveButton
from clive.__private.ui.widgets.buttons.search_operation_buttons import ClearButton, SearchButton
from clive.__private.ui.widgets.inputs.account_name_pattern_input import AccountNamePatternInput
from clive.__private.ui.widgets.inputs.clive_validated_input import CliveValidatedInput
from clive.__private.ui.widgets.inputs.integer_input import IntegerInput
......@@ -177,10 +177,10 @@ class WitnessManualSearch(Grid):
def compose(self) -> ComposeResult:
yield self._witness_input
yield self._limit_input
yield CliveButton("Search", variant="success", id_="witness-search-button")
yield CliveButton("Clear", variant="error", id_="clear-custom-witnesses-button")
yield SearchButton()
yield ClearButton()
@on(CliveButton.Pressed, "#witness-search-button")
@on(SearchButton.Pressed)
def search_witnesses(self) -> None:
if not CliveValidatedInput.validate_many(self._witness_input, self._limit_input):
return
......@@ -191,7 +191,7 @@ class WitnessManualSearch(Grid):
self.post_message(self.Search(pattern, limit))
@on(CliveButton.Pressed, "#clear-custom-witnesses-button")
@on(ClearButton.Pressed)
def clear_searched_witnesses(self) -> None:
self._witness_input.input.clear()
self._limit_input.input.value = str(self.LIMIT_MAXIMUM)
......
......@@ -7,6 +7,7 @@ from textual.containers import Horizontal
from clive.__private.ui.clive_widget import CliveWidget
from clive.__private.ui.widgets.inputs.account_name_input import AccountNameInput
from clive.__private.ui.widgets.section import Section
from clive.__private.validators.bad_account_validator import BadAccountValidator
from clive.__private.validators.set_known_account_validator import SetKnownAccountValidator
from clive.__private.validators.set_tracked_account_validator import SetTrackedAccountValidator
......@@ -33,7 +34,10 @@ class AddAccountContainer(Horizontal, CliveWidget):
self._account_input = AccountNameInput(
required=False,
validators=(
SetTrackedAccountValidator(self.profile)
[
SetTrackedAccountValidator(self.profile),
BadAccountValidator(self.profile.accounts, pass_if_known=False),
]
if accounts_type == "tracked_accounts"
else SetKnownAccountValidator(self.profile)
),
......
......@@ -15,7 +15,13 @@ if TYPE_CHECKING:
from textual.reactive import reactive
CliveButtonVariant = Literal[
"loading-variant", "success-on-transparent", "error-on-transparent", "grey-darken", "grey-lighten", ButtonVariant
"loading-variant",
"success-on-transparent",
"error-on-transparent",
"transparent",
"grey-darken",
"grey-lighten",
ButtonVariant,
]
......@@ -61,6 +67,14 @@ class CliveButton(Button, CliveWidget):
}
}
&.-transparent {
background: 0%;
&:hover {
background: black 20%;
}
}
&.-grey-lighten {
background: $panel-lighten-3;
......
from __future__ import annotations
from clive.__private.ui.widgets.buttons.one_line_button import OneLineButton
class PageUpButton(OneLineButton):
class Pressed(OneLineButton.Pressed):
"""Message send when PageUpButton is pressed."""
def __init__(self) -> None:
super().__init__("↑ PgUp", variant="transparent", id_="page-up-button")
class PageDownButton(OneLineButton):
class Pressed(OneLineButton.Pressed):
"""Message send when PageDownButton is pressed."""
def __init__(self) -> None:
super().__init__("↓ PgDn", variant="transparent", id_="page-down-button")
from __future__ import annotations
from clive.__private.ui.widgets.buttons.clive_button import CliveButton
class SearchButton(CliveButton):
class Pressed(CliveButton.Pressed):
"""Message send when SearchButton is pressed."""
def __init__(self, label: str = "Search", id_: str = "search-button") -> None:
super().__init__(label=label, id_=id_, variant="success")
class ClearButton(CliveButton):
class Pressed(CliveButton.Pressed):
"""Message send when ClearButton is pressed."""
def __init__(self, label: str = "Clear", id_: str = "clear-button") -> None:
super().__init__(label=label, id_=id_, variant="error")
......@@ -87,7 +87,7 @@ class WorkingAccountButton(DynamicOneLineButtonUnfocusable):
@on(OneLineButton.Pressed)
def switch_working_account(self) -> None:
if not self._is_current_screen_dashboard:
if not self._is_working_account_switch_allowed:
return
if not self.profile.accounts.has_tracked_accounts:
......@@ -97,7 +97,7 @@ class WorkingAccountButton(DynamicOneLineButtonUnfocusable):
@on(CliveScreen.Resumed)
def determine_tooltip(self) -> None:
if not self._is_current_screen_dashboard:
if not self._is_working_account_switch_allowed:
self.tooltip = "Go to the dashboard to modify working account"
return
......@@ -107,12 +107,22 @@ class WorkingAccountButton(DynamicOneLineButtonUnfocusable):
self.tooltip = "Switch working account"
@property
def _is_working_account_switch_allowed(self) -> bool:
return self._is_current_screen_dashboard or self._is_current_screen_account_management
@property
def _is_current_screen_dashboard(self) -> bool:
from clive.__private.ui.screens.dashboard import DashboardBase
return isinstance(self.app.screen, DashboardBase)
@property
def _is_current_screen_account_management(self) -> bool:
from clive.__private.ui.screens.config.account_management import AccountManagement
return isinstance(self.app.screen, AccountManagement)
def _push_switch_working_account_screen(self) -> None:
from clive.__private.ui.dialogs import SwitchWorkingAccountDialog
......
......@@ -6,10 +6,11 @@ from textual import on
from textual.containers import Vertical
from textual.events import Mount
from clive.__private.core.accounts.accounts import KnownAccount as KnownAccountModel
from clive.__private.core.accounts.accounts import Account
from clive.__private.core.constants.tui.placeholders import ACCOUNT_NAME_PLACEHOLDER
from clive.__private.ui.widgets.inputs.text_input import TextInput
from clive.__private.validators.account_name_validator import AccountNameValidator
from clive.__private.validators.bad_account_validator import BadAccountValidator
if TYPE_CHECKING:
from collections.abc import Iterable
......@@ -24,6 +25,7 @@ class AccountNameInput(TextInput):
_KNOWN_ACCOUNT_CLASS: Final[str] = "-known-account"
_UNKNOWN_ACCOUNT_CLASS: Final[str] = "-unknown-account"
_BAD_ACCOUNT_CLASS: Final[str] = "-bad-account"
DEFAULT_CSS = """
AccountNameInput {
......@@ -44,6 +46,11 @@ class AccountNameInput(TextInput):
border-subtitle-background: $warning-lighten-1;
border-subtitle-color: black;
}
&.-bad-account {
border-subtitle-background: $error;
}
}
}
}
......@@ -60,6 +67,7 @@ class AccountNameInput(TextInput):
show_invalid_reasons: bool = True,
required: bool = True,
show_known_account: bool = True,
show_bad_account: bool = True,
validators: Validator | Iterable[Validator] | None = None,
validate_on: Iterable[InputValidationOn] | None = None,
valid_empty: bool = False,
......@@ -83,7 +91,7 @@ class AccountNameInput(TextInput):
include_title_in_placeholder_when_blurred=include_title_in_placeholder_when_blurred,
show_invalid_reasons=show_invalid_reasons,
required=required,
validators=validators or [AccountNameValidator()],
validators=validators or [AccountNameValidator(), BadAccountValidator(self.profile.accounts)],
validate_on=validate_on,
valid_empty=valid_empty,
id=id,
......@@ -91,6 +99,7 @@ class AccountNameInput(TextInput):
disabled=disabled,
)
self._show_known_account = show_known_account
self._show_bad_account = show_bad_account
def compose(self) -> ComposeResult:
with Vertical():
......@@ -99,7 +108,7 @@ class AccountNameInput(TextInput):
@on(Mount)
def _watch_input_value_change(self) -> None:
if not self._show_known_account:
if not self._show_known_account and not self._show_bad_account:
return
# >>> start workaround for Textual calling validate on input when self.watch is used. Setting validate_on to
......@@ -118,23 +127,35 @@ class AccountNameInput(TextInput):
if not self.profile.accounts.is_account_known(account_name):
self.profile.accounts.known.add(account_name)
def _change_input_style(self, css_class: str, border_subtitle: str) -> None:
self.input.remove_class(self._UNKNOWN_ACCOUNT_CLASS, self._KNOWN_ACCOUNT_CLASS, self._BAD_ACCOUNT_CLASS)
self.input.border_subtitle = border_subtitle
self.input.add_class(css_class)
self.input.refresh() # sometimes it's not refreshed automatically without this..
def _update_account_status(self) -> None:
def handle_invalid_account_name() -> None:
self.input.border_subtitle = None
self.input.refresh()
self.input.refresh() # sometimes it's not refreshed automatically without this..
def handle_valid_account_name() -> None:
known_account_text = "known account"
unknown_account_text = "unknown account"
if self.profile.accounts.is_account_bad(self.value_raw) and self._show_bad_account:
self._change_input_style(self._BAD_ACCOUNT_CLASS, "BAD ACCOUNT!")
return
is_account_known = self.profile.accounts.is_account_known(self.input.value)
if not self._show_known_account:
self.input.border_subtitle = ""
self.input.refresh() # sometimes it's not refreshed automatically without this..
return
self.input.border_subtitle = known_account_text if is_account_known else unknown_account_text
self.input.add_class(self._KNOWN_ACCOUNT_CLASS if is_account_known else self._UNKNOWN_ACCOUNT_CLASS)
self.input.remove_class(self._UNKNOWN_ACCOUNT_CLASS if is_account_known else self._KNOWN_ACCOUNT_CLASS)
self.input.refresh() # sometimes it's not refreshed automatically without this..
if self.profile.accounts.is_account_known(self.value_raw):
self._change_input_style(self._KNOWN_ACCOUNT_CLASS, "known account")
return
self._change_input_style(self._UNKNOWN_ACCOUNT_CLASS, "unknown account")
if not KnownAccountModel.is_valid(self.input.value):
if not Account.is_valid(self.value_raw):
handle_invalid_account_name()
return
......
from __future__ import annotations
from typing import TYPE_CHECKING, Final
from textual.validation import Validator
if TYPE_CHECKING:
from textual.validation import ValidationResult
from clive.__private.core.accounts.account_manager import AccountManager
class BadAccountValidator(Validator):
BAD_ACCOUNT_IN_INPUT_FAILURE_DESCRIPTION: Final[str] = "This account is considered as bad!"
def __init__(self, account_manager: AccountManager, *, known_overrides_bad: bool = True) -> None:
super().__init__()
self._known_overrides_bad = known_overrides_bad
self._account_manager = account_manager
def validate(self, value: str) -> ValidationResult:
account_manager = self._account_manager
if not account_manager.is_account_bad(value) or self._should_pass_when_known(value, account_manager):
return self.success()
return self.failure(self.BAD_ACCOUNT_IN_INPUT_FAILURE_DESCRIPTION, value)
def _should_pass_when_known(self, value: str, account_manager: AccountManager) -> bool:
return account_manager.is_account_known(value) and self._known_overrides_bad