All arguments have the documented Python types; type coercion is unnecessary. Strings are case-sensitive. Ingredients and steps are lists of strings; empty lists, empty items, and duplicates are allowed and preserve order. Every returned dictionary/list is a detached snapshot, including nested lists. Instances must have independent state. No persistence or deletion is required.
Prototype: implement RecipeDraft(name, ingredients, steps) and snapshot(). The snapshot is {"name": name, "ingredients": [...], "steps": [...]}. Copy the input lists. The draft does not validate the name or manage permissions.
Final: implement RecipeManager() with these methods:
add_user(user_id, permissions) -> bool: permissions is a list containing only"read"and/or"write"; duplicates are harmless. Empty permissions are allowed. Empty user IDs or unknown permissions raiseValueError, even for an existing ID. ReturnFalsefor an existing valid ID, preserving its original permissions; otherwise register a copied permission set and returnTrue.add(user_id, name, ingredients, steps) -> str | None: requires write. Empty name raisesValueErrorbefore permission checks. ReturnNonefor no permission or a currently used name. Success returns"recipe1","recipe2", ... and creates v1. Rejected requests consume no identifier.update(user_id, recipe_id, name, ingredients, steps) -> int | None: the same name validation and write check apply. Missing recipe or name owned by another recipe returnsNone. Success appends a revision, even for identical content, and returns its number. Renaming frees the old current name.get(user_id, recipe_id, version=None) -> dict | None: requires read.Noneselects latest; an integer version selects that positive revision. ReturnNonefor no permission, missing recipe, or nonexistent version. A snapshot has exactlyid,version,name,ingredients, andsteps.history(user_id, recipe_id) -> list[dict]: requires read. Return snapshots in increasing version order, or[]for no permission/missing recipe.rollback(user_id, recipe_id, version) -> int | None: requires write; invalid target, missing recipe, or conflicting historical name returnsNone. Append the selected content as a new latest revision and return its number. Preserve all existing versions and the same recipe ID.by_ingredient(user_id, ingredient) -> list[str]: requires read; otherwise[]. Match exact ingredient membership in current content. Sort by ingredient count ascending, current name ascending, then ID ascending. A recipe appears once despite duplicate ingredients.
Invalid or rejected calls leave all state unchanged. Run python main.py --stage prototype, then python main.py --stage final (the default). Edit only exercise.py.
Complete the starter code, then run the checks.
Results and errors will appear here.