]>
apps.delaporte.us 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
)
73 zones
[zone
].append(str(item
))
75 results
.append(f
' ### {zone}')
76 for item
in zones
[zone
]:
80 class Store(SQLModel
, table
=True):
81 '''A place to shop at.'''
82 id: int |
None = Field(default
=None, primary_key
=True)
83 name
: str = Field(default
=None, unique
=True)
84 shopping_list
: list["Item"] = Relationship(back_populates
="stores", link_model
=ItemStoreLink
)
85 # purchase_history: list["PurchaseRecord"] = Relationship(back_populates="store")
87 return f
'{self.name} (#{self.id})'
90 class PurchaseRecord(SQLModel
, table
=True):
91 '''Tracks when and where items were purchased.'''
92 id: int |
None = Field(default
=None, primary_key
=True)
93 name
: str = Field(default
=None)
94 price
: Decimal |
None = Field(decimal_places
=2)
95 store
: str |
None = Field(default
=None)
96 purchase_date
: dt |
None = Field(default
=None)
98 date
= self
.purchase_date
.strftime(DATE_FORMAT
)
99 return f
'Bought {self.name} at {self.store} on {date}'
101 def _get_with_id(cls
, item_id
):
102 return select(cls
).where(cls
.id == item_id
)
104 def _get_with_name(cls
, name
):
105 return select(cls
).where(func
.lower(cls
.name
) == func
.lower(name
))
109 def __init__(self
, db_file
=None):
111 db_file
= os
.path
.join(os
.environ
.get('MATRIX_DATA_DIR', ''), f
'{MODULE}.db')
114 logger
.debug(f
'Shop bot is using db file {self.file}')
115 self
.engine
= self
.get_engine()
116 self
.last_store_id
= None
118 def _set_pref_store_id(self
, store_id
):
119 with Session(self
.engine
) as session
:
120 prefs
= session
.exec(select(PreferStore
)).all()
124 pref
.store_id
= store_id
128 def _get_pref_store_id(self
):
129 with Session(self
.engine
) as session
:
130 pref
= session
.exec(select(PreferStore
)).one()
133 def _add_with_name(self
, cls
, name
):
136 with Session(self
.engine
) as session
:
143 # recover by returning the item_id
144 item
= session
.exec(select(Item
).where(Item
.name
== name
)).first()
149 def add_store(self
, name
):
150 store_id
= self
._add
_with
_name
(Store
, name
)
151 self
._set
_pref
_store
_id
(store_id
)
154 def add_item(self
, name
):
155 names
= name
.split(',')
157 item_id
= self
._add
_with
_name
(Item
, name
.strip())
158 store_id
= self
._get
_pref
_store
_id
()
159 self
.assign_store(item_id
, store_id
)
162 def assign_last_store(self
, item_id
):
163 store_id
= self
._get
_pref
_store
_id
()
164 self
.assign_store(item_id
, store_id
)
167 def assign_zone(self
, item_name
, zone_name
):
169 with Session(self
.engine
) as session
:
171 new
= ItemZone(name
=item_name
)
177 pass # Already assigned
180 def assign_store(self
, item_id
, store_id
):
181 self
._set
_pref
_store
_id
(store_id
)
183 with Session(self
.engine
) as session
:
184 store
= session
.exec(_get_with_id(Store
, store_id
)).first()
185 item
= session
.exec(_get_with_id(Item
, item_id
)).first()
187 item
.stores
.append(store
)
190 pass # Already assigned
193 def list_purchases(self
, store_name
):
194 with Session(self
.engine
) as session
:
195 cmd
= select(PurchaseRecord
).where(func
.lower(PurchaseRecord
.store
) == func
.lower(store_name
))
196 records
= session
.exec(cmd
).all()
199 def purchase_items(self
, item_ids
):
201 with Session(self
.engine
) as session
:
202 # Mark items as purchased
203 for item_id
in item_ids
.split(' '):
204 item
= session
.exec(_get_with_id(Item
, item_id
)).first()
206 item
.purchase_date
= dt
.today()
208 # Track purchase history (locally only!)
209 # to generate recommended shopping lists
210 record
= PurchaseRecord()
211 record
.name
= item
.name
212 record
.price
= item
.price
213 record
.purchase_date
= item
.purchase_date
215 shopping_at
= self
._get
_pref
_store
_id
() # We bought it here
216 cmd
= select(Store
).where(Store
.id == shopping_at
)
217 store
= session
.exec(cmd
).one()
218 record
.store
= store
.name
221 results
.append(str(record
))
222 # results.append(str(item))
228 def clear_list(self
, store_name
, clear_all
=False):
230 with Session(self
.engine
) as session
:
231 store
= session
.exec(_get_with_name(Store
, store_name
)).first()
233 return [f
'Store {store_name} not found.']
235 for item
in store
.shopping_list
:
236 if clear_all
or item
.purchase_date
:
240 results
= _get_pretty_shopping_list(store
.id, session
)
243 def get_stores(self
):
244 with Session(self
.engine
) as session
:
246 return session
.exec(cmd
).all()
248 def get_unsorted_list(self
):
251 with Session(self
.engine
) as session
:
252 cmd
= select(Item
).where(Item
.stores
== None)
253 items
= session
.exec(cmd
).all()
256 return [f
'No unsorted items!']
259 def get_last_shopping_list(self
) -> List
[str]:
260 store_id
= self
._get
_pref
_store
_id
()
261 with Session(self
.engine
) as session
:
262 store
= session
.exec(_get_with_id(Store
, store_id
)).first()
264 return _get_pretty_shopping_list(store
.id, session
)
265 return [f
'Last shopping list store not found.']
267 def get_shopping_list(self
, store_name
) -> List
[str]:
269 with Session(self
.engine
) as session
:
270 store
= session
.exec(_get_with_name(Store
, store_name
)).first()
272 return [f
'Store {store_name} not found.']
273 results
= _get_pretty_shopping_list(store
.id, session
)
274 self
._set
_pref
_store
_id
(store
.id)
277 def remove_item(self
, item_id
):
278 with Session(self
.engine
) as session
:
279 item
= session
.exec(_get_with_id(Item
, item_id
)).first()
283 def remove_store(self
, store_id
):
284 with Session(self
.engine
) as session
:
285 store
= session
.exec(_get_with_id(Store
, store_id
)).first()
286 session
.delete(store
)
289 def get_engine(self
):
290 return create_engine(f
"sqlite:///{self.file}")
293 SQLModel
.metadata
.create_all(self
.engine
)