]>
us-delaporte.pagekite.me Git - matrix-shop-bot.git/blob - matrix_shop_bot/db.py
3 from datetime
import datetime
as dt
4 from decimal
import Decimal
5 from typing
import List
7 from sqlmodel
import Field
, Session
, SQLModel
, create_engine
, select
, text
, Relationship
, func
9 from sqlite3
import IntegrityError
13 DATE_FORMAT
= '%Y-%m-%d'
16 return dt
.now().strftime(DATE_FORMAT
)
19 logger
= logging
.getLogger(__name__
)
21 class ItemStoreLink(SQLModel
, table
=True):
22 item_id
: int |
None = Field(default
=None, foreign_key
="item.id", primary_key
=True)
23 store_id
: int |
None = Field(default
=None, foreign_key
="store.id", primary_key
=True)
26 class Item(SQLModel
, table
=True):
27 '''An item to shop for.'''
28 id: int |
None = Field(default
=None, primary_key
=True)
29 name
: str = Field(default
=None, unique
=True)
30 price
: Decimal |
None = Field(decimal_places
=2)
31 stores
: list["Store"] = Relationship(back_populates
="shopping_list", link_model
=ItemStoreLink
)
32 purchase_date
: dt |
None = Field(default
=None)
34 if(self
.purchase_date
):
35 return f
'- [x] {self.name} (#{self.id})'
37 return f
'- [ ] {self.name} (#{self.id})'
39 class ItemZone(SQLModel
, table
=True):
40 '''Where an item is usually found.
42 Matched only by loose text matching, since items come and go, over time.
44 id: int |
None = Field(default
=None, primary_key
=True)
45 name
: str = Field(default
=None, unique
=True) # milk
46 zone
: str = Field(default
=None, unique
=False) # dairy section
48 class PreferStore(SQLModel
, table
=True):
49 '''Tracks which store we are shopping at, at the moment.'''
50 store_id
: int |
None = Field(default
=None, foreign_key
="store.id", primary_key
=True)
52 def _get_item_zone(item_name
, session
) -> str:
53 zone
= session
.exec(select(ItemZone
).where(ItemZone
.name
== item_name
)).first()
56 return 'No Zone Assigned'
58 def _get_pretty_shopping_list(store_id
, session
) -> List
[str]:
59 store
= session
.exec(_get_with_id(Store
, store_id
)).first()
61 results
.append(f
' ## {store.name}')
62 items
= store
.shopping_list
64 results
.append(f
'-- Nothing to shop for at {store.name} --')
69 zone
= _get_item_zone(item
.name
, session
)
72 zones
[zone
].append(str(item
))
74 results
.append(f
' ### {zone}')
75 for item
in zones
[zone
]:
79 class Store(SQLModel
, table
=True):
80 '''A place to shop at.'''
81 id: int |
None = Field(default
=None, primary_key
=True)
82 name
: str = Field(default
=None, unique
=True)
83 shopping_list
: list["Item"] = Relationship(back_populates
="stores", link_model
=ItemStoreLink
)
84 # purchase_history: list["PurchaseRecord"] = Relationship(back_populates="store")
86 return f
'{self.name} (#{self.id})'
89 class PurchaseRecord(SQLModel
, table
=True):
90 '''Tracks when and where items were purchased.'''
91 id: int |
None = Field(default
=None, primary_key
=True)
92 name
: str = Field(default
=None)
93 price
: Decimal |
None = Field(decimal_places
=2)
94 store
: str |
None = Field(default
=None)
95 purchase_date
: dt |
None = Field(default
=None)
97 date
= self
.purchase_date
.strftime(DATE_FORMAT
)
98 return f
'Bought {self.name} at {self.store} on {date}'
100 def _get_with_id(cls
, item_id
):
101 return select(cls
).where(cls
.id == item_id
)
103 def _get_with_name(cls
, name
):
104 return select(cls
).where(func
.lower(cls
.name
) == func
.lower(name
))
108 def __init__(self
, db_file
=None):
110 db_file
= os
.path
.join(os
.environ
.get('MATRIX_DATA_DIR', ''), f
'{MODULE}.db')
113 logger
.debug(f
'Shop bot is using db file {self.file}')
114 self
.engine
= self
.get_engine()
115 self
.last_store_id
= None
117 def _set_pref_store_id(self
, store_id
):
118 with Session(self
.engine
) as session
:
119 prefs
= session
.exec(select(PreferStore
)).all()
123 pref
.store_id
= store_id
127 def _get_pref_store_id(self
):
128 with Session(self
.engine
) as session
:
129 pref
= session
.exec(select(PreferStore
)).one()
132 def _add_with_name(self
, cls
, name
):
135 with Session(self
.engine
) as session
:
142 # recover by returning the item_id
143 item
= session
.exec(select(Item
).where(Item
.name
== name
)).first()
148 def add_store(self
, name
):
149 store_id
= self
._add
_with
_name
(Store
, name
)
150 self
._set
_pref
_store
_id
(store_id
)
153 def add_item(self
, name
):
154 names
= name
.split(',')
156 item_id
= self
._add
_with
_name
(Item
, name
.strip())
157 store_id
= self
._get
_pref
_store
_id
()
158 self
.assign_store(item_id
, store_id
)
161 def assign_last_store(self
, item_id
):
162 store_id
= self
._get
_pref
_store
_id
()
163 self
.assign_store(item_id
, store_id
)
166 def assign_zone(self
, item_name
, zone_name
):
168 with Session(self
.engine
) as session
:
170 new
= ItemZone(name
=item_name
)
176 pass # Already assigned
179 def assign_store(self
, item_id
, store_id
):
180 self
._set
_pref
_store
_id
(store_id
)
182 with Session(self
.engine
) as session
:
183 store
= session
.exec(_get_with_id(Store
, store_id
)).first()
184 item
= session
.exec(_get_with_id(Item
, item_id
)).first()
186 item
.stores
.append(store
)
189 pass # Already assigned
192 def list_purchases(self
, store_name
):
193 with Session(self
.engine
) as session
:
194 cmd
= select(PurchaseRecord
).where(func
.lower(PurchaseRecord
.store
) == func
.lower(store_name
))
195 records
= session
.exec(cmd
).all()
198 def purchase_items(self
, item_ids
):
200 with Session(self
.engine
) as session
:
201 # Mark items as purchased
202 for item_id
in item_ids
.split(' '):
203 item
= session
.exec(_get_with_id(Item
, item_id
)).first()
205 item
.purchase_date
= dt
.today()
207 # Track purchase history (locally only!)
208 # to generate recommended shopping lists
209 record
= PurchaseRecord()
210 record
.name
= item
.name
211 record
.price
= item
.price
212 record
.purchase_date
= item
.purchase_date
214 shopping_at
= self
._get
_pref
_store
_id
() # We bought it here
215 cmd
= select(Store
).where(Store
.id == shopping_at
)
216 store
= session
.exec(cmd
).one()
217 record
.store
= store
.name
220 results
.append(str(record
))
221 # results.append(str(item))
227 def clear_list(self
, store_name
, clear_all
=False):
229 with Session(self
.engine
) as session
:
230 store
= session
.exec(_get_with_name(Store
, store_name
)).first()
232 return [f
'Store {store_name} not found.']
234 for item
in store
.shopping_list
:
235 if clear_all
or item
.purchase_date
:
239 results
= _get_pretty_shopping_list(store
.id, session
)
242 def get_stores(self
):
243 with Session(self
.engine
) as session
:
245 return session
.exec(cmd
).all()
247 def get_unsorted_list(self
):
250 with Session(self
.engine
) as session
:
251 cmd
= select(Item
).where(Item
.stores
== None)
252 items
= session
.exec(cmd
).all()
255 return [f
'No unsorted items!']
258 def get_last_shopping_list(self
) -> List
[str]:
259 store_id
= self
._get
_pref
_store
_id
()
260 with Session(self
.engine
) as session
:
261 store
= session
.exec(_get_with_id(Store
, store_id
)).first()
263 return _get_pretty_shopping_list(store
.id, session
)
264 return [f
'Last shopping list store not found.']
266 def get_shopping_list(self
, store_name
) -> List
[str]:
268 with Session(self
.engine
) as session
:
269 store
= session
.exec(_get_with_name(Store
, store_name
)).first()
271 return [f
'Store {store_name} not found.']
272 results
= _get_pretty_shopping_list(store
.id, session
)
273 self
._set
_pref
_store
_id
(store
.id)
276 def remove_item(self
, item_id
):
277 with Session(self
.engine
) as session
:
278 item
= session
.exec(_get_with_id(Item
, item_id
)).first()
282 def remove_store(self
, store_id
):
283 with Session(self
.engine
) as session
:
284 store
= session
.exec(_get_with_id(Store
, store_id
)).first()
285 session
.delete(store
)
288 def get_engine(self
):
289 return create_engine(f
"sqlite:///{self.file}")
292 SQLModel
.metadata
.create_all(self
.engine
)