Skip to content
This repository was archived by the owner on Apr 24, 2025. It is now read-only.

Commit 4683c2c

Browse files
committed
test: added unit test for Item
unit tests for ItemsDB and ExpenseIncomeStats would require more methods
1 parent 803af58 commit 4683c2c

3 files changed

Lines changed: 396 additions & 40 deletions

File tree

projects/Expense-Tracker/expense_income_stats.py

Lines changed: 41 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -9,24 +9,28 @@ def __init__(self, db_path: str, start: str = None, end: str = None):
99
self.start = start
1010
self.end = end
1111

12-
if start is None and end is None:
13-
# Get all Items
14-
self._items = self._items_db.get_all_items()
15-
pass
16-
elif start is None and end is not None:
17-
# Start from the Oldest Item(s) to the given end date string
18-
oldest = min(item.date for item in self._items_db.get_all_items())
19-
self._items = self._items_db.get_items_by_date_range(oldest.strftime("%Y-%m-%d"), end)
20-
elif start is not None and end is None:
21-
# Start from the given start date string to the latest date
22-
latest = max(item.date for item in self._items_db.get_all_items())
23-
self._items = self._items_db.get_items_by_date_range(start, latest.strftime("%Y-%m-%d"))
24-
else:
25-
# Get item from given date range
26-
self._items = self._items_db.get_items_by_date_range(start, end)
12+
# if start is None and end is None:
13+
# # Get all Items
14+
# self._items = self._items_db.get_all_items()
15+
# pass
16+
# elif start is None and end is not None:
17+
# # Start from the Oldest Item(s) to the given end date string
18+
# oldest = min(item.date for item in self._items_db.get_all_items())
19+
# self._items = self._items_db.get_items_by_date_range(oldest.strftime("%Y-%m-%d"), end)
20+
# elif start is not None and end is None:
21+
# # Start from the given start date string to the latest date
22+
# latest = max(item.date for item in self._items_db.get_all_items())
23+
# self._items = self._items_db.get_items_by_date_range(start, latest.strftime("%Y-%m-%d"))
24+
# else:
25+
# # Get item from given date range
26+
# self._items = self._items_db.get_items_by_date_range(start, end)
27+
28+
@property
29+
def items(self):
30+
return self._items_db.get_all_items()
2731

