@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,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