Aus Linux-Magazin 11/2021

Python-Praxis, Folge 3: FastAPI

© Iakov Filimonov / 123RF.com

Mit FastAPI, dem modernsten und vollständigsten Web-Framework für Python, lassen sich moderne Webservices realisieren. Dieser Beitrag stellt dies anhand von Beispielen vor.

Für Python existieren viele Web-Frameworks zur Realisierung von Web-Applikationen und Microservices. Viele davon blicken auf eine sehr lange Historie zurück. Zu den bekanntesten und am häufigsten verwendeten Frameworks zählen sicherlich Flask und Django. Alle Web-Frameworks haben gemein, dass sie eingehende HTTP-Requests über eine Routing-Konfiguration an eine entsprechende Methode weiterleiten. Diese Methode verarbeitet den Request und erzeugt eine HTTP-Response (Text, HTML, JSON oder Ähnliches).

FastAPI [1] als relativ neues Framework hat in der letzten Zeit besonders im Bereich der REST-Microservices starke Verbreitung gefunden. Das liegt vor allem an seinem breit gefächerten Funktionsspektrum (siehe Kasten “FastAPI: Funktionsumfang”).

FastAPI: Funktionsumfang

  • automatische Validierung aller eingehenden und ausgehenden JSON-Daten
  • Schema-Definition für eingehende und ausgehende Daten entweder über Python-Typ-Annotierungen oder über das Modul Pydantic
  • automatische Unterstützung von OpenAPI, vormals Swagger (Abbildung 1)
  • native Unterstützung von Pythons »async«
  • Websockets
  • gute Dokumentation und Tutorials, einfache Zugänglichkeit
Abbildung 1: OpenAPI macht das Interagieren mit der Programmierschnittstelle einfach.

Abbildung 1: OpenAPI macht das Interagieren mit der Programmierschnittstelle einfach.

FastAPI versteht sich wie das ältere Flask als agnostisch gegenüber Datenspeicherung oder HTML-Generierung und anderen Aspekten. Es grenzt sich dadurch von Frameworks wie Django ab, die von Haus aus bereits mit einer umfangreichen Unterstützung etwa für das Persistieren von Daten oder die Authentifizierung und Autorisierung daherkommen, was sie besonders für bestimmte Arten von Web-Applikationen prädestiniert.

Installation

FastAPI setzt eine aktuelle Python-Version (3.6 oder höher) voraus und lässt sich wie gewohnt mit Pip installieren; dasselbe gilt für den WSGI-Server Hypercorn (Listing 1, erste Zeile). Er macht die FastAPI-Applikation nach außen via HTTP verfügbar. Das Beispiel in Listing 2 definiert eine Methode »double()« als REST-Endpunkt, die einen übergebenen Wert verdoppelt. Nach dem Installieren lässt sich der Code mit dem Aufruf aus der zweiten Zeile von Listing 1 starten.

Listing 1

Erste Schritte

$ pip3 install fastapi hypercorn
$ bin/hypercorn app1:app

Listing 2

app1.py

from fastapi import FastAPI
app = FastAPI()
# dict als Resultat
# -> JSON Response
@app.get("/")
def root():
  return {"hello": "world"}
@app.get("/double")
def double(number: int):
  return number * 2
# dict als Resultat
# -> JSON Response
@app.get("/double_json")
def double_json(number: int):
  return dict(result=number * 2)

Der Zugriff auf die API erfolgt danach via http://localhost:8000. Die Eingabe von »curl http://localhost:8000« liefert »{“hello”:”world”}« zurück. Der Aufruf »curl http://localhost:8000/double_json?number=32« ergibt dagegen erwartungsgemäß »{“result”:64}«. Wer versucht, die Webseite mit einem unpassenden Parameter aufzurufen, erntet eine Fehlermeldung (Listing 3).

Listing 3

Parameterfehler

> curl http://localhost:8000/double_json?number=text
{"detail":[{"loc":["query","number"],"msg":"value is not a valid integer","type":"type_error.integer"}]}

Die Dokumentation zu FastAPI verweist häufig auf den besonders schnellen WSGI-Server Uvicorn. Je nach Betriebssystem kann die Installation von Uvicorn aber kompliziert sein. Daher verwenden wir meistens Hypercorn, der etwas langsamer arbeitet, aber bei der Installation weniger Probleme bereitet.

