@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.
Files changed (201) hide show
  1. package/dist/cli.d.ts +2 -0
  2. package/dist/cli.js +59 -0
  3. package/dist/cli.js.map +1 -0
  4. package/dist/routes/auth.d.ts +1 -0
  5. package/dist/routes/auth.js +123 -0
  6. package/dist/routes/auth.js.map +1 -0
  7. package/dist/routes/levels.d.ts +44 -0
  8. package/dist/routes/levels.js +78 -0
  9. package/dist/routes/levels.js.map +1 -0
  10. package/dist/routes/sessions.d.ts +17 -0
  11. package/dist/routes/sessions.js +303 -0
  12. package/dist/routes/sessions.js.map +1 -0
  13. package/dist/server.d.ts +2 -0
  14. package/dist/server.js +58 -0
  15. package/dist/server.js.map +1 -0
  16. package/dist/terminal.d.ts +6 -0
  17. package/dist/terminal.js +23 -0
  18. package/dist/terminal.js.map +1 -0
  19. package/dist/verification.d.ts +31 -0
  20. package/dist/verification.js +239 -0
  21. package/dist/verification.js.map +1 -0
  22. package/frontend/assets/index-CNVEnbfs.css +1 -0
  23. package/frontend/assets/index-D70xl9zu.js +27 -0
  24. package/frontend/index.html +14 -0
  25. package/frontend/vite.svg +1 -0
  26. package/keys/v1.pem +9 -0
  27. package/levels/01-context-is-everything/exercise/README.md +152 -0
  28. package/levels/01-context-is-everything/exercise/data/expenses.db +0 -0
  29. package/levels/01-context-is-everything/exercise/database.py +171 -0
  30. package/levels/01-context-is-everything/exercise/docs/FIRECRAWL_QUICKSTART.md +212 -0
  31. package/levels/01-context-is-everything/exercise/historical_data/expenses_2024_01.json +2306 -0
  32. package/levels/01-context-is-everything/exercise/historical_data/expenses_2024_02.json +2394 -0
  33. package/levels/01-context-is-everything/exercise/historical_data/expenses_2024_03.json +2251 -0
  34. package/levels/01-context-is-everything/exercise/historical_data/expenses_2024_04.json +1987 -0
  35. package/levels/01-context-is-everything/exercise/historical_data/expenses_2024_05.json +2229 -0
  36. package/levels/01-context-is-everything/exercise/main.py +97 -0
  37. package/levels/01-context-is-everything/exercise/models.py +141 -0
  38. package/levels/01-context-is-everything/exercise/pyproject.toml +52 -0
  39. package/levels/01-context-is-everything/exercise/reports.py +138 -0
  40. package/levels/01-context-is-everything/exercise/seed_data.py +91 -0
  41. package/levels/01-context-is-everything/exercise/tests/__init__.py +1 -0
  42. package/levels/01-context-is-everything/exercise/tests/conftest.py +69 -0
  43. package/levels/01-context-is-everything/exercise/tests/test_database.py +244 -0
  44. package/levels/01-context-is-everything/exercise/tests/test_models.py +240 -0
  45. package/levels/01-context-is-everything/exercise/tests/test_reports.py +190 -0
  46. package/levels/01-context-is-everything/exercise/utils.py +163 -0
  47. package/levels/01-context-is-everything/lesson.yaml +82 -0
  48. package/levels/02-claude-md/exercise/README.md +152 -0
  49. package/levels/02-claude-md/exercise/data/expenses.db +0 -0
  50. package/levels/02-claude-md/exercise/database.py +171 -0
  51. package/levels/02-claude-md/exercise/main.py +97 -0
  52. package/levels/02-claude-md/exercise/models.py +141 -0
  53. package/levels/02-claude-md/exercise/pyproject.toml +52 -0
  54. package/levels/02-claude-md/exercise/reports.py +138 -0
  55. package/levels/02-claude-md/exercise/seed_data.py +91 -0
  56. package/levels/02-claude-md/exercise/tests/__init__.py +1 -0
  57. package/levels/02-claude-md/exercise/tests/conftest.py +69 -0
  58. package/levels/02-claude-md/exercise/tests/test_database.py +244 -0
  59. package/levels/02-claude-md/exercise/tests/test_models.py +240 -0
  60. package/levels/02-claude-md/exercise/tests/test_reports.py +190 -0
  61. package/levels/02-claude-md/exercise/utils.py +163 -0
  62. package/levels/02-claude-md/lesson.yaml +60 -0
  63. package/levels/03-read-edit-verify/exercise/CLAUDE.md +15 -0
  64. package/levels/03-read-edit-verify/exercise/README.md +152 -0
  65. package/levels/03-read-edit-verify/exercise/data/expenses.db +0 -0
  66. package/levels/03-read-edit-verify/exercise/database.py +171 -0
  67. package/levels/03-read-edit-verify/exercise/main.py +97 -0
  68. package/levels/03-read-edit-verify/exercise/models.py +141 -0
  69. package/levels/03-read-edit-verify/exercise/pyproject.toml +52 -0
  70. package/levels/03-read-edit-verify/exercise/reports.py +138 -0
  71. package/levels/03-read-edit-verify/exercise/seed_data.py +91 -0
  72. package/levels/03-read-edit-verify/exercise/tests/__init__.py +1 -0
  73. package/levels/03-read-edit-verify/exercise/tests/conftest.py +69 -0
  74. package/levels/03-read-edit-verify/exercise/tests/test_database.py +244 -0
  75. package/levels/03-read-edit-verify/exercise/tests/test_models.py +240 -0
  76. package/levels/03-read-edit-verify/exercise/tests/test_reports.py +190 -0
  77. package/levels/03-read-edit-verify/exercise/utils.py +163 -0
  78. package/levels/03-read-edit-verify/lesson.yaml +60 -0
  79. package/levels/04-planning-mode/exercise/README.md +152 -0
  80. package/levels/04-planning-mode/exercise/data/expenses.db +0 -0
  81. package/levels/04-planning-mode/exercise/database.py +171 -0
  82. package/levels/04-planning-mode/exercise/main.py +97 -0
  83. package/levels/04-planning-mode/exercise/models.py +116 -0
  84. package/levels/04-planning-mode/exercise/pyproject.toml +52 -0
  85. package/levels/04-planning-mode/exercise/reports.py +138 -0
  86. package/levels/04-planning-mode/exercise/seed_data.py +91 -0
  87. package/levels/04-planning-mode/exercise/tests/__init__.py +1 -0
  88. package/levels/04-planning-mode/exercise/tests/conftest.py +69 -0
  89. package/levels/04-planning-mode/exercise/tests/test_database.py +244 -0
  90. package/levels/04-planning-mode/exercise/tests/test_expenses.db +0 -0
  91. package/levels/04-planning-mode/exercise/tests/test_models.py +240 -0
  92. package/levels/04-planning-mode/exercise/tests/test_reports.py +190 -0
  93. package/levels/04-planning-mode/exercise/utils.py +163 -0
  94. package/levels/04-planning-mode/lesson.yaml +53 -0
  95. package/levels/05-spec-driven/exercise/README.md +152 -0
  96. package/levels/05-spec-driven/exercise/data/expenses.db +0 -0
  97. package/levels/05-spec-driven/exercise/database.py +171 -0
  98. package/levels/05-spec-driven/exercise/main.py +97 -0
  99. package/levels/05-spec-driven/exercise/models.py +116 -0
  100. package/levels/05-spec-driven/exercise/pyproject.toml +52 -0
  101. package/levels/05-spec-driven/exercise/reports.py +138 -0
  102. package/levels/05-spec-driven/exercise/seed_data.py +91 -0
  103. package/levels/05-spec-driven/exercise/tests/__init__.py +1 -0
  104. package/levels/05-spec-driven/exercise/tests/conftest.py +69 -0
  105. package/levels/05-spec-driven/exercise/tests/test_database.py +244 -0
  106. package/levels/05-spec-driven/exercise/tests/test_expenses.db +0 -0
  107. package/levels/05-spec-driven/exercise/tests/test_models.py +240 -0
  108. package/levels/05-spec-driven/exercise/tests/test_reports.py +190 -0
  109. package/levels/05-spec-driven/exercise/utils.py +163 -0
  110. package/levels/05-spec-driven/lesson.yaml +53 -0
  111. package/levels/06-sub-agents/exercise/README.md +152 -0
  112. package/levels/06-sub-agents/exercise/data/expenses.db +0 -0
  113. package/levels/06-sub-agents/exercise/database.py +171 -0
  114. package/levels/06-sub-agents/exercise/main.py +97 -0
  115. package/levels/06-sub-agents/exercise/models.py +116 -0
  116. package/levels/06-sub-agents/exercise/pyproject.toml +52 -0
  117. package/levels/06-sub-agents/exercise/reports.py +63 -0
  118. package/levels/06-sub-agents/exercise/seed_data.py +91 -0
  119. package/levels/06-sub-agents/exercise/tests/__init__.py +1 -0
  120. package/levels/06-sub-agents/exercise/tests/conftest.py +69 -0
  121. package/levels/06-sub-agents/exercise/tests/test_database.py +244 -0
  122. package/levels/06-sub-agents/exercise/tests/test_models.py +240 -0
  123. package/levels/06-sub-agents/exercise/tests/test_reports.py +190 -0
  124. package/levels/06-sub-agents/exercise/utils.py +163 -0
  125. package/levels/06-sub-agents/lesson.yaml +49 -0
  126. package/levels/07-skills/exercise/README.md +152 -0
  127. package/levels/07-skills/exercise/data/expenses.db +0 -0
  128. package/levels/07-skills/exercise/database.py +171 -0
  129. package/levels/07-skills/exercise/main.py +97 -0
  130. package/levels/07-skills/exercise/models.py +116 -0
  131. package/levels/07-skills/exercise/pyproject.toml +52 -0
  132. package/levels/07-skills/exercise/reports.py +63 -0
  133. package/levels/07-skills/exercise/seed_data.py +91 -0
  134. package/levels/07-skills/exercise/tests/__init__.py +1 -0
  135. package/levels/07-skills/exercise/tests/conftest.py +69 -0
  136. package/levels/07-skills/exercise/tests/test_database.py +244 -0
  137. package/levels/07-skills/exercise/tests/test_models.py +240 -0
  138. package/levels/07-skills/exercise/tests/test_reports.py +190 -0
  139. package/levels/07-skills/exercise/utils.py +163 -0
  140. package/levels/07-skills/lesson.yaml +49 -0
  141. package/levels/08-mcp-servers/exercise/README.md +152 -0
  142. package/levels/08-mcp-servers/exercise/data/expenses.db +0 -0
  143. package/levels/08-mcp-servers/exercise/database.py +171 -0
  144. package/levels/08-mcp-servers/exercise/main.py +97 -0
  145. package/levels/08-mcp-servers/exercise/models.py +116 -0
  146. package/levels/08-mcp-servers/exercise/pyproject.toml +52 -0
  147. package/levels/08-mcp-servers/exercise/reports.py +63 -0
  148. package/levels/08-mcp-servers/exercise/seed_data.py +91 -0
  149. package/levels/08-mcp-servers/exercise/tests/__init__.py +1 -0
  150. package/levels/08-mcp-servers/exercise/tests/conftest.py +69 -0
  151. package/levels/08-mcp-servers/exercise/tests/test_database.py +244 -0
  152. package/levels/08-mcp-servers/exercise/tests/test_models.py +240 -0
  153. package/levels/08-mcp-servers/exercise/tests/test_reports.py +190 -0
  154. package/levels/08-mcp-servers/exercise/utils.py +163 -0
  155. package/levels/08-mcp-servers/lesson.yaml +59 -0
  156. package/levels/09-plugins/exercise/README.md +152 -0
  157. package/levels/09-plugins/exercise/data/expenses.db +0 -0
  158. package/levels/09-plugins/exercise/database.py +171 -0
  159. package/levels/09-plugins/exercise/main.py +97 -0
  160. package/levels/09-plugins/exercise/models.py +116 -0
  161. package/levels/09-plugins/exercise/pyproject.toml +52 -0
  162. package/levels/09-plugins/exercise/reports.py +63 -0
  163. package/levels/09-plugins/exercise/seed_data.py +91 -0
  164. package/levels/09-plugins/exercise/tests/__init__.py +1 -0
  165. package/levels/09-plugins/exercise/tests/conftest.py +69 -0
  166. package/levels/09-plugins/exercise/tests/test_database.py +244 -0
  167. package/levels/09-plugins/exercise/tests/test_models.py +240 -0
  168. package/levels/09-plugins/exercise/tests/test_reports.py +190 -0
  169. package/levels/09-plugins/exercise/utils.py +163 -0
  170. package/levels/09-plugins/lesson.yaml +51 -0
  171. package/levels/10-hooks/exercise/README.md +152 -0
  172. package/levels/10-hooks/exercise/data/expenses.db +0 -0
  173. package/levels/10-hooks/exercise/database.py +171 -0
  174. package/levels/10-hooks/exercise/main.py +97 -0
  175. package/levels/10-hooks/exercise/models.py +116 -0
  176. package/levels/10-hooks/exercise/pyproject.toml +52 -0
  177. package/levels/10-hooks/exercise/reports.py +63 -0
  178. package/levels/10-hooks/exercise/seed_data.py +91 -0
  179. package/levels/10-hooks/exercise/tests/__init__.py +1 -0
  180. package/levels/10-hooks/exercise/tests/conftest.py +69 -0
  181. package/levels/10-hooks/exercise/tests/test_database.py +244 -0
  182. package/levels/10-hooks/exercise/tests/test_models.py +240 -0
  183. package/levels/10-hooks/exercise/tests/test_reports.py +190 -0
  184. package/levels/10-hooks/exercise/utils.py +163 -0
  185. package/levels/10-hooks/lesson.yaml +58 -0
  186. package/levels/11-worktrees/exercise/README.md +152 -0
  187. package/levels/11-worktrees/exercise/data/expenses.db +0 -0
  188. package/levels/11-worktrees/exercise/database.py +171 -0
  189. package/levels/11-worktrees/exercise/main.py +97 -0
  190. package/levels/11-worktrees/exercise/models.py +116 -0
  191. package/levels/11-worktrees/exercise/pyproject.toml +52 -0
  192. package/levels/11-worktrees/exercise/reports.py +63 -0
  193. package/levels/11-worktrees/exercise/seed_data.py +91 -0
  194. package/levels/11-worktrees/exercise/tests/__init__.py +1 -0
  195. package/levels/11-worktrees/exercise/tests/conftest.py +69 -0
  196. package/levels/11-worktrees/exercise/tests/test_database.py +244 -0
  197. package/levels/11-worktrees/exercise/tests/test_models.py +240 -0
  198. package/levels/11-worktrees/exercise/tests/test_reports.py +190 -0
  199. package/levels/11-worktrees/exercise/utils.py +163 -0
  200. package/levels/11-worktrees/lesson.yaml +68 -0
  201. 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