Quellcode durchsuchen

Move Widgets and Mocks into separate modules and add copyright notices to files without

Daniel Sheffield vor 3 Jahren
Ursprung
Commit
5204597313
6 geänderte Dateien mit 340 neuen und 310 gelöschten Zeilen
  1. 6 1
      db_utils.py
  2. 13 308
      grocery_transactions.py
  3. 178 0
      mock.py
  4. 6 1
      reconcile.py
  5. 6 0
      txn_view.py
  6. 131 0
      widgets.py

+ 6 - 1
db_utils.py

@@ -1,4 +1,9 @@
-
+#
+# Copyright (c) Daniel Sheffield 2021
+#
+# All rights reserved
+#
+# THIS SOFTWARE IS PROVIDED AS IS WITHOUT WARRANTY
 def cursor_as_dict(cur):
     _col_idx_map=dict(map(lambda col: (col[1].name, col[0]), enumerate(cur.description)))
     for row in map(lambda row, _map=_col_idx_map: dict([

+ 13 - 308
grocery_transactions.py

@@ -8,116 +8,13 @@
 import pandas as pd
 from txn_view import get_statement as get_session_transactions_statement
 from dateutil.parser import parse as parse_time
-from datetime import datetime
+from widgets import (
+    NoTabCheckBox,
+    AutoCompleteEdit,
+    AutoCompleteFloatEdit,
+)
 import sys
-try:
-    import psycopg2
-    from psycopg2.sql import SQL
-    from db_utils import cursor_as_dict
-    MOCK = False
-except:
-    from faker import Faker
-    def cursor_as_dict(cur):
-        yield from cur.fetchall()
-        
-    Faker.seed(4321)
-    fake = Faker()
-    MOCK = True
-    # first, import a similar Provider or use the default one
-    from faker.providers import BaseProvider
-
-    # create new provider class
-    class Products(BaseProvider):
-        PRODUCTS = {
-            'Dark Chocolate': 'Chocolate',
-            'Milk Chocolate': 'Chocolate',
-            'Cooking Chocolate': 'Chocolate',
-            'Cucumber': 'Veggies',
-            'Bananas': 'Fruit',
-            'Milk': 'Milk and Cream',
-            'Cream': 'Milk and Cream',
-            'Coffee Beans': 'Coffee',
-            'Freerange Eggs': 'Eggs',
-        }
-        GROUPS = [
-            'Fish, Meat, Eggs',
-            'Dairy',
-            'Beverages',
-            'Treats',
-            'Produce',
-        ]
-        CATEGORIES = {
-            'Eggs': 'Fish, Meat, Eggs',
-            'Milk and Cream': 'Dairy',
-            'Chocolate': 'Treats',
-            'Veggies': 'Produce',
-            'Fruit': 'Produce',
-            'Coffee': 'Beverages',
-        }
-        STORES = [
-            'Paknsave',
-            'Countdown',
-            'New World',
-            'Dreamview',
-            'TOFS',
-        ]
-        CATEGORY_UNITS = {
-            'Eggs': ('g',),
-            'Milk and Cream': ('mL', 'L',),
-            'Chocolate': ('g', 'kg',),
-            'Veggies': ('g', 'kg', 'Piece',),
-            'Fruit': ('g', 'kg', 'Piece', 'Bunch',),
-            'Coffee': ('g', 'kg',),
-        }
-        ORGANIC = [
-            True,
-            False,
-        ]
-        def _dict_choice(self, _dict):
-            key = fake.random.choice([ i for i in _dict ])
-            return key, _dict[key]
-        
-        def _product_choice(self):
-            key = None
-            product = []
-            for _dict in (
-                self.PRODUCTS,
-                self.CATEGORIES,
-                dict([
-                    (k, None) for k in self.GROUPS
-                ]),
-            ):
-                if key is None:
-                    k, key = self._dict_choice(_dict)
-                    product.append(k)
-                    continue
-                product.append(key)
-                key = _dict[key]
-            return product
-        
-        def product(self):
-            return self._product_choice()
-        
-        def product_unit(self, product):
-            return fake.random.choice(self.CATEGORY_UNITS[product[1]])
-        
-        def description(self, *args, **kwargs):
-            return fake.sentence(*args, **kwargs)
-            
-        def store(self):
-            return fake.random.choice(self.STORES)
-        
-        def unit(self):
-            return fake.random.choice(self.UNITS)
-        
-        def organic(self):
-            return fake.random.choice(self.ORGANIC)
-
-    # then add new provider to faker instance
-    fake.add_provider(Products)
-
 import urwid
-from urwid import numedit
 from decimal import Decimal
 import time
 import itertools
@@ -125,26 +22,6 @@ from collections import (
     OrderedDict,
 )
 
-COPYRIGHT = "Copyright (c) Daniel Sheffield 2021"
-
-class mock_conn(object):
-    
-    def close(self):
-        pass
-
-class mock_cur(object):
-    def __init__(self, records):
-        self.records = records
-    
-    def execute(self, *args, **kwargs):
-        pass
-    
-    def fetchall(self):
-        yield from (i for i in self.records)
-    
-    def close(self):
-        pass
-
 try:
     from db_credentials import HOST, PASSWORD
     host = f'host={HOST}'
@@ -153,64 +30,18 @@ except:
     host = ''
     password = ''
 
-if MOCK:
-    records = [{
-        'ts': datetime(2021, 8, 29, 14, 8, 0, 0),
-        'id': 2,
-        'code': fake.store()[:4],
-        'store': 'Countdown',
-        'price': 4.30,
-        '$/unit': 4.3/250.0,
-        'quantity': 250.0,
-        'unit': 'g',
-        'organic': False,
-        'total': 0,
-        'description': 'Whittakers',
-        'product': 'Dark Chocolate',
-        'group': 'Treats',
-        'category': 'Chocolate',
-    }]
-    for i in range(0,100):
-        product = fake.product()
-        quantity = fake.random.random()*500 + 1
-        price = fake.random.random()*10
-        unit = fake.product_unit(product)
-        organic = fake.organic()
-        words = []
-        words.extend(fake.sentence().split())
-        words.append(
-            f'{quantity:.2f} {unit} @ {price:.2f}'
-        )
-        if organic:
-            words.append('organic')
-        
-        records.append({
-            'ts': datetime(2021, 8, 29, 14, 8, 0, 0),
-            'id': 3,
-            'code': fake.store()[:4],
-            'store': fake.store(),
-            'price': price,
-            '$/unit': price/quantity,
-            'quantity': quantity,
-            'unit': unit,
-            'organic': organic,
-            'total': 0,
-            'description': ' '.join([
-                product[0],
-                *fake.description(ext_word_list=words).split()
-            ]),
-            'product': product[0],
-            'group': product[2],
-            'category': product[1],
-        })
-    
-    conn = mock_conn()
-    cur = mock_cur(records)
-else:
+try:
+    raise Exception
+    from psycopg2.sql import SQL
+    from db_utils import cursor_as_dict
     import os
     user = os.getenv('USER')
     conn = psycopg2.connect(f"{host} dbname=das user={user} {password}")
     cur = conn.cursor()
+except:
+    from mock import *
+
+COPYRIGHT = "Copyright (c) Daniel Sheffield 2021"
 
 palette = [
     ('banner', 'light gray', 'dark red'),
@@ -242,8 +73,6 @@ cols = [
     )
 ]
 
-#cols.remove(None)
-
 NON_IDENTIFIER_COLUMNS = [
     'ts',
     'store',
@@ -332,130 +161,6 @@ def interleave(_list, div):
         yield element
         yield div
 
-class AutoCompleteEdit(urwid.Edit):
-    def __init__(self, name, *args, apply_change_func=None, **kwargs):
-        if isinstance(name, tuple):
-            pallete, title = name
-            self.name = title
-            title = title.title()
-            passthrough = (pallete, u'  ' if title.lower() == 'unit' else u'')
-        else:
-            self.name = name
-            title = name.title()
-            passthrough = u'  ' if name.lower() == 'unit' else u''
-            
-        super(AutoCompleteEdit, self).__init__(passthrough, *args, **kwargs)
-        self.apply = apply_change_func
-
-    def keypress(self, size, key):
-        if key == 'enter':
-            self.apply(self.name)
-            return
-        elif key == 'delete':
-            self.set_edit_text('')
-            return
-        
-        return super(AutoCompleteEdit, self).keypress(size, key)
-
-
-class AutoCompleteFloatEdit(numedit.FloatEdit):
-    def __init__(self, name, *args, apply_change_func=None, **kwargs):
-        self.last_val = None
-        self.op = '='
-        self.pallete = None
-        if isinstance(name, tuple):
-            self.pallete, self.name = name
-            title = self.name.title()
-            passthrough = (self.pallete, f'{self.op} ')
-        else:
-            self.name = name
-            title = name.title()
-            passthrough = title
-            
-        super(AutoCompleteFloatEdit, self).__init__(passthrough, *args, **kwargs)
-        self.apply = apply_change_func
-
-    def update_caption(self):
-        if self.pallete is not None:
-            self.set_caption((self.pallete, f'{self.op} '))
-        else:
-            self.set_caption(f'{self.op} ')
-
-    def set_op(self, op):
-        self.op = op
-        self.last_val = self.value()
-        self.set_edit_text('')
-        self.update_caption()
-        
-    def calc(self):
-        x = self.last_val
-        op = self.op
-        if op in ('+', '-',):
-            y = self.value() or Decimal(0.0)
-            if op == '+':
-                z = x + y
-            else:
-                z = x - y
-        elif op in ('*', '/'):
-            y = self.value() or Decimal(1.0)
-            if op == '*':
-                z = x * y
-            else:
-                z = x / y
-        else:
-            y = self.value() or Decimal(0.0)
-            z = y
-        
-        self.op = '='
-        self.update_caption()
-        self.set_edit_text(f'{z:.2f}')
-
-    def keypress(self, size, key):
-        if isinstance(key, tuple):
-            return
-        ops = ('+', '-', '*', '/',)
-
-        if key in ops:
-            if self.op in ops:
-                self.calc()
-            self.set_op(key)
-            return
-        elif key == 'enter':
-            if self.get_edit_text() == '' or self.value() == Decimal(0.0):
-                return self.apply(self.name)
-            self.calc()
-            return
-        elif key == '=':
-            self.calc()
-            return
-        elif key == 'delete':
-            self.set_edit_text('')
-            self.op = '='
-            self.update_caption()
-            return
-
-        return super(AutoCompleteFloatEdit, self).keypress(size, key)
-
-class NoTabCheckBox(urwid.CheckBox):
-    def keypress(self, size, key):
-        if not isinstance(key, tuple) and key == 'tab':
-            return 
-        else:
-            return super(NoTabCheckBox, self).keypress(size, key)
-
-def _set_focus_path(container, path):
-    try:
-        container.set_focus_path(path)
-        return
-    except IndexError:
-        pass
-        
-    if path[-1] == 0 and len(path) > 1:
-        _set_focus_path(container, path[:-1])
-        return
-    
-    raise IndexError
-
 class GroceryTransactionEditor(urwid.WidgetPlaceholder):
     def __init__(self, log, fields):
         super(GroceryTransactionEditor, self).__init__(urwid.SolidFill(u'/'))

+ 178 - 0
mock.py

@@ -0,0 +1,178 @@
+#
+# Copyright (c) Daniel Sheffield 2021
+#
+# All rights reserved
+#
+# THIS SOFTWARE IS PROVIDED AS IS WITHOUT WARRANTY
+from faker import Faker
+from psycopg2.sql import SQL
+from datetime import datetime
+
+class mock_conn(object):
+    
+    def close(self):
+        pass
+
+class mock_cur(object):
+    def __init__(self, records):
+        self.records = records
+    
+    def execute(self, *args, **kwargs):
+        pass
+    
+    def fetchall(self):
+        yield from (i for i in self.records)
+    
+    def close(self):
+        pass
+
+def cursor_as_dict(cur):
+    yield from cur.fetchall()
+    
+Faker.seed(4321)
+fake = Faker()
+# first, import a similar Provider or use the default one
+from faker.providers import BaseProvider
+
+# create new provider class
+class Products(BaseProvider):
+    PRODUCTS = {
+        'Dark Chocolate': 'Chocolate',
+        'Milk Chocolate': 'Chocolate',
+        'Cooking Chocolate': 'Chocolate',
+        'Cucumber': 'Veggies',
+        'Bananas': 'Fruit',
+        'Milk': 'Milk and Cream',
+        'Cream': 'Milk and Cream',
+        'Coffee Beans': 'Coffee',
+        'Freerange Eggs': 'Eggs',
+    }
+    GROUPS = [
+        'Fish, Meat, Eggs',
+        'Dairy',
+        'Beverages',
+        'Treats',
+        'Produce',
+    ]
+    CATEGORIES = {
+        'Eggs': 'Fish, Meat, Eggs',
+        'Milk and Cream': 'Dairy',
+        'Chocolate': 'Treats',
+        'Veggies': 'Produce',
+        'Fruit': 'Produce',
+        'Coffee': 'Beverages',
+    }
+    STORES = [
+        'Paknsave',
+        'Countdown',
+        'New World',
+        'Dreamview',
+        'TOFS',
+    ]
+    CATEGORY_UNITS = {
+        'Eggs': ('g',),
+        'Milk and Cream': ('mL', 'L',),
+        'Chocolate': ('g', 'kg',),
+        'Veggies': ('g', 'kg', 'Piece',),
+        'Fruit': ('g', 'kg', 'Piece', 'Bunch',),
+        'Coffee': ('g', 'kg',),
+    }
+    ORGANIC = [
+        True,
+        False,
+    ]
+    def _dict_choice(self, _dict):
+        key = fake.random.choice([ i for i in _dict ])
+        return key, _dict[key]
+    
+    def _product_choice(self):
+        key = None
+        product = []
+        for _dict in (
+            self.PRODUCTS,
+            self.CATEGORIES,
+            dict([
+                (k, None) for k in self.GROUPS
+            ]),
+        ):
+            if key is None:
+                k, key = self._dict_choice(_dict)
+                product.append(k)
+                continue
+            product.append(key)
+            key = _dict[key]
+        return product
+    
+    def product(self):
+        return self._product_choice()
+    
+    def product_unit(self, product):
+        return fake.random.choice(self.CATEGORY_UNITS[product[1]])
+    
+    def description(self, *args, **kwargs):
+        return fake.sentence(*args, **kwargs)
+        
+    def store(self):
+        return fake.random.choice(self.STORES)
+    
+    def unit(self):
+        return fake.random.choice(self.UNITS)
+    
+    def organic(self):
+        return fake.random.choice(self.ORGANIC)
+
+# then add new provider to faker instance
+fake.add_provider(Products)
+
+records = [{
+    'ts': datetime(2021, 8, 29, 14, 8, 0, 0),
+    'id': 2,
+    'code': fake.store()[:4],
+    'store': 'Countdown',
+    'price': 4.30,
+    '$/unit': 4.3/250.0,
+    'quantity': 250.0,
+    'unit': 'g',
+    'organic': False,
+    'total': 0,
+    'description': 'Whittakers',
+    'product': 'Dark Chocolate',
+    'group': 'Treats',
+    'category': 'Chocolate',
+}]
+for i in range(0,100):
+    product = fake.product()
+    quantity = fake.random.random()*500 + 1
+    price = fake.random.random()*10
+    unit = fake.product_unit(product)
+    organic = fake.organic()
+    words = []
+    words.extend(fake.sentence().split())
+    words.append(
+        f'{quantity:.2f} {unit} @ {price:.2f}'
+    )
+    if organic:
+        words.append('organic')
+    
+    records.append({
+        'ts': datetime(2021, 8, 29, 14, 8, 0, 0),
+        'id': 3,
+        'code': fake.store()[:4],
+        'store': fake.store(),
+        'price': price,
+        '$/unit': price/quantity,
+        'quantity': quantity,
+        'unit': unit,
+        'organic': organic,
+        'total': 0,
+        'description': ' '.join([
+            product[0],
+            *fake.description(ext_word_list=words).split()
+        ]),
+        'product': product[0],
+        'group': product[2],
+        'category': product[1],
+    })
+
+conn = mock_conn()
+cur = mock_cur(records)

+ 6 - 1
reconcile.py

@@ -1,4 +1,9 @@
-#!/usr/bin/python3
+#
+# Copyright (c) Daniel Sheffield 2021
+#
+# All rights reserved
+#
+# THIS SOFTWARE IS PROVIDED AS IS WITHOUT WARRANTY
 #
 # usage:  ./reconcile.py ~/gnucash/merged.gnucash 2021-11-01 2021-12-10 1
 #

+ 6 - 0
txn_view.py

@@ -1,3 +1,9 @@
+#
+# Copyright (c) Daniel Sheffield 2021
+#
+# All rights reserved
+#
+# THIS SOFTWARE IS PROVIDED AS IS WITHOUT WARRANTY
 from psycopg2.sql import (
     Identifier,
     SQL,

+ 131 - 0
widgets.py

@@ -0,0 +1,131 @@
+#
+# Copyright (c) Daniel Sheffield 2021
+#
+# All rights reserved
+#
+# THIS SOFTWARE IS PROVIDED AS IS WITHOUT WARRANTY
+import urwid
+from urwid import numedit
+class AutoCompleteEdit(urwid.Edit):
+    def __init__(self, name, *args, apply_change_func=None, **kwargs):
+        if isinstance(name, tuple):
+            pallete, title = name
+            self.name = title
+            title = title.title()
+            passthrough = (pallete, u'  ' if title.lower() == 'unit' else u'')
+        else:
+            self.name = name
+            title = name.title()
+            passthrough = u'  ' if name.lower() == 'unit' else u''
+            
+        super(AutoCompleteEdit, self).__init__(passthrough, *args, **kwargs)
+        self.apply = apply_change_func
+
+    def keypress(self, size, key):
+        if key == 'enter':
+            self.apply(self.name)
+            return
+        elif key == 'delete':
+            self.set_edit_text('')
+            return
+        
+        return super(AutoCompleteEdit, self).keypress(size, key)
+
+
+class AutoCompleteFloatEdit(numedit.FloatEdit):
+    def __init__(self, name, *args, apply_change_func=None, **kwargs):
+        self.last_val = None
+        self.op = '='
+        self.pallete = None
+        if isinstance(name, tuple):
+            self.pallete, self.name = name
+            title = self.name.title()
+            passthrough = (self.pallete, f'{self.op} ')
+        else:
+            self.name = name
+            title = name.title()
+            passthrough = title
+            
+        super(AutoCompleteFloatEdit, self).__init__(passthrough, *args, **kwargs)
+        self.apply = apply_change_func
+
+    def update_caption(self):
+        if self.pallete is not None:
+            self.set_caption((self.pallete, f'{self.op} '))
+        else:
+            self.set_caption(f'{self.op} ')
+
+    def set_op(self, op):
+        self.op = op
+        self.last_val = self.value()
+        self.set_edit_text('')
+        self.update_caption()
+        
+    def calc(self):
+        x = self.last_val
+        op = self.op
+        if op in ('+', '-',):
+            y = self.value() or Decimal(0.0)
+            if op == '+':
+                z = x + y
+            else:
+                z = x - y
+        elif op in ('*', '/'):
+            y = self.value() or Decimal(1.0)
+            if op == '*':
+                z = x * y
+            else:
+                z = x / y
+        else:
+            y = self.value() or Decimal(0.0)
+            z = y
+        
+        self.op = '='
+        self.update_caption()
+        self.set_edit_text(f'{z:.2f}')
+
+    def keypress(self, size, key):
+        if isinstance(key, tuple):
+            return
+        ops = ('+', '-', '*', '/',)
+
+        if key in ops:
+            if self.op in ops:
+                self.calc()
+            self.set_op(key)
+            return
+        elif key == 'enter':
+            if self.get_edit_text() == '' or self.value() == Decimal(0.0):
+                return self.apply(self.name)
+            self.calc()
+            return
+        elif key == '=':
+            self.calc()
+            return
+        elif key == 'delete':
+            self.set_edit_text('')
+            self.op = '='
+            self.update_caption()
+            return
+
+        return super(AutoCompleteFloatEdit, self).keypress(size, key)
+
+class NoTabCheckBox(urwid.CheckBox):
+    def keypress(self, size, key):
+        if not isinstance(key, tuple) and key == 'tab':
+            return 
+        else:
+            return super(NoTabCheckBox, self).keypress(size, key)
+
+def _set_focus_path(container, path):
+    try:
+        container.set_focus_path(path)
+        return
+    except IndexError:
+        pass
+        
+    if path[-1] == 0 and len(path) > 1:
+        _set_focus_path(container, path[:-1])
+        return
+    
+    raise IndexError