6. FastAPI 13.06.2024

6.1. Einleitung

FastAPI ist ein modernes, schneller und webbasiertes Framework für das Erstellen von APIs mit Python (ab Version 3.7). Es basiert dabei auf Standard Python-Typanmerkungen.

6.2. FastAPI Allgemeines

6.2.1. Abhängigkeiten

  • Python3.11

  • pipenv

6.2.2. Installation

  • pipenv install

6.2.3. Starten der Anwendung

  • pipenv run python3.11 main.py

6.2.4. OpenAPI und Swagger

FastAPI generiert automatisch eine OpenAPI-Dokumentation für die erstellte API. Die Dokumentation kann unter http://localhost:8000/docs erreicht werden, alternativ auch mit einer alternativen Darstellung unter http://localhost:8000/redoc.

Dadurch ist es nicht nötig, zusätzlich zur API kontinuierlich die API-Dokumentation zu pflegen, da dies automatisch passiert.

Ein paar wesentliche Metainformationen über die jeweiligen Routen müssen jedoch dennoch im jeweiligen Decorator angegeben werden:

@router.put("/api/to-do/{list_id}", status_code=200, tags=["ToDo"], summary="Insert or update a ToDoList.")

In diesem Fall wird der Status-Code 200, die Tags „ToDo“ und die Zusammenfassung „Insert or update a ToDoList“ angegeben. Die Tags werden verwendet, um die jeweiligen API-Endpoints in der Dokumentation nach Tag zu gruppieren.

Zusätzlich kann durch response_model angegeben werden, welcher Typ von Daten zurückgegeben wird:

@router.get("/api/to-do", response_model=List[ToDoListDto], tags=["ToDo"], summary="Get all ToDoLists.")

Dadurch wird in der Swagger-Dokumentation eine Beispielantwort angezeigt, die dem Typ ToDoListDto entspricht.

Diese Typinformation kann auch in einem TypeScript-Client verwendet werden, welcher die API aufruft. Es ist möglich, diesen automatisch generieren zu lassen, indem bspw. das Tool openapi-ts verwendet wird:

https://fastapi.tiangolo.com/de/advanced/generate-clients/#openapi-client-generatoren

6.2.5. pydantic

Pydantic ist eine Bibliothek, die es ermöglicht, Typen in Python zu validieren. Pydantic ist in Rust geschrieben und positioniert sich als eine der schnellsten Bibliotheken für die Datenvalidierung in Python.

Beispiel:

from datetime import datetime

from pydantic import BaseModel, PositiveInt


class User(BaseModel):
    id: int
    name: str = 'John Doe'
    signup_ts: datetime | None
    tastes: dict[str, PositiveInt]  

Im vorliegenden Beispiel wird ein User-Model definiert, welches die Attribute id, name, signup_ts und tastes enthält.

Angenommen es wird ein User-Objekt mit den folgenden JSON-Daten erstellt:

external_data = {
    'id': 123,
    'signup_ts': '2019-06-01 12:22',
    'tastes': {
        'wine': 9,
        b'cheese': 7,
        'cabbage': '1',
    },
}

user = User(**external_data) 

Wenn die Validierung fehlschlägt wird eine ValidationError-Exception geworfen, in der Details über die fehlerhaften Felder des Objekts enthalten sind.

Eine beispielhafte Verwendung findet sich auch in unserem Source Code der Todo-App:

@router.put("/api/to-do/{list_id}/{item_id}", tags=["ToDo"], summary="Insert or update a ToDoItem.")
def put_to_do_item(list_id: str, item_id: str, item: PutToDoItemDto):

Hier wird für den Endpunkt /api/to-do/{list_id}/{item_id} ein PutToDoItemDto-Objekt erwartet. Durch die Typenangabe in der Parameterliste wird die Validierung automatisch durchgeführt.

6.2.6. uvicorn

