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 raise ValueError, even for an existing ID. Return False for an existing valid ID, preserving its original permissions; otherwise register a copied permission set and return True.
  • add(user_id, name, ingredients, steps) -> str | None: requires write. Empty name raises ValueError before permission checks. Return None for 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 returns None. 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. None selects latest; an integer version selects that positive revision. Return None for no permission, missing recipe, or nonexistent version. A snapshot has exactly id, version, name, ingredients, and steps.
  • 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 returns None. 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.

Drag or use the arrow keys to resize. Home collapses the question; End collapses the editor. Enter or double-click resets the split.
Loading editor…
No results yet.

Complete the starter code, then run the checks.
Results and errors will appear here.