Skip to content

Commit

Permalink
Column manipulation operations.
Browse files Browse the repository at this point in the history
  • Loading branch information
joce committed Nov 13, 2023
1 parent 6531212 commit 42fa720
Show file tree
Hide file tree
Showing 6 changed files with 437 additions and 110 deletions.
9 changes: 4 additions & 5 deletions appui/_quote_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,9 @@ def __del__(self) -> None:
def on_mount(self) -> None:
"""The event handler called when the widget is added to the app."""

# TODO: Consider having the first column be the symbols, always, and fixed.
super().on_mount()
quote_column: QuoteColumn
for quote_column in self._state.columns:
for quote_column in self._state.quotes_columns:
styled_column: Text = self._get_styled_column_title(quote_column)
key = self.add_column(
styled_column, width=quote_column.width, key=quote_column.key
Expand Down Expand Up @@ -74,11 +73,11 @@ def _update_table(self) -> None:

# Set the column titles, including the sort arrow if needed
quote_column: QuoteColumn
for quote_column in self._state.columns:
for quote_column in self._state.quotes_columns:
styled_column: Text = self._get_styled_column_title(quote_column)
self.columns[self._column_key_map[quote_column.key]].label = styled_column

quotes: list[QuoteRow] = self._state.get_quotes_rows()
quotes: list[QuoteRow] = self._state.quotes_rows
i: int = 0
quote: QuoteRow
for i, quote in enumerate(quotes):
Expand All @@ -90,7 +89,7 @@ def _update_table(self) -> None:
for j, cell in enumerate(quote.values):
self.update_cell(
quote_key,
self._state.columns[j].key,
self._state.quotes_columns[j].key,
self._get_styled_cell(cell),
)
else:
Expand Down
220 changes: 160 additions & 60 deletions appui/quote_table_state.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""The state of the quote table."""

import logging
from re import S
from threading import Lock, Thread
from time import monotonic, sleep
from typing import Any, Callable, Optional
Expand Down Expand Up @@ -47,19 +48,18 @@ class QuoteTableState:
def __init__(self, yfin: YFinance) -> None:
self._yfin: YFinance = yfin

# Ticker is *always* the first column
self._columns_keys: list[str] = [
QuoteTableState._TICKER_COLUMN_KEY
] + QuoteTableState._DEFAULT_COLUMN_KEYS[:]

self._quotes_symbols: list[str] = QuoteTableState._DEFAULT_QUOTES[:]

self._sort_column_key: str = QuoteTableState._TICKER_COLUMN_KEY
self._sort_direction: SortDirection = QuoteTableState._DEFAULT_SORT_DIRECTION
self._query_frequency: int = QuoteTableState._DEFAULT_QUERY_FREQUENCY

# Ticker is *always* the first column
columns_keys: list[str] = [
QuoteTableState._TICKER_COLUMN_KEY
] + QuoteTableState._DEFAULT_COLUMN_KEYS[:]
self._columns: list[QuoteColumn] = [
ALL_QUOTE_COLUMNS[column] for column in self._columns_keys
ALL_QUOTE_COLUMNS[column] for column in columns_keys
]

self._sort_key_func: Callable[[YQuote], Any] = ALL_QUOTE_COLUMNS[
Expand All @@ -69,7 +69,7 @@ def __init__(self, yfin: YFinance) -> None:
self._cursor_symbol: str = self._quotes_symbols[0]

self._query_thread_running: bool = False
self._query_thread: Thread = Thread(target=self._query_quotes)
self._query_thread: Thread = Thread(target=self._retrieve_quotes)
self._last_query_time = monotonic()
self._version: int = 0

Expand All @@ -81,12 +81,6 @@ def __del__(self) -> None:
if self.query_thread_running:
self.query_thread_running = False

@property
def columns(self) -> list[QuoteColumn]:
"""The columns of the quote table."""

return self._columns

@property
def version(self) -> int:
"""The version of the quote data."""
Expand Down Expand Up @@ -185,9 +179,16 @@ def current_row(self, value: int) -> None:
# Setting the current row does not change the version. It's just mirroring the
# cursor position from the UI.

def get_quotes_rows(self) -> list[QuoteRow]:
@property
def quotes_columns(self) -> list[QuoteColumn]:
"""The columns of the quote table."""

return self._columns

@property
def quotes_rows(self) -> list[QuoteRow]:
"""
Get the quotes to display in the quote table. Each quote is comprised of the
The quotes to display in the quote table. Each quote is comprised of the
elements required for each column.
Returns:
Expand All @@ -198,62 +199,174 @@ def get_quotes_rows(self) -> list[QuoteRow]:
quote_info: list[QuoteRow] = [
QuoteRow(
q.symbol,
# TODO... Could it be possible to have this array be fixed and
# saved whenever we change the columns?
[
QuoteCell(
ALL_QUOTE_COLUMNS[column].format_func(q),
ALL_QUOTE_COLUMNS[column].sign_indicator_func(q),
ALL_QUOTE_COLUMNS[column].justification,
)
for column in self._columns_keys
for column in self.column_keys
],
)
for q in self._quotes
]

return quote_info

def _query_quotes(self) -> None:
@property
def column_keys(self) -> list[str]:
"""The keys of the columns of the quote table."""

return [c.key for c in self._columns]

def append_column(self, column_key: str) -> None:
"""
Append a new column to the quote table.
Attempting to add a column that is already present or that doesn't exist will
have no effect.
The column is added at the end of the list of existing columns.
Args:
column_key (str): The identifier of the column to add.
The identifier of the column is expected to match the ones found in the
ALL_QUOTE_COLUMNS definition.
"""

if not self._can_add_column(column_key):
return

self._columns.append(ALL_QUOTE_COLUMNS[column_key])
self._version += 1

def insert_column(self, index: int, column_key: str) -> None:
"""
Insert a new column to the quote table, at the given index.
Attempting to insert a column that is already present or that doesn't exist will
have no effect.
Args:
index (int): The index at which to insert the column.
It's important to note that the index is 1-based, as index 0 is reserved
for the ticker column. Attempting to insert at index 0 or at a negative
index smaller than -(len(self._columns)-1) will have no effect.
column_key (str): The identifier of the column to insert.
The identifier of the column is expected to match the ones found in the
ALL_QUOTE_COLUMNS definition.
"""

if not self._can_add_column(column_key):
return

# Do not insert at index 0 or below
if (index == 0) or (index < -(len(self._columns) - 1)):
return

self._columns.insert(index, ALL_QUOTE_COLUMNS[column_key])
self._version += 1

def remove_column(self, column_key: str) -> None:
"""
Remove a column from the quote table.
Attempting to remove a column that is not present in the table will have no
effect.
Args:
column_key (str): The identifier of the column to remove.
The identifier of the column is expected to match the ones found in the
ALL_QUOTE_COLUMNS definition.
"""

try:
if column_key == QuoteTableState._TICKER_COLUMN_KEY:
raise ValueError("Cannot remove ticker column")
self._columns.remove(ALL_QUOTE_COLUMNS[column_key])
if self._sort_column_key == column_key:
self._sort_column_key = QuoteTableState._TICKER_COLUMN_KEY

self._version += 1
except KeyError as exc:
raise ValueError(
f"Column key {column_key} does not exist in the quote table",
) from exc

def _can_add_column(self, column_key: str) -> bool:
"""
Check if the column can be added to the quote table
Args:
column_key (str): The identifier of the column to add.
Returns:
bool: Whether the column can be added to the quote table
"""

if column_key not in ALL_QUOTE_COLUMNS:
logging.warning(
"Invalid column key '%s' specified in config file",
column_key,
)
return False

if column_key in self.column_keys:
logging.warning(
"Duplicate column key '%s' specified in config file",
column_key,
)
return False

return True

def _retrieve_quotes(self) -> None:
"""Query for the quotes and update the change version."""

now: float = monotonic()
while self._query_thread_running:
self._load_quotes_internal(now)
self._retrieve_quotes_internal(now)

while now - self._last_query_time < self._query_frequency:
if not self._query_thread_running:
return
sleep(1)
now = monotonic()

def _sort_quotes(self):
"""Sort the quotes, according to the sort column and direction."""

if not self._quotes_lock.locked():
raise RuntimeError(
"The _quotes_lock must be acquired before calling this function"
)

self._quotes.sort(
key=self._sort_key_func,
reverse=(self._sort_direction == SortDirection.DESCENDING),
)

def _load_quotes_internal(self, monotonic_clock: float) -> None:
def _retrieve_quotes_internal(self, monotonic_clock: float) -> None:
"""
Query for the quotes from the YFinance interface and update the change version.
NOTE: Not for external use. Created for testing purposes.
Note: Not for external use. Created for testing purposes only.
Args:
monotonic_clock (float): A monotonic clock time.
"""

with self._quotes_lock:
self._quotes = self._yfin.get_quotes(self._quotes_symbols)
self._quotes = self._yfin.retrieve_quotes(self._quotes_symbols)
self._sort_quotes()
self._last_query_time = monotonic_clock
self._version += 1

def _sort_quotes(self):
"""Sort the quotes, according to the sort column and direction."""

if not self._quotes_lock.locked():
raise RuntimeError(
"The _quotes_lock must be acquired before calling this function"
)

self._quotes.sort(
key=self._sort_key_func,
reverse=(self._sort_direction == SortDirection.DESCENDING),
)

##############################################################################
## Configuration load and save
##############################################################################
def load_config(self, config: dict[str, Any]) -> None:
"""
Load the configuration for the quote table.
Expand All @@ -267,7 +380,7 @@ def load_config(self, config: dict[str, Any]) -> None:
if QuoteTableState._COLUMNS in config
else []
)
sort_column_key: Optional[str] = (
sort_key: Optional[str] = (
config[QuoteTableState._SORT_COLUMN]
if QuoteTableState._SORT_COLUMN in config
else None
Expand All @@ -293,35 +406,23 @@ def load_config(self, config: dict[str, Any]) -> None:
# Validate the column keys
if len(columns_keys) == 0:
logging.warning("No columns specified in config file")
self._columns_keys = QuoteTableState._DEFAULT_COLUMN_KEYS[:]
self._columns_keys.insert(0, QuoteTableState._TICKER_COLUMN_KEY)
columns_keys = [
QuoteTableState._TICKER_COLUMN_KEY
] + QuoteTableState._DEFAULT_COLUMN_KEYS[:]
else:
# Ticker is *always* the first column
self._columns_keys = [QuoteTableState._TICKER_COLUMN_KEY]

# Make sure the column keys are supported and there are no duplicates
for column_key in columns_keys:
if column_key not in ALL_QUOTE_COLUMNS:
logging.warning(
"Invalid column key '%s' specified in config file",
column_key,
)
continue

if column_key in self._columns_keys:
logging.warning(
"Duplicate column key '%s' specified in config file",
column_key,
)
continue
columns_keys.insert(0, QuoteTableState._TICKER_COLUMN_KEY)

self._columns_keys.append(column_key)
self._columns.clear()
# Make sure the column keys are supported and there are no duplicates
for column_key in columns_keys:
self.append_column(column_key)

# Validate the sort column key
if sort_column_key is None or sort_column_key not in self._columns_keys:
self._sort_column_key = self._columns_keys[0]
if sort_key is None or sort_key not in self.column_keys:
self._sort_column_key = self._columns[0].key
else:
self._sort_column_key = sort_column_key
self._sort_column_key = sort_key

# Validate the sort direction
try:
Expand Down Expand Up @@ -354,7 +455,6 @@ def load_config(self, config: dict[str, Any]) -> None:
self._query_frequency = query_frequency

# Set other properties based on the configuration
self._columns = [ALL_QUOTE_COLUMNS[column] for column in self._columns_keys]
self._sort_key_func = ALL_QUOTE_COLUMNS[self._sort_column_key].sort_key_func

def save_config(self) -> dict[str, Any]:
Expand All @@ -366,7 +466,7 @@ def save_config(self) -> dict[str, Any]:
"""

return {
QuoteTableState._COLUMNS: self._columns_keys[1:],
QuoteTableState._COLUMNS: self.column_keys[1:],
QuoteTableState._SORT_COLUMN: self._sort_column_key,
QuoteTableState._SORT_DIRECTION: self._sort_direction.value,
QuoteTableState._QUOTES: self._quotes_symbols[:],
Expand Down
4 changes: 2 additions & 2 deletions tests/appui/fake_yfinance.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ class FakeYFinance(YFinance):
def __init__(self) -> None:
self._quotes: list[YQuote] = []

def get_quotes(self, symbols: list[str]) -> list[YQuote]:
def retrieve_quotes(self, symbols: list[str]) -> list[YQuote]:
"""
Retrieve quotes for the given symbols.
NOTE: The quotes are pulled from the test data file
In this implementation, the quotes are pulled from the test data file.
Args:
symbols (list[str]): The symbols to get quotes for.
Expand Down
Loading

0 comments on commit 42fa720

Please sign in to comment.