Homework

12.3.3. Homework#

  • Use these questions after class to check your understanding of the chapter concepts and coding skills.

  • Sign in from the account menu before you start working on the homework.

  • Complete all true/false and coding questions before you submit the homework.

  • Launch Live Code (the rocket) to write and run code in the coding cells.

  • Multiple submissions are allowed. The highest score is kept.

1. HTTP Methods

A client can use GET to request data and POST to send a new JSON payload when an API supports those methods.

Hide code cell source

### Solution
# True
# GET asks the server to return data, and POST sends data, such as a JSON payload, to
# create a new resource. An API's documentation tells you which methods each endpoint
# accepts.

2. Status Codes

A 404 response usually means the requested API resource was not found.

Hide code cell source

### Solution
# True
# Status codes in the 400s signal client errors, and 404 Not Found means the server has no
# resource at the requested URL. Checking the status code before using the body helps you
# report the problem.

3. Query Parameters

API query parameters should normally be pasted into a URL string by hand instead of passed through a structured parameter dictionary.

Hide code cell source

### Solution
# False
# Passing a dictionary, as in requests.get(url, params=params), lets the library build and
# URL-encode the query string, for example turning {"city": "New York"} into
# city=New+York. Pasting values into the URL by hand is error-prone.

4. JSON Payloads

After response.json() returns a dictionary, Python code can navigate nested data with keys and list indexes.

Hide code cell source

### Solution
# True
# response.json() parses the JSON body into ordinary Python dictionaries and lists. You
# can then reach nested values with keys and indexes, as in data["items"][0]["name"].

5. API Keys

It is good practice to hard-code a private API key directly in a textbook notebook before committing it.

Hide code cell source

### Solution
# False
# A key committed in a notebook becomes visible to anyone who can see the repository,
# including its history. Store the key in an environment variable and read it with
# os.environ.get() instead.
### 6. Build query parameters
# In your own code, urllib.parse.urlencode(params) builds and percent-encodes a query
# string, and requests.get(url, params=params) does it for you. This grading sandbox
# cannot import urllib, so encode_value() below stands in for the encoding step: it
# escapes %, &, =, and + and turns a space into +, the same way urlencode() does.
# Pass each key and value in params through encode_value(), join each pair with "=",
# join the pairs with "&", and add the result after "/search?".
# Print exactly this line:
# # /search?q=fish+%26+chips&page=2&limit=5
params = {"q": "fish & chips", "page": 2, "limit": 5}

def encode_value(value):
    text = str(value)
    for char, code in [("%", "%25"), ("&", "%26"), ("=", "%3D"), ("+", "%2B"), (" ", "+")]:
        text = text.replace(char, code)
    return text

### Your code starts here.



### Your code ends here.

Hide code cell source

### Solution
params = {"q": "fish & chips", "page": 2, "limit": 5}

def encode_value(value):
    text = str(value)
    for char, code in [("%", "%25"), ("&", "%26"), ("=", "%3D"), ("+", "%2B"), (" ", "+")]:
        text = text.replace(char, code)
    return text

pairs = [encode_value(key) + "=" + encode_value(value) for key, value in params.items()]
print("/search?" + "&".join(pairs))
/search?q=fish+%26+chips&page=2&limit=5
### 7. Check a status code
# Use a conditional to label the response. Print exactly this line:
# # Status: retry
response = {"status_code": 503, "body": {"message": "busy"}}

### Your code starts here.



### Your code ends here.

Hide code cell source

### Solution
response = {"status_code": 503, "body": {"message": "busy"}}
status = "ok" if response["status_code"] == 200 else "retry"
print("Status:", status)
Status: retry
### 8. Retry with exponential backoff
# FlakySource raises TransientError a set number of times before it succeeds.
# Write retry_call(call_fn, retries=4, base_delay=0.5) that calls call_fn() and,
# after each TransientError, records the backoff delay base_delay * 2 ** (attempt - 1)
# in a delays list instead of calling time.sleep(). On success, return a tuple of
# the result and delays. If the last attempt also fails, re-raise the error.
# Call it on FlakySource(3).get and print the result and the delays. Then call it
# on FlakySource(5).get, catch the TransientError, and print "Gave up:" with the
# error. Print exactly these three lines:
# # Result: {'status': 'ok', 'calls': 4}
# # Delays: [0.5, 1.0, 2.0]
# # Gave up: temporary timeout
class TransientError(Exception):
    pass

