TT Lab
Get started
Learn Learning paths Courses

FastAPI — Types Are the Contract

Cursor pagination without gaps or duplicates

Continue in TT Lab

Goal

Separate sorting, filtering, cursors, and response projection, and then connect them again through FastAPI requests.

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-pagination-lab/service.py, make parse_limit(value) take a string integer, return an int if it is 1–50, and reject everything else with ValueError. Do the first preparation with the following command.
mkdir -p /root/work/fa-pagination-lab
cp /opt/fixtures/practice_depth/fa-pagination-lab/* /root/work/fa-pagination-lab/
cd /root/work/fa-pagination-lab
  1. In /root/work/fa-pagination-lab/service.py, make ordered_rows(rows) return a new list in ascending id order. Do not modify the input rows. Each row has a unique positive integer id, plus name, category, and internal_cost.
  2. In /root/work/fa-pagination-lab/service.py, make filter_rows(rows, category=None) return everything if category is None, and only rows of exactly the same category, in their original order, if it is specified. A category that does not exist gives an empty list.
  3. In /root/work/fa-pagination-lab/service.py, make encode_cursor(item_id) convert the decimal string of a positive integer id to UTF-8 and return a URL-safe base64 string including padding.
  4. In /root/work/fa-pagination-lab/service.py, make decode_cursor(token) restore the cursor above to a positive integer. Invalid base64, an empty value, 0, a negative number, or a non-numeric value is a ValueError. Allow only ASCII decimal digits and do not ignore newlines or junk characters.
  5. In /root/work/fa-pagination-lab/service.py, make page_after(rows, after, limit) return up to limit rows with id > after in ascending order. It returns (page, next_cursor), and it encodes the last id of the page only when rows remain. For an empty list and at the end, it is None.
  6. In /root/work/fa-pagination-lab/service.py, make public_item(row) return a new dictionary holding only id, name, and category. Do not delete or modify internal_cost in the original.
  7. In /root/work/fa-pagination-lab/service.py, make create_app(rows) return a FastAPI app with GET /items. limit defaults to 10, after has no default value, and category has no default value. Combine the earlier functions to return {items: [...], next_cursor: a string or null}, and respond with 422 to an invalid limit or after.

Notes

Fix the limit range as a contract

In /root/work/fa-pagination-lab/service.py, make parse_limit(value) take a string integer, return an int if it is 1–50, and reject everything else with ValueError. Do the first preparation with the following command.

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

Do not hide the int conversion error, and check the lower and upper bounds together after converting.

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

Separate the input order from the response order

In /root/work/fa-pagination-lab/service.py, make ordered_rows(rows) return a new list in ascending id order. Do not modify the input rows. Each row has a unique positive integer id, plus name, category, and internal_cost.

list.sort modifies the original. Compare sorted with a key function.

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

Apply the filter before the page

In /root/work/fa-pagination-lab/service.py, make filter_rows(rows, category=None) return everything if category is None, and only rows of exactly the same category, in their original order, if it is specified. A category that does not exist gives an empty list.

If you cut two rows first and filter by category afterwards, you get empty pages and omissions.

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

Express the last id as a cursor

In /root/work/fa-pagination-lab/service.py, make encode_cursor(item_id) convert the decimal string of a positive integer id to UTF-8 and return a URL-safe base64 string including padding.

base64.urlsafe_b64encode takes bytes. Convert the returned bytes to a string as well. Do not treat it as a security token.

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

Reject a broken cursor early

In /root/work/fa-pagination-lab/service.py, make decode_cursor(token) restore the cursor above to a positive integer. Invalid base64, an empty value, 0, a negative number, or a non-numeric value is a ValueError. Allow only ASCII decimal digits and do not ignore newlines or junk characters.

Use altchars and validate=True of b64decode, and also check the domain of the value after decoding.

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

Tell the last page apart exactly

In /root/work/fa-pagination-lab/service.py, make page_after(rows, after, limit) return up to limit rows with id > after in ascending order. It returns (page, next_cursor), and it encodes the last id of the page only when rows remain. For an empty list and at the end, it is None.

Getting limit rows is different from there being a next row. The comparison is >, not >=.

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

Remove internal fields from the response

In /root/work/fa-pagination-lab/service.py, make public_item(row) return a new dictionary holding only id, name, and category. Do not delete or modify internal_cost in the original.

Building a new object from an allow list rather than a block list is safe even when new internal fields are added.

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

Combine them into a real HTTP contract

In /root/work/fa-pagination-lab/service.py, make create_app(rows) return a FastAPI app with GET /items. limit defaults to 10, after has no default value, and category has no default value. Combine the earlier functions to return {items: [...], next_cursor: a string or null}, and respond with 422 to an invalid limit or after.

Send requests with TestClient(create_app(rows)). Keep the order of filter, cursor page, then public fields, and translate only ValueError into an HTTP error.

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