Serving over Uvicorn¶
A raw ASGI callable and a converted FastAPI application, both served on
Uvicorn through the same plugin. service.ppy is the raw ASGI callable, and
api.ppy is a FastAPI application converted from api.py with no hand
editing. The whole folder checks under strict = true, route handlers
included, as their author wrote them.
Run it¶
What it prints¶
ppy service.ppy
ppy api.ppy, ppy run api.ppy
TestClient exercises the app in-process, which is why the output is
deterministic.
The helpers gain types; the routes keep theirs¶
@ppy.pure
def describe(item: Item) -> str:
return item.name + " x" + str(item.count)
@app.post("/items")
def create_item(item: Item):
return {"label": describe(item), "cost": total_cost(item.price, item.count)}
describe and total_cost got their types from the call sites and
@ppy.pure from the checker. describe reads pydantic fields through the
pydantic plugin.
create_item was left alone. @app.get is a decorator nobody can vouch
for, and FastAPI reads __annotations__ at import to build its validation,
so the conversion policy refuses to touch the signature.
What the plugin models¶
FastAPI(), APIRouter(), the route decorators, dependency markers
(Depends, Query, …), and the in-process TestClient with its responses
all have signatures under strict mode, so client.get("/").json()
type-checks.
uvicorn.run(app)with a statically resolvable application skips the per-worker re-import by module string.- The reloader is told to watch
.ppyalongside.py.
Where the code comes from¶
service.ppy is hand-written. api.ppy is Generated, not hand-written:
exactly what ppy convert api.py writes, and examples/verify_conversions.py
checks that on every run.
Read on: Pydantic · Plugins: Uvicorn and FastAPI
27_uvicorn/api.ppy¶
import ppy
import uvicorn
from fastapi import FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel
app: FastAPI = FastAPI()
class Item(BaseModel):
name: str
price: float
count: int = 1
@ppy.pure
def total_cost(price: float, count: int) -> float:
return price * count
@ppy.pure
def describe(item: Item) -> str:
return item.name + " x" + str(item.count)
@app.get("/")
def read_root():
return {"service": "inventory"}
@app.get("/items/{item_id}")
def read_item(item_id: int, q: str | None = None):
return {"item_id": item_id, "q": q}
@app.post("/items")
def create_item(item: Item):
return {"label": describe(item), "cost": total_cost(item.price, item.count)}
def serve() -> None:
uvicorn.run("api:app", host="127.0.0.1", port=8000)
def main() -> None:
client = TestClient(app)
print(client.get("/").json())
print(client.get("/items/7?q=fast").json())
print(client.post("/items", json={"name": "bolt", "price": 0.5, "count": 12}).json())
if __name__ == "__main__":
main()
27_uvicorn/service.ppy¶
from typing import Any, Awaitable, Callable
import ppy
import uvicorn
Scope = dict[str, Any]
Receive = Callable[[], Awaitable[Scope]]
Send = Callable[[Scope], Awaitable[None]]
@ppy.pure
def render(name: str, count: int) -> bytes:
return b'{"hello": "' + name.encode() + b'", "count": ' + str(count).encode() + b"}"
@ppy.pure
def status_for(path: str) -> int:
if path == "/":
return 200
return 404
async def app(scope: Scope, _receive: Receive, send: Send) -> None:
path: str = str(scope.get("path", "/"))
status: int = status_for(path)
body: bytes = render(path, len(scope))
await send(
{
"type": "http.response.start",
"status": status,
"headers": [(b"content-type", b"application/json")],
}
)
await send({"type": "http.response.body", "body": body})
def serve() -> None:
uvicorn.run("service:app", host="127.0.0.1", port=8000)
def develop() -> None:
uvicorn.run("service:app", host="127.0.0.1", port=8000, reload=True)
if __name__ == "__main__":
print(render("/", 3).decode(), status_for("/"), status_for("/missing"))
Source: examples/27_uvicorn.