class FlakySource:
    def __init__(self, failures_before_success):
        self.failures_before_success = failures_before_success
        self.calls = 0

    def get(self):
        self.calls += 1
        if self.calls <= self.failures_before_success:
            raise TransientError("temporary timeout")
        return {"status": "ok", "calls": self.calls}

### Your code starts here.



### Your code ends here.

Hide code cell source

### Solution
class TransientError(Exception):
    pass

class FlakySource:
    def __init__(self, failures_before_success):
        self.failures_before_success = failures_before_success
        self.calls = 0

    def get(self):
        self.calls += 1
        if self.calls <= self.failures_before_success:
            raise TransientError("temporary timeout")
        return {"status": "ok", "calls": self.calls}

def retry_call(call_fn, retries=4, base_delay=0.5):
    delays = []
    for attempt in range(1, retries + 1):
        try:
            return call_fn(), delays
        except TransientError:
            if attempt == retries:
                raise
            delays.append(base_delay * (2 ** (attempt - 1)))

result, delays = retry_call(FlakySource(3).get)
print("Result:", result)
print("Delays:", delays)

try:
    retry_call(FlakySource(5).get)
except TransientError as error:
    print("Gave up:", error)
Result: {'status': 'ok', 'calls': 4}
Delays: [0.5, 1.0, 2.0]
Gave up: temporary timeout
### 9. Prepare a POST body
# Create a JSON-like body dictionary. Print exactly this line:
# # Body: {'name': 'Ana', 'role': 'analyst'}
name = "Ana"
role = "analyst"

### Your code starts here.



### Your code ends here.

Hide code cell source

### Solution
name = "Ana"
role = "analyst"
body = {"name": name, "role": role}
print("Body:", body)
Body: {'name': 'Ana', 'role': 'analyst'}
### 10. Validate a response contract
# contract maps each required key to its expected type.
# Write validate_payload(payload, contract) that raises ValueError with the message
# "Missing fields: [...]" (a list of missing keys in contract order) when keys are
# missing, raises ValueError with "Wrong type for field: <key>" for the first value
# whose type does not match, and otherwise returns a new dict with only the contract keys.
# Call it on good, missing, and bad_type in that order. Print the returned dict, or
# print "Error:" with the message when it raises. Print exactly these three lines:
# # {'city': 'Austin', 'temp': 31.5, 'humidity': 40}
# # Error: Missing fields: ['temp', 'humidity']
# # Error: Wrong type for field: temp
contract = {"city": str, "temp": float, "humidity": int}

good = {"city": "Austin", "temp": 31.5, "humidity": 40, "source": "sensor-7"}
missing = {"city": "Austin"}
bad_type = {"city": "Austin", "temp": "hot", "humidity": 40}

### Your code starts here.



### Your code ends here.

Hide code cell source

### Solution
contract = {"city": str, "temp": float, "humidity": int}

good = {"city": "Austin", "temp": 31.5, "humidity": 40, "source": "sensor-7"}
missing = {"city": "Austin"}
bad_type = {"city": "Austin", "temp": "hot", "humidity": 40}

def validate_payload(payload, contract):
    missing_keys = [key for key in contract if key not in payload]
    if missing_keys:
        raise ValueError(f"Missing fields: {missing_keys}")
    for key, expected_type in contract.items():
        if not isinstance(payload[key], expected_type):
            raise ValueError(f"Wrong type for field: {key}")
    return {key: payload[key] for key in contract}

for payload in [good, missing, bad_type]:
    try:
        print(validate_payload(payload, contract))
    except ValueError as error:
        print("Error:", error)
{'city': 'Austin', 'temp': 31.5, 'humidity': 40}
Error: Missing fields: ['temp', 'humidity']
Error: Wrong type for field: temp