Uvicorn ist ein leichtgewichtiger, schneller und asynchroner ASGI-Server in Python. Er wird verwendet, um asynchrone Python-Webanwendungen wie FastAPI-Anwendungen zu betreiben. Uvicorn zeichnet sich durch seine einfache Einrichtung, hohe Leistung und seine Unterstützung für moderne Python-Features wie Typ-Hinweise und asynchrones Warten aus.

ASGI (Asynchronous Server Gateway Interface) ist ein Spezifikationsstandard mit Unterstützung für asynchrone Programmierung. ASGI ermöglicht eine effiziente Handhabung von asynchronen Funktionen wie WebSockets und HTTP/2. Es dient als Schnittstelle zwischen asynchronen Webframeworks und Webservern, sodass Entwickler die Vorteile der asynchronen Programmierung in Python nutzen können, um skalierbare und leistungsfähige Webdienste zu erstellen.

6.3. FastAPI in TodoApp

  1. models.py

    In dem File models.py wird Pydantic verwendet, um die benötigten Models zu definieren. Im folgenden ist exemplarisch die Klasse ToDoItemDto zu sehen:

    class ToDoItemDto(BaseModel):
        id: str
        sequenceNumber: int
        name: str = Field(..., min_length=1, max_length=100)
        description: Optional[str] = Field(None, min_length=1, max_length=1000)
        done: bool
    
    

    sieht man, dass die Klasse das BaseModel verwendet. Nun kann man z.B. bei name wird die Funktion Field() verwendet, um zusätzliche Metadaten und Validierungsregeln für die Felder eines Modells festzulegen.name muss eine Zeichenkette mit der Mindestlänge von 1 und eine maximale Länge von 100 haben. das ... ist ein Platzhalter und gibt an, dass das Feld erforderlich ist. Im Vergleich zur description wird hier als default: None angegeben, was bedeutet, dass dieser Parameter nicht erforderlich ist.

  2. todo_controller.py

    Im todo_controller werden mit Hilfe von FastAPI eine Reihe von RESTful-API-Endpunkten definiert. Die Endpunkte verwalten die Todo-Listen und die ToDo-Elemente. Zunächst muss der Router initialisiert werden. Das geschieht mit

    router = APIRouter()
    

    Dies initialisiert eine Instanz von APIRouter, damit die API-Routen definiert werden können. Nun kann man mit Hilfe der router-Instanz zum Beispiel eine get-Anfrage stellen, um sich alle Todo-Listen anzeigen zu lassen.

    @router.get("/api/to-do", response_model=List[ToDoListDto], tags=["ToDo"], summary="Get all ToDoLists.")
    def get_to_do_lists():
      return list(todo_lists.values())
    

    Mit@router.get kann man den API-Endpunkt definieren. der erste Parameter der folgt sagt, unter welcher Route dieser Endpunkt zu erreichen ist. response_model gibt an, was zuückgegeben werden. Im Falle der obigen Abfrage, bekommt man eine Liste von TodoListDto zurück.

    Um einen delete-Endpunkt zu erstellen, kann man die @router.delete Annotation schreiben.

    @router.delete("`/api/to-do/{list_id}/{item_id}", tags=["ToDo"], summary="Delete a ToDoItem.")
    def delete_to_do_item(list_id: str, item_id: str):
        if list_id not in todo_lists:
            raise HTTPException(status_code=404, detail="ToDoList not found")
    
        todo_list = todo_lists[list_id]
        for item in todo_list.items:
            if item.id == item_id:
                todo_list.items.remove(item)
        for other_item in todo_list.items:
            if other_item.sequenceNumber > item.sequenceNumber:
                other_item.sequenceNumber -= 1
        return todo_list
    
        raise HTTPException(status_code=404, detail="ToDoItem not found")
    

    Hier sieht man wieder den Endpunkt /api/to-do/{list_id}/{item_id}. In der Methode findet man zwei HTTPExceptions, die einen 404-Errorwerfen, wenn entweder eine Todo-Liste oder ein Todo-Item nicht gefunden wurde.

  3. database.py

    In der database.py wird eine Datenbank verwendet, die bei jedem Neustart wieder eine neues leeres Dictionary initialisiert. Man kann natürlich auch eine konsistente Datenbank verwenden. Man könnte beispielsweise die Python-Bibliothek sqlalchemy` verwenden. Das ist ein Open-Source-SQL-Toolkit und eine Object-Relationa-Mapping Bibliothek. Da wir in unserer Anwendung die Funktionsweise für FastAPI darstellen wollten, war jedoch eine simple ‚ Datenbank‘ ausreichend.

    todo_lists: Dict[int, ToDoListDto] = {}
    
  4. main.py

    app = FastAPI(
     title="ToDo API",
     version="0.0.1"
    )
    
    origins = [
        "http://localhost",
        "http://localhost:5173",
    ]
    
    app.add_middleware(
        CORSMiddleware,
        allow_origins=origins,
        allow_credentials=True,
        allow_methods=["*"],
        allow_headers=["*"],
    )
    
    app.include_router(to_do_router)
    
    if __name__ == "__main__":
        uvicorn.run("main:app", host="localhost", port=8000, reload=True)
    

    In der main.py wird nun mit Hilfe von app = FastAPI() eine Instanz der FastAPI-Anwendung erstellt. der Parameter title beschreibt den Titel der API, der in der automatisch generierten Dokumentation angezeigt wird. der Parameter version beschreibt die Version der API.

    Danach wird durch origins eine Liste von URLs definiert, von denen aus die API aufgerufen werden darf. Danach wird durch app.add_middleware die CORS-Middleware zur Anwendung hinzugefügt. im Folgenden werden die einzelnen Parameter dazu erklärt:

    • allow_origins: Erlaubt die angegebenen URLs

    • allow_credentials: Erlaubt das Senden von Cookies und anderen Anmeldeinformationen.

    • allow_methods: Erlaubt mit [*] alle HTTP-Methoden, also Get, Post, Put, Delete, etc.

    • allow_headers: Erlaubt alle HTTP-Headers

    Dann muss noch der Router eingebunden werden durch app.include_router(to_do_router). Dies fügt die Routen und die Logik des ToDo-Moduls hinzu.

    Im letzten Schritt wird dann die Anwendung in der if __name__ == "__main__": ausgeführt. Hier wird dann mit uvicorn.run() der Uvicron-Server gestartet und die FastAPI-Anwendung ausgeführt.

    • "main:app": verweist auf die app-Instanz

    • host="localhost: führt den Server auf „localhost“ aus

    • port=8000: zeigt, auf welchen Port der Sever zu erreichen ist

    • reload=True: aktiviert den automatischen Neustart des Servers bei Codeänderungen

6.4. Vorteile

  • Vergleichsweise schnell:

Framework

Test 1

Test 2

FastAPI

17 ms

6.2 ms

Django

517.2 ms

5.834

Flask

507.2

508.9

  • Schnelle Entwicklung aufgrund der geringen Anzahl an LOC

  • Einfaches Testen mithilfe des TestClients on FastAPI:

app = FastAPI()


@app.get("/")
async def read_main():
    return {"msg": "Hello World"}


client = TestClient(app)


def test_read_main():
    response = client.get("/")


assert response.status_code == 200
assert response.json() == {"msg": "Hello World"}
  • Automatische API-Dokumentation mit Swagger

  • Out-of-box-Unterstützung von asynchronem Code mithilfe der async/await-Keywords

6.5. Nachteile

  • In FastAPI ist alles an die FastAPI-App gebunden, sodass die main.py-Datei schnell überladen sein kann

  • Das Ökosystem von FastAPI entwickelt sich noch im Vergleich zu etablierteren Frameworks wie Django

  • Abhängigkeiten zu anderen Packages

  • Im Vergleich zu einigen anderen Frameworks bietet FastAPI weniger Projekttemplates und Konventionen

6.6. Quellen