TT Lab
Get started
Learn Learning paths Courses

FastAPI — Types Are the Contract

Prevent lost updates with ETags

Continue in TT Lab

Goal

When two users edit the same draft, you prevent lost updates with a conditional SQL update and HTTP responses.

Why it matters

Success on one normal request does not guarantee boundary values or failure recovery. In this lab you implement or test the contract of each function in small pieces and then connect them in real execution. The grader does not look only at whether code exists or at the wording of a report; it checks results, exceptions, and stored state. Keep the code from the earlier steps as you move to the next step.

Steps

  1. In /root/work/fa-conditional-lab/service.py, make init_db(path) create notes(id TEXT PRIMARY KEY, title TEXT NOT NULL, version INTEGER NOT NULL) in a SQLite file. Calling it repeatedly must not erase existing data. Do the first preparation with the following command.
mkdir -p /root/work/fa-conditional-lab
cp /opt/fixtures/practice_depth/fa-conditional-lab/* /root/work/fa-conditional-lab/
cd /root/work/fa-conditional-lab
  1. In /root/work/fa-conditional-lab/service.py, make create_note(path, note_id, title) INSERT a note with version 1 and commit. A duplicate id is rejected with sqlite3.IntegrityError, and the existing title is not overwritten.
  2. In /root/work/fa-conditional-lab/service.py, make read_note(path, note_id) return an {id, title, version} dictionary or None. Do not concatenate the id into the SQL as a string; bind it.
  3. In /root/work/fa-conditional-lab/service.py, make etag(version) return the integer version as a string that includes the double quotes. For example, version 3 is the string '"v3"'.
  4. In /root/work/fa-conditional-lab/service.py, make parse_match(header) accept only a tag that is a v followed by one positive decimal integer inside double quotes, and return the version. None, weak tags, wildcards, lists, v0, and so on are ValueError. For example, '"v12"' → 12.
  5. In /root/work/fa-conditional-lab/service.py, make update_note(path, note_id, title, version) update the title and raise the version by 1 only when both the id and the version match. It returns True if one row changed and False if none did. Even with concurrent requests there must be exactly one winner.
  6. In /root/work/fa-conditional-lab/service.py, make put_note(path, note_id, title, header) return 404 for an id that does not exist, 428 for a missing condition, 412 for an unsupported condition or a version mismatch, and 204 for success. On rejection, do not change the DB.
  7. In /root/work/fa-conditional-lab/service.py, make create_app(path) provide GET /notes/{note_id} and PUT /notes/{note_id}. GET returns the note JSON and the ETag; PUT accepts {title: a string of 1–100 characters} and If-Match and returns an empty response with the result codes above. An empty title is 422, and a GET for a missing note is 404.

Notes

Create a store that carries a version

In /root/work/fa-conditional-lab/service.py, make init_db(path) create notes(id TEXT PRIMARY KEY, title TEXT NOT NULL, version INTEGER NOT NULL) in a SQLite file. Calling it repeatedly must not erase existing data. Do the first preparation with the following command.

mkdir -p /root/work/fa-conditional-lab
cp /opt/fixtures/practice_depth/fa-conditional-lab/* /root/work/fa-conditional-lab/
cd /root/work/fa-conditional-lab

Use CREATE TABLE IF NOT EXISTS and set the commit boundary with with sqlite3.connect(path).

You can reproduce the grading directly with bash /opt/lab/checks/fa-conditional-lab/01-contract.sh. Save the file and run it again.

Fix the version of the first save

In /root/work/fa-conditional-lab/service.py, make create_note(path, note_id, title) INSERT a note with version 1 and commit. A duplicate id is rejected with sqlite3.IntegrityError, and the existing title is not overwritten.

INSERT OR REPLACE turns a duplicate creation into an update. The contract of this step is creating a new note.

You can reproduce the grading directly with bash /opt/lab/checks/fa-conditional-lab/02-contract.sh. Save the file and run it again.

Tell a missing note from an existing one

In /root/work/fa-conditional-lab/service.py, make read_note(path, note_id) return an {id, title, version} dictionary or None. Do not concatenate the id into the SQL as a string; bind it.

First check whether fetchone is None. Do not return the tuple as it is.

You can reproduce the grading directly with bash /opt/lab/checks/fa-conditional-lab/03-contract.sh. Save the file and run it again.

Build a strong ETag

In /root/work/fa-conditional-lab/service.py, make etag(version) return the integer version as a string that includes the double quotes. For example, version 3 is the string '"v3"'.

The double quotes of an HTTP tag are not notation for explanation; they are part of the actual header value.

You can reproduce the grading directly with bash /opt/lab/checks/fa-conditional-lab/04-contract.sh. Save the file and run it again.

Accept only the supported condition syntax

In /root/work/fa-conditional-lab/service.py, make parse_match(header) accept only a tag that is a v followed by one positive decimal integer inside double quotes, and return the version. None, weak tags, wildcards, lists, v0, and so on are ValueError. For example, '"v12"' → 12.

If you use re.fullmatch, extra text after the tag is not tolerated. This lab does not implement the whole HTTP tag grammar.

You can reproduce the grading directly with bash /opt/lab/checks/fa-conditional-lab/05-contract.sh. Save the file and run it again.

Bind the comparison and the write into one SQL statement

In /root/work/fa-conditional-lab/service.py, make update_note(path, note_id, title, version) update the title and raise the version by 1 only when both the id and the version match. It returns True if one row changed and False if none did. Even with concurrent requests there must be exactly one winner.

Look at the rowcount of UPDATE ... WHERE id=? AND version=?. An unconditional UPDATE after a SELECT cannot prevent the race.

You can reproduce the grading directly with bash /opt/lab/checks/fa-conditional-lab/06-contract.sh. Save the file and run it again.

Classify failures as HTTP results

In /root/work/fa-conditional-lab/service.py, make put_note(path, note_id, title, header) return 404 for an id that does not exist, 428 for a missing condition, 412 for an unsupported condition or a version mismatch, and 204 for success. On rejection, do not change the DB.

Decide whether the resource exists first, and then interpret the condition. Do not turn a failure into a success by reading again.

You can reproduce the grading directly with bash /opt/lab/checks/fa-conditional-lab/07-contract.sh. Save the file and run it again.

Reproduce saving from a stale screen over HTTP

In /root/work/fa-conditional-lab/service.py, make create_app(path) provide GET /notes/{note_id} and PUT /notes/{note_id}. GET returns the note JSON and the ETag; PUT accepts {title: a string of 1–100 characters} and If-Match and returns an empty response with the result codes above. An empty title is 422, and a GET for a missing note is 404.

Receive If-Match with Header, and with TestClient do a GET and then send the same tag twice. Only the first edit should apply, and a new GET should show v2.

You can reproduce the grading directly with bash /opt/lab/checks/fa-conditional-lab/08-contract.sh. Save the file and run it again.