add code first file
parent
5f98b22da3
commit
4c8dc27ba9
|
|
@ -0,0 +1,307 @@
|
|||
"""
|
||||
code_first.py — FastAPI CRUD Beispiel (Code First OpenAPI)
|
||||
|
||||
Dieses Skript zeigt, wie man mit FastAPI eine kleine CRUD-API baut,
|
||||
die automatisch eine OpenAPI-Dokumentation erzeugt (Swagger UI /docs).
|
||||
|
||||
Wichtige Idee (Code First):
|
||||
- Wir schreiben zuerst den Code (Endpoints + Typen + Modelle).
|
||||
- FastAPI generiert daraus automatisch die OpenAPI-Spezifikation.
|
||||
- Swagger UI zeigt die Dokumentation interaktiv an.
|
||||
|
||||
Starten:
|
||||
python3 -m uvicorn code_first:app --reload
|
||||
|
||||
Öffnen im Browser:
|
||||
Swagger UI: http://127.0.0.1:8000/docs
|
||||
OpenAPI JSON: http://127.0.0.1:8000/openapi.json
|
||||
"""
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# 1) Imports: Welche Bausteine brauchen wir?
|
||||
# -----------------------------------------------------------------------------
|
||||
# FastAPI: das Framework
|
||||
# HTTPException: um sauber Fehler (z.B. 404) zurückzugeben
|
||||
# Path, Query: um Parameter in URL und Query sauber zu beschreiben/validieren
|
||||
# status: komfortable HTTP-Statuscodes (z.B. 201, 204)
|
||||
from fastapi import FastAPI, HTTPException, Path, Query, status
|
||||
|
||||
# Pydantic: Modelle für Datenvalidierung (Request/Response-Schemas)
|
||||
# BaseModel: Basis für Datenmodelle
|
||||
# Field: Metadaten zu Feldern (Beispiele, Constraints, Beschreibung)
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
# typing: Typen für Listen etc. (OpenAPI profitiert davon)
|
||||
from typing import List
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# 2) App-Objekt: Das ist "die API"
|
||||
# -----------------------------------------------------------------------------
|
||||
# `app` ist der Einstiegspunkt:
|
||||
# - Alle Endpoints (@app.get, @app.post, ...) hängen an diesem Objekt.
|
||||
# - FastAPI sammelt daraus automatisch die OpenAPI-Doku.
|
||||
app = FastAPI(
|
||||
title="User Service API",
|
||||
description="Simple CRUD API to demonstrate Code First OpenAPI generation with FastAPI",
|
||||
version="1.0.0",
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# 3) Datenmodelle (Pydantic): Der "Vertrag" der API
|
||||
# -----------------------------------------------------------------------------
|
||||
# FastAPI nutzt diese Modelle für:
|
||||
# - Validierung von eingehenden Daten (Request Body)
|
||||
# - automatische Erzeugung der OpenAPI-Schemas
|
||||
# - Ausgabeformat (Response) über response_model
|
||||
#
|
||||
# WICHTIG:
|
||||
# "Pydantic macht unsere Typannotationen ausführbar: es prüft Daten zur Laufzeit."
|
||||
|
||||
|
||||
class UserBase(BaseModel):
|
||||
"""
|
||||
Basis-Modell für einen User.
|
||||
|
||||
Enthält die Felder, die sowohl beim Erstellen als auch beim Zurückgeben
|
||||
eines Users (Response) vorkommen.
|
||||
|
||||
Warum "Base"?
|
||||
- Wir können Modelle wiederverwenden und Duplikate vermeiden.
|
||||
"""
|
||||
|
||||
# Field(...): bedeutet "Pflichtfeld" (required)
|
||||
# json_schema_extra: Beispielwerte für Swagger/OpenAPI (Pydantic v2-konform)
|
||||
name: str = Field(
|
||||
...,
|
||||
description="Der Name des Users",
|
||||
json_schema_extra={"example": "Alice"},
|
||||
)
|
||||
age: int = Field(
|
||||
...,
|
||||
description="Das Alter des Users (als Zahl)",
|
||||
json_schema_extra={"example": 22},
|
||||
ge=0, # ge = greater or equal: verhindert negative Alterswerte
|
||||
)
|
||||
email: str = Field(
|
||||
...,
|
||||
description="E-Mail-Adresse des Users",
|
||||
json_schema_extra={"example": "alice@example.com"},
|
||||
)
|
||||
|
||||
|
||||
class UserCreate(UserBase):
|
||||
"""
|
||||
Modell für das Erstellen eines Users.
|
||||
|
||||
In diesem Beispiel ist es identisch zu UserBase.
|
||||
Warum trotzdem eine eigene Klasse?
|
||||
- In echten Projekten unterscheiden sich Create-Modelle oft (z.B. kein 'id').
|
||||
- So bleibt die API sauber erweiterbar, ohne später alles umzubauen.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class User(UserBase):
|
||||
"""
|
||||
Modell für den User, so wie er von der API zurückgegeben wird.
|
||||
|
||||
Unterschied zu UserCreate:
|
||||
- 'id' kommt hinzu (die wird serverseitig vergeben).
|
||||
"""
|
||||
|
||||
id: int = Field(
|
||||
...,
|
||||
description="Eindeutige ID des Users",
|
||||
json_schema_extra={"example": 1},
|
||||
ge=1,
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# 4) Fake-Datenbank (In-Memory)
|
||||
# -----------------------------------------------------------------------------
|
||||
# Für den Workshop nutzen wir absichtlich keine echte Datenbank,
|
||||
# damit jeder die API sofort lokal starten kann.
|
||||
#
|
||||
# Wichtig:
|
||||
# - Daten sind weg, sobald der Server neu startet.
|
||||
# - Nicht threadsafe/produktionsreif.
|
||||
# - Für Demo/Workshop absolut perfekt.
|
||||
users_db: dict[int, User] = {}
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# 5) CRUD Endpoints
|
||||
# -----------------------------------------------------------------------------
|
||||
# CRUD = Create, Read, Update, Delete
|
||||
#
|
||||
# Wir benutzen HTTP-Methoden:
|
||||
# - POST => Create
|
||||
# - GET => Read (list / detail)
|
||||
# - PUT => Update
|
||||
# - DELETE => Delete
|
||||
#
|
||||
# FastAPI generiert aus den folgenden Decorators automatisch OpenAPI:
|
||||
# - Pfade (/users, /users/{user_id})
|
||||
# - Methoden (GET/POST/PUT/DELETE)
|
||||
# - Parameter (Path/Query)
|
||||
# - Request Body (Pydantic-Model)
|
||||
# - Response-Model (response_model=...)
|
||||
# - Statuscodes, Summary, Description, Tags
|
||||
|
||||
|
||||
@app.post(
|
||||
"/users",
|
||||
response_model=User,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
summary="Create a new user",
|
||||
description="Creates a new user and returns the created resource",
|
||||
tags=["Users"],
|
||||
)
|
||||
def create_user(user: UserCreate):
|
||||
"""
|
||||
CREATE: Einen neuen User anlegen.
|
||||
|
||||
Request:
|
||||
- Body enthält ein UserCreate JSON (name, age, email)
|
||||
- FastAPI validiert automatisch (z.B. age muss int sein)
|
||||
|
||||
Response:
|
||||
- 201 Created
|
||||
- User (inkl. id)
|
||||
|
||||
Hinweis:
|
||||
- Wir generieren die ID hier simpel über `len(users_db) + 1`.
|
||||
Das ist für Demo ok, aber in Produktion würde das die Datenbank übernehmen.
|
||||
"""
|
||||
user_id = len(users_db) + 1
|
||||
|
||||
# Pydantic v2: model_dump() statt dict()
|
||||
new_user = User(id=user_id, **user.model_dump())
|
||||
|
||||
users_db[user_id] = new_user
|
||||
return new_user
|
||||
|
||||
|
||||
@app.get(
|
||||
"/users",
|
||||
response_model=List[User],
|
||||
summary="List all users",
|
||||
description="Returns a list of all users",
|
||||
tags=["Users"],
|
||||
)
|
||||
def list_users(
|
||||
limit: int = Query(
|
||||
10,
|
||||
ge=1,
|
||||
le=100,
|
||||
description="Maximum number of users to return",
|
||||
)
|
||||
):
|
||||
"""
|
||||
READ (List): Alle Users zurückgeben.
|
||||
|
||||
Query-Parameter:
|
||||
- limit: wie viele Users maximal zurückgegeben werden
|
||||
- Default: 10
|
||||
- Minimum: 1
|
||||
- Maximum: 100
|
||||
|
||||
Response:
|
||||
- 200 OK
|
||||
- Liste von Users
|
||||
"""
|
||||
return list(users_db.values())[:limit]
|
||||
|
||||
|
||||
@app.get(
|
||||
"/users/{user_id}",
|
||||
response_model=User,
|
||||
summary="Get user by ID",
|
||||
description="Returns a single user by its ID",
|
||||
tags=["Users"],
|
||||
)
|
||||
def get_user(
|
||||
user_id: int = Path(
|
||||
...,
|
||||
ge=1,
|
||||
description="The ID of the user",
|
||||
)
|
||||
):
|
||||
"""
|
||||
READ (Detail): Einen User anhand seiner ID holen.
|
||||
|
||||
Path-Parameter:
|
||||
- user_id: ID in der URL, z.B. /users/3
|
||||
|
||||
Wenn der User nicht existiert:
|
||||
- 404 Not Found mit {"detail": "User not found"}
|
||||
"""
|
||||
user = users_db.get(user_id)
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
return user
|
||||
|
||||
|
||||
@app.put(
|
||||
"/users/{user_id}",
|
||||
response_model=User,
|
||||
summary="Update a user",
|
||||
description="Updates an existing user",
|
||||
tags=["Users"],
|
||||
)
|
||||
def update_user(
|
||||
user_id: int = Path(..., ge=1, description="The ID of the user"),
|
||||
user: UserCreate = ...,
|
||||
):
|
||||
"""
|
||||
UPDATE: Einen bestehenden User vollständig überschreiben.
|
||||
|
||||
PUT bedeutet in der Regel:
|
||||
- Der Client schickt den kompletten Datensatz (name, age, email)
|
||||
- Der Server ersetzt den existierenden Eintrag vollständig
|
||||
|
||||
Wenn der User nicht existiert:
|
||||
- 404 Not Found
|
||||
|
||||
Response:
|
||||
- 200 OK
|
||||
- Aktualisierter User (inkl. id)
|
||||
"""
|
||||
if user_id not in users_db:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
updated_user = User(id=user_id, **user.model_dump())
|
||||
users_db[user_id] = updated_user
|
||||
return updated_user
|
||||
|
||||
|
||||
@app.delete(
|
||||
"/users/{user_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
summary="Delete a user",
|
||||
description="Deletes a user by its ID",
|
||||
tags=["Users"],
|
||||
)
|
||||
def delete_user(
|
||||
user_id: int = Path(..., ge=1, description="The ID of the user"),
|
||||
):
|
||||
"""
|
||||
DELETE: Einen User löschen.
|
||||
|
||||
Wenn der User nicht existiert:
|
||||
- 404 Not Found
|
||||
|
||||
Response:
|
||||
- 204 No Content (kein Response-Body)
|
||||
-> Das ist üblich bei erfolgreichen DELETEs.
|
||||
"""
|
||||
if user_id not in users_db:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
del users_db[user_id]
|
||||
# Bei 204 geben wir keinen Inhalt zurück.
|
||||
return
|
||||
Loading…
Reference in New Issue