Typorithm · medium

Capstone: an owner-scoped technical-role API

pythonfastapiindependent-practice
Saved on this device
Full lesson · 4 learning steps Open any exercise for focused practice
1

Step 1 of 4 · intro card

Capstone: an owner-scoped technical-role API

0%

Concept and contract

Combine identity, bounded filters, public fields and honest observed counts.

A small useful API combines several independent contracts. It identifies the caller, selects only permitted data, validates filters, bounds each page and emits a deliberate public representation. No single decorator proves all of those properties. The capstone keeps them visible in a compact owned fixture.

Two synthetic principals have technical roles in Ukraine and Israel. Internal records also contain private notes. The endpoint uses a dedicated principal dependency, filters owner and country before taking a page, and returns an observed_total plus public items. It contacts no production feed and changes no stored data.

Mechanism and worked example

A demo X-Demo-User header maps to one of two explicitly allowed fixture identities.

A demo X-Demo-User header maps to one of two explicitly allowed fixture identities. This is a local teaching boundary, not deployable authentication. Invalid or missing identities receive 401. The country vocabulary and positive bounded limit are validated independently.

Selection first keeps records owned by the resolved user in the requested country, then sorts by ID and applies the limit. The response models exclude owner and private_note. observed_total counts that filtered owned collection before the page is taken, so it can exceed the number of returned items.

Complete worked example

python

from typing import Annotated, Literal
from fastapi import Depends, FastAPI, Header, HTTPException, Query
from fastapi.testclient import TestClient
from pydantic import BaseModel

class PublicRole(BaseModel):
    id: int
    title: str
    country: str
class Page(BaseModel):
    observed_total: int
    items: list[PublicRole]

def inspect_example(request):
    records = [{"id": 1, "title": "QA", "country": "UA", "owner": "alice", "private_note": "a"},
               {"id": 2, "title": "API", "country": "UA", "owner": "alice", "private_note": "b"},
               {"id": 3, "title": "Data", "country": "UA", "owner": "bob", "private_note": "c"},
               {"id": 4, "title": "Web", "country": "IL", "owner": "alice", "private_note": "d"}]
    app = FastAPI()
    def principal(x_demo_user: Annotated[str | None, Header()] = None):
        if x_demo_user not in ("alice", "bob"):
            raise HTTPException(401, "Sign in")
        return x_demo_user
    @app.get("/roles", response_model=Page)
    def read(user: Annotated[str, Depends(principal)], country: Literal["UA", "IL"],
             limit: Annotated[int, Query(ge=1, le=2)] = 1):
        selected = sorted([r for r in records if r["owner"] == user and r["country"] == country],
                          key=lambda r: r["id"])
        return {"observed_total": len(selected), "items": selected[:limit]}
    headers = {} if "user" not in request else {"X-Demo-User": request["user"]}
    with TestClient(app) as client:
        response = client.get("/roles", params=request.get("query", {"country": "UA"}), headers=headers)
        body = response.json()
        if response.status_code == 422:
            body = sorted({e["type"] for e in body["detail"]})
        return {"status": response.status_code, "body": body}
Reasoning
A demo X-Demo-User header maps to one of two explicitly allowed fixture identities. This is a local teaching boundary, not deployable authentication. Invalid or missing identities receive 401. The country vocabulary and positive bounded limit are validated independently. Selection first keeps records owned by the resolved user in the requested country, then sorts by ID and applies the limit. The response models exclude owner and private_note. observed_total counts that filtered owned collection before the page is taken, so it can exceed the number of returned items.

Focused decision: use the complete example for context

python

selected = sorted(
    [r for r in records if r["owner"] == user and r["country"] == country],
    key=lambda r: r["id"],
)
page = selected[:limit]
return {"observed_total": len(selected), "items": page}
Focused decision
Reconstruct this focused decision after the warm-up. The complete imports, model or application, and test client remain in the worked example. Typing records practice; only the downloadable computer lab executes Python.

Counterexamples and common mistakes

The counterexample removes the owner predicate while keeping country filtering and response models.

The counterexample removes the owner predicate while keeping country filtering and response models. A Ukrainian page then includes bob role or changes its observed total. Even when private notes stay excluded, cross-owner disclosure remains an authorization failure. Representation and ownership must both be correct.

Do not treat the synthetic header as an identity system or advertise this fixture as a production security template. A real service must replace it with verified credentials, enforce storage-level boundaries and use appropriate cache keys that include every private scope. Public caching of an owner-specific response can itself leak data.

