]>
apps.delaporte.us Git - matrix-shop-bot.git/blob - matrix_shop_bot/db.py
6cacc33b5a92a86e0f44a8bcac02d11c8c5a8e5e
3 from datetime
import datetime
as dt
4 from datetime
import timedelta
5 from decimal
import Decimal
6 from typing
import List
8 from sqlmodel
import Field
, Session
, SQLModel
, create_engine
, select
, text
, Relationship
, func
10 from sqlite3
import IntegrityError
14 DATE_FORMAT
= '%Y-%m-%d'
17 return dt
.now().strftime(DATE_FORMAT
)
20 logger
= logging
.getLogger(__name__
)
22 class ItemStoreLink(SQLModel
, table
=True):
23 item_id
: int |
None = Field(default
=None, foreign_key
="item.id", primary_key
=True)
24 store_id
: int |
None = Field(default
=None, foreign_key
="store.id", primary_key
=True)
27 class Item(SQLModel
, table
=True):
28 '''An item to shop for.'''
29 id: int |
None = Field(default
=None, primary_key
=True)
30 name
: str = Field(default
=None, unique
=True)
31 price
: Decimal |
None = Field(decimal_places
=2)
32 stores
: list["Store"] = Relationship(back_populates
="shopping_list", link_model
=ItemStoreLink
)
33 purchase_date
: dt |
None = Field(default
=None)
35 if(self
.purchase_date
):
36 return f
'- [x] {self.name} (#{self.id})'
38 return f
'- [ ] {self.name} (#{self.id})'
40 class ItemZone(SQLModel
, table
=True):
41 '''Where an item is usually found.
43 Matched only by loose text matching, since items come and go, over time.
45 id: int |
None = Field(default
=None, primary_key
=True)
46 name
: str = Field(default
=None, unique
=True) # milk
47 zone
: str = Field(default
=None, unique
=False) # dairy section
49 class PreferStore(SQLModel
, table
=True):
50 '''Tracks which store we are shopping at, at the moment.'''
51 store_id
: int |
None = Field(default
=None, foreign_key
="store.id", primary_key
=True)
53 def _get_item_zone(item_name
, session
) -> str:
54 zone
= session
.exec(select(ItemZone
).where(func
.lower(ItemZone
.name
) == func
.lower(item_name
))).first()
57 return 'No Zone Assigned'
59 def _get_pretty_shopping_list(store_id
, session
) -> List
[str]:
60 store
= session
.exec(_get_with_id(Store
, store_id
)).first()
62 results
.append(f
' ## {store.name}')
63 items
= store
.shopping_list
65 results
.append(f
'-- Nothing to shop for at {store.name} --')
70 zone
= str(_get_item_zone(item
.name
, session
)).title() # Title Case
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 recent
= dt
.now() - timedelta(days
=90)
196 cmd
= select(PurchaseRecord
).where(func
.lower(PurchaseRecord
.store
) == func
.lower(store_name
)).where(PurchaseRecord
.purchase_date
> recent
)
197 records
= session
.exec(cmd
).all()
200 bought
.add(item
.name
)
203 def purchase_items(self
, item_ids
):
205 with Session(self
.engine
) as session
:
206 # Mark items as purchased
207 for item_id
in item_ids
.split(' '):
208 item
= session
.exec(_get_with_id(Item
, item_id
)).first()
210 item
.purchase_date
= dt
.today()
212 # Track purchase history (locally only!)
213 # to generate recommended shopping lists
214 record
= PurchaseRecord()
215 record
.name
= item
.name
216 record
.price
= item
.price
217 record
.purchase_date
= item
.purchase_date
219 shopping_at
= self
._get
_pref
_store
_id
() # We bought it here
220 cmd
= select(Store
).where(Store
.id == shopping_at
)
221 store
= session
.exec(cmd
).one()
222 record
.store
= store
.name
225 results
.append(str(record
))
226 # results.append(str(item))
232 def clear_list(self
, store_name
, clear_all
=False):
234 with Session(self
.engine
) as session
:
235 store
= session
.exec(_get_with_name(Store
, store_name
)).first()
237 return [f
'Store {store_name} not found.']
239 for item
in store
.shopping_list
:
240 if clear_all
or item
.purchase_date
:
244 results
= _get_pretty_shopping_list(store
.id, session
)
247 def get_stores(self
):
248 with Session(self
.engine
) as session
:
250 return session
.exec(cmd
).all()
252 def get_unsorted_list(self
):
255 with Session(self
.engine
) as session
:
256 cmd
= select(Item
).where(Item
.stores
== None)
257 items
= session
.exec(cmd
).all()
260 return [f
'No unsorted items!']
263 def get_last_shopping_list(self
) -> List
[str]:
264 store_id
= self
._get
_pref
_store
_id
()
265 with Session(self
.engine
) as session
:
266 store
= session
.exec(_get_with_id(Store
, store_id
)).first()
268 return _get_pretty_shopping_list(store
.id, session
)
269 return [f
'Last shopping list store not found.']
271 def get_shopping_list(self
, store_name
) -> List
[str]:
273 with Session(self
.engine
) as session
:
274 store
= session
.exec(_get_with_name(Store
, store_name
)).first()
276 return [f
'Store {store_name} not found.']
277 results
= _get_pretty_shopping_list(store
.id, session
)
278 self
._set
_pref
_store
_id
(store
.id)
281 def remove_item(self
, item_id
):
282 with Session(self
.engine
) as session
:
283 item
= session
.exec(_get_with_id(Item
, item_id
)).first()
287 def remove_store(self
, store_id
):
288 with Session(self
.engine
) as session
:
289 store
= session
.exec(_get_with_id(Store
, store_id
)).first()
290 session
.delete(store
)
293 def get_engine(self
):
294 return create_engine(f
"sqlite:///{self.file}")
297 SQLModel
.metadata
.create_all(self
.engine
)