CogniSkill · REST APIs for IT Professionals
Content review copy. This does not replace the site theme. For SC-300, this preview shows additions; the existing teaching is retained on import.
An API returning status 200 is the beginning of validation, not the end. A collector may receive only the first page, accept an HTML sign-in page as data, follow an unexpected next link, or retry forever after a rate limit. This course builds a small client that makes those failure modes visible and refuses to turn partial collection into an authoritative inventory.
You will understand an HTTP request/response contract, distinguish authentication from permission, interpret JSON, traverse bounded pages, handle selected transient failures, and build a reproducible test package. The code uses invented responses and a reserved example hostname. It makes no network requests, contains no credentials and changes no tenant. No cloud account or book purchase is required.
Python Foundations is recommended for the executable project. Learners concentrating on operational API concepts can trace the fixtures and test matrix before writing their own adapter. Use Python 3.10 or later in a local practice directory; the code requires only the standard library. The inventory-reconciliation course is a useful next step because it consumes the kind of export this client is designed to produce.
Four chapters, with practical work
- Read the contract behind a request
- Validate pages and follow links within a boundary
- Add bounded retries and a working collector
- Independent tests, an export adapter and an operational review
Applied capability and learning route
Design safe request handling for pagination, retries and uncertain write outcomes.
Follow the lessons in order: establish the model, trace the supplied evidence, attempt the case, compare the worked solution, and complete the changed-condition project. The target is intermediate reasoning within this stated scope, not mastery of the entire profession or product. If a prerequisite is unfamiliar, pause and use the linked foundational teaching before attempting the project.
All teaching, explained answers and core synthetic practice are free. Some operational labs require separately available software, a licensed test tenant or an authorized lab; offline completion does not claim hands-on product proficiency.
How to complete this course
Study the worked examples, attempt the exercises before opening solutions, and submit your own project notes. Completion records are self-reported; practical competence requires the stated verification.
Read the contract behind a request
Identify the resource, method and representation
An HTTP request has a method, a target resource, headers and sometimes a body. A response has a status, headers and sometimes a representation. REST is an architectural approach; an endpoint calling itself REST does not remove the need to inspect its actual provider contract.
Our fictional resource is https://api.example.invalid/v1/assets. Its collection operation behaves like a GET: the client requests asset representations rather than changing asset ownership. The versioned path identifies the interface contract. A cursor query selects the next part of a collection. An Accept: application/json header would state a preference to a real provider; the received Content-Type still needs verification.
A target URI is not just a string to concatenate blindly. Scheme and origin determine where the request goes. The path identifies the resource. Query parameters influence selection. A fragment is normally a client-side identifier rather than a field the HTTP server receives in the request target. Our narrow client rejects fragments because they have no role in this page contract.
Do not put an access token in a query string for this project. Query strings can appear in logs, browser history and copied links. A real authentication adapter should use its provider's documented credential transport and avoid logging the credential. The offline fixture needs none.
Interpret status in context
A status in the success range does not guarantee the representation is the expected data. A particular endpoint may return 200 with a JSON object, 204 without a body, or another documented success outcome. Our collection contract requires exactly 200 with the expected JSON page. It does not try to parse an empty 204 as an inventory.
A 400 response often indicates a request-contract problem, such as an invalid query. A 401 response commonly concerns missing or unacceptable authentication, while 403 commonly indicates that the authenticated request is forbidden. Providers can expose more specific error codes and policies; read those before adding privileges or repeatedly refreshing a token. A 404 can mean a missing resource, a wrong path or an intentionally concealed resource. Status alone does not prove which explanation applies.
A 429 communicates rate limiting. A 503 communicates a service-unavailable condition. They may be eligible for a bounded retry under the endpoint's contract. Our example retries only those two statuses and a transport timeout. It stops for a forbidden request, invalid data or an unsupported redirect. Retrying a schema defect usually repeats the same problem; retrying permission failure can amplify noise without improving access.
Record the operation, safe correlation context, status and next diagnostic question. Do not log every response body automatically: it may contain confidential fields even when the request fails. This course's fictional data can be printed safely, but the habit should remain selective.
Authentication and authorization answer different questions
Authentication establishes the identity or credential accepted for a request. Authorization decides whether that identity can perform this operation on this resource. A token with a valid signature may still target a different API audience, lack the required scope, or represent an identity that has no access to the requested population.
In a human delegated flow, permissions can depend on both application consent and the signed-in user's authority. A workload flow has a different identity and permission model. The details are provider-specific. Before writing a collector, document the endpoint, operation, required permission, resource scope, identity owner and token-storage boundary.
“Use an administrator token” is not a dependable troubleshooting method. It can hide the actual missing permission and expand the collector's access far beyond the report. A read collector should use the narrow permission required for its resource, and changes to access require the environment's approved process. The lab avoids that dependency by using a transport that returns fixed in-memory responses.
Read the page schema manually
Our provider returns an object with exactly three properties:
{
"items": [
{"id":"A-002","owner_id":"u102","site":"HQ"},
{"id":"A-001","owner_id":"u101","site":"HQ"}
],
"next": "https://api.example.invalid/v1/assets?cursor=page2",
"total": 3
}
items is an array of asset objects. next is either an approved absolute page URL or null. total is the number of distinct assets this collection claims to expose under the current query and access scope. It is an integer, not a numeric-looking string or a Boolean. The first page has two items even though the collection advertises three.
Each item has a stable case-sensitive id, an owner identity and an accepted site code. A valid item identifier has the form A-001. Exact field validation is intentionally strict in this first version. Accepting optional fields or schema evolution is a later project, with a documented compatibility policy.
Prediction exercise
The second page has one item, A-003,u103,BRANCH, a null next link and total 3. Predict the final sorted identifiers and number of transport calls. You should collect A-001, A-002, A-003 in that order with two successful page calls. The first page's display order does not become the export ordering contract.
Reason about a misleading first-page report
If the client returns the first page's two items and ignores the next link, it has omitted A-003. A later reconciliation could wrongly call A-003 register-only. HTTP 200 on the first request establishes only a successful first-page response under this endpoint contract, not completed collection.
Reasoning model
Read and write retries have different consequences. A timed-out response does not prove the server did not perform the operation. For a create request, blindly retrying may duplicate a resource unless the API provides an applicable idempotency mechanism or a supported reconciliation path. Read the service contract for pagination, status handling and retry guidance. Keep credentials out of URLs, exception dumps and sample logs.
Official reference for further study
Validate pages and follow links within a boundary
JSON syntax is weaker than an application schema
A JSON parser can produce dictionaries, lists, strings, numbers, Booleans and null values. A syntactically valid object such as {"message":"sign in"} is not an asset page. Validate required keys, types and domains before adding anything to the collected population.
Our parser also rejects duplicate JSON properties and non-standard numeric constants. Some parsers accept duplicate properties by taking a later value, and Python's default JSON behavior permits special numeric constants beyond strict JSON. Those convenient behaviors would make a strict total contract ambiguous. The code explicitly chooses rejection.
The Content-Type check prevents an obvious representation mismatch such as a 200 HTML login page. It does not prove that a JSON body is trustworthy or that a provider's header is truthful. Schema and identity checks still apply. A malformed UTF-8 body is rejected instead of decoded with replacement characters that could alter an identifier.
A maximum accepted body size limits what this parsing layer accepts, but the offline transport already holds bytes in memory. A real HTTP adapter must enforce a bounded download before materializing an enormous body. Checking size after an unbounded network read is not the same as controlling memory during collection.
Understand cursor pagination
The provider's next link is opaque collection state. Follow the documented link or token rather than guessing that page2 can be incremented to page3. Offset and cursor APIs have different contracts, and a provider may require preserving particular query parameters across pages.
This fictional API allows only the cursor query parameter with one non-empty value of bounded length. The client accepts a next link only if it uses HTTPS, the exact approved origin and the same resource path. It rejects credential-bearing or foreign origins, unexpected paths, duplicate cursor parameters and fragments. These are deliberate limits of the lab adapter, not a universal rule that every API uses this URL structure.
A real provider may return relative links, separate cursor tokens or links containing additional fixed query parameters. Extend the adapter from its documentation and test the accepted forms. Do not relax origin checks merely because a first attempt fails. Authentication belongs to a defined destination.
Detect cycles by meaningful state
A server bug can return a next link that repeats a page already visited. A loop that keeps requesting that link can run indefinitely or accumulate duplicate records. The client records the accepted cursor state and rejects a repeat before another request.
It compares the parsed cursor state rather than raw URL spelling. Encoding differences can make two URL strings look different while selecting the same cursor. Our contract's single cursor parameter makes that canonical state simple. More complex provider state needs a carefully defined canonical request identity.
The client also has a finite page budget. A non-repeating sequence of bad cursors should not escape the cycle check and continue forever. Under the default budget it accepts at most ten pages, with at most three attempts per page. Hitting a budget causes collection failure, not a successful shortened inventory.
Reconcile records and advertised totals
After reaching a terminal page, the number of distinct accepted items must equal the advertised total. The total must remain unchanged across pages. More collected items than the declared total causes immediate rejection. A duplicate item identity also causes rejection; the code does not silently overwrite or remove it.
These choices make changes during collection visible, but they do not guarantee a transactionally consistent snapshot. A provider can return three different valid records while keeping total 3. A directory could change between pages. A provider's snapshot token, versioning guarantees or collection design would be needed for stronger consistency.
A completed authorized collection is also not automatically an organization-wide inventory. The token may be scoped to one group or business unit. The advertised total is the population visible to this operation. Preserve query and authorization-scope evidence when connecting the collector to an operational report.
Page-fault exercise
Trace four proposed second pages. Page A repeats A-002 instead of supplying A-003. Page B supplies A-003 but changes total from 3 to 4. Page C supplies no items, null next and total 3. Page D supplies A-003 but sends a next URL on another origin.
All four should fail the collection contract. A is duplicate identity, B is inconsistent total, C is terminal count mismatch, and D is an unapproved next link. In D, the client must not request the foreign URL. A test that merely checks the final exception misses that important boundary; inspect transport-call history too.
Evidence laboratory — attempt before reading the solution
A mocked ticket API receives POST create-ticket with client reference R-71. The server creates ticket T-9, but the response is lost. The client sees a timeout. The fictional API supports a documented idempotency key: identical requests using the same key return the original result during a stated retention window. A faulty client generates a new key for every retry. Explain the uncertainty and design a correct retry sequence within the stated window.
Official reference for further study
Add bounded retries and a working collector
A timeout is an uncertain transport outcome
A timeout tells the client that it did not obtain the expected response within its transport boundary. For a read-only operation, a bounded retry may be reasonable. For a mutation, a timeout does not prove the server did nothing. The server could have created a ticket while the response was lost.
Our fictional collection operation is a GET-style read. The client retries a timeout at most twice after the first failed attempt. A network adapter must actually honor the supplied timeout. A timeout argument in a function signature alone does not enforce a real deadline; the in-memory fixture simply records it and raises the chosen fault.
The example does not implement a complete wall-clock deadline for the whole collection. Default page and attempt budgets bound counts, while each real request would need an effective timeout. A production integration may need a total execution deadline, cancellation and scheduler behavior. Those are explicit design tasks in the project section.
Respect rate limits within a delay budget
For a retryable status, the example uses a small backoff unless it receives an accepted Retry-After delay. It supports non-negative decimal delay seconds. HTTP also permits a date form; this first adapter does not interpret that form and falls back to its bounded backoff. A real provider adapter must document and test its date handling rather than claim full header support.
If a supported delay is longer than the client's five-second wait budget, the code stops and reports that limitation. It does not shorten a requested thirty-second wait to five seconds and then hammer the provider too soon. A scheduler could defer the job for the requested interval; that is a different policy from waiting inside this small collector.
The offline examples record delays in a list. They do not sleep or contact a server. This lets you verify retry behavior immediately and deterministically. It also means that a passing lab does not validate real timing, network cancellation or provider rate-limit semantics.
Keep mutation retries separate
An operation is idempotent when repeating the same request has the same intended effect as making it once under its contract. That does not mean every response body or log entry must be identical. GET is intended to be safe; PUT and DELETE have idempotent semantics in HTTP, but an API's implementation and business workflow still need verification. A generic POST retry is not automatically safe.
For a ticket-creation endpoint, a provider-supported idempotency key or a durable request identity may let the client recover from an ambiguous response. Merely generating a random header does not make the server honor it. The provider must define key scope, retention, conflict behavior and a way to establish the prior result.
This course implements no mutation path. Its collector cannot create, delete or reassign an asset. In your project design, explicitly keep the read transport and any future write workflow separate so a retry helper meant for reads is not casually reused for state-changing operations.
Save the complete example
Save this original course program as asset_client.py. FixtureTransport is the whole transport for the lab: it returns selected responses and records calls. There is no hidden network implementation.
"""Offline GET-style page collector. It has no network adapter or credentials."""
from dataclasses import dataclass
import json
import re
from urllib.parse import parse_qs, urlsplit
BASE = 'https://api.example.invalid/v1/assets'
MAX_BODY = 500_000
MAX_ITEMS = 1_000
class ApiError(Exception):
pass
@dataclass
class Response:
status: int
headers: dict
body: bytes
def cursor_key(url):
if not isinstance(url, str):
raise ApiError('Page URL must be text')
try:
parsed = urlsplit(url)
except ValueError as error:
raise ApiError('Malformed page URL') from error
if parsed.scheme != 'https' or parsed.netloc != 'api.example.invalid' or parsed.path != '/v1/assets' or parsed.fragment:
raise ApiError('Page URL is outside the approved origin/resource')
params = parse_qs(parsed.query, keep_blank_values=True)
if not params:
return ''
if set(params) != {'cursor'} or len(params['cursor']) != 1 or not re.fullmatch(r'[A-Za-z0-9_-]{1,64}', params['cursor'][0]):
raise ApiError('Invalid pagination cursor')
return params['cursor'][0]
def unique_object(pairs):
result = {}
for key, value in pairs:
if key in result:
raise ApiError('Duplicate JSON property')
result[key] = value
return result
def reject_constant(value):
raise ApiError('Non-standard JSON numeric constant')
def parse_page(response):
if not isinstance(response.body, bytes) or len(response.body) > MAX_BODY:
raise ApiError('Response body exceeds the accepted contract')
raw_type = response.headers.get('content-type', '')
if not isinstance(raw_type, str):
raise ApiError('Content-Type must be text')
content_type = raw_type.split(';', 1)[0].strip().lower()
if content_type != 'application/json':
raise ApiError('Expected application/json')
try:
page = json.loads(response.body.decode('utf-8'), object_pairs_hook=unique_object, parse_constant=reject_constant)
except (ValueError, UnicodeError) as error:
raise ApiError('Invalid JSON response') from error
if not isinstance(page, dict) or set(page) != {'items', 'next', 'total'}:
raise ApiError('Unexpected page schema')
if type(page['total']) is not int or not 0 <= page['total'] <= MAX_ITEMS:
raise ApiError('Invalid advertised total')
if not isinstance(page['items'], list):
raise ApiError('items must be an array')
if page['next'] is not None:
cursor_key(page['next'])
for row in page['items']:
if not isinstance(row, dict) or set(row) != {'id', 'owner_id', 'site'}:
raise ApiError('Unexpected item schema')
if not isinstance(row['id'], str) or not re.fullmatch(r'A-[0-9]{3}', row['id']):
raise ApiError('Invalid item identity')
if not isinstance(row['owner_id'], str) or not re.fullmatch(r'u[0-9]{3}', row['owner_id']):
raise ApiError('Invalid owner identity')
if row['site'] not in ('HQ', 'BRANCH'):
raise ApiError('Invalid site code')
return page
def read_page(transport, url, sleep, attempts=3):
for attempt in range(attempts):
response = None
try:
response = transport.fetch(url, timeout=5)
except TimeoutError:
retryable = True
else:
if type(response.status) is not int or not 200 <= response.status <= 599:
raise ApiError('Invalid HTTP status contract')
if response.status == 200:
return parse_page(response)
retryable = response.status in (429, 503)
if not retryable:
raise ApiError(f'Non-retryable HTTP status {response.status}')
if attempt == attempts - 1:
raise ApiError('Page request exhausted its retry budget')
delay = min(5, 2 ** attempt)
if response is not None:
raw = response.headers.get('retry-after', '')
if isinstance(raw, str) and re.fullmatch(r'[0-9]{1,6}', raw):
seconds = int(raw)
if seconds > 5:
raise ApiError('Retry-After exceeds this client delay budget')
delay = seconds
sleep(delay)
raise ApiError('No page response')
def collect(transport, sleep, max_pages=10):
if type(max_pages) is not int or not 1 <= max_pages <= 100:
raise ApiError('max_pages must be an integer from 1 to 100')
url = BASE
visited, identities = set(), set()
items, total = [], None
for page_number in range(max_pages):
cursor = cursor_key(url)
if cursor in visited:
raise ApiError('Pagination cycle detected')
visited.add(cursor)
page = read_page(transport, url, sleep)
if total is None:
total = page['total']
elif total != page['total']:
raise ApiError('Advertised total changed across pages')
for row in page['items']:
if row['id'] in identities:
raise ApiError('Duplicate identity across collected pages')
identities.add(row['id'])
items.append(row.copy())
if len(items) > MAX_ITEMS or len(items) > total:
raise ApiError('Collected population exceeds its advertised limit')
if page['next'] is None:
if len(items) != total:
raise ApiError('Terminal population does not reconcile to advertised total')
return sorted(items, key=lambda row: row['id'])
url = page['next']
raise ApiError('Pagination exceeded the page budget')
class FixtureTransport:
def __init__(self, responses):
self.responses = {key: list(value) for key, value in responses.items()}
self.calls = []
def fetch(self, url, timeout):
self.calls.append((url, timeout))
choices = self.responses.get(url, [])
if not choices:
raise ApiError('No response remaining for this fixture URL')
result = choices.pop(0)
if isinstance(result, Exception):
raise result
return result
def json_response(page, status=200, headers=None):
values = {'content-type': 'application/json'}
values.update(headers or {})
return Response(status, values, json.dumps(page).encode('utf-8'))
def baseline_transport():
return FixtureTransport({
BASE: [json_response({'items': [
{'id':'A-002','owner_id':'u102','site':'HQ'},
{'id':'A-001','owner_id':'u101','site':'HQ'},
], 'next':BASE+'?cursor=page2','total':3})],
BASE+'?cursor=page2': [json_response({'items':[
{'id':'A-003','owner_id':'u103','site':'BRANCH'},
],'next':None,'total':3})],
})
if __name__ == '__main__':
transport = baseline_transport()
delays = []
print(json.dumps(collect(transport, delays.append), indent=2))
print(f'{len(transport.calls)} fixture requests; simulated retry delays: {delays}')
Run python asset_client.py. The printed result must contain three sorted assets and say that two fixture requests occurred with no retry delays. Trace the first collect iteration: validate the initial URL, read and parse page one, record its identities, then move to the approved page-two link. Trace the second iteration and its terminal-count reconciliation.
Notice that the collection function returns only after the terminal population has validated. On an error it raises ApiError rather than returning the partially accumulated list. A caller must not catch that error and treat an empty replacement list as a successful collection. Error handling is part of the integration contract.
Inject a rate limit without waiting
Create a short practice runner beside the program:
from asset_client import BASE, FixtureTransport, Response, collect, json_response
transport = FixtureTransport({
BASE: [
Response(429, {'retry-after': '2'}, b''),
json_response({'items': [], 'next': None, 'total': 0}),
]
})
delays = []
print(collect(transport, delays.append))
print(delays)
print(len(transport.calls))
Expected outputs are an empty accepted collection, the delay list [2], and two calls. The empty collection is accepted because the valid terminal page advertises total zero. It does not arise from converting the 429 into an empty result.
Replace 429 with 403. The client should stop after one call and never record a retry delay. Replace the delay with 30. It should stop because that delay exceeds the example's wait budget, rather than retry after two seconds. These cases test a policy distinction, not just whether a loop executes.
Do not hide response faults behind a success message
Try a response with status 200, HTML content type and a sign-in message. The parser must reject it. Try valid JSON with a string total "3", a duplicate JSON key, or a missing item identifier. Those must also fail. A successful transport exchange is not a successful inventory collection.
When connecting this design to a real HTTP library, define how it exposes non-success responses and headers. Normalize header names consistently in the adapter. Disable or explicitly validate redirects before forwarding authentication to any redirected destination. The offline origin check cannot protect an automatic redirect hidden inside a future network library.
Log a safe operation identifier, attempt count and failure category for troubleshooting. Keep access tokens, raw authorization headers and unnecessary asset details out of routine logs. The lab's test history deliberately records URL and timeout only, and the URL contains no credential.
Worked laboratory solution
Compare your reasoning with the worked solution
The outcome is unknown to the client, not definitely failed. Reusing the original key with the same request under the API’s contract allows reconciliation with T-9; a new key describes a different operation and can create a duplicate. Persist the operation identity before sending if recovery across process restarts is required. Do not generalize this behavior to APIs that do not document it. When no idempotency support exists, use an approved query/reconciliation workflow or surface the uncertainty for an operator.
Official reference for further study
Workbook already downloaded
Independent tests, an export adapter and an operational review
Test both the result and the request history
Save this as test_asset_client.py beside the collector:
import unittest
from asset_client import ApiError, BASE, FixtureTransport, Response, baseline_transport, collect, json_response
class ClientTests(unittest.TestCase):
def test_baseline_two_pages(self):
transport = baseline_transport()
actual = collect(transport, lambda seconds: None)
self.assertEqual(actual, [
{'id':'A-001','owner_id':'u101','site':'HQ'},
{'id':'A-002','owner_id':'u102','site':'HQ'},
{'id':'A-003','owner_id':'u103','site':'BRANCH'},
])
self.assertEqual(len(transport.calls), 2)
def test_rate_limit_wait_then_success(self):
ok = json_response({'items':[], 'next':None, 'total':0})
transport = FixtureTransport({BASE:[Response(429,{'retry-after':'2'},b''),ok]})
delays = []
self.assertEqual(collect(transport, delays.append), [])
self.assertEqual(delays, [2])
self.assertEqual(len(transport.calls), 2)
def test_forbidden_not_retried(self):
transport = FixtureTransport({BASE:[Response(403,{},b'')]})
with self.assertRaisesRegex(ApiError,'Non-retryable'):
collect(transport, lambda seconds: None)
self.assertEqual(len(transport.calls), 1)
def test_cross_origin_next_not_requested(self):
transport = FixtureTransport({BASE:[json_response({'items':[], 'next':'https://other.example/v1/assets?cursor=x', 'total':0})]})
with self.assertRaisesRegex(ApiError,'approved origin'):
collect(transport, lambda seconds: None)
self.assertEqual(len(transport.calls), 1)
def test_terminal_count_must_reconcile(self):
transport = FixtureTransport({BASE:[json_response({'items':[], 'next':None, 'total':3})]})
with self.assertRaisesRegex(ApiError,'does not reconcile'):
collect(transport, lambda seconds: None)
def test_timeouts_have_a_finite_budget(self):
transport = FixtureTransport({BASE:[TimeoutError(),TimeoutError(),TimeoutError()]})
delays = []
with self.assertRaisesRegex(ApiError,'retry budget'):
collect(transport, delays.append)
self.assertEqual(len(transport.calls),3)
self.assertEqual(delays,[1,2])
if __name__ == '__main__':
unittest.main()
Run python -m unittest -v. The baseline result is a literal expected list. Retry tests inspect delays and request counts. The foreign-next-link test verifies that only the initial request happened. The timeout test verifies a finite attempt budget instead of merely waiting for an eventual exception.
Introduce a deliberate defect in a separate copy: return immediately after validating page one. The baseline test must fail because A-003 is missing and only one page was requested. Restore the program and rerun. This demonstrates that the tests distinguish a complete collector from a plausible first-page demo.
Add your own test for a cursor cycle and for a duplicate item on page two. Choose data that would produce different results if the faulty implementation silently overwrote a prior item. A fixture with no duplicate cannot verify a duplicate-rejection rule.
Project A — Export into the inventory reconciliation contract
Build a wrapper that calls the collector, maps id to asset_id, and writes the accepted fields to observed.csv plus an export manifest. Use a newly reserved output directory and never overwrite inputs. The manifest should record complete only after collection and export succeed.
Use the returned item count for the exported row count, and a timezone-aware capture timestamp whose meaning you document. In a real paginated collection, the start and finish timestamps may both be useful. A single timestamp must not falsely imply that every record was observed at exactly that instant.
If collection fails on page two, the wrapper must not publish a successful empty or partial observed inventory. Demonstrate this with the timeout fixture. Then use a valid two-page export with the Python automation course's register input and explain its comparison results.
Worked mapping and reconciliation expectations
The valid API fixture exports A-001/u101/HQ, A-002/u102/HQ and A-003/u103/BRANCH. Compared with that course's four-row register, the only finding is A-004 as register-only. The API has supplied three assets, so the observed export manifest must declare three parsed records, not four and not two.
Project B — Add a total deadline with a controllable clock
Design a total collection deadline in addition to per-request timeout and page limits. Inject a clock into the collector so tests can advance time without real sleeping. Decide whether a retry may start when the remaining deadline is shorter than the planned delay plus a request timeout. State your choice explicitly.
Use cases with time remaining, a deadline reached exactly at a boundary, and a delay longer than the remaining budget. Ensure the collector raises a clear failure and does not return partial items as complete. If you add real sleeping later, use a monotonic clock for elapsed-time reasoning rather than assuming the wall clock never changes.
A total deadline is not the same as a scheduler guarantee. The HTTP adapter must honor cancellation or timeout behavior, and the scheduler must know whether to retry later. Your README should distinguish the behavior you tested offline from what requires provider and network verification.
Project C — Make schema evolution a reviewed decision
The current page parser requires exactly three page properties and three item properties. Extend it to tolerate documented optional item fields while still rejecting missing required fields, wrong identity types and duplicate JSON keys. Define which unknown fields are ignored, which are preserved and which are rejected.
Test a harmless optional display name, an unknown field that violates your policy, a missing id, and an id with a wrong type. Keep the exported inventory schema stable unless you deliberately version it. Do not silently choose an arbitrary field as identity when the expected field is absent.
Project D — Write the real-provider adapter review
Before implementing a network adapter, prepare a review table covering endpoint, resource population, read permission, token audience, credential storage, redirects, response-size limits, error representation, pagination consistency, retry rules and cancellation. Link each provider-specific claim to its official documentation and record what needs a tenant test.
A passing offline client proves the fixture behavior. It does not prove that the actual endpoint supplies this schema, supports these cursors, uses stable totals, or permits the same retry policy. This review is how the course becomes useful beyond copied sample code: you can identify the differences that matter before connecting it to a live system.
Final acceptance and reasoning check
Deliver the program, tests, baseline response fixture, fault matrix and one completed extension. Your evidence must demonstrate two-page completion, a bounded retry, a non-retried forbidden request, an unrequested foreign link and a terminal count mismatch. State the accepted result grain and population scope.
Explain why status 200 is insufficient; why the final collected count needs reconciliation; why a permission failure is not repaired by an endless retry; why a missing second page cannot become an authoritative absence finding; and why a read retry helper cannot automatically provide exactly-once ticket creation.
The client is useful when its evidence boundary remains visible to the next system. Completing the website's learning check records study progress. It does not certify a provider integration or authorize changes to permissions, credentials or assets.
References: HTTP semantics in https://www.rfc-editor.org/rfc/rfc9110.html and Python's official json and urllib.parse documentation at https://docs.python.org/3/library/ . The fixture client is original course material and intentionally contains no real network adapter.
Transfer project — changed conditions
Build a mock client test for success, timeout-after-commit, 429 with bounded retry and a permanent authorization error. Add a paginated read with an empty final page and verify completion. Record request correlation and safe metadata without tokens or full personal data. Explain how changed payloads with a reused key should be handled according to the fictional contract rather than guessed by the client.
Practical assessment rubric
Score each dimension 0–2: 0 = absent or materially incorrect; 1 = partly correct with an unresolved gap; 2 = correct, explicit and supported by the supplied evidence or an actual labeled lab result.
- Evidence/model: identify the relevant entities, inputs, assumptions and boundaries without inventing facts.
- Main result: derive the case-specific expected result, calculation or decision and show the intermediate reasoning.
- Failure/boundary: test the changed condition and explain why the original conclusion does or does not transfer.
- Verification: supply reproducible steps and expected/observed results; label manual traces as manual, not executed.
- Communication/safety: explain limitations, owners and a proportionate next action without unsafe or unnecessary access.
Target: at least 8/10, with no zero in main result or communication/safety. This is a local learning rubric, not an accredited certification standard. A written/manual solution demonstrates reasoning only; operational competence additionally requires the relevant executable lab in an authorized environment. The native multiple-choice score is separate and does not automatically grade this rubric.
Submission template
- My input/evidence and assumptions:
- Expected result with calculations or trace:
- My implementation/decision and justification:
- Changed-condition result:
- Observed execution, or explicitly labeled manual trace:
- Failure and recovery evidence:
- Unverified limitation and next action:
- Self-score with evidence for each dimension:
Remediation
If the main result is wrong, reconstruct the supplied case from the raw inputs before changing code or controls. If the boundary case is wrong, identify the assumption that changed and rerun both the original and changed case. If verification is missing, do not convert a prediction into an observed result. Reattempt the transfer task without copying the worked solution, then explain it to another person.
Official reference for further study
Assessment: explain your decisions
Choose an answer and write your reason before opening each explanation. Then explain why the other choices do not fit the evidence. Use missed questions to revisit the lesson. This knowledge check is only one part of assessment: complete the practical transfer project as well. It does not predict certification-exam success.
1. How many rows are in the sample across both pages?
- Three
- Two
- One
Answer and explanation
Three — Two plus one equals three.
2. What should a throttled page become?
- An empty successful page
- A bounded retry or explicit incomplete result
- An infinite immediate retry
Answer and explanation
A bounded retry or explicit incomplete result — Completeness must not be silently lost.
3. Which data should be excluded from debug output?
- Status category
- Accepted count
- Authorization token
Answer and explanation
Authorization token — A token is a credential.
4. Page one returns HTTP 200, two items, next=page2 and total=3. Can a collector publish those two items as the completed inventory?
- No; follow approved pages and reconcile the terminal population.
- Yes, if the two item identities are unique.
- Yes, because HTTP 200 certifies complete collection.
Answer and explanation
No; follow approved pages and reconcile the terminal population. — HTTP success for a page is not terminal collection. The collector must follow the next-link contract and verify the final count; otherwise a later report can invent absence findings for omitted records.
5. A 429 response requests a 30-second delay, but the example client has a five-second delay budget. What should it do?
- Immediately retry three times to obtain a success response.
- Stop with a clear budget limitation so scheduling policy can decide what happens next.
- Wait five seconds and retry sooner than requested.
Answer and explanation
Stop with a clear budget limitation so scheduling policy can decide what happens next. — Capping a required supported delay downward would retry too soon. This small client refuses the long wait rather than pretending to support it. A scheduler or provider adapter can implement an approved deferral policy.
6. Page one points to a different origin for the next page. What should the narrow fixture collector do?
- Forward the same authentication automatically.
- Treat the first page as complete to avoid requesting the link.
- Reject the link before making a request to that origin.
Answer and explanation
Reject the link before making a request to that origin. — Pagination must stay within the adapter destination contract. Returning the first page as complete is also wrong. A real transport must validate redirects as well as explicit next links before forwarding credentials.
7. What does the timeout establish?
- The client lacks a response; the server may already have committed.
- No ticket was created.
- Exactly two tickets were created.
Answer and explanation
The client lacks a response; the server may already have committed. — Transport uncertainty does not determine server-side commit state. The fixture explicitly describes a committed ticket.
8. Which retry follows the supplied idempotency contract?
- Retry forever regardless of response.
- Same operation key and identical request within the supported window.
- A new random key on every attempt.
Answer and explanation
Same operation key and identical request within the supported window. — Stable operation identity is what allows the service to recognize the retry as the same intended action.
9. Should the client retry a persistent authorization error as if it were throttling?
- Yes; permissions always improve with repetition.
- Yes; increase the request rate.
- No; diagnose authorization rather than repeatedly sending the same invalid request.
Answer and explanation
No; diagnose authorization rather than repeatedly sending the same invalid request. — Authorization failure and transient throttling have different causes and recovery actions.
Use the explained questions above for offline practice. A scored attempt requires the course assessment on CogniSkill.ca.