Controlled mistake: observe the actual wrong behavior

python

from typing import Annotated, Literal
from fastapi import Depends, FastAPI, Header, HTTPException, Query
from fastapi.testclient import TestClient
from pydantic import BaseModel

class PublicRole(BaseModel):
    id: int
    title: str
    country: str
class Page(BaseModel):
    observed_total: int
    items: list[PublicRole]

def inspect_example(request):
    records = [{"id": 1, "title": "QA", "country": "UA", "owner": "alice", "private_note": "a"},
               {"id": 2, "title": "API", "country": "UA", "owner": "alice", "private_note": "b"},
               {"id": 3, "title": "Data", "country": "UA", "owner": "bob", "private_note": "c"},
               {"id": 4, "title": "Web", "country": "IL", "owner": "alice", "private_note": "d"}]
    app = FastAPI()
    def principal(x_demo_user: Annotated[str | None, Header()] = None):
        if x_demo_user not in ("alice", "bob"):
            raise HTTPException(401, "Sign in")
        return x_demo_user
    @app.get("/roles", response_model=Page)
    def read(user: Annotated[str, Depends(principal)], country: Literal["UA", "IL"],
             limit: Annotated[int, Query(ge=1, le=2)] = 1):
        selected = sorted([r for r in records if r["country"] == country],
                          key=lambda r: r["id"])
        return {"observed_total": len(selected), "items": selected[:limit]}
    headers = {} if "user" not in request else {"X-Demo-User": request["user"]}
    with TestClient(app) as client:
        response = client.get("/roles", params=request.get("query", {"country": "UA"}), headers=headers)
        body = response.json()
        if response.status_code == 422:
            body = sorted({e["type"] for e in body["detail"]})
        return {"status": response.status_code, "body": body}
Reasoning
The counterexample removes the owner predicate while keeping country filtering and response models. A Ukrainian page then includes bob role or changes its observed total. Even when private notes stay excluded, cross-owner disclosure remains an authorization failure. Representation and ownership must both be correct. Do not treat the synthetic header as an identity system or advertise this fixture as a production security template. A real service must replace it with verified credentials, enforce storage-level boundaries and use appropriate cache keys that include every private scope. Public caching of an owner-specific response can itself leak data.

Independent practice: one bounded change

Add observed_returned to the response envelope and set it to the actual page length.

Add observed_returned to the response envelope and set it to the actual page length. Keep observed_total as the pre-page filtered count. This small final change tests whether you can add useful metadata without confusing two different scopes.

Check alice Ukrainian page with default limit one, an explicit two-item page and bob Ukrainian page. The returned count should be one or two as appropriate, while alice observed_total remains two. Preserve the unauthorized case and private-field projection when running your own additional tests.

Step 1: Add observed_returned for the page length without changing the filtered owned total or leaking internal data. Step 2: Retain the working imports and application structure. Change the stated policy, run the supplied practice cases, then explain the newly accepted or rejected boundary.

Practice starter: solve before reading the solution

python

from typing import Annotated, Literal
from fastapi import Depends, FastAPI, Header, HTTPException, Query
from fastapi.testclient import TestClient
from pydantic import BaseModel

class PublicRole(BaseModel):
    id: int
    title: str
    country: str
class Page(BaseModel):
    observed_total: int
    items: list[PublicRole]

def inspect_example(request):
    records = [{"id": 1, "title": "QA", "country": "UA", "owner": "alice", "private_note": "a"},
               {"id": 2, "title": "API", "country": "UA", "owner": "alice", "private_note": "b"},
               {"id": 3, "title": "Data", "country": "UA", "owner": "bob", "private_note": "c"},
               {"id": 4, "title": "Web", "country": "IL", "owner": "alice", "private_note": "d"}]
    app = FastAPI()
    def principal(x_demo_user: Annotated[str | None, Header()] = None):
        if x_demo_user not in ("alice", "bob"):
            raise HTTPException(401, "Sign in")
        return x_demo_user
    @app.get("/roles", response_model=Page)
    def read(user: Annotated[str, Depends(principal)], country: Literal["UA", "IL"],
             limit: Annotated[int, Query(ge=1, le=2)] = 1):
        selected = sorted([r for r in records if r["owner"] == user and r["country"] == country],
                          key=lambda r: r["id"])
        return {"observed_total": len(selected), "items": selected[:limit]}
    headers = {} if "user" not in request else {"X-Demo-User": request["user"]}
    with TestClient(app) as client:
        response = client.get("/roles", params=request.get("query", {"country": "UA"}), headers=headers)
        body = response.json()
        if response.status_code == 422:
            body = sorted({e["type"] for e in body["detail"]})
        return {"status": response.status_code, "body": body}