Im letzten Beispiel sieht man, dass FastAPI wegen der Typ-Annotation »int« den Parameter für »number« abweist. FastAPI prüft also Parameter bereits vor der Verarbeitung hinsichtlich des Typs. Es antwortet im beschriebenen Fall mit »HTTP 442 (“Unprocessable Entity”)«.

Für die anderen HTTP-Request-Methoden stehen analog zu »@app.get()« die entsprechenden Pendants »@app.post()«, »@app.delete()«, »@app.put()« und »@app.patch()« bereit.

Datenmodellierung mit Pydantic

Neben den Standard-Typ-Annotationen von Python 3 kann man für komplexere Datenstrukturen auch das Python-Package Pydantic [2] verwenden. Konzeptionell ist es die logische Weiterentwicklung von Pythons Data Classes. Jedoch bringen Datenmodelle, die auf Pydantic beruhen, viele Vorteile mit sich wie Datenvalidierung, den Export der Modelle zum Beispiel als JSON-Schema oder einer Modell-Instanz als JSON beziehungsweise ORM-Node sowie eine erweiterte Modellkonfiguration.

Das Beispiel aus Listing 4 demonstriert, wie man das Datenmodell für eine Standard-Benutzerregistrierung mit Benutzernamen und Passwort heranzieht. Validatoren kann man sowohl feldbezogen als auch auf das gesamte Datenmodell bezogen definieren. Eine konkrete Anwendung demonstriert Listing 5.

Listing 4

Datenmodell für Benutzerregistrierung

from pydantic import BaseModel, ValidationError, validator
class User(BaseModel):
  name: str
  username: str
  password1: str
  password2: str
  @validator('name')
  def name_must_contain_space(cls, v):
    if ' ' not in v:
      raise ValueError('must contain a space')
    return v.title()
  @validator('password2')
  def passwords_match(cls, v, values, **kwargs):
    if 'password1' in values and v != values['password1']:
      raise ValueError('passwords do not match')
    return v
  @validator('username')
  def username_alphanumeric(cls, v):
    assert v.isalnum(), 'must be alphanumeric'
    return v

Listing 5

Anwendungsbeispiele

> user = User(name='samuel colvin', username='scolvin', password1='zxcvbn', password2='zxcvbn')
> print(user)
name='Samuel Colvin' username='scolvin' password1='zxcvbn' password2='zxcvbn'
# falscher Benutzername und Passwortfehler
try:
  User(name='samuel', username='scolvin', password1='zxcvbn', password2='zxcvbn2')
except ValidationError as e:
  print(e)
  """
  2 validation errors for User
  name
    must contain a space (type=value_error)
  password2
    passwords do not match (type=value_error)
  """

Das User-Modell lässt sich dann als Parameter im folgenden Beispiel für einen REST-Endpunkt zur Benutzerregistrierung verwenden. Zusätzlich stellt das Modell »Response« sicher, dass die Antwort die beiden Felder »msg« und »status« enthält. Die Validierung der eingehenden Registrierung erfolgt bereits auf FastAPI-Ebene. Die Methode »register()« bekommt über »registration_data« immer valide Daten; fehlerhafte Daten werden direkt im Vorfeld von »register()« abgewiesen (Listing 6).

Listing 6

Pydantic und FastAPI

class Response(BaseModel):
  msg: str
  status: str
app = FastAPI()
@app.post("/register",
  summary="Register a new user",
  response_description="Dict with msg & status",
  response_model=Response)
def register(registration_data: User):
  """ Register a new user
      Note: `registration_data` is already validated
      - **some parameter**: some parameter
  """
  # ...
  return Response(status="OK", msg="All fine")

Aus den Informationen des Endpunkts und dem Docstring (hier kann man Markdown verwenden) erzeugt FastAPI automatisch mithilfe von OpenAPI/Swagger ein Web-Interface, das alle Endpunkte mit deren Parametern und Schemata umfasst. Das Web-Interface ist automatisch über »/docs« zu erreichen und erlaubt, alle Endpunkte interaktiv über »Try it out« zu testen.

Asynchrone Verarbeitung

