]>
apps.delaporte.us Git - matrix-shop-bot.git/blob - matrix_shop_bot/db.py
464e8f165c472bd8508ff166b104de5fe4e895b9
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 return f
'{self.name} (#{self.id})'
36 class ItemZone(SQLModel
, table
=True):
37 '''Where an item is usually found.
39 Matched only by loose text matching, since items come and go, over time.
41 id: int |
None = Field(default
=None, primary_key
=True)
42 name
: str = Field(default
=None, unique
=True) # milk
43 zone
: str = Field(default
=None, unique
=False) # dairy section
46 class PreferStore(SQLModel
, table
=True):
47 '''Tracks which store we are shopping at, at the moment.'''
48 store_id
: int |
None = Field(default
=None, foreign_key
="store.id", primary_key
=True)
50 class Store(SQLModel
, table
=True):
51 '''A place to shop at.'''
52 id: int |
None = Field(default
=None, primary_key
=True)
53 name
: str = Field(default
=None, unique
=True)
54 shopping_list
: list["Item"] = Relationship(back_populates
="stores", link_model
=ItemStoreLink
)
55 # purchase_history: list["PurchaseRecord"] = Relationship(back_populates="store")
57 return f
'{self.name} (#{self.id})'
59 def get_pretty_shopping_list(self
) -> List
[str]:
61 results
.append(f
' ## {self.name}')
62 items
= self
.shopping_list
64 results
.append(f
'-- Nothing to shop for at {self.name} --')
67 if(item
.purchase_date
):
68 results
.append(f
' - [x] {item}')
70 results
.append(f
' - [ ] {item}')
74 class PurchaseRecord(SQLModel
, table
=True):
75 '''Tracks when and where items were purchased.'''
76 id: int |
None = Field(default
=None, primary_key
=True)
77 name
: str = Field(default
=None)
78 price
: Decimal |
None = Field(decimal_places
=2)
79 store
: str |
None = Field(default
=None)
80 purchase_date
: dt |
None = Field(default
=None)
82 date
= self
.purchase_date
.strftime(DATE_FORMAT
)
83 return f
'Bought {self.name} at {self.store} on {date}'
85 def _get_with_id(cls
, item_id
):
86 return select(cls
).where(cls
.id == item_id
)
88 def _get_with_name(cls
, name
):
89 return select(cls
).where(func
.lower(cls
.name
) == func
.lower(name
))
93 def __init__(self
, db_file
=None):
95 db_file
= os
.path
.join(os
.environ
.get('MATRIX_DATA_DIR', ''), f
'{MODULE}.db')
98 logger
.debug(f
'Shop bot is using db file {self.file}')
99 self
.engine
= self
.get_engine()
100 self
.last_store_id
= None
102 def _set_pref_store_id(self
, store_id
):
103 with Session(self
.engine
) as session
:
104 prefs
= session
.exec(select(PreferStore
)).all()
108 pref
.store_id
= store_id
112 def _get_pref_store_id(self
):
113 with Session(self
.engine
) as session
:
114 pref
= session
.exec(select(PreferStore
)).one()
117 def _add_with_name(self
, cls
, name
):
120 with Session(self
.engine
) as session
:
127 # recover by returning the item_id
128 item
= session
.exec(select(Item
).where(Item
.name
== name
)).first()
133 def add_store(self
, name
):
134 store_id
= self
._add
_with
_name
(Store
, name
)
135 self
._set
_pref
_store
_id
(store_id
)
138 def add_item(self
, name
):
139 names
= name
.split(',')
141 item_id
= self
._add
_with
_name
(Item
, name
.strip())
142 store_id
= self
._get
_pref
_store
_id
()
143 self
.assign_store(item_id
, store_id
)
146 def assign_last_store(self
, item_id
):
147 store_id
= self
._get
_pref
_store
_id
()
148 self
.assign_store(item_id
, store_id
)
151 def assign_zone(self
, item_name
, zone_name
):
153 with Session(self
.engine
) as session
:
155 new
= ItemZone(name
=item_name
)
161 pass # Already assigned
164 def assign_store(self
, item_id
, store_id
):
165 self
._set
_pref
_store
_id
(store_id
)
167 with Session(self
.engine
) as session
:
168 store
= session
.exec(_get_with_id(Store
, store_id
)).first()
169 item
= session
.exec(_get_with_id(Item
, item_id
)).first()
171 item
.stores
.append(store
)
174 pass # Already assigned
177 def list_purchases(self
, store_name
):
178 with Session(self
.engine
) as session
:
179 cmd
= select(PurchaseRecord
).where(func
.lower(PurchaseRecord
.store
) == func
.lower(store_name
))
180 records
= session
.exec(cmd
).all()
183 def purchase_items(self
, item_ids
):
185 with Session(self
.engine
) as session
:
186 # Mark items as purchased
187 for item_id
in item_ids
.split(' '):
188 item
= session
.exec(_get_with_id(Item
, item_id
)).first()
190 item
.purchase_date
= dt
.today()
192 # Track purchase history (locally only!)
193 # to generate recommended shopping lists
194 record
= PurchaseRecord()
195 record
.name
= item
.name
196 record
.price
= item
.price
197 record
.purchase_date
= item
.purchase_date
199 shopping_at
= self
._get
_pref
_store
_id
() # We bought it here
200 cmd
= select(Store
).where(Store
.id == shopping_at
)
201 store
= session
.exec(cmd
).one()
202 record
.store
= store
.name
205 results
.append(str(record
))
206 # results.append(str(item))
212 def clear_list(self
, store_name
, clear_all
=False):
214 with Session(self
.engine
) as session
:
215 store
= session
.exec(_get_with_name(Store
, store_name
)).first()
217 return [f
'Store {store_name} not found.']
219 for item
in store
.shopping_list
:
220 if clear_all
or item
.purchase_date
:
224 results
= store
.get_pretty_shopping_list()
227 def get_stores(self
):
228 with Session(self
.engine
) as session
:
230 return session
.exec(cmd
).all()
232 def get_unsorted_list(self
):
235 with Session(self
.engine
) as session
:
236 cmd
= select(Item
).where(Item
.stores
== None)
237 items
= session
.exec(cmd
).all()
240 return [f
'No unsorted items!']
243 def get_last_shopping_list(self
) -> List
[str]:
244 store_id
= self
._get
_pref
_store
_id
()
245 with Session(self
.engine
) as session
:
246 store
= session
.exec(_get_with_id(Store
, store_id
)).first()
248 return store
.get_pretty_shopping_list()
249 return [f
'Last shopping list store not found.']
251 def get_shopping_list(self
, store_name
) -> List
[str]:
253 with Session(self
.engine
) as session
:
254 store
= session
.exec(_get_with_name(Store
, store_name
)).first()
256 return [f
'Store {store_name} not found.']
257 results
= store
.get_pretty_shopping_list()
258 self
._set
_pref
_store
_id
(store
.id)
261 def remove_item(self
, item_id
):
262 with Session(self
.engine
) as session
:
263 item
= session
.exec(_get_with_id(Item
, item_id
)).first()
267 def remove_store(self
, store_id
):
268 with Session(self
.engine
) as session
:
269 store
= session
.exec(_get_with_id(Store
, store_id
)).first()
270 session
.delete(store
)
273 def get_engine(self
):
274 return create_engine(f
"sqlite:///{self.file}")
277 SQLModel
.metadata
.create_all(self
.engine
)