OpenAPI and security¶
Use this when you want API documentation (Swagger UI) built from your routes, and a way to sign in from it.
from korvex import App
app = App()
app.docs(title="Tasks", version="1.0") # /openapi.json and Swagger UI at /docs
The document is built from the routes: paths, typed parameters, bodies,
returned models, and each handler's docstring (its first line is the
summary). app.openapi() gives it as a dict. Swagger UI comes from a CDN;
docs_path=None leaves it out.
What else to say about a route¶
from uuid import UUID
import msgspec
from korvex import App, Response
class Task(msgspec.Struct): # your models
id: UUID
title: str
class NewTask(msgspec.Struct):
title: str
class Conflict(msgspec.Struct):
detail: str
app = App()
api = app.group("/api", tags=["tasks"], responses={401: "Not signed in"})
@api.post("/tasks", status=201, responses={409: ("Title taken", Conflict)})
def create_task(body: NewTask) -> Task:
return Task(id=UUID(int=1), title=body.title)
@api.delete("/tasks/{id:uuid}", status=204, responses={404: "No such task"})
def delete_task(id: UUID) -> None: ...
@app.get("/v1/tasks", deprecated=True)
def old_tasks() -> list[Task]: ...
@app.get("/health", include_in_schema=False)
def health():
return Response.text("ok")
status: the success, and the status of what the handler returns (a model, orNonefor an empty response). A returnedResponsekeeps its own.responses: other answers, by status: a description, a model, or both ((description, model))."4XX"and"default"work too.tags,deprecated,include_in_schema: as they say.- On a group or a host, they apply to its routes: tags add up, and a route's responses replace the group's for the same status.
Signing in from Swagger UI¶
korvex.security reads a credential from the request, refuses the
request without one (401), and tells OpenAPI, so Swagger UI's
"Authorize" button sends it:
from typing import Optional
import msgspec
from korvex import App, Depends, HTTPException
from korvex.security import Bearer
class User(msgspec.Struct):
name: str
class Users: # your code: look a token up in your database
async def by_token(self, token: str) -> Optional[User]:
return User("ada") if token == "t0ken" else None
users = Users()
app = App()
bearer = Bearer() # Authorization: Bearer <token>
async def current_user(token: str = Depends(bearer)) -> User:
user = await users.by_token(token)
if user is None:
raise HTTPException(401, "invalid token", headers={"WWW-Authenticate": "Bearer"})
return user
@app.get("/me")
def me(user: User = Depends(current_user)) -> User:
return user
Bearer(format="JWT"),APIKey(header="X-API-Key")(orquery=,cookie=) andOAuth2Password(token_url="/token", scopes={...})(withrefresh_url=if tokens can be refreshed). Prefer a header for an API key: one in the query string ends up in access logs.- Each takes
name=(its name in OpenAPI) anddescription=(shown in Swagger UI). - A scheme only reads the credential; the dependency that takes it
checks it, like
current_userabove. required=FalsegivesNoneinstead of a 401.- A route lists every scheme it depends on, directly, through another dependency or through its group.
Your own scheme¶
Subclass SecurityScheme: credential() finds it in the request,
openapi() describes it, and challenge goes in the 401's
WWW-Authenticate. HTTP Basic, for example:
from korvex import App, Depends, Response
from korvex.security import SecurityScheme
class Basic(SecurityScheme):
challenge = "Basic"
def __init__(self):
super().__init__(name="basic", description=None, required=True)
def credential(self, request):
scheme, _, value = request.headers.get("authorization", "").partition(" ")
return value if scheme.lower() == "basic" else None
def openapi(self):
return {"type": "http", "scheme": "basic"}
app = App()
@app.get("/basic")
def basic(credential: str = Depends(Basic())):
return Response.json({"credential": credential}) # base64 of user:password
Next: Ready-made middleware.