Spätestens seit Version 3.7 verfügt Python über eine stabile API für das asynchrone Verarbeiten von Code mithilfe der bekannten Konstrukte »async« und »await«. Die Verwendung von »async« ist immer angesagt, wenn ein Codesegment auf eine Antwort von einem externen Subsystem warten muss, zum Beispiel von einer Datenbank oder aus einem HTTP-Request. FastAPI erlaubt die gemischte Verwendung von synchroner und asynchroner Verarbeitung. Es genügt, die entsprechende Request-Methode mit dem Schlüsselwort »async« zu markieren (Listing 7).

Listing 7

Synchron und asynchron

# synchron
@app.get('/')
def read_results():
  results = some_library()
  return results
# asynchron
@app.get('/')
async def read_results():
  results = await some_library()
  return results

FastAPI verfügt über einen nativen »async«-Support und hebt sich damit von Flask ab, das erst in der neuen Version 2.0 eine (eher halbherzige) Unterstützung für »async« erhielt.

FastAPI-Middleware

Neben dem eigentlichen Applikationscode einer Web-Anwendung benötigt man häufig auch Middleware, um sowohl die HTTP-Anfrage als auch die HTTP-Antwort verarbeiten zu können. Typische Szenarien dafür sind Authentifizierung, CORS-Header (Cross-Origin Resource Sharing), Umleitungen, Logging und so weiter. Da FastAPI auf Starlette [3] basiert, lässt sich auch dessen gesamte Middleware in FastAPI verwenden.

Generell kann eine Middleware die HTTP-Anfrage vor dem Applikationscode manipulieren und auch die HTTP-Antwort nach dem Applikationscode. Eine eigene Methode lässt sich über »@app.middleware()« in die interne HTTP-Verarbeitungspipeline einbauen. Sie erhält dann Zugriff auf den Request und muss nach der Verarbeitung die Daten via »call_next()« weiterreichen. Das Beispiel aus Listing 8 ermittelt die Dauer des Requests und baut die HTTP-Antwort als HTTP-Header »X-Process-Time« ein.

Listing 8

Middleware

@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
  start_time = time.time()
  response = await call_next(request)
  process_time = time.time() - start_time
  response.headers["X-Process-Time"] = str(process_time)
  return response

Sicherheit und Authentifizierung

FastAPI unterstützt über das Modul »fastapi.security« [4] die von OpenAPI vorgesehenen Sicherheitsschemata API Key, HTTP (Bearer Header, HTTP Basic Auth, HTTP Digest), OAuth2 sowie OpenIDConnect.

Das Beispiel aus Listing 9 implementiert eine API-Key-Authentifizierung, die den REST-Endpunkt »/info« schützt. Die OpenAPI-Spezifikation erlaubt, einen API-Key als Header, als Query-Parameter oder als Cookie zu übergeben (Listing 10). Die Überprüfung des API-Keys »access_token« ist in der Methode »get_api_key()« implementiert und wird wie bei FastAPI üblich dem Endpunkt über eine Dependency Injection untergeschoben.

Listing 9

Absichern eines Endpunkts

from fastapi import Depends, FastAPI, HTTPException, Security
from fastapi.security.api_key import APIKey, APIKeyCookie, APIKeyHeader, APIKeyQuery
from starlette.status import HTTP_403_FORBIDDEN
app = FastAPI()
API_KEY = "0123456789"
API_KEY_NAME = "access_token"
api_key_query = APIKeyQuery(name=API_KEY_NAME, auto_error=False)
api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False)
api_key_cookie = APIKeyHeader(name=API_KEY_NAME, auto_error=False)
async def get_api_key(
  api_key_query: str = Security(api_key_query),
  api_key_header: str = Security(api_key_header),
  api_key_cookie: str = Security(api_key_cookie),
):
  if api_key_query == API_KEY:
    return api_key_query
  elif api_key_header == API_KEY:
    return api_key_header
  elif api_key_cookie == API_KEY:
    return api_key_cookie
  else:
    raise HTTPException(
      status_code=HTTP_403_FORBIDDEN, detail="Could not validate credentials"
    )
@app.get("/info")
def info(api_key: APIKey = Depends(get_api_key)):
  return dict(msg="hello world")

Listing 10

Client-seitige Authentifizierung

$ bin/hypercorn apikey.py
### via HTTP-Header
> curl -H "access_token: 0123456789" "http://localhost:8000/info"
{"msg":"hello world"}
> curl -H "access_token: XXXXXXX" "http://localhost:8000/info"
{"detail":"Could not validate credentials"}
### via URL Query Parameter
> curl "http://localhost:8000/info?access_token=0123456789"
{"msg":"hello world"}
> curl  "http://localhost:8000/info?access_token=XXXXXX"
{"detail":"Could not validate credentials"}%

