FastAPI — Types Are the Contract
Prevent lost updates with ETags
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
- 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
- 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. - 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. - 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"'. - 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. - 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. - 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. - 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
- The packages are installed in the image. You do not need an internet connection or pip install.
- Do the preparation copy only once, at the start. If you copy again, your working files are reset.
- The grader imports your service.py and runs it with different ids and a temporary store. Do not hard-code the results.
- Grading of each step runs within 45 seconds. Do not create infinite loops or real waiting.
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.