3. FastAPI¶
3.1. Einführung¶
In unserem 3. Versuch haben wir uns ein kleines Projekt mit FastAPI überlegt. Dieses soll eine Todo-API bereitstellen, also das Erstellen und Verwalten von sowohl Todo-Listen, als auch von Todo-Items. Zudem soll die Anwendung eine Authentifizierungslogik beinhalten.
3.2. FastAPI¶
FastAPI ist ein schnelles, modernes Web-Framework für Python, das auf der Grundlage von Typ-Annotationen entwickelt wurde. Hiermit wird ermöglicht, schnell und einfach APIs zu erstellen, ohne auf Leistung oder Funktionen zu verzichten. Diese APIs eignen sich vor allem als Backend-APIs für Web- und mobile Anwendungen.
Der größte Pluspunkt von FastAPI ist die extreme Schnelligkeit, sowohl im Hinblick auf Performance, als auch auf Code-Erstellung. Diese Schnelligkeit ist unter anderem den geringen Latenzzeiten geschuldet. Es basiert auf dem ASGI-Server und verwendet das Starlette-Framework, das eine optimierte Leistung bietet.
Außerdem verwendet FastAPI eine OpenAPI-Spezifikation zur automatischen Generierung von Dokumentationen. Diese Dokumentationen können dann über eine Swagger-UI-Oberfläche oder eine alternative Schnittstelle abgerufen werden.
Swagger:
Swagger ist ein Open-Source-Framework für die Dokumentation von APIs, das eine standardisierte, maschinenlesbare Beschreibung der API-Spezifikation bereitstellt. Es ermöglicht Entwicklern schnell zu verstehen wie eine bestimmte API funktioniert, welche Parameter erwartet werden und welche Daten & Fehlercodes zurückgegeben werden können.
3.3. Installation¶
Einrichtung FastAPI über pip möglich:
pip install fastapi
Einrichtung der ASGI-Serverbibliothek ebenfalls über pip:
pip install uvicorn
Alternativ: Einrichtung über Poetry und einer config.toml file:
[tool.poetry]
name = "fastapitest"
version = "0.1.0"
description = ""
authors = ["Maximilian Asam <Maximilian.Asam1@HS-Augsburg.DE>"]
readme = "README.md"
packages = [{ include = "src" }]
[tool.poetry.dependencies]
python = "^3.11"
fastapi = {extras = ["all"], version = "^0.95.1"}
sqlalchemy = "^2.0.10"
python-jose = {extras = ["cryptography"], version = "^3.3.0"}
passlib = {extras = ["bcrypt"], version = "^1.7.4"}
[tool.poetry.group.dev.dependencies]
black = "^23.1.0"
mypy = "^1.1.1"
isort = "^5.12.0"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
[tool.poetry.scripts]
dev = "src.main:dev"
3.4. Hello World¶
Um einen Einstieg in FastAPI zu bekommen, hier nun ein einfaches Beispiel, bei dem eine einfache HTTP-Anfrage auf eine Anfrage mit einem JSON-Körper antwortet.
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def root():
return {"message": "Hello World"}
Hier wird eine Instanz von FastAPI und anschließend eine Funktion root definiert, die eine simple Antwort mit einer JSON-Nachricht zurückgibt. Die Funktion wird mit der Annotation @app.get(„/“) an die Wurzel-URL der Anwendung gebunden. Nun kann der Uvicorn-Server und somit die Anwendung folgenderweise gestartet werden und über die URL http://127.0.0.1:8000 geöffnet werden:
uvicorn.run("src.main:app", port=8000, reload=True)
3.5. Projektstruktur¶
fastapitest/
│
├── src/
│ ├── __init__.py
│ ├── main.py # Erstellen und Starten der FastAPI-Anwendung
│ ├── routers/ # Router-Modelle, die die HTTP-Endpunkte definieren, die auf die Anforderungen reagieren
│ │ ├── __init__.py
│ │ ├── auth.py # Authentifizierungs-Router
│ │ ├── todoLists.py # Todo-Listen-Router
│ │ └── todos.py # Todo-Router
│ ├── schemas/ # Schemas, welche die Struktur und Typen der Datenmodelle definieren
│ │ ├── __init__.py
│ │ ├── auth.py # Authentifizierungsschemas
│ │ ├── todoLists.py # Todo-Listen-Schemas
│ │ └── todos.py # Todo-Schemas
│ └── util/ # Hilfsmodule/-funktionen für die Anwendung
│ ├── __init__.py
│ ├── auth.py # Authentifizierungsfunktionen
│ └── database.py # Funktionen zur Verbindung mit der Datenbank
│
├── pyproject.toml
└── poetry.lock
3.6. Todo-Anwendung¶
Sowohl Todo-Listen, als auch Todos werden in unserem Projekt in einer SQLite-Datenbank gespeichert und können mithilfe von RESTful APIs abgerufen, erstellt, aktualisiert und gelöscht werden. Die Todo-Listen enthalten mehrere Todos und sind eindeutig einem bestimmten Benutzer zugewiesen. Damit pflegt eine Todo-Liste eine One-To-Many-Relationship mit den Todos. Jedes Todo hat ebenfalls eine eindeutige ID und enthält Informationen wie Titel, Beschreibung, Fälligkeitsdatum und den Status (abgeschlossen oder nicht abgeschlossen).
Die Logik hinter den Todo-Listen- und den einzelnen Todos wird in den entsprechenden Routern definiert und durch die Verwendung von Pydantic-Schemas validiert. Für die CRUD-Operationen (Create, Read, Update, Delete) auf den Todo-Listen und Todos werden Funktionen definiert, die mithilfe von SQLAlchemy-Methoden auf die Datenbank zugreifen. Wenn eine Todo-Liste gelöscht wird, werden alle zugehörigen Todos ebenfalls gelöscht.
Grundsätzlich kann man sagen, dass sowohl für die Todo-Listen, als auch für die einzelnen Todo-Items, die grundlegenden CRUD-Operationen implementiert wurden.
3.7. Authentifizierung¶
Für die Authentifizierung haben wir JSON Web Tokens (JWT) implementiert. Wenn sich ein Benutzer erfolgreich authentifiziert hat, erhält er ein JWT-Token, das er dann bei zukünftigen Anfragen an die geschützten Endpunkte des Servers (Todo-Listen und Todo-Elemente) mitliefern kann.
Um die JWT-Token zu erzeugen und zu überprüfen, verwenden wir die Bibliothek Python-JOSE. Die Konfigurationsdaten wie der Secret Key und die Token-Lebensdauer sind in einer .secret-Datei gespeichert, die vom Projekt gelesen wird oder bei Bedarf neu erstellt wird.
SECRET_KEY = None
try:
with open(".secret", "r") as file:
SECRET_KEY = file.read()
except IOError:
print("generating new key")
if not SECRET_KEY:
# generate 32 hex digits using token hex
SECRET_KEY = secrets.token_hex(32)
with open(".secret", "w") as file:
file.write(SECRET_KEY)
Die grundlegende Logik der Authentifizierung ist in der get_current_user-Funktion implementiert. Wenn ein Benutzer eine Anfrage an einen geschützten Endpunkt stellt, ruft FastAPI diese Funktion auf, um den Benutzer anhand seines Tokens zu authentifizieren. Der Token wird zunächst decodiert, um zu überprüfen, ob er von unserem Server signiert wurde und nicht manipuliert wurde. Danach wird die Benutzer-ID aus dem Token extrahiert und anhand dieser wird der Benutzer in der Datenbank gesucht. Wenn der Benutzer gefunden wird, ist die Authentifizierung erfolgreich und die Anfrage wird fortgesetzt. Andernfalls wird eine HTTP-Exception ausgelöst.
async def get_current_user(
token: str = Depends(oauth2_scheme),
db: Session = Depends(get_db),
):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str | None = payload.get("sub")
if username is None:
raise credentials_exception
token_data = TokenData(username=username)
except JWTError:
raise credentials_exception
user = get_user(db, username=token_data.username)
if user is None:
raise credentials_exception
return user
Die Funktion authenticate_user wird aufgerufen, wenn ein Benutzer versucht, sich mit seinen Anmeldeinformationen anzumelden. Hierbei wird der Benutzername und das Passwort des Benutzers überprüft und, wenn sie korrekt sind, der Benutzer zurückgegeben.
def authenticate_user(
db: Session,
username: str,
password: str,
):
user = get_user(db, username)
if not user:
return False
if not verify_password(password, user.hashed_password):
return False
return user
Um die Funktionalität der Authentifizierung testbar zu machen, haben wir uns dazu entschlossen, bei Start des Servers zwei Dummy-User zu erstellen, falls die DB leer ist. Somit kann man sich entweder über die Username - Passwort Kombination foo - bar oder über root - admin anmelden.
def add_dummy_users_if_empty(db: Session = Depends(get_db)):
if not db.query(user_models.User).first():
create_user(
db,
UserCreate(
username="foo",
email="test@example.de",
cleartext_password="bar",
full_name="Foo Bar",
),
)
create_user(
db,
UserCreate(
username="root",
email="test2@example.de",
cleartext_password="admin",
full_name="Root Admin",
),
)
3.8. Fazit¶
In unserem Projekt haben wir FastAPI erfolgreich eingesetzt, um eine RESTful-API aufzubauen. Die Unterstützung für automatisch generierte Dokumentation mit Swagger hat uns sehr geholfen, die Übersichtlichkeit und Nachvollziehbarkeit unserer API zu erhöhen. Außerdem hat uns SQLAlchemy ermöglicht, eine solide und erweiterbare Datenbankanbindung aufzubauen. Auch die Implementierung der Authentifizierung war dank der einfachen Handhabung von FastAPI vergleichsweise unkompliziert.
Zusammenfassend lässt sich sagen, dass FastAPI ein sehr empfehlenswertes Framework für die Entwicklung von APIs ist, besonders für Anwendungen, die hohen Durchsatz erfordern oder asynchronen Code verwenden.