HTML-Generierung

Für die klassische Generierung von HTML via FastAPI kann man sich generell aller HTML-Frameworks für Python bedienen. Die Integration von Jinja2-Template erfolgt mit dem Python-Package fastapi-jinja2, das der Aufruf »pip install fastapi-jinja2« installiert.

Das Beispiel aus Listing 11 zeigt die typische Integration eines Templates »main.html« (Listing 12), das mit den Query-Parametern des eingehenden Requests befüllt wird (Listing 13).

Listing 11

app_html.py

from fastapi import FastAPI, Request
app = FastAPI()
import os
import fastapi_jinja
dev_mode = True
folder = os.path.dirname(__file__)
template_folder = os.path.join(folder, 'templates')
template_folder = os.path.abspath(template_folder)
fastapi_jinja.global_init(template_folder, auto_reload=dev_mode)
@app.get('/')
@fastapi_jinja.template('main.html')
async def root(request: Request):
  return dict(request.query_params)

Listing 12

templates/main.html

<div>
  Nachricht: {{message}}
  <br/>
  Zahl: {{number}}
</div>

Listing 13

app_html starten

$ hypercorn app_html:app
> curl http://localhost:8000?number=42&message=abc
<div>
  Nachricht: abc
  <br/>
  Zahl: 42
</div>

Zur Auslieferung umfangreicher Daten oder zum Streamen von Mediendateien stellt FastAPI darüber hinaus spezielle Response-Typen wie »StreamingResponse« oder »FileResponse« zur Verfügung, die ein effizientes Ausliefern der Daten unter Zuhilfenahme von »async« ermöglichen.

FastAPI skalieren

Das Skalieren einer FastAPI-Applikation ist auf verschiedene Art und Weise möglich. Im einfachsten Fall erlauben WSGI-Server wie Hypercorn oder Uvicorn den Start einer bestimmten Anzahl von Workern (Listing 14), die die eingehenden Requests abarbeiten. Dadurch lässt sich der Global Interpreter Lock (GIL) von Python umgehen: Jeder Worker läuft als eigener Prozess, sodass sich die vorhandenen CPUs saturieren lassen.

Listing 14

Mehrere Worker

$ bin/hypercorn --workers 4 apikey:app
### oder
$ bin/uvicorn --workers 4 apikey:app

Die korrekte Wahl des WSGI-Servers und der entsprechenden Worker-Klassen kann entscheidenden Einfluss auf die Leistung einer FastAPI-Applikation haben. Man darf jedoch nicht vergessen, dass Benchmarks normalerweise künstlich sind, und sollte immer den eigenen Applikationscode im Fokus behalten. Darüber hinaus lässt sich eine FastAPI-Applikation auch in einen Container packen, der dann via Docker, Kubernetes oder einer anderen Container-Runtime skaliert.

Zusammenfassung

Bei FastAPI handelt es sich um das modernste Python-Framework zur Realisierung von REST-Schnittstellen und Microservices. Es würde den Rahmen des Artikels sprengen, hier auf weitere Features wie Websockets, GraphQL und andere einzugehen.

Die Stärke von FastAPI liegt in der direkten Unterstützung von Typ-Annotationen und des Pydantic-Moduls, um Schnittstellen und deren Typen programmatisch zu definieren. FastAPI stellt für die OpenAPI-Schnittstelle automatisch zusätzliche Informationen aus den Docstrings zur Verfügung. Das macht es einfach zugänglich, denn der Anwender definiert und dokumentiert so die eigenen REST-Dienste auf natürliche Weise. (jcb)

Der Autor

Andreas Jung arbeitet seit 28 Jahren mit Python und realisiert als Freiberufler Python-Applikationen im Bereich Web, Electronic Publishing, Pharma und Medizin. Für den medizinischen Fachverband DGHO e.V. betreut er das Leitlinien-Portal Onkopedia (http://onkopedia.com). Sein Projekt Print-CSS.rocks (https://print-css.rocks) realisiert das Generieren hochqualitativer PDF-Dokumente mithilfe von HTML/XML und CSS auf Basis von CSS Paged Media.

DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 6 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben