Som regel er den automatisk genererte OpenAPI-dokumentasjonen av FastAPI
tilstrekkelig dokumentasjon for HTTP-endepunktene i applikasjonen vår. WebSocket
-endepunktene lar seg derimot ikke dokumentere like lett. Og hva med mer komplekse endepunkt som gjerne behøver konkrete kodeeksempler?
Jo, her er det tydelig at noe mer dokumentasjon må på plass.
Koden fra dette innlegget er tilgjengelig på repoet endrekrohn/fastapi-with-md-docs 🧑💻
Forbehold 🔗
Ved større prosjekt kan det gi mening å gå direkte til et tyngre verktøy som MkDocs
, Docusaurus
eller AsyncAPI
(for WebSockets). I mitt tilfelle ville jeg derimot unngå å introdusere enda en tjeneste før absolutt nødvendig. Det var også viktig at dette valget ikke påvirket en senere overgang.
Løsningen 🔗
Den endelige løsningen ble å servere HTML fra FastAPI
ved hjelp av følgende Python
-bibliotek:
markdown2
for å lese og konvertere markdown-filer til HTML.Jinja2
for å rendre HTML-responser.Pygments
for syntaksutheving.
poetry add markdown2 jinja2 pygments
pip install markdown2 jinja2 pygments
Struktur 🔗
Koden er strukturert på følgende vis.
/application
/code
main.py
docs.py
config.py
/docs
template.html
**/*.md
/static
styles.css
Alle dokumentasjonsfiler legges her i mappen docs
. Disse kan videre nøstes i egne undermapper (eksempelvis /docs/feature-1/endpoint-1/client.md
). Dette er gjort for å helt smertefritt kunne konvertere til MkDocs
senere.
Lesing av fil 🔗
For å lese markdown-filene inn i Python
bruker vi os
fra standardbiblioteket, og utløser en 404-respons dersom filen vi etterspør ikke finnes.
def get_path(filename: str) -> str:
return os.getcwd() + f"/docs/{filename}"
def get_md(filepath: str) -> str:
if not os.path.exists(filepath):
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Could not find file."
)
with open(filepath) as md:
return md.read()
Konvertering av markdown
🔗
Videre ønsker vi å konvertere strengen med markdown til HTML ved hjelp av markdown2
-pakken. Her har jeg laget en dataklasse kalt HTML for å gjøre lesbarheten noe bedre.
@dataclass()
class HTML:
html: Any
toc: Any
metadata: dict[str, Any]
def md_to_html(md: str) -> HTML:
data = markdown2.markdown(
md, extras=["fenced-code-blocks", "header-ids", "metadata", "toc"]
)
return HTML(html=data, toc=data.toc_html, metadata=data.metadata)
Legg merke til at vi også legger inn noen extras
. Kort forklart:
fenced-code-blocks
gir mulighet for å skrive kodeblokker med backticks.header-ids
legger inn etid
-attributt på overskriftene våre.metadata
henter ut ekstra informasjon, også kjent somfrontmatter
.toc
lager en innholdsfortegnelse (table of contents).
Deretter bruker vi Jinja2
-templates til å omslutte den genererte HTMLen fra markdown2
. Dette gjør vi for å kunne påføre stiler, samt ha en felles navigasjon. Malen template.html
er tilgjengelig på repoet.
def get_html(md: str) -> str:
try:
data = md_to_html(md)
with open(get_path("template.html")) as file_:
return jinja2.Template(file_.read()).render(
title=data.metadata.get("title", None),
toc=data.toc,
content=data.html,
)
except Exception:
raise HTTPException(
status.HTTP_424_FAILED_DEPENDENCY, detail="Could not hydrate HTML."
)
Endepunktet 🔗
Til slutt kan vi bruke funksjonene i endepunktet i FastAPI
. Legg merke til path-parameteret med type path
for å kunne matche alle stier.
@router.get("/docs/{filepath:path}", response_class=HTMLResponse)
def get_extra_doc(filepath: str) -> str:
return get_html(get_md(get_path(f"{filepath}.md")))
Caching 🔗
I og med at vi leser og konverterer statiske filer kan det gi mening å implementere en basic form for caching. Legg merke til at caching er skrudd av under utvikling.
import functools
def cache(func, /):
if settings.IS_DEV:
return func
return functools.cache(func)
@cache
def get_path(filename: str) -> str:
...
@cache
def get_md(filepath: str) -> str:
...
@cache
def get_html(md: str) -> str:
...
Uthevet kodesyntaks 🔗
For å påføre styling på kodeblokkene våre er vi avhengig av egen CSS
. Denne CSS
-filen kan vi servere ved hjelp av FastAPIs statiske mounts.
app = FastAPI()
app.mount("/static", StaticFiles(directory="static"), name="static")
Videre må vi sette denne som stylesheet
i Jinja2
-malen vår template.html
.
<link rel="stylesheet" type="text/css" href="/static/styles.css" />
Kodeuthevingen er generert ved hjelp av
pygments
. Du kan fint endre denne selv så lenge du beholder klassenavnene.
Løsningen i praksis 🔗
Med endepunktet for å servere dokumentasjonen på plass kan vi bruke funksjonaliteten i andre endepunkt. Jeg foretrekker å linke til dokumentasjonen direkte i docsstringen
:
@app.get("/health", tags=["Status"])
def healthcheck():
"""
This route has additional written documentation at:
- 📄 [Health](/docs/health)
"""
return "ok"
Om dokumentasjonen angår en hel OpenAPI
-tag, kan du også bruke url
-attributtet i externalDocs
.