Merge branch 'master' into fix/uploadfile-downcast

This commit is contained in:
Motov Yurii 2025-12-05 07:41:06 +01:00 committed by GitHub
commit 38bf832c34
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
102 changed files with 3055 additions and 602 deletions

View File

@ -24,7 +24,9 @@ jobs:
env: env:
GITHUB_CONTEXT: ${{ toJson(github) }} GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT" run: echo "$GITHUB_CONTEXT"
- uses: actions/checkout@v6 # pin to actions/checkout@v5 for compatibility with latest-changes
# Ref: https://github.com/actions/checkout/issues/2313
- uses: actions/checkout@v5
with: with:
# To allow latest-changes to commit to the main branch # To allow latest-changes to commit to the main branch
token: ${{ secrets.FASTAPI_LATEST_CHANGES }} token: ${{ secrets.FASTAPI_LATEST_CHANGES }}
@ -34,7 +36,7 @@ jobs:
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled == 'true' }} if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled == 'true' }}
with: with:
limit-access-to-actor: true limit-access-to-actor: true
- uses: tiangolo/latest-changes@0.4.0 - uses: tiangolo/latest-changes@0.4.1
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
latest_changes_file: docs/en/docs/release-notes.md latest_changes_file: docs/en/docs/release-notes.md

View File

@ -40,7 +40,7 @@ The key features are:
* **Robust**: Get production-ready code. With automatic interactive documentation. * **Robust**: Get production-ready code. With automatic interactive documentation.
* **Standards-based**: Based on (and fully compatible with) the open standards for APIs: <a href="https://github.com/OAI/OpenAPI-Specification" class="external-link" target="_blank">OpenAPI</a> (previously known as Swagger) and <a href="https://json-schema.org/" class="external-link" target="_blank">JSON Schema</a>. * **Standards-based**: Based on (and fully compatible with) the open standards for APIs: <a href="https://github.com/OAI/OpenAPI-Specification" class="external-link" target="_blank">OpenAPI</a> (previously known as Swagger) and <a href="https://json-schema.org/" class="external-link" target="_blank">JSON Schema</a>.
<small>* estimation based on tests on an internal development team, building production applications.</small> <small>* estimation based on tests conducted by an internal development team, building production applications.</small>
## Sponsors ## Sponsors
@ -61,6 +61,7 @@ The key features are:
<a href="https://subtotal.com/?utm_source=fastapi&utm_medium=sponsorship&utm_campaign=open-source" target="_blank" title="The Gold Standard in Retail Account Linking"><img src="https://fastapi.tiangolo.com/img/sponsors/subtotal.svg"></a> <a href="https://subtotal.com/?utm_source=fastapi&utm_medium=sponsorship&utm_campaign=open-source" target="_blank" title="The Gold Standard in Retail Account Linking"><img src="https://fastapi.tiangolo.com/img/sponsors/subtotal.svg"></a>
<a href="https://docs.railway.com/guides/fastapi?utm_medium=integration&utm_source=docs&utm_campaign=fastapi" target="_blank" title="Deploy enterprise applications at startup speed"><img src="https://fastapi.tiangolo.com/img/sponsors/railway.png"></a> <a href="https://docs.railway.com/guides/fastapi?utm_medium=integration&utm_source=docs&utm_campaign=fastapi" target="_blank" title="Deploy enterprise applications at startup speed"><img src="https://fastapi.tiangolo.com/img/sponsors/railway.png"></a>
<a href="https://serpapi.com/?utm_source=fastapi_website" target="_blank" title="SerpApi: Web Search API"><img src="https://fastapi.tiangolo.com/img/sponsors/serpapi.png"></a> <a href="https://serpapi.com/?utm_source=fastapi_website" target="_blank" title="SerpApi: Web Search API"><img src="https://fastapi.tiangolo.com/img/sponsors/serpapi.png"></a>
<a href="https://www.greptile.com/?utm_source=fastapi&utm_medium=sponsorship&utm_campaign=fastapi_sponsor_page" target="_blank" title="Greptile: The AI Code Reviewer"><img src="https://fastapi.tiangolo.com/img/sponsors/greptile.png"></a>
<a href="https://databento.com/?utm_source=fastapi&utm_medium=sponsor&utm_content=display" target="_blank" title="Pay as you go for market data"><img src="https://fastapi.tiangolo.com/img/sponsors/databento.svg"></a> <a href="https://databento.com/?utm_source=fastapi&utm_medium=sponsor&utm_content=display" target="_blank" title="Pay as you go for market data"><img src="https://fastapi.tiangolo.com/img/sponsors/databento.svg"></a>
<a href="https://speakeasy.com/editor?utm_source=fastapi+repo&utm_medium=github+sponsorship" target="_blank" title="SDKs for your API | Speakeasy"><img src="https://fastapi.tiangolo.com/img/sponsors/speakeasy.png"></a> <a href="https://speakeasy.com/editor?utm_source=fastapi+repo&utm_medium=github+sponsorship" target="_blank" title="SDKs for your API | Speakeasy"><img src="https://fastapi.tiangolo.com/img/sponsors/speakeasy.png"></a>
<a href="https://www.svix.com/" target="_blank" title="Svix - Webhooks as a service"><img src="https://fastapi.tiangolo.com/img/sponsors/svix.svg"></a> <a href="https://www.svix.com/" target="_blank" title="Svix - Webhooks as a service"><img src="https://fastapi.tiangolo.com/img/sponsors/svix.svg"></a>

View File

