|
@@ -10,6 +10,7 @@ import itertools
|
|
|
import sys
|
|
|
import urwid
|
|
|
import pandas as pd
|
|
|
+import numpy as np
|
|
|
from widgets import COPYRIGHT
|
|
|
from db_utils import QueryManager
|
|
|
from dateutil.parser import parse as parse_time
|
|
@@ -21,7 +22,7 @@ from widgets import (
|
|
|
ActivityManager,
|
|
|
#_set_focus_path,
|
|
|
)
|
|
|
-
|
|
|
+from decimal import Decimal, InvalidOperation
|
|
|
from collections import (
|
|
|
OrderedDict,
|
|
|
)
|
|
@@ -48,8 +49,8 @@ except:
|
|
|
|
|
|
palette = [
|
|
|
('banner', 'light gray', 'dark red'),
|
|
|
- ('streak', 'light red', 'dark gray'),
|
|
|
- ('bg', 'light red', 'black'),
|
|
|
+ ('streak', 'light gray', 'black'),
|
|
|
+ ('bg', 'dark red', 'black'),
|
|
|
]
|
|
|
|
|
|
top_pane = [
|
|
@@ -61,7 +62,9 @@ top_pane = [
|
|
|
left_pane = [
|
|
|
'product',
|
|
|
'organic',
|
|
|
- None,
|
|
|
+ 'div',
|
|
|
+ 'spread',
|
|
|
+ 'rating',
|
|
|
]
|
|
|
right_pane = [
|
|
|
'unit',
|
|
@@ -69,21 +72,15 @@ right_pane = [
|
|
|
'price',
|
|
|
]
|
|
|
bottom_pane = [
|
|
|
- #'rating',
|
|
|
'dbview',
|
|
|
]
|
|
|
|
|
|
inputs = filter(
|
|
|
- lambda x: x is not None,
|
|
|
+ lambda x: x is not None and x not in ('rating','spread', 'div'),
|
|
|
itertools.chain(
|
|
|
left_pane, right_pane
|
|
|
)
|
|
|
)
|
|
|
-inputs_layout = [
|
|
|
- ['product', 'unit', ],
|
|
|
- ['organic', 'quantity', ],
|
|
|
- ['price', None, ],
|
|
|
-]
|
|
|
|
|
|
display_map = {
|
|
|
'price': lambda x: f"{x:.4f}",
|
|
@@ -191,102 +188,147 @@ class PriceCheck(urwid.WidgetPlaceholder):
|
|
|
else:
|
|
|
return super().keypress(size, key)
|
|
|
|
|
|
- def _init_data(self, fields):
|
|
|
- self.data = OrderedDict([
|
|
|
- (k, '') for k in fields
|
|
|
- ])
|
|
|
- self.clear()
|
|
|
-
|
|
|
def apply_choice(self, name, widget, value):
|
|
|
- self._apply_changes(name, value)
|
|
|
+ self.apply_changes(name, widget, value)
|
|
|
for k,v in self.data.items():
|
|
|
if k == name or v:
|
|
|
continue
|
|
|
options = self.query_manager.unique_suggestions(k, **self.data)
|
|
|
if len(options) == 1:
|
|
|
- self._apply_changes(k, list(options)[0])
|
|
|
+ self.apply_changes(k, widget, list(options)[0])
|
|
|
|
|
|
- def _apply_changes(self, name, value):
|
|
|
- self.data.update({
|
|
|
- name: value,
|
|
|
- })
|
|
|
-
|
|
|
- def apply_changes(self, name):
|
|
|
- return lambda w,x: self._apply_changes(name, x)
|
|
|
-
|
|
|
- def apply_organic_state(self, w, state):
|
|
|
- self.data['organic'] = repr(state).lower()
|
|
|
+ def apply_changes(self, name, widget, value):
|
|
|
+ if name in self.data:
|
|
|
+
|
|
|
+ self.data.update({
|
|
|
+ name: value,
|
|
|
+ })
|
|
|
+ self.update_historic_prices()
|
|
|
|
|
|
def clear(self):
|
|
|
for k in self.data:
|
|
|
- if k in ('ts', 'store',):
|
|
|
+ if k == 'organic':
|
|
|
+ self.data[k] = 'mixed'
|
|
|
continue
|
|
|
self.data[k] = ''
|
|
|
- self.organic_checkbox.set_state('mixed')
|
|
|
+ self.update()
|
|
|
+ return self
|
|
|
|
|
|
def update(self):
|
|
|
for k in self.edit_fields:
|
|
|
- self.edit_fields[k].set_edit_text(self.data[k])
|
|
|
+ if self.data[k] != self.edit_fields[k].get_edit_text():
|
|
|
+ self.edit_fields[k].set_edit_text(self.data[k])
|
|
|
+
|
|
|
+ if self.data['organic'] != self.organic_checkbox.state:
|
|
|
+ self.organic_checkbox.set_state(self.data['organic'])
|
|
|
+
|
|
|
+ self.update_historic_prices()
|
|
|
|
|
|
- sort = '$/unit' if self.buttons['sort_price'].state else 'ts'
|
|
|
- #print(self.organic_checkbox.state)
|
|
|
- #input()
|
|
|
- organic = None if self.organic_checkbox.state == "mixed" else self.organic_checkbox.state
|
|
|
- #print(organic)
|
|
|
- #input()
|
|
|
- self.text_fields['dbview'].set_text(
|
|
|
- self.query_manager.get_historic_prices(sort, self.data['product'], self.data['unit'], organic=organic)
|
|
|
- )
|
|
|
- self.organic_checkbox.set_state({
|
|
|
- "'mixed'": 'mixed',
|
|
|
- 'true': True,
|
|
|
- 'false': False,
|
|
|
- }[self.data['organic']])
|
|
|
return self
|
|
|
+
|
|
|
+ def update_rating(self, _avg, _min, _max, price=None, quantity=None):
|
|
|
+ if None in (_avg, _min, _max):
|
|
|
+ return
|
|
|
+ current = None if None in (price, quantity) else float(price/quantity)
|
|
|
+ size = 16
|
|
|
+ chars = ['|', *['-']*(size - 2), '|' ]
|
|
|
+ rating = [' ']*len(chars)
|
|
|
+ _min, _max = min(_min, current or _min), max(_max, current or _max)
|
|
|
+ ls = np.linspace(_min, _max, len(chars))
|
|
|
+
|
|
|
+ for idx, (e, a) in enumerate(zip(ls, ls[1:])):
|
|
|
+ if e <= _avg < a:
|
|
|
+ chars[max(idx-4,0)] = '['
|
|
|
+ chars[min(idx+4,len(chars)-1)] = ']'
|
|
|
+ for _idx, c in enumerate(f'{_avg:>7.2f}'):
|
|
|
+ chars[max(idx-3+_idx,0)] = c
|
|
|
+
|
|
|
+ if current is not None and e <= current < a:
|
|
|
+ rating[idx] = '^'
|
|
|
+
|
|
|
+ self.text_fields['spread'].set_text(f"{_min:>7.2f}{''.join(chars)}{_max:<7.2f}")
|
|
|
+ self.text_fields['rating'].set_text(f"{' '*7}{''.join(rating)}{' '*7}")
|
|
|
+
|
|
|
+
|
|
|
+ def update_historic_prices(self):
|
|
|
+ organic = None if self.data['organic'] == 'mixed' else self.data['organic']
|
|
|
+ sort = '$/unit' if self.buttons['sort_price'].state else 'ts'
|
|
|
+ product, unit = self.data['product'] or None, self.data['unit'] or None
|
|
|
+ try:
|
|
|
+ price = Decimal(self.data['price']) or None
|
|
|
+ except InvalidOperation:
|
|
|
+ price = None
|
|
|
+
|
|
|
+ try:
|
|
|
+ quantity = Decimal(self.data['quantity']) or None
|
|
|
+ except InvalidOperation:
|
|
|
+ price = None
|
|
|
+
|
|
|
+ if None not in (sort, product, unit):
|
|
|
+ self.text_fields['dbview'].set_text(
|
|
|
+ self.query_manager.get_historic_prices(
|
|
|
+ lambda *args: self.update_rating(*args, price=price, quantity=quantity),
|
|
|
+ sort, product, unit, organic=organic)
|
|
|
+ )
|
|
|
|
|
|
def __init__(self, query_manager, fields,
|
|
|
top_pane, left_pane, right_pane, bottom_pane,
|
|
|
autocomplete_cb):
|
|
|
super().__init__(urwid.SolidFill(u'/'))
|
|
|
- self.organic_checkbox = NoTabCheckBox(
|
|
|
- u"Organic",
|
|
|
- on_state_change=self.apply_organic_state
|
|
|
- )
|
|
|
self.query_manager = query_manager
|
|
|
- self._init_data(fields)
|
|
|
self.top_pane = top_pane
|
|
|
self.left_pane = left_pane
|
|
|
self.right_pane = right_pane
|
|
|
self.bottom_pane = bottom_pane
|
|
|
self.edit_fields = OrderedDict()
|
|
|
self.text_fields = OrderedDict()
|
|
|
- for k in self.data:
|
|
|
+ self.data = OrderedDict()
|
|
|
+ self.organic_checkbox = NoTabCheckBox(
|
|
|
+ u"Organic",
|
|
|
+ state='mixed',
|
|
|
+ )
|
|
|
+ urwid.connect_signal(self.organic_checkbox, 'change', lambda w,v: self.apply_changes('organic', w, v))
|
|
|
+
|
|
|
+ fields = [f for f in fields ]
|
|
|
+ for k in fields:
|
|
|
if k in self.right_pane and k != 'unit':
|
|
|
ef = AutoCompleteFloatEdit(('bg', k), apply_change_func=lambda name: autocomplete_cb(name, self.data))
|
|
|
elif k != 'organic':
|
|
|
ef = AutoCompleteEdit(('bg', k), apply_change_func=lambda name: autocomplete_cb(name, self.data))
|
|
|
- else:
|
|
|
+ elif k == 'organic':
|
|
|
+ self.data[k] = 'mixed'
|
|
|
continue
|
|
|
|
|
|
- ef.set_edit_text(self.data[k])
|
|
|
- urwid.connect_signal(ef, 'change', self.apply_changes(k))
|
|
|
+ self.data[k] = ''
|
|
|
+ urwid.connect_signal(ef, 'change', lambda w,v,name=k: self.apply_changes(name, w, v))
|
|
|
self.edit_fields[k] = ef
|
|
|
-
|
|
|
- header = urwid.Text(u'Price Check', 'center')
|
|
|
- _copyright = urwid.Text(COPYRIGHT, 'center')
|
|
|
+
|
|
|
self.buttons = OrderedDict()
|
|
|
group = []
|
|
|
for k in top_pane:
|
|
|
if k != 'sort':
|
|
|
- self.buttons.update({ k: urwid.Button(('streak', f'{k}')) })
|
|
|
+ self.buttons.update({ k: urwid.Button(('streak', f'{k.title()}')) })
|
|
|
continue
|
|
|
- self.buttons.update({ f'{k}_price': urwid.RadioButton(group, ('streak', u'Best Price'), state="first True",
|
|
|
- on_state_change=None, user_data=[ self.buttons[b] for b in self.buttons if 'sort' in b ] )})
|
|
|
- self.buttons.update({ f'{k}_date': urwid.RadioButton(group, ('streak', u'Last Price'), state="first True",
|
|
|
- on_state_change=None, user_data=[ self.buttons[b] for b in self.buttons if 'sort' in b ] )})
|
|
|
+ self.buttons.update({ f'{k}_price': urwid.RadioButton(group, ('streak', u'Best'), state="first True") })
|
|
|
+ self.buttons.update({ f'{k}_date': urwid.RadioButton(group, ('streak', u'Last'), state="first True") })
|
|
|
+ for button in group:
|
|
|
+ urwid.connect_signal(button, 'postchange', lambda *args: self.update_historic_prices())
|
|
|
+
|
|
|
+ urwid.connect_signal(self.buttons['clear'], 'click', lambda x: self.clear().update())
|
|
|
+ urwid.connect_signal(self.buttons['exit'], 'click', lambda x: show_or_exit('esc'))
|
|
|
|
|
|
- urwid.connect_signal(self.buttons['clear'], 'click', lambda: None)
|
|
|
- urwid.connect_signal(self.buttons['exit'], 'click', lambda: None)
|
|
|
+ dbview, spread, rating = [ urwid.Text('') for i in range(0,3) ]
|
|
|
+
|
|
|
+ self.text_fields.update({
|
|
|
+ 'dbview': dbview,
|
|
|
+ 'spread': spread,
|
|
|
+ 'rating': rating,
|
|
|
+ })
|
|
|
+
|
|
|
+ self.clear()
|
|
|
+
|
|
|
+ header = urwid.Text(u'Price Check', 'center')
|
|
|
+ _copyright = urwid.Text(COPYRIGHT, 'center')
|
|
|
|
|
|
banner = urwid.Pile([
|
|
|
urwid.Padding(header, 'center', width=('relative', 100)),
|
|
@@ -297,29 +339,47 @@ class PriceCheck(urwid.WidgetPlaceholder):
|
|
|
(k, urwid.LineBox(urwid.AttrMap(self.edit_fields[k], 'streak'), title=k.title(), title_align='left')) for k in self.edit_fields
|
|
|
])
|
|
|
fields['organic'] = urwid.LineBox(urwid.AttrMap(self.organic_checkbox, 'bg'))
|
|
|
- dbview = urwid.Text('')
|
|
|
- self.text_fields.update({'dbview': dbview})
|
|
|
fields.update({
|
|
|
'dbview': urwid.LineBox(
|
|
|
urwid.AttrMap(dbview, 'streak'),
|
|
|
title="Historic Prices",
|
|
|
title_align='center',
|
|
|
- )
|
|
|
+ ),
|
|
|
+ 'div': urwid.Divider(),
|
|
|
+ 'spread': spread,
|
|
|
+ 'rating': rating,
|
|
|
})
|
|
|
-
|
|
|
- right_pane_widget = (12, urwid.Pile(map(
|
|
|
+
|
|
|
+ right_pane_widget = (16, urwid.Pile(map(
|
|
|
lambda x: fields[x] if x is not None else urwid.Divider(),
|
|
|
self.right_pane
|
|
|
)))
|
|
|
- left_pane_widget = urwid.Pile(map(
|
|
|
+ left_pane_widget = (urwid.Pile(map(
|
|
|
lambda x: fields[x] if x is not None else urwid.Divider(),
|
|
|
self.left_pane
|
|
|
- ))
|
|
|
+ )))
|
|
|
|
|
|
widget = urwid.Pile([
|
|
|
banner,
|
|
|
urwid.Divider(),
|
|
|
- urwid.Columns([v for v in self.buttons.values()],
|
|
|
+ urwid.Columns(
|
|
|
+ [
|
|
|
+ (9, urwid.Pile([
|
|
|
+ urwid.Divider(),
|
|
|
+ urwid.AttrMap(self.buttons['clear'], 'streak'),
|
|
|
+ urwid.Divider(),
|
|
|
+ ])),
|
|
|
+ urwid.LineBox(
|
|
|
+ urwid.Columns([ v for k,v in self.buttons.items() if 'sort' in k]),
|
|
|
+ title="Sort price by",
|
|
|
+ title_align='left',
|
|
|
+ ),
|
|
|
+ (9, urwid.Pile([
|
|
|
+ urwid.Divider(),
|
|
|
+ urwid.AttrMap(self.buttons['exit'], 'streak'),
|
|
|
+ urwid.Divider(),
|
|
|
+ ]))
|
|
|
+ ],
|
|
|
dividechars=2,
|
|
|
),
|
|
|
urwid.Columns((left_pane_widget, right_pane_widget),
|