From 4c8dc27ba99abb70e736a7470b776804f8a13ac7 Mon Sep 17 00:00:00 2001 From: Fabian <2121190@stud.hs-mannheim.de> Date: Tue, 10 Feb 2026 13:44:26 +0100 Subject: [PATCH] add code first file --- CodeFirst/code_first_explanation.py | 307 ++++++++++++++++++++++++++++ 1 file changed, 307 insertions(+) create mode 100644 CodeFirst/code_first_explanation.py diff --git a/CodeFirst/code_first_explanation.py b/CodeFirst/code_first_explanation.py new file mode 100644 index 0000000..a4ec9ae --- /dev/null +++ b/CodeFirst/code_first_explanation.py @@ -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 \ No newline at end of file