Files
soleprint/soleprint/station/tools/docgen/export/specs/vanilla.py
2026-09-12 06:42:49 -03:00

293 lines
9.3 KiB
Python

"""
The API notebook: set parameters, call, print the result, change one parameter,
call again.
This is a **shape, not a description of any particular API**. The service it
drives is confidential and is not readable from here, so every place that needs
a real value carries a `# FILL:` comment instead of a guess. Search the emitted
notebook for `FILL` and that is the complete list of what has to be replaced —
nothing else in the file makes a claim about the API.
Why placeholders rather than an approximation: an endpoint that is nearly right
is worse than one that is obviously blank. The blank one gets filled in; the
nearly-right one gets run, fails somewhere in the middle, and costs whoever
opened it an afternoon deciding whether the notebook or the service is wrong.
The client is `urllib.request` from the standard library rather than `httpx` or
`requests`. A notebook that opens in Colab and runs without a `pip install` cell
is a notebook that runs; one that needs a dependency has a step before the first
step, and that step fails behind a proxy.
"""
from ..doc import Doc
# One place for the placeholder names, so the emitted notebook and the FILL list
# cannot disagree about what they are called.
BASE_URL = "https://api.example.invalid"
ENV_VAR = "API_TOKEN"
def build() -> Doc:
doc = Doc(title="API walkthrough")
doc.md(
f"""
# API walkthrough
Set the parameters, make a call, read the result, change a parameter, call
again. That is the whole notebook.
**Before running anything**, replace every `FILL` in the cells below. There are
no other values to change — anything not marked `FILL` is either standard
library or scaffolding.
| | |
|---|---|
| base URL | the `BASE_URL` cell |
| credentials | read from the `{ENV_VAR}` environment variable, never written here |
| endpoints | one section each, `FILL` on the path and the body |
Nothing is installed. The client below is `urllib.request` from the standard
library, so this runs on a bare Python 3 kernel and on Colab as-is.
**Credentials do not go in this file.** Set the environment variable before
starting the kernel, or use your platform's secret store. A token pasted into a
cell is a token in every copy of the notebook from then on, including the ones
in someone's Downloads folder.
"""
)
# --- Parameters ---------------------------------------------------------
doc.md(
"""
## 1. Parameters
Everything the calls depend on, in one cell, so changing where this points is
one edit in one place rather than a search through the notebook.
"""
)
doc.code(
f'''
import json
import os
import time
import urllib.error
import urllib.parse
import urllib.request
# FILL: the base URL of the service, no trailing slash
BASE_URL = "{BASE_URL}"
# FILL: confirm the variable name your deployment uses.
# Read from the environment on purpose — a token written into a cell travels
# with every copy of this notebook.
TOKEN = os.environ.get("{ENV_VAR}", "")
# FILL: the header the service expects. Bearer is the common case; some services
# want "X-Api-Key" or a query parameter instead.
AUTH_HEADER = {{"Authorization": f"Bearer {{TOKEN}}"}} if TOKEN else {{}}
TIMEOUT = 30 # seconds; raise it if the service is slow to warm up
VERBOSE = True # print the request line before each call
print(f"base {{BASE_URL}}")
print(f"token {{'set — ' + str(len(TOKEN)) + ' chars' if TOKEN else 'NOT SET — export {ENV_VAR}=... and restart the kernel'}}")
print(f"timeout {{TIMEOUT}}s")
'''
)
# --- Client -------------------------------------------------------------
doc.md(
"""
## 2. The client
One function for every call in the notebook. It returns the status, the headers
and the parsed body rather than raising on a non-2xx, because a 401 or a 422 is
a result worth reading — the body usually says what was wrong with the request,
and an exception throws that away.
"""
)
doc.code(
'''
def call(method, path, params=None, body=None, headers=None, timeout=None):
"""Make one request. Returns (status, headers, parsed_body).
Non-2xx is returned, not raised: the error body is the useful part.
"""
url = BASE_URL.rstrip("/") + "/" + path.lstrip("/")
if params:
url += "?" + urllib.parse.urlencode(params)
data = None
hdrs = {"Accept": "application/json", **AUTH_HEADER, **(headers or {})}
if body is not None:
data = json.dumps(body).encode()
hdrs["Content-Type"] = "application/json"
if VERBOSE:
print(f"-> {method} {url}")
req = urllib.request.Request(url, data=data, headers=hdrs, method=method)
started = time.time()
try:
with urllib.request.urlopen(req, timeout=timeout or TIMEOUT) as resp:
status, raw, got = resp.status, resp.read(), dict(resp.headers)
except urllib.error.HTTPError as e:
# An HTTPError *is* the response. Read it rather than re-raising.
status, raw, got = e.code, e.read(), dict(e.headers)
except urllib.error.URLError as e:
print(f"<- could not reach {url}: {e.reason}")
return None, {}, None
elapsed = time.time() - started
try:
parsed = json.loads(raw) if raw else None
except json.JSONDecodeError:
parsed = raw.decode("utf-8", "replace")
if VERBOSE:
print(f"<- {status} in {elapsed:.2f}s, {len(raw)} bytes")
return status, got, parsed
def show(result, limit=2000):
"""Print a result readably, and say so when it has been cut short."""
status, _, body = result
if status is None:
print("no response")
return
text = json.dumps(body, indent=2, ensure_ascii=False) if not isinstance(body, str) else body
print(f"status {status}")
print(text[:limit])
if len(text) > limit:
print(f"... {len(text) - limit} more characters")
'''
)
# --- Health -------------------------------------------------------------
doc.md(
"""
## 3. Is it up?
The cheapest call the service has. Run this first — every failure below is
easier to read once you know whether the problem is the request or the network.
"""
)
doc.code(
'''
# FILL: the service's health or version path
HEALTH_PATH = "/health"
show(call("GET", HEALTH_PATH))
'''
)
doc.code(
'''
# Reachability and latency, before anything that costs money.
for attempt in range(3):
started = time.time()
status, _, _ = call("GET", HEALTH_PATH)
print(f" attempt {attempt + 1}: {status} in {time.time() - started:.2f}s")
''',
live_only=True,
)
# --- First endpoint -----------------------------------------------------
doc.md(
"""
## 4. First call
The parameters live in their own cell above the call. That is the point of the
notebook: change the cell, re-run the two below it, compare.
"""
)
doc.code(
'''
# FILL: the path for this endpoint
ENDPOINT = "/resource"
# FILL: the query parameters it takes, with values that return something small
QUERY = {
"limit": 10,
}
'''
)
doc.code('result = call("GET", ENDPOINT, params=QUERY)\nshow(result)')
# --- Second endpoint ----------------------------------------------------
doc.md(
"""
## 5. A call with a body
Same shape, POST instead of GET. Keep the request body in its own cell for the
same reason as the query above.
"""
)
doc.code(
'''
# FILL: the path for the endpoint that takes a body
POST_ENDPOINT = "/resource"
# FILL: the request body. Keep it minimal — the smallest thing that returns a
# valid response, so a failure is about the endpoint and not about the payload.
PAYLOAD = {
"example": "value",
}
'''
)
doc.code('created = call("POST", POST_ENDPOINT, body=PAYLOAD)\nshow(created)')
# --- Update params and call again ---------------------------------------
doc.md(
"""
## 6. Change a parameter, call again
The comparison is the reason to do this in a notebook rather than with `curl`:
both results are still in memory, so the difference is one cell rather than two
terminal scrollbacks.
"""
)
doc.code(
'''
# FILL: the parameter worth varying, and a second value for it
QUERY = {**QUERY, "limit": 50}
second = call("GET", ENDPOINT, params=QUERY)
show(second)
'''
)
doc.code(
'''
first_status, _, first_body = result
second_status, _, second_body = second
print(f"first {first_status} {type(first_body).__name__}")
print(f"second {second_status} {type(second_body).__name__}")
# FILL: whatever "how much came back" means for this endpoint — a list length,
# a `total` field, a row count.
for name, payload in (("first", first_body), ("second", second_body)):
size = len(payload) if isinstance(payload, (list, dict, str)) else "n/a"
print(f"{name:8} size {size}")
'''
)
# --- Closing ------------------------------------------------------------
doc.md(
"""
## Notes
- Every `FILL` above is a value this notebook could not know. Nothing else needs
editing.
- `call` returns non-2xx rather than raising, so read the body on a failure —
it usually names the field that was wrong.
- If `URLError` comes back instead of a status, nothing reached the service:
check `BASE_URL`, then the network path, before changing anything about the
request.
- The token is read from the environment. If you change it, restart the kernel —
`os.environ` is read once, when the parameters cell runs.
"""
)
return doc