# Independent task: Add observed_returned for the page length without changing the filtered owned total or leaking internal data.
Reasoning
Add observed_returned for the page length without changing the filtered owned total or leaking internal data.
  • Hint 1: Find the one field or branch that owns this policy.
  • Hint 2: Compare the first changed acceptance case with the retained baseline.

Solution, tests and trade-offs

The solution materializes the bounded page once, then derives observed_returned from that page.

The solution materializes the bounded page once, then derives observed_returned from that page. It retains the owner predicate, stable ordering, country selection and response model. The fixture asserts exact keys and values, so an accidental private field or mismatched count cannot hide behind a success status.

A production version would query a suitable indexed or cached snapshot and measure latency under realistic workloads. This tiny example is not a performance benchmark. It demonstrates the ordering and scope that such an optimized implementation must preserve, including after cache refreshes or ingestion updates.

Practice solution and acceptance checks

python

from typing import Annotated, Literal
from fastapi import Depends, FastAPI, Header, HTTPException, Query
from fastapi.testclient import TestClient
from pydantic import BaseModel

class PublicRole(BaseModel):
    id: int
    title: str
    country: str
class Page(BaseModel):
    observed_total: int
    observed_returned: int
    items: list[PublicRole]

def inspect_example(request):
    records = [{"id": 1, "title": "QA", "country": "UA", "owner": "alice", "private_note": "a"},
               {"id": 2, "title": "API", "country": "UA", "owner": "alice", "private_note": "b"},
               {"id": 3, "title": "Data", "country": "UA", "owner": "bob", "private_note": "c"},
               {"id": 4, "title": "Web", "country": "IL", "owner": "alice", "private_note": "d"}]
    app = FastAPI()
    def principal(x_demo_user: Annotated[str | None, Header()] = None):
        if x_demo_user not in ("alice", "bob"):
            raise HTTPException(401, "Sign in")
        return x_demo_user
    @app.get("/roles", response_model=Page)
    def read(user: Annotated[str, Depends(principal)], country: Literal["UA", "IL"],
             limit: Annotated[int, Query(ge=1, le=2)] = 1):
        selected = sorted([r for r in records if r["owner"] == user and r["country"] == country],
                          key=lambda r: r["id"])
        page = selected[:limit]
        return {"observed_total": len(selected), "observed_returned": len(page), "items": page}
    headers = {} if "user" not in request else {"X-Demo-User": request["user"]}
    with TestClient(app) as client:
        response = client.get("/roles", params=request.get("query", {"country": "UA"}), headers=headers)
        body = response.json()
        if response.status_code == 422:
            body = sorted({e["type"] for e in body["detail"]})
        return {"status": response.status_code, "body": body}
Reasoning
The solution materializes the bounded page once, then derives observed_returned from that page. It retains the owner predicate, stable ordering, country selection and response model. The fixture asserts exact keys and values, so an accidental private field or mismatched count cannot hide behind a success status. A production version would query a suitable indexed or cached snapshot and measure latency under realistic workloads. This tiny example is not a performance benchmark. It demonstrates the ordering and scope that such an optimized implementation must preserve, including after cache refreshes or ingestion updates.

Interview reasoning and next step

Review the complete request path and name a failing test for each guarantee: missing identity, cross-owner selection, country mismatch, excessive limit, incorrect total and leaked internal field.

Review the complete request path and name a failing test for each guarantee: missing identity, cross-owner selection, country mismatch, excessive limit, incorrect total and leaked internal field. If one test only checks a success code, strengthen it with the observable contract it is meant to protect.

Continue next with Django REST Framework or database persistence from the curriculum audit. Reuse these boundary habits rather than assuming the next library changes the meaning of authorization, validation or durability. The goal is to transfer the reasoning, not memorize framework-specific decorations.

Optional stretch — after the core task works: Add one independently reasoned boundary case and explain its expected value before running it.

Computer practice: https://applydjinn.com/typorithm/python-api-lab-20261105.zip. Use an isolated Python environment and the pinned dependencies. Run verify.py --track fastapi, then edit the working exercise baseline and run --practice fa-capstone. The reference suite rejects meaningful counterexamples and checks values, types and unchanged inputs. All data is owned and all HTTP clients are in-process. No production database, feed, account or paid service is used.

Combine identity, bounded filters, public fields and honest observed counts.