X-Git-Url: https://us-delaporte.pagekite.me/gitweb/matrix-shop-bot.git/blobdiff_plain/8b639943d820c97787fed92731f0ec8d13e9ac7e..HEAD:/matrix_shop_bot/db.py diff --git a/matrix_shop_bot/db.py b/matrix_shop_bot/db.py index 60339ee..cf75f84 100644 --- a/matrix_shop_bot/db.py +++ b/matrix_shop_bot/db.py @@ -1,6 +1,7 @@ import logging import os from datetime import datetime as dt +from datetime import timedelta from decimal import Decimal from typing import List @@ -24,18 +25,61 @@ class ItemStoreLink(SQLModel, table=True): class Item(SQLModel, table=True): + '''An item to shop for.''' id: int | None = Field(default=None, primary_key=True) name: str = Field(default=None, unique=True) price: Decimal | None = Field(decimal_places=2) stores: list["Store"] = Relationship(back_populates="shopping_list", link_model=ItemStoreLink) purchase_date: dt | None = Field(default=None) def __str__(self): - return f'{self.name} (#{self.id})' + if(self.purchase_date): + return f'- [x] {self.name} (#{self.id})' + else: + return f'- [ ] {self.name} (#{self.id})' + +class ItemZone(SQLModel, table=True): + '''Where an item is usually found. + + Matched only by loose text matching, since items come and go, over time. + ''' + id: int | None = Field(default=None, primary_key=True) + name: str = Field(default=None, unique=True) # milk + zone: str = Field(default=None, unique=False) # dairy section class PreferStore(SQLModel, table=True): + '''Tracks which store we are shopping at, at the moment.''' store_id: int | None = Field(default=None, foreign_key="store.id", primary_key=True) +def _get_item_zone(item_name, session) -> str: + zone = session.exec(select(ItemZone).where(func.lower(item_name).contains(func.lower(ItemZone.name)))).first() + if zone: + return zone.zone + return 'No Zone Assigned' + +def _get_pretty_shopping_list(store_id, session) -> List[str]: + store = session.exec(_get_with_id(Store, store_id)).first() + results = [] + results.append(f' ## {store.name}') + items = store.shopping_list + if not items: + results.append(f'-- Nothing to shop for at {store.name} --') + + zones = {} + + for item in items: + zone = str(_get_item_zone(item.name, session)).title() # Title Case + if not zone in zones: + zones[zone] = [] + zones[zone].append(str(item).title()) + for zone in zones: + zones[zone].sort() + results.append(f' ### {zone}') + for item in zones[zone]: + results.append(item) + return results + class Store(SQLModel, table=True): + '''A place to shop at.''' id: int | None = Field(default=None, primary_key=True) name: str = Field(default=None, unique=True) shopping_list: list["Item"] = Relationship(back_populates="stores", link_model=ItemStoreLink) @@ -43,23 +87,9 @@ class Store(SQLModel, table=True): def __str__(self): return f'{self.name} (#{self.id})' - def get_pretty_shopping_list(self) -> List[str]: - results = [] - results.append(f' ## {self.name}') - items = self.shopping_list - if not items: - results.append(f'-- Nothing to shop for at {self.name} --') - else: - for item in items: - if(item.purchase_date): - results.append(f' - [x] {item}') - else: - results.append(f' - [ ] {item}') - return results - -# May get used later... class PurchaseRecord(SQLModel, table=True): + '''Tracks when and where items were purchased.''' id: int | None = Field(default=None, primary_key=True) name: str = Field(default=None) price: Decimal | None = Field(decimal_places=2) @@ -75,6 +105,12 @@ def _get_with_id(cls, item_id): def _get_with_name(cls, name): return select(cls).where(func.lower(cls.name) == func.lower(name)) +def _rename(cls, session, old, new) -> bool: + with session: + record = session.exec(_get_with_name(cls, old)).first() + record.name = new + session.commit() + return True class Data(): def __init__(self, db_file=None): @@ -86,6 +122,14 @@ class Data(): self.engine = self.get_engine() self.last_store_id = None + def rename_store(self, old, new): + with Session(self.engine) as session: + return _rename(Store, session, old, new) + + def rename_item(self, old, new): + with Session(self.engine) as session: + return _rename(Item, session, old, new) + def _set_pref_store_id(self, store_id): with Session(self.engine) as session: prefs = session.exec(select(PreferStore)).all() @@ -135,6 +179,19 @@ class Data(): self.assign_store(item_id, store_id) return store_id + def assign_zone(self, item_name, zone_name): + item_id = -1 + with Session(self.engine) as session: + try: + new = ItemZone(name=item_name) + new.zone = zone_name + session.add(new) + session.commit() + item_id = new.id + except: + pass # Already assigned + return item_id + def assign_store(self, item_id, store_id): self._set_pref_store_id(store_id) @@ -149,10 +206,15 @@ class Data(): return ['Success'] def list_purchases(self, store_name): + '''List unique purchases at store in the past 90 days''' with Session(self.engine) as session: - cmd = select(PurchaseRecord).where(PurchaseRecord.store == store_name) + recent = dt.now() - timedelta(days=90) + cmd = select(PurchaseRecord).where(func.lower(PurchaseRecord.store) == func.lower(store_name)).where(PurchaseRecord.purchase_date > recent) records = session.exec(cmd).all() - return records + bought = set() + for item in records: + bought.add(item.name) + return list(bought) def purchase_items(self, item_ids): results = [] @@ -183,13 +245,20 @@ class Data(): return results - def clear_list(self, store_id, clear_all=False): + def clear_list(self, store_name, clear_all=False): + results = [] with Session(self.engine) as session: - store = session.exec(_get_with_id(Store, store_id)).first() + store = session.exec(_get_with_name(Store, store_name)).first() + if not store: + return [f'Store {store_name} not found.'] + for item in store.shopping_list: if clear_all or item.purchase_date: session.delete(item) + session.commit() + results = _get_pretty_shopping_list(store.id, session) + return results def get_stores(self): with Session(self.engine) as session: @@ -212,7 +281,7 @@ class Data(): with Session(self.engine) as session: store = session.exec(_get_with_id(Store, store_id)).first() if store: - return store.get_pretty_shopping_list() + return _get_pretty_shopping_list(store.id, session) return [f'Last shopping list store not found.'] def get_shopping_list(self, store_name) -> List[str]: @@ -221,7 +290,7 @@ class Data(): store = session.exec(_get_with_name(Store, store_name)).first() if not store: return [f'Store {store_name} not found.'] - results = store.get_pretty_shopping_list() + results = _get_pretty_shopping_list(store.id, session) self._set_pref_store_id(store.id) return results @@ -242,6 +311,34 @@ class Data(): def setup(self): SQLModel.metadata.create_all(self.engine) + with Session(self.engine) as session: + # Add some obvious store zones by default + DEFAULT_ZONES = { + 'Bakery': ['bread', 'cake', 'cookie', 'muffin', 'pie'], + 'Baking': ['flour', 'sugar'], + 'Books': ['book'], + 'Clothes': ['bra', 'coat', 'jacket', 'panties', 'pants', 'shirt', 'shorts', 'socks', 'sweater', 'underwear'], + 'Cooler': ['butter', 'cream', 'eggs', 'juice', 'milk', 'tea'], + 'Deli': ['beef', 'chicken', 'salmon', 'steak', 'turkey'], + 'Movies': ['movie', 'DVD'], + 'Music': ['cd', 'album'], + 'Produce': ['carrot', 'cucumber', 'greens', 'lettuce'], + 'Shoes': ['shoe'], + 'Television': ['tv', 'season'], + } + for zone in DEFAULT_ZONES: + logging.debug(f'Adding default items for {zone}') + for item in DEFAULT_ZONES[zone]: + try: + logging.debug(f'Attempt adding {item} to {zone}...') + new = ItemZone(name=item, zone=zone) + session.add(new) + session.commit() + logging.debug(f'Success adding {item} to {zone}...') + except Exception as ex: + session.rollback() + logging.debug(f'Failed adding {item} to {zone}: {ex}') + return 'Success'