@ -443,7 +443,7 @@ Für einige sprachspezifische Anweisungen, siehe z. B. den Abschnitt `### Headin
* die Workload * die Workload
* das Deployment * das Deployment
* bereitstellen * deployen
* das SDK * das SDK
* das Software Development Kit * das Software Development Kit

View File

@ -144,7 +144,7 @@ Dies wurde in Version 0.110.0 geändert, um unbehandelten Speicherverbrauch durc
### Hintergrundtasks und Abhängigkeiten mit `yield`, Technische Details { #background-tasks-and-dependencies-with-yield-technical-details } ### Hintergrundtasks und Abhängigkeiten mit `yield`, Technische Details { #background-tasks-and-dependencies-with-yield-technical-details }
Vor FastAPI 0.106.0 war das Werfen von Exceptions nach `yield` nicht möglich, der Exit-Code in Abhängigkeiten mit `yield` wurde ausgeführt, nachdem die Response gesendet wurde, sodass [Exceptionhandler](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank} bereits ausgeführt worden wären. Vor FastAPI 0.106.0 war das Werfen von Exceptions nach `yield` nicht möglich, der Exit-Code in Abhängigkeiten mit `yield` wurde ausgeführt, nachdem die Response gesendet wurde, sodass [Exceptionhandler](../tutorial/handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank} bereits ausgeführt worden wären.
Dies war so designt, hauptsächlich um die Verwendung derselben von Abhängigkeiten „geyieldeten“ Objekte in Hintergrundtasks zu ermöglichen, da der Exit-Code erst ausgeführt wurde, nachdem die Hintergrundtasks abgeschlossen waren. Dies war so designt, hauptsächlich um die Verwendung derselben von Abhängigkeiten „geyieldeten“ Objekte in Hintergrundtasks zu ermöglichen, da der Exit-Code erst ausgeführt wurde, nachdem die Hintergrundtasks abgeschlossen waren.

View File

@ -64,7 +64,7 @@ Wenn Sie mehr über HTTPS erfahren möchten, lesen Sie den Leitfaden [Über HTTP
/// ///
### Wie Proxy-Forwarded-Header funktionieren ### Wie Proxy-Forwarded-Header funktionieren { #how-proxy-forwarded-headers-work }
Hier ist eine visuelle Darstellung, wie der **Proxy** weitergeleitete Header zwischen dem Client und dem **Anwendungsserver** hinzufügt: Hier ist eine visuelle Darstellung, wie der **Proxy** weitergeleitete Header zwischen dem Client und dem **Anwendungsserver** hinzufügt:
@ -228,7 +228,7 @@ Die Übergabe des `root_path` an `FastAPI` wäre das Äquivalent zur Übergabe d
Beachten Sie, dass der Server (Uvicorn) diesen `root_path` für nichts anderes verwendet als für die Weitergabe an die Anwendung. Beachten Sie, dass der Server (Uvicorn) diesen `root_path` für nichts anderes verwendet als für die Weitergabe an die Anwendung.
Aber wenn Sie mit Ihrem Browser auf <a href="http://127.0.0.1:8000" class="external-link" target="_blank">http://127.0.0.1:8000/app</a> gehen, sehen Sie die normale Response: Aber wenn Sie mit Ihrem Browser auf <a href="http://127.0.0.1:8000/app" class="external-link" target="_blank">http://127.0.0.1:8000/app</a> gehen, sehen Sie die normale Response:
```JSON ```JSON
{ {
@ -443,6 +443,14 @@ Die Dokumentationsoberfläche interagiert mit dem von Ihnen ausgewählten Server
/// ///
/// note | Technische Details
Die Eigenschaft `servers` in der OpenAPI-Spezifikation ist optional.
Wenn Sie den Parameter `servers` nicht angeben und `root_path` den Wert `/` hat, wird die Eigenschaft `servers` im generierten OpenAPI-Schema standardmäßig vollständig weggelassen, was dem Äquivalent eines einzelnen Servers mit einem `url`-Wert von `/` entspricht.
///
### Den automatischen Server von `root_path` deaktivieren { #disable-automatic-server-from-root-path } ### Den automatischen Server von `root_path` deaktivieren { #disable-automatic-server-from-root-path }
Wenn Sie nicht möchten, dass **FastAPI** einen automatischen Server inkludiert, welcher `root_path` verwendet, können Sie den Parameter `root_path_in_servers=False` verwenden: Wenn Sie nicht möchten, dass **FastAPI** einen automatischen Server inkludiert, welcher `root_path` verwendet, können Sie den Parameter `root_path_in_servers=False` verwenden:

View File

@ -1,16 +1,24 @@
# FastAPI bei Cloudanbietern bereitstellen { #deploy-fastapi-on-cloud-providers } # FastAPI bei Cloudanbietern deployen { #deploy-fastapi-on-cloud-providers }
Sie können praktisch **jeden Cloudanbieter** verwenden, um Ihre FastAPI-Anwendung bereitzustellen. Sie können praktisch **jeden Cloudanbieter** verwenden, um Ihre FastAPI-Anwendung bereitzustellen.
In den meisten Fällen bieten die großen Cloudanbieter Anleitungen zum Bereitstellen von FastAPI an. In den meisten Fällen bieten die großen Cloudanbieter Anleitungen zum Deployment von FastAPI an.
## FastAPI Cloud { #fastapi-cloud }
**<a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>** wurde vom selben Autor und Team hinter **FastAPI** entwickelt.
Es vereinfacht den Prozess des **Erstellens**, **Deployens** und **Zugreifens** auf eine API mit minimalem Aufwand.
Es bringt die gleiche **Developer-Experience** beim Erstellen von Apps mit FastAPI auch zum **Deployment** in der Cloud. 🎉
FastAPI Cloud ist der Hauptsponsor und Finanzierungsgeber für die *FastAPI and friends* Open-Source-Projekte. ✨
## Cloudanbieter Sponsoren { #cloud-providers-sponsors } ## Cloudanbieter Sponsoren { #cloud-providers-sponsors }
Einige Cloudanbieter ✨ [**sponsern FastAPI**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨, dies stellt die kontinuierliche und gesunde **Entwicklung** von FastAPI und seinem **Ökosystem** sicher. Einige andere Cloudanbieter ✨ [**sponsern FastAPI**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨ ebenfalls. 🙇
Und es zeigt ihr wahres Engagement für FastAPI und seine **Community** (Sie), da sie Ihnen nicht nur einen **guten Service** bieten möchten, sondern auch sicherstellen möchten, dass Sie ein **gutes und gesundes Framework**, FastAPI, haben. 🙇 Sie könnten diese ebenfalls in Betracht ziehen, deren Anleitungen folgen und ihre Dienste ausprobieren:
Vielleicht möchten Sie deren Dienste ausprobieren und deren Anleitungen folgen:
* <a href="https://docs.render.com/deploy-fastapi?utm_source=deploydoc&utm_medium=referral&utm_campaign=fastapi" class="external-link" target="_blank">Render</a> * <a href="https://docs.render.com/deploy-fastapi?utm_source=deploydoc&utm_medium=referral&utm_campaign=fastapi" class="external-link" target="_blank">Render</a>
* <a href="https://docs.railway.com/guides/fastapi?utm_medium=integration&utm_source=docs&utm_campaign=fastapi" class="external-link" target="_blank">Railway</a> * <a href="https://docs.railway.com/guides/fastapi?utm_medium=integration&utm_source=docs&utm_campaign=fastapi" class="external-link" target="_blank">Railway</a>

View File

@ -1,6 +1,6 @@
# Deployment-Konzepte { #deployments-concepts } # Deployment-Konzepte { #deployments-concepts }
Bei dem Deployment der Bereitstellung einer **FastAPI**-Anwendung, oder eigentlich jeder Art von Web-API, gibt es mehrere Konzepte, die Sie wahrscheinlich interessieren, und mithilfe der Sie die **am besten geeignete** Methode zur **Bereitstellung Ihrer Anwendung** finden können. Bei dem Deployment der Bereitstellung einer **FastAPI**-Anwendung, oder eigentlich jeder Art von Web-API, gibt es mehrere Konzepte, die Sie wahrscheinlich interessieren, und mithilfe der Sie die **am besten geeignete** Methode zum **Deployment Ihrer Anwendung** finden können.
Einige wichtige Konzepte sind: Einige wichtige Konzepte sind:
@ -15,11 +15,11 @@ Wir werden sehen, wie diese sich auf das **Deployment** auswirken.
Letztendlich besteht das ultimative Ziel darin, **Ihre API-Clients** auf **sichere** Weise zu versorgen, um **Unterbrechungen** zu vermeiden und die **Rechenressourcen** (z. B. entfernte Server/virtuelle Maschinen) so effizient wie möglich zu nutzen. 🚀 Letztendlich besteht das ultimative Ziel darin, **Ihre API-Clients** auf **sichere** Weise zu versorgen, um **Unterbrechungen** zu vermeiden und die **Rechenressourcen** (z. B. entfernte Server/virtuelle Maschinen) so effizient wie möglich zu nutzen. 🚀
Ich erzähle Ihnen hier etwas mehr über diese **Konzepte**, was Ihnen hoffentlich die **Intuition** gibt, die Sie benötigen, um zu entscheiden, wie Sie Ihre API in sehr unterschiedlichen Umgebungen bereitstellen, möglicherweise sogar in **zukünftigen**, die jetzt noch nicht existieren. Ich erzähle Ihnen hier etwas mehr über diese **Konzepte**, was Ihnen hoffentlich die **Intuition** gibt, die Sie benötigen, um zu entscheiden, wie Sie Ihre API in sehr unterschiedlichen Umgebungen deployen, möglicherweise sogar in **zukünftigen**, die jetzt noch nicht existieren.
Durch die Berücksichtigung dieser Konzepte können Sie die beste Variante der Bereitstellung **Ihrer eigenen APIs** **evaluieren und konzipieren**. Durch die Berücksichtigung dieser Konzepte können Sie die beste Variante des Deployments **Ihrer eigenen APIs** **evaluieren und konzipieren**.
In den nächsten Kapiteln werde ich Ihnen mehr **konkrete Rezepte** für die Bereitstellung von FastAPI-Anwendungen geben. In den nächsten Kapiteln werde ich Ihnen mehr **konkrete Rezepte** für das Deployment von FastAPI-Anwendungen geben.
Aber schauen wir uns zunächst einmal diese grundlegenden **konzeptionellen Ideen** an. Diese Konzepte gelten auch für jede andere Art von Web-API. 💡 Aber schauen wir uns zunächst einmal diese grundlegenden **konzeptionellen Ideen** an. Diese Konzepte gelten auch für jede andere Art von Web-API. 💡
@ -271,7 +271,7 @@ In diesem Fall müssen Sie sich darüber keine Sorgen machen. 🤷
### Beispiele für Strategien für Vorab-Schritte { #examples-of-previous-steps-strategies } ### Beispiele für Strategien für Vorab-Schritte { #examples-of-previous-steps-strategies }
Es hängt **stark** davon ab, wie Sie **Ihr System bereitstellen**, und hängt wahrscheinlich mit der Art und Weise zusammen, wie Sie Programme starten, Neustarts durchführen, usw. Es hängt **stark** davon ab, wie Sie **Ihr System deployen**, und hängt wahrscheinlich mit der Art und Weise zusammen, wie Sie Programme starten, Neustarts durchführen, usw.
Hier sind einige mögliche Ideen: Hier sind einige mögliche Ideen:
@ -307,7 +307,7 @@ Sie können einfache Tools wie `htop` verwenden, um die in Ihrem Server verwende
## Zusammenfassung { #recap } ## Zusammenfassung { #recap }
Sie haben hier einige der wichtigsten Konzepte gelesen, die Sie wahrscheinlich berücksichtigen müssen, wenn Sie entscheiden, wie Sie Ihre Anwendung bereitstellen: Sie haben hier einige der wichtigsten Konzepte gelesen, die Sie wahrscheinlich berücksichtigen müssen, wenn Sie entscheiden, wie Sie Ihre Anwendung deployen:
* Sicherheit HTTPS * Sicherheit HTTPS
* Beim Hochfahren ausführen * Beim Hochfahren ausführen

View File

@ -1,6 +1,6 @@
# FastAPI in Containern Docker { #fastapi-in-containers-docker } # FastAPI in Containern Docker { #fastapi-in-containers-docker }
Beim Deployment von FastAPI-Anwendungen besteht ein gängiger Ansatz darin, ein **Linux-Containerimage** zu erstellen. Normalerweise erfolgt dies mit <a href="https://www.docker.com/" class="external-link" target="_blank">**Docker**</a>. Sie können dieses Containerimage dann auf eine von mehreren möglichen Arten bereitstellen. Beim Deployment von FastAPI-Anwendungen besteht ein gängiger Ansatz darin, ein **Linux-Containerimage** zu erstellen. Normalerweise erfolgt dies mit <a href="https://www.docker.com/" class="external-link" target="_blank">**Docker**</a>. Sie können dieses Containerimage dann auf eine von mehreren möglichen Arten deployen.
Die Verwendung von Linux-Containern bietet mehrere Vorteile, darunter **Sicherheit**, **Replizierbarkeit**, **Einfachheit** und andere. Die Verwendung von Linux-Containern bietet mehrere Vorteile, darunter **Sicherheit**, **Replizierbarkeit**, **Einfachheit** und andere.
@ -40,7 +40,7 @@ Linux-Container werden mit demselben Linux-Kernel des Hosts (Maschine, virtuelle
Auf diese Weise verbrauchen Container **wenig Ressourcen**, eine Menge vergleichbar mit der direkten Ausführung der Prozesse (eine virtuelle Maschine würde viel mehr verbrauchen). Auf diese Weise verbrauchen Container **wenig Ressourcen**, eine Menge vergleichbar mit der direkten Ausführung der Prozesse (eine virtuelle Maschine würde viel mehr verbrauchen).
Container verfügen außerdem über ihre eigenen **isoliert** laufenden Prozesse (üblicherweise nur einen Prozess), über ihr eigenes Dateisystem und ihr eigenes Netzwerk, was die Bereitstellung, Sicherheit, Entwicklung usw. vereinfacht. Container verfügen außerdem über ihre eigenen **isoliert** laufenden Prozesse (üblicherweise nur einen Prozess), über ihr eigenes Dateisystem und ihr eigenes Netzwerk, was Deployment, Sicherheit, Entwicklung usw. vereinfacht.
## Was ist ein Containerimage { #what-is-a-container-image } ## Was ist ein Containerimage { #what-is-a-container-image }
@ -598,7 +598,7 @@ Zum Beispiel:
* Mit einem **Kubernetes**-Cluster * Mit einem **Kubernetes**-Cluster
* Mit einem Docker Swarm Mode-Cluster * Mit einem Docker Swarm Mode-Cluster
* Mit einem anderen Tool wie Nomad * Mit einem anderen Tool wie Nomad
* Mit einem Cloud-Dienst, der Ihr Containerimage nimmt und es bereitstellt * Mit einem Cloud-Dienst, der Ihr Containerimage nimmt und es deployt
## Docker-Image mit `uv` { #docker-image-with-uv } ## Docker-Image mit `uv` { #docker-image-with-uv }

View File

@ -0,0 +1,65 @@
# FastAPI Cloud { #fastapi-cloud }
Sie können Ihre FastAPI-App in der <a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a> mit **einem einzigen Befehl** deployen tragen Sie sich in die Warteliste ein, falls noch nicht geschehen. 🚀
## Anmelden { #login }
Stellen Sie sicher, dass Sie bereits ein **FastAPI-Cloud-Konto** haben (wir haben Sie von der Warteliste eingeladen 😉).
Melden Sie sich dann an:
<div class="termy">
```console
$ fastapi login
You are logged in to FastAPI Cloud 🚀
```
</div>
## Deployen { #deploy }
Stellen Sie Ihre App jetzt mit **einem einzigen Befehl** bereit:
<div class="termy">
```console
$ fastapi deploy
Deploying to FastAPI Cloud...
✅ Deployment successful!
🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev
```
</div>
Das wars! Jetzt können Sie Ihre App unter dieser URL aufrufen. ✨
## Über FastAPI Cloud { #about-fastapi-cloud }
**<a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>** wird vom gleichen Autor und Team hinter **FastAPI** entwickelt.
Es vereinfacht den Prozess des **Erstellens**, **Deployens** und **Nutzens** einer API mit minimalem Aufwand.
Es bringt die gleiche **Developer-Experience** beim Erstellen von Apps mit FastAPI auch zum **Deployment** in der Cloud. 🎉
Es kümmert sich außerdem um das meiste, was beim Deployen einer App nötig ist, zum Beispiel:
* HTTPS
* Replikation, mit Autoscaling basierend auf Requests
* usw.
FastAPI Cloud ist Hauptsponsor und Finanzierer der Open-Source-Projekte *FastAPI and friends*. ✨
## Bei anderen Cloudanbietern deployen { #deploy-to-other-cloud-providers }
FastAPI ist Open Source und basiert auf Standards. Sie können FastAPI-Apps bei jedem Cloudanbieter Ihrer Wahl deployen.
Folgen Sie den Anleitungen Ihres Cloudanbieters, um dort FastAPI-Apps zu deployen. 🤓
## Auf den eigenen Server deployen { #deploy-your-own-server }
Ich werde Ihnen später in diesem **Deployment-Leitfaden** auch alle Details zeigen, sodass Sie verstehen, was passiert, was geschehen muss und wie Sie FastAPI-Apps selbst deployen können, auch auf Ihre eigenen Server. 🤓

View File

@ -14,7 +14,9 @@ Das steht im Gegensatz zu den **Entwicklungsphasen**, in denen Sie ständig den
Es gibt mehrere Möglichkeiten, dies zu tun, abhängig von Ihrem spezifischen Anwendungsfall und den von Ihnen verwendeten Tools. Es gibt mehrere Möglichkeiten, dies zu tun, abhängig von Ihrem spezifischen Anwendungsfall und den von Ihnen verwendeten Tools.
Sie könnten mithilfe einer Kombination von Tools selbst **einen Server bereitstellen**, Sie könnten einen **Cloud-Dienst** nutzen, der einen Teil der Arbeit für Sie erledigt, oder andere mögliche Optionen. Sie könnten mithilfe einer Kombination von Tools selbst **einen Server deployen**, Sie könnten einen **Cloud-Dienst** nutzen, der einen Teil der Arbeit für Sie erledigt, oder andere mögliche Optionen.
Zum Beispiel haben wir, das Team hinter FastAPI, <a href="https://fastapicloud.com" class="external-link" target="_blank">**FastAPI Cloud**</a> entwickelt, um das Deployment von FastAPI-Apps in der Cloud so reibungslos wie möglich zu gestalten, mit derselben Developer-Experience wie beim Arbeiten mit FastAPI.
Ich zeige Ihnen einige der wichtigsten Konzepte, die Sie beim Deployment einer **FastAPI**-Anwendung wahrscheinlich berücksichtigen sollten (obwohl das meiste davon auch für jede andere Art von Webanwendung gilt). Ich zeige Ihnen einige der wichtigsten Konzepte, die Sie beim Deployment einer **FastAPI**-Anwendung wahrscheinlich berücksichtigen sollten (obwohl das meiste davon auch für jede andere Art von Webanwendung gilt).

View File

@ -11,7 +11,7 @@ Schauen wir uns die Deployment-Konzepte von früher noch einmal an:
Bis zu diesem Punkt, in allen Tutorials in der Dokumentation, haben Sie wahrscheinlich ein **Serverprogramm** ausgeführt, zum Beispiel mit dem `fastapi`-Befehl, der Uvicorn startet, und einen **einzelnen Prozess** ausführt. Bis zu diesem Punkt, in allen Tutorials in der Dokumentation, haben Sie wahrscheinlich ein **Serverprogramm** ausgeführt, zum Beispiel mit dem `fastapi`-Befehl, der Uvicorn startet, und einen **einzelnen Prozess** ausführt.
Wenn Sie Anwendungen bereitstellen, möchten Sie wahrscheinlich eine gewisse **Replikation von Prozessen**, um **mehrere Kerne** zu nutzen und mehr <abbr title="Request Anfrage: Daten, die der Client zum Server sendet">Requests</abbr> bearbeiten zu können. Wenn Sie Anwendungen deployen, möchten Sie wahrscheinlich eine gewisse **Replikation von Prozessen**, um **mehrere Kerne** zu nutzen und mehr <abbr title="Request Anfrage: Daten, die der Client zum Server sendet">Requests</abbr> bearbeiten zu können.
Wie Sie im vorherigen Kapitel über [Deployment-Konzepte](concepts.md){.internal-link target=_blank} gesehen haben, gibt es mehrere Strategien, die Sie anwenden können. Wie Sie im vorherigen Kapitel über [Deployment-Konzepte](concepts.md){.internal-link target=_blank} gesehen haben, gibt es mehrere Strategien, die Sie anwenden können.

View File

@ -66,7 +66,7 @@ Das Ausführen von `fastapi run` startet FastAPI standardmäßig im Produktionsm
Standardmäßig ist **Autoreload** deaktiviert. Es horcht auch auf der IP-Adresse `0.0.0.0`, was alle verfügbaren IP-Adressen bedeutet, so wird es öffentlich zugänglich für jeden, der mit der Maschine kommunizieren kann. So würden Sie es normalerweise in der Produktion ausführen, beispielsweise in einem Container. Standardmäßig ist **Autoreload** deaktiviert. Es horcht auch auf der IP-Adresse `0.0.0.0`, was alle verfügbaren IP-Adressen bedeutet, so wird es öffentlich zugänglich für jeden, der mit der Maschine kommunizieren kann. So würden Sie es normalerweise in der Produktion ausführen, beispielsweise in einem Container.
In den meisten Fällen würden (und sollten) Sie einen „Terminierungsproxy“ haben, der HTTPS für Sie verwaltet. Dies hängt davon ab, wie Sie Ihre Anwendung bereitstellen. Ihr Anbieter könnte dies für Sie erledigen, oder Sie müssen es selbst einrichten. In den meisten Fällen würden (und sollten) Sie einen „Terminierungsproxy“ haben, der HTTPS für Sie verwaltet. Dies hängt davon ab, wie Sie Ihre Anwendung deployen. Ihr Anbieter könnte dies für Sie erledigen, oder Sie müssen es selbst einrichten.
/// tip | Tipp /// tip | Tipp

View File

@ -0,0 +1,17 @@
# Alte 403-Authentifizierungsfehler-Statuscodes verwenden { #use-old-403-authentication-error-status-codes }
Vor FastAPI-Version `0.122.0` verwendeten die integrierten Sicherheits-Utilities den HTTP-Statuscode `403 Forbidden`, wenn sie dem Client nach einer fehlgeschlagenen Authentifizierung einen Fehler zurückgaben.
Ab FastAPI-Version `0.122.0` verwenden sie den passenderen HTTP-Statuscode `401 Unauthorized` und geben in der Response einen sinnvollen `WWW-Authenticate`-Header zurück, gemäß den HTTP-Spezifikationen, <a href="https://datatracker.ietf.org/doc/html/rfc7235#section-3.1" class="external-link" target="_blank">RFC 7235</a>, <a href="https://datatracker.ietf.org/doc/html/rfc9110#name-401-unauthorized" class="external-link" target="_blank">RFC 9110</a>.
Aber falls Ihre Clients aus irgendeinem Grund vom alten Verhalten abhängen, können Sie darauf zurückgreifen, indem Sie in Ihren Sicherheitsklassen die Methode `make_not_authenticated_error` überschreiben.
Sie können beispielsweise eine Unterklasse von `HTTPBearer` erstellen, die einen Fehler `403 Forbidden` zurückgibt, statt des Default-`401 Unauthorized`-Fehlers:
{* ../../docs_src/authentication_error_status_code/tutorial001_an_py39.py hl[9:13] *}
/// tip | Tipp
Beachten Sie, dass die Funktion die Exception-Instanz zurückgibt; sie wirft sie nicht. Das Werfen erfolgt im restlichen internen Code.
///

View File

@ -46,20 +46,26 @@ Seine Schlüssel-Merkmale sind:
* **Robust**: Erhalten Sie produktionsreifen Code. Mit automatischer, interaktiver Dokumentation. * **Robust**: Erhalten Sie produktionsreifen Code. Mit automatischer, interaktiver Dokumentation.
* **Standards-basiert**: Basierend auf (und vollständig kompatibel mit) den offenen Standards für APIs: <a href="https://github.com/OAI/OpenAPI-Specification" class="external-link" target="_blank">OpenAPI</a> (früher bekannt als Swagger) und <a href="https://json-schema.org/" class="external-link" target="_blank">JSON Schema</a>. * **Standards-basiert**: Basierend auf (und vollständig kompatibel mit) den offenen Standards für APIs: <a href="https://github.com/OAI/OpenAPI-Specification" class="external-link" target="_blank">OpenAPI</a> (früher bekannt als Swagger) und <a href="https://json-schema.org/" class="external-link" target="_blank">JSON Schema</a>.
<small>* Schätzung basierend auf Tests in einem internen Entwicklungsteam, das Produktionsanwendungen erstellt.</small> <small>* Schätzung basierend auf Tests, die von einem internen Entwicklungsteam durchgeführt wurden, das Produktionsanwendungen erstellt.</small>
## Sponsoren { #sponsors } ## Sponsoren { #sponsors }
<!-- sponsors --> <!-- sponsors -->
{% if sponsors %} ### Keystone-Sponsor
{% for sponsor in sponsors.keystone -%}
<a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}" style="border-radius:15px"></a>
{% endfor -%}
### Gold- und Silber-Sponsoren
{% for sponsor in sponsors.gold -%} {% for sponsor in sponsors.gold -%}
<a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}" style="border-radius:15px"></a> <a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}" style="border-radius:15px"></a>
{% endfor -%} {% endfor -%}
{%- for sponsor in sponsors.silver -%} {%- for sponsor in sponsors.silver -%}
<a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}" style="border-radius:15px"></a> <a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}" style="border-radius:15px"></a>
{% endfor %} {% endfor %}
{% endif %}
<!-- /sponsors --> <!-- /sponsors -->
@ -444,6 +450,58 @@ Für ein vollständigeres Beispiel, mit weiteren Funktionen, siehe das <a href="
* **Cookie-Sessions** * **Cookie-Sessions**
* ... und mehr. * ... und mehr.
### Ihre App deployen (optional) { #deploy-your-app-optional }
Optional können Sie Ihre FastAPI-App in die <a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a> deployen, treten Sie der Warteliste bei, falls noch nicht geschehen. 🚀
Wenn Sie bereits ein **FastAPI Cloud**-Konto haben (wir haben Sie von der Warteliste eingeladen 😉), können Sie Ihre Anwendung mit einem einzigen Befehl deployen.
Stellen Sie vor dem Deployen sicher, dass Sie eingeloggt sind:
<div class="termy">
```console
$ fastapi login
You are logged in to FastAPI Cloud 🚀
```
</div>
Stellen Sie dann Ihre App bereit:
<div class="termy">
```console
$ fastapi deploy
Deploying to FastAPI Cloud...
✅ Deployment successful!
🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev
```
</div>
Das wars! Jetzt können Sie unter dieser URL auf Ihre App zugreifen. ✨
#### Über FastAPI Cloud { #about-fastapi-cloud }
**<a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>** wird vom selben Autor und Team hinter **FastAPI** entwickelt.
Es vereinfacht den Prozess des **Erstellens**, **Deployens** und **Zugreifens** auf eine API mit minimalem Aufwand.
Es bringt die gleiche **Developer-Experience** beim Erstellen von Apps mit FastAPI auch zum **Deployment** in der Cloud. 🎉
FastAPI Cloud ist der Hauptsponsor und Finanzierer der „FastAPI and friends“ Open-Source-Projekte. ✨
#### Bei anderen Cloudanbietern deployen { #deploy-to-other-cloud-providers }
FastAPI ist Open Source und basiert auf Standards. Sie können FastAPI-Apps bei jedem Cloudanbieter Ihrer Wahl deployen.
Folgen Sie den Anleitungen Ihres Cloudanbieters, um FastAPI-Apps dort bereitzustellen. 🤓
## Performanz { #performance } ## Performanz { #performance }
Unabhängige TechEmpower-Benchmarks zeigen **FastAPI**-Anwendungen, die unter Uvicorn laufen, als <a href="https://www.techempower.com/benchmarks/#section=test&runid=7464e520-0dc2-473d-bd34-dbdfd7e85911&hw=ph&test=query&l=zijzen-7" class="external-link" target="_blank">eines der schnellsten verfügbaren Python-Frameworks</a>, nur hinter Starlette und Uvicorn selbst (intern von FastAPI verwendet). (*) Unabhängige TechEmpower-Benchmarks zeigen **FastAPI**-Anwendungen, die unter Uvicorn laufen, als <a href="https://www.techempower.com/benchmarks/#section=test&runid=7464e520-0dc2-473d-bd34-dbdfd7e85911&hw=ph&test=query&l=zijzen-7" class="external-link" target="_blank">eines der schnellsten verfügbaren Python-Frameworks</a>, nur hinter Starlette und Uvicorn selbst (intern von FastAPI verwendet). (*)

View File

@ -25,4 +25,4 @@ GitHub-Repository: <a href="https://github.com/tiangolo/full-stack-fastapi-templ
- ✅ Tests mit [Pytest](https://pytest.org). - ✅ Tests mit [Pytest](https://pytest.org).
- 📞 [Traefik](https://traefik.io) als Reverse-Proxy / Load Balancer. - 📞 [Traefik](https://traefik.io) als Reverse-Proxy / Load Balancer.
- 🚢 Deployment-Anleitungen unter Verwendung von Docker Compose, einschließlich der Einrichtung eines Frontend-Traefik-Proxys zur Handhabung automatischer HTTPS-Zertifikate. - 🚢 Deployment-Anleitungen unter Verwendung von Docker Compose, einschließlich der Einrichtung eines Frontend-Traefik-Proxys zur Handhabung automatischer HTTPS-Zertifikate.
- 🏭 CI (kontinuierliche Integration) und CD (kontinuierliche Bereitstellung) basierend auf GitHub Actions. - 🏭 CI (kontinuierliche Integration) und CD (kontinuierliches Deployment) basierend auf GitHub Actions.

View File

@ -143,6 +143,42 @@ Es gibt dutzende Alternativen, die alle auf OpenAPI basieren. Sie können jede d
Ebenfalls können Sie es verwenden, um automatisch Code für Clients zu generieren, die mit Ihrer API kommunizieren. Zum Beispiel für Frontend-, Mobile- oder IoT-Anwendungen. Ebenfalls können Sie es verwenden, um automatisch Code für Clients zu generieren, die mit Ihrer API kommunizieren. Zum Beispiel für Frontend-, Mobile- oder IoT-Anwendungen.
### Ihre App deployen (optional) { #deploy-your-app-optional }
Sie können optional Ihre FastAPI-App in der <a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a> deployen, treten Sie der Warteliste bei, falls Sie es noch nicht getan haben. 🚀
Wenn Sie bereits ein **FastAPI Cloud**-Konto haben (wir haben Sie von der Warteliste eingeladen 😉), können Sie Ihre Anwendung mit einem Befehl deployen.
Vor dem Deployen, stellen Sie sicher, dass Sie eingeloggt sind:
<div class="termy">
```console
$ fastapi login
You are logged in to FastAPI Cloud 🚀
```
</div>
Dann stellen Sie Ihre App bereit:
<div class="termy">
```console
$ fastapi deploy
Deploying to FastAPI Cloud...
✅ Deployment successful!
🐔 Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev
```
</div>
Das war's! Jetzt können Sie Ihre App unter dieser URL aufrufen. ✨
## Zusammenfassung, Schritt für Schritt { #recap-step-by-step } ## Zusammenfassung, Schritt für Schritt { #recap-step-by-step }
### Schritt 1: `FastAPI` importieren { #step-1-import-fastapi } ### Schritt 1: `FastAPI` importieren { #step-1-import-fastapi }
@ -314,6 +350,26 @@ Sie können auch Pydantic-Modelle zurückgeben (dazu später mehr).
Es gibt viele andere Objekte und Modelle, die automatisch zu JSON konvertiert werden (einschließlich ORMs, usw.). Versuchen Sie, Ihre Lieblingsobjekte zu verwenden. Es ist sehr wahrscheinlich, dass sie bereits unterstützt werden. Es gibt viele andere Objekte und Modelle, die automatisch zu JSON konvertiert werden (einschließlich ORMs, usw.). Versuchen Sie, Ihre Lieblingsobjekte zu verwenden. Es ist sehr wahrscheinlich, dass sie bereits unterstützt werden.
### Schritt 6: Deployen { #step-6-deploy-it }
Stellen Sie Ihre App in der **<a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>** mit einem Befehl bereit: `fastapi deploy`. 🎉
#### Über FastAPI Cloud { #about-fastapi-cloud }
**<a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>** wird vom selben Autor und Team hinter **FastAPI** entwickelt.
Es vereinfacht den Prozess des Erstellens, Deployens und des Zugriffs auf eine API mit minimalem Aufwand.
Es bringt die gleiche **Developer-Experience** beim Erstellen von Apps mit FastAPI auch zum **Deployment** in der Cloud. 🎉
FastAPI Cloud ist der Hauptsponsor und Finanzierer der „FastAPI and friends“ Open-Source-Projekte. ✨
#### Zu anderen Cloudanbietern deployen { #deploy-to-other-cloud-providers }
FastAPI ist Open Source und basiert auf Standards. Sie können FastAPI-Apps bei jedem Cloudanbieter Ihrer Wahl deployen.
Folgen Sie den Anleitungen Ihres Cloudanbieters, um dort FastAPI-Apps bereitzustellen. 🤓
## Zusammenfassung { #recap } ## Zusammenfassung { #recap }
* Importieren Sie `FastAPI`. * Importieren Sie `FastAPI`.
@ -321,3 +377,4 @@ Es gibt viele andere Objekte und Modelle, die automatisch zu JSON konvertiert we
* Schreiben Sie einen **Pfadoperation-Dekorator** unter Verwendung von Dekoratoren wie `@app.get("/")`. * Schreiben Sie einen **Pfadoperation-Dekorator** unter Verwendung von Dekoratoren wie `@app.get("/")`.
* Definieren Sie eine **Pfadoperation-Funktion**, zum Beispiel `def root(): ...`. * Definieren Sie eine **Pfadoperation-Funktion**, zum Beispiel `def root(): ...`.
* Starten Sie den Entwicklungsserver mit dem Befehl `fastapi dev`. * Starten Sie den Entwicklungsserver mit dem Befehl `fastapi dev`.
* Optional: Ihre App mit `fastapi deploy` deployen.

View File

@ -65,7 +65,7 @@ Es gibt ein paar Unterschiede:
* `Field(primary_key=True)` sagt SQLModel, dass die `id` der **Primärschlüssel** in der SQL-Datenbank ist (Sie können mehr über SQL-Primärschlüssel in der SQLModel-Dokumentation erfahren). * `Field(primary_key=True)` sagt SQLModel, dass die `id` der **Primärschlüssel** in der SQL-Datenbank ist (Sie können mehr über SQL-Primärschlüssel in der SQLModel-Dokumentation erfahren).
Durch das Festlegen des Typs als `int | None` wird SQLModel wissen, dass diese Spalte ein `INTEGER` in der SQL-Datenbank sein sollte und dass sie `NULLABLE` sein sollte. **Hinweis:** Wir verwenden für das Primärschlüsselfeld `int | None`, damit wir im Python-Code *ein Objekt ohne `id` erstellen* können (`id=None`), in der Annahme, dass die Datenbank sie *beim Speichern generiert*. SQLModel versteht, dass die Datenbank die `id` bereitstellt, und *definiert die Spalte im Datenbankschema als ein Nicht-Null-`INTEGER`*. Siehe die <a href="https://sqlmodel.tiangolo.com/tutorial/create-db-and-table/#primary-key-id" class="external-link" target="_blank">SQLModel-Dokumentation zu Primärschlüsseln</a> für Details.
* `Field(index=True)` sagt SQLModel, dass es einen **SQL-Index** für diese Spalte erstellen soll, was schnelleres Suchen in der Datenbank ermöglicht, wenn Daten mittels dieser Spalte gefiltert werden. * `Field(index=True)` sagt SQLModel, dass es einen **SQL-Index** für diese Spalte erstellen soll, was schnelleres Suchen in der Datenbank ermöglicht, wenn Daten mittels dieser Spalte gefiltert werden.

View File

@ -242,6 +242,26 @@ $ python -m pip install --upgrade pip
</div> </div>
/// tip | Tipp
Manchmal kann beim Versuch, `pip` zu aktualisieren, der Fehler **`No module named pip`** auftreten.
Wenn das passiert, installieren und aktualisieren Sie `pip` mit dem folgenden Befehl:
<div class="termy">
```console
$ python -m ensurepip --upgrade
---> 100%
```
</div>
Dieser Befehl installiert `pip`, falls es noch nicht installiert ist, und stellt außerdem sicher, dass die installierte Version von `pip` mindestens so aktuell ist wie die in `ensurepip` verfügbare.
///
## `.gitignore` hinzufügen { #add-gitignore } ## `.gitignore` hinzufügen { #add-gitignore }
Wenn Sie **Git** verwenden (was Sie sollten), fügen Sie eine `.gitignore`-Datei hinzu, um alles in Ihrem `.venv` von Git auszuschließen. Wenn Sie **Git** verwenden (was Sie sollten), fügen Sie eine `.gitignore`-Datei hinzu, um alles in Ihrem `.venv` von Git auszuschließen.

View File

@ -255,6 +255,7 @@ Below is a list of English terms and their preferred German translations, separa
* «the default value»: «der Defaultwert» * «the default value»: «der Defaultwert»
* «the default value»: NOT «der Standardwert» * «the default value»: NOT «der Standardwert»
* «the default declaration»: «die Default-Deklaration» * «the default declaration»: «die Default-Deklaration»
* «the deployment»: «das Deployment»
* «the dict»: «das Dict» * «the dict»: «das Dict»
* «the dictionary»: «das Dictionary» * «the dictionary»: «das Dictionary»
* «the enumeration»: «die Enumeration» * «the enumeration»: «die Enumeration»
@ -316,6 +317,7 @@ Below is a list of English terms and their preferred German translations, separa
* «the worker process»: «der Workerprozess» * «the worker process»: «der Workerprozess»
* «the worker process»: NOT «der Arbeiterprozess» * «the worker process»: NOT «der Arbeiterprozess»
* «to commit»: «committen» * «to commit»: «committen»
* «to deploy» (in the cloud): «deployen»
* «to modify»: «ändern» * «to modify»: «ändern»
* «to serve» (an application): «bereitstellen» * «to serve» (an application): «bereitstellen»
* «to serve» (a response): «ausliefern» * «to serve» (a response): «ausliefern»

View File

@ -1,21 +1,21 @@
tiangolo: tiangolo:
login: tiangolo login: tiangolo
count: 794 count: 808
avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=cb5d06e73a9e1998141b1641aa88e443c6717651&v=4 avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=cb5d06e73a9e1998141b1641aa88e443c6717651&v=4
url: https://github.com/tiangolo url: https://github.com/tiangolo
dependabot: dependabot:
login: dependabot login: dependabot
count: 126 count: 130
avatarUrl: https://avatars.githubusercontent.com/in/29110?v=4 avatarUrl: https://avatars.githubusercontent.com/in/29110?v=4
url: https://github.com/apps/dependabot url: https://github.com/apps/dependabot
alejsdev: alejsdev:
login: alejsdev login: alejsdev
count: 52 count: 52
avatarUrl: https://avatars.githubusercontent.com/u/90076947?u=447d12a1b347f466b35378bee4c7104cc9b2c571&v=4 avatarUrl: https://avatars.githubusercontent.com/u/90076947?u=85ceac49fb87138aebe8d663912e359447329090&v=4
url: https://github.com/alejsdev url: https://github.com/alejsdev
pre-commit-ci: pre-commit-ci:
login: pre-commit-ci login: pre-commit-ci
count: 49 count: 50
avatarUrl: https://avatars.githubusercontent.com/in/68672?v=4 avatarUrl: https://avatars.githubusercontent.com/in/68672?v=4
url: https://github.com/apps/pre-commit-ci url: https://github.com/apps/pre-commit-ci
github-actions: github-actions:
@ -28,31 +28,31 @@ Kludex:
count: 25 count: 25
avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=df8a3f06ba8f55ae1967a3e2d5ed882903a4e330&v=4 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=df8a3f06ba8f55ae1967a3e2d5ed882903a4e330&v=4
url: https://github.com/Kludex url: https://github.com/Kludex
YuriiMotov:
login: YuriiMotov
count: 20
avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=b9b13d598dddfab529a52d264df80a900bfe7060&v=4
url: https://github.com/YuriiMotov
dmontagu: dmontagu:
login: dmontagu login: dmontagu
count: 17 count: 17
avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=540f30c937a6450812628b9592a1dfe91bbe148e&v=4 avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=540f30c937a6450812628b9592a1dfe91bbe148e&v=4
url: https://github.com/dmontagu url: https://github.com/dmontagu
YuriiMotov:
login: YuriiMotov
count: 15
avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=b9b13d598dddfab529a52d264df80a900bfe7060&v=4
url: https://github.com/YuriiMotov
nilslindemann: nilslindemann:
login: nilslindemann login: nilslindemann
count: 14 count: 15
avatarUrl: https://avatars.githubusercontent.com/u/6892179?u=1dca6a22195d6cd1ab20737c0e19a4c55d639472&v=4 avatarUrl: https://avatars.githubusercontent.com/u/6892179?u=1dca6a22195d6cd1ab20737c0e19a4c55d639472&v=4
url: https://github.com/nilslindemann url: https://github.com/nilslindemann
svlandeg:
login: svlandeg
count: 14
avatarUrl: https://avatars.githubusercontent.com/u/8796347?u=556c97650c27021911b0b9447ec55e75987b0e8a&v=4
url: https://github.com/svlandeg
euri10: euri10:
login: euri10 login: euri10
count: 13 count: 13
avatarUrl: https://avatars.githubusercontent.com/u/1104190?u=321a2e953e6645a7d09b732786c7a8061e0f8a8b&v=4 avatarUrl: https://avatars.githubusercontent.com/u/1104190?u=321a2e953e6645a7d09b732786c7a8061e0f8a8b&v=4
url: https://github.com/euri10 url: https://github.com/euri10
svlandeg:
login: svlandeg
count: 13
avatarUrl: https://avatars.githubusercontent.com/u/8796347?u=556c97650c27021911b0b9447ec55e75987b0e8a&v=4
url: https://github.com/svlandeg
kantandane: kantandane:
login: kantandane login: kantandane
count: 13 count: 13
@ -103,6 +103,11 @@ waynerv:
count: 5 count: 5
avatarUrl: https://avatars.githubusercontent.com/u/39515546?u=ec35139777597cdbbbddda29bf8b9d4396b429a9&v=4 avatarUrl: https://avatars.githubusercontent.com/u/39515546?u=ec35139777597cdbbbddda29bf8b9d4396b429a9&v=4
url: https://github.com/waynerv url: https://github.com/waynerv
musicinmybrain:
login: musicinmybrain
count: 5
avatarUrl: https://avatars.githubusercontent.com/u/6898909?u=9010312053e7141383b9bdf538036c7f37fbaba0&v=4
url: https://github.com/musicinmybrain
krishnamadhavan: krishnamadhavan:
login: krishnamadhavan login: krishnamadhavan
count: 5 count: 5
@ -133,11 +138,6 @@ iudeen:
count: 4 count: 4
avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=f09cdd745e5bf16138f29b42732dd57c7f02bee1&v=4 avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=f09cdd745e5bf16138f29b42732dd57c7f02bee1&v=4
url: https://github.com/iudeen url: https://github.com/iudeen
musicinmybrain:
login: musicinmybrain
count: 4
avatarUrl: https://avatars.githubusercontent.com/u/6898909?u=9010312053e7141383b9bdf538036c7f37fbaba0&v=4
url: https://github.com/musicinmybrain
philipokiokio: philipokiokio:
login: philipokiokio login: philipokiokio
count: 4 count: 4
@ -483,6 +483,11 @@ nzig:
count: 2 count: 2
avatarUrl: https://avatars.githubusercontent.com/u/7372858?u=e769add36ed73c778cdb136eb10bf96b1e119671&v=4 avatarUrl: https://avatars.githubusercontent.com/u/7372858?u=e769add36ed73c778cdb136eb10bf96b1e119671&v=4
url: https://github.com/nzig url: https://github.com/nzig
kristjanvalur:
login: kristjanvalur
count: 2
avatarUrl: https://avatars.githubusercontent.com/u/6009543?u=1419f20bbfff8f031be8cb470962e7e62de2595e&v=4
url: https://github.com/kristjanvalur
yezz123: yezz123:
login: yezz123 login: yezz123
count: 2 count: 2

View File

@ -23,9 +23,6 @@ sponsors:
- login: railwayapp - login: railwayapp
avatarUrl: https://avatars.githubusercontent.com/u/66716858?v=4 avatarUrl: https://avatars.githubusercontent.com/u/66716858?v=4
url: https://github.com/railwayapp url: https://github.com/railwayapp
- login: scalar
avatarUrl: https://avatars.githubusercontent.com/u/301879?v=4
url: https://github.com/scalar
- - login: dribia - - login: dribia
avatarUrl: https://avatars.githubusercontent.com/u/41189616?v=4 avatarUrl: https://avatars.githubusercontent.com/u/41189616?v=4
url: https://github.com/dribia url: https://github.com/dribia
@ -44,25 +41,25 @@ sponsors:
- login: permitio - login: permitio
avatarUrl: https://avatars.githubusercontent.com/u/71775833?v=4 avatarUrl: https://avatars.githubusercontent.com/u/71775833?v=4
url: https://github.com/permitio url: https://github.com/permitio
- - login: BoostryJP - - login: Ponte-Energy-Partners
avatarUrl: https://avatars.githubusercontent.com/u/57932412?v=4
url: https://github.com/BoostryJP
- login: mercedes-benz
avatarUrl: https://avatars.githubusercontent.com/u/34240465?v=4
url: https://github.com/mercedes-benz
- login: Ponte-Energy-Partners
avatarUrl: https://avatars.githubusercontent.com/u/114745848?v=4 avatarUrl: https://avatars.githubusercontent.com/u/114745848?v=4
url: https://github.com/Ponte-Energy-Partners url: https://github.com/Ponte-Energy-Partners
- login: LambdaTest-Inc - login: LambdaTest-Inc
avatarUrl: https://avatars.githubusercontent.com/u/171592363?u=96606606a45fa170427206199014f2a5a2a4920b&v=4 avatarUrl: https://avatars.githubusercontent.com/u/171592363?u=96606606a45fa170427206199014f2a5a2a4920b&v=4
url: https://github.com/LambdaTest-Inc url: https://github.com/LambdaTest-Inc
- login: BoostryJP
avatarUrl: https://avatars.githubusercontent.com/u/57932412?v=4
url: https://github.com/BoostryJP
- login: requestly - login: requestly
avatarUrl: https://avatars.githubusercontent.com/u/12287519?v=4 avatarUrl: https://avatars.githubusercontent.com/u/12287519?v=4
url: https://github.com/requestly url: https://github.com/requestly
- login: acsone - login: acsone
avatarUrl: https://avatars.githubusercontent.com/u/7601056?v=4 avatarUrl: https://avatars.githubusercontent.com/u/7601056?v=4
url: https://github.com/acsone url: https://github.com/acsone
- - login: Trivie - - login: scalar
avatarUrl: https://avatars.githubusercontent.com/u/301879?v=4
url: https://github.com/scalar
- login: Trivie
avatarUrl: https://avatars.githubusercontent.com/u/8161763?v=4 avatarUrl: https://avatars.githubusercontent.com/u/8161763?v=4
url: https://github.com/Trivie url: https://github.com/Trivie
- - login: takashi-yoneya - - login: takashi-yoneya
@ -71,42 +68,30 @@ sponsors:
- login: Doist - login: Doist
avatarUrl: https://avatars.githubusercontent.com/u/2565372?v=4 avatarUrl: https://avatars.githubusercontent.com/u/2565372?v=4
url: https://github.com/Doist url: https://github.com/Doist
- login: bholagabbar
avatarUrl: https://avatars.githubusercontent.com/u/11693595?v=4
url: https://github.com/bholagabbar
- - login: mainframeindustries - - login: mainframeindustries
avatarUrl: https://avatars.githubusercontent.com/u/55092103?v=4 avatarUrl: https://avatars.githubusercontent.com/u/55092103?v=4
url: https://github.com/mainframeindustries url: https://github.com/mainframeindustries
- - login: alixlahuec - - login: alixlahuec
avatarUrl: https://avatars.githubusercontent.com/u/29543316?u=44357eb2a93bccf30fb9d389b8befe94a3d00985&v=4 avatarUrl: https://avatars.githubusercontent.com/u/29543316?u=44357eb2a93bccf30fb9d389b8befe94a3d00985&v=4
url: https://github.com/alixlahuec url: https://github.com/alixlahuec
- login: Partho
avatarUrl: https://avatars.githubusercontent.com/u/2034301?u=ce195ac36835cca0cdfe6dd6e897bd38873a1524&v=4
url: https://github.com/Partho
- - login: primer-io - - login: primer-io
avatarUrl: https://avatars.githubusercontent.com/u/62146168?v=4 avatarUrl: https://avatars.githubusercontent.com/u/62146168?v=4
url: https://github.com/primer-io url: https://github.com/primer-io
- login: xsalagarcia
avatarUrl: https://avatars.githubusercontent.com/u/66035908?v=4
url: https://github.com/xsalagarcia
- - login: upciti - - login: upciti
avatarUrl: https://avatars.githubusercontent.com/u/43346262?v=4 avatarUrl: https://avatars.githubusercontent.com/u/43346262?v=4
url: https://github.com/upciti url: https://github.com/upciti
- login: GonnaFlyMethod
avatarUrl: https://avatars.githubusercontent.com/u/60840539?u=edf70b373fd4f1a83d3eb7c6802f4b6addb572cf&v=4
url: https://github.com/GonnaFlyMethod
- login: ChargeStorm - login: ChargeStorm
avatarUrl: https://avatars.githubusercontent.com/u/26000165?v=4 avatarUrl: https://avatars.githubusercontent.com/u/26000165?v=4
url: https://github.com/ChargeStorm url: https://github.com/ChargeStorm
- login: DanielYang59
avatarUrl: https://avatars.githubusercontent.com/u/80093591?u=63873f701c7c74aac83c906800a1dddc0bc8c92f&v=4
url: https://github.com/DanielYang59
- login: nilslindemann - login: nilslindemann
avatarUrl: https://avatars.githubusercontent.com/u/6892179?u=1dca6a22195d6cd1ab20737c0e19a4c55d639472&v=4 avatarUrl: https://avatars.githubusercontent.com/u/6892179?u=1dca6a22195d6cd1ab20737c0e19a4c55d639472&v=4
url: https://github.com/nilslindemann url: https://github.com/nilslindemann
- - login: samuelcolvin - - login: samuelcolvin
avatarUrl: https://avatars.githubusercontent.com/u/4039449?u=42eb3b833047c8c4b4f647a031eaef148c16d93f&v=4 avatarUrl: https://avatars.githubusercontent.com/u/4039449?u=42eb3b833047c8c4b4f647a031eaef148c16d93f&v=4
url: https://github.com/samuelcolvin url: https://github.com/samuelcolvin
- login: vincentkoc
avatarUrl: https://avatars.githubusercontent.com/u/25068?u=fbd5b2d51142daa4bdbc21e21953a3b8b8188a4a&v=4
url: https://github.com/vincentkoc
- login: otosky - login: otosky
avatarUrl: https://avatars.githubusercontent.com/u/42260747?u=69d089387c743d89427aa4ad8740cfb34045a9e0&v=4 avatarUrl: https://avatars.githubusercontent.com/u/42260747?u=69d089387c743d89427aa4ad8740cfb34045a9e0&v=4
url: https://github.com/otosky url: https://github.com/otosky
@ -137,9 +122,6 @@ sponsors:
- login: jugeeem - login: jugeeem
avatarUrl: https://avatars.githubusercontent.com/u/116043716?u=ae590d79c38ac79c91b9c5caa6887d061e865a3d&v=4 avatarUrl: https://avatars.githubusercontent.com/u/116043716?u=ae590d79c38ac79c91b9c5caa6887d061e865a3d&v=4
url: https://github.com/jugeeem url: https://github.com/jugeeem
- login: connorpark24
avatarUrl: https://avatars.githubusercontent.com/u/142128990?u=09b84a4beb1f629b77287a837bcf3729785cdd89&v=4
url: https://github.com/connorpark24
- login: patsatsia - login: patsatsia
avatarUrl: https://avatars.githubusercontent.com/u/61111267?u=3271b85f7a37b479c8d0ae0a235182e83c166edf&v=4 avatarUrl: https://avatars.githubusercontent.com/u/61111267?u=3271b85f7a37b479c8d0ae0a235182e83c166edf&v=4
url: https://github.com/patsatsia url: https://github.com/patsatsia
@ -155,9 +137,9 @@ sponsors:
- login: kaoru0310 - login: kaoru0310
avatarUrl: https://avatars.githubusercontent.com/u/80977929?u=1b61d10142b490e56af932ddf08a390fae8ee94f&v=4 avatarUrl: https://avatars.githubusercontent.com/u/80977929?u=1b61d10142b490e56af932ddf08a390fae8ee94f&v=4
url: https://github.com/kaoru0310 url: https://github.com/kaoru0310
- login: DelfinaCare - login: jstanden
avatarUrl: https://avatars.githubusercontent.com/u/83734439?v=4 avatarUrl: https://avatars.githubusercontent.com/u/63288?u=c3658d57d2862c607a0e19c2101c3c51876e36ad&v=4
url: https://github.com/DelfinaCare url: https://github.com/jstanden
- login: knallgelb - login: knallgelb
avatarUrl: https://avatars.githubusercontent.com/u/2358812?u=c48cb6362b309d74cbf144bd6ad3aed3eb443e82&v=4 avatarUrl: https://avatars.githubusercontent.com/u/2358812?u=c48cb6362b309d74cbf144bd6ad3aed3eb443e82&v=4
url: https://github.com/knallgelb url: https://github.com/knallgelb
@ -191,9 +173,6 @@ sponsors:
- login: oliverxchen - login: oliverxchen
avatarUrl: https://avatars.githubusercontent.com/u/4471774?u=534191f25e32eeaadda22dfab4b0a428733d5489&v=4 avatarUrl: https://avatars.githubusercontent.com/u/4471774?u=534191f25e32eeaadda22dfab4b0a428733d5489&v=4
url: https://github.com/oliverxchen url: https://github.com/oliverxchen
- login: jstanden
avatarUrl: https://avatars.githubusercontent.com/u/63288?u=c3658d57d2862c607a0e19c2101c3c51876e36ad&v=4
url: https://github.com/jstanden
- login: paulcwatts - login: paulcwatts
avatarUrl: https://avatars.githubusercontent.com/u/150269?u=1819e145d573b44f0ad74b87206d21cd60331d4e&v=4 avatarUrl: https://avatars.githubusercontent.com/u/150269?u=1819e145d573b44f0ad74b87206d21cd60331d4e&v=4
url: https://github.com/paulcwatts url: https://github.com/paulcwatts
@ -233,9 +212,6 @@ sponsors:
- login: mjohnsey - login: mjohnsey
avatarUrl: https://avatars.githubusercontent.com/u/16784016?u=38fad2e6b411244560b3af99c5f5a4751bc81865&v=4 avatarUrl: https://avatars.githubusercontent.com/u/16784016?u=38fad2e6b411244560b3af99c5f5a4751bc81865&v=4
url: https://github.com/mjohnsey url: https://github.com/mjohnsey
- login: enguy-hub
avatarUrl: https://avatars.githubusercontent.com/u/16822912?u=2c45f9e7f427b2f2f3b023d7fdb0d44764c92ae8&v=4
url: https://github.com/enguy-hub
- login: ashi-agrawal - login: ashi-agrawal
avatarUrl: https://avatars.githubusercontent.com/u/17105294?u=99c7a854035e5398d8e7b674f2d42baae6c957f8&v=4 avatarUrl: https://avatars.githubusercontent.com/u/17105294?u=99c7a854035e5398d8e7b674f2d42baae6c957f8&v=4
url: https://github.com/ashi-agrawal url: https://github.com/ashi-agrawal
@ -260,10 +236,7 @@ sponsors:
- - login: manoelpqueiroz - - login: manoelpqueiroz
avatarUrl: https://avatars.githubusercontent.com/u/23669137?u=b12e84b28a84369ab5b30bd5a79e5788df5a0756&v=4 avatarUrl: https://avatars.githubusercontent.com/u/23669137?u=b12e84b28a84369ab5b30bd5a79e5788df5a0756&v=4
url: https://github.com/manoelpqueiroz url: https://github.com/manoelpqueiroz
- - login: ceb10n - - login: pawamoy
avatarUrl: https://avatars.githubusercontent.com/u/235213?u=edcce471814a1eba9f0cdaa4cd0de18921a940a6&v=4
url: https://github.com/ceb10n
- login: pawamoy
avatarUrl: https://avatars.githubusercontent.com/u/3999221?u=b030e4c89df2f3a36bc4710b925bdeb6745c9856&v=4 avatarUrl: https://avatars.githubusercontent.com/u/3999221?u=b030e4c89df2f3a36bc4710b925bdeb6745c9856&v=4
url: https://github.com/pawamoy url: https://github.com/pawamoy
- login: siavashyj - login: siavashyj
@ -281,9 +254,9 @@ sponsors:
- login: hgalytoby - login: hgalytoby
avatarUrl: https://avatars.githubusercontent.com/u/50397689?u=6cc9028f3db63f8f60ad21c17b1ce4b88c4e2e60&v=4 avatarUrl: https://avatars.githubusercontent.com/u/50397689?u=6cc9028f3db63f8f60ad21c17b1ce4b88c4e2e60&v=4
url: https://github.com/hgalytoby url: https://github.com/hgalytoby
- login: johnl28 - login: nisutec
avatarUrl: https://avatars.githubusercontent.com/u/54412955?u=47dd06082d1c39caa90c752eb55566e4f3813957&v=4 avatarUrl: https://avatars.githubusercontent.com/u/25281462?u=e562484c451fdfc59053163f64405f8eb262b8b0&v=4
url: https://github.com/johnl28 url: https://github.com/nisutec
- login: hoenie-ams - login: hoenie-ams
avatarUrl: https://avatars.githubusercontent.com/u/25708487?u=cda07434f0509ac728d9edf5e681117c0f6b818b&v=4 avatarUrl: https://avatars.githubusercontent.com/u/25708487?u=cda07434f0509ac728d9edf5e681117c0f6b818b&v=4
url: https://github.com/hoenie-ams url: https://github.com/hoenie-ams
@ -299,21 +272,24 @@ sponsors:
- login: petercool - login: petercool
avatarUrl: https://avatars.githubusercontent.com/u/37613029?u=75aa8c6729e6e8f85a300561c4dbeef9d65c8797&v=4 avatarUrl: https://avatars.githubusercontent.com/u/37613029?u=75aa8c6729e6e8f85a300561c4dbeef9d65c8797&v=4
url: https://github.com/petercool url: https://github.com/petercool
- login: johnl28
avatarUrl: https://avatars.githubusercontent.com/u/54412955?u=47dd06082d1c39caa90c752eb55566e4f3813957&v=4
url: https://github.com/johnl28
- login: PunRabbit - login: PunRabbit
avatarUrl: https://avatars.githubusercontent.com/u/70463212?u=1a835cfbc99295a60c8282f6aa6199d1b42241a5&v=4 avatarUrl: https://avatars.githubusercontent.com/u/70463212?u=1a835cfbc99295a60c8282f6aa6199d1b42241a5&v=4
url: https://github.com/PunRabbit url: https://github.com/PunRabbit
- login: PelicanQ - login: PelicanQ
avatarUrl: https://avatars.githubusercontent.com/u/77930606?v=4 avatarUrl: https://avatars.githubusercontent.com/u/77930606?v=4
url: https://github.com/PelicanQ url: https://github.com/PelicanQ
- login: WillHogan
avatarUrl: https://avatars.githubusercontent.com/u/1661551?u=8a80356e3e7d5a417157aba7ea565dabc8678327&v=4
url: https://github.com/WillHogan
- login: my3 - login: my3
avatarUrl: https://avatars.githubusercontent.com/u/1825270?v=4 avatarUrl: https://avatars.githubusercontent.com/u/1825270?v=4
url: https://github.com/my3 url: https://github.com/my3
- login: danielunderwood - login: danielunderwood
avatarUrl: https://avatars.githubusercontent.com/u/4472301?v=4 avatarUrl: https://avatars.githubusercontent.com/u/4472301?v=4
url: https://github.com/danielunderwood url: https://github.com/danielunderwood
- login: rangulvers
avatarUrl: https://avatars.githubusercontent.com/u/5235430?u=e254d4af4ace5a05fa58372ae677c7d26f0d5a53&v=4
url: https://github.com/rangulvers
- login: ddanier - login: ddanier
avatarUrl: https://avatars.githubusercontent.com/u/113563?u=ed1dc79de72f93bd78581f88ebc6952b62f472da&v=4 avatarUrl: https://avatars.githubusercontent.com/u/113563?u=ed1dc79de72f93bd78581f88ebc6952b62f472da&v=4
url: https://github.com/ddanier url: https://github.com/ddanier
@ -323,15 +299,21 @@ sponsors:
- login: slafs - login: slafs
avatarUrl: https://avatars.githubusercontent.com/u/210173?v=4 avatarUrl: https://avatars.githubusercontent.com/u/210173?v=4
url: https://github.com/slafs url: https://github.com/slafs
- login: ceb10n
avatarUrl: https://avatars.githubusercontent.com/u/235213?u=edcce471814a1eba9f0cdaa4cd0de18921a940a6&v=4
url: https://github.com/ceb10n
- login: tochikuji - login: tochikuji
avatarUrl: https://avatars.githubusercontent.com/u/851759?v=4 avatarUrl: https://avatars.githubusercontent.com/u/851759?v=4
url: https://github.com/tochikuji url: https://github.com/tochikuji
- login: miguelgr - login: miguelgr
avatarUrl: https://avatars.githubusercontent.com/u/1484589?u=54556072b8136efa12ae3b6902032ea2a39ace4b&v=4 avatarUrl: https://avatars.githubusercontent.com/u/1484589?u=54556072b8136efa12ae3b6902032ea2a39ace4b&v=4
url: https://github.com/miguelgr url: https://github.com/miguelgr
- login: WillHogan - login: xncbf
avatarUrl: https://avatars.githubusercontent.com/u/1661551?u=8a80356e3e7d5a417157aba7ea565dabc8678327&v=4 avatarUrl: https://avatars.githubusercontent.com/u/9462045?u=a80a7bb349555b277645632ed66639ff43400614&v=4
url: https://github.com/WillHogan url: https://github.com/xncbf
- login: DMantis
avatarUrl: https://avatars.githubusercontent.com/u/9536869?u=652dd0d49717803c0cbcbf44f7740e53cf2d4892&v=4
url: https://github.com/DMantis
- login: hard-coders - login: hard-coders
avatarUrl: https://avatars.githubusercontent.com/u/9651103?u=95db33927bbff1ed1c07efddeb97ac2ff33068ed&v=4 avatarUrl: https://avatars.githubusercontent.com/u/9651103?u=95db33927bbff1ed1c07efddeb97ac2ff33068ed&v=4
url: https://github.com/hard-coders url: https://github.com/hard-coders
@ -347,9 +329,9 @@ sponsors:
- login: joshuatz - login: joshuatz
avatarUrl: https://avatars.githubusercontent.com/u/17817563?u=f1bf05b690d1fc164218f0b420cdd3acb7913e21&v=4 avatarUrl: https://avatars.githubusercontent.com/u/17817563?u=f1bf05b690d1fc164218f0b420cdd3acb7913e21&v=4
url: https://github.com/joshuatz url: https://github.com/joshuatz
- login: nisutec - login: rangulvers
avatarUrl: https://avatars.githubusercontent.com/u/25281462?u=e562484c451fdfc59053163f64405f8eb262b8b0&v=4 avatarUrl: https://avatars.githubusercontent.com/u/5235430?u=e254d4af4ace5a05fa58372ae677c7d26f0d5a53&v=4
url: https://github.com/nisutec url: https://github.com/rangulvers
- login: sdevkota - login: sdevkota
avatarUrl: https://avatars.githubusercontent.com/u/5250987?u=4ed9a120c89805a8aefda1cbdc0cf6512e64d1b4&v=4 avatarUrl: https://avatars.githubusercontent.com/u/5250987?u=4ed9a120c89805a8aefda1cbdc0cf6512e64d1b4&v=4
url: https://github.com/sdevkota url: https://github.com/sdevkota
@ -368,19 +350,7 @@ sponsors:
- login: moonape1226 - login: moonape1226
avatarUrl: https://avatars.githubusercontent.com/u/8532038?u=d9f8b855a429fff9397c3833c2ff83849ebf989d&v=4 avatarUrl: https://avatars.githubusercontent.com/u/8532038?u=d9f8b855a429fff9397c3833c2ff83849ebf989d&v=4
url: https://github.com/moonape1226 url: https://github.com/moonape1226
- login: xncbf - - login: andrecorumba
avatarUrl: https://avatars.githubusercontent.com/u/9462045?u=a80a7bb349555b277645632ed66639ff43400614&v=4
url: https://github.com/xncbf
- login: DMantis
avatarUrl: https://avatars.githubusercontent.com/u/9536869?u=652dd0d49717803c0cbcbf44f7740e53cf2d4892&v=4
url: https://github.com/DMantis
- - login: morzan1001
avatarUrl: https://avatars.githubusercontent.com/u/47593005?u=c30ab7230f82a12a9b938dcb54f84a996931409a&v=4
url: https://github.com/morzan1001
- login: larsyngvelundin
avatarUrl: https://avatars.githubusercontent.com/u/34173819?u=74958599695bf83ac9f1addd935a51548a10c6b0&v=4
url: https://github.com/larsyngvelundin
- login: andrecorumba
avatarUrl: https://avatars.githubusercontent.com/u/37807517?u=9b9be3b41da9bda60957da9ef37b50dbf65baa61&v=4 avatarUrl: https://avatars.githubusercontent.com/u/37807517?u=9b9be3b41da9bda60957da9ef37b50dbf65baa61&v=4
url: https://github.com/andrecorumba url: https://github.com/andrecorumba
- login: KOZ39 - login: KOZ39
@ -389,21 +359,30 @@ sponsors:
- login: rwxd - login: rwxd
avatarUrl: https://avatars.githubusercontent.com/u/40308458?u=cd04a39e3655923be4f25c2ba8a5a07b3da3230a&v=4 avatarUrl: https://avatars.githubusercontent.com/u/40308458?u=cd04a39e3655923be4f25c2ba8a5a07b3da3230a&v=4
url: https://github.com/rwxd url: https://github.com/rwxd
- login: morzan1001
avatarUrl: https://avatars.githubusercontent.com/u/47593005?u=c30ab7230f82a12a9b938dcb54f84a996931409a&v=4
url: https://github.com/morzan1001
- login: Olegt0rr
avatarUrl: https://avatars.githubusercontent.com/u/25399456?u=3e87b5239a2f4600975ba13be73054f8567c6060&v=4
url: https://github.com/Olegt0rr
- login: dinoz0rg
avatarUrl: https://avatars.githubusercontent.com/u/32940067?u=739cda1eb123a2dd5e1db45c361396f239e23f8b&v=4
url: https://github.com/dinoz0rg
- login: larsyngvelundin
avatarUrl: https://avatars.githubusercontent.com/u/34173819?u=74958599695bf83ac9f1addd935a51548a10c6b0&v=4
url: https://github.com/larsyngvelundin
- login: hippoley - login: hippoley
avatarUrl: https://avatars.githubusercontent.com/u/135493401?u=1164ef48a645a7c12664fabc1638fbb7e1c459b0&v=4 avatarUrl: https://avatars.githubusercontent.com/u/135493401?u=1164ef48a645a7c12664fabc1638fbb7e1c459b0&v=4
url: https://github.com/hippoley url: https://github.com/hippoley
- login: 4anklee
avatarUrl: https://avatars.githubusercontent.com/u/144109238?u=a79c0d581b2a3d8f3897e7ef4c012640a6c1eb3a&v=4
url: https://github.com/4anklee
- login: CoderDeltaLAN - login: CoderDeltaLAN
avatarUrl: https://avatars.githubusercontent.com/u/152043745?u=4ff541efffb7d134e60c5fcf2dd1e343f90bb782&v=4 avatarUrl: https://avatars.githubusercontent.com/u/152043745?u=4ff541efffb7d134e60c5fcf2dd1e343f90bb782&v=4
url: https://github.com/CoderDeltaLAN url: https://github.com/CoderDeltaLAN
- login: chris1ding1
avatarUrl: https://avatars.githubusercontent.com/u/194386334?u=5500604b50e35ed8a5aeb82ce34aa5d3ee3f88c7&v=4
url: https://github.com/chris1ding1
- login: onestn - login: onestn
avatarUrl: https://avatars.githubusercontent.com/u/62360849?u=746dd21c34e7e06eefb11b03e8bb01aaae3c2a4f&v=4 avatarUrl: https://avatars.githubusercontent.com/u/62360849?u=746dd21c34e7e06eefb11b03e8bb01aaae3c2a4f&v=4
url: https://github.com/onestn url: https://github.com/onestn
- login: Rubinskiy
avatarUrl: https://avatars.githubusercontent.com/u/62457878?u=f2e35ed3d196a99cfadb5a29a91950342af07e34&v=4
url: https://github.com/Rubinskiy
- login: nayasinghania - login: nayasinghania
avatarUrl: https://avatars.githubusercontent.com/u/74111380?u=752e99a5e139389fdc0a0677122adc08438eb076&v=4 avatarUrl: https://avatars.githubusercontent.com/u/74111380?u=752e99a5e139389fdc0a0677122adc08438eb076&v=4
url: https://github.com/nayasinghania url: https://github.com/nayasinghania
@ -413,9 +392,6 @@ sponsors:
- login: andreagrandi - login: andreagrandi
avatarUrl: https://avatars.githubusercontent.com/u/636391?u=13d90cb8ec313593a5b71fbd4e33b78d6da736f5&v=4 avatarUrl: https://avatars.githubusercontent.com/u/636391?u=13d90cb8ec313593a5b71fbd4e33b78d6da736f5&v=4
url: https://github.com/andreagrandi url: https://github.com/andreagrandi
- login: Olegt0rr
avatarUrl: https://avatars.githubusercontent.com/u/25399456?u=3e87b5239a2f4600975ba13be73054f8567c6060&v=4
url: https://github.com/Olegt0rr
- login: msserpa - login: msserpa
avatarUrl: https://avatars.githubusercontent.com/u/6334934?u=82c4489eb1559d88d2990d60001901b14f722bbb&v=4 avatarUrl: https://avatars.githubusercontent.com/u/6334934?u=82c4489eb1559d88d2990d60001901b14f722bbb&v=4
url: https://github.com/msserpa url: https://github.com/msserpa

View File

@ -33,6 +33,9 @@ gold:
- url: https://serpapi.com/?utm_source=fastapi_website - url: https://serpapi.com/?utm_source=fastapi_website
title: "SerpApi: Web Search API" title: "SerpApi: Web Search API"
img: https://fastapi.tiangolo.com/img/sponsors/serpapi.png img: https://fastapi.tiangolo.com/img/sponsors/serpapi.png
- url: https://www.greptile.com/?utm_source=fastapi&utm_medium=sponsorship&utm_campaign=fastapi_sponsor_page
title: "Greptile: The AI Code Reviewer"
img: https://fastapi.tiangolo.com/img/sponsors/greptile.png
silver: silver:
- url: https://databento.com/?utm_source=fastapi&utm_medium=sponsor&utm_content=display - url: https://databento.com/?utm_source=fastapi&utm_medium=sponsor&utm_content=display
title: Pay as you go for market data title: Pay as you go for market data

View File

@ -47,3 +47,4 @@ logins:
- railwayapp - railwayapp
- subtotal - subtotal
- requestly - requestly
- greptileai

View File

@ -1,368 +1,388 @@
- name: full-stack-fastapi-template - name: full-stack-fastapi-template
html_url: https://github.com/fastapi/full-stack-fastapi-template html_url: https://github.com/fastapi/full-stack-fastapi-template
stars: 38779 stars: 39475
owner_login: fastapi owner_login: fastapi
owner_html_url: https://github.com/fastapi owner_html_url: https://github.com/fastapi
- name: Hello-Python - name: Hello-Python
html_url: https://github.com/mouredev/Hello-Python html_url: https://github.com/mouredev/Hello-Python
stars: 32726 stars: 33090
owner_login: mouredev owner_login: mouredev
owner_html_url: https://github.com/mouredev owner_html_url: https://github.com/mouredev
- name: serve - name: serve
html_url: https://github.com/jina-ai/serve html_url: https://github.com/jina-ai/serve
stars: 21779 stars: 21798
owner_login: jina-ai owner_login: jina-ai
owner_html_url: https://github.com/jina-ai owner_html_url: https://github.com/jina-ai
- name: HivisionIDPhotos - name: HivisionIDPhotos
html_url: https://github.com/Zeyi-Lin/HivisionIDPhotos html_url: https://github.com/Zeyi-Lin/HivisionIDPhotos
stars: 20028 stars: 20258
owner_login: Zeyi-Lin owner_login: Zeyi-Lin
owner_html_url: https://github.com/Zeyi-Lin owner_html_url: https://github.com/Zeyi-Lin
- name: sqlmodel - name: sqlmodel
html_url: https://github.com/fastapi/sqlmodel html_url: https://github.com/fastapi/sqlmodel
stars: 17038 stars: 17212
owner_login: fastapi owner_login: fastapi
owner_html_url: https://github.com/fastapi owner_html_url: https://github.com/fastapi
- name: Douyin_TikTok_Download_API - name: Douyin_TikTok_Download_API
html_url: https://github.com/Evil0ctal/Douyin_TikTok_Download_API html_url: https://github.com/Evil0ctal/Douyin_TikTok_Download_API
stars: 14786 stars: 15145
owner_login: Evil0ctal owner_login: Evil0ctal
owner_html_url: https://github.com/Evil0ctal owner_html_url: https://github.com/Evil0ctal
- name: fastapi-best-practices - name: fastapi-best-practices
html_url: https://github.com/zhanymkanov/fastapi-best-practices html_url: https://github.com/zhanymkanov/fastapi-best-practices
stars: 13968 stars: 14644
owner_login: zhanymkanov owner_login: zhanymkanov
owner_html_url: https://github.com/zhanymkanov owner_html_url: https://github.com/zhanymkanov
- name: machine-learning-zoomcamp - name: machine-learning-zoomcamp
html_url: https://github.com/DataTalksClub/machine-learning-zoomcamp html_url: https://github.com/DataTalksClub/machine-learning-zoomcamp
stars: 12171 stars: 12320
owner_login: DataTalksClub owner_login: DataTalksClub
owner_html_url: https://github.com/DataTalksClub owner_html_url: https://github.com/DataTalksClub
- name: fastapi_mcp - name: fastapi_mcp
html_url: https://github.com/tadata-org/fastapi_mcp html_url: https://github.com/tadata-org/fastapi_mcp
stars: 10976 stars: 11174
owner_login: tadata-org owner_login: tadata-org
owner_html_url: https://github.com/tadata-org owner_html_url: https://github.com/tadata-org
- name: awesome-fastapi
html_url: https://github.com/mjhea0/awesome-fastapi
stars: 10618
owner_login: mjhea0
owner_html_url: https://github.com/mjhea0
- name: SurfSense - name: SurfSense
html_url: https://github.com/MODSetter/SurfSense html_url: https://github.com/MODSetter/SurfSense
stars: 10243 stars: 10858
owner_login: MODSetter owner_login: MODSetter
owner_html_url: https://github.com/MODSetter owner_html_url: https://github.com/MODSetter
- name: awesome-fastapi
html_url: https://github.com/mjhea0/awesome-fastapi
stars: 10758
owner_login: mjhea0
owner_html_url: https://github.com/mjhea0
- name: XHS-Downloader - name: XHS-Downloader
html_url: https://github.com/JoeanAmier/XHS-Downloader html_url: https://github.com/JoeanAmier/XHS-Downloader
stars: 9062 stars: 9313
owner_login: JoeanAmier owner_login: JoeanAmier
owner_html_url: https://github.com/JoeanAmier owner_html_url: https://github.com/JoeanAmier
- name: FastUI - name: FastUI
html_url: https://github.com/pydantic/FastUI html_url: https://github.com/pydantic/FastUI
stars: 8892 stars: 8915
owner_login: pydantic owner_login: pydantic
owner_html_url: https://github.com/pydantic owner_html_url: https://github.com/pydantic
- name: polar - name: polar
html_url: https://github.com/polarsource/polar html_url: https://github.com/polarsource/polar
stars: 8084 stars: 8339
owner_login: polarsource owner_login: polarsource
owner_html_url: https://github.com/polarsource owner_html_url: https://github.com/polarsource
- name: FileCodeBox - name: FileCodeBox
html_url: https://github.com/vastsa/FileCodeBox html_url: https://github.com/vastsa/FileCodeBox
stars: 7494 stars: 7721
owner_login: vastsa owner_login: vastsa
owner_html_url: https://github.com/vastsa owner_html_url: https://github.com/vastsa
- name: nonebot2 - name: nonebot2
html_url: https://github.com/nonebot/nonebot2 html_url: https://github.com/nonebot/nonebot2
stars: 7128 stars: 7170
owner_login: nonebot owner_login: nonebot
owner_html_url: https://github.com/nonebot owner_html_url: https://github.com/nonebot
- name: hatchet - name: hatchet
html_url: https://github.com/hatchet-dev/hatchet html_url: https://github.com/hatchet-dev/hatchet
stars: 6155 stars: 6253
owner_login: hatchet-dev owner_login: hatchet-dev
owner_html_url: https://github.com/hatchet-dev owner_html_url: https://github.com/hatchet-dev
- name: serge
html_url: https://github.com/serge-chat/serge
stars: 5754
owner_login: serge-chat
owner_html_url: https://github.com/serge-chat
- name: fastapi-users - name: fastapi-users
html_url: https://github.com/fastapi-users/fastapi-users html_url: https://github.com/fastapi-users/fastapi-users
stars: 5683 stars: 5849
owner_login: fastapi-users owner_login: fastapi-users
owner_html_url: https://github.com/fastapi-users owner_html_url: https://github.com/fastapi-users
- name: serge
html_url: https://github.com/serge-chat/serge
stars: 5756
owner_login: serge-chat
owner_html_url: https://github.com/serge-chat
- name: strawberry - name: strawberry
html_url: https://github.com/strawberry-graphql/strawberry html_url: https://github.com/strawberry-graphql/strawberry
stars: 4452 stars: 4569
owner_login: strawberry-graphql owner_login: strawberry-graphql
owner_html_url: https://github.com/strawberry-graphql owner_html_url: https://github.com/strawberry-graphql
- name: chatgpt-web-share - name: chatgpt-web-share
html_url: https://github.com/chatpire/chatgpt-web-share html_url: https://github.com/chatpire/chatgpt-web-share
stars: 4296 stars: 4294
owner_login: chatpire owner_login: chatpire
owner_html_url: https://github.com/chatpire owner_html_url: https://github.com/chatpire
- name: poem - name: poem
html_url: https://github.com/poem-web/poem html_url: https://github.com/poem-web/poem
stars: 4235 stars: 4276
owner_login: poem-web owner_login: poem-web
owner_html_url: https://github.com/poem-web owner_html_url: https://github.com/poem-web
- name: dynaconf - name: dynaconf
html_url: https://github.com/dynaconf/dynaconf html_url: https://github.com/dynaconf/dynaconf
stars: 4174 stars: 4202
owner_login: dynaconf owner_login: dynaconf
owner_html_url: https://github.com/dynaconf owner_html_url: https://github.com/dynaconf
- name: atrilabs-engine - name: atrilabs-engine
html_url: https://github.com/Atri-Labs/atrilabs-engine html_url: https://github.com/Atri-Labs/atrilabs-engine
stars: 4094 stars: 4093
owner_login: Atri-Labs owner_login: Atri-Labs
owner_html_url: https://github.com/Atri-Labs owner_html_url: https://github.com/Atri-Labs
- name: Kokoro-FastAPI - name: Kokoro-FastAPI
html_url: https://github.com/remsky/Kokoro-FastAPI html_url: https://github.com/remsky/Kokoro-FastAPI
stars: 3875 stars: 4019
owner_login: remsky owner_login: remsky
owner_html_url: https://github.com/remsky owner_html_url: https://github.com/remsky
- name: logfire - name: logfire
html_url: https://github.com/pydantic/logfire html_url: https://github.com/pydantic/logfire
stars: 3717 stars: 3805
owner_login: pydantic owner_login: pydantic
owner_html_url: https://github.com/pydantic owner_html_url: https://github.com/pydantic
- name: LitServe - name: LitServe
html_url: https://github.com/Lightning-AI/LitServe html_url: https://github.com/Lightning-AI/LitServe
stars: 3615 stars: 3719
owner_login: Lightning-AI owner_login: Lightning-AI
owner_html_url: https://github.com/Lightning-AI owner_html_url: https://github.com/Lightning-AI
- name: fastapi-admin
html_url: https://github.com/fastapi-admin/fastapi-admin
stars: 3632
owner_login: fastapi-admin
owner_html_url: https://github.com/fastapi-admin
- name: datamodel-code-generator - name: datamodel-code-generator
html_url: https://github.com/koxudaxi/datamodel-code-generator html_url: https://github.com/koxudaxi/datamodel-code-generator
stars: 3554 stars: 3609
owner_login: koxudaxi owner_login: koxudaxi
owner_html_url: https://github.com/koxudaxi owner_html_url: https://github.com/koxudaxi
- name: huma - name: huma
html_url: https://github.com/danielgtaylor/huma html_url: https://github.com/danielgtaylor/huma
stars: 3521 stars: 3603
owner_login: danielgtaylor owner_login: danielgtaylor
owner_html_url: https://github.com/danielgtaylor owner_html_url: https://github.com/danielgtaylor
- name: fastapi-admin
html_url: https://github.com/fastapi-admin/fastapi-admin
stars: 3497
owner_login: fastapi-admin
owner_html_url: https://github.com/fastapi-admin
- name: farfalle - name: farfalle
html_url: https://github.com/rashadphz/farfalle html_url: https://github.com/rashadphz/farfalle
stars: 3476 stars: 3490
owner_login: rashadphz owner_login: rashadphz
owner_html_url: https://github.com/rashadphz owner_html_url: https://github.com/rashadphz
- name: tracecat - name: tracecat
html_url: https://github.com/TracecatHQ/tracecat html_url: https://github.com/TracecatHQ/tracecat
stars: 3310 stars: 3379
owner_login: TracecatHQ owner_login: TracecatHQ
owner_html_url: https://github.com/TracecatHQ owner_html_url: https://github.com/TracecatHQ
- name: opyrator - name: opyrator
html_url: https://github.com/ml-tooling/opyrator html_url: https://github.com/ml-tooling/opyrator
stars: 3134 stars: 3135
owner_login: ml-tooling owner_login: ml-tooling
owner_html_url: https://github.com/ml-tooling owner_html_url: https://github.com/ml-tooling
- name: docarray - name: docarray
html_url: https://github.com/docarray/docarray html_url: https://github.com/docarray/docarray
stars: 3108 stars: 3114
owner_login: docarray owner_login: docarray
owner_html_url: https://github.com/docarray owner_html_url: https://github.com/docarray
- name: devpush
html_url: https://github.com/hunvreus/devpush
stars: 3097
owner_login: hunvreus
owner_html_url: https://github.com/hunvreus
- name: fastapi-realworld-example-app - name: fastapi-realworld-example-app
html_url: https://github.com/nsidnev/fastapi-realworld-example-app html_url: https://github.com/nsidnev/fastapi-realworld-example-app
stars: 2945 stars: 3050
owner_login: nsidnev owner_login: nsidnev
owner_html_url: https://github.com/nsidnev owner_html_url: https://github.com/nsidnev
- name: uvicorn-gunicorn-fastapi-docker - name: uvicorn-gunicorn-fastapi-docker
html_url: https://github.com/tiangolo/uvicorn-gunicorn-fastapi-docker html_url: https://github.com/tiangolo/uvicorn-gunicorn-fastapi-docker
stars: 2809 stars: 2911
owner_login: tiangolo owner_login: tiangolo
owner_html_url: https://github.com/tiangolo owner_html_url: https://github.com/tiangolo
- name: devpush
html_url: https://github.com/hunvreus/devpush
stars: 2784
owner_login: hunvreus
owner_html_url: https://github.com/hunvreus
- name: mcp-context-forge - name: mcp-context-forge
html_url: https://github.com/IBM/mcp-context-forge html_url: https://github.com/IBM/mcp-context-forge
stars: 2763 stars: 2899
owner_login: IBM owner_login: IBM
owner_html_url: https://github.com/IBM owner_html_url: https://github.com/IBM
- name: best-of-web-python - name: best-of-web-python
html_url: https://github.com/ml-tooling/best-of-web-python html_url: https://github.com/ml-tooling/best-of-web-python
stars: 2630 stars: 2648
owner_login: ml-tooling owner_login: ml-tooling
owner_html_url: https://github.com/ml-tooling owner_html_url: https://github.com/ml-tooling
- name: fastapi-react
html_url: https://github.com/Buuntu/fastapi-react
stars: 2464
owner_login: Buuntu
owner_html_url: https://github.com/Buuntu
- name: FastAPI-template - name: FastAPI-template
html_url: https://github.com/s3rius/FastAPI-template html_url: https://github.com/s3rius/FastAPI-template
stars: 2453 stars: 2637
owner_login: s3rius owner_login: s3rius
owner_html_url: https://github.com/s3rius owner_html_url: https://github.com/s3rius
- name: RasaGPT - name: YC-Killer
html_url: https://github.com/paulpierre/RasaGPT html_url: https://github.com/sahibzada-allahyar/YC-Killer
stars: 2444 stars: 2599
owner_login: paulpierre owner_login: sahibzada-allahyar
owner_html_url: https://github.com/paulpierre owner_html_url: https://github.com/sahibzada-allahyar
- name: sqladmin - name: fastapi-react
html_url: https://github.com/aminalaee/sqladmin html_url: https://github.com/Buuntu/fastapi-react
stars: 2423 stars: 2569
owner_login: aminalaee owner_login: Buuntu
owner_html_url: https://github.com/aminalaee owner_html_url: https://github.com/Buuntu
- name: nextpy
html_url: https://github.com/dot-agent/nextpy
stars: 2325
owner_login: dot-agent
owner_html_url: https://github.com/dot-agent
- name: supabase-py
html_url: https://github.com/supabase/supabase-py
stars: 2292
owner_login: supabase
owner_html_url: https://github.com/supabase
- name: 30-Days-of-Python
html_url: https://github.com/codingforentrepreneurs/30-Days-of-Python
stars: 2214
owner_login: codingforentrepreneurs
owner_html_url: https://github.com/codingforentrepreneurs
- name: Yuxi-Know - name: Yuxi-Know
html_url: https://github.com/xerrors/Yuxi-Know html_url: https://github.com/xerrors/Yuxi-Know
stars: 2212 stars: 2563
owner_login: xerrors owner_login: xerrors
owner_html_url: https://github.com/xerrors owner_html_url: https://github.com/xerrors
- name: langserve - name: sqladmin
html_url: https://github.com/langchain-ai/langserve html_url: https://github.com/aminalaee/sqladmin
stars: 2191 stars: 2558
owner_login: langchain-ai owner_login: aminalaee
owner_html_url: https://github.com/langchain-ai owner_html_url: https://github.com/aminalaee
- name: RasaGPT
html_url: https://github.com/paulpierre/RasaGPT
stars: 2451
owner_login: paulpierre
owner_html_url: https://github.com/paulpierre
- name: supabase-py
html_url: https://github.com/supabase/supabase-py
stars: 2344
owner_login: supabase
owner_html_url: https://github.com/supabase
- name: nextpy
html_url: https://github.com/dot-agent/nextpy
stars: 2335
owner_login: dot-agent
owner_html_url: https://github.com/dot-agent
- name: fastapi-utils - name: fastapi-utils
html_url: https://github.com/fastapiutils/fastapi-utils html_url: https://github.com/fastapiutils/fastapi-utils
stars: 2185 stars: 2291
owner_login: fastapiutils owner_login: fastapiutils
owner_html_url: https://github.com/fastapiutils owner_html_url: https://github.com/fastapiutils
- name: 30-Days-of-Python
html_url: https://github.com/codingforentrepreneurs/30-Days-of-Python
stars: 2220
owner_login: codingforentrepreneurs
owner_html_url: https://github.com/codingforentrepreneurs
- name: langserve
html_url: https://github.com/langchain-ai/langserve
stars: 2215
owner_login: langchain-ai
owner_html_url: https://github.com/langchain-ai
- name: solara - name: solara
html_url: https://github.com/widgetti/solara html_url: https://github.com/widgetti/solara
stars: 2111 stars: 2122
owner_login: widgetti owner_login: widgetti
owner_html_url: https://github.com/widgetti owner_html_url: https://github.com/widgetti
- name: mangum - name: mangum
html_url: https://github.com/Kludex/mangum html_url: https://github.com/Kludex/mangum
stars: 2011 stars: 2029
owner_login: Kludex owner_login: Kludex
owner_html_url: https://github.com/Kludex owner_html_url: https://github.com/Kludex
- name: agentkit - name: agentkit
html_url: https://github.com/BCG-X-Official/agentkit html_url: https://github.com/BCG-X-Official/agentkit
stars: 1826 stars: 1912
owner_login: BCG-X-Official owner_login: BCG-X-Official
owner_html_url: https://github.com/BCG-X-Official owner_html_url: https://github.com/BCG-X-Official
- name: python-week-2022
html_url: https://github.com/rochacbruno/python-week-2022
stars: 1815
owner_login: rochacbruno
owner_html_url: https://github.com/rochacbruno
- name: manage-fastapi - name: manage-fastapi
html_url: https://github.com/ycd/manage-fastapi html_url: https://github.com/ycd/manage-fastapi
stars: 1787 stars: 1885
owner_login: ycd owner_login: ycd
owner_html_url: https://github.com/ycd owner_html_url: https://github.com/ycd
- name: ormar
html_url: https://github.com/collerek/ormar
stars: 1780
owner_login: collerek
owner_html_url: https://github.com/collerek
- name: vue-fastapi-admin
html_url: https://github.com/mizhexiaoxiao/vue-fastapi-admin
stars: 1758
owner_login: mizhexiaoxiao
owner_html_url: https://github.com/mizhexiaoxiao
- name: openapi-python-client - name: openapi-python-client
html_url: https://github.com/openapi-generators/openapi-python-client html_url: https://github.com/openapi-generators/openapi-python-client
stars: 1731 stars: 1862
owner_login: openapi-generators owner_login: openapi-generators
owner_html_url: https://github.com/openapi-generators owner_html_url: https://github.com/openapi-generators
- name: piccolo - name: piccolo
html_url: https://github.com/piccolo-orm/piccolo html_url: https://github.com/piccolo-orm/piccolo
stars: 1711 stars: 1836
owner_login: piccolo-orm owner_login: piccolo-orm
owner_html_url: https://github.com/piccolo-orm owner_html_url: https://github.com/piccolo-orm
- name: fastapi-cache - name: vue-fastapi-admin
html_url: https://github.com/long2ice/fastapi-cache html_url: https://github.com/mizhexiaoxiao/vue-fastapi-admin
stars: 1677 stars: 1831
owner_login: long2ice owner_login: mizhexiaoxiao
owner_html_url: https://github.com/long2ice owner_html_url: https://github.com/mizhexiaoxiao
- name: python-week-2022
html_url: https://github.com/rochacbruno/python-week-2022
stars: 1817
owner_login: rochacbruno
owner_html_url: https://github.com/rochacbruno
- name: slowapi - name: slowapi
html_url: https://github.com/laurentS/slowapi html_url: https://github.com/laurentS/slowapi
stars: 1669 stars: 1798
owner_login: laurentS owner_login: laurentS
owner_html_url: https://github.com/laurentS owner_html_url: https://github.com/laurentS
- name: langchain-serve - name: fastapi-cache
html_url: https://github.com/jina-ai/langchain-serve html_url: https://github.com/long2ice/fastapi-cache
stars: 1632 stars: 1789
owner_login: jina-ai owner_login: long2ice
owner_html_url: https://github.com/jina-ai owner_html_url: https://github.com/long2ice
- name: ormar
html_url: https://github.com/collerek/ormar
stars: 1783
owner_login: collerek
owner_html_url: https://github.com/collerek
- name: termpair - name: termpair
html_url: https://github.com/cs01/termpair html_url: https://github.com/cs01/termpair
stars: 1621 stars: 1716
owner_login: cs01 owner_login: cs01
owner_html_url: https://github.com/cs01 owner_html_url: https://github.com/cs01
- name: FastAPI-boilerplate - name: FastAPI-boilerplate
html_url: https://github.com/benavlabs/FastAPI-boilerplate html_url: https://github.com/benavlabs/FastAPI-boilerplate
stars: 1596 stars: 1660
owner_login: benavlabs owner_login: benavlabs
owner_html_url: https://github.com/benavlabs owner_html_url: https://github.com/benavlabs
- name: coronavirus-tracker-api - name: fastapi-langgraph-agent-production-ready-template
html_url: https://github.com/ExpDev07/coronavirus-tracker-api html_url: https://github.com/wassim249/fastapi-langgraph-agent-production-ready-template
stars: 1573 stars: 1638
owner_login: ExpDev07 owner_login: wassim249
owner_html_url: https://github.com/ExpDev07 owner_html_url: https://github.com/wassim249
- name: fastapi-crudrouter - name: langchain-serve
html_url: https://github.com/awtkns/fastapi-crudrouter html_url: https://github.com/jina-ai/langchain-serve
stars: 1553 stars: 1635
owner_login: awtkns owner_login: jina-ai
owner_html_url: https://github.com/awtkns owner_html_url: https://github.com/jina-ai
- name: awesome-fastapi-projects - name: awesome-fastapi-projects
html_url: https://github.com/Kludex/awesome-fastapi-projects html_url: https://github.com/Kludex/awesome-fastapi-projects
stars: 1485 stars: 1589
owner_login: Kludex owner_login: Kludex
owner_html_url: https://github.com/Kludex owner_html_url: https://github.com/Kludex
- name: fastapi-pagination - name: fastapi-pagination
html_url: https://github.com/uriyyo/fastapi-pagination html_url: https://github.com/uriyyo/fastapi-pagination
stars: 1473 stars: 1585
owner_login: uriyyo owner_login: uriyyo
owner_html_url: https://github.com/uriyyo owner_html_url: https://github.com/uriyyo
- name: coronavirus-tracker-api
html_url: https://github.com/ExpDev07/coronavirus-tracker-api
stars: 1574
owner_login: ExpDev07
owner_html_url: https://github.com/ExpDev07
- name: fastapi-crudrouter
html_url: https://github.com/awtkns/fastapi-crudrouter
stars: 1559
owner_login: awtkns
owner_html_url: https://github.com/awtkns
- name: bracket - name: bracket
html_url: https://github.com/evroon/bracket html_url: https://github.com/evroon/bracket
stars: 1470 stars: 1489
owner_login: evroon owner_login: evroon
owner_html_url: https://github.com/evroon owner_html_url: https://github.com/evroon
- name: fastapi-langgraph-agent-production-ready-template - name: fastapi-amis-admin
html_url: https://github.com/wassim249/fastapi-langgraph-agent-production-ready-template html_url: https://github.com/amisadmin/fastapi-amis-admin
stars: 1456 stars: 1475
owner_login: wassim249 owner_login: amisadmin
owner_html_url: https://github.com/wassim249 owner_html_url: https://github.com/amisadmin
- name: fastapi-boilerplate - name: fastapi-boilerplate
html_url: https://github.com/teamhide/fastapi-boilerplate html_url: https://github.com/teamhide/fastapi-boilerplate
stars: 1424 stars: 1436
owner_login: teamhide owner_login: teamhide
owner_html_url: https://github.com/teamhide owner_html_url: https://github.com/teamhide
- name: awesome-python-resources - name: awesome-python-resources
html_url: https://github.com/DjangoEx/awesome-python-resources html_url: https://github.com/DjangoEx/awesome-python-resources
stars: 1420 stars: 1426
owner_login: DjangoEx owner_login: DjangoEx
owner_html_url: https://github.com/DjangoEx owner_html_url: https://github.com/DjangoEx
- name: fastapi-amis-admin
html_url: https://github.com/amisadmin/fastapi-amis-admin
stars: 1363
owner_login: amisadmin
owner_html_url: https://github.com/amisadmin
- name: fastcrud - name: fastcrud
html_url: https://github.com/benavlabs/fastcrud html_url: https://github.com/benavlabs/fastcrud
stars: 1362 stars: 1414
owner_login: benavlabs owner_login: benavlabs
owner_html_url: https://github.com/benavlabs owner_html_url: https://github.com/benavlabs
- name: prometheus-fastapi-instrumentator
html_url: https://github.com/trallnag/prometheus-fastapi-instrumentator
stars: 1388
owner_login: trallnag
owner_html_url: https://github.com/trallnag
- name: fastapi_best_architecture
html_url: https://github.com/fastapi-practices/fastapi_best_architecture
stars: 1378
owner_login: fastapi-practices
owner_html_url: https://github.com/fastapi-practices
- name: fastapi-code-generator
html_url: https://github.com/koxudaxi/fastapi-code-generator
stars: 1375
owner_login: koxudaxi
owner_html_url: https://github.com/koxudaxi
- name: budgetml - name: budgetml
html_url: https://github.com/ebhy/budgetml html_url: https://github.com/ebhy/budgetml
stars: 1345 stars: 1345
@ -370,126 +390,106 @@
owner_html_url: https://github.com/ebhy owner_html_url: https://github.com/ebhy
- name: fastapi-tutorial - name: fastapi-tutorial
html_url: https://github.com/liaogx/fastapi-tutorial html_url: https://github.com/liaogx/fastapi-tutorial
stars: 1315 stars: 1327
owner_login: liaogx owner_login: liaogx
owner_html_url: https://github.com/liaogx owner_html_url: https://github.com/liaogx
- name: fastapi_best_architecture - name: fastapi-alembic-sqlmodel-async
html_url: https://github.com/fastapi-practices/fastapi_best_architecture html_url: https://github.com/jonra1993/fastapi-alembic-sqlmodel-async
stars: 1311 stars: 1259
owner_login: fastapi-practices owner_login: jonra1993
owner_html_url: https://github.com/fastapi-practices owner_html_url: https://github.com/jonra1993
- name: fastapi-code-generator - name: fastapi-scaff
html_url: https://github.com/koxudaxi/fastapi-code-generator html_url: https://github.com/atpuxiner/fastapi-scaff
stars: 1270 stars: 1255
owner_login: koxudaxi owner_login: atpuxiner
owner_html_url: https://github.com/koxudaxi owner_html_url: https://github.com/atpuxiner
- name: prometheus-fastapi-instrumentator
html_url: https://github.com/trallnag/prometheus-fastapi-instrumentator
stars: 1264
owner_login: trallnag
owner_html_url: https://github.com/trallnag
- name: bedrock-chat - name: bedrock-chat
html_url: https://github.com/aws-samples/bedrock-chat html_url: https://github.com/aws-samples/bedrock-chat
stars: 1243 stars: 1254
owner_login: aws-samples owner_login: aws-samples
owner_html_url: https://github.com/aws-samples owner_html_url: https://github.com/aws-samples
- name: bolt-python - name: bolt-python
html_url: https://github.com/slackapi/bolt-python html_url: https://github.com/slackapi/bolt-python
stars: 1238 stars: 1253
owner_login: slackapi owner_login: slackapi
owner_html_url: https://github.com/slackapi owner_html_url: https://github.com/slackapi
- name: fastapi_production_template - name: fastapi_production_template
html_url: https://github.com/zhanymkanov/fastapi_production_template html_url: https://github.com/zhanymkanov/fastapi_production_template
stars: 1209 stars: 1217
owner_login: zhanymkanov owner_login: zhanymkanov
owner_html_url: https://github.com/zhanymkanov owner_html_url: https://github.com/zhanymkanov
- name: fastapi-scaff
html_url: https://github.com/atpuxiner/fastapi-scaff
stars: 1200
owner_login: atpuxiner
owner_html_url: https://github.com/atpuxiner
- name: langchain-extract - name: langchain-extract
html_url: https://github.com/langchain-ai/langchain-extract html_url: https://github.com/langchain-ai/langchain-extract
stars: 1173 stars: 1176
owner_login: langchain-ai owner_login: langchain-ai
owner_html_url: https://github.com/langchain-ai owner_html_url: https://github.com/langchain-ai
- name: fastapi-alembic-sqlmodel-async
html_url: https://github.com/jonra1993/fastapi-alembic-sqlmodel-async
stars: 1162
owner_login: jonra1993
owner_html_url: https://github.com/jonra1993
- name: odmantic
html_url: https://github.com/art049/odmantic
stars: 1137
owner_login: art049
owner_html_url: https://github.com/art049
- name: restish - name: restish
html_url: https://github.com/rest-sh/restish html_url: https://github.com/rest-sh/restish
stars: 1129 stars: 1140
owner_login: rest-sh owner_login: rest-sh
owner_html_url: https://github.com/rest-sh owner_html_url: https://github.com/rest-sh
- name: kubetorch - name: odmantic
html_url: https://github.com/run-house/kubetorch html_url: https://github.com/art049/odmantic
stars: 1065 stars: 1138
owner_login: run-house owner_login: art049
owner_html_url: https://github.com/run-house owner_html_url: https://github.com/art049
- name: flock
html_url: https://github.com/Onelevenvy/flock
stars: 1039
owner_login: Onelevenvy
owner_html_url: https://github.com/Onelevenvy
- name: authx - name: authx
html_url: https://github.com/yezz123/authx html_url: https://github.com/yezz123/authx
stars: 1017 stars: 1119
owner_login: yezz123 owner_login: yezz123
owner_html_url: https://github.com/yezz123 owner_html_url: https://github.com/yezz123
- name: autollm - name: NoteDiscovery
html_url: https://github.com/viddexa/autollm html_url: https://github.com/gamosoft/NoteDiscovery
stars: 997 stars: 1107
owner_login: viddexa owner_login: gamosoft
owner_html_url: https://github.com/viddexa owner_html_url: https://github.com/gamosoft
- name: lanarky - name: flock
html_url: https://github.com/ajndkr/lanarky html_url: https://github.com/Onelevenvy/flock
stars: 993 stars: 1055
owner_login: ajndkr owner_login: Onelevenvy
owner_html_url: https://github.com/ajndkr owner_html_url: https://github.com/Onelevenvy
- name: RuoYi-Vue3-FastAPI
html_url: https://github.com/insistence/RuoYi-Vue3-FastAPI
stars: 974
owner_login: insistence
owner_html_url: https://github.com/insistence
- name: aktools
html_url: https://github.com/akfamily/aktools
stars: 972
owner_login: akfamily
owner_html_url: https://github.com/akfamily
- name: titiler
html_url: https://github.com/developmentseed/titiler
stars: 965
owner_login: developmentseed
owner_html_url: https://github.com/developmentseed
- name: secure
html_url: https://github.com/TypeError/secure
stars: 953
owner_login: TypeError
owner_html_url: https://github.com/TypeError
- name: energy-forecasting
html_url: https://github.com/iusztinpaul/energy-forecasting
stars: 949
owner_login: iusztinpaul
owner_html_url: https://github.com/iusztinpaul
- name: every-pdf
html_url: https://github.com/DDULDDUCK/every-pdf
stars: 942
owner_login: DDULDDUCK
owner_html_url: https://github.com/DDULDDUCK
- name: langcorn
html_url: https://github.com/msoedov/langcorn
stars: 933
owner_login: msoedov
owner_html_url: https://github.com/msoedov
- name: fastapi-observability - name: fastapi-observability
html_url: https://github.com/blueswen/fastapi-observability html_url: https://github.com/blueswen/fastapi-observability
stars: 923 stars: 1038
owner_login: blueswen owner_login: blueswen
owner_html_url: https://github.com/blueswen owner_html_url: https://github.com/blueswen
- name: aktools
html_url: https://github.com/akfamily/aktools
stars: 1027
owner_login: akfamily
owner_html_url: https://github.com/akfamily
- name: RuoYi-Vue3-FastAPI
html_url: https://github.com/insistence/RuoYi-Vue3-FastAPI
stars: 1016
owner_login: insistence
owner_html_url: https://github.com/insistence
- name: autollm
html_url: https://github.com/viddexa/autollm
stars: 1002
owner_login: viddexa
owner_html_url: https://github.com/viddexa
- name: titiler
html_url: https://github.com/developmentseed/titiler
stars: 999
owner_login: developmentseed
owner_html_url: https://github.com/developmentseed
- name: lanarky
html_url: https://github.com/ajndkr/lanarky
stars: 994
owner_login: ajndkr
owner_html_url: https://github.com/ajndkr
- name: every-pdf
html_url: https://github.com/DDULDDUCK/every-pdf
stars: 985
owner_login: DDULDDUCK
owner_html_url: https://github.com/DDULDDUCK
- name: enterprise-deep-research
html_url: https://github.com/SalesforceAIResearch/enterprise-deep-research
stars: 973
owner_login: SalesforceAIResearch
owner_html_url: https://github.com/SalesforceAIResearch
- name: fastapi-mail
html_url: https://github.com/sabuhish/fastapi-mail
stars: 964
owner_login: sabuhish
owner_html_url: https://github.com/sabuhish

View File

@ -75,7 +75,7 @@ mattwang44:
url: https://github.com/mattwang44 url: https://github.com/mattwang44
tiangolo: tiangolo:
login: tiangolo login: tiangolo
count: 55 count: 56
avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=cb5d06e73a9e1998141b1641aa88e443c6717651&v=4 avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=cb5d06e73a9e1998141b1641aa88e443c6717651&v=4
url: https://github.com/tiangolo url: https://github.com/tiangolo
Laineyzhang55: Laineyzhang55:
@ -136,7 +136,7 @@ JavierSanchezCastro:
alejsdev: alejsdev:
login: alejsdev login: alejsdev
count: 37 count: 37
avatarUrl: https://avatars.githubusercontent.com/u/90076947?u=447d12a1b347f466b35378bee4c7104cc9b2c571&v=4 avatarUrl: https://avatars.githubusercontent.com/u/90076947?u=85ceac49fb87138aebe8d663912e359447329090&v=4
url: https://github.com/alejsdev url: https://github.com/alejsdev
stlucasgarcia: stlucasgarcia:
login: stlucasgarcia login: stlucasgarcia
@ -436,7 +436,7 @@ jburckel:
peidrao: peidrao:
login: peidrao login: peidrao
count: 13 count: 13
avatarUrl: https://avatars.githubusercontent.com/u/32584628?u=64c634bb10381905038ff7faf3c8c3df47fb799a&v=4 avatarUrl: https://avatars.githubusercontent.com/u/32584628?u=979c62398e16ff000cc0faa028e028efd679887c&v=4
url: https://github.com/peidrao url: https://github.com/peidrao
impocode: impocode:
login: impocode login: impocode
@ -1006,7 +1006,7 @@ takacs:
anton2yakovlev: anton2yakovlev:
login: anton2yakovlev login: anton2yakovlev
count: 5 count: 5
avatarUrl: https://avatars.githubusercontent.com/u/44229180?u=bdd445ba99074b378e7298d23c4bf6d707d2c282&v=4 avatarUrl: https://avatars.githubusercontent.com/u/44229180?u=ac245e57bc834ff80f08ca8128000bb650a77a3d&v=4
url: https://github.com/anton2yakovlev url: https://github.com/anton2yakovlev
ILoveSorasakiHina: ILoveSorasakiHina:
login: ILoveSorasakiHina login: ILoveSorasakiHina
@ -1161,7 +1161,7 @@ cookie-byte217:
AbolfazlKameli: AbolfazlKameli:
login: AbolfazlKameli login: AbolfazlKameli
count: 4 count: 4
avatarUrl: https://avatars.githubusercontent.com/u/120686133?u=e41743da3c1820efafc59c5870cacd4f4425334c&v=4 avatarUrl: https://avatars.githubusercontent.com/u/120686133?u=af8f025278cce0d489007071254e4055df60b78c&v=4
url: https://github.com/AbolfazlKameli url: https://github.com/AbolfazlKameli
tyronedamasceno: tyronedamasceno:
login: tyronedamasceno login: tyronedamasceno
@ -1196,7 +1196,7 @@ Xaraxx:
Suyoung789: Suyoung789:
login: Suyoung789 login: Suyoung789
count: 3 count: 3
avatarUrl: https://avatars.githubusercontent.com/u/31277231?u=744bd3e641413e19bfad6b06a90bb0887c3f9332&v=4 avatarUrl: https://avatars.githubusercontent.com/u/31277231?u=1591aaf651eb860017231a36590050e154c026b6&v=4
url: https://github.com/Suyoung789 url: https://github.com/Suyoung789
akagaeng: akagaeng:
login: akagaeng login: akagaeng
@ -1806,7 +1806,7 @@ MrL8199:
ivintoiu: ivintoiu:
login: ivintoiu login: ivintoiu
count: 2 count: 2
avatarUrl: https://avatars.githubusercontent.com/u/1853336?u=b537c905ad08b69993de8796fb235c8d4d47f039&v=4 avatarUrl: https://avatars.githubusercontent.com/u/1853336?u=e3de5fd0ab17efc12256b4295285b504ca281440&v=4
url: https://github.com/ivintoiu url: https://github.com/ivintoiu
TechnoService2: TechnoService2:
login: TechnoService2 login: TechnoService2
@ -1841,7 +1841,7 @@ NavesSapnis:
eqsdxr: eqsdxr:
login: eqsdxr login: eqsdxr
count: 2 count: 2
avatarUrl: https://avatars.githubusercontent.com/u/157279130?u=d7aaffb29f542b647cf0f6b0e05722490863658a&v=4 avatarUrl: https://avatars.githubusercontent.com/u/157279130?u=7927dc0366995334f9a18c3204a41d3a34d6d96f&v=4
url: https://github.com/eqsdxr url: https://github.com/eqsdxr
syedasamina56: syedasamina56:
login: syedasamina56 login: syedasamina56

View File

@ -1,6 +1,6 @@
nilslindemann: nilslindemann:
login: nilslindemann login: nilslindemann
count: 124 count: 125
avatarUrl: https://avatars.githubusercontent.com/u/6892179?u=1dca6a22195d6cd1ab20737c0e19a4c55d639472&v=4 avatarUrl: https://avatars.githubusercontent.com/u/6892179?u=1dca6a22195d6cd1ab20737c0e19a4c55d639472&v=4
url: https://github.com/nilslindemann url: https://github.com/nilslindemann
jaystone776: jaystone776:
@ -8,16 +8,16 @@ jaystone776:
count: 46 count: 46
avatarUrl: https://avatars.githubusercontent.com/u/11191137?u=299205a95e9b6817a43144a48b643346a5aac5cc&v=4 avatarUrl: https://avatars.githubusercontent.com/u/11191137?u=299205a95e9b6817a43144a48b643346a5aac5cc&v=4
url: https://github.com/jaystone776 url: https://github.com/jaystone776
ceb10n:
login: ceb10n
count: 29
avatarUrl: https://avatars.githubusercontent.com/u/235213?u=edcce471814a1eba9f0cdaa4cd0de18921a940a6&v=4
url: https://github.com/ceb10n
valentinDruzhinin: valentinDruzhinin:
login: valentinDruzhinin login: valentinDruzhinin
count: 29 count: 29
avatarUrl: https://avatars.githubusercontent.com/u/12831905?u=aae1ebc675c91e8fa582df4fcc4fc4128106344d&v=4 avatarUrl: https://avatars.githubusercontent.com/u/12831905?u=aae1ebc675c91e8fa582df4fcc4fc4128106344d&v=4
url: https://github.com/valentinDruzhinin url: https://github.com/valentinDruzhinin
ceb10n:
login: ceb10n
count: 27
avatarUrl: https://avatars.githubusercontent.com/u/235213?u=edcce471814a1eba9f0cdaa4cd0de18921a940a6&v=4
url: https://github.com/ceb10n
tokusumi: tokusumi:
login: tokusumi login: tokusumi
count: 23 count: 23
@ -286,7 +286,7 @@ hsuanchi:
alejsdev: alejsdev:
login: alejsdev login: alejsdev
count: 3 count: 3
avatarUrl: https://avatars.githubusercontent.com/u/90076947?u=447d12a1b347f466b35378bee4c7104cc9b2c571&v=4 avatarUrl: https://avatars.githubusercontent.com/u/90076947?u=85ceac49fb87138aebe8d663912e359447329090&v=4
url: https://github.com/alejsdev url: https://github.com/alejsdev
riroan: riroan:
login: riroan login: riroan
@ -358,6 +358,11 @@ ruzia:
count: 3 count: 3
avatarUrl: https://avatars.githubusercontent.com/u/24503?v=4 avatarUrl: https://avatars.githubusercontent.com/u/24503?v=4
url: https://github.com/ruzia url: https://github.com/ruzia
YuriiMotov:
login: YuriiMotov
count: 3
avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=b9b13d598dddfab529a52d264df80a900bfe7060&v=4
url: https://github.com/YuriiMotov
izaguerreiro: izaguerreiro:
login: izaguerreiro login: izaguerreiro
count: 2 count: 2
@ -543,8 +548,3 @@ EdmilsonRodrigues:
count: 2 count: 2
avatarUrl: https://avatars.githubusercontent.com/u/62777025?u=217d6f3cd6cc750bb8818a3af7726c8d74eb7c2d&v=4 avatarUrl: https://avatars.githubusercontent.com/u/62777025?u=217d6f3cd6cc750bb8818a3af7726c8d74eb7c2d&v=4
url: https://github.com/EdmilsonRodrigues url: https://github.com/EdmilsonRodrigues
YuriiMotov:
login: YuriiMotov
count: 2
avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=b9b13d598dddfab529a52d264df80a900bfe7060&v=4
url: https://github.com/YuriiMotov

View File

@ -443,6 +443,14 @@ The docs UI will interact with the server that you select.
/// ///
/// note | Technical Details
The `servers` property in the OpenAPI specification is optional.
If you don't specify the `servers` parameter and `root_path` is equal to `/`, the `servers` property in the generated OpenAPI schema will be omitted entirely by default, which is the equivalent of a single server with a `url` value of `/`.
///
### Disable automatic server from `root_path` { #disable-automatic-server-from-root-path } ### Disable automatic server from `root_path` { #disable-automatic-server-from-root-path }
If you don't want **FastAPI** to include an automatic server using the `root_path`, you can use the parameter `root_path_in_servers=False`: If you don't want **FastAPI** to include an automatic server using the `root_path`, you can use the parameter `root_path_in_servers=False`:

View File

@ -1,9 +1,16 @@
/* Fira Code, including characters used by Rich output, like the "heavy right-pointing angle bracket ornament", not included in Google Fonts */ /* Fira Code, including characters used by Rich output, like the "heavy right-pointing angle bracket ornament", not included in Google Fonts */
@import url(https://cdn.jsdelivr.net/npm/firacode@6.2.0/distr/fira_code.css); @import url(https://cdn.jsdelivr.net/npm/firacode@6.2.0/distr/fira_code.css);
/* Noto Color Emoji for emoji support with the same font everywhere */
@import url(https://fonts.googleapis.com/css2?family=Noto+Color+Emoji&display=swap);
/* Override default code font in Material for MkDocs to Fira Code */ /* Override default code font in Material for MkDocs to Fira Code */
:root { :root {
--md-code-font: "Fira Code", monospace; --md-code-font: "Fira Code", monospace, "Noto Color Emoji";
}
/* Override default regular font in Material for MkDocs to include Noto Color Emoji */
:root {
--md-text-font: "Roboto", "Noto Color Emoji";
} }
.termynal-comment { .termynal-comment {

View File

@ -0,0 +1,17 @@
# Use Old 403 Authentication Error Status Codes { #use-old-403-authentication-error-status-codes }
Before FastAPI version `0.122.0`, when the integrated security utilities returned an error to the client after a failed authentication, they used the HTTP status code `403 Forbidden`.
Starting with FastAPI version `0.122.0`, they use the more appropriate HTTP status code `401 Unauthorized`, and return a sensible `WWW-Authenticate` header in the response, following the HTTP specifications, <a href="https://datatracker.ietf.org/doc/html/rfc7235#section-3.1" class="external-link" target="_blank">RFC 7235</a>, <a href="https://datatracker.ietf.org/doc/html/rfc9110#name-401-unauthorized" class="external-link" target="_blank">RFC 9110</a>.
But if for some reason your clients depend on the old behavior, you can revert to it by overriding the method `make_not_authenticated_error` in your security classes.
For example, you can create a subclass of `HTTPBearer` that returns a `403 Forbidden` error instead of the default `401 Unauthorized` error:
{* ../../docs_src/authentication_error_status_code/tutorial001_an_py39.py hl[9:13] *}
/// tip
Notice that the function returns the exception instance, it doesn't raise it. The raising is done in the rest of the internal code.
///

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

@ -46,7 +46,7 @@ The key features are:
* **Robust**: Get production-ready code. With automatic interactive documentation. * **Robust**: Get production-ready code. With automatic interactive documentation.
* **Standards-based**: Based on (and fully compatible with) the open standards for APIs: <a href="https://github.com/OAI/OpenAPI-Specification" class="external-link" target="_blank">OpenAPI</a> (previously known as Swagger) and <a href="https://json-schema.org/" class="external-link" target="_blank">JSON Schema</a>. * **Standards-based**: Based on (and fully compatible with) the open standards for APIs: <a href="https://github.com/OAI/OpenAPI-Specification" class="external-link" target="_blank">OpenAPI</a> (previously known as Swagger) and <a href="https://json-schema.org/" class="external-link" target="_blank">JSON Schema</a>.
<small>* estimation based on tests on an internal development team, building production applications.</small> <small>* estimation based on tests conducted by an internal development team, building production applications.</small>
## Sponsors { #sponsors } ## Sponsors { #sponsors }

View File

@ -7,8 +7,134 @@ hide:
## Latest Changes ## Latest Changes
## 0.123.9
### Fixes
* 🐛 Fix OAuth2 scopes in OpenAPI in extra corner cases, parent dependency with scopes, sub-dependency security scheme without scopes. PR [#14459](https://github.com/fastapi/fastapi/pull/14459) by [@tiangolo](https://github.com/tiangolo).
## 0.123.8
### Fixes
* 🐛 Fix OpenAPI security scheme OAuth2 scopes declaration, deduplicate security schemes with different scopes. PR [#14455](https://github.com/fastapi/fastapi/pull/14455) by [@tiangolo](https://github.com/tiangolo).
## 0.123.7
### Fixes
* 🐛 Fix evaluating stringified annotations in Python 3.10. PR [#11355](https://github.com/fastapi/fastapi/pull/11355) by [@chaen](https://github.com/chaen).
## 0.123.6
### Fixes
* 🐛 Fix support for functools wraps and partial combined, for async and regular functions and classes in path operations and dependencies. PR [#14448](https://github.com/fastapi/fastapi/pull/14448) by [@tiangolo](https://github.com/tiangolo).
## 0.123.5
### Features
* ✨ Allow using dependables with `functools.partial()`. PR [#9753](https://github.com/fastapi/fastapi/pull/9753) by [@lieryan](https://github.com/lieryan).
* ✨ Add support for wrapped functions (e.g. `@functools.wraps()`) used with forward references. PR [#5077](https://github.com/fastapi/fastapi/pull/5077) by [@lucaswiman](https://github.com/lucaswiman).
* ✨ Handle wrapped dependencies. PR [#9555](https://github.com/fastapi/fastapi/pull/9555) by [@phy1729](https://github.com/phy1729).
### Fixes
* 🐛 Fix optional sequence handling with new union syntax from Python 3.10. PR [#14430](https://github.com/fastapi/fastapi/pull/14430) by [@Viicos](https://github.com/Viicos).
### Refactors
* 🔥 Remove dangling extra condiitonal no longer needed. PR [#14435](https://github.com/fastapi/fastapi/pull/14435) by [@tiangolo](https://github.com/tiangolo).
* ♻️ Refactor internals, update `is_coroutine` check to reuse internal supported variants (unwrap, check class). PR [#14434](https://github.com/fastapi/fastapi/pull/14434) by [@tiangolo](https://github.com/tiangolo).
### Translations
* 🌐 Sync German docs. PR [#14367](https://github.com/fastapi/fastapi/pull/14367) by [@nilslindemann](https://github.com/nilslindemann).
## 0.123.4
### Fixes
* 🐛 Fix OpenAPI schema support for computed fields when using `separate_input_output_schemas=False`. PR [#13207](https://github.com/fastapi/fastapi/pull/13207) by [@vgrafe](https://github.com/vgrafe).
### Docs
* 📝 Fix docstring of `servers` parameter. PR [#14405](https://github.com/fastapi/fastapi/pull/14405) by [@YuriiMotov](https://github.com/YuriiMotov).
## 0.123.3
### Fixes
* 🐛 Fix Query\Header\Cookie parameter model alias. PR [#14360](https://github.com/fastapi/fastapi/pull/14360) by [@YuriiMotov](https://github.com/YuriiMotov).
* 🐛 Fix optional sequence handling in `serialize sequence value` with Pydantic V2. PR [#14297](https://github.com/fastapi/fastapi/pull/14297) by [@YuriiMotov](https://github.com/YuriiMotov).
## 0.123.2
### Fixes
* 🐛 Fix unformatted `{type_}` in FastAPIError. PR [#14416](https://github.com/fastapi/fastapi/pull/14416) by [@Just-Helpful](https://github.com/Just-Helpful).
* 🐛 Fix parsing extra non-body parameter list. PR [#14356](https://github.com/fastapi/fastapi/pull/14356) by [@YuriiMotov](https://github.com/YuriiMotov).
* 🐛 Fix parsing extra `Form` parameter list. PR [#14303](https://github.com/fastapi/fastapi/pull/14303) by [@YuriiMotov](https://github.com/YuriiMotov).
* 🐛 Fix support for form values with empty strings interpreted as missing (`None` if that's the default), for compatibility with HTML forms. PR [#13537](https://github.com/fastapi/fastapi/pull/13537) by [@MarinPostma](https://github.com/MarinPostma).
### Docs
* 📝 Add tip on how to install `pip` in case of `No module named pip` error in `virtual-environments.md`. PR [#14211](https://github.com/fastapi/fastapi/pull/14211) by [@zadevhub](https://github.com/zadevhub).
* 📝 Update Primary Key notes for the SQL databases tutorial to avoid confusion. PR [#14120](https://github.com/fastapi/fastapi/pull/14120) by [@FlaviusRaducu](https://github.com/FlaviusRaducu).
* 📝 Clarify estimation note in documentation. PR [#14070](https://github.com/fastapi/fastapi/pull/14070) by [@SaisakthiM](https://github.com/SaisakthiM).
## 0.123.1
### Fixes
* 🐛 Avoid accessing non-existing "$ref" key for Pydantic v2 compat remapping. PR [#14361](https://github.com/fastapi/fastapi/pull/14361) by [@svlandeg](https://github.com/svlandeg).
* 🐛 Fix `TypeError` when encoding a decimal with a `NaN` or `Infinity` value. PR [#12935](https://github.com/fastapi/fastapi/pull/12935) by [@kentwelcome](https://github.com/kentwelcome).
### Internal ### Internal
* 🐛 Fix Windows UnicodeEncodeError in CLI test. PR [#14295](https://github.com/fastapi/fastapi/pull/14295) by [@hemanth-thirthahalli](https://github.com/hemanth-thirthahalli).
* 🔧 Update sponsors: add Greptile. PR [#14429](https://github.com/fastapi/fastapi/pull/14429) by [@tiangolo](https://github.com/tiangolo).
* 👥 Update FastAPI GitHub topic repositories. PR [#14426](https://github.com/fastapi/fastapi/pull/14426) by [@tiangolo](https://github.com/tiangolo).
* ⬆ Bump markdown-include-variants from 0.0.6 to 0.0.7. PR [#14423](https://github.com/fastapi/fastapi/pull/14423) by [@YuriiMotov](https://github.com/YuriiMotov).
* 👥 Update FastAPI People - Sponsors. PR [#14422](https://github.com/fastapi/fastapi/pull/14422) by [@tiangolo](https://github.com/tiangolo).
* 👥 Update FastAPI People - Contributors and Translators. PR [#14420](https://github.com/fastapi/fastapi/pull/14420) by [@tiangolo](https://github.com/tiangolo).
## 0.123.0
### Fixes
* 🐛 Cache dependencies that don't use scopes and don't have sub-dependencies with scopes. PR [#14419](https://github.com/fastapi/fastapi/pull/14419) by [@tiangolo](https://github.com/tiangolo).
## 0.122.1
### Fixes
* 🐛 Fix hierarchical security scope propagation. PR [#5624](https://github.com/fastapi/fastapi/pull/5624) by [@kristjanvalur](https://github.com/kristjanvalur).
### Docs
* 💅 Update CSS to explicitly use emoji font. PR [#14415](https://github.com/fastapi/fastapi/pull/14415) by [@tiangolo](https://github.com/tiangolo).
### Internal
* ⬆ Bump markdown-include-variants from 0.0.5 to 0.0.6. PR [#14418](https://github.com/fastapi/fastapi/pull/14418) by [@YuriiMotov](https://github.com/YuriiMotov).
## 0.122.0
### Fixes
* 🐛 Use `401` status code in security classes when credentials are missing. PR [#13786](https://github.com/fastapi/fastapi/pull/13786) by [@YuriiMotov](https://github.com/YuriiMotov).
* If your code depended on these classes raising the old (less correct) `403` status code, check the new docs about how to override the classes, to use the same old behavior: [Use Old 403 Authentication Error Status Codes](https://fastapi.tiangolo.com/how-to/authentication-error-status-code/).
### Internal
* 🔧 Configure labeler to exclude files that start from underscore for `lang-all` label. PR [#14213](https://github.com/fastapi/fastapi/pull/14213) by [@YuriiMotov](https://github.com/YuriiMotov).
* 👷 Add pre-commit config with local script for permalinks. PR [#14398](https://github.com/fastapi/fastapi/pull/14398) by [@tiangolo](https://github.com/tiangolo).
* 💄 Use font Fira Code to fix display of Rich panels in docs in Windows. PR [#14387](https://github.com/fastapi/fastapi/pull/14387) by [@tiangolo](https://github.com/tiangolo).
* 👷 Add custom pre-commit CI. PR [#14397](https://github.com/fastapi/fastapi/pull/14397) by [@tiangolo](https://github.com/tiangolo).
* ⬆ Bump actions/checkout from 5 to 6. PR [#14381](https://github.com/fastapi/fastapi/pull/14381) by [@dependabot[bot]](https://github.com/apps/dependabot).
* 👷 Upgrade `latest-changes` GitHub Action and pin `actions/checkout@v5`. PR [#14403](https://github.com/fastapi/fastapi/pull/14403) by [@svlandeg](https://github.com/svlandeg).
* 🛠️ Add `add-permalinks` and `add-permalinks-page` to `scripts/docs.py`. PR [#14033](https://github.com/fastapi/fastapi/pull/14033) by [@YuriiMotov](https://github.com/YuriiMotov). * 🛠️ Add `add-permalinks` and `add-permalinks-page` to `scripts/docs.py`. PR [#14033](https://github.com/fastapi/fastapi/pull/14033) by [@YuriiMotov](https://github.com/YuriiMotov).
* 🔧 Upgrade Material for MkDocs and remove insiders. PR [#14375](https://github.com/fastapi/fastapi/pull/14375) by [@tiangolo](https://github.com/tiangolo). * 🔧 Upgrade Material for MkDocs and remove insiders. PR [#14375](https://github.com/fastapi/fastapi/pull/14375) by [@tiangolo](https://github.com/tiangolo).

View File

@ -65,7 +65,7 @@ There are a few differences:
* `Field(primary_key=True)` tells SQLModel that the `id` is the **primary key** in the SQL database (you can learn more about SQL primary keys in the SQLModel docs). * `Field(primary_key=True)` tells SQLModel that the `id` is the **primary key** in the SQL database (you can learn more about SQL primary keys in the SQLModel docs).
By having the type as `int | None`, SQLModel will know that this column should be an `INTEGER` in the SQL database and that it should be `NULLABLE`. **Note:** We use `int | None` for the primary key field so that in Python code we can *create an object without an `id`* (`id=None`), assuming the database will *generate it when saving*. SQLModel understands that the database will provide the `id` and *defines the column as a non-null `INTEGER`* in the database schema. See <a href="https://sqlmodel.tiangolo.com/tutorial/create-db-and-table/#primary-key-id" class="external-link" target="_blank">SQLModel docs on primary keys</a> for details.
* `Field(index=True)` tells SQLModel that it should create a **SQL index** for this column, that would allow faster lookups in the database when reading data filtered by this column. * `Field(index=True)` tells SQLModel that it should create a **SQL index** for this column, that would allow faster lookups in the database when reading data filtered by this column.

View File

@ -242,6 +242,26 @@ $ python -m pip install --upgrade pip
</div> </div>
/// tip
Sometimes, you might get a **`No module named pip`** error when trying to upgrade pip.
If this happens, install and upgrade pip using the command below:
<div class="termy">
```console
$ python -m ensurepip --upgrade
---> 100%
```
</div>
This command will install pip if it is not already installed and also ensures that the installed version of pip is at least as recent as the one available in `ensurepip`.
///
## Add `.gitignore` { #add-gitignore } ## Add `.gitignore` { #add-gitignore }
If you are using **Git** (you should), add a `.gitignore` file to exclude everything in your `.venv` from Git. If you are using **Git** (you should), add a `.gitignore` file to exclude everything in your `.venv` from Git.

View File

@ -215,6 +215,7 @@ nav:
- how-to/custom-docs-ui-assets.md - how-to/custom-docs-ui-assets.md
- how-to/configure-swagger-ui.md - how-to/configure-swagger-ui.md
- how-to/testing-database.md - how-to/testing-database.md
- how-to/authentication-error-status-code.md
- Reference (Code API): - Reference (Code API):
- reference/index.md - reference/index.md
- reference/fastapi.md - reference/fastapi.md

View File

@ -93,6 +93,12 @@
<img class="sponsor-image" src="/img/sponsors/serpapi-banner.png" /> <img class="sponsor-image" src="/img/sponsors/serpapi-banner.png" />
</a> </a>
</div> </div>
<div class="item">
<a title="Greptile: The AI Code Reviewer" style="display: block; position: relative;" href="https://www.greptile.com/?utm_source=fastapi&utm_medium=sponsorship&utm_campaign=fastapi_sponsor_page" target="_blank">
<span class="sponsor-badge">sponsor</span>
<img class="sponsor-image" src="/img/sponsors/greptile-banner.png" />
</a>
</div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -0,0 +1,20 @@
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from typing_extensions import Annotated
app = FastAPI()
class HTTPBearer403(HTTPBearer):
def make_not_authenticated_error(self) -> HTTPException:
return HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail="Not authenticated"
)
CredentialsDep = Annotated[HTTPAuthorizationCredentials, Depends(HTTPBearer403())]
@app.get("/me")
def read_me(credentials: CredentialsDep):
return {"message": "You are authenticated", "token": credentials.credentials}

View File

@ -0,0 +1,21 @@
from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
app = FastAPI()
class HTTPBearer403(HTTPBearer):
def make_not_authenticated_error(self) -> HTTPException:
return HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail="Not authenticated"
)
CredentialsDep = Annotated[HTTPAuthorizationCredentials, Depends(HTTPBearer403())]
@app.get("/me")
def read_me(credentials: CredentialsDep):
return {"message": "You are authenticated", "token": credentials.credentials}

View File

@ -60,7 +60,7 @@ async def get_current_user(token: str = Depends(oauth2_scheme)):
if not user: if not user:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication credentials", detail="Not authenticated",
headers={"WWW-Authenticate": "Bearer"}, headers={"WWW-Authenticate": "Bearer"},
) )
return user return user

View File

@ -61,7 +61,7 @@ async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]):
if not user: if not user:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication credentials", detail="Not authenticated",
headers={"WWW-Authenticate": "Bearer"}, headers={"WWW-Authenticate": "Bearer"},
) )
return user return user

View File

@ -60,7 +60,7 @@ async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]):
if not user: if not user:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication credentials", detail="Not authenticated",
headers={"WWW-Authenticate": "Bearer"}, headers={"WWW-Authenticate": "Bearer"},
) )
return user return user

View File

@ -60,7 +60,7 @@ async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]):
if not user: if not user:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication credentials", detail="Not authenticated",
headers={"WWW-Authenticate": "Bearer"}, headers={"WWW-Authenticate": "Bearer"},
) )
return user return user

View File

@ -58,7 +58,7 @@ async def get_current_user(token: str = Depends(oauth2_scheme)):
if not user: if not user:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication credentials", detail="Not authenticated",
headers={"WWW-Authenticate": "Bearer"}, headers={"WWW-Authenticate": "Bearer"},
) )
return user return user

View File

@ -1,6 +1,6 @@
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production""" """FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
__version__ = "0.121.3" __version__ = "0.123.9"
from starlette import status as status from starlette import status as status

View File

@ -17,7 +17,7 @@ from typing import (
from fastapi._compat import may_v1, shared from fastapi._compat import may_v1, shared
from fastapi.openapi.constants import REF_TEMPLATE from fastapi.openapi.constants import REF_TEMPLATE
from fastapi.types import IncEx, ModelNameMap from fastapi.types import IncEx, ModelNameMap, UnionType
from pydantic import BaseModel, TypeAdapter, create_model from pydantic import BaseModel, TypeAdapter, create_model
from pydantic import PydanticSchemaGenerationError as PydanticSchemaGenerationError from pydantic import PydanticSchemaGenerationError as PydanticSchemaGenerationError
from pydantic import PydanticUndefinedAnnotation as PydanticUndefinedAnnotation from pydantic import PydanticUndefinedAnnotation as PydanticUndefinedAnnotation
@ -180,8 +180,13 @@ def get_schema_from_model_field(
], ],
separate_input_output_schemas: bool = True, separate_input_output_schemas: bool = True,
) -> Dict[str, Any]: ) -> Dict[str, Any]:
computed_fields = field._type_adapter.core_schema.get("schema", {}).get(
"computed_fields", []
)
override_mode: Union[Literal["validation"], None] = ( override_mode: Union[Literal["validation"], None] = (
None if separate_input_output_schemas else "validation" None
if (separate_input_output_schemas or len(computed_fields) > 0)
else "validation"
) )
# This expects that GenerateJsonSchema was already used to generate the definitions # This expects that GenerateJsonSchema was already used to generate the definitions
json_schema = field_mapping[(field, override_mode or field.mode)] json_schema = field_mapping[(field, override_mode or field.mode)]
@ -203,9 +208,14 @@ def get_definitions(
Dict[Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue], Dict[Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue],
Dict[str, Dict[str, Any]], Dict[str, Dict[str, Any]],
]: ]:
has_computed_fields: bool = any(
field._type_adapter.core_schema.get("schema", {}).get("computed_fields", [])
for field in fields
)
schema_generator = GenerateJsonSchema(ref_template=REF_TEMPLATE) schema_generator = GenerateJsonSchema(ref_template=REF_TEMPLATE)
override_mode: Union[Literal["validation"], None] = ( override_mode: Union[Literal["validation"], None] = (
None if separate_input_output_schemas else "validation" None if (separate_input_output_schemas or has_computed_fields) else "validation"
) )
validation_fields = [field for field in fields if field.mode == "validation"] validation_fields = [field for field in fields if field.mode == "validation"]
serialization_fields = [field for field in fields if field.mode == "serialization"] serialization_fields = [field for field in fields if field.mode == "serialization"]
@ -304,7 +314,7 @@ def _remap_definitions_and_field_mappings(
old_name_to_new_name_map = {} old_name_to_new_name_map = {}
for field_key, schema in field_mapping.items(): for field_key, schema in field_mapping.items():
model = field_key[0].type_ model = field_key[0].type_
if model not in model_name_map: if model not in model_name_map or "$ref" not in schema:
continue continue
new_name = model_name_map[model] new_name = model_name_map[model]
old_name = schema["$ref"].split("/")[-1] old_name = schema["$ref"].split("/")[-1]
@ -371,6 +381,13 @@ def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo:
def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]: def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]:
origin_type = get_origin(field.field_info.annotation) or field.field_info.annotation origin_type = get_origin(field.field_info.annotation) or field.field_info.annotation
if origin_type is Union or origin_type is UnionType: # Handle optional sequences
union_args = get_args(field.field_info.annotation)
for union_arg in union_args:
if union_arg is type(None):
continue
origin_type = get_origin(union_arg) or union_arg
break
assert issubclass(origin_type, shared.sequence_types) # type: ignore[arg-type] assert issubclass(origin_type, shared.sequence_types) # type: ignore[arg-type]
return shared.sequence_annotation_to_type[origin_type](value) # type: ignore[no-any-return] return shared.sequence_annotation_to_type[origin_type](value) # type: ignore[no-any-return]

View File

@ -301,7 +301,12 @@ class FastAPI(Starlette):
browser tabs open). Or if you want to leave fixed the possible URLs. browser tabs open). Or if you want to leave fixed the possible URLs.
If the servers `list` is not provided, or is an empty `list`, the If the servers `list` is not provided, or is an empty `list`, the
default value would be a `dict` with a `url` value of `/`. `servers` property in the generated OpenAPI will be:
* a `dict` with a `url` value of the application's mounting point
(`root_path`) if it's different from `/`.
* otherwise, the `servers` property will be omitted from the OpenAPI
schema.
Each item in the `list` is a `dict` containing: Each item in the `list` is a `dict` containing:

View File

@ -1,8 +1,8 @@
import inspect import inspect
import sys import sys
from dataclasses import dataclass, field from dataclasses import dataclass, field
from functools import cached_property from functools import cached_property, partial
from typing import Any, Callable, List, Optional, Sequence, Union from typing import Any, Callable, List, Optional, Union
from fastapi._compat import ModelField from fastapi._compat import ModelField
from fastapi.security.base import SecurityBase from fastapi.security.base import SecurityBase
@ -15,10 +15,17 @@ else: # pragma: no cover
from asyncio import iscoroutinefunction from asyncio import iscoroutinefunction
@dataclass def _unwrapped_call(call: Optional[Callable[..., Any]]) -> Any:
class SecurityRequirement: if call is None:
security_scheme: SecurityBase return call # pragma: no cover
scopes: Optional[Sequence[str]] = None unwrapped = inspect.unwrap(_impartial(call))
return unwrapped
def _impartial(func: Callable[..., Any]) -> Callable[..., Any]:
while isinstance(func, partial):
func = func.func
return func
@dataclass @dataclass
@ -29,7 +36,6 @@ class Dependant:
cookie_params: List[ModelField] = field(default_factory=list) cookie_params: List[ModelField] = field(default_factory=list)
body_params: List[ModelField] = field(default_factory=list) body_params: List[ModelField] = field(default_factory=list)
dependencies: List["Dependant"] = field(default_factory=list) dependencies: List["Dependant"] = field(default_factory=list)
security_requirements: List[SecurityRequirement] = field(default_factory=list)
name: Optional[str] = None name: Optional[str] = None
call: Optional[Callable[..., Any]] = None call: Optional[Callable[..., Any]] = None
request_param_name: Optional[str] = None request_param_name: Optional[str] = None
@ -38,41 +44,140 @@ class Dependant:
response_param_name: Optional[str] = None response_param_name: Optional[str] = None
background_tasks_param_name: Optional[str] = None background_tasks_param_name: Optional[str] = None
security_scopes_param_name: Optional[str] = None security_scopes_param_name: Optional[str] = None
security_scopes: Optional[List[str]] = None own_oauth_scopes: Optional[List[str]] = None
parent_oauth_scopes: Optional[List[str]] = None
use_cache: bool = True use_cache: bool = True
path: Optional[str] = None path: Optional[str] = None
scope: Union[Literal["function", "request"], None] = None scope: Union[Literal["function", "request"], None] = None
@cached_property
def oauth_scopes(self) -> List[str]:
scopes = self.parent_oauth_scopes.copy() if self.parent_oauth_scopes else []
# This doesn't use a set to preserve order, just in case
for scope in self.own_oauth_scopes or []:
if scope not in scopes:
scopes.append(scope)
return scopes
@cached_property @cached_property
def cache_key(self) -> DependencyCacheKey: def cache_key(self) -> DependencyCacheKey:
scopes_for_cache = (
tuple(sorted(set(self.oauth_scopes or []))) if self._uses_scopes else ()
)
return ( return (
self.call, self.call,
tuple(sorted(set(self.security_scopes or []))), scopes_for_cache,
self.computed_scope or "", self.computed_scope or "",
) )
@cached_property @cached_property
def is_gen_callable(self) -> bool: def _uses_scopes(self) -> bool:
if inspect.isgeneratorfunction(self.call): if self.own_oauth_scopes:
return True return True
dunder_call = getattr(self.call, "__call__", None) # noqa: B004 if self.security_scopes_param_name is not None:
return inspect.isgeneratorfunction(dunder_call) return True
if self._is_security_scheme:
return True
for sub_dep in self.dependencies:
if sub_dep._uses_scopes:
return True
return False
@cached_property
def _is_security_scheme(self) -> bool:
if self.call is None:
return False # pragma: no cover
unwrapped = _unwrapped_call(self.call)
return isinstance(unwrapped, SecurityBase)
# Mainly to get the type of SecurityBase, but it's the same self.call
@cached_property
def _security_scheme(self) -> SecurityBase:
unwrapped = _unwrapped_call(self.call)
assert isinstance(unwrapped, SecurityBase)
return unwrapped
@cached_property
def _security_dependencies(self) -> List["Dependant"]:
security_deps = [dep for dep in self.dependencies if dep._is_security_scheme]
return security_deps
@cached_property
def is_gen_callable(self) -> bool:
if self.call is None:
return False # pragma: no cover
if inspect.isgeneratorfunction(
_impartial(self.call)
) or inspect.isgeneratorfunction(_unwrapped_call(self.call)):
return True
dunder_call = getattr(_impartial(self.call), "__call__", None) # noqa: B004
if dunder_call is None:
return False # pragma: no cover
if inspect.isgeneratorfunction(
_impartial(dunder_call)
) or inspect.isgeneratorfunction(_unwrapped_call(dunder_call)):
return True
dunder_unwrapped_call = getattr(_unwrapped_call(self.call), "__call__", None) # noqa: B004
if dunder_unwrapped_call is None:
return False # pragma: no cover
if inspect.isgeneratorfunction(
_impartial(dunder_unwrapped_call)
) or inspect.isgeneratorfunction(_unwrapped_call(dunder_unwrapped_call)):
return True
return False
@cached_property @cached_property
def is_async_gen_callable(self) -> bool: def is_async_gen_callable(self) -> bool:
if inspect.isasyncgenfunction(self.call): if self.call is None:
return False # pragma: no cover
if inspect.isasyncgenfunction(
_impartial(self.call)
) or inspect.isasyncgenfunction(_unwrapped_call(self.call)):
return True return True
dunder_call = getattr(self.call, "__call__", None) # noqa: B004 dunder_call = getattr(_impartial(self.call), "__call__", None) # noqa: B004
return inspect.isasyncgenfunction(dunder_call) if dunder_call is None:
return False # pragma: no cover
if inspect.isasyncgenfunction(
_impartial(dunder_call)
) or inspect.isasyncgenfunction(_unwrapped_call(dunder_call)):
return True
dunder_unwrapped_call = getattr(_unwrapped_call(self.call), "__call__", None) # noqa: B004
if dunder_unwrapped_call is None:
return False # pragma: no cover
if inspect.isasyncgenfunction(
_impartial(dunder_unwrapped_call)
) or inspect.isasyncgenfunction(_unwrapped_call(dunder_unwrapped_call)):
return True
return False
@cached_property @cached_property
def is_coroutine_callable(self) -> bool: def is_coroutine_callable(self) -> bool:
if inspect.isroutine(self.call): if self.call is None:
return iscoroutinefunction(self.call) return False # pragma: no cover
if inspect.isclass(self.call): if inspect.isroutine(_impartial(self.call)) and iscoroutinefunction(
return False _impartial(self.call)
dunder_call = getattr(self.call, "__call__", None) # noqa: B004 ):
return iscoroutinefunction(dunder_call) return True
if inspect.isroutine(_unwrapped_call(self.call)) and iscoroutinefunction(
_unwrapped_call(self.call)
):
return True
dunder_call = getattr(_impartial(self.call), "__call__", None) # noqa: B004
if dunder_call is None:
return False # pragma: no cover
if iscoroutinefunction(_impartial(dunder_call)) or iscoroutinefunction(
_unwrapped_call(dunder_call)
):
return True
dunder_unwrapped_call = getattr(_unwrapped_call(self.call), "__call__", None) # noqa: B004
if dunder_unwrapped_call is None:
return False # pragma: no cover
if iscoroutinefunction(
_impartial(dunder_unwrapped_call)
) or iscoroutinefunction(_unwrapped_call(dunder_unwrapped_call)):
return True
# if inspect.isclass(self.call): False, covered by default return
return False
@cached_property @cached_property
def computed_scope(self) -> Union[str, None]: def computed_scope(self) -> Union[str, None]:

View File

@ -1,5 +1,6 @@
import dataclasses import dataclasses
import inspect import inspect
import sys
from contextlib import AsyncExitStack, contextmanager from contextlib import AsyncExitStack, contextmanager
from copy import copy, deepcopy from copy import copy, deepcopy
from dataclasses import dataclass from dataclasses import dataclass
@ -55,12 +56,10 @@ from fastapi.concurrency import (
contextmanager_in_threadpool, contextmanager_in_threadpool,
) )
from fastapi.datastructures import UploadFile as FastAPIUploadFile from fastapi.datastructures import UploadFile as FastAPIUploadFile
from fastapi.dependencies.models import Dependant, SecurityRequirement from fastapi.dependencies.models import Dependant
from fastapi.exceptions import DependencyScopeError from fastapi.exceptions import DependencyScopeError
from fastapi.logger import logger from fastapi.logger import logger
from fastapi.security.base import SecurityBase from fastapi.security.oauth2 import SecurityScopes
from fastapi.security.oauth2 import OAuth2, SecurityScopes
from fastapi.security.open_id_connect_url import OpenIdConnect
from fastapi.types import DependencyCacheKey from fastapi.types import DependencyCacheKey
from fastapi.utils import create_model_field, get_path_param_names from fastapi.utils import create_model_field, get_path_param_names
from pydantic import BaseModel from pydantic import BaseModel
@ -127,14 +126,14 @@ def get_parameterless_sub_dependant(*, depends: params.Depends, path: str) -> De
assert callable(depends.dependency), ( assert callable(depends.dependency), (
"A parameter-less dependency must have a callable dependency" "A parameter-less dependency must have a callable dependency"
) )
use_security_scopes: List[str] = [] own_oauth_scopes: List[str] = []
if isinstance(depends, params.Security) and depends.scopes: if isinstance(depends, params.Security) and depends.scopes:
use_security_scopes.extend(depends.scopes) own_oauth_scopes.extend(depends.scopes)
return get_dependant( return get_dependant(
path=path, path=path,
call=depends.dependency, call=depends.dependency,
scope=depends.scope, scope=depends.scope,
security_scopes=use_security_scopes, own_oauth_scopes=own_oauth_scopes,
) )
@ -143,10 +142,14 @@ def get_flat_dependant(
*, *,
skip_repeats: bool = False, skip_repeats: bool = False,
visited: Optional[List[DependencyCacheKey]] = None, visited: Optional[List[DependencyCacheKey]] = None,
parent_oauth_scopes: Optional[List[str]] = None,
) -> Dependant: ) -> Dependant:
if visited is None: if visited is None:
visited = [] visited = []
visited.append(dependant.cache_key) visited.append(dependant.cache_key)
use_parent_oauth_scopes = (parent_oauth_scopes or []) + (
dependant.oauth_scopes or []
)
flat_dependant = Dependant( flat_dependant = Dependant(
path_params=dependant.path_params.copy(), path_params=dependant.path_params.copy(),
@ -154,22 +157,37 @@ def get_flat_dependant(
header_params=dependant.header_params.copy(), header_params=dependant.header_params.copy(),
cookie_params=dependant.cookie_params.copy(), cookie_params=dependant.cookie_params.copy(),
body_params=dependant.body_params.copy(), body_params=dependant.body_params.copy(),
security_requirements=dependant.security_requirements.copy(), name=dependant.name,
call=dependant.call,
request_param_name=dependant.request_param_name,
websocket_param_name=dependant.websocket_param_name,
http_connection_param_name=dependant.http_connection_param_name,
response_param_name=dependant.response_param_name,
background_tasks_param_name=dependant.background_tasks_param_name,
security_scopes_param_name=dependant.security_scopes_param_name,
own_oauth_scopes=dependant.own_oauth_scopes,
parent_oauth_scopes=use_parent_oauth_scopes,
use_cache=dependant.use_cache, use_cache=dependant.use_cache,
path=dependant.path, path=dependant.path,
scope=dependant.scope,
) )
for sub_dependant in dependant.dependencies: for sub_dependant in dependant.dependencies:
if skip_repeats and sub_dependant.cache_key in visited: if skip_repeats and sub_dependant.cache_key in visited:
continue continue
flat_sub = get_flat_dependant( flat_sub = get_flat_dependant(
sub_dependant, skip_repeats=skip_repeats, visited=visited sub_dependant,
skip_repeats=skip_repeats,
visited=visited,
parent_oauth_scopes=flat_dependant.oauth_scopes,
) )
flat_dependant.dependencies.append(flat_sub)
flat_dependant.path_params.extend(flat_sub.path_params) flat_dependant.path_params.extend(flat_sub.path_params)
flat_dependant.query_params.extend(flat_sub.query_params) flat_dependant.query_params.extend(flat_sub.query_params)
flat_dependant.header_params.extend(flat_sub.header_params) flat_dependant.header_params.extend(flat_sub.header_params)
flat_dependant.cookie_params.extend(flat_sub.cookie_params) flat_dependant.cookie_params.extend(flat_sub.cookie_params)
flat_dependant.body_params.extend(flat_sub.body_params) flat_dependant.body_params.extend(flat_sub.body_params)
flat_dependant.security_requirements.extend(flat_sub.security_requirements) flat_dependant.dependencies.extend(flat_sub.dependencies)
return flat_dependant return flat_dependant
@ -193,8 +211,12 @@ def get_flat_params(dependant: Dependant) -> List[ModelField]:
def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature: def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature:
signature = inspect.signature(call) if sys.version_info >= (3, 10):
globalns = getattr(call, "__globals__", {}) signature = inspect.signature(call, eval_str=True)
else:
signature = inspect.signature(call)
unwrapped = inspect.unwrap(call)
globalns = getattr(unwrapped, "__globals__", {})
typed_params = [ typed_params = [
inspect.Parameter( inspect.Parameter(
name=param.name, name=param.name,
@ -218,13 +240,17 @@ def get_typed_annotation(annotation: Any, globalns: Dict[str, Any]) -> Any:
def get_typed_return_annotation(call: Callable[..., Any]) -> Any: def get_typed_return_annotation(call: Callable[..., Any]) -> Any:
signature = inspect.signature(call) if sys.version_info >= (3, 10):
signature = inspect.signature(call, eval_str=True)
else:
signature = inspect.signature(call)
unwrapped = inspect.unwrap(call)
annotation = signature.return_annotation annotation = signature.return_annotation
if annotation is inspect.Signature.empty: if annotation is inspect.Signature.empty:
return None return None
globalns = getattr(call, "__globals__", {}) globalns = getattr(unwrapped, "__globals__", {})
return get_typed_annotation(annotation, globalns) return get_typed_annotation(annotation, globalns)
@ -233,7 +259,8 @@ def get_dependant(
path: str, path: str,
call: Callable[..., Any], call: Callable[..., Any],
name: Optional[str] = None, name: Optional[str] = None,
security_scopes: Optional[List[str]] = None, own_oauth_scopes: Optional[List[str]] = None,
parent_oauth_scopes: Optional[List[str]] = None,
use_cache: bool = True, use_cache: bool = True,
scope: Union[Literal["function", "request"], None] = None, scope: Union[Literal["function", "request"], None] = None,
) -> Dependant: ) -> Dependant:
@ -241,21 +268,15 @@ def get_dependant(
call=call, call=call,
name=name, name=name,
path=path, path=path,
security_scopes=security_scopes,
use_cache=use_cache, use_cache=use_cache,
scope=scope, scope=scope,
own_oauth_scopes=own_oauth_scopes,
parent_oauth_scopes=parent_oauth_scopes,
) )
current_scopes = (parent_oauth_scopes or []) + (own_oauth_scopes or [])
path_param_names = get_path_param_names(path) path_param_names = get_path_param_names(path)
endpoint_signature = get_typed_signature(call) endpoint_signature = get_typed_signature(call)
signature_params = endpoint_signature.parameters signature_params = endpoint_signature.parameters
if isinstance(call, SecurityBase):
use_scopes: List[str] = []
if isinstance(call, (OAuth2, OpenIdConnect)):
use_scopes = security_scopes or use_scopes
security_requirement = SecurityRequirement(
security_scheme=call, scopes=use_scopes
)
dependant.security_requirements.append(security_requirement)
for param_name, param in signature_params.items(): for param_name, param in signature_params.items():
is_path_param = param_name in path_param_names is_path_param = param_name in path_param_names
param_details = analyze_param( param_details = analyze_param(
@ -276,15 +297,16 @@ def get_dependant(
f'The dependency "{dependant.call.__name__}" has a scope of ' f'The dependency "{dependant.call.__name__}" has a scope of '
'"request", it cannot depend on dependencies with scope "function".' '"request", it cannot depend on dependencies with scope "function".'
) )
use_security_scopes = security_scopes or [] sub_own_oauth_scopes: List[str] = []
if isinstance(param_details.depends, params.Security): if isinstance(param_details.depends, params.Security):
if param_details.depends.scopes: if param_details.depends.scopes:
use_security_scopes.extend(param_details.depends.scopes) sub_own_oauth_scopes = list(param_details.depends.scopes)
sub_dependant = get_dependant( sub_dependant = get_dependant(
path=path, path=path,
call=param_details.depends.dependency, call=param_details.depends.dependency,
name=param_name, name=param_name,
security_scopes=use_security_scopes, own_oauth_scopes=sub_own_oauth_scopes,
parent_oauth_scopes=current_scopes,
use_cache=param_details.depends.use_cache, use_cache=param_details.depends.use_cache,
scope=param_details.depends.scope, scope=param_details.depends.scope,
) )
@ -547,10 +569,10 @@ async def _solve_generator(
*, dependant: Dependant, stack: AsyncExitStack, sub_values: Dict[str, Any] *, dependant: Dependant, stack: AsyncExitStack, sub_values: Dict[str, Any]
) -> Any: ) -> Any:
assert dependant.call assert dependant.call
if dependant.is_gen_callable: if dependant.is_async_gen_callable:
cm = contextmanager_in_threadpool(contextmanager(dependant.call)(**sub_values))
elif dependant.is_async_gen_callable:
cm = asynccontextmanager(dependant.call)(**sub_values) cm = asynccontextmanager(dependant.call)(**sub_values)
elif dependant.is_gen_callable:
cm = contextmanager_in_threadpool(contextmanager(dependant.call)(**sub_values))
return await stack.enter_async_context(cm) return await stack.enter_async_context(cm)
@ -610,7 +632,7 @@ async def solve_dependencies(
path=use_path, path=use_path,
call=call, call=call,
name=sub_dependant.name, name=sub_dependant.name,
security_scopes=sub_dependant.security_scopes, parent_oauth_scopes=sub_dependant.oauth_scopes,
scope=sub_dependant.scope, scope=sub_dependant.scope,
) )
@ -692,7 +714,7 @@ async def solve_dependencies(
values[dependant.response_param_name] = response values[dependant.response_param_name] = response
if dependant.security_scopes_param_name: if dependant.security_scopes_param_name:
values[dependant.security_scopes_param_name] = SecurityScopes( values[dependant.security_scopes_param_name] = SecurityScopes(
scopes=dependant.security_scopes scopes=dependant.oauth_scopes
) )
return SolvedDependency( return SolvedDependency(
values=values, values=values,
@ -788,13 +810,19 @@ def request_params_to_args(
) )
value = _get_multidict_value(field, received_params, alias=alias) value = _get_multidict_value(field, received_params, alias=alias)
if value is not None: if value is not None:
params_to_process[field.name] = value params_to_process[field.alias] = value
processed_keys.add(alias or field.alias) processed_keys.add(alias or field.alias)
processed_keys.add(field.name)
for key, value in received_params.items(): for key in received_params.keys():
if key not in processed_keys: if key not in processed_keys:
params_to_process[key] = value if hasattr(received_params, "getlist"):
value = received_params.getlist(key)
if isinstance(value, list) and (len(value) == 1):
params_to_process[key] = value[0]
else:
params_to_process[key] = value
else:
params_to_process[key] = received_params.get(key)
if single_not_embedded_field: if single_not_embedded_field:
field_info = first_field.field_info field_info = first_field.field_info
@ -911,9 +939,14 @@ async def _extract_form_body(
] ]
if value is not None: if value is not None:
values[field.alias] = value values[field.alias] = value
for key, value in received_body.items(): field_aliases = {field.alias for field in body_fields}
if key not in values: for key in received_body.keys():
values[key] = value if key not in field_aliases:
param_values = received_body.getlist(key)
if len(param_values) == 1:
values[key] = param_values[0]
else:
values[key] = param_values
return values return values

View File

@ -34,14 +34,14 @@ def isoformat(o: Union[datetime.date, datetime.time]) -> str:
return o.isoformat() return o.isoformat()
# Taken from Pydantic v1 as is # Adapted from Pydantic v1
# TODO: pv2 should this return strings instead? # TODO: pv2 should this return strings instead?
def decimal_encoder(dec_value: Decimal) -> Union[int, float]: def decimal_encoder(dec_value: Decimal) -> Union[int, float]:
""" """
Encodes a Decimal as int of there's no exponent, otherwise float Encodes a Decimal as int if there's no exponent, otherwise float
This is useful when we use ConstrainedDecimal to represent Numeric(x,0) This is useful when we use ConstrainedDecimal to represent Numeric(x,0)
where a integer (but not int typed) is used. Encoding this as a float where an integer (but not int typed) is used. Encoding this as a float
results in failed round-tripping between encode and parse. results in failed round-tripping between encode and parse.
Our Id type is a prime example of this. Our Id type is a prime example of this.
@ -50,8 +50,12 @@ def decimal_encoder(dec_value: Decimal) -> Union[int, float]:
>>> decimal_encoder(Decimal("1")) >>> decimal_encoder(Decimal("1"))
1 1
>>> decimal_encoder(Decimal("NaN"))
nan
""" """
if dec_value.as_tuple().exponent >= 0: # type: ignore[operator] exponent = dec_value.as_tuple().exponent
if isinstance(exponent, int) and exponent >= 0:
return int(dec_value) return int(dec_value)
else: else:
return float(dec_value) return float(dec_value)

View File

@ -79,16 +79,25 @@ def get_openapi_security_definitions(
flat_dependant: Dependant, flat_dependant: Dependant,
) -> Tuple[Dict[str, Any], List[Dict[str, Any]]]: ) -> Tuple[Dict[str, Any], List[Dict[str, Any]]]:
security_definitions = {} security_definitions = {}
operation_security = [] # Use a dict to merge scopes for same security scheme
for security_requirement in flat_dependant.security_requirements: operation_security_dict: Dict[str, List[str]] = {}
for security_dependency in flat_dependant._security_dependencies:
security_definition = jsonable_encoder( security_definition = jsonable_encoder(
security_requirement.security_scheme.model, security_dependency._security_scheme.model,
by_alias=True, by_alias=True,
exclude_none=True, exclude_none=True,
) )
security_name = security_requirement.security_scheme.scheme_name security_name = security_dependency._security_scheme.scheme_name
security_definitions[security_name] = security_definition security_definitions[security_name] = security_definition
operation_security.append({security_name: security_requirement.scopes}) # Merge scopes for the same security scheme
if security_name not in operation_security_dict:
operation_security_dict[security_name] = []
for scope in security_dependency.oauth_scopes or []:
if scope not in operation_security_dict[security_name]:
operation_security_dict[security_name].append(scope)
operation_security = [
{name: scopes} for name, scopes in operation_security_dict.items()
]
return security_definitions, operation_security return security_definitions, operation_security

View File

@ -3,7 +3,6 @@ import email.message
import functools import functools
import inspect import inspect
import json import json
import sys
from contextlib import AsyncExitStack, asynccontextmanager from contextlib import AsyncExitStack, asynccontextmanager
from enum import Enum, IntEnum from enum import Enum, IntEnum
from typing import ( from typing import (
@ -79,11 +78,6 @@ from starlette.types import AppType, ASGIApp, Lifespan, Receive, Scope, Send
from starlette.websockets import WebSocket from starlette.websockets import WebSocket
from typing_extensions import Annotated, deprecated from typing_extensions import Annotated, deprecated
if sys.version_info >= (3, 13): # pragma: no cover
from inspect import iscoroutinefunction
else: # pragma: no cover
from asyncio import iscoroutinefunction
# Copy of starlette.routing.request_response modified to include the # Copy of starlette.routing.request_response modified to include the
# dependencies' AsyncExitStack # dependencies' AsyncExitStack
@ -308,7 +302,7 @@ def get_request_handler(
embed_body_fields: bool = False, embed_body_fields: bool = False,
) -> Callable[[Request], Coroutine[Any, Any, Response]]: ) -> Callable[[Request], Coroutine[Any, Any, Response]]:
assert dependant.call is not None, "dependant.call must be a function" assert dependant.call is not None, "dependant.call must be a function"
is_coroutine = iscoroutinefunction(dependant.call) is_coroutine = dependant.is_coroutine_callable
is_body_form = body_field and isinstance( is_body_form = body_field and isinstance(
body_field.field_info, (params.Form, temp_pydantic_v1_params.Form) body_field.field_info, (params.Form, temp_pydantic_v1_params.Form)
) )

View File

@ -1,22 +1,52 @@
from typing import Optional from typing import Optional, Union
from annotated_doc import Doc from annotated_doc import Doc
from fastapi.openapi.models import APIKey, APIKeyIn from fastapi.openapi.models import APIKey, APIKeyIn
from fastapi.security.base import SecurityBase from fastapi.security.base import SecurityBase
from starlette.exceptions import HTTPException from starlette.exceptions import HTTPException
from starlette.requests import Request from starlette.requests import Request
from starlette.status import HTTP_403_FORBIDDEN from starlette.status import HTTP_401_UNAUTHORIZED
from typing_extensions import Annotated from typing_extensions import Annotated
class APIKeyBase(SecurityBase): class APIKeyBase(SecurityBase):
@staticmethod def __init__(
def check_api_key(api_key: Optional[str], auto_error: bool) -> Optional[str]: self,
location: APIKeyIn,
name: str,
description: Union[str, None],
scheme_name: Union[str, None],
auto_error: bool,
):
self.auto_error = auto_error
self.model: APIKey = APIKey(
**{"in": location},
name=name,
description=description,
)
self.scheme_name = scheme_name or self.__class__.__name__
def make_not_authenticated_error(self) -> HTTPException:
"""
The WWW-Authenticate header is not standardized for API Key authentication but
the HTTP specification requires that an error of 401 "Unauthorized" must
include a WWW-Authenticate header.
Ref: https://datatracker.ietf.org/doc/html/rfc9110#name-401-unauthorized
For this, this method sends a custom challenge `APIKey`.
"""
return HTTPException(
status_code=HTTP_401_UNAUTHORIZED,
detail="Not authenticated",
headers={"WWW-Authenticate": "APIKey"},
)
def check_api_key(self, api_key: Optional[str]) -> Optional[str]:
if not api_key: if not api_key:
if auto_error: if self.auto_error:
raise HTTPException( raise self.make_not_authenticated_error()
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
)
return None return None
return api_key return api_key
@ -100,17 +130,17 @@ class APIKeyQuery(APIKeyBase):
), ),
] = True, ] = True,
): ):
self.model: APIKey = APIKey( super().__init__(
**{"in": APIKeyIn.query}, location=APIKeyIn.query,
name=name, name=name,
scheme_name=scheme_name,
description=description, description=description,
auto_error=auto_error,
) )
self.scheme_name = scheme_name or self.__class__.__name__
self.auto_error = auto_error
async def __call__(self, request: Request) -> Optional[str]: async def __call__(self, request: Request) -> Optional[str]:
api_key = request.query_params.get(self.model.name) api_key = request.query_params.get(self.model.name)
return self.check_api_key(api_key, self.auto_error) return self.check_api_key(api_key)
class APIKeyHeader(APIKeyBase): class APIKeyHeader(APIKeyBase):
@ -188,17 +218,17 @@ class APIKeyHeader(APIKeyBase):
), ),
] = True, ] = True,
): ):
self.model: APIKey = APIKey( super().__init__(
**{"in": APIKeyIn.header}, location=APIKeyIn.header,
name=name, name=name,
scheme_name=scheme_name,
description=description, description=description,
auto_error=auto_error,
) )
self.scheme_name = scheme_name or self.__class__.__name__
self.auto_error = auto_error
async def __call__(self, request: Request) -> Optional[str]: async def __call__(self, request: Request) -> Optional[str]:
api_key = request.headers.get(self.model.name) api_key = request.headers.get(self.model.name)
return self.check_api_key(api_key, self.auto_error) return self.check_api_key(api_key)
class APIKeyCookie(APIKeyBase): class APIKeyCookie(APIKeyBase):
@ -276,14 +306,14 @@ class APIKeyCookie(APIKeyBase):
), ),
] = True, ] = True,
): ):
self.model: APIKey = APIKey( super().__init__(
**{"in": APIKeyIn.cookie}, location=APIKeyIn.cookie,
name=name, name=name,
scheme_name=scheme_name,
description=description, description=description,
auto_error=auto_error,
) )
self.scheme_name = scheme_name or self.__class__.__name__
self.auto_error = auto_error
async def __call__(self, request: Request) -> Optional[str]: async def __call__(self, request: Request) -> Optional[str]:
api_key = request.cookies.get(self.model.name) api_key = request.cookies.get(self.model.name)
return self.check_api_key(api_key, self.auto_error) return self.check_api_key(api_key)

View File

@ -1,6 +1,6 @@
import binascii import binascii
from base64 import b64decode from base64 import b64decode
from typing import Optional from typing import Dict, Optional
from annotated_doc import Doc from annotated_doc import Doc
from fastapi.exceptions import HTTPException from fastapi.exceptions import HTTPException
@ -10,7 +10,7 @@ from fastapi.security.base import SecurityBase
from fastapi.security.utils import get_authorization_scheme_param from fastapi.security.utils import get_authorization_scheme_param
from pydantic import BaseModel from pydantic import BaseModel
from starlette.requests import Request from starlette.requests import Request
from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN from starlette.status import HTTP_401_UNAUTHORIZED
from typing_extensions import Annotated from typing_extensions import Annotated
@ -76,10 +76,22 @@ class HTTPBase(SecurityBase):
description: Optional[str] = None, description: Optional[str] = None,
auto_error: bool = True, auto_error: bool = True,
): ):
self.model = HTTPBaseModel(scheme=scheme, description=description) self.model: HTTPBaseModel = HTTPBaseModel(
scheme=scheme, description=description
)
self.scheme_name = scheme_name or self.__class__.__name__ self.scheme_name = scheme_name or self.__class__.__name__
self.auto_error = auto_error self.auto_error = auto_error
def make_authenticate_headers(self) -> Dict[str, str]:
return {"WWW-Authenticate": f"{self.model.scheme.title()}"}
def make_not_authenticated_error(self) -> HTTPException:
return HTTPException(
status_code=HTTP_401_UNAUTHORIZED,
detail="Not authenticated",
headers=self.make_authenticate_headers(),
)
async def __call__( async def __call__(
self, request: Request self, request: Request
) -> Optional[HTTPAuthorizationCredentials]: ) -> Optional[HTTPAuthorizationCredentials]:
@ -87,9 +99,7 @@ class HTTPBase(SecurityBase):
scheme, credentials = get_authorization_scheme_param(authorization) scheme, credentials = get_authorization_scheme_param(authorization)
if not (authorization and scheme and credentials): if not (authorization and scheme and credentials):
if self.auto_error: if self.auto_error:
raise HTTPException( raise self.make_not_authenticated_error()
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
)
else: else:
return None return None
return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials) return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials)
@ -99,6 +109,8 @@ class HTTPBasic(HTTPBase):
""" """
HTTP Basic authentication. HTTP Basic authentication.
Ref: https://datatracker.ietf.org/doc/html/rfc7617
## Usage ## Usage
Create an instance object and use that object as the dependency in `Depends()`. Create an instance object and use that object as the dependency in `Depends()`.
@ -185,36 +197,28 @@ class HTTPBasic(HTTPBase):
self.realm = realm self.realm = realm
self.auto_error = auto_error self.auto_error = auto_error
def make_authenticate_headers(self) -> Dict[str, str]:
if self.realm:
return {"WWW-Authenticate": f'Basic realm="{self.realm}"'}
return {"WWW-Authenticate": "Basic"}
async def __call__( # type: ignore async def __call__( # type: ignore
self, request: Request self, request: Request
) -> Optional[HTTPBasicCredentials]: ) -> Optional[HTTPBasicCredentials]:
authorization = request.headers.get("Authorization") authorization = request.headers.get("Authorization")
scheme, param = get_authorization_scheme_param(authorization) scheme, param = get_authorization_scheme_param(authorization)
if self.realm:
unauthorized_headers = {"WWW-Authenticate": f'Basic realm="{self.realm}"'}
else:
unauthorized_headers = {"WWW-Authenticate": "Basic"}
if not authorization or scheme.lower() != "basic": if not authorization or scheme.lower() != "basic":
if self.auto_error: if self.auto_error:
raise HTTPException( raise self.make_not_authenticated_error()
status_code=HTTP_401_UNAUTHORIZED,
detail="Not authenticated",
headers=unauthorized_headers,
)
else: else:
return None return None
invalid_user_credentials_exc = HTTPException(
status_code=HTTP_401_UNAUTHORIZED,
detail="Invalid authentication credentials",
headers=unauthorized_headers,
)
try: try:
data = b64decode(param).decode("ascii") data = b64decode(param).decode("ascii")
except (ValueError, UnicodeDecodeError, binascii.Error): except (ValueError, UnicodeDecodeError, binascii.Error) as e:
raise invalid_user_credentials_exc # noqa: B904 raise self.make_not_authenticated_error() from e
username, separator, password = data.partition(":") username, separator, password = data.partition(":")
if not separator: if not separator:
raise invalid_user_credentials_exc raise self.make_not_authenticated_error()
return HTTPBasicCredentials(username=username, password=password) return HTTPBasicCredentials(username=username, password=password)
@ -306,17 +310,12 @@ class HTTPBearer(HTTPBase):
scheme, credentials = get_authorization_scheme_param(authorization) scheme, credentials = get_authorization_scheme_param(authorization)
if not (authorization and scheme and credentials): if not (authorization and scheme and credentials):
if self.auto_error: if self.auto_error:
raise HTTPException( raise self.make_not_authenticated_error()
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
)
else: else:
return None return None
if scheme.lower() != "bearer": if scheme.lower() != "bearer":
if self.auto_error: if self.auto_error:
raise HTTPException( raise self.make_not_authenticated_error()
status_code=HTTP_403_FORBIDDEN,
detail="Invalid authentication credentials",
)
else: else:
return None return None
return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials) return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials)
@ -326,6 +325,12 @@ class HTTPDigest(HTTPBase):
""" """
HTTP Digest authentication. HTTP Digest authentication.
**Warning**: this is only a stub to connect the components with OpenAPI in FastAPI,
but it doesn't implement the full Digest scheme, you would need to to subclass it
and implement it in your code.
Ref: https://datatracker.ietf.org/doc/html/rfc7616
## Usage ## Usage
Create an instance object and use that object as the dependency in `Depends()`. Create an instance object and use that object as the dependency in `Depends()`.
@ -408,17 +413,12 @@ class HTTPDigest(HTTPBase):
scheme, credentials = get_authorization_scheme_param(authorization) scheme, credentials = get_authorization_scheme_param(authorization)
if not (authorization and scheme and credentials): if not (authorization and scheme and credentials):
if self.auto_error: if self.auto_error:
raise HTTPException( raise self.make_not_authenticated_error()
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
)
else: else:
return None return None
if scheme.lower() != "digest": if scheme.lower() != "digest":
if self.auto_error: if self.auto_error:
raise HTTPException( raise self.make_not_authenticated_error()
status_code=HTTP_403_FORBIDDEN,
detail="Invalid authentication credentials",
)
else: else:
return None return None
return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials) return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials)

View File

@ -8,7 +8,7 @@ from fastapi.param_functions import Form
from fastapi.security.base import SecurityBase from fastapi.security.base import SecurityBase
from fastapi.security.utils import get_authorization_scheme_param from fastapi.security.utils import get_authorization_scheme_param
from starlette.requests import Request from starlette.requests import Request
from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN from starlette.status import HTTP_401_UNAUTHORIZED
# TODO: import from typing when deprecating Python 3.9 # TODO: import from typing when deprecating Python 3.9
from typing_extensions import Annotated from typing_extensions import Annotated
@ -377,13 +377,33 @@ class OAuth2(SecurityBase):
self.scheme_name = scheme_name or self.__class__.__name__ self.scheme_name = scheme_name or self.__class__.__name__
self.auto_error = auto_error self.auto_error = auto_error
def make_not_authenticated_error(self) -> HTTPException:
"""
The OAuth 2 specification doesn't define the challenge that should be used,
because a `Bearer` token is not really the only option to authenticate.
But declaring any other authentication challenge would be application-specific
as it's not defined in the specification.
For practical reasons, this method uses the `Bearer` challenge by default, as
it's probably the most common one.
If you are implementing an OAuth2 authentication scheme other than the provided
ones in FastAPI (based on bearer tokens), you might want to override this.
Ref: https://datatracker.ietf.org/doc/html/rfc6749
"""
return HTTPException(
status_code=HTTP_401_UNAUTHORIZED,
detail="Not authenticated",
headers={"WWW-Authenticate": "Bearer"},
)
async def __call__(self, request: Request) -> Optional[str]: async def __call__(self, request: Request) -> Optional[str]:
authorization = request.headers.get("Authorization") authorization = request.headers.get("Authorization")
if not authorization: if not authorization:
if self.auto_error: if self.auto_error:
raise HTTPException( raise self.make_not_authenticated_error()
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
)
else: else:
return None return None
return authorization return authorization
@ -491,11 +511,7 @@ class OAuth2PasswordBearer(OAuth2):
scheme, param = get_authorization_scheme_param(authorization) scheme, param = get_authorization_scheme_param(authorization)
if not authorization or scheme.lower() != "bearer": if not authorization or scheme.lower() != "bearer":
if self.auto_error: if self.auto_error:
raise HTTPException( raise self.make_not_authenticated_error()
status_code=HTTP_401_UNAUTHORIZED,
detail="Not authenticated",
headers={"WWW-Authenticate": "Bearer"},
)
else: else:
return None return None
return param return param
@ -601,11 +617,7 @@ class OAuth2AuthorizationCodeBearer(OAuth2):
scheme, param = get_authorization_scheme_param(authorization) scheme, param = get_authorization_scheme_param(authorization)
if not authorization or scheme.lower() != "bearer": if not authorization or scheme.lower() != "bearer":
if self.auto_error: if self.auto_error:
raise HTTPException( raise self.make_not_authenticated_error()
status_code=HTTP_401_UNAUTHORIZED,
detail="Not authenticated",
headers={"WWW-Authenticate": "Bearer"},
)
else: else:
return None # pragma: nocover return None # pragma: nocover
return param return param

View File

@ -5,7 +5,7 @@ from fastapi.openapi.models import OpenIdConnect as OpenIdConnectModel
from fastapi.security.base import SecurityBase from fastapi.security.base import SecurityBase
from starlette.exceptions import HTTPException from starlette.exceptions import HTTPException
from starlette.requests import Request from starlette.requests import Request
from starlette.status import HTTP_403_FORBIDDEN from starlette.status import HTTP_401_UNAUTHORIZED
from typing_extensions import Annotated from typing_extensions import Annotated
@ -13,6 +13,11 @@ class OpenIdConnect(SecurityBase):
""" """
OpenID Connect authentication class. An instance of it would be used as a OpenID Connect authentication class. An instance of it would be used as a
dependency. dependency.
**Warning**: this is only a stub to connect the components with OpenAPI in FastAPI,
but it doesn't implement the full OpenIdConnect scheme, for example, it doesn't use
the OpenIDConnect URL. You would need to to subclass it and implement it in your
code.
""" """
def __init__( def __init__(
@ -73,13 +78,18 @@ class OpenIdConnect(SecurityBase):
self.scheme_name = scheme_name or self.__class__.__name__ self.scheme_name = scheme_name or self.__class__.__name__
self.auto_error = auto_error self.auto_error = auto_error
def make_not_authenticated_error(self) -> HTTPException:
return HTTPException(
status_code=HTTP_401_UNAUTHORIZED,
detail="Not authenticated",
headers={"WWW-Authenticate": "Bearer"},
)
async def __call__(self, request: Request) -> Optional[str]: async def __call__(self, request: Request) -> Optional[str]:
authorization = request.headers.get("Authorization") authorization = request.headers.get("Authorization")
if not authorization: if not authorization:
if self.auto_error: if self.auto_error:
raise HTTPException( raise self.make_not_authenticated_error()
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
)
else: else:
return None return None
return authorization return authorization

View File

@ -110,7 +110,9 @@ def create_model_field(
try: try:
return v1.ModelField(**v1_kwargs) # type: ignore[no-any-return] return v1.ModelField(**v1_kwargs) # type: ignore[no-any-return]
except RuntimeError: except RuntimeError:
raise fastapi.exceptions.FastAPIError(_invalid_args_message) from None raise fastapi.exceptions.FastAPIError(
_invalid_args_message.format(type_=type_)
) from None
elif PYDANTIC_V2: elif PYDANTIC_V2:
from ._compat import v2 from ._compat import v2
@ -121,7 +123,9 @@ def create_model_field(
try: try:
return v2.ModelField(**kwargs) # type: ignore[return-value,arg-type] return v2.ModelField(**kwargs) # type: ignore[return-value,arg-type]
except PydanticSchemaGenerationError: except PydanticSchemaGenerationError:
raise fastapi.exceptions.FastAPIError(_invalid_args_message) from None raise fastapi.exceptions.FastAPIError(
_invalid_args_message.format(type_=type_)
) from None
# Pydantic v2 is not installed, but it's not a Pydantic v1 ModelField, it could be # Pydantic v2 is not installed, but it's not a Pydantic v1 ModelField, it could be
# a Pydantic v1 type, like a constrained int # a Pydantic v1 type, like a constrained int
from fastapi._compat import v1 from fastapi._compat import v1
@ -129,7 +133,9 @@ def create_model_field(
try: try:
return v1.ModelField(**v1_kwargs) # type: ignore[no-any-return] return v1.ModelField(**v1_kwargs) # type: ignore[no-any-return]
except RuntimeError: except RuntimeError:
raise fastapi.exceptions.FastAPIError(_invalid_args_message) from None raise fastapi.exceptions.FastAPIError(
_invalid_args_message.format(type_=type_)
) from None
def create_cloned_field( def create_cloned_field(

View File

@ -17,5 +17,5 @@ griffe-warnings-deprecated==1.1.0
# For griffe, it formats with black # For griffe, it formats with black
black==25.1.0 black==25.1.0
mkdocs-macros-plugin==1.4.1 mkdocs-macros-plugin==1.4.1
markdown-include-variants==0.0.5 markdown-include-variants==0.0.7
python-slugify==8.0.4 python-slugify==8.0.4

View File

@ -0,0 +1,9 @@
from pydantic import BaseModel
def forwardref_method(input: "ForwardRefModel") -> "ForwardRefModel":
return ForwardRefModel(x=input.x + 1)
class ForwardRefModel(BaseModel):
x: int = 0

View File

@ -14,7 +14,7 @@ from fastapi.testclient import TestClient
from pydantic import BaseModel, ConfigDict from pydantic import BaseModel, ConfigDict
from pydantic.fields import FieldInfo from pydantic.fields import FieldInfo
from .utils import needs_py_lt_314, needs_pydanticv2 from .utils import needs_py310, needs_py_lt_314, needs_pydanticv2
@needs_pydanticv2 @needs_pydanticv2
@ -136,6 +136,43 @@ def test_is_uploadfile_sequence_annotation():
assert is_uploadfile_sequence_annotation(Union[List[str], List[UploadFile]]) assert is_uploadfile_sequence_annotation(Union[List[str], List[UploadFile]])
@needs_pydanticv2
def test_serialize_sequence_value_with_optional_list():
"""Test that serialize_sequence_value handles optional lists correctly."""
from fastapi._compat import v2
field_info = FieldInfo(annotation=Union[List[str], None])
field = v2.ModelField(name="items", field_info=field_info)
result = v2.serialize_sequence_value(field=field, value=["a", "b", "c"])
assert result == ["a", "b", "c"]
assert isinstance(result, list)
@needs_pydanticv2
@needs_py310
def test_serialize_sequence_value_with_optional_list_pipe_union():
"""Test that serialize_sequence_value handles optional lists correctly (with new syntax)."""
from fastapi._compat import v2
field_info = FieldInfo(annotation=list[str] | None)
field = v2.ModelField(name="items", field_info=field_info)
result = v2.serialize_sequence_value(field=field, value=["a", "b", "c"])
assert result == ["a", "b", "c"]
assert isinstance(result, list)
@needs_pydanticv2
def test_serialize_sequence_value_with_none_first_in_union():
"""Test that serialize_sequence_value handles Union[None, List[...]] correctly."""
from fastapi._compat import v2
field_info = FieldInfo(annotation=Union[None, List[str]])
field = v2.ModelField(name="items", field_info=field_info)
result = v2.serialize_sequence_value(field=field, value=["x", "y"])
assert result == ["x", "y"]
assert isinstance(result, list)
@needs_py_lt_314 @needs_py_lt_314
def test_is_pv1_scalar_field(): def test_is_pv1_scalar_field():
from fastapi._compat import v1 from fastapi._compat import v1

View File

@ -6,8 +6,9 @@ from .utils import needs_pydanticv2
@pytest.fixture(name="client") @pytest.fixture(name="client")
def get_client(): def get_client(request):
app = FastAPI() separate_input_output_schemas = request.param
app = FastAPI(separate_input_output_schemas=separate_input_output_schemas)
from pydantic import BaseModel, computed_field from pydantic import BaseModel, computed_field
@ -32,6 +33,7 @@ def get_client():
return client return client
@pytest.mark.parametrize("client", [True, False], indirect=True)
@pytest.mark.parametrize("path", ["/", "/responses"]) @pytest.mark.parametrize("path", ["/", "/responses"])
@needs_pydanticv2 @needs_pydanticv2
def test_get(client: TestClient, path: str): def test_get(client: TestClient, path: str):
@ -40,6 +42,7 @@ def test_get(client: TestClient, path: str):
assert response.json() == {"width": 3, "length": 4, "area": 12} assert response.json() == {"width": 3, "length": 4, "area": 12}
@pytest.mark.parametrize("client", [True, False], indirect=True)
@needs_pydanticv2 @needs_pydanticv2
def test_openapi_schema(client: TestClient): def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json") response = client.get("/openapi.json")

View File

@ -0,0 +1,251 @@
from functools import partial
from typing import AsyncGenerator, Generator
import pytest
from fastapi import Depends, FastAPI
from fastapi.testclient import TestClient
from typing_extensions import Annotated
app = FastAPI()
def function_dependency(value: str) -> str:
return value
async def async_function_dependency(value: str) -> str:
return value
def gen_dependency(value: str) -> Generator[str, None, None]:
yield value
async def async_gen_dependency(value: str) -> AsyncGenerator[str, None]:
yield value
class CallableDependency:
def __call__(self, value: str) -> str:
return value
class CallableGenDependency:
def __call__(self, value: str) -> Generator[str, None, None]:
yield value
class AsyncCallableDependency:
async def __call__(self, value: str) -> str:
return value
class AsyncCallableGenDependency:
async def __call__(self, value: str) -> AsyncGenerator[str, None]:
yield value
class MethodsDependency:
def synchronous(self, value: str) -> str:
return value
async def asynchronous(self, value: str) -> str:
return value
def synchronous_gen(self, value: str) -> Generator[str, None, None]:
yield value
async def asynchronous_gen(self, value: str) -> AsyncGenerator[str, None]:
yield value
callable_dependency = CallableDependency()
callable_gen_dependency = CallableGenDependency()
async_callable_dependency = AsyncCallableDependency()
async_callable_gen_dependency = AsyncCallableGenDependency()
methods_dependency = MethodsDependency()
@app.get("/partial-function-dependency")
async def get_partial_function_dependency(
value: Annotated[
str, Depends(partial(function_dependency, "partial-function-dependency"))
],
) -> str:
return value
@app.get("/partial-async-function-dependency")
async def get_partial_async_function_dependency(
value: Annotated[
str,
Depends(
partial(async_function_dependency, "partial-async-function-dependency")
),
],
) -> str:
return value
@app.get("/partial-gen-dependency")
async def get_partial_gen_dependency(
value: Annotated[str, Depends(partial(gen_dependency, "partial-gen-dependency"))],
) -> str:
return value
@app.get("/partial-async-gen-dependency")
async def get_partial_async_gen_dependency(
value: Annotated[
str, Depends(partial(async_gen_dependency, "partial-async-gen-dependency"))
],
) -> str:
return value
@app.get("/partial-callable-dependency")
async def get_partial_callable_dependency(
value: Annotated[
str, Depends(partial(callable_dependency, "partial-callable-dependency"))
],
) -> str:
return value
@app.get("/partial-callable-gen-dependency")
async def get_partial_callable_gen_dependency(
value: Annotated[
str,
Depends(partial(callable_gen_dependency, "partial-callable-gen-dependency")),
],
) -> str:
return value
@app.get("/partial-async-callable-dependency")
async def get_partial_async_callable_dependency(
value: Annotated[
str,
Depends(
partial(async_callable_dependency, "partial-async-callable-dependency")
),
],
) -> str:
return value
@app.get("/partial-async-callable-gen-dependency")
async def get_partial_async_callable_gen_dependency(
value: Annotated[
str,
Depends(
partial(
async_callable_gen_dependency, "partial-async-callable-gen-dependency"
)
),
],
) -> str:
return value
@app.get("/partial-synchronous-method-dependency")
async def get_partial_synchronous_method_dependency(
value: Annotated[
str,
Depends(
partial(
methods_dependency.synchronous, "partial-synchronous-method-dependency"
)
),
],
) -> str:
return value
@app.get("/partial-synchronous-method-gen-dependency")
async def get_partial_synchronous_method_gen_dependency(
value: Annotated[
str,
Depends(
partial(
methods_dependency.synchronous_gen,
"partial-synchronous-method-gen-dependency",
)
),
],
) -> str:
return value
@app.get("/partial-asynchronous-method-dependency")
async def get_partial_asynchronous_method_dependency(
value: Annotated[
str,
Depends(
partial(
methods_dependency.asynchronous,
"partial-asynchronous-method-dependency",
)
),
],
) -> str:
return value
@app.get("/partial-asynchronous-method-gen-dependency")
async def get_partial_asynchronous_method_gen_dependency(
value: Annotated[
str,
Depends(
partial(
methods_dependency.asynchronous_gen,
"partial-asynchronous-method-gen-dependency",
)
),
],
) -> str:
return value
client = TestClient(app)
@pytest.mark.parametrize(
"route,value",
[
("/partial-function-dependency", "partial-function-dependency"),
(
"/partial-async-function-dependency",
"partial-async-function-dependency",
),
("/partial-gen-dependency", "partial-gen-dependency"),
("/partial-async-gen-dependency", "partial-async-gen-dependency"),
("/partial-callable-dependency", "partial-callable-dependency"),
("/partial-callable-gen-dependency", "partial-callable-gen-dependency"),
("/partial-async-callable-dependency", "partial-async-callable-dependency"),
(
"/partial-async-callable-gen-dependency",
"partial-async-callable-gen-dependency",
),
(
"/partial-synchronous-method-dependency",
"partial-synchronous-method-dependency",
),
(
"/partial-synchronous-method-gen-dependency",
"partial-synchronous-method-gen-dependency",
),
(
"/partial-asynchronous-method-dependency",
"partial-asynchronous-method-dependency",
),
(
"/partial-asynchronous-method-gen-dependency",
"partial-asynchronous-method-gen-dependency",
),
],
)
def test_dependency_types_with_partial(route: str, value: str) -> None:
response = client.get(route)
assert response.status_code == 200, response.text
assert response.json() == value

View File

@ -0,0 +1,449 @@
import inspect
import sys
from functools import wraps
from typing import AsyncGenerator, Generator
import pytest
from fastapi import Depends, FastAPI
from fastapi.concurrency import iterate_in_threadpool, run_in_threadpool
from fastapi.testclient import TestClient
if sys.version_info >= (3, 13): # pragma: no cover
from inspect import iscoroutinefunction
else: # pragma: no cover
from asyncio import iscoroutinefunction
def noop_wrap(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
def noop_wrap_async(func):
if inspect.isgeneratorfunction(func):
@wraps(func)
async def gen_wrapper(*args, **kwargs):
async for item in iterate_in_threadpool(func(*args, **kwargs)):
yield item
return gen_wrapper
elif inspect.isasyncgenfunction(func):
@wraps(func)
async def async_gen_wrapper(*args, **kwargs):
async for item in func(*args, **kwargs):
yield item
return async_gen_wrapper
@wraps(func)
async def wrapper(*args, **kwargs):
if inspect.isroutine(func) and iscoroutinefunction(func):
return await func(*args, **kwargs)
if inspect.isclass(func):
return await run_in_threadpool(func, *args, **kwargs)
dunder_call = getattr(func, "__call__", None) # noqa: B004
if iscoroutinefunction(dunder_call):
return await dunder_call(*args, **kwargs)
return await run_in_threadpool(func, *args, **kwargs)
return wrapper
class ClassInstanceDep:
def __call__(self):
return True
class_instance_dep = ClassInstanceDep()
wrapped_class_instance_dep = noop_wrap(class_instance_dep)
wrapped_class_instance_dep_async_wrapper = noop_wrap_async(class_instance_dep)
class ClassInstanceGenDep:
def __call__(self):
yield True
class_instance_gen_dep = ClassInstanceGenDep()
wrapped_class_instance_gen_dep = noop_wrap(class_instance_gen_dep)
class ClassInstanceWrappedDep:
@noop_wrap
def __call__(self):
return True
class_instance_wrapped_dep = ClassInstanceWrappedDep()
class ClassInstanceWrappedAsyncDep:
@noop_wrap_async
def __call__(self):
return True
class_instance_wrapped_async_dep = ClassInstanceWrappedAsyncDep()
class ClassInstanceWrappedGenDep:
@noop_wrap
def __call__(self):
yield True
class_instance_wrapped_gen_dep = ClassInstanceWrappedGenDep()
class ClassInstanceWrappedAsyncGenDep:
@noop_wrap_async
def __call__(self):
yield True
class_instance_wrapped_async_gen_dep = ClassInstanceWrappedAsyncGenDep()
class ClassDep:
def __init__(self):
self.value = True
wrapped_class_dep = noop_wrap(ClassDep)
wrapped_class_dep_async_wrapper = noop_wrap_async(ClassDep)
class ClassInstanceAsyncDep:
async def __call__(self):
return True
class_instance_async_dep = ClassInstanceAsyncDep()
wrapped_class_instance_async_dep = noop_wrap(class_instance_async_dep)
wrapped_class_instance_async_dep_async_wrapper = noop_wrap_async(
class_instance_async_dep
)
class ClassInstanceAsyncGenDep:
async def __call__(self):
yield True
class_instance_async_gen_dep = ClassInstanceAsyncGenDep()
wrapped_class_instance_async_gen_dep = noop_wrap(class_instance_async_gen_dep)
class ClassInstanceAsyncWrappedDep:
@noop_wrap
async def __call__(self):
return True
class_instance_async_wrapped_dep = ClassInstanceAsyncWrappedDep()
class ClassInstanceAsyncWrappedAsyncDep:
@noop_wrap_async
async def __call__(self):
return True
class_instance_async_wrapped_async_dep = ClassInstanceAsyncWrappedAsyncDep()
class ClassInstanceAsyncWrappedGenDep:
@noop_wrap
async def __call__(self):
yield True
class_instance_async_wrapped_gen_dep = ClassInstanceAsyncWrappedGenDep()
class ClassInstanceAsyncWrappedGenAsyncDep:
@noop_wrap_async
async def __call__(self):
yield True
class_instance_async_wrapped_gen_async_dep = ClassInstanceAsyncWrappedGenAsyncDep()
app = FastAPI()
# Sync wrapper
@noop_wrap
def wrapped_dependency() -> bool:
return True
@noop_wrap
def wrapped_gen_dependency() -> Generator[bool, None, None]:
yield True
@noop_wrap
async def async_wrapped_dependency() -> bool:
return True
@noop_wrap
async def async_wrapped_gen_dependency() -> AsyncGenerator[bool, None]:
yield True
@app.get("/wrapped-dependency/")
async def get_wrapped_dependency(value: bool = Depends(wrapped_dependency)):
return value
@app.get("/wrapped-gen-dependency/")
async def get_wrapped_gen_dependency(value: bool = Depends(wrapped_gen_dependency)):
return value
@app.get("/async-wrapped-dependency/")
async def get_async_wrapped_dependency(value: bool = Depends(async_wrapped_dependency)):
return value
@app.get("/async-wrapped-gen-dependency/")
async def get_async_wrapped_gen_dependency(
value: bool = Depends(async_wrapped_gen_dependency),
):
return value
@app.get("/wrapped-class-instance-dependency/")
async def get_wrapped_class_instance_dependency(
value: bool = Depends(wrapped_class_instance_dep),
):
return value
@app.get("/wrapped-class-instance-async-dependency/")
async def get_wrapped_class_instance_async_dependency(
value: bool = Depends(wrapped_class_instance_async_dep),
):
return value
@app.get("/wrapped-class-instance-gen-dependency/")
async def get_wrapped_class_instance_gen_dependency(
value: bool = Depends(wrapped_class_instance_gen_dep),
):
return value
@app.get("/wrapped-class-instance-async-gen-dependency/")
async def get_wrapped_class_instance_async_gen_dependency(
value: bool = Depends(wrapped_class_instance_async_gen_dep),
):
return value
@app.get("/class-instance-wrapped-dependency/")
async def get_class_instance_wrapped_dependency(
value: bool = Depends(class_instance_wrapped_dep),
):
return value
@app.get("/class-instance-wrapped-async-dependency/")
async def get_class_instance_wrapped_async_dependency(
value: bool = Depends(class_instance_wrapped_async_dep),
):
return value
@app.get("/class-instance-async-wrapped-dependency/")
async def get_class_instance_async_wrapped_dependency(
value: bool = Depends(class_instance_async_wrapped_dep),
):
return value
@app.get("/class-instance-async-wrapped-async-dependency/")
async def get_class_instance_async_wrapped_async_dependency(
value: bool = Depends(class_instance_async_wrapped_async_dep),
):
return value
@app.get("/class-instance-wrapped-gen-dependency/")
async def get_class_instance_wrapped_gen_dependency(
value: bool = Depends(class_instance_wrapped_gen_dep),
):
return value
@app.get("/class-instance-wrapped-async-gen-dependency/")
async def get_class_instance_wrapped_async_gen_dependency(
value: bool = Depends(class_instance_wrapped_async_gen_dep),
):
return value
@app.get("/class-instance-async-wrapped-gen-dependency/")
async def get_class_instance_async_wrapped_gen_dependency(
value: bool = Depends(class_instance_async_wrapped_gen_dep),
):
return value
@app.get("/class-instance-async-wrapped-gen-async-dependency/")
async def get_class_instance_async_wrapped_gen_async_dependency(
value: bool = Depends(class_instance_async_wrapped_gen_async_dep),
):
return value
@app.get("/wrapped-class-dependency/")
async def get_wrapped_class_dependency(value: ClassDep = Depends(wrapped_class_dep)):
return value.value
@app.get("/wrapped-endpoint/")
@noop_wrap
def get_wrapped_endpoint():
return True
@app.get("/async-wrapped-endpoint/")
@noop_wrap
async def get_async_wrapped_endpoint():
return True
# Async wrapper
@noop_wrap_async
def wrapped_dependency_async_wrapper() -> bool:
return True
@noop_wrap_async
def wrapped_gen_dependency_async_wrapper() -> Generator[bool, None, None]:
yield True
@noop_wrap_async
async def async_wrapped_dependency_async_wrapper() -> bool:
return True
@noop_wrap_async
async def async_wrapped_gen_dependency_async_wrapper() -> AsyncGenerator[bool, None]:
yield True
@app.get("/wrapped-dependency-async-wrapper/")
async def get_wrapped_dependency_async_wrapper(
value: bool = Depends(wrapped_dependency_async_wrapper),
):
return value
@app.get("/wrapped-gen-dependency-async-wrapper/")
async def get_wrapped_gen_dependency_async_wrapper(
value: bool = Depends(wrapped_gen_dependency_async_wrapper),
):
return value
@app.get("/async-wrapped-dependency-async-wrapper/")
async def get_async_wrapped_dependency_async_wrapper(
value: bool = Depends(async_wrapped_dependency_async_wrapper),
):
return value
@app.get("/async-wrapped-gen-dependency-async-wrapper/")
async def get_async_wrapped_gen_dependency_async_wrapper(
value: bool = Depends(async_wrapped_gen_dependency_async_wrapper),
):
return value
@app.get("/wrapped-class-instance-dependency-async-wrapper/")
async def get_wrapped_class_instance_dependency_async_wrapper(
value: bool = Depends(wrapped_class_instance_dep_async_wrapper),
):
return value
@app.get("/wrapped-class-instance-async-dependency-async-wrapper/")
async def get_wrapped_class_instance_async_dependency_async_wrapper(
value: bool = Depends(wrapped_class_instance_async_dep_async_wrapper),
):
return value
@app.get("/wrapped-class-dependency-async-wrapper/")
async def get_wrapped_class_dependency_async_wrapper(
value: ClassDep = Depends(wrapped_class_dep_async_wrapper),
):
return value.value
@app.get("/wrapped-endpoint-async-wrapper/")
@noop_wrap_async
def get_wrapped_endpoint_async_wrapper():
return True
@app.get("/async-wrapped-endpoint-async-wrapper/")
@noop_wrap_async
async def get_async_wrapped_endpoint_async_wrapper():
return True
client = TestClient(app)
@pytest.mark.parametrize(
"route",
[
"/wrapped-dependency/",
"/wrapped-gen-dependency/",
"/async-wrapped-dependency/",
"/async-wrapped-gen-dependency/",
"/wrapped-class-instance-dependency/",
"/wrapped-class-instance-async-dependency/",
"/wrapped-class-instance-gen-dependency/",
"/wrapped-class-instance-async-gen-dependency/",
"/class-instance-wrapped-dependency/",
"/class-instance-wrapped-async-dependency/",
"/class-instance-async-wrapped-dependency/",
"/class-instance-async-wrapped-async-dependency/",
"/class-instance-wrapped-gen-dependency/",
"/class-instance-wrapped-async-gen-dependency/",
"/class-instance-async-wrapped-gen-dependency/",
"/class-instance-async-wrapped-gen-async-dependency/",
"/wrapped-class-dependency/",
"/wrapped-endpoint/",
"/async-wrapped-endpoint/",
"/wrapped-dependency-async-wrapper/",
"/wrapped-gen-dependency-async-wrapper/",
"/async-wrapped-dependency-async-wrapper/",
"/async-wrapped-gen-dependency-async-wrapper/",
"/wrapped-class-instance-dependency-async-wrapper/",
"/wrapped-class-instance-async-dependency-async-wrapper/",
"/wrapped-class-dependency-async-wrapper/",
"/wrapped-endpoint-async-wrapper/",
"/async-wrapped-endpoint-async-wrapper/",
],
)
def test_class_dependency(route):
response = client.get(route)
assert response.status_code == 200, response.text
assert response.json() is True

View File

@ -1,3 +1,4 @@
import os
import subprocess import subprocess
import sys import sys
from unittest.mock import patch from unittest.mock import patch
@ -20,6 +21,7 @@ def test_fastapi_cli():
], ],
capture_output=True, capture_output=True,
encoding="utf-8", encoding="utf-8",
env={**os.environ, "PYTHONIOENCODING": "utf-8"},
) )
assert result.returncode == 1, result.stdout assert result.returncode == 1, result.stdout
assert "Path does not exist non_existent_file.py" in result.stdout assert "Path does not exist non_existent_file.py" in result.stdout

View File

@ -0,0 +1,35 @@
from typing import Optional
from fastapi import FastAPI, File, Form
from starlette.testclient import TestClient
from typing_extensions import Annotated
app = FastAPI()
@app.post("/urlencoded")
async def post_url_encoded(age: Annotated[Optional[int], Form()] = None):
return age
@app.post("/multipart")
async def post_multi_part(
age: Annotated[Optional[int], Form()] = None,
file: Annotated[Optional[bytes], File()] = None,
):
return {"file": file, "age": age}
client = TestClient(app)
def test_form_default_url_encoded():
response = client.post("/urlencoded", data={"age": ""})
assert response.status_code == 200
assert response.text == "null"
def test_form_default_multi_part():
response = client.post("/multipart", data={"age": ""})
assert response.status_code == 200
assert response.json() == {"file": None, "age": None}

View File

@ -2,6 +2,7 @@ from typing import List, Optional
from dirty_equals import IsDict from dirty_equals import IsDict
from fastapi import FastAPI, Form from fastapi import FastAPI, Form
from fastapi._compat import PYDANTIC_V2
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from typing_extensions import Annotated from typing_extensions import Annotated
@ -17,11 +18,27 @@ class FormModel(BaseModel):
alias_with: str = Field(alias="with", default="nothing") alias_with: str = Field(alias="with", default="nothing")
class FormModelExtraAllow(BaseModel):
param: str
if PYDANTIC_V2:
model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
@app.post("/form/") @app.post("/form/")
def post_form(user: Annotated[FormModel, Form()]): def post_form(user: Annotated[FormModel, Form()]):
return user return user
@app.post("/form-extra-allow/")
def post_form_extra_allow(params: Annotated[FormModelExtraAllow, Form()]):
return params
client = TestClient(app) client = TestClient(app)
@ -131,3 +148,33 @@ def test_no_data():
] ]
} }
) )
def test_extra_param_single():
response = client.post(
"/form-extra-allow/",
data={
"param": "123",
"extra_param": "456",
},
)
assert response.status_code == 200, response.text
assert response.json() == {
"param": "123",
"extra_param": "456",
}
def test_extra_param_list():
response = client.post(
"/form-extra-allow/",
data={
"param": "123",
"extra_params": ["456", "789"],
},
)
assert response.status_code == 200, response.text
assert response.json() == {
"param": "123",
"extra_params": ["456", "789"],
}

View File

@ -3,6 +3,7 @@ from dataclasses import dataclass
from datetime import datetime, timezone from datetime import datetime, timezone
from decimal import Decimal from decimal import Decimal
from enum import Enum from enum import Enum
from math import isinf, isnan
from pathlib import PurePath, PurePosixPath, PureWindowsPath from pathlib import PurePath, PurePosixPath, PureWindowsPath
from typing import Optional from typing import Optional
@ -306,6 +307,20 @@ def test_decimal_encoder_int():
assert jsonable_encoder(data) == {"value": 2} assert jsonable_encoder(data) == {"value": 2}
@needs_pydanticv2
def test_decimal_encoder_nan():
data = {"value": Decimal("NaN")}
assert isnan(jsonable_encoder(data)["value"])
@needs_pydanticv2
def test_decimal_encoder_infinity():
data = {"value": Decimal("Infinity")}
assert isinf(jsonable_encoder(data)["value"])
data = {"value": Decimal("-Infinity")}
assert isinf(jsonable_encoder(data)["value"])
def test_encode_deque_encodes_child_models(): def test_encode_deque_encodes_child_models():
class Model(BaseModel): class Model(BaseModel):
test: str test: str

View File

@ -0,0 +1,30 @@
from typing import List, Optional
from fastapi import FastAPI, File
from fastapi.testclient import TestClient
app = FastAPI()
@app.post("/files")
async def upload_files(files: Optional[List[bytes]] = File(None)):
if files is None:
return {"files_count": 0}
return {"files_count": len(files), "sizes": [len(f) for f in files]}
def test_optional_bytes_list():
client = TestClient(app)
response = client.post(
"/files",
files=[("files", b"content1"), ("files", b"content2")],
)
assert response.status_code == 200
assert response.json() == {"files_count": 2, "sizes": [8, 8]}
def test_optional_bytes_list_no_files():
client = TestClient(app)
response = client.post("/files")
assert response.status_code == 200
assert response.json() == {"files_count": 0}

View File

@ -0,0 +1,111 @@
from fastapi import Cookie, FastAPI, Header, Query
from fastapi._compat import PYDANTIC_V2
from fastapi.testclient import TestClient
from pydantic import BaseModel
app = FastAPI()
class Model(BaseModel):
param: str
if PYDANTIC_V2:
model_config = {"extra": "allow"}
else:
class Config:
extra = "allow"
@app.get("/query")
async def query_model_with_extra(data: Model = Query()):
return data
@app.get("/header")
async def header_model_with_extra(data: Model = Header()):
return data
@app.get("/cookie")
async def cookies_model_with_extra(data: Model = Cookie()):
return data
def test_query_pass_extra_list():
client = TestClient(app)
resp = client.get(
"/query",
params={
"param": "123",
"param2": ["456", "789"], # Pass a list of values as extra parameter
},
)
assert resp.status_code == 200
assert resp.json() == {
"param": "123",
"param2": ["456", "789"],
}
def test_query_pass_extra_single():
client = TestClient(app)
resp = client.get(
"/query",
params={
"param": "123",
"param2": "456",
},
)
assert resp.status_code == 200
assert resp.json() == {
"param": "123",
"param2": "456",
}
def test_header_pass_extra_list():
client = TestClient(app)
resp = client.get(
"/header",
headers=[
("param", "123"),
("param2", "456"), # Pass a list of values as extra parameter
("param2", "789"),
],
)
assert resp.status_code == 200
resp_json = resp.json()
assert "param2" in resp_json
assert resp_json["param2"] == ["456", "789"]
def test_header_pass_extra_single():
client = TestClient(app)
resp = client.get(
"/header",
headers=[
("param", "123"),
("param2", "456"),
],
)
assert resp.status_code == 200
resp_json = resp.json()
assert "param2" in resp_json
assert resp_json["param2"] == "456"
def test_cookie_pass_extra_list():
client = TestClient(app)
client.cookies = [
("param", "123"),
("param2", "456"), # Pass a list of values as extra parameter
("param2", "789"),
]
resp = client.get("/cookie")
assert resp.status_code == 200
resp_json = resp.json()
assert "param2" in resp_json
assert resp_json["param2"] == "789" # Cookies only keep the last value

View File

@ -0,0 +1,76 @@
from dirty_equals import IsPartialDict
from fastapi import Cookie, FastAPI, Header, Query
from fastapi._compat import PYDANTIC_V2
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field
app = FastAPI()
class Model(BaseModel):
param: str = Field(alias="param_alias")
@app.get("/query")
async def query_model(data: Model = Query()):
return {"param": data.param}
@app.get("/header")
async def header_model(data: Model = Header()):
return {"param": data.param}
@app.get("/cookie")
async def cookie_model(data: Model = Cookie()):
return {"param": data.param}
def test_query_model_with_alias():
client = TestClient(app)
response = client.get("/query", params={"param_alias": "value"})
assert response.status_code == 200, response.text
assert response.json() == {"param": "value"}
def test_header_model_with_alias():
client = TestClient(app)
response = client.get("/header", headers={"param_alias": "value"})
assert response.status_code == 200, response.text
assert response.json() == {"param": "value"}
def test_cookie_model_with_alias():
client = TestClient(app)
client.cookies.set("param_alias", "value")
response = client.get("/cookie")
assert response.status_code == 200, response.text
assert response.json() == {"param": "value"}
def test_query_model_with_alias_by_name():
client = TestClient(app)
response = client.get("/query", params={"param": "value"})
assert response.status_code == 422, response.text
details = response.json()
if PYDANTIC_V2:
assert details["detail"][0]["input"] == {"param": "value"}
def test_header_model_with_alias_by_name():
client = TestClient(app)
response = client.get("/header", headers={"param": "value"})
assert response.status_code == 422, response.text
details = response.json()
if PYDANTIC_V2:
assert details["detail"][0]["input"] == IsPartialDict({"param": "value"})
def test_cookie_model_with_alias_by_name():
client = TestClient(app)
client.cookies.set("param", "value")
response = client.get("/cookie")
assert response.status_code == 422, response.text
details = response.json()
if PYDANTIC_V2:
assert details["detail"][0]["input"] == {"param": "value"}

View File

@ -0,0 +1,92 @@
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from inline_snapshot import snapshot
from pydantic import BaseModel
from tests.utils import needs_py310, needs_pydanticv2
@pytest.fixture(name="client")
def get_client():
from enum import Enum
app = FastAPI()
class PlatformRole(str, Enum):
admin = "admin"
user = "user"
class OtherRole(str, Enum): ...
class User(BaseModel):
username: str
role: PlatformRole | OtherRole
@app.get("/users")
async def get_user() -> User:
return {"username": "alice", "role": "admin"}
client = TestClient(app)
return client
@needs_py310
@needs_pydanticv2
def test_get(client: TestClient):
response = client.get("/users")
assert response.json() == {"username": "alice", "role": "admin"}
@needs_py310
@needs_pydanticv2
def test_openapi_schema(client: TestClient):
response = client.get("openapi.json")
assert response.json() == snapshot(
{
"openapi": "3.1.0",
"info": {"title": "FastAPI", "version": "0.1.0"},
"paths": {
"/users": {
"get": {
"summary": "Get User",
"operationId": "get_user_users_get",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/User"}
}
},
}
},
}
}
},
"components": {
"schemas": {
"PlatformRole": {
"type": "string",
"enum": ["admin", "user"],
"title": "PlatformRole",
},
"User": {
"properties": {
"username": {"type": "string", "title": "Username"},
"role": {
"anyOf": [
{"$ref": "#/components/schemas/PlatformRole"},
{"enum": [], "title": "OtherRole"},
],
"title": "Role",
},
},
"type": "object",
"required": ["username", "role"],
"title": "User",
},
}
},
}
)

View File

@ -32,8 +32,9 @@ def test_security_api_key():
def test_security_api_key_no_key(): def test_security_api_key_no_key():
client = TestClient(app) client = TestClient(app)
response = client.get("/users/me") response = client.get("/users/me")
assert response.status_code == 403, response.text assert response.status_code == 401, response.text
assert response.json() == {"detail": "Not authenticated"} assert response.json() == {"detail": "Not authenticated"}
assert response.headers["WWW-Authenticate"] == "APIKey"
def test_openapi_schema(): def test_openapi_schema():

View File

@ -32,8 +32,9 @@ def test_security_api_key():
def test_security_api_key_no_key(): def test_security_api_key_no_key():
client = TestClient(app) client = TestClient(app)
response = client.get("/users/me") response = client.get("/users/me")
assert response.status_code == 403, response.text assert response.status_code == 401, response.text
assert response.json() == {"detail": "Not authenticated"} assert response.json() == {"detail": "Not authenticated"}
assert response.headers["WWW-Authenticate"] == "APIKey"
def test_openapi_schema(): def test_openapi_schema():

View File

@ -33,8 +33,9 @@ def test_security_api_key():
def test_security_api_key_no_key(): def test_security_api_key_no_key():
response = client.get("/users/me") response = client.get("/users/me")
assert response.status_code == 403, response.text assert response.status_code == 401, response.text
assert response.json() == {"detail": "Not authenticated"} assert response.json() == {"detail": "Not authenticated"}
assert response.headers["WWW-Authenticate"] == "APIKey"
def test_openapi_schema(): def test_openapi_schema():

View File

@ -33,8 +33,9 @@ def test_security_api_key():
def test_security_api_key_no_key(): def test_security_api_key_no_key():
response = client.get("/users/me") response = client.get("/users/me")
assert response.status_code == 403, response.text assert response.status_code == 401, response.text
assert response.json() == {"detail": "Not authenticated"} assert response.json() == {"detail": "Not authenticated"}
assert response.headers["WWW-Authenticate"] == "APIKey"
def test_openapi_schema(): def test_openapi_schema():

View File

@ -33,8 +33,9 @@ def test_security_api_key():
def test_security_api_key_no_key(): def test_security_api_key_no_key():
response = client.get("/users/me") response = client.get("/users/me")
assert response.status_code == 403, response.text assert response.status_code == 401, response.text
assert response.json() == {"detail": "Not authenticated"} assert response.json() == {"detail": "Not authenticated"}
assert response.headers["WWW-Authenticate"] == "APIKey"
def test_openapi_schema(): def test_openapi_schema():

View File

@ -33,8 +33,9 @@ def test_security_api_key():
def test_security_api_key_no_key(): def test_security_api_key_no_key():
response = client.get("/users/me") response = client.get("/users/me")
assert response.status_code == 403, response.text assert response.status_code == 401, response.text
assert response.json() == {"detail": "Not authenticated"} assert response.json() == {"detail": "Not authenticated"}
assert response.headers["WWW-Authenticate"] == "APIKey"
def test_openapi_schema(): def test_openapi_schema():

View File

@ -23,8 +23,9 @@ def test_security_http_base():
def test_security_http_base_no_credentials(): def test_security_http_base_no_credentials():
response = client.get("/users/me") response = client.get("/users/me")
assert response.status_code == 403, response.text assert response.status_code == 401, response.text
assert response.json() == {"detail": "Not authenticated"} assert response.json() == {"detail": "Not authenticated"}
assert response.headers["WWW-Authenticate"] == "Other"
def test_openapi_schema(): def test_openapi_schema():

View File

@ -23,8 +23,9 @@ def test_security_http_base():
def test_security_http_base_no_credentials(): def test_security_http_base_no_credentials():
response = client.get("/users/me") response = client.get("/users/me")
assert response.status_code == 403, response.text assert response.status_code == 401, response.text
assert response.json() == {"detail": "Not authenticated"} assert response.json() == {"detail": "Not authenticated"}
assert response.headers["WWW-Authenticate"] == "Other"
def test_openapi_schema(): def test_openapi_schema():

View File

@ -38,7 +38,7 @@ def test_security_http_basic_invalid_credentials():
) )
assert response.status_code == 401, response.text assert response.status_code == 401, response.text
assert response.headers["WWW-Authenticate"] == "Basic" assert response.headers["WWW-Authenticate"] == "Basic"
assert response.json() == {"detail": "Invalid authentication credentials"} assert response.json() == {"detail": "Not authenticated"}
def test_security_http_basic_non_basic_credentials(): def test_security_http_basic_non_basic_credentials():
@ -47,7 +47,7 @@ def test_security_http_basic_non_basic_credentials():
response = client.get("/users/me", headers={"Authorization": auth_header}) response = client.get("/users/me", headers={"Authorization": auth_header})
assert response.status_code == 401, response.text assert response.status_code == 401, response.text
assert response.headers["WWW-Authenticate"] == "Basic" assert response.headers["WWW-Authenticate"] == "Basic"
assert response.json() == {"detail": "Invalid authentication credentials"} assert response.json() == {"detail": "Not authenticated"}
def test_openapi_schema(): def test_openapi_schema():

View File

@ -36,7 +36,7 @@ def test_security_http_basic_invalid_credentials():
) )
assert response.status_code == 401, response.text assert response.status_code == 401, response.text
assert response.headers["WWW-Authenticate"] == 'Basic realm="simple"' assert response.headers["WWW-Authenticate"] == 'Basic realm="simple"'
assert response.json() == {"detail": "Invalid authentication credentials"} assert response.json() == {"detail": "Not authenticated"}
def test_security_http_basic_non_basic_credentials(): def test_security_http_basic_non_basic_credentials():
@ -45,7 +45,7 @@ def test_security_http_basic_non_basic_credentials():
response = client.get("/users/me", headers={"Authorization": auth_header}) response = client.get("/users/me", headers={"Authorization": auth_header})
assert response.status_code == 401, response.text assert response.status_code == 401, response.text
assert response.headers["WWW-Authenticate"] == 'Basic realm="simple"' assert response.headers["WWW-Authenticate"] == 'Basic realm="simple"'
assert response.json() == {"detail": "Invalid authentication credentials"} assert response.json() == {"detail": "Not authenticated"}
def test_openapi_schema(): def test_openapi_schema():

View File

@ -36,7 +36,7 @@ def test_security_http_basic_invalid_credentials():
) )
assert response.status_code == 401, response.text assert response.status_code == 401, response.text
assert response.headers["WWW-Authenticate"] == 'Basic realm="simple"' assert response.headers["WWW-Authenticate"] == 'Basic realm="simple"'
assert response.json() == {"detail": "Invalid authentication credentials"} assert response.json() == {"detail": "Not authenticated"}
def test_security_http_basic_non_basic_credentials(): def test_security_http_basic_non_basic_credentials():
@ -45,7 +45,7 @@ def test_security_http_basic_non_basic_credentials():
response = client.get("/users/me", headers={"Authorization": auth_header}) response = client.get("/users/me", headers={"Authorization": auth_header})
assert response.status_code == 401, response.text assert response.status_code == 401, response.text
assert response.headers["WWW-Authenticate"] == 'Basic realm="simple"' assert response.headers["WWW-Authenticate"] == 'Basic realm="simple"'
assert response.json() == {"detail": "Invalid authentication credentials"} assert response.json() == {"detail": "Not authenticated"}
def test_openapi_schema(): def test_openapi_schema():

View File

@ -23,14 +23,16 @@ def test_security_http_bearer():
def test_security_http_bearer_no_credentials(): def test_security_http_bearer_no_credentials():
response = client.get("/users/me") response = client.get("/users/me")
assert response.status_code == 403, response.text assert response.status_code == 401, response.text
assert response.json() == {"detail": "Not authenticated"} assert response.json() == {"detail": "Not authenticated"}
assert response.headers["WWW-Authenticate"] == "Bearer"
def test_security_http_bearer_incorrect_scheme_credentials(): def test_security_http_bearer_incorrect_scheme_credentials():
response = client.get("/users/me", headers={"Authorization": "Basic notreally"}) response = client.get("/users/me", headers={"Authorization": "Basic notreally"})
assert response.status_code == 403, response.text assert response.status_code == 401, response.text
assert response.json() == {"detail": "Invalid authentication credentials"} assert response.json() == {"detail": "Not authenticated"}
assert response.headers["WWW-Authenticate"] == "Bearer"
def test_openapi_schema(): def test_openapi_schema():

View File

@ -23,14 +23,16 @@ def test_security_http_bearer():
def test_security_http_bearer_no_credentials(): def test_security_http_bearer_no_credentials():
response = client.get("/users/me") response = client.get("/users/me")
assert response.status_code == 403, response.text assert response.status_code == 401, response.text
assert response.json() == {"detail": "Not authenticated"} assert response.json() == {"detail": "Not authenticated"}
assert response.headers["WWW-Authenticate"] == "Bearer"
def test_security_http_bearer_incorrect_scheme_credentials(): def test_security_http_bearer_incorrect_scheme_credentials():
response = client.get("/users/me", headers={"Authorization": "Basic notreally"}) response = client.get("/users/me", headers={"Authorization": "Basic notreally"})
assert response.status_code == 403, response.text assert response.status_code == 401, response.text
assert response.json() == {"detail": "Invalid authentication credentials"} assert response.json() == {"detail": "Not authenticated"}
assert response.headers["WWW-Authenticate"] == "Bearer"
def test_openapi_schema(): def test_openapi_schema():

View File

@ -23,16 +23,18 @@ def test_security_http_digest():
def test_security_http_digest_no_credentials(): def test_security_http_digest_no_credentials():
response = client.get("/users/me") response = client.get("/users/me")
assert response.status_code == 403, response.text assert response.status_code == 401, response.text
assert response.json() == {"detail": "Not authenticated"} assert response.json() == {"detail": "Not authenticated"}
assert response.headers["WWW-Authenticate"] == "Digest"
def test_security_http_digest_incorrect_scheme_credentials(): def test_security_http_digest_incorrect_scheme_credentials():
response = client.get( response = client.get(
"/users/me", headers={"Authorization": "Other invalidauthorization"} "/users/me", headers={"Authorization": "Other invalidauthorization"}
) )
assert response.status_code == 403, response.text assert response.status_code == 401, response.text
assert response.json() == {"detail": "Invalid authentication credentials"} assert response.json() == {"detail": "Not authenticated"}
assert response.headers["WWW-Authenticate"] == "Digest"
def test_openapi_schema(): def test_openapi_schema():

View File

@ -23,16 +23,18 @@ def test_security_http_digest():
def test_security_http_digest_no_credentials(): def test_security_http_digest_no_credentials():
response = client.get("/users/me") response = client.get("/users/me")
assert response.status_code == 403, response.text assert response.status_code == 401, response.text
assert response.json() == {"detail": "Not authenticated"} assert response.json() == {"detail": "Not authenticated"}
assert response.headers["WWW-Authenticate"] == "Digest"
def test_security_http_digest_incorrect_scheme_credentials(): def test_security_http_digest_incorrect_scheme_credentials():
response = client.get( response = client.get(
"/users/me", headers={"Authorization": "Other invalidauthorization"} "/users/me", headers={"Authorization": "Other invalidauthorization"}
) )
assert response.status_code == 403, response.text assert response.status_code == 401, response.text
assert response.json() == {"detail": "Invalid authentication credentials"} assert response.json() == {"detail": "Not authenticated"}
assert response.headers["WWW-Authenticate"] == "Digest"
def test_openapi_schema(): def test_openapi_schema():

View File

@ -56,8 +56,9 @@ def test_security_oauth2_password_other_header():
def test_security_oauth2_password_bearer_no_header(): def test_security_oauth2_password_bearer_no_header():
response = client.get("/users/me") response = client.get("/users/me")
assert response.status_code == 403, response.text assert response.status_code == 401, response.text
assert response.json() == {"detail": "Not authenticated"} assert response.json() == {"detail": "Not authenticated"}
assert response.headers["WWW-Authenticate"] == "Bearer"
def test_strict_login_no_data(): def test_strict_login_no_data():

View File

@ -0,0 +1,198 @@
# Ref: https://github.com/fastapi/fastapi/issues/14454
from typing import Optional
from fastapi import APIRouter, Depends, FastAPI, Security
from fastapi.security import OAuth2AuthorizationCodeBearer
from fastapi.testclient import TestClient
from inline_snapshot import snapshot
from typing_extensions import Annotated
oauth2_scheme = OAuth2AuthorizationCodeBearer(
authorizationUrl="authorize",
tokenUrl="token",
auto_error=True,
scopes={"read": "Read access", "write": "Write access"},
)
async def get_token(token: Annotated[str, Depends(oauth2_scheme)]) -> str:
return token
app = FastAPI(dependencies=[Depends(get_token)])
@app.get("/")
async def root():
return {"message": "Hello World"}
@app.get(
"/with-oauth2-scheme",
dependencies=[Security(oauth2_scheme, scopes=["read", "write"])],
)
async def read_with_oauth2_scheme():
return {"message": "Admin Access"}
@app.get(
"/with-get-token", dependencies=[Security(get_token, scopes=["read", "write"])]
)
async def read_with_get_token():
return {"message": "Admin Access"}
router = APIRouter(dependencies=[Security(oauth2_scheme, scopes=["read"])])
@router.get("/items/")
async def read_items(token: Optional[str] = Depends(oauth2_scheme)):
return {"token": token}
@router.post("/items/")
async def create_item(
token: Optional[str] = Security(oauth2_scheme, scopes=["read", "write"]),
):
return {"token": token}
app.include_router(router)
client = TestClient(app)
def test_root():
response = client.get("/", headers={"Authorization": "Bearer testtoken"})
assert response.status_code == 200, response.text
assert response.json() == {"message": "Hello World"}
def test_read_with_oauth2_scheme():
response = client.get(
"/with-oauth2-scheme", headers={"Authorization": "Bearer testtoken"}
)
assert response.status_code == 200, response.text
assert response.json() == {"message": "Admin Access"}
def test_read_with_get_token():
response = client.get(
"/with-get-token", headers={"Authorization": "Bearer testtoken"}
)
assert response.status_code == 200, response.text
assert response.json() == {"message": "Admin Access"}
def test_read_token():
response = client.get("/items/", headers={"Authorization": "Bearer testtoken"})
assert response.status_code == 200, response.text
assert response.json() == {"token": "testtoken"}
def test_create_token():
response = client.post("/items/", headers={"Authorization": "Bearer testtoken"})
assert response.status_code == 200, response.text
assert response.json() == {"token": "testtoken"}
def test_openapi_schema():
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == snapshot(
{
"openapi": "3.1.0",
"info": {"title": "FastAPI", "version": "0.1.0"},
"paths": {
"/": {
"get": {
"summary": "Root",
"operationId": "root__get",
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
}
},
"security": [{"OAuth2AuthorizationCodeBearer": []}],
}
},
"/with-oauth2-scheme": {
"get": {
"summary": "Read With Oauth2 Scheme",
"operationId": "read_with_oauth2_scheme_with_oauth2_scheme_get",
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
}
},
"security": [
{"OAuth2AuthorizationCodeBearer": ["read", "write"]}
],
}
},
"/with-get-token": {
"get": {
"summary": "Read With Get Token",
"operationId": "read_with_get_token_with_get_token_get",
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
}
},
"security": [
{"OAuth2AuthorizationCodeBearer": ["read", "write"]}
],
}
},
"/items/": {
"get": {
"summary": "Read Items",
"operationId": "read_items_items__get",
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
}
},
"security": [
{"OAuth2AuthorizationCodeBearer": ["read"]},
],
},
"post": {
"summary": "Create Item",
"operationId": "create_item_items__post",
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
}
},
"security": [
{"OAuth2AuthorizationCodeBearer": ["read", "write"]},
],
},
},
},
"components": {
"securitySchemes": {
"OAuth2AuthorizationCodeBearer": {
"type": "oauth2",
"flows": {
"authorizationCode": {
"scopes": {
"read": "Read access",
"write": "Write access",
},
"authorizationUrl": "authorize",
"tokenUrl": "token",
}
},
}
}
},
}
)

View File

@ -0,0 +1,79 @@
# Ref: https://github.com/fastapi/fastapi/issues/14454
from fastapi import Depends, FastAPI, Security
from fastapi.security import OAuth2AuthorizationCodeBearer
from fastapi.testclient import TestClient
from inline_snapshot import snapshot
from typing_extensions import Annotated
oauth2_scheme = OAuth2AuthorizationCodeBearer(
authorizationUrl="api/oauth/authorize",
tokenUrl="/api/oauth/token",
scopes={"read": "Read access", "write": "Write access"},
)
async def get_token(token: Annotated[str, Depends(oauth2_scheme)]) -> str:
return token
app = FastAPI(dependencies=[Depends(get_token)])
@app.get("/admin", dependencies=[Security(get_token, scopes=["read", "write"])])
async def read_admin():
return {"message": "Admin Access"}
client = TestClient(app)
def test_read_admin():
response = client.get("/admin", headers={"Authorization": "Bearer faketoken"})
assert response.status_code == 200, response.text
assert response.json() == {"message": "Admin Access"}
def test_openapi_schema():
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == snapshot(
{
"openapi": "3.1.0",
"info": {"title": "FastAPI", "version": "0.1.0"},
"paths": {
"/admin": {
"get": {
"summary": "Read Admin",
"operationId": "read_admin_admin_get",
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
}
},
"security": [
{"OAuth2AuthorizationCodeBearer": ["read", "write"]}
],
}
}
},
"components": {
"securitySchemes": {
"OAuth2AuthorizationCodeBearer": {
"type": "oauth2",
"flows": {
"authorizationCode": {
"scopes": {
"read": "Read access",
"write": "Write access",
},
"authorizationUrl": "api/oauth/authorize",
"tokenUrl": "/api/oauth/token",
}
},
}
}
},
}
)

View File

@ -39,8 +39,9 @@ def test_security_oauth2_password_other_header():
def test_security_oauth2_password_bearer_no_header(): def test_security_oauth2_password_bearer_no_header():
response = client.get("/users/me") response = client.get("/users/me")
assert response.status_code == 403, response.text assert response.status_code == 401, response.text
assert response.json() == {"detail": "Not authenticated"} assert response.json() == {"detail": "Not authenticated"}
assert response.headers["WWW-Authenticate"] == "Bearer"
def test_openapi_schema(): def test_openapi_schema():

View File

@ -41,8 +41,9 @@ def test_security_oauth2_password_other_header():
def test_security_oauth2_password_bearer_no_header(): def test_security_oauth2_password_bearer_no_header():
response = client.get("/users/me") response = client.get("/users/me")
assert response.status_code == 403, response.text assert response.status_code == 401, response.text
assert response.json() == {"detail": "Not authenticated"} assert response.json() == {"detail": "Not authenticated"}
assert response.headers["WWW-Authenticate"] == "Bearer"
def test_openapi_schema(): def test_openapi_schema():

View File

@ -0,0 +1,46 @@
from typing import Dict
import pytest
from fastapi import Depends, FastAPI, Security
from fastapi.testclient import TestClient
from typing_extensions import Annotated
@pytest.fixture(name="call_counter")
def call_counter_fixture():
return {"count": 0}
@pytest.fixture(name="app")
def app_fixture(call_counter: Dict[str, int]):
def get_db():
call_counter["count"] += 1
return f"db_{call_counter['count']}"
def get_user(db: Annotated[str, Depends(get_db)]):
return "user"
app = FastAPI()
@app.get("/")
def endpoint(
db: Annotated[str, Depends(get_db)],
user: Annotated[str, Security(get_user, scopes=["read"])],
):
return {"db": db}
return app
@pytest.fixture(name="client")
def client_fixture(app: FastAPI):
return TestClient(app)
def test_security_scopes_dependency_called_once(
client: TestClient, call_counter: Dict[str, int]
):
response = client.get("/")
assert response.status_code == 200
assert call_counter["count"] == 1

View File

@ -0,0 +1,45 @@
# Ref: https://github.com/tiangolo/fastapi/issues/5623
from typing import Any, Dict, List
from fastapi import FastAPI, Security
from fastapi.security import SecurityScopes
from fastapi.testclient import TestClient
from typing_extensions import Annotated
async def security1(scopes: SecurityScopes):
return scopes.scopes
async def security2(scopes: SecurityScopes):
return scopes.scopes
async def dep3(
dep1: Annotated[List[str], Security(security1, scopes=["scope1"])],
dep2: Annotated[List[str], Security(security2, scopes=["scope2"])],
):
return {"dep1": dep1, "dep2": dep2}
app = FastAPI()
@app.get("/scopes")
def get_scopes(
dep3: Annotated[Dict[str, Any], Security(dep3, scopes=["scope3"])],
):
return dep3
client = TestClient(app)
def test_security_scopes_dont_propagate():
response = client.get("/scopes")
assert response.status_code == 200
assert response.json() == {
"dep1": ["scope3", "scope1"],
"dep2": ["scope3", "scope2"],
}

View File

@ -0,0 +1,107 @@
# Ref: https://github.com/fastapi/fastapi/discussions/6024#discussioncomment-8541913
from typing import Dict
import pytest
from fastapi import Depends, FastAPI, Security
from fastapi.security import SecurityScopes
from fastapi.testclient import TestClient
from typing_extensions import Annotated
@pytest.fixture(name="call_counts")
def call_counts_fixture():
return {
"get_db_session": 0,
"get_current_user": 0,
"get_user_me": 0,
"get_user_items": 0,
}
@pytest.fixture(name="app")
def app_fixture(call_counts: Dict[str, int]):
def get_db_session():
call_counts["get_db_session"] += 1
return f"db_session_{call_counts['get_db_session']}"
def get_current_user(
security_scopes: SecurityScopes,
db_session: Annotated[str, Depends(get_db_session)],
):
call_counts["get_current_user"] += 1
return {
"user": f"user_{call_counts['get_current_user']}",
"scopes": security_scopes.scopes,
"db_session": db_session,
}
def get_user_me(
current_user: Annotated[dict, Security(get_current_user, scopes=["me"])],
):
call_counts["get_user_me"] += 1
return {
"user_me": f"user_me_{call_counts['get_user_me']}",
"current_user": current_user,
}
def get_user_items(
user_me: Annotated[dict, Depends(get_user_me)],
):
call_counts["get_user_items"] += 1
return {
"user_items": f"user_items_{call_counts['get_user_items']}",
"user_me": user_me,
}
app = FastAPI()
@app.get("/")
def path_operation(
user_me: Annotated[dict, Depends(get_user_me)],
user_items: Annotated[dict, Security(get_user_items, scopes=["items"])],
):
return {
"user_me": user_me,
"user_items": user_items,
}
return app
@pytest.fixture(name="client")
def client_fixture(app: FastAPI):
return TestClient(app)
def test_security_scopes_sub_dependency_caching(
client: TestClient, call_counts: Dict[str, int]
):
response = client.get("/")
assert response.status_code == 200
assert call_counts["get_db_session"] == 1
assert call_counts["get_current_user"] == 2
assert call_counts["get_user_me"] == 2
assert call_counts["get_user_items"] == 1
assert response.json() == {
"user_me": {
"user_me": "user_me_1",
"current_user": {
"user": "user_1",
"scopes": ["me"],
"db_session": "db_session_1",
},
},
"user_items": {
"user_items": "user_items_1",
"user_me": {
"user_me": "user_me_2",
"current_user": {
"user": "user_2",
"scopes": ["items", "me"],
"db_session": "db_session_1",
},
},
},
}

View File

@ -0,0 +1,26 @@
from __future__ import annotations
from fastapi import Depends, FastAPI, Request
from fastapi.testclient import TestClient
from typing_extensions import Annotated
from .utils import needs_py310
class Dep:
def __call__(self, request: Request):
return "test"
@needs_py310
def test_stringified_annotations():
app = FastAPI()
client = TestClient(app)
@app.get("/test/")
def call(test: Annotated[str, Depends(Dep())]):
return {"test": test}
response = client.get("/test")
assert response.status_code == 200

View File

@ -27,7 +27,7 @@ def test_get_root():
def test_get_root_no_token(): def test_get_root_no_token():
response = client.get("/") response = client.get("/")
assert response.status_code == 403, response.text assert response.status_code == 401, response.text
assert response.json() == {"detail": "Not authenticated"} assert response.json() == {"detail": "Not authenticated"}

View File

@ -0,0 +1,69 @@
import importlib
import pytest
from fastapi.testclient import TestClient
from inline_snapshot import snapshot
from ...utils import needs_py39
@pytest.fixture(
name="client",
params=[
"tutorial001_an",
pytest.param("tutorial001_an_py39", marks=needs_py39),
],
)
def get_client(request: pytest.FixtureRequest):
mod = importlib.import_module(
f"docs_src.authentication_error_status_code.{request.param}"
)
client = TestClient(mod.app)
return client
def test_get_me(client: TestClient):
response = client.get("/me", headers={"Authorization": "Bearer secrettoken"})
assert response.status_code == 200
assert response.json() == {
"message": "You are authenticated",
"token": "secrettoken",
}
def test_get_me_no_credentials(client: TestClient):
response = client.get("/me")
assert response.status_code == 403
assert response.json() == {"detail": "Not authenticated"}
def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == snapshot(
{
"openapi": "3.1.0",
"info": {"title": "FastAPI", "version": "0.1.0"},
"paths": {
"/me": {
"get": {
"summary": "Read Me",
"operationId": "read_me_me_get",
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
}
},
"security": [{"HTTPBearer403": []}],
}
}
},
"components": {
"securitySchemes": {
"HTTPBearer403": {"type": "http", "scheme": "bearer"}
}
},
}
)

View File

@ -77,7 +77,7 @@ def test_header_param_model_no_underscore(client: TestClient):
"user-agent": "testclient", "user-agent": "testclient",
"save-data": "true", "save-data": "true",
"if-modified-since": "yesterday", "if-modified-since": "yesterday",
"x-tag": "two", "x-tag": ["one", "two"],
}, },
} }
) )

View File

@ -66,7 +66,7 @@ def test_token(client: TestClient):
def test_incorrect_token(client: TestClient): def test_incorrect_token(client: TestClient):
response = client.get("/users/me", headers={"Authorization": "Bearer nonexistent"}) response = client.get("/users/me", headers={"Authorization": "Bearer nonexistent"})
assert response.status_code == 401, response.text assert response.status_code == 401, response.text
assert response.json() == {"detail": "Invalid authentication credentials"} assert response.json() == {"detail": "Not authenticated"}
assert response.headers["WWW-Authenticate"] == "Bearer" assert response.headers["WWW-Authenticate"] == "Bearer"

Some files were not shown because too many files have changed in this diff Show More