FastAPI-Sprint 2024, Freiburg
- https://cusy.io/de/ueber/trefft-uns/fastapi-sprint-2024-freiburg
- FastAPI-Sprint 2024, Freiburg
- 2024-04-03T00:00:00+02:00
- 2024-04-05T23:59:59+02:00
- Was Agile Entwciklung Data Science Data Engineering Python Training Featured
- Wann 03.04.2024 bis 05.04.2024 (Europe/Berlin / UTC200)
- Wo Kreativpark Lokhalle Freiburg, Paul-Ehrlich-Straße 5-13, 79106 Freiburg im Breisgau
- Name des Kontakts Veit Schiele
- Telefon des Kontakts +49 30 22430082
- Teilnehmer Veit Schiele Kristian Rother Tim Weber Frank Hofmann
- Termin zum Kalender hinzufügen iCal
FastAPI ist ein Web-Framework zum Erstellen von APIs mit auf Python 3.6+ basierenden Type-Hints.
Die Hauptmerkmale sind:
- sehr hohe Leistung dank pydantic für den Datenteil und Starlette für den Web-Teil
- schnell und einfach zu programmieren
- Validierung für die meisten Python-Datentypen
- robuster, produktionsreifer Code mit automatischer interaktiver Dokumentation
- basierend auf den offenen Standards für APIs: OpenAPI (früher bekannt als Swagger) und JSON Schema.
Schneller Start am ersten Tag
Schon am ersten Sprint-Tag machte sich bemerkbar, wie schnell sich mit FastAPI eine Web-Anwendung erstellen lässt. Nach kürzester Zeit waren die ersten Prototypen fertiggestellt.
Testen der API
Nicht viel länger dauerte es, um sie mit der TestClient-Klasse von FastAPI Tests zu schreiben:
from fastapi.testclient import TestClient from fastapi_htmx.main import app def test_items(): client = TestClient(app) response = client.get('/items') assert response.status_code == 200 j = response.json() assert len(j) == 3 assert j[0]["name"] == "Alpha"
Web-Frontend
Als nächstes schauten wir uns htmx an um ein Web-Frontend für FastAPI bereitstellen zu können. So schnell und einfach hatten wir schon lange keine interaktive Web-Anwendung gebaut. Zunächst war es auch nur eine einzelne statische HTML-Datei:
<!DOCTYPE html> <html> <head> <script src="https://unpkg.com/htmx.org@1.9.11"></script> <script src="https://unpkg.com/nunjucks@3.2.4/browser/nunjucks.min.js"></script> <script src="https://unpkg.com/htmx.org@1.9.11/dist/ext/client-side-templates.js"></script> <body> <h1>Hello World!</h1> <p>Items:</p> <div hx-ext="client-side-templates" hx-get="/items" hx-trigger="load" hx-target="#items" nunjucks-array-template="render-items" > <div id="items"></div> <template id="render-items"> <ul> {% for item in data %} <li>{{ item.name }}</li> {% endfor %} </ul> </template> </div> </body> </html>
In diesem Beispiel wird mit der htmx-Erweiterung client-side-templates das Rendering der Seite im Browser unterstützt.
Automatische Tests für das Web-Frontend
Auch für dieses Web-Frontend schrieben wir Tests, diesmal jedoch mit Playwright, z.B.:
from playwright.sync_api import sync_playwright def test_frontend_local(): with sync_playwright() as p: browser = p.firefox.launch(headless=True) context = browser.new_context() page = browser.new_page() page.goto("http://localhost:8000/") page.locator("#item_summary").click() page.locator("#item_summary").fill("Some item") page.locator("input[name=\"owner\"]").click() page.locator("input[name=\"owner\"]").fill("Kristian Rother") page.get_by_role("button", name="Add").click() html = page.inner_html('#item') assert "Summary: Some item" in html assert "Owner: Kristian Rother" in html context.close() browser.close()
Playwright bietet auch ein pytest-Plugin, mit dem sich Test-Fixtures erstellen lassen um in einem Test auch mehrere Browser testen zu können. Werden die Tests mit headless=False aufgerufen, können sie auch gut zur Fehlersuche verwendet werden.
Dynamisches Routing
Zum Abschluss des ersten Tages wollten wir sowohl die API wie auch die HTML-Seiten über dieselbe URL ausliefern. Leider konnten wir hier nicht eine einfache Middleware verwenden, da wir uns in die Anfrage/Antwort-Bearbeitung von FastAPI einklinken wollten um automatisch eine Vorlage auszuwählen zu können, die auf der Pfad-Funktion basiert, die die Anfrage bearbeitet hat:
def get_template_render_route_class(template_dir: str) -> type: """Return APIRoute subclass that renders HTML templates from JSON. This is a function that returns a class, mainly because we'd like to be able to customize the template_dir, but we don't call __init__() on the class ourself, FastAPI does it. """ class RenderRoute(APIRoute): # This method will be called on startup to get the actual handler. def get_route_handler(self) -> Callable: # Get the handler from the parent class. original = super().get_route_handler() # Set up Jinja to be able to render templates. jinja_env = Environment( loader=FileSystemLoader(template_dir), ) def is_hx(request: Request) -> bool: """Check whether the request is sent by HTMX.""" # HTMX always sets this header, see # <https://htmx.org/docs/#request-headers>. return request.headers.get("HX-Request", "") == "true" def wants_html(request: Request) -> bool: """Check whether the client requested HTML.""" # This is just a proof of concept, more solid `Accept` header # parsing would be required. return request.headers.get("Accept", "") == "text/html" def should_render(request: Request) -> bool: """Check whether this request should be rendered as HTML.""" return is_hx(request) or wants_html(request) def get_template(endpoint: Callable|None) -> Template|None: """Get Jinja template instance for a given path operation.""" if endpoint is None: # We don't know the function that handled this request, # therefore we can't select a corresponding template. return None # Simply add `.html` to the function name and try to look this # up as a template file. try: return jinja_env.get_template(f"{endpoint.__name__}.html") except TemplateNotFound: return None # This is the actual function that is called on every request. async def route_handler(request: Request) -> Response: # Handle the request and get back a response. response = await original(request) if ( # rendering as HTML makes sense should_render(request) # the response is actually JSON that we can work with and isinstance(response, PreserveJSONResponse) # we have a template available and (template := get_template(self.dependant.call)) ): # Render the Jinja template and return as HTML response. return HTMLResponse(template.render( data=response.original_data, )) # If anything failed or the requirements were not met, return # the original response. return response # Provide FastAPI with the route handler. return route_handler return RenderRoute
In der RenderRoute-Klasse definieren wir einen Route-Handler, der sich in das request/response-Handling von FastAPI einklinkt. Damit kommen wir in die Lage, ein Jinja2-Template auf Basis der Pfad-Informationen auszuwählen. Dies wurde erforderlich, da eine einfache Middleware keinen Zugriff auf diese Informationen hätte. Zudem wird im HTML-Header nachgeschaut, ob er HX-Request oder Accept: text/html enthält um anschließend ein Template mit dem Namen des Endpunkts und dem .html-Suffix auszuwählen.
Dieser Mechanismus erlaubt uns auch, über dieselbe URL die Daten in unterschiedlichen Serialisierungsformaten (z.B. JSON, XML oder csv) bereitzustellen.