Everything in this module is built and graded as local simulations over the real fixture — no live network calls anywhere. Same logic real Okta/Slack/Google scripts use, fully testable offline.
You met these in Module 9. An endpoint is a specific URL path an API exposes. A request is what you send; a response is what comes back. This module goes deeper into the parts of a request/response you haven't touched yet: methods, headers, pagination, and rate limits — all as local, no-network simulations.
base_url = "https://example.okta.com"
endpoint = f"{base_url}/api/v1/users"
print(endpoint)
Run the example above. Print the result.
https://example.okta.com/api/v1/usersBuild an endpoint string for "https://slack.com" + "/api/chat.postMessage" using an f-string.
https://slack.com/api/chat.postMessageIn one sentence: what's the difference between a request and a response?
Write a function build_endpoint(base_url, path) that returns the combined URL. Test it with two different base URLs.
Why would a script keep base_url as a separate variable instead of hardcoding the full URL everywhere it's needed?
Open problems/01_endpoint_recap.py and do all 4 problems locally.
The method tells the API what KIND of action you want. GET asks for data. POST creates something new. PUT replaces something existing. DELETE removes something.
def build_request(method, endpoint):
return {"method": method, "endpoint": endpoint}
req = build_request("GET", "/api/v1/users")
print(req)
Run the example above. Print the result.
{'method': 'GET', 'endpoint': '/api/v1/users'}Which method would you use to fetch a list of users?
GETWhich method would you use to send a new Slack message?
POSTWhich method would you use to deactivate (remove) an Okta user?
DELETEBuild 4 request dicts, one for each method (GET/POST/PUT/DELETE), using build_request. Print all 4.
Open problems/02_http_methods.py and do all 5 problems locally.
Headers are metadata sent alongside a request — like auth credentials or the content type. They're just a dict of key/value pairs.
default_headers = {"Content-Type": "application/json"}
custom_headers = {"Authorization": "Bearer abc123"}
merged = {**default_headers, **custom_headers}
print(merged)
Run the example above. Print the merged result.
{'Content-Type': 'application/json', 'Authorization': 'Bearer abc123'}Write a function build_headers(auth_token) that returns a dict merging the default Content-Type header with an Authorization header built from the token.
Call your build_headers with "xyz789". Print the result.
{'Content-Type': 'application/json', 'Authorization': 'Bearer xyz789'}If two dicts being merged with {**a, **b} share a key, which one wins — a or b?
b — the later one overwritesPrint just the keys of your headers dict from Lab 3, as a list.
['Content-Type', 'Authorization']Open problems/03_headers.py and do all 5 problems locally.
An auth token is the credential proving who's calling. It should always come from an env var (Module 10), never hardcoded — then get built into the Authorization header.
import os
def get_required_env(name):
val = os.environ.get(name)
if val is None:
raise ValueError(f"{name} is required but not set")
return val
def build_auth_header(token):
return {"Authorization": f"Bearer {token}"}
Write both functions exactly as shown. Call get_required_env on an unset variable name. Let it crash.
ValueErrorSet an env var yourself (inside your script, with os.environ["X"] = "..."), then call get_required_env on it. Print the result.
Call build_auth_header with "abc123". Print the result.
{'Authorization': 'Bearer abc123'}Combine both: set an env var, read it with get_required_env, then build the auth header from it. Print the header.
In one sentence: why is get_required_env a better choice here than .get() with a default?
Open problems/04_auth_tokens.py and do all 5 problems locally.
build_request()Combine method, endpoint, headers, and auth into one function that builds a complete request record.
def build_request(method, base_url, path, token):
return {
"method": method,
"endpoint": f"{base_url}{path}",
"headers": {"Content-Type": "application/json", "Authorization": f"Bearer {token}"},
}
Write build_request exactly as shown. Call it with "GET", "https://example.okta.com", "/api/v1/users", "abc123". Print the result.
Call it again with method "POST". Confirm only the method field changes.
Print just req["headers"]["Authorization"] from your Lab 1 result.
Bearer abc123Build 3 different requests for 3 different endpoints, same token. Print all 3 endpoint strings.
Write a function that builds a request AND validates the method is one of GET/POST/PUT/DELETE, raising ValueError otherwise. Test with a valid and an invalid method.
Open problems/05_build_request_checkpoint.py — 5 problems, #5 is a checkpoint combining sub-skills 1-4.
You met 200/401/404 in Module 9. Two more matter a lot in real scripts: 429 means you're calling too fast (rate limited) — back off and retry later. 500 means the SERVER failed — not your fault, but you still need to handle it gracefully.
def handle_status(status_code):
if status_code == 200:
return "success"
elif status_code == 401:
return "bad auth token"
elif status_code == 404:
return "not found"
elif status_code == 429:
return "rate limited -- back off"
elif status_code == 500:
return "server error -- not your fault"
else:
return f"unexpected: {status_code}"
Write handle_status exactly as shown. Call it with all 5 named codes plus one unnamed one (like 418). Print all 6 results.
Is a 429 your script's fault, the server's fault, or neither (just a limit being hit)?
Is a 500 something retrying immediately usually fixes?
Write a function should_retry(status_code) that returns True for 429 and 500, False otherwise. Test it on all 5 named codes.
False, False, False, True, TrueLoop a list of mock status codes [200, 429, 404, 500, 200], printing handle_status's result for each.
Open problems/06_status_codes_deepened.py and do all 5 problems locally.
Since this course hasn't taught classes, a mock "response" is just a plain dict: {"status_code": ..., "json": {...}}. Handle it the same way you'd handle a real one — check the status first, then use the data.
def parse_response(response):
if response["status_code"] == 200:
return response["json"]
raise Exception(f"request failed: {response['status_code']}")
ok_response = {"status_code": 200, "json": {"id": "00u001"}}
bad_response = {"status_code": 404, "json": None}
Write parse_response exactly as shown. Call it on ok_response. Print the result.
{'id': '00u001'}Call it on bad_response. Let it crash.
Exception: request failed: 404Wrap a call to parse_response(bad_response) in try/except, printing "handled gracefully" instead of crashing.
handled gracefullyBuild your own mock response dict for a 500 error. Confirm parse_response raises on it too.
Write a function that takes a LIST of mock responses, returning only the .json data from the successful ones (skip failures, don't crash the whole loop).
Open problems/07_mock_response_handling.py and do all 5 problems locally.
Real APIs return results in pages instead of all at once — a "list users" call might return 5 at a time, with a flag telling you whether more pages exist. This mock simulates that over the real fixture, no network needed.
def fetch_page(page_number, page_size=5, users=USERS):
start = (page_number - 1) * page_size
end = start + page_size
return {
"items": users[start:end],
"page": page_number,
"has_more": end < len(users),
}
Write fetch_page exactly as shown. Call fetch_page(1). Print len(result["items"]) and result["has_more"].
5 then TrueCall fetch_page(3) (12 users, page size 5 -- this is the last page). Print the same two values.
2 then FalseCall fetch_page(4) (out of range). Print len(result["items"]) -- confirm no crash.
0Write a loop that calls fetch_page starting at page 1, incrementing until has_more is False, accumulating all items into one list. Print len() of the accumulated list.
12Call fetch_page(1, page_size=3). Print len(result["items"]).
3Open problems/08_pagination.py and do all 5 problems locally.
A rate limit caps how many calls you can make in a window. This mock uses a plain dict as a call-budget counter — no network, no timers, just a number that goes up and a check against a max.
def call_with_limit(state, max_calls):
state["calls"] += 1
if state["calls"] > max_calls:
raise Exception("429: rate limit exceeded")
return "ok"
Write call_with_limit exactly as shown. Create state = {"calls": 0}, call it 3 times with max_calls=3. Confirm no crash.
Call it a 4th time on the same state. Let it crash.
Exception: 429: rate limit exceededFresh state, call call_with_limit 5 times in a loop with max_calls=3, catching the exception each time. Print how many succeeded.
3Print state["calls"] after that loop -- does it stop incrementing once the limit is hit?
5 — it keeps counting attempts, even failed onesWrite a function calls_remaining(state, max_calls) that returns how many calls are left before hitting the limit (never negative). Test it after 2 calls with max_calls=5.
3Open problems/09_rate_limits.py and do all 5 problems locally.
Combine pagination and rate limiting: fetch every page, but stop early (gracefully) if the call budget runs out mid-fetch.
Write a function that fetches ALL pages using fetch_page, but calls call_with_limit once per page fetched. Use max_calls=10 (plenty). Print total items fetched.
12Run the same function with max_calls=2 instead (12 users / page_size 5 = 3 pages needed). Wrap each page-fetch in try/except, stopping the loop (not crashing) when the limit hits.
Print how many items were fetched before the limit stopped you in Lab 2.
10 (2 pages of 5)Print a message noting how many pages were successfully fetched vs. how many total pages exist (3).
fetched 2 of 3 pagesWrite one function fetch_all_within_budget(max_calls) combining all of the above: paginate, respect the budget, stop gracefully, return whatever items were collected plus a flag saying whether it got everything.
Open problems/10_pagination_rate_limit_checkpoint.py — 5 problems, #5 is a checkpoint combining sub-skills 8-9.
Now that both halves of this module are taught — request-building (method/endpoint/headers/auth) AND pagination/rate-limits — combine them into one realistic chain, the shape a real Okta/Slack script actually takes.
def mock_api_call(page_number, state, max_calls, token):
call_with_limit(state, max_calls)
headers = build_auth_header(token)
page = fetch_page(page_number)
return {"status_code": 200, "json": page, "headers_sent": headers}
Write mock_api_call exactly as shown (reuse your earlier functions). Call it for page 1 with a fresh state and max_calls=10. Print result["status_code"].
200Print result["headers_sent"] from Lab 1.
{'Authorization': 'Bearer ...'}Call mock_api_call 3 times in a row for pages 1, 2, 3 on the SAME state. Print state["calls"] afterward.
3Repeat Lab 3 with max_calls=2 instead. Wrap the 3rd call in try/except -- confirm it raises the rate-limit exception.
Combine mock_api_call with parse_response (sub-skill 7): call it, then parse the result to pull out just the page data.
Loop pages 1-3, calling mock_api_call and printing each page's item count.
5, 5, 2Open problems/11_mixed_recap.py and do all 6 problems locally.
Everything: env-var token → headers → paginated, rate-limited fetch of the whole fixture → status-code handling → flatten (Module 11) → sort/enumerate/write-to-file (Module 10). The heaviest script in the course so far.
Read a token from an env var with get_required_env, build headers from it.
Fetch every page of USERS using your pagination + rate-limit function from sub-skill 10, with a generous max_calls. Confirm all 12 come back.
12Flatten the fetched users into (user, group) rows, reusing Module 11's flatten_memberships.
15 rowsHandle a MOCK 500 response using handle_status/parse_response-style logic somewhere in your chain -- show it doesn't crash the whole script.
Sort the flattened rows, number them with enumerate, write them to a file (Module 10 style).
Print a one-line summary before writing: total rows, distinct groups, and the env-var-derived token's first 4 characters (never print a full token in real code).
One script, start to finish: auth → paginated/rate-limited fetch → flatten → sort/number → write to file → summary printed. Zero crashes, end to end.
Open problems/12_final_capstone.py — 7 problems, #7 is the final checkpoint for all of Module 12.
Run python check.py in problems/ to grade everything.