From fa6bae1f5cd348d781c884c06340d68b498a30e6 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Wed, 2 Jan 2019 21:12:42 -0500 Subject: [PATCH] Reorg table widgets into a new module --- piker/ui/monitor.py | 463 +------------------------------------- piker/ui/option_chain.py | 3 +- piker/ui/tabular.py | 470 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 475 insertions(+), 461 deletions(-) create mode 100644 piker/ui/tabular.py diff --git a/piker/ui/monitor.py b/piker/ui/monitor.py index 5518bb22..537227ef 100644 --- a/piker/ui/monitor.py +++ b/piker/ui/monitor.py @@ -5,483 +5,26 @@ Launch with ``piker monitor ``. (Currently there's a bunch of questrade specific stuff in here) """ -from itertools import chain from types import ModuleType, AsyncGeneratorType from typing import List, Callable -from bisect import bisect import trio import tractor from kivy.uix.boxlayout import BoxLayout -from kivy.uix.gridlayout import GridLayout -from kivy.uix.stacklayout import StackLayout -from kivy.uix.button import Button from kivy.lang import Builder -from kivy import utils from kivy.app import async_runTouchApp from kivy.core.window import Window -from kivy.properties import BooleanProperty +from .tabular import ( + Row, TickerTable, _kv, _black_rgba, colorcode, +) from ..log import get_logger from .pager import PagerView -from .kivy.mouse_over import new_mouse_over_group -HoverBehavior = new_mouse_over_group() log = get_logger('monitor') -_colors2hexs = { - 'darkgray': 'a9a9a9', - 'gray': '808080', - 'green': '008000', - 'forestgreen': '228b22', - 'red2': 'ff3333', - 'red': 'ff0000', - 'firebrick': 'b22222', -} - -_colors = {key: utils.rgba(val) for key, val in _colors2hexs.items()} - - -def colorcode(name): - return _colors[name if name else 'gray'] - - -_bs = 0.75 # border size -_fs = 20 # font size - -# medium shade of gray that seems to match the -# default i3 window borders -_i3_rgba = [0.14]*3 + [1] - -# slightly off black like the jellybean bg from -# vim colorscheme -_cell_rgba = [0.07]*3 + [1] -_black_rgba = [0]*4 - -_kv = (f''' -#:kivy 1.10.0 - - - font_size: {_fs} - - # make text wrap to botom - text_size: self.size - halign: 'center' - valign: 'middle' - size: self.texture_size - - # don't set these as the update loop already does it - # color: {colorcode('gray')} - # font_color: {colorcode('gray')} - # font_name: 'Hack-Regular' - - # if `highlight` is set use i3 color by default transparent; use row color - # this is currently used for expiry cells on the options chain - background_color: {_i3_rgba} if self.click_toggle else {_black_rgba} - # must be set to allow 'plain bg colors' since default texture is grey - # but right now is only set for option chain expiry buttons - # background_normal: '' - # spacing: 0, 0 - # padding: 3, 3 - - - - font_size: {_fs} - # canvas.before: - # Color: - # rgba: [0.13]*4 - # BorderImage: # use a fixed size border - # pos: self.pos - # size: [self.size[0] - {_bs}, self.size[1]] - # # 0s are because the containing TickerTable already has spacing - # # border: [0, {_bs} , 0, {_bs}] - # border: [0, {_bs} , 0, 0] - - - - spacing: [{_bs}] - # row_force_default: True - row_default_height: 56 - cols: 1 - canvas.before: - Color: - # i3 style gray as background - rgba: {_i3_rgba} - Rectangle: - # scale with container self here refers to the widget i.e BoxLayout - pos: self.pos - size: self.size - - - - spacing: [{_bs}, 0] - - - - # minimum_height: 200 # should be pulled from Cell text size - # minimum_width: 200 - # row_force_default: True - # row_default_height: 61 # determines the header row size - padding: [0]*4 - spacing: [0] - canvas.before: - Color: - rgba: {_cell_rgba} - Rectangle: - # self here refers to the widget i.e Row(GridLayout) - pos: self.pos - size: self.size - # row higlighting on mouse over - Color: - rgba: {_i3_rgba} - # RoundedRectangle: - Rectangle: - size: self.width, self.height if self.hovered else 1 - pos: self.pos - # radius: (0,) - - -# part of the `PagerView` - - size_hint: 1, None - # static size of 51 px - height: 51 - font_size: 25 - background_color: {_i3_rgba} -''') - - -class Cell(Button): - """Data cell: the fundemental widget. - - ``key`` is the column name index value. - """ - click_toggle = BooleanProperty(False) - - def __init__(self, key=None, is_header=False, **kwargs): - super(Cell, self).__init__(**kwargs) - self.key = key - self.row = None - self.is_header = is_header - - def on_press(self, value=None): - self.row.on_press() - - -class HeaderCell(Cell): - """Column header cell label. - """ - def on_press(self, value=None): - """Clicking on a col header indicates to sort rows by this column - in `update_quotes()`. - """ - table = self.row.table - # if this is a row header cell then sort by the clicked field - if self.row.is_header: - table.sort_key = self.key - - last = table.last_clicked_col_cell - if last and last is not self: - last.underline = False - last.bold = False - - # outline the header text to indicate it's been the last clicked - self.underline = True - self.bold = True - # mark this cell as the last selected - table.last_clicked_col_cell = self - # sort and render the rows immediately - self.row.table.render_rows(table.symbols2rows.values()) - - # TODO: make this some kind of small geometry instead - # (maybe like how trading view does it). - # allow highlighting of row headers for tracking - elif self.is_header: - if self.background_color == self.color: - self.background_color = _black_rgba - else: - self.background_color = self.color - - -class BidAskLayout(StackLayout): - """Cell which houses three buttons containing a last, bid, and ask in a - single unit oriented with the last 2 under the first. - """ - def __init__(self, values, header=False, **kwargs): - # uncomment to get vertical stacked bid-ask - # super(BidAskLayout, self).__init__(orientation='bt-lr', **kwargs) - super(BidAskLayout, self).__init__(orientation='lr-tb', **kwargs) - assert len(values) == 3, "You can only provide 3 values: last,bid,ask" - self._keys2cells = {} - cell_type = HeaderCell if header else Cell - top_size = cell_type().font_size - small_size = top_size - 4 - top_prop = 0.5 # proportion of size used by top cell - bottom_prop = 1 - top_prop - for (key, size_hint, font_size), value in zip( - [('last', (1, top_prop), top_size), - ('bid', (0.5, bottom_prop), small_size), - ('ask', (0.5, bottom_prop), small_size)], - # uncomment to get vertical stacked bid-ask - # [('last', (top_prop, 1), top_size), - # ('bid', (bottom_prop, 0.5), small_size), - # ('ask', (bottom_prop, 0.5), small_size)], - values - ): - cell = cell_type( - text=str(value), - size_hint=size_hint, - # width=self.width/2 - 3, - font_size=font_size - ) - self._keys2cells[key] = cell - cell.key = value - cell.is_header = header - setattr(self, key, cell) - self.add_widget(cell) - - # should be assigned by referrer - self.row = None - - def get_cell(self, key): - return self._keys2cells.get(key) - - @property - def row(self): - return self.row - - @row.setter - def row(self, row): - # so hideous - for cell in self.cells: - cell.row = row - - @property - def cells(self): - return [self.last, self.bid, self.ask] - - -class Row(HoverBehavior, GridLayout): - """A grid for displaying a row of ticker quote data. - """ - def __init__( - self, - record, - headers=(), - no_cell=(), - bidasks=None, - table=None, - is_header=False, - cell_type=None, - **kwargs - ): - super().__init__(cols=len(record), **kwargs) - self._cell_widgets = {} - self._last_record = record - self.table = table - self.is_header = is_header - self._cell_type = cell_type - self.widget = self - - # Create `BidAskCells` first. - # bid/ask cells are just 3 cells grouped in a - # ``BidAskLayout`` which just stacks the parent cell - # on top of 2 children. - layouts = {} - bidasks = bidasks or {} - ba_cells = {} - for key, children in bidasks.items(): - layout = BidAskLayout( - [record[key]] + [record[child] for child in children], - header=is_header - ) - layout.row = self - layouts[key] = layout - for i, child in enumerate([key] + children): - ba_cells[child] = layout.cells[i] - - children_flat = list(chain.from_iterable(bidasks.values())) - self._cell_widgets.update(ba_cells) - - # build out row using Cell labels - for (key, val) in record.items(): - header = key in headers - - # handle bidask cells - if key in layouts: - self.add_widget(layouts[key]) - elif key in children_flat: - # these cells have already been added to the `BidAskLayout` - continue - elif key not in no_cell: - cell = self._append_cell(val, key, header=header) - cell.key = key - self._cell_widgets[key] = cell - - def iter_cells(self): - return self._cell_widgets.items() - - def get_cell(self, key): - return self._cell_widgets.get(key) - - def get_field(self, key): - return self._last_record[key] - - def _append_cell(self, text, key, header=False): - if not len(self._cell_widgets) < self.cols: - raise ValueError(f"Can not append more then {self.cols} cells") - - # header cells just have a different colour - celltype = self._cell_type or (HeaderCell if header else Cell) - cell = celltype(text=str(text), key=key) - cell.is_header = header - cell.row = self - self.add_widget(cell) - return cell - - def update(self, record, displayable): - """Update this row's cells with new values from a quote - ``record``. - - Return all cells that changed in a ``dict``. - """ - # color changed field values - cells = {} - gray = colorcode('gray') - fgreen = colorcode('forestgreen') - red = colorcode('red2') - for key, val in record.items(): - last = self.get_field(key) - color = gray - try: - # logic for cell text coloring: up-green, down-red - if last < val: - color = fgreen - elif last > val: - color = red - except TypeError: - log.warn(f"wtf QT {val} is not regular?") - - cell = self.get_cell(key) - # some displayable fields might have specifically - # not had cells created as set in the `no_cell` attr - if cell is not None: - cell.text = str(displayable[key]) - cell.color = color - if color != gray: - cells[key] = cell - - self._last_record = record - return cells - - # mouse over handlers - def on_enter(self): - """Highlight layout on enter. - """ - log.debug( - f"Entered row {self} through {self.border_point}") - # don't highlight header row - if self.is_header: - self.hovered = False - - def on_leave(self): - """Un-highlight layout on exit. - """ - log.debug( - f"Left row {self} through {self.border_point}") - - def on_press(self, value=None): - log.info(f"Pressed row for {self._last_record['symbol']}") - if self.table and not self.is_header: - for q in self.table._click_queues: - q.put_nowait(self._last_record['symbol']) - - -class TickerTable(GridLayout): - """A grid for displaying ticker quote records as a table. - """ - def __init__(self, sort_key='%', auto_sort=True, **kwargs): - super(TickerTable, self).__init__(**kwargs) - self.symbols2rows = {} - self.sort_key = sort_key - # for tracking last clicked column header cell - self.last_clicked_col_cell = None - self._auto_sort = auto_sort - self._symbols2index = {} - self._sorted = [] - self._click_queues: List[trio.Queue] = [] - - def append_row(self, key, row): - """Append a `Row` of `Cell` objects to this table. - """ - # store ref to each row - self.symbols2rows[key] = row - self.add_widget(row) - self._sorted.append(row) - return row - - def clear(self): - self.clear_widgets() - self._sorted.clear() - - def render_rows( - self, - changed: set, - sort_key: str = None, - ): - """Sort and render all rows on the ticker grid from ``syms2rows``. - """ - sort_key = sort_key or self.sort_key - key_row_pairs = list(sorted( - [(row.get_field(sort_key), row) for row in self._sorted], - key=lambda item: item[0], - )) - if key_row_pairs: - sorted_keys, sorted_rows = zip(*key_row_pairs) - sorted_keys, sorted_rows = list(sorted_keys), list(sorted_rows) - else: - sorted_keys, sorted_rows = [], [] - - # now remove and re-insert any rows that need to be shuffled - # due to new a new field change - for row in changed: - try: - old_index = sorted_rows.index(row) - except ValueError: - # row is not yet added so nothing to remove - pass - else: - del sorted_rows[old_index] - del sorted_keys[old_index] - self._sorted.remove(row) - self.remove_widget(row) - - for row in changed: - key = row.get_field(sort_key) - index = bisect(sorted_keys, key) - sorted_keys.insert(index, key) - self._sorted.insert(index, row) - self.add_widget(row, index=index) - - def ticker_search(self, patt): - """Return sequence of matches when pattern ``patt`` is in a - symbol name. Most naive algo possible for the moment. - """ - for symbol, row in self.symbols2rows.items(): - if patt in symbol: - yield symbol, row - - def get_row(self, symbol: str) -> Row: - return self.symbols2rows[symbol] - - def search(self, patt): - """Search bar api compat. - """ - return dict(self.ticker_search(patt)) or {} - - async def update_quotes( nursery: trio._core._run.Nursery, formatter: Callable, diff --git a/piker/ui/option_chain.py b/piker/ui/option_chain.py index 531ea0d5..237c8431 100644 --- a/piker/ui/option_chain.py +++ b/piker/ui/option_chain.py @@ -20,7 +20,8 @@ from ..log import get_logger from ..brokers.core import contracts from .pager import PagerView -from .monitor import Row, HeaderCell, Cell, TickerTable, update_quotes +from .tabular import Row, HeaderCell, Cell, TickerTable +from .monitor import update_quotes log = get_logger('option_chain') diff --git a/piker/ui/tabular.py b/piker/ui/tabular.py new file mode 100644 index 00000000..7a9aca2f --- /dev/null +++ b/piker/ui/tabular.py @@ -0,0 +1,470 @@ +""" +Real-time table components +""" +from itertools import chain +from typing import List +from bisect import bisect + +import trio +from kivy.uix.gridlayout import GridLayout +from kivy.uix.stacklayout import StackLayout +from kivy.uix.button import Button +from kivy import utils +from kivy.properties import BooleanProperty + +from ..log import get_logger +from .kivy.mouse_over import new_mouse_over_group + + +HoverBehavior = new_mouse_over_group() +log = get_logger('monitor') + +_colors2hexs = { + 'darkgray': 'a9a9a9', + 'gray': '808080', + 'green': '008000', + 'forestgreen': '228b22', + 'red2': 'ff3333', + 'red': 'ff0000', + 'firebrick': 'b22222', +} + +_colors = {key: utils.rgba(val) for key, val in _colors2hexs.items()} + + +def colorcode(name): + return _colors[name if name else 'gray'] + + +_bs = 0.75 # border size +_fs = 20 # font size + +# medium shade of gray that seems to match the +# default i3 window borders +_i3_rgba = [0.14]*3 + [1] + +# slightly off black like the jellybean bg from +# vim colorscheme +_cell_rgba = [0.07]*3 + [1] +_black_rgba = [0]*4 + +_kv = (f''' +#:kivy 1.10.0 + + + font_size: {_fs} + + # make text wrap to botom + text_size: self.size + halign: 'center' + valign: 'middle' + size: self.texture_size + + # don't set these as the update loop already does it + # color: {colorcode('gray')} + # font_color: {colorcode('gray')} + # font_name: 'Hack-Regular' + + # if `highlight` is set use i3 color by default transparent; use row color + # this is currently used for expiry cells on the options chain + background_color: {_i3_rgba} if self.click_toggle else {_black_rgba} + # must be set to allow 'plain bg colors' since default texture is grey + # but right now is only set for option chain expiry buttons + # background_normal: '' + # spacing: 0, 0 + # padding: 3, 3 + + + + font_size: {_fs} + # canvas.before: + # Color: + # rgba: [0.13]*4 + # BorderImage: # use a fixed size border + # pos: self.pos + # size: [self.size[0] - {_bs}, self.size[1]] + # # 0s are because the containing TickerTable already has spacing + # # border: [0, {_bs} , 0, {_bs}] + # border: [0, {_bs} , 0, 0] + + + + spacing: [{_bs}] + # row_force_default: True + row_default_height: 56 + cols: 1 + canvas.before: + Color: + # i3 style gray as background + rgba: {_i3_rgba} + Rectangle: + # scale with container self here refers to the widget i.e BoxLayout + pos: self.pos + size: self.size + + + + spacing: [{_bs}, 0] + + + + # minimum_height: 200 # should be pulled from Cell text size + # minimum_width: 200 + # row_force_default: True + # row_default_height: 61 # determines the header row size + padding: [0]*4 + spacing: [0] + canvas.before: + Color: + rgba: {_cell_rgba} + Rectangle: + # self here refers to the widget i.e Row(GridLayout) + pos: self.pos + size: self.size + # row higlighting on mouse over + Color: + rgba: {_i3_rgba} + # RoundedRectangle: + Rectangle: + size: self.width, self.height if self.hovered else 1 + pos: self.pos + # radius: (0,) + + +# part of the `PagerView` + + size_hint: 1, None + # static size of 51 px + height: 51 + font_size: 25 + background_color: {_i3_rgba} +''') + + +class Cell(Button): + """Data cell: the fundemental widget. + + ``key`` is the column name index value. + """ + click_toggle = BooleanProperty(False) + + def __init__(self, key=None, is_header=False, **kwargs): + super(Cell, self).__init__(**kwargs) + self.key = key + self.row = None + self.is_header = is_header + + def on_press(self, value=None): + self.row.on_press() + + +class HeaderCell(Cell): + """Column header cell label. + """ + def on_press(self, value=None): + """Clicking on a col header indicates to sort rows by this column + in `update_quotes()`. + """ + table = self.row.table + # if this is a row header cell then sort by the clicked field + if self.row.is_header: + table.sort_key = self.key + + last = table.last_clicked_col_cell + if last and last is not self: + last.underline = False + last.bold = False + + # outline the header text to indicate it's been the last clicked + self.underline = True + self.bold = True + # mark this cell as the last selected + table.last_clicked_col_cell = self + # sort and render the rows immediately + self.row.table.render_rows(table.symbols2rows.values()) + + # TODO: make this some kind of small geometry instead + # (maybe like how trading view does it). + # allow highlighting of row headers for tracking + elif self.is_header: + if self.background_color == self.color: + self.background_color = _black_rgba + else: + self.background_color = self.color + + +class BidAskLayout(StackLayout): + """Cell which houses three buttons containing a last, bid, and ask in a + single unit oriented with the last 2 under the first. + """ + def __init__(self, values, header=False, **kwargs): + # uncomment to get vertical stacked bid-ask + # super(BidAskLayout, self).__init__(orientation='bt-lr', **kwargs) + super(BidAskLayout, self).__init__(orientation='lr-tb', **kwargs) + assert len(values) == 3, "You can only provide 3 values: last,bid,ask" + self._keys2cells = {} + cell_type = HeaderCell if header else Cell + top_size = cell_type().font_size + small_size = top_size - 4 + top_prop = 0.5 # proportion of size used by top cell + bottom_prop = 1 - top_prop + for (key, size_hint, font_size), value in zip( + [('last', (1, top_prop), top_size), + ('bid', (0.5, bottom_prop), small_size), + ('ask', (0.5, bottom_prop), small_size)], + # uncomment to get vertical stacked bid-ask + # [('last', (top_prop, 1), top_size), + # ('bid', (bottom_prop, 0.5), small_size), + # ('ask', (bottom_prop, 0.5), small_size)], + values + ): + cell = cell_type( + text=str(value), + size_hint=size_hint, + # width=self.width/2 - 3, + font_size=font_size + ) + self._keys2cells[key] = cell + cell.key = value + cell.is_header = header + setattr(self, key, cell) + self.add_widget(cell) + + # should be assigned by referrer + self.row = None + + def get_cell(self, key): + return self._keys2cells.get(key) + + @property + def row(self): + return self.row + + @row.setter + def row(self, row): + # so hideous + for cell in self.cells: + cell.row = row + + @property + def cells(self): + return [self.last, self.bid, self.ask] + + +class Row(HoverBehavior, GridLayout): + """A grid for displaying a row of ticker quote data. + """ + def __init__( + self, + record, + headers=(), + no_cell=(), + bidasks=None, + table=None, + is_header=False, + cell_type=None, + **kwargs + ): + super().__init__(cols=len(record), **kwargs) + self._cell_widgets = {} + self._last_record = record + self.table = table + self.is_header = is_header + self._cell_type = cell_type + self.widget = self + + # Create `BidAskCells` first. + # bid/ask cells are just 3 cells grouped in a + # ``BidAskLayout`` which just stacks the parent cell + # on top of 2 children. + layouts = {} + bidasks = bidasks or {} + ba_cells = {} + for key, children in bidasks.items(): + layout = BidAskLayout( + [record[key]] + [record[child] for child in children], + header=is_header + ) + layout.row = self + layouts[key] = layout + for i, child in enumerate([key] + children): + ba_cells[child] = layout.cells[i] + + children_flat = list(chain.from_iterable(bidasks.values())) + self._cell_widgets.update(ba_cells) + + # build out row using Cell labels + for (key, val) in record.items(): + header = key in headers + + # handle bidask cells + if key in layouts: + self.add_widget(layouts[key]) + elif key in children_flat: + # these cells have already been added to the `BidAskLayout` + continue + elif key not in no_cell: + cell = self._append_cell(val, key, header=header) + cell.key = key + self._cell_widgets[key] = cell + + def iter_cells(self): + return self._cell_widgets.items() + + def get_cell(self, key): + return self._cell_widgets.get(key) + + def get_field(self, key): + return self._last_record[key] + + def _append_cell(self, text, key, header=False): + if not len(self._cell_widgets) < self.cols: + raise ValueError(f"Can not append more then {self.cols} cells") + + # header cells just have a different colour + celltype = self._cell_type or (HeaderCell if header else Cell) + cell = celltype(text=str(text), key=key) + cell.is_header = header + cell.row = self + self.add_widget(cell) + return cell + + def update(self, record, displayable): + """Update this row's cells with new values from a quote + ``record``. + + Return all cells that changed in a ``dict``. + """ + # color changed field values + cells = {} + gray = colorcode('gray') + fgreen = colorcode('forestgreen') + red = colorcode('red2') + for key, val in record.items(): + last = self.get_field(key) + color = gray + try: + # logic for cell text coloring: up-green, down-red + if last < val: + color = fgreen + elif last > val: + color = red + except TypeError: + log.warn(f"wtf QT {val} is not regular?") + + cell = self.get_cell(key) + # some displayable fields might have specifically + # not had cells created as set in the `no_cell` attr + if cell is not None: + cell.text = str(displayable[key]) + cell.color = color + if color != gray: + cells[key] = cell + + self._last_record = record + return cells + + # mouse over handlers + def on_enter(self): + """Highlight layout on enter. + """ + log.debug( + f"Entered row {self} through {self.border_point}") + # don't highlight header row + if self.is_header: + self.hovered = False + + def on_leave(self): + """Un-highlight layout on exit. + """ + log.debug( + f"Left row {self} through {self.border_point}") + + def on_press(self, value=None): + log.info(f"Pressed row for {self._last_record['symbol']}") + if self.table and not self.is_header: + for q in self.table._click_queues: + q.put_nowait(self._last_record['symbol']) + + +class TickerTable(GridLayout): + """A grid for displaying ticker quote records as a table. + """ + def __init__(self, sort_key='%', auto_sort=True, **kwargs): + super(TickerTable, self).__init__(**kwargs) + self.symbols2rows = {} + self.sort_key = sort_key + # for tracking last clicked column header cell + self.last_clicked_col_cell = None + self._auto_sort = auto_sort + self._symbols2index = {} + self._sorted = [] + self._click_queues: List[trio.Queue] = [] + + def append_row(self, key, row): + """Append a `Row` of `Cell` objects to this table. + """ + # store ref to each row + self.symbols2rows[key] = row + self.add_widget(row) + self._sorted.append(row) + return row + + def clear(self): + self.clear_widgets() + self._sorted.clear() + + def render_rows( + self, + changed: set, + sort_key: str = None, + ): + """Sort and render all rows on the ticker grid from ``syms2rows``. + """ + sort_key = sort_key or self.sort_key + key_row_pairs = list(sorted( + [(row.get_field(sort_key), row) for row in self._sorted], + key=lambda item: item[0], + )) + if key_row_pairs: + sorted_keys, sorted_rows = zip(*key_row_pairs) + sorted_keys, sorted_rows = list(sorted_keys), list(sorted_rows) + else: + sorted_keys, sorted_rows = [], [] + + # now remove and re-insert any rows that need to be shuffled + # due to new a new field change + for row in changed: + try: + old_index = sorted_rows.index(row) + except ValueError: + # row is not yet added so nothing to remove + pass + else: + del sorted_rows[old_index] + del sorted_keys[old_index] + self._sorted.remove(row) + self.remove_widget(row) + + for row in changed: + key = row.get_field(sort_key) + index = bisect(sorted_keys, key) + sorted_keys.insert(index, key) + self._sorted.insert(index, row) + self.add_widget(row, index=index) + + def ticker_search(self, patt): + """Return sequence of matches when pattern ``patt`` is in a + symbol name. Most naive algo possible for the moment. + """ + for symbol, row in self.symbols2rows.items(): + if patt in symbol: + yield symbol, row + + def get_row(self, symbol: str) -> Row: + return self.symbols2rows[symbol] + + def search(self, patt): + """Search bar api compat. + """ + return dict(self.ticker_search(patt)) or {}