2832
@staticmethod
29-
def _get_stats(items) -> dict:
33+
def get_stats_expense_and_income(items) -> dict:
3034
expense_items = [item.amount for item in items if item.amount < 0]
3135
if len(expense_items) > 0:
3236
expense_stats = {
@@ -49,20 +53,26 @@ def _get_stats(items) -> dict:
4953

5054
return expense_stats | income_stats
5155

52-
def get_stats(self):
53-
return self._get_stats(self._items)
56+
@staticmethod
57+
def get_stats(items) -> dict:
58+
if len(items) < 1:
59+
raise ValueError('The list of items must contain at least one item')
60+
61+
return {
62+
"average": statistics.mean(items),
63+
"max": max(items),
64+
"min": min(items),
65+
}
5466

55-
def get_stats_by_category(self) -> dict:
56-
# The 'root' of category (i.e. it aggregates the subcategories). We do not care about the subcategories.
57-
# Set of Root Categories
58-
# item.get_category_str() for an item without category would return 'Uncategorized'
59-
category_names = {item.category.name if item.category is not None else item.get_category_str() for item in
60-
self._items}
67+
def get_stats_all_items(self):
68+
return self.get_stats_expense_and_income(self.items)
6169

70+
def get_stats_by_category(self) -> dict:
71+
category_names = self._items_db.get_category_names()
6272
out_dict = {}
6373
for category_name in category_names:
64-
items = [item for item in self._items if item.category.name == category_name]
65-
out_dict[category_name] = self._get_stats(items)
74+
items = [item for item in self._items_db.get_items_by_category(category_name)]
75+
out_dict[category_name] = self.get_stats_expense_and_income(items)
6676

6777
return out_dict
6878

@@ -71,12 +81,14 @@ def get_stats_by_category_with_subcategories(self) -> dict:
7181
stats_dict = {}
7282
for category_name in category_names:
7383
# Case: With Category and Without Subcategory
74-
stats_dict[f'{category_name}-NoSubcategory'] = self._get_stats(
84+
stats_dict[f'{category_name}-NoSubcategory'] = self.get_stats_expense_and_income(
7585
self._items_db.get_items_without_subcategory(category_name))
86+
7687
# Case: With Category and With Subcategory
77-
for subcategory in self._items_db.get_subcategory_name(category_name):
88+
for subcategory in self._items_db.get_subcategory_names(category_name):
7889
stats_dict[f'{category_name}-{subcategory}'] = \
79-
self._get_stats(self._items_db.get_items_by_category_and_subcategory(category_name, subcategory))
90+
self.get_stats_expense_and_income(
91+
self._items_db.get_items_by_category_and_subcategory(category_name, subcategory))
8092

8193
return stats_dict
8294

projects/Expense-Tracker/item.py

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -100,48 +100,56 @@ def to_serializable_dict(self) -> dict:
100100
class ItemsDB:
101101
def __init__(self, db_path: str):
102102
self.db_path = db_path
103-
self._db = TinyDB(self.db_path)
104103

105104
def __len__(self):
106-
return len(self._db)
105+
with TinyDB(self.db_path) as db:
106+
return len(db)
107107

108108
def print_db(self):
109109
for item in self.get_all_items():
110110
print(item.to_json_str(indent=4))
111111

112112
def insert_item(self, item: Item) -> None:
113113
data_dict = dict(json.loads(item.to_json_str()))
114-
self._db.insert(data_dict)
114+
with TinyDB(self.db_path) as db:
115+
db.insert(data_dict)
115116

116117
def insert_items(self, items: List[Item]) -> None:
117-
self._db.insert_multiple([dict(json.loads(item.to_json_str())) for item in items])
118+
with TinyDB(self.db_path) as db:
119+
db.insert_multiple([dict(json.loads(item.to_json_str())) for item in items])
118120

119121
def update_items(self, update_dict: dict, query: Query) -> None:
120-
self._db.update(update_dict, query)
122+
with TinyDB(self.db_path) as db:
123+
db.update(update_dict, query)
121124

122125
def upsert_item(self, item: Item) -> None:
123126
dct = item.to_serializable_dict()
124-
self._db.upsert(dct, Query().item_id == str(dct['item_id']))
127+
with TinyDB(self.db_path) as db:
128+
db.upsert(dct, Query().item_id == str(dct['item_id']))
125129

126130
def delete_items(self, cond: Query) -> None:
127-
self._db.remove(cond)
131+
with TinyDB(self.db_path) as db:
132+
db.remove(cond)
128133

129134
def delete_item(self, item: Item) -> None:
130135
self.delete_items(Query().item_id == item.item_id)
131136

132137
def delete_all_items(self):
133-
self._db.truncate()
138+
with TinyDB(self.db_path) as db:
139+
db.truncate()
134140

135141
def get_all_items(self) -> List[Item]:
136-
return [Item.from_json_str(json.dumps(doc)) for doc in self._db.all()]
142+
with TinyDB(self.db_path) as db:
143+
return [Item.from_json_str(json.dumps(doc)) for doc in db.all()]
137144

138145
def get_items_by_date_range(self, start: str, end: str) -> List[Item]:
139146
start_date = datetime.strptime(start, "%Y-%m-%d")
140147
end_date = datetime.strptime(end, "%Y-%m-%d")
141148
return [item for item in self.get_all_items() if start_date <= item.date <= end_date]
142149

143150
def get_items_by_category(self, category_name: str) -> List[Item]:
144-
return [item for item in self.get_all_items() if item.category.name == category_name]
151+
return [item for item in self.get_all_items() if
152+
item.category is not None and item.category.name == category_name]
145153

146154
def get_items_by_category_and_subcategory(self, category_name: str, subcategory_name: str) -> List[Item]:
147155
return [item for item in self.get_all_items() if
@@ -165,8 +173,14 @@ def get_category_names(self) -> Set[str]:
165173
# This excludes the Items with No Category (i.e. item.category is None)
166174
return {item.category.name for item in self.get_all_items() if item.category is not None}
167175

168-
def get_subcategory_name(self, category_name: str) -> Set[str]:
176+
def get_subcategory_names(self, category_name: str) -> Set[str]:
169177
# This excludes items without subcategories
170178
return {item.category.subcategory for item in self.get_all_items() if
171179
item.category is not None and item.category.name == category_name and item.category.subcategory is not
172180
None}
181+
182+
def get_expense_items(self):
183+
return [item for item in self.get_all_items() if item.amount < 0]
184+
185+
def get_income_items(self):
186+
return [item for item in self.get_all_items() if item.amount > 0]

0 commit comments

Comments
 (0)