@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,190 @@
|
|
|
1
|
+
"""Tests for report generation.
|
|
2
|
+
|
|
3
|
+
Note: These tests will FAIL until students implement reports.py in Lesson 4.
|
|
4
|
+
The tests serve as a specification for what needs to be implemented.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from models import add_expense, Expense
|
|
10
|
+
from reports import (
|
|
11
|
+
generate_monthly_report,
|
|
12
|
+
get_category_breakdown,
|
|
13
|
+
get_top_expenses,
|
|
14
|
+
save_report
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TestGenerateMonthlyReport:
|
|
19
|
+
"""Tests for the generate_monthly_report function."""
|
|
20
|
+
|
|
21
|
+
@pytest.mark.skip(reason="Implement in Lesson 4")
|
|
22
|
+
def test_report_for_empty_month(self):
|
|
23
|
+
"""Test report generation for month with no expenses."""
|
|
24
|
+
report = generate_monthly_report("2020-01")
|
|
25
|
+
assert "No expenses" in report or "no expenses" in report.lower()
|
|
26
|
+
|
|
27
|
+
@pytest.mark.skip(reason="Implement in Lesson 4")
|
|
28
|
+
def test_report_includes_total(self):
|
|
29
|
+
"""Test that report includes total amount."""
|
|
30
|
+
add_expense(100.0, "Food", "Groceries", datetime(2026, 1, 15))
|
|
31
|
+
add_expense(50.0, "Transport", "Uber", datetime(2026, 1, 15))
|
|
32
|
+
|
|
33
|
+
report = generate_monthly_report("2026-01")
|
|
34
|
+
|
|
35
|
+
# Should contain total of $150
|
|
36
|
+
assert "150" in report
|
|
37
|
+
|
|
38
|
+
@pytest.mark.skip(reason="Implement in Lesson 4")
|
|
39
|
+
def test_report_includes_categories(self):
|
|
40
|
+
"""Test that report includes category breakdown."""
|
|
41
|
+
add_expense(100.0, "Food", "Groceries", datetime(2026, 1, 15))
|
|
42
|
+
add_expense(50.0, "Transport", "Uber", datetime(2026, 1, 15))
|
|
43
|
+
|
|
44
|
+
report = generate_monthly_report("2026-01")
|
|
45
|
+
|
|
46
|
+
assert "Food" in report
|
|
47
|
+
assert "Transport" in report
|
|
48
|
+
|
|
49
|
+
@pytest.mark.skip(reason="Implement in Lesson 4")
|
|
50
|
+
def test_report_defaults_to_current_month(self):
|
|
51
|
+
"""Test that no month argument uses current month."""
|
|
52
|
+
# This test is tricky since it depends on current date
|
|
53
|
+
# Just verify it doesn't error
|
|
54
|
+
report = generate_monthly_report()
|
|
55
|
+
assert report is not None
|
|
56
|
+
assert isinstance(report, str)
|
|
57
|
+
|
|
58
|
+
@pytest.mark.skip(reason="Implement in Lesson 4")
|
|
59
|
+
def test_report_format_is_markdown(self):
|
|
60
|
+
"""Test that report is formatted as markdown."""
|
|
61
|
+
add_expense(100.0, "Food", "Test", datetime(2026, 1, 15))
|
|
62
|
+
report = generate_monthly_report("2026-01")
|
|
63
|
+
|
|
64
|
+
# Should have markdown headers
|
|
65
|
+
assert "#" in report
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class TestGetCategoryBreakdown:
|
|
69
|
+
"""Tests for the get_category_breakdown function."""
|
|
70
|
+
|
|
71
|
+
@pytest.mark.skip(reason="Implement in Lesson 4")
|
|
72
|
+
def test_single_category(self):
|
|
73
|
+
"""Test breakdown with single category."""
|
|
74
|
+
expenses = [
|
|
75
|
+
Expense("1", 50.0, "Food", "Lunch", datetime.now()),
|
|
76
|
+
Expense("2", 25.0, "Food", "Coffee", datetime.now()),
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
breakdown = get_category_breakdown(expenses)
|
|
80
|
+
|
|
81
|
+
assert "Food" in breakdown
|
|
82
|
+
assert breakdown["Food"] == 75.0
|
|
83
|
+
|
|
84
|
+
@pytest.mark.skip(reason="Implement in Lesson 4")
|
|
85
|
+
def test_multiple_categories(self):
|
|
86
|
+
"""Test breakdown with multiple categories."""
|
|
87
|
+
expenses = [
|
|
88
|
+
Expense("1", 50.0, "Food", "Lunch", datetime.now()),
|
|
89
|
+
Expense("2", 30.0, "Transport", "Bus", datetime.now()),
|
|
90
|
+
Expense("3", 20.0, "Food", "Snack", datetime.now()),
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
breakdown = get_category_breakdown(expenses)
|
|
94
|
+
|
|
95
|
+
assert breakdown["Food"] == 70.0
|
|
96
|
+
assert breakdown["Transport"] == 30.0
|
|
97
|
+
|
|
98
|
+
@pytest.mark.skip(reason="Implement in Lesson 4")
|
|
99
|
+
def test_empty_list(self):
|
|
100
|
+
"""Test breakdown with empty expense list."""
|
|
101
|
+
breakdown = get_category_breakdown([])
|
|
102
|
+
assert breakdown == {}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class TestGetTopExpenses:
|
|
106
|
+
"""Tests for the get_top_expenses function."""
|
|
107
|
+
|
|
108
|
+
@pytest.mark.skip(reason="Implement in Lesson 4")
|
|
109
|
+
def test_top_expenses_ordering(self):
|
|
110
|
+
"""Test that expenses are sorted by amount descending."""
|
|
111
|
+
expenses = [
|
|
112
|
+
Expense("1", 25.0, "Food", "Small", datetime.now()),
|
|
113
|
+
Expense("2", 100.0, "Bills", "Big", datetime.now()),
|
|
114
|
+
Expense("3", 50.0, "Shopping", "Medium", datetime.now()),
|
|
115
|
+
]
|
|
116
|
+
|
|
117
|
+
top = get_top_expenses(expenses, 3)
|
|
118
|
+
|
|
119
|
+
assert top[0].amount == 100.0
|
|
120
|
+
assert top[1].amount == 50.0
|
|
121
|
+
assert top[2].amount == 25.0
|
|
122
|
+
|
|
123
|
+
@pytest.mark.skip(reason="Implement in Lesson 4")
|
|
124
|
+
def test_top_n_limit(self):
|
|
125
|
+
"""Test limiting to N expenses."""
|
|
126
|
+
expenses = [
|
|
127
|
+
Expense(str(i), float(i * 10), "Cat", "Desc", datetime.now())
|
|
128
|
+
for i in range(1, 11)
|
|
129
|
+
]
|
|
130
|
+
|
|
131
|
+
top = get_top_expenses(expenses, 3)
|
|
132
|
+
assert len(top) == 3
|
|
133
|
+
|
|
134
|
+
@pytest.mark.skip(reason="Implement in Lesson 4")
|
|
135
|
+
def test_top_with_fewer_expenses(self):
|
|
136
|
+
"""Test when requesting more than available."""
|
|
137
|
+
expenses = [
|
|
138
|
+
Expense("1", 50.0, "Food", "Only one", datetime.now()),
|
|
139
|
+
]
|
|
140
|
+
|
|
141
|
+
top = get_top_expenses(expenses, 5)
|
|
142
|
+
assert len(top) == 1
|
|
143
|
+
|
|
144
|
+
@pytest.mark.skip(reason="Implement in Lesson 4")
|
|
145
|
+
def test_default_n_is_5(self):
|
|
146
|
+
"""Test that default n value is 5."""
|
|
147
|
+
expenses = [
|
|
148
|
+
Expense(str(i), float(i), "Cat", "Desc", datetime.now())
|
|
149
|
+
for i in range(10)
|
|
150
|
+
]
|
|
151
|
+
|
|
152
|
+
top = get_top_expenses(expenses)
|
|
153
|
+
assert len(top) == 5
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class TestSaveReport:
|
|
157
|
+
"""Tests for the save_report function."""
|
|
158
|
+
|
|
159
|
+
@pytest.mark.skip(reason="Implement in Lesson 4")
|
|
160
|
+
def test_save_creates_file(self, tmp_path, monkeypatch):
|
|
161
|
+
"""Test that save_report creates a file."""
|
|
162
|
+
import os
|
|
163
|
+
monkeypatch.chdir(tmp_path)
|
|
164
|
+
|
|
165
|
+
report_content = "# Test Report\n\nThis is a test."
|
|
166
|
+
path = save_report(report_content, "test-report.md")
|
|
167
|
+
|
|
168
|
+
assert path.exists()
|
|
169
|
+
assert path.read_text() == report_content
|
|
170
|
+
|
|
171
|
+
@pytest.mark.skip(reason="Implement in Lesson 4")
|
|
172
|
+
def test_save_creates_directory(self, tmp_path, monkeypatch):
|
|
173
|
+
"""Test that save_report creates reports directory if needed."""
|
|
174
|
+
import os
|
|
175
|
+
monkeypatch.chdir(tmp_path)
|
|
176
|
+
|
|
177
|
+
save_report("# Test", "2026-01.md")
|
|
178
|
+
|
|
179
|
+
assert (tmp_path / "reports").is_dir()
|
|
180
|
+
|
|
181
|
+
@pytest.mark.skip(reason="Implement in Lesson 4")
|
|
182
|
+
def test_save_returns_path(self, tmp_path, monkeypatch):
|
|
183
|
+
"""Test that save_report returns the file path."""
|
|
184
|
+
import os
|
|
185
|
+
monkeypatch.chdir(tmp_path)
|
|
186
|
+
|
|
187
|
+
path = save_report("# Test", "my-report.md")
|
|
188
|
+
|
|
189
|
+
assert path.name == "my-report.md"
|
|
190
|
+
assert "reports" in str(path)
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""Utility functions for the expense tracker."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timedelta
|
|
4
|
+
from typing import Optional
|
|
5
|
+
import locale
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def format_currency(amount: float) -> str:
|
|
9
|
+
"""Format amount as currency.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
amount: The numeric amount
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
Formatted string like '$1,234.56'
|
|
16
|
+
|
|
17
|
+
Example:
|
|
18
|
+
>>> format_currency(1234.5)
|
|
19
|
+
'$1,234.50'
|
|
20
|
+
"""
|
|
21
|
+
try:
|
|
22
|
+
locale.setlocale(locale.LC_ALL, '')
|
|
23
|
+
return locale.currency(amount, grouping=True)
|
|
24
|
+
except (locale.Error, ValueError):
|
|
25
|
+
# Fallback if locale not available
|
|
26
|
+
return f"${amount:,.2f}"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def parse_date(date_str: Optional[str]) -> Optional[datetime]:
|
|
30
|
+
"""Parse a date string in various formats.
|
|
31
|
+
|
|
32
|
+
Supports:
|
|
33
|
+
- YYYY-MM-DD (2026-01-15)
|
|
34
|
+
- MM/DD/YYYY (01/15/2026)
|
|
35
|
+
- 'today'
|
|
36
|
+
- 'yesterday'
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
date_str: The date string to parse
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
datetime object or None if input is None
|
|
43
|
+
|
|
44
|
+
Raises:
|
|
45
|
+
ValueError: If date format is not recognized
|
|
46
|
+
|
|
47
|
+
Example:
|
|
48
|
+
>>> parse_date('2026-01-15')
|
|
49
|
+
datetime(2026, 1, 15, 0, 0, 0)
|
|
50
|
+
>>> parse_date('today')
|
|
51
|
+
datetime(...) # Today at midnight
|
|
52
|
+
"""
|
|
53
|
+
if not date_str:
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
date_str = date_str.lower().strip()
|
|
57
|
+
|
|
58
|
+
# Handle relative dates
|
|
59
|
+
if date_str == 'today':
|
|
60
|
+
return datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
|
|
61
|
+
|
|
62
|
+
if date_str == 'yesterday':
|
|
63
|
+
return (datetime.now() - timedelta(days=1)).replace(
|
|
64
|
+
hour=0, minute=0, second=0, microsecond=0
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Try YYYY-MM-DD (ISO format)
|
|
68
|
+
try:
|
|
69
|
+
return datetime.strptime(date_str, '%Y-%m-%d')
|
|
70
|
+
except ValueError:
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
# Try MM/DD/YYYY (US format)
|
|
74
|
+
try:
|
|
75
|
+
return datetime.strptime(date_str, '%m/%d/%Y')
|
|
76
|
+
except ValueError:
|
|
77
|
+
pass
|
|
78
|
+
|
|
79
|
+
# Try DD/MM/YYYY (European format)
|
|
80
|
+
try:
|
|
81
|
+
return datetime.strptime(date_str, '%d/%m/%Y')
|
|
82
|
+
except ValueError:
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
raise ValueError(f"Could not parse date: {date_str}. Use YYYY-MM-DD format.")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def validate_category(category: str) -> str:
|
|
89
|
+
"""Normalize category name.
|
|
90
|
+
|
|
91
|
+
Capitalizes first letter, strips whitespace.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
category: Raw category input
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Normalized category name
|
|
98
|
+
|
|
99
|
+
Example:
|
|
100
|
+
>>> validate_category(' food ')
|
|
101
|
+
'Food'
|
|
102
|
+
"""
|
|
103
|
+
return category.strip().capitalize()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def format_date(dt: datetime) -> str:
|
|
107
|
+
"""Format a datetime for display.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
dt: The datetime to format
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
Formatted string like 'Jan 15, 2026'
|
|
114
|
+
"""
|
|
115
|
+
return dt.strftime('%b %d, %Y')
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def format_month(month_str: str) -> str:
|
|
119
|
+
"""Format a YYYY-MM string for display.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
month_str: Month in YYYY-MM format
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Formatted string like 'January 2026'
|
|
126
|
+
|
|
127
|
+
Example:
|
|
128
|
+
>>> format_month('2026-01')
|
|
129
|
+
'January 2026'
|
|
130
|
+
"""
|
|
131
|
+
dt = datetime.strptime(month_str, '%Y-%m')
|
|
132
|
+
return dt.strftime('%B %Y')
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# Valid categories for validation/suggestions
|
|
136
|
+
VALID_CATEGORIES = [
|
|
137
|
+
"Food",
|
|
138
|
+
"Transport",
|
|
139
|
+
"Entertainment",
|
|
140
|
+
"Shopping",
|
|
141
|
+
"Bills",
|
|
142
|
+
"Health",
|
|
143
|
+
"Education",
|
|
144
|
+
"Travel",
|
|
145
|
+
"Other"
|
|
146
|
+
]
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def suggest_category(partial: str) -> list[str]:
|
|
150
|
+
"""Suggest categories matching a partial input.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
partial: Partial category name
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
List of matching categories
|
|
157
|
+
|
|
158
|
+
Example:
|
|
159
|
+
>>> suggest_category('fo')
|
|
160
|
+
['Food']
|
|
161
|
+
"""
|
|
162
|
+
partial_lower = partial.lower()
|
|
163
|
+
return [cat for cat in VALID_CATEGORIES if cat.lower().startswith(partial_lower)]
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
id: "skills"
|
|
2
|
+
number: 7
|
|
3
|
+
module: "Building Blocks"
|
|
4
|
+
title: "Skills (Auto-Activating Commands)"
|
|
5
|
+
|
|
6
|
+
video:
|
|
7
|
+
url: "https://youtu.be/vBgMO4U7kZg"
|
|
8
|
+
duration_seconds: 300
|
|
9
|
+
|
|
10
|
+
exercise:
|
|
11
|
+
intro: |
|
|
12
|
+
Create a skill that auto-activates when you ask about code.
|
|
13
|
+
objective: "Create a code explainer skill"
|
|
14
|
+
|
|
15
|
+
intro: |
|
|
16
|
+
Skills live in .claude/skills/ and auto-activate based on context.
|
|
17
|
+
|
|
18
|
+
CLAUDE.md (Lesson 2) loads every session - it's always-on context.
|
|
19
|
+
Skills load on demand - only when Claude thinks they're relevant.
|
|
20
|
+
|
|
21
|
+
Use CLAUDE.md for things that always matter.
|
|
22
|
+
Use skills for workflow-specific instructions.
|
|
23
|
+
|
|
24
|
+
Create a code explainer skill:
|
|
25
|
+
|
|
26
|
+
Type: claude
|
|
27
|
+
|
|
28
|
+
Say: "Create a skill called 'code-explainer' that activates when I ask
|
|
29
|
+
to explain code. It should read the relevant files and explain what
|
|
30
|
+
they do in plain English."
|
|
31
|
+
|
|
32
|
+
After creating, test it by asking: "Explain the models.py file"
|
|
33
|
+
|
|
34
|
+
verification:
|
|
35
|
+
- type: glob_exists
|
|
36
|
+
pattern: ".claude/skills/*/SKILL.md"
|
|
37
|
+
description: "Create a skill file"
|
|
38
|
+
|
|
39
|
+
success: |
|
|
40
|
+
You created an auto-activating skill!
|
|
41
|
+
|
|
42
|
+
Test it: "Explain the database.py file"
|
|
43
|
+
|
|
44
|
+
Skills auto-trigger based on context.
|
|
45
|
+
Commands require explicit /invocation.
|
|
46
|
+
|
|
47
|
+
limits:
|
|
48
|
+
max_duration_minutes: 15
|
|
49
|
+
max_claude_messages: 20
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# Expense Tracker
|
|
2
|
+
|
|
3
|
+
A simple CLI expense tracker built with Python and SQLite.
|
|
4
|
+
|
|
5
|
+
This is the project used throughout the Claude Code course. You'll learn to use
|
|
6
|
+
Claude Code to understand, debug, and extend this codebase.
|
|
7
|
+
|
|
8
|
+
## Quick Start
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
# 1. Seed the database with sample data
|
|
12
|
+
python seed_data.py
|
|
13
|
+
|
|
14
|
+
# 2. Try some commands
|
|
15
|
+
python main.py list
|
|
16
|
+
python main.py summary
|
|
17
|
+
python main.py list --category Food
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Commands
|
|
21
|
+
|
|
22
|
+
### Add an expense
|
|
23
|
+
```bash
|
|
24
|
+
python main.py add <amount> <category> <description> [--date YYYY-MM-DD]
|
|
25
|
+
|
|
26
|
+
# Examples:
|
|
27
|
+
python main.py add 45.50 Food "Grocery shopping"
|
|
28
|
+
python main.py add 25.00 Transport "Uber" --date 2026-01-15
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### List expenses
|
|
32
|
+
```bash
|
|
33
|
+
python main.py list [--category CATEGORY] [--month YYYY-MM] [--limit N]
|
|
34
|
+
|
|
35
|
+
# Examples:
|
|
36
|
+
python main.py list
|
|
37
|
+
python main.py list --category Food
|
|
38
|
+
python main.py list --month 2026-01
|
|
39
|
+
python main.py list --limit 5
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Show expense details
|
|
43
|
+
```bash
|
|
44
|
+
python main.py show <expense_id>
|
|
45
|
+
|
|
46
|
+
# Example:
|
|
47
|
+
python main.py show abc123
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Delete an expense
|
|
51
|
+
```bash
|
|
52
|
+
python main.py delete <expense_id>
|
|
53
|
+
|
|
54
|
+
# Example:
|
|
55
|
+
python main.py delete abc123
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### View spending summary
|
|
59
|
+
```bash
|
|
60
|
+
python main.py summary
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Running Tests
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# Run all tests
|
|
67
|
+
pytest
|
|
68
|
+
|
|
69
|
+
# Run with verbose output
|
|
70
|
+
pytest -v
|
|
71
|
+
|
|
72
|
+
# Run specific test file
|
|
73
|
+
pytest tests/test_models.py
|
|
74
|
+
|
|
75
|
+
# Run tests matching a pattern
|
|
76
|
+
pytest -k "category"
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Project Structure
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
expense_tracker/
|
|
83
|
+
├── main.py # CLI entry point
|
|
84
|
+
├── models.py # Expense data model and core functions
|
|
85
|
+
├── database.py # SQLite database operations
|
|
86
|
+
├── utils.py # Helper functions (formatting, parsing)
|
|
87
|
+
├── reports.py # Report generation (Lesson 4)
|
|
88
|
+
├── seed_data.py # Script to populate sample data
|
|
89
|
+
├── tests/
|
|
90
|
+
│ ├── conftest.py # Pytest fixtures
|
|
91
|
+
│ ├── test_models.py # Model tests
|
|
92
|
+
│ ├── test_database.py # Database tests
|
|
93
|
+
│ └── test_reports.py # Report tests (Lesson 4)
|
|
94
|
+
├── data/
|
|
95
|
+
│ └── expenses.db # SQLite database (created on first run)
|
|
96
|
+
└── README.md
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Course Lessons
|
|
100
|
+
|
|
101
|
+
This codebase is used in the following lessons:
|
|
102
|
+
|
|
103
|
+
| Lesson | Focus | What You'll Do |
|
|
104
|
+
|--------|-------|----------------|
|
|
105
|
+
| 1 | Context | Explore the codebase, see context limits |
|
|
106
|
+
| 2 | CLAUDE.md | Create project memory |
|
|
107
|
+
| 3 | Bug Fix | Fix the negative amount validation bug |
|
|
108
|
+
| 4 | Debugging | Fix the case-sensitive category bug |
|
|
109
|
+
| 5 | Specs | Add recurring expenses feature |
|
|
110
|
+
| 6 | Planning | Implement the reports module |
|
|
111
|
+
| 7+ | Building Blocks | Create commands, skills, workflows |
|
|
112
|
+
|
|
113
|
+
## Known Issues (For Learning)
|
|
114
|
+
|
|
115
|
+
This codebase has intentional bugs for learning purposes:
|
|
116
|
+
|
|
117
|
+
1. **Negative amounts accepted** (Lesson 3)
|
|
118
|
+
- `add_expense()` doesn't validate that amount > 0
|
|
119
|
+
- Tests `test_add_negative_amount_should_fail` and `test_add_zero_amount_should_fail` fail
|
|
120
|
+
|
|
121
|
+
2. **Case-sensitive category filtering** (a later lesson)
|
|
122
|
+
- `list_expenses(category="Food")` won't find expenses with category "food" or "FOOD"
|
|
123
|
+
- Test `test_list_by_category_case_insensitive` fails
|
|
124
|
+
|
|
125
|
+
3. **Reports not implemented** (Lesson 4)
|
|
126
|
+
- `reports.py` has stub functions that raise `NotImplementedError`
|
|
127
|
+
- All tests in `test_reports.py` are skipped until implementation
|
|
128
|
+
|
|
129
|
+
## Development
|
|
130
|
+
|
|
131
|
+
### Requirements
|
|
132
|
+
- Python 3.10+
|
|
133
|
+
- pytest (for running tests)
|
|
134
|
+
|
|
135
|
+
### Setup
|
|
136
|
+
```bash
|
|
137
|
+
# Create virtual environment (optional but recommended)
|
|
138
|
+
python -m venv venv
|
|
139
|
+
source venv/bin/activate # On Windows: venv\Scripts\activate
|
|
140
|
+
|
|
141
|
+
# Install dev dependencies
|
|
142
|
+
pip install pytest ruff
|
|
143
|
+
|
|
144
|
+
# Initialize database
|
|
145
|
+
python seed_data.py
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Code Style
|
|
149
|
+
- Type hints on all function signatures
|
|
150
|
+
- Docstrings on public functions
|
|
151
|
+
- Black-compatible formatting
|
|
152
|
+
- No unused imports
|
|
Binary file
|