@opslane/claude-code-game 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +59 -0
- package/dist/cli.js.map +1 -0
- package/dist/routes/auth.d.ts +1 -0
- package/dist/routes/auth.js +123 -0
- package/dist/routes/auth.js.map +1 -0
- package/dist/routes/levels.d.ts +44 -0
- package/dist/routes/levels.js +78 -0
- package/dist/routes/levels.js.map +1 -0
- package/dist/routes/sessions.d.ts +17 -0
- package/dist/routes/sessions.js +303 -0
- package/dist/routes/sessions.js.map +1 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +58 -0
- package/dist/server.js.map +1 -0
- package/dist/terminal.d.ts +6 -0
- package/dist/terminal.js +23 -0
- package/dist/terminal.js.map +1 -0
- package/dist/verification.d.ts +31 -0
- package/dist/verification.js +239 -0
- package/dist/verification.js.map +1 -0
- package/frontend/assets/index-CNVEnbfs.css +1 -0
- package/frontend/assets/index-D70xl9zu.js +27 -0
- package/frontend/index.html +14 -0
- package/frontend/vite.svg +1 -0
- package/keys/v1.pem +9 -0
- package/levels/01-context-is-everything/exercise/README.md +152 -0
- package/levels/01-context-is-everything/exercise/data/expenses.db +0 -0
- package/levels/01-context-is-everything/exercise/database.py +171 -0
- package/levels/01-context-is-everything/exercise/docs/FIRECRAWL_QUICKSTART.md +212 -0
- package/levels/01-context-is-everything/exercise/historical_data/expenses_2024_01.json +2306 -0
- package/levels/01-context-is-everything/exercise/historical_data/expenses_2024_02.json +2394 -0
- package/levels/01-context-is-everything/exercise/historical_data/expenses_2024_03.json +2251 -0
- package/levels/01-context-is-everything/exercise/historical_data/expenses_2024_04.json +1987 -0
- package/levels/01-context-is-everything/exercise/historical_data/expenses_2024_05.json +2229 -0
- package/levels/01-context-is-everything/exercise/main.py +97 -0
- package/levels/01-context-is-everything/exercise/models.py +141 -0
- package/levels/01-context-is-everything/exercise/pyproject.toml +52 -0
- package/levels/01-context-is-everything/exercise/reports.py +138 -0
- package/levels/01-context-is-everything/exercise/seed_data.py +91 -0
- package/levels/01-context-is-everything/exercise/tests/__init__.py +1 -0
- package/levels/01-context-is-everything/exercise/tests/conftest.py +69 -0
- package/levels/01-context-is-everything/exercise/tests/test_database.py +244 -0
- package/levels/01-context-is-everything/exercise/tests/test_models.py +240 -0
- package/levels/01-context-is-everything/exercise/tests/test_reports.py +190 -0
- package/levels/01-context-is-everything/exercise/utils.py +163 -0
- package/levels/01-context-is-everything/lesson.yaml +82 -0
- package/levels/02-claude-md/exercise/README.md +152 -0
- package/levels/02-claude-md/exercise/data/expenses.db +0 -0
- package/levels/02-claude-md/exercise/database.py +171 -0
- package/levels/02-claude-md/exercise/main.py +97 -0
- package/levels/02-claude-md/exercise/models.py +141 -0
- package/levels/02-claude-md/exercise/pyproject.toml +52 -0
- package/levels/02-claude-md/exercise/reports.py +138 -0
- package/levels/02-claude-md/exercise/seed_data.py +91 -0
- package/levels/02-claude-md/exercise/tests/__init__.py +1 -0
- package/levels/02-claude-md/exercise/tests/conftest.py +69 -0
- package/levels/02-claude-md/exercise/tests/test_database.py +244 -0
- package/levels/02-claude-md/exercise/tests/test_models.py +240 -0
- package/levels/02-claude-md/exercise/tests/test_reports.py +190 -0
- package/levels/02-claude-md/exercise/utils.py +163 -0
- package/levels/02-claude-md/lesson.yaml +60 -0
- package/levels/03-read-edit-verify/exercise/CLAUDE.md +15 -0
- package/levels/03-read-edit-verify/exercise/README.md +152 -0
- package/levels/03-read-edit-verify/exercise/data/expenses.db +0 -0
- package/levels/03-read-edit-verify/exercise/database.py +171 -0
- package/levels/03-read-edit-verify/exercise/main.py +97 -0
- package/levels/03-read-edit-verify/exercise/models.py +141 -0
- package/levels/03-read-edit-verify/exercise/pyproject.toml +52 -0
- package/levels/03-read-edit-verify/exercise/reports.py +138 -0
- package/levels/03-read-edit-verify/exercise/seed_data.py +91 -0
- package/levels/03-read-edit-verify/exercise/tests/__init__.py +1 -0
- package/levels/03-read-edit-verify/exercise/tests/conftest.py +69 -0
- package/levels/03-read-edit-verify/exercise/tests/test_database.py +244 -0
- package/levels/03-read-edit-verify/exercise/tests/test_models.py +240 -0
- package/levels/03-read-edit-verify/exercise/tests/test_reports.py +190 -0
- package/levels/03-read-edit-verify/exercise/utils.py +163 -0
- package/levels/03-read-edit-verify/lesson.yaml +60 -0
- package/levels/04-planning-mode/exercise/README.md +152 -0
- package/levels/04-planning-mode/exercise/data/expenses.db +0 -0
- package/levels/04-planning-mode/exercise/database.py +171 -0
- package/levels/04-planning-mode/exercise/main.py +97 -0
- package/levels/04-planning-mode/exercise/models.py +116 -0
- package/levels/04-planning-mode/exercise/pyproject.toml +52 -0
- package/levels/04-planning-mode/exercise/reports.py +138 -0
- package/levels/04-planning-mode/exercise/seed_data.py +91 -0
- package/levels/04-planning-mode/exercise/tests/__init__.py +1 -0
- package/levels/04-planning-mode/exercise/tests/conftest.py +69 -0
- package/levels/04-planning-mode/exercise/tests/test_database.py +244 -0
- package/levels/04-planning-mode/exercise/tests/test_expenses.db +0 -0
- package/levels/04-planning-mode/exercise/tests/test_models.py +240 -0
- package/levels/04-planning-mode/exercise/tests/test_reports.py +190 -0
- package/levels/04-planning-mode/exercise/utils.py +163 -0
- package/levels/04-planning-mode/lesson.yaml +53 -0
- package/levels/05-spec-driven/exercise/README.md +152 -0
- package/levels/05-spec-driven/exercise/data/expenses.db +0 -0
- package/levels/05-spec-driven/exercise/database.py +171 -0
- package/levels/05-spec-driven/exercise/main.py +97 -0
- package/levels/05-spec-driven/exercise/models.py +116 -0
- package/levels/05-spec-driven/exercise/pyproject.toml +52 -0
- package/levels/05-spec-driven/exercise/reports.py +138 -0
- package/levels/05-spec-driven/exercise/seed_data.py +91 -0
- package/levels/05-spec-driven/exercise/tests/__init__.py +1 -0
- package/levels/05-spec-driven/exercise/tests/conftest.py +69 -0
- package/levels/05-spec-driven/exercise/tests/test_database.py +244 -0
- package/levels/05-spec-driven/exercise/tests/test_expenses.db +0 -0
- package/levels/05-spec-driven/exercise/tests/test_models.py +240 -0
- package/levels/05-spec-driven/exercise/tests/test_reports.py +190 -0
- package/levels/05-spec-driven/exercise/utils.py +163 -0
- package/levels/05-spec-driven/lesson.yaml +53 -0
- package/levels/06-sub-agents/exercise/README.md +152 -0
- package/levels/06-sub-agents/exercise/data/expenses.db +0 -0
- package/levels/06-sub-agents/exercise/database.py +171 -0
- package/levels/06-sub-agents/exercise/main.py +97 -0
- package/levels/06-sub-agents/exercise/models.py +116 -0
- package/levels/06-sub-agents/exercise/pyproject.toml +52 -0
- package/levels/06-sub-agents/exercise/reports.py +63 -0
- package/levels/06-sub-agents/exercise/seed_data.py +91 -0
- package/levels/06-sub-agents/exercise/tests/__init__.py +1 -0
- package/levels/06-sub-agents/exercise/tests/conftest.py +69 -0
- package/levels/06-sub-agents/exercise/tests/test_database.py +244 -0
- package/levels/06-sub-agents/exercise/tests/test_models.py +240 -0
- package/levels/06-sub-agents/exercise/tests/test_reports.py +190 -0
- package/levels/06-sub-agents/exercise/utils.py +163 -0
- package/levels/06-sub-agents/lesson.yaml +49 -0
- package/levels/07-skills/exercise/README.md +152 -0
- package/levels/07-skills/exercise/data/expenses.db +0 -0
- package/levels/07-skills/exercise/database.py +171 -0
- package/levels/07-skills/exercise/main.py +97 -0
- package/levels/07-skills/exercise/models.py +116 -0
- package/levels/07-skills/exercise/pyproject.toml +52 -0
- package/levels/07-skills/exercise/reports.py +63 -0
- package/levels/07-skills/exercise/seed_data.py +91 -0
- package/levels/07-skills/exercise/tests/__init__.py +1 -0
- package/levels/07-skills/exercise/tests/conftest.py +69 -0
- package/levels/07-skills/exercise/tests/test_database.py +244 -0
- package/levels/07-skills/exercise/tests/test_models.py +240 -0
- package/levels/07-skills/exercise/tests/test_reports.py +190 -0
- package/levels/07-skills/exercise/utils.py +163 -0
- package/levels/07-skills/lesson.yaml +49 -0
- package/levels/08-mcp-servers/exercise/README.md +152 -0
- package/levels/08-mcp-servers/exercise/data/expenses.db +0 -0
- package/levels/08-mcp-servers/exercise/database.py +171 -0
- package/levels/08-mcp-servers/exercise/main.py +97 -0
- package/levels/08-mcp-servers/exercise/models.py +116 -0
- package/levels/08-mcp-servers/exercise/pyproject.toml +52 -0
- package/levels/08-mcp-servers/exercise/reports.py +63 -0
- package/levels/08-mcp-servers/exercise/seed_data.py +91 -0
- package/levels/08-mcp-servers/exercise/tests/__init__.py +1 -0
- package/levels/08-mcp-servers/exercise/tests/conftest.py +69 -0
- package/levels/08-mcp-servers/exercise/tests/test_database.py +244 -0
- package/levels/08-mcp-servers/exercise/tests/test_models.py +240 -0
- package/levels/08-mcp-servers/exercise/tests/test_reports.py +190 -0
- package/levels/08-mcp-servers/exercise/utils.py +163 -0
- package/levels/08-mcp-servers/lesson.yaml +59 -0
- package/levels/09-plugins/exercise/README.md +152 -0
- package/levels/09-plugins/exercise/data/expenses.db +0 -0
- package/levels/09-plugins/exercise/database.py +171 -0
- package/levels/09-plugins/exercise/main.py +97 -0
- package/levels/09-plugins/exercise/models.py +116 -0
- package/levels/09-plugins/exercise/pyproject.toml +52 -0
- package/levels/09-plugins/exercise/reports.py +63 -0
- package/levels/09-plugins/exercise/seed_data.py +91 -0
- package/levels/09-plugins/exercise/tests/__init__.py +1 -0
- package/levels/09-plugins/exercise/tests/conftest.py +69 -0
- package/levels/09-plugins/exercise/tests/test_database.py +244 -0
- package/levels/09-plugins/exercise/tests/test_models.py +240 -0
- package/levels/09-plugins/exercise/tests/test_reports.py +190 -0
- package/levels/09-plugins/exercise/utils.py +163 -0
- package/levels/09-plugins/lesson.yaml +51 -0
- package/levels/10-hooks/exercise/README.md +152 -0
- package/levels/10-hooks/exercise/data/expenses.db +0 -0
- package/levels/10-hooks/exercise/database.py +171 -0
- package/levels/10-hooks/exercise/main.py +97 -0
- package/levels/10-hooks/exercise/models.py +116 -0
- package/levels/10-hooks/exercise/pyproject.toml +52 -0
- package/levels/10-hooks/exercise/reports.py +63 -0
- package/levels/10-hooks/exercise/seed_data.py +91 -0
- package/levels/10-hooks/exercise/tests/__init__.py +1 -0
- package/levels/10-hooks/exercise/tests/conftest.py +69 -0
- package/levels/10-hooks/exercise/tests/test_database.py +244 -0
- package/levels/10-hooks/exercise/tests/test_models.py +240 -0
- package/levels/10-hooks/exercise/tests/test_reports.py +190 -0
- package/levels/10-hooks/exercise/utils.py +163 -0
- package/levels/10-hooks/lesson.yaml +58 -0
- package/levels/11-worktrees/exercise/README.md +152 -0
- package/levels/11-worktrees/exercise/data/expenses.db +0 -0
- package/levels/11-worktrees/exercise/database.py +171 -0
- package/levels/11-worktrees/exercise/main.py +97 -0
- package/levels/11-worktrees/exercise/models.py +116 -0
- package/levels/11-worktrees/exercise/pyproject.toml +52 -0
- package/levels/11-worktrees/exercise/reports.py +63 -0
- package/levels/11-worktrees/exercise/seed_data.py +91 -0
- package/levels/11-worktrees/exercise/tests/__init__.py +1 -0
- package/levels/11-worktrees/exercise/tests/conftest.py +69 -0
- package/levels/11-worktrees/exercise/tests/test_database.py +244 -0
- package/levels/11-worktrees/exercise/tests/test_models.py +240 -0
- package/levels/11-worktrees/exercise/tests/test_reports.py +190 -0
- package/levels/11-worktrees/exercise/utils.py +163 -0
- package/levels/11-worktrees/lesson.yaml +68 -0
- package/package.json +38 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Expense data models and core operations."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import Optional
|
|
6
|
+
import uuid
|
|
7
|
+
from database import save_expense, load_expenses, remove_expense, get_expense_by_id
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class Expense:
|
|
12
|
+
"""Represents a single expense entry."""
|
|
13
|
+
id: str
|
|
14
|
+
amount: float
|
|
15
|
+
category: str
|
|
16
|
+
description: str
|
|
17
|
+
date: datetime
|
|
18
|
+
recurring: bool = False
|
|
19
|
+
recurring_frequency: Optional[str] = None # 'daily', 'weekly', 'monthly'
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def generate_id() -> str:
|
|
23
|
+
"""Generate a unique expense ID."""
|
|
24
|
+
return str(uuid.uuid4())[:8]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def add_expense(
|
|
28
|
+
amount: float,
|
|
29
|
+
category: str,
|
|
30
|
+
description: str,
|
|
31
|
+
date: Optional[datetime] = None
|
|
32
|
+
) -> Expense:
|
|
33
|
+
"""Add a new expense with validation."""
|
|
34
|
+
if amount <= 0:
|
|
35
|
+
raise ValueError("Amount must be greater than 0")
|
|
36
|
+
|
|
37
|
+
if not category or not category.strip():
|
|
38
|
+
raise ValueError("Category cannot be empty")
|
|
39
|
+
|
|
40
|
+
expense = Expense(
|
|
41
|
+
id=generate_id(),
|
|
42
|
+
amount=amount,
|
|
43
|
+
category=category.strip(),
|
|
44
|
+
description=description,
|
|
45
|
+
date=date or datetime.now()
|
|
46
|
+
)
|
|
47
|
+
save_expense(expense)
|
|
48
|
+
return expense
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def list_expenses(
|
|
52
|
+
category: Optional[str] = None,
|
|
53
|
+
month: Optional[str] = None,
|
|
54
|
+
limit: int = 100
|
|
55
|
+
) -> list[Expense]:
|
|
56
|
+
"""List expenses with optional filters."""
|
|
57
|
+
expenses = load_expenses()
|
|
58
|
+
|
|
59
|
+
if category:
|
|
60
|
+
category_lower = category.lower()
|
|
61
|
+
expenses = [e for e in expenses if e.category.lower() == category_lower]
|
|
62
|
+
|
|
63
|
+
if month:
|
|
64
|
+
expenses = [e for e in expenses if e.date.strftime('%Y-%m') == month]
|
|
65
|
+
|
|
66
|
+
expenses.sort(key=lambda x: x.date, reverse=True)
|
|
67
|
+
return expenses[:limit]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def get_expense(expense_id: str) -> Optional[Expense]:
|
|
71
|
+
"""Get a single expense by ID.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
expense_id: The unique expense ID
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
The Expense if found, None otherwise
|
|
78
|
+
"""
|
|
79
|
+
return get_expense_by_id(expense_id)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def delete_expense(expense_id: str) -> bool:
|
|
83
|
+
"""Delete an expense by ID.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
expense_id: The unique expense ID
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
True if deleted, False if not found
|
|
90
|
+
"""
|
|
91
|
+
return remove_expense(expense_id)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def get_total_by_category(category: str) -> float:
|
|
95
|
+
"""Get total spent in a category.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
category: The category name
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
Total amount spent in that category
|
|
102
|
+
|
|
103
|
+
Note: Also case-sensitive - shares bug with list_expenses.
|
|
104
|
+
"""
|
|
105
|
+
expenses = list_expenses(category=category)
|
|
106
|
+
return sum(e.amount for e in expenses)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def get_categories() -> list[str]:
|
|
110
|
+
"""Get all unique categories from expenses.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
List of category names
|
|
114
|
+
"""
|
|
115
|
+
expenses = load_expenses()
|
|
116
|
+
return list(set(e.category for e in expenses))
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "expense-tracker"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "A simple CLI expense tracker for the Claude Code course"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
license = { text = "MIT" }
|
|
8
|
+
|
|
9
|
+
# No runtime dependencies - uses only stdlib
|
|
10
|
+
dependencies = []
|
|
11
|
+
|
|
12
|
+
[project.optional-dependencies]
|
|
13
|
+
dev = [
|
|
14
|
+
"pytest>=7.0",
|
|
15
|
+
"ruff>=0.1.0",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.scripts]
|
|
19
|
+
expense = "main:main"
|
|
20
|
+
|
|
21
|
+
[tool.pytest.ini_options]
|
|
22
|
+
testpaths = ["tests"]
|
|
23
|
+
python_files = ["test_*.py"]
|
|
24
|
+
python_functions = ["test_*"]
|
|
25
|
+
addopts = "-v --tb=short"
|
|
26
|
+
filterwarnings = [
|
|
27
|
+
"ignore::DeprecationWarning",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[tool.ruff]
|
|
31
|
+
line-length = 100
|
|
32
|
+
target-version = "py310"
|
|
33
|
+
|
|
34
|
+
[tool.ruff.lint]
|
|
35
|
+
select = [
|
|
36
|
+
"E", # pycodestyle errors
|
|
37
|
+
"F", # pyflakes
|
|
38
|
+
"I", # isort
|
|
39
|
+
"N", # pep8-naming
|
|
40
|
+
"W", # pycodestyle warnings
|
|
41
|
+
"UP", # pyupgrade
|
|
42
|
+
]
|
|
43
|
+
ignore = [
|
|
44
|
+
"E501", # Line too long (handled by formatter)
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
[tool.ruff.lint.isort]
|
|
48
|
+
known-first-party = ["models", "database", "utils", "reports"]
|
|
49
|
+
|
|
50
|
+
[build-system]
|
|
51
|
+
requires = ["setuptools>=61.0"]
|
|
52
|
+
build-backend = "setuptools.build_meta"
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""Report generation for expense tracker.
|
|
2
|
+
|
|
3
|
+
This module generates expense reports with summaries and breakdowns.
|
|
4
|
+
|
|
5
|
+
TODO: This module is incomplete. Students implement it in Lesson 4.
|
|
6
|
+
|
|
7
|
+
Features to implement:
|
|
8
|
+
- Monthly expense report
|
|
9
|
+
- Category breakdown
|
|
10
|
+
- Top expenses list
|
|
11
|
+
- Export to markdown file
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Optional, TYPE_CHECKING
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from models import Expense
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def generate_monthly_report(month: Optional[str] = None) -> str:
|
|
23
|
+
"""Generate a monthly expense report.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
month: Month in YYYY-MM format. Defaults to current month.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
Formatted report as a string.
|
|
30
|
+
|
|
31
|
+
The report should include:
|
|
32
|
+
- Total spent
|
|
33
|
+
- Number of expenses
|
|
34
|
+
- Breakdown by category with percentages
|
|
35
|
+
- Top 5 expenses
|
|
36
|
+
|
|
37
|
+
TODO (Lesson 4): Implement this function.
|
|
38
|
+
"""
|
|
39
|
+
raise NotImplementedError(
|
|
40
|
+
"Implement this in Lesson 4! "
|
|
41
|
+
"The function should return a formatted report string."
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_category_breakdown(expenses: list["Expense"]) -> dict[str, float]:
|
|
46
|
+
"""Get spending breakdown by category.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
expenses: List of Expense objects
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Dictionary mapping category name to total amount
|
|
53
|
+
|
|
54
|
+
Example:
|
|
55
|
+
>>> expenses = [Expense(..., category="Food", amount=50), ...]
|
|
56
|
+
>>> get_category_breakdown(expenses)
|
|
57
|
+
{'Food': 75.0, 'Transport': 30.0}
|
|
58
|
+
|
|
59
|
+
TODO (Lesson 4): Implement this function.
|
|
60
|
+
"""
|
|
61
|
+
raise NotImplementedError(
|
|
62
|
+
"Implement this in Lesson 4! "
|
|
63
|
+
"Sum amounts by category and return as dict."
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def get_top_expenses(expenses: list["Expense"], n: int = 5) -> list["Expense"]:
|
|
68
|
+
"""Get the top N expenses by amount.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
expenses: List of Expense objects
|
|
72
|
+
n: Number of top expenses to return (default 5)
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
List of the N highest expenses, sorted by amount descending
|
|
76
|
+
|
|
77
|
+
TODO (Lesson 4): Implement this function.
|
|
78
|
+
"""
|
|
79
|
+
raise NotImplementedError(
|
|
80
|
+
"Implement this in Lesson 4! "
|
|
81
|
+
"Sort by amount and return top N."
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def save_report(report: str, filename: str) -> Path:
|
|
86
|
+
"""Save report to a file in the reports directory.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
report: The report content as a string
|
|
90
|
+
filename: Name of the file (e.g., '2026-01.md')
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Path to the saved file
|
|
94
|
+
|
|
95
|
+
Creates the reports/ directory if it doesn't exist.
|
|
96
|
+
|
|
97
|
+
TODO (Lesson 4): Implement this function.
|
|
98
|
+
"""
|
|
99
|
+
raise NotImplementedError(
|
|
100
|
+
"Implement this in Lesson 4! "
|
|
101
|
+
"Save to reports/{filename} and return the path."
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# Reference implementation (for instructor use)
|
|
106
|
+
# Students should implement similar logic:
|
|
107
|
+
#
|
|
108
|
+
# def generate_monthly_report(month: Optional[str] = None) -> str:
|
|
109
|
+
# from models import list_expenses
|
|
110
|
+
# from utils import format_currency
|
|
111
|
+
#
|
|
112
|
+
# if month is None:
|
|
113
|
+
# month = datetime.now().strftime("%Y-%m")
|
|
114
|
+
#
|
|
115
|
+
# expenses = list_expenses(month=month)
|
|
116
|
+
#
|
|
117
|
+
# if not expenses:
|
|
118
|
+
# return f"# Expense Report: {month}\n\nNo expenses found."
|
|
119
|
+
#
|
|
120
|
+
# total = sum(e.amount for e in expenses)
|
|
121
|
+
# breakdown = get_category_breakdown(expenses)
|
|
122
|
+
# top = get_top_expenses(expenses, 5)
|
|
123
|
+
#
|
|
124
|
+
# report = f"# Expense Report: {month}\n\n"
|
|
125
|
+
# report += f"## Summary\n"
|
|
126
|
+
# report += f"- **Total Spent:** {format_currency(total)}\n"
|
|
127
|
+
# report += f"- **Number of Expenses:** {len(expenses)}\n\n"
|
|
128
|
+
#
|
|
129
|
+
# report += "## By Category\n"
|
|
130
|
+
# for cat, amount in sorted(breakdown.items(), key=lambda x: x[1], reverse=True):
|
|
131
|
+
# pct = (amount / total) * 100
|
|
132
|
+
# report += f"- {cat}: {format_currency(amount)} ({pct:.1f}%)\n"
|
|
133
|
+
#
|
|
134
|
+
# report += "\n## Top 5 Expenses\n"
|
|
135
|
+
# for i, exp in enumerate(top, 1):
|
|
136
|
+
# report += f"{i}. {format_currency(exp.amount)} - {exp.description} ({exp.category})\n"
|
|
137
|
+
#
|
|
138
|
+
# return report
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Seed the database with sample expenses for learning.
|
|
3
|
+
|
|
4
|
+
Run this script to populate the database with example data:
|
|
5
|
+
python seed_data.py
|
|
6
|
+
|
|
7
|
+
This creates a realistic set of expenses for students to work with.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from database import init_db, clear_all_expenses
|
|
12
|
+
from models import add_expense
|
|
13
|
+
|
|
14
|
+
# Sample expenses for the course
|
|
15
|
+
# Note: One entry has lowercase "food" to trigger the case-sensitivity bug
|
|
16
|
+
SAMPLE_EXPENSES = [
|
|
17
|
+
# January 2026 - Primary month for exercises
|
|
18
|
+
(45.50, "Food", "Grocery shopping at Whole Foods", "2026-01-15"),
|
|
19
|
+
(12.00, "Transport", "Uber to airport", "2026-01-14"),
|
|
20
|
+
(89.99, "Shopping", "Running shoes from Nike", "2026-01-10"),
|
|
21
|
+
(150.00, "Bills", "Electric bill", "2026-01-01"),
|
|
22
|
+
(8.50, "food", "Coffee at Starbucks", "2026-01-20"), # lowercase - triggers bug!
|
|
23
|
+
(35.00, "Entertainment", "Movie tickets for two", "2026-01-18"),
|
|
24
|
+
(22.00, "Food", "Lunch with team", "2026-01-19"),
|
|
25
|
+
(65.00, "Health", "Gym membership monthly", "2026-01-01"),
|
|
26
|
+
(15.75, "Food", "Thai takeout dinner", "2026-01-17"),
|
|
27
|
+
(42.00, "Transport", "Weekly metro pass", "2026-01-13"),
|
|
28
|
+
(28.99, "Entertainment", "Netflix + Spotify", "2026-01-05"),
|
|
29
|
+
(120.00, "Shopping", "Winter jacket on sale", "2026-01-08"),
|
|
30
|
+
|
|
31
|
+
# December 2025 - For month filtering tests
|
|
32
|
+
(200.00, "Shopping", "Holiday gifts for family", "2025-12-20"),
|
|
33
|
+
(55.00, "Food", "Holiday dinner groceries", "2025-12-25"),
|
|
34
|
+
(75.00, "Entertainment", "Concert tickets", "2025-12-15"),
|
|
35
|
+
(30.00, "Transport", "Airport parking", "2025-12-23"),
|
|
36
|
+
|
|
37
|
+
# November 2025 - Additional historical data
|
|
38
|
+
(95.00, "Bills", "Internet bill", "2025-11-15"),
|
|
39
|
+
(180.00, "Health", "Doctor visit copay", "2025-11-10"),
|
|
40
|
+
(45.00, "Food", "Birthday dinner", "2025-11-22"),
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def seed_database(verbose: bool = True) -> int:
|
|
45
|
+
"""Initialize and seed the database with sample data.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
verbose: Whether to print progress messages
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Number of expenses added
|
|
52
|
+
"""
|
|
53
|
+
if verbose:
|
|
54
|
+
print("Initializing database...")
|
|
55
|
+
init_db()
|
|
56
|
+
|
|
57
|
+
if verbose:
|
|
58
|
+
print("Clearing existing data...")
|
|
59
|
+
clear_all_expenses()
|
|
60
|
+
|
|
61
|
+
if verbose:
|
|
62
|
+
print("Adding sample expenses...")
|
|
63
|
+
|
|
64
|
+
count = 0
|
|
65
|
+
for amount, category, desc, date_str in SAMPLE_EXPENSES:
|
|
66
|
+
date = datetime.strptime(date_str, "%Y-%m-%d")
|
|
67
|
+
expense = add_expense(amount, category, desc, date)
|
|
68
|
+
count += 1
|
|
69
|
+
if verbose:
|
|
70
|
+
print(f" Added: {expense.id} - ${amount:.2f} - {desc[:40]}")
|
|
71
|
+
|
|
72
|
+
if verbose:
|
|
73
|
+
print(f"\n{'='*50}")
|
|
74
|
+
print(f"Seeded {count} expenses successfully!")
|
|
75
|
+
print(f"{'='*50}")
|
|
76
|
+
print("\nTry these commands:")
|
|
77
|
+
print(" python main.py list")
|
|
78
|
+
print(" python main.py list --category Food")
|
|
79
|
+
print(" python main.py summary")
|
|
80
|
+
print(" pytest -v")
|
|
81
|
+
|
|
82
|
+
return count
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def main():
|
|
86
|
+
"""Entry point for seeding script."""
|
|
87
|
+
seed_database()
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
if __name__ == "__main__":
|
|
91
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Test package for expense tracker."""
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Pytest configuration and shared fixtures."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import pytest
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
# Add parent directory to path so we can import our modules
|
|
9
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
10
|
+
|
|
11
|
+
# Use a separate test database
|
|
12
|
+
TEST_DB_PATH = Path(__file__).parent / "test_expenses.db"
|
|
13
|
+
os.environ["EXPENSE_DB_PATH"] = str(TEST_DB_PATH)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.fixture(autouse=True)
|
|
17
|
+
def clean_test_db():
|
|
18
|
+
"""Ensure clean database for each test.
|
|
19
|
+
|
|
20
|
+
This fixture runs automatically before and after each test.
|
|
21
|
+
It initializes the database and cleans up after.
|
|
22
|
+
"""
|
|
23
|
+
from database import init_db, clear_all_expenses
|
|
24
|
+
|
|
25
|
+
# Setup: initialize fresh database
|
|
26
|
+
init_db()
|
|
27
|
+
clear_all_expenses()
|
|
28
|
+
|
|
29
|
+
yield # Run the test
|
|
30
|
+
|
|
31
|
+
# Teardown: clean up
|
|
32
|
+
clear_all_expenses()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@pytest.fixture
|
|
36
|
+
def sample_expenses():
|
|
37
|
+
"""Create a set of sample expenses for testing.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
List of created Expense objects
|
|
41
|
+
"""
|
|
42
|
+
from datetime import datetime
|
|
43
|
+
from models import add_expense
|
|
44
|
+
|
|
45
|
+
expenses = [
|
|
46
|
+
add_expense(50.00, "Food", "Groceries", datetime(2026, 1, 15)),
|
|
47
|
+
add_expense(30.00, "Transport", "Uber ride", datetime(2026, 1, 14)),
|
|
48
|
+
add_expense(25.00, "Food", "Lunch", datetime(2026, 1, 16)),
|
|
49
|
+
add_expense(100.00, "Shopping", "Clothes", datetime(2026, 1, 10)),
|
|
50
|
+
add_expense(15.00, "Entertainment", "Movie", datetime(2026, 1, 12)),
|
|
51
|
+
]
|
|
52
|
+
return expenses
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@pytest.fixture
|
|
56
|
+
def mixed_case_expenses():
|
|
57
|
+
"""Create expenses with mixed case categories for bug testing.
|
|
58
|
+
|
|
59
|
+
This fixture specifically tests the case-sensitivity bug.
|
|
60
|
+
"""
|
|
61
|
+
from datetime import datetime
|
|
62
|
+
from models import add_expense
|
|
63
|
+
|
|
64
|
+
expenses = [
|
|
65
|
+
add_expense(50.00, "Food", "Groceries", datetime(2026, 1, 15)),
|
|
66
|
+
add_expense(25.00, "food", "Coffee", datetime(2026, 1, 16)), # lowercase
|
|
67
|
+
add_expense(30.00, "FOOD", "Dinner", datetime(2026, 1, 17)), # uppercase
|
|
68
|
+
]
|
|
69
|
+
return expenses
|