Capstone: the ExpenseApp
This is the finale -> your mid-tier Python certification artifact. Part 1 gave you the data layer. Now you build a real, runnable app: an ExpenseApp class you drive with text commands, the way a tiny CLI works. One method, dispatch(command), takes a string like "add Coffee 3.50 food" and returns the line to show.
This single class exercises the whole module at once -> a class holding state, a decorator that wraps a method, a generator that yields lazily, and JSON persistence so the app can save and reload. Build all of it.
The commands dispatch handles
Split the command on spaces; the first word is the verb. Return these exact strings:
add <desc> <amount> <category>-> records an expense, returns"Added Coffee: 3.50 (food)"(amount to two decimals).total-> returns"TOTAL: 12.50"(grand total, two decimals).report-> one"category: total"line per category sorted alphabetically (two decimals), then a final"TOTAL: grand_total"line, joined by newlines. An empty ledger returns"No expenses recorded."budget <category> <amount>-> sets a spending limit, returns"Budget for food: 9.00".over-> if any categories are over budget, returns"Over budget: food, fun"(alphabetical, comma-joined); otherwise"All categories within budget."
Store the ledger and budgets on self in __init__. Amounts arrive as strings from the command -> convert with float(...). Format with f"{value:.2f}".
The decorator: @log_calls
Write log_calls and apply it to dispatch. Each time the wrapped method runs it appends the function's name to self.call_log, then calls through. Use @functools.wraps(func) on the inner wrapper so the decorated method keeps its real __name__ (the tests check ExpenseApp.dispatch.__name__ == "dispatch"). Because the wrapper takes self first, signature is def wrapper(self, *args, **kwargs):.
The generator: iter_over_budget(self)
A real generator method -> it yields each category whose spend exceeds its budget, in alphabetical order. The hidden test asserts inspect.isgeneratorfunction(ExpenseApp.iter_over_budget), so it must use yield, not return a list. The over command should consume this generator with list(self.iter_over_budget()).
Persistence: to_json / from_json
to_json(self) returns a JSON string capturing the ledger and budgets. from_json(cls, text) is a @classmethod that builds a fresh app from that string. The round-trip must be lossless -> ExpenseApp.from_json(app.to_json()) reproduces the same ledger and budgets.
Try the AI tutor
If your tutor is on (the app wires window._floatiTutor), ask it to suggest a budgeting tip for your biggest category. Grading never touches model output -> only your deterministic command transcript is checked.
Press Run: the hidden tests build an app, fire a scripted list of commands at dispatch, and assert the exact transcript line by line -> then check the generator, the JSON round-trip, and the preserved name. Pass them and you have shipped a real, tested, mid-tier Python app.
Build a runnable ExpenseApp class. dispatch(command) handles add <desc> <amount> <category>, total, report, budget <category> <amount>, and over, returning the exact strings in the lesson. Write a log_calls decorator (using functools.wraps) applied to dispatch that appends each call's name to self.call_log. Write iter_over_budget(self) as a GENERATOR yielding over-budget categories alphabetically. Add to_json(self) and a from_json(cls, text) classmethod so the app round-trips losslessly. Never call input() -> commands arrive only through dispatch.
This lesson is locked
Lessons open one at a time. Finish the previous lesson to unlock this one.