mirror of
https://github.com/fastapi/fastapi.git
synced 2025-12-24 14:48:35 -05:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b1d9769f97 | ||
|
|
89157a803c | ||
|
|
d86c47477e | ||
|
|
3fe6522aae | ||
|
|
80d1f732e5 | ||
|
|
c0556ac3a5 | ||
|
|
1fcec88ad2 | ||
|
|
f8b216df30 | ||
|
|
4b905b614c | ||
|
|
6c54bcefd3 | ||
|
|
475ce41268 | ||
|
|
564a4ac1b8 | ||
|
|
931e80f20c | ||
|
|
a7ba9932ba | ||
|
|
009c8af7fe | ||
|
|
4c4d520198 | ||
|
|
1cf7cd8af0 | ||
|
|
4a9f13763d | ||
|
|
30747a69c8 | ||
|
|
cd9d093f60 | ||
|
|
4a98a66778 | ||
|
|
442cb306f6 |
51
.github/workflows/test.yml
vendored
51
.github/workflows/test.yml
vendored
@@ -44,22 +44,45 @@ jobs:
|
||||
run: bash scripts/lint.sh
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version:
|
||||
- "3.14"
|
||||
- "3.13"
|
||||
- "3.12"
|
||||
- "3.11"
|
||||
- "3.10"
|
||||
- "3.9"
|
||||
- "3.8"
|
||||
pydantic-version: ["pydantic-v1", "pydantic-v2"]
|
||||
exclude:
|
||||
- python-version: "3.14"
|
||||
os: [ windows-latest, macos-latest ]
|
||||
python-version: [ "3.14" ]
|
||||
pydantic-version: [ "pydantic-v2" ]
|
||||
include:
|
||||
- os: macos-latest
|
||||
python-version: "3.8"
|
||||
pydantic-version: "pydantic-v1"
|
||||
- os: windows-latest
|
||||
python-version: "3.8"
|
||||
pydantic-version: "pydantic-v2"
|
||||
coverage: coverage
|
||||
- os: ubuntu-latest
|
||||
python-version: "3.9"
|
||||
pydantic-version: "pydantic-v1"
|
||||
coverage: coverage
|
||||
- os: macos-latest
|
||||
python-version: "3.10"
|
||||
pydantic-version: "pydantic-v2"
|
||||
- os: windows-latest
|
||||
python-version: "3.11"
|
||||
pydantic-version: "pydantic-v1"
|
||||
- os: ubuntu-latest
|
||||
python-version: "3.12"
|
||||
pydantic-version: "pydantic-v2"
|
||||
- os: macos-latest
|
||||
python-version: "3.13"
|
||||
pydantic-version: "pydantic-v1"
|
||||
- os: windows-latest
|
||||
python-version: "3.13"
|
||||
pydantic-version: "pydantic-v2"
|
||||
coverage: coverage
|
||||
- os: ubuntu-latest
|
||||
python-version: "3.14"
|
||||
pydantic-version: "pydantic-v2"
|
||||
coverage: coverage
|
||||
fail-fast: false
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Dump GitHub context
|
||||
env:
|
||||
@@ -96,10 +119,12 @@ jobs:
|
||||
env:
|
||||
COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }}
|
||||
CONTEXT: ${{ runner.os }}-py${{ matrix.python-version }}
|
||||
# Do not store coverage for all possible combinations to avoid file size max errors in Smokeshow
|
||||
- name: Store coverage files
|
||||
if: matrix.coverage == 'coverage'
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: coverage-${{ matrix.python-version }}-${{ matrix.pydantic-version }}
|
||||
name: coverage-${{ runner.os }}-${{ matrix.python-version }}-${{ matrix.pydantic-version }}
|
||||
path: coverage
|
||||
include-hidden-files: true
|
||||
|
||||
|
||||
@@ -175,7 +175,7 @@ Sie können denselben `responses`-Parameter verwenden, um verschiedene Medientyp
|
||||
|
||||
Sie können beispielsweise einen zusätzlichen Medientyp `image/png` hinzufügen und damit deklarieren, dass Ihre *Pfadoperation* ein JSON-Objekt (mit dem Medientyp `application/json`) oder ein PNG-Bild zurückgeben kann:
|
||||
|
||||
{* ../../docs_src/additional_responses/tutorial002.py hl[19:24,28] *}
|
||||
{* ../../docs_src/additional_responses/tutorial002_py310.py hl[17:22,26] *}
|
||||
|
||||
/// note | Hinweis
|
||||
|
||||
@@ -237,7 +237,7 @@ Mit dieser Technik können Sie einige vordefinierte Responses in Ihren *Pfadoper
|
||||
|
||||
Zum Beispiel:
|
||||
|
||||
{* ../../docs_src/additional_responses/tutorial004.py hl[13:17,26] *}
|
||||
{* ../../docs_src/additional_responses/tutorial004_py310.py hl[11:15,24] *}
|
||||
|
||||
## Weitere Informationen zu OpenAPI-Responses { #more-information-about-openapi-responses }
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ FastAPI basiert auf **Pydantic**, und ich habe Ihnen gezeigt, wie Sie Pydantic-M
|
||||
|
||||
Aber FastAPI unterstützt auf die gleiche Weise auch die Verwendung von <a href="https://docs.python.org/3/library/dataclasses.html" class="external-link" target="_blank">`dataclasses`</a>:
|
||||
|
||||
{* ../../docs_src/dataclasses/tutorial001.py hl[1,7:12,19:20] *}
|
||||
{* ../../docs_src/dataclasses/tutorial001_py310.py hl[1,6:11,18:19] *}
|
||||
|
||||
Das ist dank **Pydantic** ebenfalls möglich, da es <a href="https://docs.pydantic.dev/latest/concepts/dataclasses/#use-of-stdlib-dataclasses-with-basemodel" class="external-link" target="_blank">`dataclasses` intern unterstützt</a>.
|
||||
|
||||
@@ -32,7 +32,7 @@ Wenn Sie jedoch eine Menge Datenklassen herumliegen haben, ist dies ein guter Tr
|
||||
|
||||
Sie können `dataclasses` auch im Parameter `response_model` verwenden:
|
||||
|
||||
{* ../../docs_src/dataclasses/tutorial002.py hl[1,7:13,19] *}
|
||||
{* ../../docs_src/dataclasses/tutorial002_py310.py hl[1,6:12,18] *}
|
||||
|
||||
Die Datenklasse wird automatisch in eine Pydantic-Datenklasse konvertiert.
|
||||
|
||||
@@ -48,7 +48,7 @@ In einigen Fällen müssen Sie möglicherweise immer noch Pydantics Version von
|
||||
|
||||
In diesem Fall können Sie einfach die Standard-`dataclasses` durch `pydantic.dataclasses` ersetzen, was einen direkten Ersatz darstellt:
|
||||
|
||||
{* ../../docs_src/dataclasses/tutorial003.py hl[1,5,8:11,14:17,23:25,28] *}
|
||||
{* ../../docs_src/dataclasses/tutorial003_py310.py hl[1,4,7:10,13:16,22:24,27] *}
|
||||
|
||||
1. Wir importieren `field` weiterhin von Standard-`dataclasses`.
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ Sie verfügt über eine *Pfadoperation*, die einen `Invoice`-Body empfängt, und
|
||||
|
||||
Dieser Teil ist ziemlich normal, der größte Teil des Codes ist Ihnen wahrscheinlich bereits bekannt:
|
||||
|
||||
{* ../../docs_src/openapi_callbacks/tutorial001.py hl[9:13,36:53] *}
|
||||
{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[7:11,34:51] *}
|
||||
|
||||
/// tip | Tipp
|
||||
|
||||
@@ -90,7 +90,7 @@ Wenn Sie diese Sichtweise (des *externen Entwicklers*) vorübergehend übernehme
|
||||
|
||||
Erstellen Sie zunächst einen neuen `APIRouter`, der einen oder mehrere Callbacks enthält.
|
||||
|
||||
{* ../../docs_src/openapi_callbacks/tutorial001.py hl[3,25] *}
|
||||
{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[1,23] *}
|
||||
|
||||
### Die Callback-*Pfadoperation* erstellen { #create-the-callback-path-operation }
|
||||
|
||||
@@ -101,7 +101,7 @@ Sie sollte wie eine normale FastAPI-*Pfadoperation* aussehen:
|
||||
* Sie sollte wahrscheinlich eine Deklaration des Bodys enthalten, die sie erhalten soll, z. B. `body: InvoiceEvent`.
|
||||
* Und sie könnte auch eine Deklaration der Response enthalten, die zurückgegeben werden soll, z. B. `response_model=InvoiceEventReceived`.
|
||||
|
||||
{* ../../docs_src/openapi_callbacks/tutorial001.py hl[16:18,21:22,28:32] *}
|
||||
{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[14:16,19:20,26:30] *}
|
||||
|
||||
Es gibt zwei Hauptunterschiede zu einer normalen *Pfadoperation*:
|
||||
|
||||
@@ -169,7 +169,7 @@ An diesem Punkt haben Sie die benötigte(n) *Callback-Pfadoperation(en)* (diejen
|
||||
|
||||
Verwenden Sie nun den Parameter `callbacks` im *Pfadoperation-Dekorator Ihrer API*, um das Attribut `.routes` (das ist eigentlich nur eine `list`e von Routen/*Pfadoperationen*) dieses Callback-Routers zu übergeben:
|
||||
|
||||
{* ../../docs_src/openapi_callbacks/tutorial001.py hl[35] *}
|
||||
{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[33] *}
|
||||
|
||||
/// tip | Tipp
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ Das Hinzufügen eines `\f` (ein maskiertes „Form Feed“-Zeichen) führt dazu,
|
||||
|
||||
Sie wird nicht in der Dokumentation angezeigt, aber andere Tools (z. B. Sphinx) können den Rest verwenden.
|
||||
|
||||
{* ../../docs_src/path_operation_advanced_configuration/tutorial004.py hl[19:29] *}
|
||||
{* ../../docs_src/path_operation_advanced_configuration/tutorial004_py310.py hl[17:27] *}
|
||||
|
||||
## Zusätzliche Responses { #additional-responses }
|
||||
|
||||
@@ -155,13 +155,13 @@ In der folgenden Anwendung verwenden wir beispielsweise weder die integrierte Fu
|
||||
|
||||
//// tab | Pydantic v2
|
||||
|
||||
{* ../../docs_src/path_operation_advanced_configuration/tutorial007.py hl[17:22, 24] *}
|
||||
{* ../../docs_src/path_operation_advanced_configuration/tutorial007_py39.py hl[15:20, 22] *}
|
||||
|
||||
////
|
||||
|
||||
//// tab | Pydantic v1
|
||||
|
||||
{* ../../docs_src/path_operation_advanced_configuration/tutorial007_pv1.py hl[17:22, 24] *}
|
||||
{* ../../docs_src/path_operation_advanced_configuration/tutorial007_pv1_py39.py hl[15:20, 22] *}
|
||||
|
||||
////
|
||||
|
||||
@@ -179,13 +179,13 @@ Und dann parsen wir in unserem Code diesen YAML-Inhalt direkt und verwenden dann
|
||||
|
||||
//// tab | Pydantic v2
|
||||
|
||||
{* ../../docs_src/path_operation_advanced_configuration/tutorial007.py hl[26:33] *}
|
||||
{* ../../docs_src/path_operation_advanced_configuration/tutorial007_py39.py hl[24:31] *}
|
||||
|
||||
////
|
||||
|
||||
//// tab | Pydantic v1
|
||||
|
||||
{* ../../docs_src/path_operation_advanced_configuration/tutorial007_pv1.py hl[26:33] *}
|
||||
{* ../../docs_src/path_operation_advanced_configuration/tutorial007_pv1_py39.py hl[24:31] *}
|
||||
|
||||
////
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ Sie können beispielsweise kein Pydantic-Modell in eine `JSONResponse` einfügen
|
||||
|
||||
In diesen Fällen können Sie den `jsonable_encoder` verwenden, um Ihre Daten zu konvertieren, bevor Sie sie an eine Response übergeben:
|
||||
|
||||
{* ../../docs_src/response_directly/tutorial001.py hl[6:7,21:22] *}
|
||||
{* ../../docs_src/response_directly/tutorial001_py310.py hl[5:6,20:21] *}
|
||||
|
||||
/// note | Technische Details
|
||||
|
||||
|
||||
@@ -148,7 +148,7 @@ Dies könnte besonders beim Testen nützlich sein, da es sehr einfach ist, eine
|
||||
|
||||
Ausgehend vom vorherigen Beispiel könnte Ihre Datei `config.py` so aussehen:
|
||||
|
||||
{* ../../docs_src/settings/app02/config.py hl[10] *}
|
||||
{* ../../docs_src/settings/app02_an_py39/config.py hl[10] *}
|
||||
|
||||
Beachten Sie, dass wir jetzt keine Standardinstanz `settings = Settings()` erstellen.
|
||||
|
||||
@@ -174,7 +174,7 @@ Und dann können wir das von der *Pfadoperation-Funktion* als Abhängigkeit einf
|
||||
|
||||
Dann wäre es sehr einfach, beim Testen ein anderes Einstellungsobjekt bereitzustellen, indem man eine Abhängigkeitsüberschreibung für `get_settings` erstellt:
|
||||
|
||||
{* ../../docs_src/settings/app02/test_main.py hl[9:10,13,21] *}
|
||||
{* ../../docs_src/settings/app02_an_py39/test_main.py hl[9:10,13,21] *}
|
||||
|
||||
Bei der Abhängigkeitsüberschreibung legen wir einen neuen Wert für `admin_email` fest, wenn wir das neue `Settings`-Objekt erstellen, und geben dann dieses neue Objekt zurück.
|
||||
|
||||
@@ -217,7 +217,7 @@ Und dann aktualisieren Sie Ihre `config.py` mit:
|
||||
|
||||
//// tab | Pydantic v2
|
||||
|
||||
{* ../../docs_src/settings/app03_an/config.py hl[9] *}
|
||||
{* ../../docs_src/settings/app03_an_py39/config.py hl[9] *}
|
||||
|
||||
/// tip | Tipp
|
||||
|
||||
@@ -229,7 +229,7 @@ Das Attribut `model_config` wird nur für die Pydantic-Konfiguration verwendet.
|
||||
|
||||
//// tab | Pydantic v1
|
||||
|
||||
{* ../../docs_src/settings/app03_an/config_pv1.py hl[9:10] *}
|
||||
{* ../../docs_src/settings/app03_an_py39/config_pv1.py hl[9:10] *}
|
||||
|
||||
/// tip | Tipp
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ FastAPI enthält einige Defaultkonfigurationsparameter, die für die meisten Anw
|
||||
|
||||
Es umfasst die folgenden Defaultkonfigurationen:
|
||||
|
||||
{* ../../fastapi/openapi/docs.py ln[8:23] hl[17:23] *}
|
||||
{* ../../fastapi/openapi/docs.py ln[9:24] hl[18:24] *}
|
||||
|
||||
Sie können jede davon überschreiben, indem Sie im Argument `swagger_ui_parameters` einen anderen Wert festlegen.
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ Wenn der Header kein `gzip` enthält, wird nicht versucht, den Body zu dekomprim
|
||||
|
||||
Auf diese Weise kann dieselbe Routenklasse gzip-komprimierte oder unkomprimierte Requests verarbeiten.
|
||||
|
||||
{* ../../docs_src/custom_request_and_route/tutorial001.py hl[8:15] *}
|
||||
{* ../../docs_src/custom_request_and_route/tutorial001_an_py310.py hl[9:16] *}
|
||||
|
||||
### Eine benutzerdefinierte `GzipRoute`-Klasse erstellen { #create-a-custom-gziproute-class }
|
||||
|
||||
@@ -54,7 +54,7 @@ Diese Methode gibt eine Funktion zurück. Und diese Funktion empfängt einen <ab
|
||||
|
||||
Hier verwenden wir sie, um aus dem ursprünglichen Request einen `GzipRequest` zu erstellen.
|
||||
|
||||
{* ../../docs_src/custom_request_and_route/tutorial001.py hl[18:26] *}
|
||||
{* ../../docs_src/custom_request_and_route/tutorial001_an_py310.py hl[19:27] *}
|
||||
|
||||
/// note | Technische Details
|
||||
|
||||
@@ -92,18 +92,18 @@ Wir können denselben Ansatz auch verwenden, um in einem Exceptionhandler auf de
|
||||
|
||||
Alles, was wir tun müssen, ist, den Request innerhalb eines `try`/`except`-Blocks zu handhaben:
|
||||
|
||||
{* ../../docs_src/custom_request_and_route/tutorial002.py hl[13,15] *}
|
||||
{* ../../docs_src/custom_request_and_route/tutorial002_an_py310.py hl[14,16] *}
|
||||
|
||||
Wenn eine Exception auftritt, befindet sich die `Request`-Instanz weiterhin im Gültigkeitsbereich, sodass wir den Requestbody lesen und bei der Fehlerbehandlung verwenden können:
|
||||
|
||||
{* ../../docs_src/custom_request_and_route/tutorial002.py hl[16:18] *}
|
||||
{* ../../docs_src/custom_request_and_route/tutorial002_an_py310.py hl[17:19] *}
|
||||
|
||||
## Benutzerdefinierte `APIRoute`-Klasse in einem Router { #custom-apiroute-class-in-a-router }
|
||||
|
||||
Sie können auch den Parameter `route_class` eines `APIRouter` festlegen:
|
||||
|
||||
{* ../../docs_src/custom_request_and_route/tutorial003.py hl[26] *}
|
||||
{* ../../docs_src/custom_request_and_route/tutorial003_py310.py hl[26] *}
|
||||
|
||||
In diesem Beispiel verwenden die *Pfadoperationen* unter dem `router` die benutzerdefinierte `TimedRoute`-Klasse und haben in der Response einen zusätzlichen `X-Response-Time`-Header mit der Zeit, die zum Generieren der Response benötigt wurde:
|
||||
|
||||
{* ../../docs_src/custom_request_and_route/tutorial003.py hl[13:20] *}
|
||||
{* ../../docs_src/custom_request_and_route/tutorial003_py310.py hl[13:20] *}
|
||||
|
||||
@@ -9,18 +9,18 @@ GitHub-Repository: <a href="https://github.com/tiangolo/full-stack-fastapi-templ
|
||||
## Full Stack FastAPI Template – Technologiestack und Funktionen { #full-stack-fastapi-template-technology-stack-and-features }
|
||||
|
||||
- ⚡ [**FastAPI**](https://fastapi.tiangolo.com/de) für die Python-Backend-API.
|
||||
- 🧰 [SQLModel](https://sqlmodel.tiangolo.com) für die Interaktion mit der Python-SQL-Datenbank (ORM).
|
||||
- 🔍 [Pydantic](https://docs.pydantic.dev), verwendet von FastAPI, für die Datenvalidierung und das Einstellungsmanagement.
|
||||
- 💾 [PostgreSQL](https://www.postgresql.org) als SQL-Datenbank.
|
||||
- 🧰 [SQLModel](https://sqlmodel.tiangolo.com) für die Interaktion mit der Python-SQL-Datenbank (ORM).
|
||||
- 🔍 [Pydantic](https://docs.pydantic.dev), verwendet von FastAPI, für die Datenvalidierung und das Einstellungsmanagement.
|
||||
- 💾 [PostgreSQL](https://www.postgresql.org) als SQL-Datenbank.
|
||||
- 🚀 [React](https://react.dev) für das Frontend.
|
||||
- 💃 Verwendung von TypeScript, Hooks, [Vite](https://vitejs.dev) und anderen Teilen eines modernen Frontend-Stacks.
|
||||
- 🎨 [Tailwind CSS](https://tailwindcss.com) und [shadcn/ui](https://ui.shadcn.com) für die Frontend-Komponenten.
|
||||
- 🤖 Ein automatisch generierter Frontend-Client.
|
||||
- 🧪 [Playwright](https://playwright.dev) für End-to-End-Tests.
|
||||
- 🦇 Unterstützung des Dunkelmodus.
|
||||
- 💃 Verwendung von TypeScript, Hooks, [Vite](https://vitejs.dev) und anderen Teilen eines modernen Frontend-Stacks.
|
||||
- 🎨 [Tailwind CSS](https://tailwindcss.com) und [shadcn/ui](https://ui.shadcn.com) für die Frontend-Komponenten.
|
||||
- 🤖 Ein automatisch generierter Frontend-Client.
|
||||
- 🧪 [Playwright](https://playwright.dev) für End-to-End-Tests.
|
||||
- 🦇 „Dark-Mode“-Unterstützung.
|
||||
- 🐋 [Docker Compose](https://www.docker.com) für Entwicklung und Produktion.
|
||||
- 🔒 Sicheres Passwort-Hashing standardmäßig.
|
||||
- 🔑 JWT-Token-Authentifizierung.
|
||||
- 🔑 JWT (JSON Web Token)-Token-Authentifizierung.
|
||||
- 📫 E-Mail-basierte Passwortwiederherstellung.
|
||||
- ✅ Tests mit [Pytest](https://pytest.org).
|
||||
- 📞 [Traefik](https://traefik.io) als Reverse-Proxy / Load Balancer.
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
# Ressourcen { #resources }
|
||||
|
||||
Zusätzliche Ressourcen, externe Links, Artikel und mehr. ✈️
|
||||
Zusätzliche Ressourcen, externe Links und mehr. ✈️
|
||||
|
||||
@@ -85,9 +85,7 @@ Sie können die *Pfadoperationen* für dieses Modul mit `APIRouter` erstellen.
|
||||
|
||||
Sie importieren ihn und erstellen eine „Instanz“ auf die gleiche Weise wie mit der Klasse `FastAPI`:
|
||||
|
||||
```Python hl_lines="1 3" title="app/routers/users.py"
|
||||
{!../../docs_src/bigger_applications/app/routers/users.py!}
|
||||
```
|
||||
{* ../../docs_src/bigger_applications/app_an_py39/routers/users.py hl[1,3] title["app/routers/users.py"] *}
|
||||
|
||||
### *Pfadoperationen* mit `APIRouter` { #path-operations-with-apirouter }
|
||||
|
||||
@@ -95,9 +93,7 @@ Und dann verwenden Sie ihn, um Ihre *Pfadoperationen* zu deklarieren.
|
||||
|
||||
Verwenden Sie ihn auf die gleiche Weise wie die Klasse `FastAPI`:
|
||||
|
||||
```Python hl_lines="6 11 16" title="app/routers/users.py"
|
||||
{!../../docs_src/bigger_applications/app/routers/users.py!}
|
||||
```
|
||||
{* ../../docs_src/bigger_applications/app_an_py39/routers/users.py hl[6,11,16] title["app/routers/users.py"] *}
|
||||
|
||||
Sie können sich `APIRouter` als eine „Mini-`FastAPI`“-Klasse vorstellen.
|
||||
|
||||
@@ -121,35 +117,7 @@ Also fügen wir sie in ihr eigenes `dependencies`-Modul (`app/dependencies.py`)
|
||||
|
||||
Wir werden nun eine einfache Abhängigkeit verwenden, um einen benutzerdefinierten `X-Token`-Header zu lesen:
|
||||
|
||||
//// tab | Python 3.9+
|
||||
|
||||
```Python hl_lines="3 6-8" title="app/dependencies.py"
|
||||
{!> ../../docs_src/bigger_applications/app_an_py39/dependencies.py!}
|
||||
```
|
||||
|
||||
////
|
||||
|
||||
//// tab | Python 3.8+
|
||||
|
||||
```Python hl_lines="1 5-7" title="app/dependencies.py"
|
||||
{!> ../../docs_src/bigger_applications/app_an/dependencies.py!}
|
||||
```
|
||||
|
||||
////
|
||||
|
||||
//// tab | Python 3.8+ nicht annotiert
|
||||
|
||||
/// tip | Tipp
|
||||
|
||||
Bevorzugen Sie die `Annotated`-Version, falls möglich.
|
||||
|
||||
///
|
||||
|
||||
```Python hl_lines="1 4-6" title="app/dependencies.py"
|
||||
{!> ../../docs_src/bigger_applications/app/dependencies.py!}
|
||||
```
|
||||
|
||||
////
|
||||
{* ../../docs_src/bigger_applications/app_an_py39/dependencies.py hl[3,6:8] title["app/dependencies.py"] *}
|
||||
|
||||
/// tip | Tipp
|
||||
|
||||
@@ -181,9 +149,7 @@ Wir wissen, dass alle *Pfadoperationen* in diesem Modul folgendes haben:
|
||||
|
||||
Anstatt also alles zu jeder *Pfadoperation* hinzuzufügen, können wir es dem `APIRouter` hinzufügen.
|
||||
|
||||
```Python hl_lines="5-10 16 21" title="app/routers/items.py"
|
||||
{!../../docs_src/bigger_applications/app/routers/items.py!}
|
||||
```
|
||||
{* ../../docs_src/bigger_applications/app_an_py39/routers/items.py hl[5:10,16,21] title["app/routers/items.py"] *}
|
||||
|
||||
Da der Pfad jeder *Pfadoperation* mit `/` beginnen muss, wie in:
|
||||
|
||||
@@ -242,9 +208,7 @@ Und wir müssen die Abhängigkeitsfunktion aus dem Modul `app.dependencies` impo
|
||||
|
||||
Daher verwenden wir einen relativen Import mit `..` für die Abhängigkeiten:
|
||||
|
||||
```Python hl_lines="3" title="app/routers/items.py"
|
||||
{!../../docs_src/bigger_applications/app/routers/items.py!}
|
||||
```
|
||||
{* ../../docs_src/bigger_applications/app_an_py39/routers/items.py hl[3] title["app/routers/items.py"] *}
|
||||
|
||||
#### Wie relative Importe funktionieren { #how-relative-imports-work }
|
||||
|
||||
@@ -315,9 +279,7 @@ Wir fügen weder das Präfix `/items` noch `tags=["items"]` zu jeder *Pfadoperat
|
||||
|
||||
Aber wir können immer noch _mehr_ `tags` hinzufügen, die auf eine bestimmte *Pfadoperation* angewendet werden, sowie einige zusätzliche `responses`, die speziell für diese *Pfadoperation* gelten:
|
||||
|
||||
```Python hl_lines="30-31" title="app/routers/items.py"
|
||||
{!../../docs_src/bigger_applications/app/routers/items.py!}
|
||||
```
|
||||
{* ../../docs_src/bigger_applications/app_an_py39/routers/items.py hl[30:31] title["app/routers/items.py"] *}
|
||||
|
||||
/// tip | Tipp
|
||||
|
||||
@@ -343,17 +305,13 @@ Sie importieren und erstellen wie gewohnt eine `FastAPI`-Klasse.
|
||||
|
||||
Und wir können sogar [globale Abhängigkeiten](dependencies/global-dependencies.md){.internal-link target=_blank} deklarieren, die mit den Abhängigkeiten für jeden `APIRouter` kombiniert werden:
|
||||
|
||||
```Python hl_lines="1 3 7" title="app/main.py"
|
||||
{!../../docs_src/bigger_applications/app/main.py!}
|
||||
```
|
||||
{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[1,3,7] title["app/main.py"] *}
|
||||
|
||||
### Den `APIRouter` importieren { #import-the-apirouter }
|
||||
|
||||
Jetzt importieren wir die anderen Submodule, die `APIRouter` haben:
|
||||
|
||||
```Python hl_lines="4-5" title="app/main.py"
|
||||
{!../../docs_src/bigger_applications/app/main.py!}
|
||||
```
|
||||
{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[4:5] title["app/main.py"] *}
|
||||
|
||||
Da es sich bei den Dateien `app/routers/users.py` und `app/routers/items.py` um Submodule handelt, die Teil desselben Python-Packages `app` sind, können wir einen einzelnen Punkt `.` verwenden, um sie mit „relativen Imports“ zu importieren.
|
||||
|
||||
@@ -416,17 +374,13 @@ würde der `router` von `users` den von `items` überschreiben und wir könnten
|
||||
|
||||
Um also beide in derselben Datei verwenden zu können, importieren wir die Submodule direkt:
|
||||
|
||||
```Python hl_lines="5" title="app/main.py"
|
||||
{!../../docs_src/bigger_applications/app/main.py!}
|
||||
```
|
||||
{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[5] title["app/main.py"] *}
|
||||
|
||||
### Die `APIRouter` für `users` und `items` inkludieren { #include-the-apirouters-for-users-and-items }
|
||||
|
||||
Inkludieren wir nun die `router` aus diesen Submodulen `users` und `items`:
|
||||
|
||||
```Python hl_lines="10-11" title="app/main.py"
|
||||
{!../../docs_src/bigger_applications/app/main.py!}
|
||||
```
|
||||
{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[10:11] title["app/main.py"] *}
|
||||
|
||||
/// info | Info
|
||||
|
||||
@@ -466,17 +420,13 @@ Sie enthält einen `APIRouter` mit einigen administrativen *Pfadoperationen*, di
|
||||
|
||||
In diesem Beispiel wird es ganz einfach sein. Nehmen wir jedoch an, dass wir, da sie mit anderen Projekten in der Organisation geteilt wird, sie nicht ändern und kein `prefix`, `dependencies`, `tags`, usw. direkt zum `APIRouter` hinzufügen können:
|
||||
|
||||
```Python hl_lines="3" title="app/internal/admin.py"
|
||||
{!../../docs_src/bigger_applications/app/internal/admin.py!}
|
||||
```
|
||||
{* ../../docs_src/bigger_applications/app_an_py39/internal/admin.py hl[3] title["app/internal/admin.py"] *}
|
||||
|
||||
Aber wir möchten immer noch ein benutzerdefiniertes `prefix` festlegen, wenn wir den `APIRouter` einbinden, sodass alle seine *Pfadoperationen* mit `/admin` beginnen, wir möchten es mit den `dependencies` sichern, die wir bereits für dieses Projekt haben, und wir möchten `tags` und `responses` hinzufügen.
|
||||
|
||||
Wir können das alles deklarieren, ohne den ursprünglichen `APIRouter` ändern zu müssen, indem wir diese Parameter an `app.include_router()` übergeben:
|
||||
|
||||
```Python hl_lines="14-17" title="app/main.py"
|
||||
{!../../docs_src/bigger_applications/app/main.py!}
|
||||
```
|
||||
{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[14:17] title["app/main.py"] *}
|
||||
|
||||
Auf diese Weise bleibt der ursprüngliche `APIRouter` unverändert, sodass wir dieselbe `app/internal/admin.py`-Datei weiterhin mit anderen Projekten in der Organisation teilen können.
|
||||
|
||||
@@ -497,9 +447,7 @@ Wir können *Pfadoperationen* auch direkt zur `FastAPI`-App hinzufügen.
|
||||
|
||||
Hier machen wir es ... nur um zu zeigen, dass wir es können 🤷:
|
||||
|
||||
```Python hl_lines="21-23" title="app/main.py"
|
||||
{!../../docs_src/bigger_applications/app/main.py!}
|
||||
```
|
||||
{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[21:23] title["app/main.py"] *}
|
||||
|
||||
und es wird korrekt funktionieren, zusammen mit allen anderen *Pfadoperationen*, die mit `app.include_router()` hinzugefügt wurden.
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ Ihre API hat jetzt die Macht, ihre eigene <abbr title="Das ist ein Scherz, nur f
|
||||
|
||||
Sie können die Modellkonfiguration von Pydantic verwenden, um `extra` Felder zu verbieten (`forbid`):
|
||||
|
||||
{* ../../docs_src/cookie_param_models/tutorial002_an_py39.py hl[10] *}
|
||||
{* ../../docs_src/cookie_param_models/tutorial002_an_py310.py hl[10] *}
|
||||
|
||||
Wenn ein Client versucht, einige **zusätzliche Cookies** zu senden, erhält er eine **Error-<abbr title="Response – Antwort: Daten, die der Server zum anfragenden Client zurücksendet">Response</abbr>**.
|
||||
|
||||
|
||||
@@ -127,7 +127,7 @@ Um diesen zu überschreiben, importieren Sie den `RequestValidationError` und ve
|
||||
|
||||
Der Exceptionhandler erhält einen `Request` und die Exception.
|
||||
|
||||
{* ../../docs_src/handling_errors/tutorial004.py hl[2,14:16] *}
|
||||
{* ../../docs_src/handling_errors/tutorial004.py hl[2,14:19] *}
|
||||
|
||||
Wenn Sie nun zu `/items/foo` gehen, erhalten Sie anstelle des standardmäßigen JSON-Fehlers mit:
|
||||
|
||||
@@ -149,36 +149,17 @@ Wenn Sie nun zu `/items/foo` gehen, erhalten Sie anstelle des standardmäßigen
|
||||
eine Textversion mit:
|
||||
|
||||
```
|
||||
1 validation error
|
||||
path -> item_id
|
||||
value is not a valid integer (type=type_error.integer)
|
||||
Validation errors:
|
||||
Field: ('path', 'item_id'), Error: Input should be a valid integer, unable to parse string as an integer
|
||||
```
|
||||
|
||||
#### `RequestValidationError` vs. `ValidationError` { #requestvalidationerror-vs-validationerror }
|
||||
|
||||
/// warning | Achtung
|
||||
|
||||
Dies sind technische Details, die Sie überspringen können, wenn sie für Sie jetzt nicht wichtig sind.
|
||||
|
||||
///
|
||||
|
||||
`RequestValidationError` ist eine Unterklasse von Pydantics <a href="https://docs.pydantic.dev/latest/concepts/models/#error-handling" class="external-link" target="_blank">`ValidationError`</a>.
|
||||
|
||||
**FastAPI** verwendet diesen so, dass, wenn Sie ein Pydantic-Modell in `response_model` verwenden und Ihre Daten einen Fehler haben, Sie den Fehler in Ihrem Log sehen.
|
||||
|
||||
Aber der Client/Benutzer wird ihn nicht sehen. Stattdessen erhält der Client einen „Internal Server Error“ mit einem HTTP-Statuscode `500`.
|
||||
|
||||
Es sollte so sein, denn wenn Sie einen Pydantic `ValidationError` in Ihrer *Response* oder irgendwo anders in Ihrem Code haben (nicht im *Request* des Clients), ist es tatsächlich ein Fehler in Ihrem Code.
|
||||
|
||||
Und während Sie den Fehler beheben, sollten Ihre Clients/Benutzer keinen Zugriff auf interne Informationen über den Fehler haben, da das eine Sicherheitslücke aufdecken könnte.
|
||||
|
||||
### Überschreiben des `HTTPException`-Fehlerhandlers { #override-the-httpexception-error-handler }
|
||||
|
||||
Auf die gleiche Weise können Sie den `HTTPException`-Handler überschreiben.
|
||||
|
||||
Zum Beispiel könnten Sie eine Klartext-Response statt JSON für diese Fehler zurückgeben wollen:
|
||||
|
||||
{* ../../docs_src/handling_errors/tutorial004.py hl[3:4,9:11,22] *}
|
||||
{* ../../docs_src/handling_errors/tutorial004.py hl[3:4,9:11,25] *}
|
||||
|
||||
/// note | Technische Details
|
||||
|
||||
@@ -188,6 +169,14 @@ Sie könnten auch `from starlette.responses import PlainTextResponse` verwenden.
|
||||
|
||||
///
|
||||
|
||||
/// warning | Achtung
|
||||
|
||||
Beachten Sie, dass der `RequestValidationError` Informationen über den Dateinamen und die Zeile enthält, in der der Validierungsfehler auftritt, sodass Sie ihn bei Bedarf mit den relevanten Informationen in Ihren Logs anzeigen können.
|
||||
|
||||
Das bedeutet aber auch, dass, wenn Sie ihn einfach in einen String umwandeln und diese Informationen direkt zurückgeben, Sie möglicherweise ein paar Informationen über Ihr System preisgeben. Daher extrahiert und zeigt der Code hier jeden Fehler getrennt.
|
||||
|
||||
///
|
||||
|
||||
### Verwenden des `RequestValidationError`-Bodys { #use-the-requestvalidationerror-body }
|
||||
|
||||
Der `RequestValidationError` enthält den empfangenen `body` mit den ungültigen Daten.
|
||||
|
||||
@@ -122,63 +122,13 @@ Sie verfügt über eine `POST`-Operation, die mehrere Fehler zurückgeben könnt
|
||||
|
||||
Beide *Pfadoperationen* erfordern einen `X-Token`-Header.
|
||||
|
||||
//// tab | Python 3.10+
|
||||
|
||||
```Python
|
||||
{!> ../../docs_src/app_testing/app_b_an_py310/main.py!}
|
||||
```
|
||||
|
||||
////
|
||||
|
||||
//// tab | Python 3.9+
|
||||
|
||||
```Python
|
||||
{!> ../../docs_src/app_testing/app_b_an_py39/main.py!}
|
||||
```
|
||||
|
||||
////
|
||||
|
||||
//// tab | Python 3.8+
|
||||
|
||||
```Python
|
||||
{!> ../../docs_src/app_testing/app_b_an/main.py!}
|
||||
```
|
||||
|
||||
////
|
||||
|
||||
//// tab | Python 3.10+ nicht annotiert
|
||||
|
||||
/// tip | Tipp
|
||||
|
||||
Bevorzugen Sie die `Annotated`-Version, falls möglich.
|
||||
|
||||
///
|
||||
|
||||
```Python
|
||||
{!> ../../docs_src/app_testing/app_b_py310/main.py!}
|
||||
```
|
||||
|
||||
////
|
||||
|
||||
//// tab | Python 3.8+ nicht annotiert
|
||||
|
||||
/// tip | Tipp
|
||||
|
||||
Bevorzugen Sie die `Annotated`-Version, falls möglich.
|
||||
|
||||
///
|
||||
|
||||
```Python
|
||||
{!> ../../docs_src/app_testing/app_b/main.py!}
|
||||
```
|
||||
|
||||
////
|
||||
{* ../../docs_src/app_testing/app_b_an_py310/main.py *}
|
||||
|
||||
### Erweiterte Testdatei { #extended-testing-file }
|
||||
|
||||
Anschließend könnten Sie `test_main.py` mit den erweiterten Tests aktualisieren:
|
||||
|
||||
{* ../../docs_src/app_testing/app_b/test_main.py *}
|
||||
{* ../../docs_src/app_testing/app_b_an_py310/test_main.py *}
|
||||
|
||||
|
||||
Wenn Sie möchten, dass der Client Informationen im Request übergibt und Sie nicht wissen, wie das geht, können Sie suchen (googeln), wie es mit `httpx` gemacht wird, oder sogar, wie es mit `requests` gemacht wird, da das Design von HTTPX auf dem Design von Requests basiert.
|
||||
|
||||
@@ -1,430 +0,0 @@
|
||||
Articles:
|
||||
English:
|
||||
- author: Apitally
|
||||
author_link: https://apitally.io
|
||||
link: https://apitally.io/blog/getting-started-with-logging-in-fastapi
|
||||
title: Getting started with logging in FastAPI
|
||||
- author: Balthazar Rouberol
|
||||
author_link: https://balthazar-rouberol.com
|
||||
link: https://blog.balthazar-rouberol.com/how-to-profile-a-fastapi-asynchronous-request
|
||||
title: How to profile a FastAPI asynchronous request
|
||||
- author: Stephen Siegert - Neon
|
||||
link: https://neon.tech/blog/deploy-a-serverless-fastapi-app-with-neon-postgres-and-aws-app-runner-at-any-scale
|
||||
title: Deploy a Serverless FastAPI App with Neon Postgres and AWS App Runner at any scale
|
||||
- author: Kurtis Pykes - NVIDIA
|
||||
link: https://developer.nvidia.com/blog/building-a-machine-learning-microservice-with-fastapi/
|
||||
title: Building a Machine Learning Microservice with FastAPI
|
||||
- author: Ravgeet Dhillon - Twilio
|
||||
link: https://www.twilio.com/en-us/blog/booking-appointments-twilio-notion-fastapi
|
||||
title: Booking Appointments with Twilio, Notion, and FastAPI
|
||||
- author: Abhinav Tripathi - Microsoft Blogs
|
||||
link: https://devblogs.microsoft.com/cosmosdb/azure-cosmos-db-python-and-fastapi/
|
||||
title: Write a Python data layer with Azure Cosmos DB and FastAPI
|
||||
- author: Donny Peeters
|
||||
author_link: https://github.com/Donnype
|
||||
link: https://bitestreams.com/blog/fastapi-sqlalchemy/
|
||||
title: 10 Tips for adding SQLAlchemy to FastAPI
|
||||
- author: Jessica Temporal
|
||||
author_link: https://jtemporal.com/socials
|
||||
link: https://jtemporal.com/tips-on-migrating-from-flask-to-fastapi-and-vice-versa/
|
||||
title: Tips on migrating from Flask to FastAPI and vice-versa
|
||||
- author: Ankit Anchlia
|
||||
author_link: https://linkedin.com/in/aanchlia21
|
||||
link: https://hackernoon.com/explore-how-to-effectively-use-jwt-with-fastapi
|
||||
title: Explore How to Effectively Use JWT With FastAPI
|
||||
- author: Nicoló Lino
|
||||
author_link: https://www.nlino.com
|
||||
link: https://github.com/softwarebloat/python-tracing-demo
|
||||
title: Instrument FastAPI with OpenTelemetry tracing and visualize traces in Grafana Tempo.
|
||||
- author: Mikhail Rozhkov, Elena Samuylova
|
||||
author_link: https://www.linkedin.com/in/mnrozhkov/
|
||||
link: https://www.evidentlyai.com/blog/fastapi-tutorial
|
||||
title: ML serving and monitoring with FastAPI and Evidently
|
||||
- author: Visual Studio Code Team
|
||||
author_link: https://code.visualstudio.com/
|
||||
link: https://code.visualstudio.com/docs/python/tutorial-fastapi
|
||||
title: FastAPI Tutorial in Visual Studio Code
|
||||
- author: Apitally
|
||||
author_link: https://apitally.io
|
||||
link: https://blog.apitally.io/fastapi-application-monitoring-made-easy
|
||||
title: FastAPI application monitoring made easy
|
||||
- author: John Philip
|
||||
author_link: https://medium.com/@amjohnphilip
|
||||
link: https://python.plainenglish.io/building-a-restful-api-with-fastapi-secure-signup-and-login-functionality-included-45cdbcb36106
|
||||
title: "Building a RESTful API with FastAPI: Secure Signup and Login Functionality Included"
|
||||
- author: Keshav Malik
|
||||
author_link: https://theinfosecguy.xyz/
|
||||
link: https://blog.theinfosecguy.xyz/building-a-crud-api-with-fastapi-and-supabase-a-step-by-step-guide
|
||||
title: Building a CRUD API with FastAPI and Supabase
|
||||
- author: Adejumo Ridwan Suleiman
|
||||
author_link: https://www.linkedin.com/in/adejumoridwan/
|
||||
link: https://medium.com/python-in-plain-english/build-an-sms-spam-classifier-serverless-database-with-faunadb-and-fastapi-23dbb275bc5b
|
||||
title: Build an SMS Spam Classifier Serverless Database with FaunaDB and FastAPI
|
||||
- author: Raf Rasenberg
|
||||
author_link: https://rafrasenberg.com/about/
|
||||
link: https://rafrasenberg.com/fastapi-lambda/
|
||||
title: 'FastAPI lambda container: serverless simplified'
|
||||
- author: Teresa N. Fontanella De Santis
|
||||
author_link: https://dev.to/
|
||||
link: https://dev.to/teresafds/authorization-on-fastapi-with-casbin-41og
|
||||
title: Authorization on FastAPI with Casbin
|
||||
- author: New Relic
|
||||
author_link: https://newrelic.com
|
||||
link: https://newrelic.com/instant-observability/fastapi/e559ec64-f765-4470-a15f-1901fcebb468
|
||||
title: How to monitor FastAPI application performance using Python agent
|
||||
- author: Jean-Baptiste Rocher
|
||||
author_link: https://hashnode.com/@jibrocher
|
||||
link: https://dev.indooroutdoor.io/series/fastapi-react-poll-app
|
||||
title: Building the Poll App From Django Tutorial With FastAPI And React
|
||||
- author: Silvan Melchior
|
||||
author_link: https://github.com/silvanmelchior
|
||||
link: https://blog.devgenius.io/seamless-fastapi-configuration-with-confz-90949c14ea12
|
||||
title: Seamless FastAPI Configuration with ConfZ
|
||||
- author: Kaustubh Gupta
|
||||
author_link: https://medium.com/@kaustubhgupta1828/
|
||||
link: https://levelup.gitconnected.com/5-advance-features-of-fastapi-you-should-try-7c0ac7eebb3e
|
||||
title: 5 Advanced Features of FastAPI You Should Try
|
||||
- author: Kaustubh Gupta
|
||||
author_link: https://medium.com/@kaustubhgupta1828/
|
||||
link: https://www.analyticsvidhya.com/blog/2021/06/deploying-ml-models-as-api-using-fastapi-and-heroku/
|
||||
title: Deploying ML Models as API Using FastAPI and Heroku
|
||||
- link: https://jarmos.netlify.app/posts/using-github-actions-to-deploy-a-fastapi-project-to-heroku/
|
||||
title: Using GitHub Actions to Deploy a FastAPI Project to Heroku
|
||||
author_link: https://jarmos.netlify.app/
|
||||
author: Somraj Saha
|
||||
- author: "@pystar"
|
||||
author_link: https://pystar.substack.com/
|
||||
link: https://pystar.substack.com/p/how-to-create-a-fake-certificate
|
||||
title: How to Create A Fake Certificate Authority And Generate TLS Certs for FastAPI
|
||||
- author: Ben Gamble
|
||||
author_link: https://uk.linkedin.com/in/bengamble7
|
||||
link: https://ably.com/blog/realtime-ticket-booking-solution-kafka-fastapi-ably
|
||||
title: Building a realtime ticket booking solution with Kafka, FastAPI, and Ably
|
||||
- author: Shahriyar(Shako) Rzayev
|
||||
author_link: https://www.linkedin.com/in/shahriyar-rzayev/
|
||||
link: https://www.azepug.az/posts/fastapi/#building-simple-e-commerce-with-nuxtjs-and-fastapi-series
|
||||
title: Building simple E-Commerce with NuxtJS and FastAPI
|
||||
- author: Rodrigo Arenas
|
||||
author_link: https://rodrigo-arenas.medium.com/
|
||||
link: https://medium.com/analytics-vidhya/serve-a-machine-learning-model-using-sklearn-fastapi-and-docker-85aabf96729b
|
||||
title: "Serve a machine learning model using Sklearn, FastAPI and Docker"
|
||||
- author: Yashasvi Singh
|
||||
author_link: https://hashnode.com/@aUnicornDev
|
||||
link: https://aunicorndev.hashnode.dev/series/supafast-api
|
||||
title: "Building an API with FastAPI and Supabase and Deploying on Deta"
|
||||
- author: Navule Pavan Kumar Rao
|
||||
author_link: https://www.linkedin.com/in/navule/
|
||||
link: https://www.tutlinks.com/deploy-fastapi-on-ubuntu-gunicorn-caddy-2/
|
||||
title: Deploy FastAPI on Ubuntu and Serve using Caddy 2 Web Server
|
||||
- author: Patrick Ladon
|
||||
author_link: https://dev.to/factorlive
|
||||
link: https://dev.to/factorlive/python-facebook-messenger-webhook-with-fastapi-on-glitch-4n90
|
||||
title: Python Facebook messenger webhook with FastAPI on Glitch
|
||||
- author: Valon Januzaj
|
||||
author_link: https://www.linkedin.com/in/valon-januzaj-b02692187/
|
||||
link: https://valonjanuzaj.medium.com/deploy-a-dockerized-fastapi-application-to-aws-cc757830ba1b
|
||||
title: Deploy a dockerized FastAPI application to AWS
|
||||
- author: Amit Chaudhary
|
||||
author_link: https://x.com/amitness
|
||||
link: https://amitness.com/2020/06/fastapi-vs-flask/
|
||||
title: FastAPI for Flask Users
|
||||
- author: Louis Guitton
|
||||
author_link: https://x.com/louis_guitton
|
||||
link: https://guitton.co/posts/fastapi-monitoring/
|
||||
title: How to monitor your FastAPI service
|
||||
- author: Precious Ndubueze
|
||||
author_link: https://medium.com/@gabbyprecious2000
|
||||
link: https://medium.com/@gabbyprecious2000/creating-a-crud-app-with-fastapi-part-one-7c049292ad37
|
||||
title: Creating a CRUD App with FastAPI (Part one)
|
||||
- author: Farhad Malik
|
||||
author_link: https://medium.com/@farhadmalik
|
||||
link: https://towardsdatascience.com/build-and-host-fast-data-science-applications-using-fastapi-823be8a1d6a0
|
||||
title: Build And Host Fast Data Science Applications Using FastAPI
|
||||
- author: Navule Pavan Kumar Rao
|
||||
author_link: https://www.linkedin.com/in/navule/
|
||||
link: https://www.tutlinks.com/deploy-fastapi-on-azure/
|
||||
title: Deploy FastAPI on Azure App Service
|
||||
- author: Davide Fiocco
|
||||
author_link: https://github.com/davidefiocco
|
||||
link: https://davidefiocco.github.io/streamlit-fastapi-ml-serving/
|
||||
title: Machine learning model serving in Python using FastAPI and streamlit
|
||||
- author: Netflix
|
||||
author_link: https://netflixtechblog.com/
|
||||
link: https://netflixtechblog.com/introducing-dispatch-da4b8a2a8072
|
||||
title: Introducing Dispatch
|
||||
- author: Stavros Korokithakis
|
||||
author_link: https://x.com/Stavros
|
||||
link: https://www.stavros.io/posts/fastapi-with-django/
|
||||
title: Using FastAPI with Django
|
||||
- author: Twilio
|
||||
author_link: https://www.twilio.com
|
||||
link: https://www.twilio.com/blog/build-secure-twilio-webhook-python-fastapi
|
||||
title: Build a Secure Twilio Webhook with Python and FastAPI
|
||||
- author: Sebastián Ramírez (tiangolo)
|
||||
author_link: https://x.com/tiangolo
|
||||
link: https://dev.to/tiangolo/build-a-web-api-from-scratch-with-fastapi-the-workshop-2ehe
|
||||
title: Build a web API from scratch with FastAPI - the workshop
|
||||
- author: Paul Sec
|
||||
author_link: https://x.com/PaulWebSec
|
||||
link: https://paulsec.github.io/posts/fastapi_plus_zeit_serverless_fu/
|
||||
title: FastAPI + Zeit.co = 🚀
|
||||
- author: cuongld2
|
||||
author_link: https://dev.to/cuongld2
|
||||
link: https://dev.to/cuongld2/build-simple-api-service-with-python-fastapi-part-1-581o
|
||||
title: Build simple API service with Python FastAPI — Part 1
|
||||
- author: Paurakh Sharma Humagain
|
||||
author_link: https://x.com/PaurakhSharma
|
||||
link: https://dev.to/paurakhsharma/microservice-in-python-using-fastapi-24cc
|
||||
title: Microservice in Python using FastAPI
|
||||
- author: Guillermo Cruz
|
||||
author_link: https://wuilly.com/
|
||||
link: https://wuilly.com/2019/10/real-time-notifications-with-python-and-postgres/
|
||||
title: Real-time Notifications with Python and Postgres
|
||||
- author: Navule Pavan Kumar Rao
|
||||
author_link: https://www.linkedin.com/in/navule/
|
||||
link: https://www.tutlinks.com/create-and-deploy-fastapi-app-to-heroku/
|
||||
title: Create and Deploy FastAPI app to Heroku without using Docker
|
||||
- author: Arthur Henrique
|
||||
author_link: https://x.com/arthurheinrique
|
||||
link: https://medium.com/@arthur393/another-boilerplate-to-fastapi-azure-pipeline-ci-pytest-3c8d9a4be0bb
|
||||
title: 'Another Boilerplate to FastAPI: Azure Pipeline CI + Pytest'
|
||||
- author: Shane Soh
|
||||
author_link: https://medium.com/@shane.soh
|
||||
link: https://medium.com/analytics-vidhya/deploy-machine-learning-models-with-keras-fastapi-redis-and-docker-4940df614ece
|
||||
title: Deploy Machine Learning Models with Keras, FastAPI, Redis and Docker
|
||||
- author: Mandy Gu
|
||||
author_link: https://towardsdatascience.com/@mandygu
|
||||
link: https://towardsdatascience.com/deploying-iris-classifications-with-fastapi-and-docker-7c9b83fdec3a
|
||||
title: 'Towards Data Science: Deploying Iris Classifications with FastAPI and Docker'
|
||||
- author: Michael Herman
|
||||
author_link: https://testdriven.io/authors/herman
|
||||
link: https://testdriven.io/blog/fastapi-crud/
|
||||
title: 'TestDriven.io: Developing and Testing an Asynchronous API with FastAPI and Pytest'
|
||||
- author: Bernard Brenyah
|
||||
author_link: https://medium.com/@bbrenyah
|
||||
link: https://medium.com/python-data/how-to-deploy-tensorflow-2-0-models-as-an-api-service-with-fastapi-docker-128b177e81f3
|
||||
title: How To Deploy Tensorflow 2.0 Models As An API Service With FastAPI & Docker
|
||||
- author: Dylan Anthony
|
||||
author_link: https://dev.to/dbanty
|
||||
link: https://dev.to/dbanty/why-i-m-leaving-flask-3ki6
|
||||
title: Why I'm Leaving Flask
|
||||
- author: Mike Moritz
|
||||
author_link: https://medium.com/@mike.p.moritz
|
||||
link: https://medium.com/@mike.p.moritz/using-docker-compose-to-deploy-a-lightweight-python-rest-api-with-a-job-queue-37e6072a209b
|
||||
title: Using Docker Compose to deploy a lightweight Python REST API with a job queue
|
||||
- author: '@euri10'
|
||||
author_link: https://gitlab.com/euri10
|
||||
link: https://gitlab.com/euri10/fastapi_cheatsheet
|
||||
title: A FastAPI and Swagger UI visual cheatsheet
|
||||
- author: Uber Engineering
|
||||
author_link: https://eng.uber.com
|
||||
link: https://eng.uber.com/ludwig-v0-2/
|
||||
title: 'Uber: Ludwig v0.2 Adds New Features and Other Improvements to its Deep Learning Toolbox [including a FastAPI server]'
|
||||
- author: Maarten Grootendorst
|
||||
author_link: https://www.linkedin.com/in/mgrootendorst/
|
||||
link: https://towardsdatascience.com/how-to-deploy-a-machine-learning-model-dc51200fe8cf
|
||||
title: How to Deploy a Machine Learning Model
|
||||
- author: Johannes Gontrum
|
||||
author_link: https://x.com/gntrm
|
||||
link: https://medium.com/@gntrm/jwt-authentication-with-fastapi-and-aws-cognito-1333f7f2729e
|
||||
title: JWT Authentication with FastAPI and AWS Cognito
|
||||
- author: Ankush Thakur
|
||||
author_link: https://geekflare.com/author/ankush/
|
||||
link: https://geekflare.com/python-asynchronous-web-frameworks/
|
||||
title: Top 5 Asynchronous Web Frameworks for Python
|
||||
- author: Nico Axtmann
|
||||
author_link: https://www.linkedin.com/in/nico-axtmann
|
||||
link: https://medium.com/@nico.axtmann95/deploying-a-scikit-learn-model-with-onnx-und-fastapi-1af398268915
|
||||
title: Deploying a scikit-learn model with ONNX and FastAPI
|
||||
- author: Nils de Bruin
|
||||
author_link: https://medium.com/@nilsdebruin
|
||||
link: https://medium.com/data-rebels/fastapi-authentication-revisited-enabling-api-key-authentication-122dc5975680
|
||||
title: 'FastAPI authentication revisited: Enabling API key authentication'
|
||||
- author: Nick Cortale
|
||||
author_link: https://nickc1.github.io/
|
||||
link: https://nickc1.github.io/api,/scikit-learn/2019/01/10/scikit-fastapi.html
|
||||
title: 'FastAPI and Scikit-Learn: Easily Deploy Models'
|
||||
- author: Errieta Kostala
|
||||
author_link: https://dev.to/errietta
|
||||
link: https://dev.to/errietta/introduction-to-the-fastapi-python-framework-2n10
|
||||
title: Introduction to the fastapi python framework
|
||||
- author: Nils de Bruin
|
||||
author_link: https://medium.com/@nilsdebruin
|
||||
link: https://medium.com/data-rebels/fastapi-how-to-add-basic-and-cookie-authentication-a45c85ef47d3
|
||||
title: FastAPI — How to add basic and cookie authentication
|
||||
- author: Nils de Bruin
|
||||
author_link: https://medium.com/@nilsdebruin
|
||||
link: https://medium.com/data-rebels/fastapi-google-as-an-external-authentication-provider-3a527672cf33
|
||||
title: FastAPI — Google as an external authentication provider
|
||||
- author: William Hayes
|
||||
author_link: https://medium.com/@williamhayes
|
||||
link: https://medium.com/@williamhayes/fastapi-starlette-debug-vs-prod-5f7561db3a59
|
||||
title: FastAPI/Starlette debug vs prod
|
||||
- author: Mukul Mantosh
|
||||
author_link: https://x.com/MantoshMukul
|
||||
link: https://www.jetbrains.com/pycharm/guide/tutorials/fastapi-aws-kubernetes/
|
||||
title: Developing FastAPI Application using K8s & AWS
|
||||
- author: KrishNa
|
||||
author_link: https://medium.com/@krishnardt365
|
||||
link: https://medium.com/@krishnardt365/fastapi-docker-and-postgres-91943e71be92
|
||||
title: Fastapi, Docker(Docker compose) and Postgres
|
||||
- author: Devon Ray
|
||||
author_link: https://devonray.com
|
||||
link: https://devonray.com/blog/deploying-a-fastapi-project-using-aws-lambda-aurora-cdk
|
||||
title: Deployment using Docker, Lambda, Aurora, CDK & GH Actions
|
||||
- author: Shubhendra Kushwaha
|
||||
author_link: https://www.linkedin.com/in/theshubhendra/
|
||||
link: https://theshubhendra.medium.com/mastering-soft-delete-advanced-sqlalchemy-techniques-4678f4738947
|
||||
title: 'Mastering Soft Delete: Advanced SQLAlchemy Techniques'
|
||||
- author: Shubhendra Kushwaha
|
||||
author_link: https://www.linkedin.com/in/theshubhendra/
|
||||
link: https://theshubhendra.medium.com/role-based-row-filtering-advanced-sqlalchemy-techniques-733e6b1328f6
|
||||
title: 'Role based row filtering: Advanced SQLAlchemy Techniques'
|
||||
German:
|
||||
- author: Marcel Sander (actidoo)
|
||||
author_link: https://www.actidoo.com
|
||||
link: https://www.actidoo.com/de/blog/python-fastapi-domain-driven-design
|
||||
title: Domain-driven Design mit Python und FastAPI
|
||||
- author: Nico Axtmann
|
||||
author_link: https://x.com/_nicoax
|
||||
link: https://blog.codecentric.de/2019/08/inbetriebnahme-eines-scikit-learn-modells-mit-onnx-und-fastapi/
|
||||
title: Inbetriebnahme eines scikit-learn-Modells mit ONNX und FastAPI
|
||||
- author: Felix Schürmeyer
|
||||
author_link: https://hellocoding.de/autor/felix-schuermeyer/
|
||||
link: https://hellocoding.de/blog/coding-language/python/fastapi
|
||||
title: REST-API Programmieren mittels Python und dem FastAPI Modul
|
||||
Japanese:
|
||||
- author: '@bee2'
|
||||
author_link: https://qiita.com/bee2
|
||||
link: https://qiita.com/bee2/items/75d9c0d7ba20e7a4a0e9
|
||||
title: '[FastAPI] Python製のASGI Web フレームワーク FastAPIに入門する'
|
||||
- author: '@bee2'
|
||||
author_link: https://qiita.com/bee2
|
||||
link: https://qiita.com/bee2/items/0ad260ab9835a2087dae
|
||||
title: PythonのWeb frameworkのパフォーマンス比較 (Django, Flask, responder, FastAPI, japronto)
|
||||
- author: ライトコードメディア編集部
|
||||
author_link: https://rightcode.co.jp/author/jun
|
||||
link: https://rightcode.co.jp/blog/information-technology/fastapi-tutorial-todo-apps-admin-page-improvement
|
||||
title: '【第4回】FastAPIチュートリアル: toDoアプリを作ってみよう【管理者ページ改良編】'
|
||||
- author: ライトコードメディア編集部
|
||||
author_link: https://rightcode.co.jp/author/jun
|
||||
link: https://rightcode.co.jp/blog/information-technology/fastapi-tutorial-todo-apps-authentication-user-registration
|
||||
title: '【第3回】FastAPIチュートリアル: toDoアプリを作ってみよう【認証・ユーザ登録編】'
|
||||
- author: ライトコードメディア編集部
|
||||
author_link: https://rightcode.co.jp/author/jun
|
||||
link: https://rightcode.co.jp/blog/information-technology/fastapi-tutorial-todo-apps-model-building
|
||||
title: '【第2回】FastAPIチュートリアル: ToDoアプリを作ってみよう【モデル構築編】'
|
||||
- author: ライトコードメディア編集部
|
||||
author_link: https://rightcode.co.jp/author/jun
|
||||
link: https://rightcode.co.jp/blog/information-technology/fastapi-tutorial-todo-apps-environment
|
||||
title: '【第1回】FastAPIチュートリアル: ToDoアプリを作ってみよう【環境構築編】'
|
||||
- author: Hikaru Takahashi
|
||||
author_link: https://qiita.com/hikarut
|
||||
link: https://qiita.com/hikarut/items/b178af2e2440c67c6ac4
|
||||
title: フロントエンド開発者向けのDockerによるPython開発環境構築
|
||||
- author: '@angel_katayoku'
|
||||
author_link: https://qiita.com/angel_katayoku
|
||||
link: https://qiita.com/angel_katayoku/items/8a458a8952f50b73f420
|
||||
title: FastAPIでPOSTされたJSONのレスポンスbodyを受け取る
|
||||
- author: '@angel_katayoku'
|
||||
author_link: https://qiita.com/angel_katayoku
|
||||
link: https://qiita.com/angel_katayoku/items/4fbc1a4e2b33fa2237d2
|
||||
title: FastAPIをMySQLと接続してDockerで管理してみる
|
||||
- author: '@angel_katayoku'
|
||||
author_link: https://qiita.com/angel_katayoku
|
||||
link: https://qiita.com/angel_katayoku/items/0e1f5dbbe62efc612a78
|
||||
title: FastAPIでCORSを回避
|
||||
- author: '@ryoryomaru'
|
||||
author_link: https://qiita.com/ryoryomaru
|
||||
link: https://qiita.com/ryoryomaru/items/59958ed385b3571d50de
|
||||
title: python製の最新APIフレームワーク FastAPI を触ってみた
|
||||
- author: '@mtitg'
|
||||
author_link: https://qiita.com/mtitg
|
||||
link: https://qiita.com/mtitg/items/47770e9a562dd150631d
|
||||
title: FastAPI|DB接続してCRUDするPython製APIサーバーを構築
|
||||
Portuguese:
|
||||
- author: Eduardo Mendes
|
||||
author_link: https://bolha.us/@dunossauro
|
||||
link: https://fastapidozero.dunossauro.com/
|
||||
title: FastAPI do ZERO
|
||||
- author: Jessica Temporal
|
||||
author_link: https://jtemporal.com/socials
|
||||
link: https://jtemporal.com/dicas-para-migrar-de-flask-para-fastapi-e-vice-versa/
|
||||
title: Dicas para migrar uma aplicação de Flask para FastAPI e vice-versa
|
||||
Russian:
|
||||
- author: Troy Köhler
|
||||
author_link: https://www.linkedin.com/in/trkohler/
|
||||
link: https://trkohler.com/fast-api-introduction-to-framework
|
||||
title: 'FastAPI: знакомимся с фреймворком'
|
||||
- author: prostomarkeloff
|
||||
author_link: https://github.com/prostomarkeloff
|
||||
link: https://habr.com/ru/post/478620/
|
||||
title: Почему Вы должны попробовать FastAPI?
|
||||
- author: Andrey Korchak
|
||||
author_link: https://habr.com/ru/users/57uff3r/
|
||||
link: https://habr.com/ru/post/454440/
|
||||
title: 'Мелкая питонячая радость #2: Starlette - Солидная примочка – FastAPI'
|
||||
Vietnamese:
|
||||
- author: Nguyễn Nhân
|
||||
author_link: https://fullstackstation.com/author/figonking/
|
||||
link: https://fullstackstation.com/fastapi-trien-khai-bang-docker/
|
||||
title: 'FASTAPI: TRIỂN KHAI BẰNG DOCKER'
|
||||
Taiwanese:
|
||||
- author: Leon
|
||||
author_link: http://editor.leonh.space/
|
||||
link: https://editor.leonh.space/2022/tortoise/
|
||||
title: 'Tortoise ORM / FastAPI 整合快速筆記'
|
||||
Spanish:
|
||||
- author: Eduardo Zepeda
|
||||
author_link: https://coffeebytes.dev/en/authors/eduardo-zepeda/
|
||||
link: https://coffeebytes.dev/es/python-fastapi-el-mejor-framework-de-python/
|
||||
title: 'Tutorial de FastAPI, ¿el mejor framework de Python?'
|
||||
Podcasts:
|
||||
English:
|
||||
- author: Behind the Commit
|
||||
author_link: https://www.youtube.com/@BehindtheCommit
|
||||
link: https://youtu.be/iaDRYUQ0OMM
|
||||
title: Why FastAPI Became Python’s Fastest‑Growing Framework – Chat with Sebastián Ramírez
|
||||
- author: Real Python
|
||||
author_link: https://realpython.com/
|
||||
link: https://realpython.com/podcasts/rpp/72/
|
||||
title: Starting With FastAPI and Examining Python's Import System - Episode 72
|
||||
- author: Python Bytes FM
|
||||
author_link: https://pythonbytes.fm/
|
||||
link: https://www.pythonpodcast.com/fastapi-web-application-framework-episode-259/
|
||||
title: 'Do you dare to press "."? - Episode 247 - Dan #6: SQLModel - use the same models for SQL and FastAPI'
|
||||
- author: Podcast.`__init__`
|
||||
author_link: https://www.pythonpodcast.com/
|
||||
link: https://www.pythonpodcast.com/fastapi-web-application-framework-episode-259/
|
||||
title: Build The Next Generation Of Python Web Applications With FastAPI - Episode 259 - interview to Sebastían Ramírez (tiangolo)
|
||||
- author: Python Bytes FM
|
||||
author_link: https://pythonbytes.fm/
|
||||
link: https://pythonbytes.fm/episodes/show/123/time-to-right-the-py-wrongs?time_in_sec=855
|
||||
title: FastAPI on PythonBytes
|
||||
Talks:
|
||||
English:
|
||||
- author: Sebastián Ramírez (tiangolo)
|
||||
author_link: https://x.com/tiangolo
|
||||
link: https://www.youtube.com/watch?v=mwvmfl8nN_U
|
||||
title: 'Keynote: Behind the scenes of FastAPI and friends for developers and builders — Sebastián Ramírez'
|
||||
- author: Jeny Sadadia
|
||||
author_link: https://github.com/JenySadadia
|
||||
link: https://www.youtube.com/watch?v=uZdTe8_Z6BQ
|
||||
title: 'PyCon AU 2023: Testing asynchronous applications with FastAPI and pytest'
|
||||
- author: Sebastián Ramírez (tiangolo)
|
||||
author_link: https://x.com/tiangolo
|
||||
link: https://www.youtube.com/watch?v=PnpTY1f4k2U
|
||||
title: '[VIRTUAL] Py.Amsterdam''s flying Software Circus: Intro to FastAPI'
|
||||
- author: Sebastián Ramírez (tiangolo)
|
||||
author_link: https://x.com/tiangolo
|
||||
link: https://www.youtube.com/watch?v=z9K5pwb0rt8
|
||||
title: 'PyConBY 2020: Serve ML models easily with FastAPI'
|
||||
- author: Chris Withers
|
||||
author_link: https://x.com/chriswithers13
|
||||
link: https://www.youtube.com/watch?v=3DLwPcrE5mA
|
||||
title: 'PyCon UK 2019: FastAPI from the ground up'
|
||||
Taiwanese:
|
||||
- author: Blueswen
|
||||
author_link: https://github.com/blueswen
|
||||
link: https://www.youtube.com/watch?v=y3sumuoDq4w
|
||||
title: 'PyCon TW 2024: 全方位強化 Python 服務可觀測性:以 FastAPI 和 Grafana Stack 為例'
|
||||
@@ -15,7 +15,7 @@ Use as follows:
|
||||
|
||||
The tests:
|
||||
|
||||
## Code snippets { #code-snippets}
|
||||
## Code snippets { #code-snippets }
|
||||
|
||||
//// tab | Test
|
||||
|
||||
@@ -53,7 +53,7 @@ See for example section `### Quotes` in `docs/de/llm-prompt.md`.
|
||||
|
||||
////
|
||||
|
||||
## Quotes in code snippets { #quotes-in-code-snippets}
|
||||
## Quotes in code snippets { #quotes-in-code-snippets }
|
||||
|
||||
//// tab | Test
|
||||
|
||||
|
||||
@@ -1,36 +1,22 @@
|
||||
# External Links and Articles
|
||||
# External Links
|
||||
|
||||
**FastAPI** has a great community constantly growing.
|
||||
|
||||
There are many posts, articles, tools, and projects, related to **FastAPI**.
|
||||
|
||||
Here's an incomplete list of some of them.
|
||||
You could easily use a search engine or video platform to find many resources related to FastAPI.
|
||||
|
||||
/// tip
|
||||
/// info
|
||||
|
||||
If you have an article, project, tool, or anything related to **FastAPI** that is not yet listed here, create a <a href="https://github.com/fastapi/fastapi/edit/master/docs/en/data/external_links.yml" class="external-link" target="_blank">Pull Request adding it</a>.
|
||||
Before, this page used to list links to external articles.
|
||||
|
||||
But now that FastAPI is the backend framework with the most GitHub stars across languages, and the most starred and used framework in Python, it no longer makes sense to attempt to list all articles written about it.
|
||||
|
||||
///
|
||||
|
||||
{% for section_name, section_content in external_links.items() %}
|
||||
|
||||
## {{ section_name }}
|
||||
|
||||
{% for lang_name, lang_content in section_content.items() %}
|
||||
|
||||
### {{ lang_name }}
|
||||
|
||||
{% for item in lang_content %}
|
||||
|
||||
* <a href="{{ item.link }}" class="external-link" target="_blank">{{ item.title }}</a> by <a href="{{ item.author_link }}" class="external-link" target="_blank">{{ item.author }}</a>.
|
||||
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
|
||||
## GitHub Repositories
|
||||
|
||||
Most starred GitHub repositories with the topic `fastapi`:
|
||||
Most starred <a href="https://github.com/topics/fastapi" class="external-link" target="_blank">GitHub repositories with the topic `fastapi`</a>:
|
||||
|
||||
{% for repo in topic_repos %}
|
||||
|
||||
|
||||
@@ -52,13 +52,13 @@ The key features are:
|
||||
|
||||
<!-- sponsors -->
|
||||
|
||||
### Keystone Sponsor
|
||||
### Keystone Sponsor { #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 and Silver Sponsors
|
||||
### Gold and Silver Sponsors { #gold-and-silver-sponsors }
|
||||
|
||||
{% for sponsor in sponsors.gold -%}
|
||||
<a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}" style="border-radius:15px"></a>
|
||||
|
||||
@@ -7,6 +7,38 @@ hide:
|
||||
|
||||
## Latest Changes
|
||||
|
||||
## 0.124.4
|
||||
|
||||
### Fixes
|
||||
|
||||
* 🐛 Fix parameter aliases. PR [#14371](https://github.com/fastapi/fastapi/pull/14371) by [@YuriiMotov](https://github.com/YuriiMotov).
|
||||
|
||||
## 0.124.3
|
||||
|
||||
### Fixes
|
||||
|
||||
* 🐛 Fix support for tagged union with discriminator inside of `Annotated` with `Body()`. PR [#14512](https://github.com/fastapi/fastapi/pull/14512) by [@tiangolo](https://github.com/tiangolo).
|
||||
|
||||
### Refactors
|
||||
|
||||
* ✅ Add set of tests for request parameters and alias. PR [#14358](https://github.com/fastapi/fastapi/pull/14358) by [@YuriiMotov](https://github.com/YuriiMotov).
|
||||
|
||||
### Docs
|
||||
|
||||
* 📝 Tweak links format. PR [#14505](https://github.com/fastapi/fastapi/pull/14505) by [@tiangolo](https://github.com/tiangolo).
|
||||
* 📝 Update docs about re-raising validation errors, do not include string as is to not leak information. PR [#14487](https://github.com/fastapi/fastapi/pull/14487) by [@tiangolo](https://github.com/tiangolo).
|
||||
* 🔥 Remove external links section. PR [#14486](https://github.com/fastapi/fastapi/pull/14486) by [@tiangolo](https://github.com/tiangolo).
|
||||
|
||||
### Translations
|
||||
|
||||
* 🌐 Sync Russian docs. PR [#14509](https://github.com/fastapi/fastapi/pull/14509) by [@YuriiMotov](https://github.com/YuriiMotov).
|
||||
* 🌐 Sync German docs. PR [#14488](https://github.com/fastapi/fastapi/pull/14488) by [@nilslindemann](https://github.com/nilslindemann).
|
||||
|
||||
### Internal
|
||||
|
||||
* 👷 Tweak coverage to not pass Smokeshow max file size limit. PR [#14507](https://github.com/fastapi/fastapi/pull/14507) by [@tiangolo](https://github.com/tiangolo).
|
||||
* ✅ Expand test matrix to include Windows and MacOS. PR [#14171](https://github.com/fastapi/fastapi/pull/14171) by [@svlandeg](https://github.com/svlandeg).
|
||||
|
||||
## 0.124.2
|
||||
|
||||
### Fixes
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
# Resources { #resources }
|
||||
|
||||
Additional resources, external links, articles and more. ✈️
|
||||
Additional resources, external links, and more. ✈️
|
||||
|
||||
@@ -127,7 +127,7 @@ To override it, import the `RequestValidationError` and use it with `@app.except
|
||||
|
||||
The exception handler will receive a `Request` and the exception.
|
||||
|
||||
{* ../../docs_src/handling_errors/tutorial004.py hl[2,14:16] *}
|
||||
{* ../../docs_src/handling_errors/tutorial004.py hl[2,14:19] *}
|
||||
|
||||
Now, if you go to `/items/foo`, instead of getting the default JSON error with:
|
||||
|
||||
@@ -149,36 +149,17 @@ Now, if you go to `/items/foo`, instead of getting the default JSON error with:
|
||||
you will get a text version, with:
|
||||
|
||||
```
|
||||
1 validation error
|
||||
path -> item_id
|
||||
value is not a valid integer (type=type_error.integer)
|
||||
Validation errors:
|
||||
Field: ('path', 'item_id'), Error: Input should be a valid integer, unable to parse string as an integer
|
||||
```
|
||||
|
||||
#### `RequestValidationError` vs `ValidationError` { #requestvalidationerror-vs-validationerror }
|
||||
|
||||
/// warning
|
||||
|
||||
These are technical details that you might skip if it's not important for you now.
|
||||
|
||||
///
|
||||
|
||||
`RequestValidationError` is a sub-class of Pydantic's <a href="https://docs.pydantic.dev/latest/concepts/models/#error-handling" class="external-link" target="_blank">`ValidationError`</a>.
|
||||
|
||||
**FastAPI** uses it so that, if you use a Pydantic model in `response_model`, and your data has an error, you will see the error in your log.
|
||||
|
||||
But the client/user will not see it. Instead, the client will receive an "Internal Server Error" with an HTTP status code `500`.
|
||||
|
||||
It should be this way because if you have a Pydantic `ValidationError` in your *response* or anywhere in your code (not in the client's *request*), it's actually a bug in your code.
|
||||
|
||||
And while you fix it, your clients/users shouldn't have access to internal information about the error, as that could expose a security vulnerability.
|
||||
|
||||
### Override the `HTTPException` error handler { #override-the-httpexception-error-handler }
|
||||
|
||||
The same way, you can override the `HTTPException` handler.
|
||||
|
||||
For example, you could want to return a plain text response instead of JSON for these errors:
|
||||
|
||||
{* ../../docs_src/handling_errors/tutorial004.py hl[3:4,9:11,22] *}
|
||||
{* ../../docs_src/handling_errors/tutorial004.py hl[3:4,9:11,25] *}
|
||||
|
||||
/// note | Technical Details
|
||||
|
||||
@@ -188,6 +169,14 @@ You could also use `from starlette.responses import PlainTextResponse`.
|
||||
|
||||
///
|
||||
|
||||
/// warning
|
||||
|
||||
Have in mind that the `RequestValidationError` contains the information of the file name and line where the validation error happens so that you can show it in your logs with the relevant information if you want to.
|
||||
|
||||
But that means that if you just convert it to a string and return that information directly, you could be leaking a bit of information about your system, that's why here the code extracts and shows each error independently.
|
||||
|
||||
///
|
||||
|
||||
### Use the `RequestValidationError` body { #use-the-requestvalidationerror-body }
|
||||
|
||||
The `RequestValidationError` contains the `body` it received with invalid data.
|
||||
|
||||
@@ -59,7 +59,6 @@ plugins:
|
||||
search: null
|
||||
macros:
|
||||
include_yaml:
|
||||
- external_links: ../en/data/external_links.yml
|
||||
- github_sponsors: ../en/data/github_sponsors.yml
|
||||
- people: ../en/data/people.yml
|
||||
- contributors: ../en/data/contributors.yml
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
Тесты:
|
||||
|
||||
## Фрагменты кода { #code-snippets}
|
||||
## Фрагменты кода { #code-snippets }
|
||||
|
||||
//// tab | Тест
|
||||
|
||||
@@ -53,7 +53,7 @@ LLM, вероятно, переведёт это неправильно. Инт
|
||||
|
||||
////
|
||||
|
||||
## Кавычки во фрагментах кода { #quotes-in-code-snippets}
|
||||
## Кавычки во фрагментах кода { #quotes-in-code-snippets }
|
||||
|
||||
//// tab | Тест
|
||||
|
||||
|
||||
@@ -175,7 +175,7 @@
|
||||
|
||||
Например, вы можете добавить дополнительный тип содержимого `image/png`, объявив, что ваша операция пути может возвращать JSON‑объект (с типом содержимого `application/json`) или PNG‑изображение:
|
||||
|
||||
{* ../../docs_src/additional_responses/tutorial002.py hl[19:24,28] *}
|
||||
{* ../../docs_src/additional_responses/tutorial002_py310.py hl[17:22,26] *}
|
||||
|
||||
/// note | Примечание
|
||||
|
||||
@@ -237,7 +237,7 @@ new_dict = {**old_dict, "new key": "new value"}
|
||||
|
||||
Например:
|
||||
|
||||
{* ../../docs_src/additional_responses/tutorial004.py hl[13:17,26] *}
|
||||
{* ../../docs_src/additional_responses/tutorial004_py310.py hl[11:15,24] *}
|
||||
|
||||
## Дополнительная информация об ответах OpenAPI { #more-information-about-openapi-responses }
|
||||
|
||||
|
||||
@@ -144,7 +144,7 @@ checker(q="somequery")
|
||||
|
||||
### Фоновые задачи и зависимости с `yield`, технические детали { #background-tasks-and-dependencies-with-yield-technical-details }
|
||||
|
||||
До FastAPI 0.106.0 вызывать исключения после `yield` было невозможно: код после `yield` в зависимостях выполнялся уже после отправки ответа, поэтому [Обработчики исключений](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank} к тому моменту уже отработали.
|
||||
До FastAPI 0.106.0 вызывать исключения после `yield` было невозможно: код после `yield` в зависимостях выполнялся уже после отправки ответа, поэтому [Обработчики исключений](../tutorial/handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank} к тому моменту уже отработали.
|
||||
|
||||
Так было сделано в основном для того, чтобы можно было использовать те же объекты, «отданные» зависимостями через `yield`, внутри фоновых задач, потому что код после `yield` выполнялся после завершения фоновых задач.
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ https://mysuperapp.com/items/
|
||||
|
||||
///
|
||||
|
||||
### Как работают пересылаемые заголовки прокси
|
||||
### Как работают пересылаемые заголовки прокси { #how-proxy-forwarded-headers-work }
|
||||
|
||||
Ниже показано, как прокси добавляет пересылаемые заголовки между клиентом и сервером приложения:
|
||||
|
||||
@@ -443,6 +443,14 @@ $ fastapi run main.py --forwarded-allow-ips="*" --root-path /api/v1
|
||||
|
||||
///
|
||||
|
||||
/// note | Технические детали
|
||||
|
||||
Свойство `servers` в спецификации OpenAPI является необязательным.
|
||||
|
||||
Если вы не укажете параметр `servers`, а `root_path` равен `/`, то свойство `servers` в сгенерированной схеме OpenAPI по умолчанию будет опущено. Это эквивалентно серверу со значением `url` равным `/`.
|
||||
|
||||
///
|
||||
|
||||
### Отключить автоматическое добавление сервера из `root_path` { #disable-automatic-server-from-root-path }
|
||||
|
||||
Если вы не хотите, чтобы FastAPI добавлял автоматический сервер, используя `root_path`, укажите параметр `root_path_in_servers=False`:
|
||||
|
||||
@@ -4,7 +4,7 @@ FastAPI построен поверх **Pydantic**, и я показывал в
|
||||
|
||||
Но FastAPI также поддерживает использование <a href="https://docs.python.org/3/library/dataclasses.html" class="external-link" target="_blank">`dataclasses`</a> тем же способом:
|
||||
|
||||
{* ../../docs_src/dataclasses/tutorial001.py hl[1,7:12,19:20] *}
|
||||
{* ../../docs_src/dataclasses/tutorial001_py310.py hl[1,6:11,18:19] *}
|
||||
|
||||
Это по-прежнему поддерживается благодаря **Pydantic**, так как в нём есть <a href="https://docs.pydantic.dev/latest/concepts/dataclasses/#use-of-stdlib-dataclasses-with-basemodel" class="external-link" target="_blank">встроенная поддержка `dataclasses`</a>.
|
||||
|
||||
@@ -32,7 +32,7 @@ FastAPI построен поверх **Pydantic**, и я показывал в
|
||||
|
||||
Вы также можете использовать `dataclasses` в параметре `response_model`:
|
||||
|
||||
{* ../../docs_src/dataclasses/tutorial002.py hl[1,7:13,19] *}
|
||||
{* ../../docs_src/dataclasses/tutorial002_py310.py hl[1,6:12,18] *}
|
||||
|
||||
Этот dataclass будет автоматически преобразован в Pydantic dataclass.
|
||||
|
||||
@@ -48,7 +48,7 @@ FastAPI построен поверх **Pydantic**, и я показывал в
|
||||
|
||||
В таком случае вы можете просто заменить стандартные `dataclasses` на `pydantic.dataclasses`, которая является полностью совместимой заменой (drop-in replacement):
|
||||
|
||||
{* ../../docs_src/dataclasses/tutorial003.py hl[1,5,8:11,14:17,23:25,28] *}
|
||||
{* ../../docs_src/dataclasses/tutorial003_py310.py hl[1,4,7:10,13:16,22:24,27] *}
|
||||
|
||||
1. Мы по-прежнему импортируем `field` из стандартных `dataclasses`.
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
|
||||
Эта часть вполне обычна, большая часть кода вам уже знакома:
|
||||
|
||||
{* ../../docs_src/openapi_callbacks/tutorial001.py hl[9:13,36:53] *}
|
||||
{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[7:11,34:51] *}
|
||||
|
||||
/// tip | Совет
|
||||
|
||||
@@ -90,7 +90,7 @@ httpx.post(callback_url, json={"description": "Invoice paid", "paid": True})
|
||||
|
||||
Сначала создайте новый `APIRouter`, который будет содержать один или несколько обратных вызовов.
|
||||
|
||||
{* ../../docs_src/openapi_callbacks/tutorial001.py hl[3,25] *}
|
||||
{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[1,23] *}
|
||||
|
||||
### Создайте *операцию пути* для обратного вызова { #create-the-callback-path-operation }
|
||||
|
||||
@@ -101,7 +101,7 @@ httpx.post(callback_url, json={"description": "Invoice paid", "paid": True})
|
||||
* Вероятно, в ней должно быть объявление тела запроса, например `body: InvoiceEvent`.
|
||||
* А также может быть объявление модели ответа, например `response_model=InvoiceEventReceived`.
|
||||
|
||||
{* ../../docs_src/openapi_callbacks/tutorial001.py hl[16:18,21:22,28:32] *}
|
||||
{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[14:16,19:20,26:30] *}
|
||||
|
||||
Есть 2 основных отличия от обычной *операции пути*:
|
||||
|
||||
@@ -169,7 +169,7 @@ https://www.external.org/events/invoices/2expen51ve
|
||||
|
||||
Теперь используйте параметр `callbacks` в *декораторе операции пути вашего API*, чтобы передать атрибут `.routes` (это, по сути, просто `list` маршрутов/*операций пути*) из этого маршрутизатора обратных вызовов:
|
||||
|
||||
{* ../../docs_src/openapi_callbacks/tutorial001.py hl[35] *}
|
||||
{* ../../docs_src/openapi_callbacks/tutorial001_py310.py hl[33] *}
|
||||
|
||||
/// tip | Совет
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
|
||||
Эта часть не попадёт в документацию, но другие инструменты (например, Sphinx) смогут использовать остальное.
|
||||
|
||||
{* ../../docs_src/path_operation_advanced_configuration/tutorial004.py hl[19:29] *}
|
||||
{* ../../docs_src/path_operation_advanced_configuration/tutorial004_py310.py hl[17:27] *}
|
||||
|
||||
## Дополнительные ответы { #additional-responses }
|
||||
|
||||
@@ -155,13 +155,13 @@
|
||||
|
||||
//// tab | Pydantic v2
|
||||
|
||||
{* ../../docs_src/path_operation_advanced_configuration/tutorial007.py hl[17:22, 24] *}
|
||||
{* ../../docs_src/path_operation_advanced_configuration/tutorial007_py39.py hl[15:20, 22] *}
|
||||
|
||||
////
|
||||
|
||||
//// tab | Pydantic v1
|
||||
|
||||
{* ../../docs_src/path_operation_advanced_configuration/tutorial007_pv1.py hl[17:22, 24] *}
|
||||
{* ../../docs_src/path_operation_advanced_configuration/tutorial007_pv1_py39.py hl[15:20, 22] *}
|
||||
|
||||
////
|
||||
|
||||
@@ -179,13 +179,13 @@
|
||||
|
||||
//// tab | Pydantic v2
|
||||
|
||||
{* ../../docs_src/path_operation_advanced_configuration/tutorial007.py hl[26:33] *}
|
||||
{* ../../docs_src/path_operation_advanced_configuration/tutorial007_py39.py hl[24:31] *}
|
||||
|
||||
////
|
||||
|
||||
//// tab | Pydantic v1
|
||||
|
||||
{* ../../docs_src/path_operation_advanced_configuration/tutorial007_pv1.py hl[26:33] *}
|
||||
{* ../../docs_src/path_operation_advanced_configuration/tutorial007_pv1_py39.py hl[24:31] *}
|
||||
|
||||
////
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
|
||||
В таких случаях вы можете использовать `jsonable_encoder` для преобразования данных перед передачей их в ответ:
|
||||
|
||||
{* ../../docs_src/response_directly/tutorial001.py hl[6:7,21:22] *}
|
||||
{* ../../docs_src/response_directly/tutorial001_py310.py hl[5:6,20:21] *}
|
||||
|
||||
/// note | Технические детали
|
||||
|
||||
|
||||
@@ -148,7 +148,7 @@ $ ADMIN_EMAIL="deadpool@example.com" APP_NAME="ChimichangApp" fastapi run main.p
|
||||
|
||||
Продолжая предыдущий пример, ваш файл `config.py` может выглядеть так:
|
||||
|
||||
{* ../../docs_src/settings/app02/config.py hl[10] *}
|
||||
{* ../../docs_src/settings/app02_an_py39/config.py hl[10] *}
|
||||
|
||||
Обратите внимание, что теперь мы не создаем экземпляр по умолчанию `settings = Settings()`.
|
||||
|
||||
@@ -174,7 +174,7 @@ $ ADMIN_EMAIL="deadpool@example.com" APP_NAME="ChimichangApp" fastapi run main.p
|
||||
|
||||
Далее будет очень просто предоставить другой объект настроек во время тестирования, создав переопределение зависимости для `get_settings`:
|
||||
|
||||
{* ../../docs_src/settings/app02/test_main.py hl[9:10,13,21] *}
|
||||
{* ../../docs_src/settings/app02_an_py39/test_main.py hl[9:10,13,21] *}
|
||||
|
||||
В переопределении зависимости мы задаем новое значение `admin_email` при создании нового объекта `Settings`, а затем возвращаем этот новый объект.
|
||||
|
||||
@@ -217,7 +217,7 @@ APP_NAME="ChimichangApp"
|
||||
|
||||
//// tab | Pydantic v2
|
||||
|
||||
{* ../../docs_src/settings/app03_an/config.py hl[9] *}
|
||||
{* ../../docs_src/settings/app03_an_py39/config.py hl[9] *}
|
||||
|
||||
/// tip | Совет
|
||||
|
||||
@@ -229,7 +229,7 @@ APP_NAME="ChimichangApp"
|
||||
|
||||
//// tab | Pydantic v1
|
||||
|
||||
{* ../../docs_src/settings/app03_an/config_pv1.py hl[9:10] *}
|
||||
{* ../../docs_src/settings/app03_an_py39/config_pv1.py hl[9:10] *}
|
||||
|
||||
/// tip | Совет
|
||||
|
||||
|
||||
@@ -4,11 +4,19 @@
|
||||
|
||||
В большинстве случаев у основных облачных провайдеров есть руководства по развертыванию FastAPI на их платформе.
|
||||
|
||||
## FastAPI Cloud { #fastapi-cloud }
|
||||
|
||||
**<a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>** создан тем же автором и командой, стоящими за **FastAPI**.
|
||||
|
||||
Он упрощает процесс **создания образа**, **развертывания** и **доступа** к API с минимальными усилиями.
|
||||
|
||||
Он переносит тот же **опыт разработчика** создания приложений с FastAPI на их **развертывание** в облаке. 🎉
|
||||
|
||||
FastAPI Cloud — основной спонсор и источник финансирования для open source проектов *FastAPI and friends*. ✨
|
||||
|
||||
## Облачные провайдеры — спонсоры { #cloud-providers-sponsors }
|
||||
|
||||
Некоторые облачные провайдеры ✨ [**спонсируют FastAPI**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨ — это обеспечивает непрерывное и здоровое развитие FastAPI и его экосистемы.
|
||||
|
||||
И это показывает их искреннюю приверженность FastAPI и его сообществу (вам): они не только хотят предоставить вам хороший сервис, но и стремятся гарантировать, что у вас будет хороший и стабильный фреймворк — FastAPI. 🙇
|
||||
Некоторые другие облачные провайдеры ✨ [**спонсируют FastAPI**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨ тоже. 🙇
|
||||
|
||||
Возможно, вы захотите попробовать их сервисы и воспользоваться их руководствами:
|
||||
|
||||
|
||||
65
docs/ru/docs/deployment/fastapicloud.md
Normal file
65
docs/ru/docs/deployment/fastapicloud.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# FastAPI Cloud { #fastapi-cloud }
|
||||
|
||||
Вы можете развернуть своё приложение FastAPI в <a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a> одной командой, присоединяйтесь к списку ожидания, если ещё не сделали этого. 🚀
|
||||
|
||||
## Вход { #login }
|
||||
|
||||
Убедитесь, что у вас уже есть аккаунт **FastAPI Cloud** (мы пригласили вас из списка ожидания 😉).
|
||||
|
||||
Затем выполните вход:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ fastapi login
|
||||
|
||||
You are logged in to FastAPI Cloud 🚀
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
## Деплой { #deploy }
|
||||
|
||||
Теперь разверните приложение одной командой:
|
||||
|
||||
<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>
|
||||
|
||||
Вот и всё! Теперь вы можете открыть своё приложение по этому URL. ✨
|
||||
|
||||
## О FastAPI Cloud { #about-fastapi-cloud }
|
||||
|
||||
**<a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>** создан тем же автором и командой, что и **FastAPI**.
|
||||
|
||||
Он упрощает процесс **создания образа**, **развертывания** и **доступа** к API с минимальными усилиями.
|
||||
|
||||
Он переносит тот же **опыт разработчика**, что вы получаете при создании приложений на FastAPI, на их **развертывание** в облаке. 🎉
|
||||
|
||||
Он также возьмёт на себя большинство вещей, которые требуются при развертывании приложения, например:
|
||||
|
||||
* HTTPS
|
||||
* Репликация с автоматическим масштабированием на основе запросов
|
||||
* и т.д.
|
||||
|
||||
FastAPI Cloud — основной спонсор и источник финансирования open source‑проектов «FastAPI и друзья». ✨
|
||||
|
||||
## Развертывание у других облачных провайдеров { #deploy-to-other-cloud-providers }
|
||||
|
||||
FastAPI — проект с открытым исходным кодом и основан на стандартах. Вы можете развернуть приложения FastAPI у любого облачного провайдера на ваш выбор.
|
||||
|
||||
Следуйте руководствам вашего облачного провайдера, чтобы развернуть приложения FastAPI у них. 🤓
|
||||
|
||||
## Развертывание на собственном сервере { #deploy-your-own-server }
|
||||
|
||||
Позже в этом руководстве по **развертыванию** я также расскажу все детали — чтобы вы понимали, что происходит, что нужно сделать и как развернуть приложения FastAPI самостоятельно, в том числе на собственных серверах. 🤓
|
||||
@@ -12,10 +12,12 @@
|
||||
|
||||
## Стратегии развёртывания { #deployment-strategies }
|
||||
|
||||
В зависимости от вашего конкретного случая, есть несколько способов сделать это.
|
||||
Есть несколько способов сделать это, в зависимости от вашего конкретного случая и используемых вами инструментов.
|
||||
|
||||
Вы можете **развернуть сервер** самостоятельно, используя различные инструменты. Например, можно использовать **облачный сервис**, который выполнит часть работы за вас. Также возможны и другие варианты.
|
||||
|
||||
Например, мы, команда, стоящая за FastAPI, создали <a href="https://fastapicloud.com" class="external-link" target="_blank">**FastAPI Cloud**</a>, чтобы сделать развёртывание приложений FastAPI в облаке как можно более простым и прямолинейным, с тем же удобством для разработчика, что и при работе с FastAPI.
|
||||
|
||||
В этом блоке я покажу вам некоторые из основных концепций, которые вы, вероятно, должны иметь в виду при развертывании приложения **FastAPI** (хотя большинство из них применимо к любому другому типу веб-приложений).
|
||||
|
||||
В последующих разделах вы узнаете больше деталей и методов, необходимых для этого. ✨
|
||||
|
||||
17
docs/ru/docs/how-to/authentication-error-status-code.md
Normal file
17
docs/ru/docs/how-to/authentication-error-status-code.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Использование старых статус-кодов ошибок аутентификации 403 { #use-old-403-authentication-error-status-codes }
|
||||
|
||||
До версии FastAPI `0.122.0`, когда встроенные утилиты безопасности возвращали ошибку клиенту после неудачной аутентификации, они использовали HTTP статус-код `403 Forbidden`.
|
||||
|
||||
Начиная с версии FastAPI `0.122.0`, используется более подходящий HTTP статус-код `401 Unauthorized`, и в ответе возвращается имеющий смысл HTTP-заголовок `WWW-Authenticate` в соответствии со спецификациями HTTP, <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>.
|
||||
|
||||
Но если по какой-то причине ваши клиенты зависят от старого поведения, вы можете вернуть его, переопределив метод `make_not_authenticated_error` в ваших Security-классах.
|
||||
|
||||
Например, вы можете создать подкласс `HTTPBearer`, который будет возвращать ошибку `403 Forbidden` вместо стандартной `401 Unauthorized`:
|
||||
|
||||
{* ../../docs_src/authentication_error_status_code/tutorial001_an_py39.py hl[9:13] *}
|
||||
|
||||
/// tip | Совет
|
||||
|
||||
Обратите внимание, что функция возвращает экземпляр исключения, не вызывает его. Выброс выполняется остальным внутренним кодом.
|
||||
|
||||
///
|
||||
@@ -40,7 +40,7 @@ FastAPI включает некоторые параметры конфигур
|
||||
|
||||
Это включает следующие настройки по умолчанию:
|
||||
|
||||
{* ../../fastapi/openapi/docs.py ln[8:23] hl[17:23] *}
|
||||
{* ../../fastapi/openapi/docs.py ln[9:24] hl[18:24] *}
|
||||
|
||||
Вы можете переопределить любую из них, указав другое значение в аргументе `swagger_ui_parameters`.
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
|
||||
Таким образом, один и тот же класс маршрута сможет обрабатывать как gzip-сжатые, так и несжатые запросы.
|
||||
|
||||
{* ../../docs_src/custom_request_and_route/tutorial001.py hl[8:15] *}
|
||||
{* ../../docs_src/custom_request_and_route/tutorial001_an_py310.py hl[9:16] *}
|
||||
|
||||
### Создать пользовательский класс `GzipRoute` { #create-a-custom-gziproute-class }
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
|
||||
Здесь мы используем её, чтобы создать `GzipRequest` из исходного HTTP-запроса.
|
||||
|
||||
{* ../../docs_src/custom_request_and_route/tutorial001.py hl[18:26] *}
|
||||
{* ../../docs_src/custom_request_and_route/tutorial001_an_py310.py hl[19:27] *}
|
||||
|
||||
/// note | Технические детали
|
||||
|
||||
@@ -92,18 +92,18 @@
|
||||
|
||||
Нужно лишь обработать запрос внутри блока `try`/`except`:
|
||||
|
||||
{* ../../docs_src/custom_request_and_route/tutorial002.py hl[13,15] *}
|
||||
{* ../../docs_src/custom_request_and_route/tutorial002_an_py310.py hl[14,16] *}
|
||||
|
||||
Если произойдёт исключение, экземпляр `Request` всё ещё будет в области видимости, поэтому мы сможем прочитать тело запроса и использовать его при обработке ошибки:
|
||||
|
||||
{* ../../docs_src/custom_request_and_route/tutorial002.py hl[16:18] *}
|
||||
{* ../../docs_src/custom_request_and_route/tutorial002_an_py310.py hl[17:19] *}
|
||||
|
||||
## Пользовательский класс `APIRoute` в роутере { #custom-apiroute-class-in-a-router }
|
||||
|
||||
Вы также можете задать параметр `route_class` у `APIRouter`:
|
||||
|
||||
{* ../../docs_src/custom_request_and_route/tutorial003.py hl[26] *}
|
||||
{* ../../docs_src/custom_request_and_route/tutorial003_py310.py hl[26] *}
|
||||
|
||||
В этом примере *операции пути*, объявленные в `router`, будут использовать пользовательский класс `TimedRoute` и получат дополнительный HTTP-заголовок `X-Response-Time` в ответе с временем, затраченным на формирование ответа:
|
||||
|
||||
{* ../../docs_src/custom_request_and_route/tutorial003.py hl[13:20] *}
|
||||
{* ../../docs_src/custom_request_and_route/tutorial003_py310.py hl[13:20] *}
|
||||
|
||||
@@ -52,14 +52,20 @@ FastAPI — это современный, быстрый (высокопрои
|
||||
|
||||
<!-- 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-and-silver-sponsors }
|
||||
|
||||
{% for sponsor in sponsors.gold -%}
|
||||
<a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}" style="border-radius:15px"></a>
|
||||
{% endfor -%}
|
||||
{%- for sponsor in sponsors.silver -%}
|
||||
<a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}" style="border-radius:15px"></a>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<!-- /sponsors -->
|
||||
|
||||
@@ -444,6 +450,58 @@ item: Item
|
||||
* **сессии с использованием cookie**
|
||||
* ...и многое другое.
|
||||
|
||||
### Разверните приложение (опционально) { #deploy-your-app-optional }
|
||||
|
||||
При желании вы можете развернуть своё приложение FastAPI в <a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>, присоединяйтесь к списку ожидания, если ещё не сделали этого. 🚀
|
||||
|
||||
Если у вас уже есть аккаунт **FastAPI Cloud** (мы пригласили вас из списка ожидания 😉), вы можете развернуть ваше приложение одной командой.
|
||||
|
||||
Перед развертыванием убедитесь, что вы вошли в систему:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ fastapi login
|
||||
|
||||
You are logged in to FastAPI Cloud 🚀
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
Затем разверните приложение:
|
||||
|
||||
<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>
|
||||
|
||||
Вот и всё! Теперь вы можете открыть ваше приложение по этой ссылке. ✨
|
||||
|
||||
#### О FastAPI Cloud { #about-fastapi-cloud }
|
||||
|
||||
**<a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>** создан тем же автором и командой, что и **FastAPI**.
|
||||
|
||||
Он упрощает процесс **создания образа**, **развертывания** и **доступа** к API при минимальных усилиях.
|
||||
|
||||
Он переносит тот же **опыт разработчика**, что и при создании приложений на FastAPI, на их **развертывание** в облаке. 🎉
|
||||
|
||||
FastAPI Cloud — основной спонсор и источник финансирования для проектов с открытым исходным кодом из экосистемы *FastAPI and friends*. ✨
|
||||
|
||||
#### Развертывание у других облачных провайдеров { #deploy-to-other-cloud-providers }
|
||||
|
||||
FastAPI — это open source и стандартизированный фреймворк. Вы можете развернуть приложения FastAPI у любого облачного провайдера на ваш выбор.
|
||||
|
||||
Следуйте руководствам вашего облачного провайдера по развертыванию приложений FastAPI. 🤓
|
||||
|
||||
## Производительность { #performance }
|
||||
|
||||
Независимые бенчмарки TechEmpower показывают приложения **FastAPI**, работающие под управлением Uvicorn, как <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">один из самых быстрых доступных фреймворков Python</a>, уступающий только самим Starlette и Uvicorn (используются внутри FastAPI). (*)
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
- 🔍 [Pydantic](https://docs.pydantic.dev), используется FastAPI, для валидации данных и управления настройками.
|
||||
- 💾 [PostgreSQL](https://www.postgresql.org) в качестве SQL‑базы данных.
|
||||
- 🚀 [React](https://react.dev) для фронтенда.
|
||||
- 💃 Используются TypeScript, хуки, [Vite](https://vitejs.dev) и другие части современного фронтенд‑стека.
|
||||
- 🎨 [Chakra UI](https://chakra-ui.com) для компонентов фронтенда.
|
||||
- 💃 Используются TypeScript, хуки, Vite и другие части современного фронтенд‑стека.
|
||||
- 🎨 [Tailwind CSS](https://tailwindcss.com) и [shadcn/ui](https://ui.shadcn.com) для компонентов фронтенда.
|
||||
- 🤖 Автоматически сгенерированный фронтенд‑клиент.
|
||||
- 🧪 [Playwright](https://playwright.dev) для End‑to‑End тестирования.
|
||||
- 🦇 Поддержка тёмной темы.
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
# Ресурсы { #resources }
|
||||
|
||||
Дополнительные ресурсы, внешние ссылки, статьи и многое другое. ✈️
|
||||
Дополнительные ресурсы, внешние ссылки и многое другое. ✈️
|
||||
|
||||
@@ -85,17 +85,13 @@ from app.routers import items
|
||||
|
||||
Точно также, как и в случае с классом `FastAPI`, вам нужно импортировать и создать объект класса `APIRouter`.
|
||||
|
||||
```Python hl_lines="1 3" title="app/routers/users.py"
|
||||
{!../../docs_src/bigger_applications/app/routers/users.py!}
|
||||
```
|
||||
{* ../../docs_src/bigger_applications/app_an_py39/routers/users.py hl[1,3] title["app/routers/users.py"] *}
|
||||
|
||||
### Создание *эндпоинтов* с помощью `APIRouter` { #path-operations-with-apirouter }
|
||||
|
||||
В дальнейшем используйте `APIRouter` для объявления *эндпоинтов*, точно также, как вы используете класс `FastAPI`:
|
||||
|
||||
```Python hl_lines="6 11 16" title="app/routers/users.py"
|
||||
{!../../docs_src/bigger_applications/app/routers/users.py!}
|
||||
```
|
||||
{* ../../docs_src/bigger_applications/app_an_py39/routers/users.py hl[6,11,16] title["app/routers/users.py"] *}
|
||||
|
||||
Вы можете думать об `APIRouter` как об "уменьшенной версии" класса FastAPI`.
|
||||
|
||||
@@ -119,35 +115,7 @@ from app.routers import items
|
||||
|
||||
Теперь мы воспользуемся простой зависимостью, чтобы прочитать кастомизированный `X-Token` из заголовка:
|
||||
|
||||
//// tab | Python 3.9+
|
||||
|
||||
```Python hl_lines="3 6-8" title="app/dependencies.py"
|
||||
{!> ../../docs_src/bigger_applications/app_an_py39/dependencies.py!}
|
||||
```
|
||||
|
||||
////
|
||||
|
||||
//// tab | Python 3.8+
|
||||
|
||||
```Python hl_lines="1 5-7" title="app/dependencies.py"
|
||||
{!> ../../docs_src/bigger_applications/app_an/dependencies.py!}
|
||||
```
|
||||
|
||||
////
|
||||
|
||||
//// tab | Python 3.8+ non-Annotated
|
||||
|
||||
/// tip | Подсказка
|
||||
|
||||
Мы рекомендуем использовать версию `Annotated`, когда это возможно.
|
||||
|
||||
///
|
||||
|
||||
```Python hl_lines="1 4-6" title="app/dependencies.py"
|
||||
{!> ../../docs_src/bigger_applications/app/dependencies.py!}
|
||||
```
|
||||
|
||||
////
|
||||
{* ../../docs_src/bigger_applications/app_an_py39/dependencies.py hl[3,6:8] title["app/dependencies.py"] *}
|
||||
|
||||
/// tip | Подсказка
|
||||
|
||||
@@ -180,9 +148,7 @@ from app.routers import items
|
||||
Таким образом, вместо того чтобы добавлять все эти свойства в функцию каждого отдельного *эндпоинта*,
|
||||
мы добавим их в `APIRouter`.
|
||||
|
||||
```Python hl_lines="5-10 16 21" title="app/routers/items.py"
|
||||
{!../../docs_src/bigger_applications/app/routers/items.py!}
|
||||
```
|
||||
{* ../../docs_src/bigger_applications/app_an_py39/routers/items.py hl[5:10,16,21] title["app/routers/items.py"] *}
|
||||
|
||||
Так как каждый *эндпоинт* начинается с символа `/`:
|
||||
|
||||
@@ -241,9 +207,7 @@ async def read_item(item_id: str):
|
||||
|
||||
Мы используем операцию относительного импорта `..` для импорта зависимости:
|
||||
|
||||
```Python hl_lines="3" title="app/routers/items.py"
|
||||
{!../../docs_src/bigger_applications/app/routers/items.py!}
|
||||
```
|
||||
{* ../../docs_src/bigger_applications/app_an_py39/routers/items.py hl[3] title["app/routers/items.py"] *}
|
||||
|
||||
#### Как работает относительный импорт? { #how-relative-imports-work }
|
||||
|
||||
@@ -313,9 +277,7 @@ from ...dependencies import get_token_header
|
||||
|
||||
Но помимо этого мы можем добавить новые теги для каждого отдельного *эндпоинта*, а также некоторые дополнительные ответы (`responses`), характерные для данного *эндпоинта*:
|
||||
|
||||
```Python hl_lines="30-31" title="app/routers/items.py"
|
||||
{!../../docs_src/bigger_applications/app/routers/items.py!}
|
||||
```
|
||||
{* ../../docs_src/bigger_applications/app_an_py39/routers/items.py hl[30:31] title["app/routers/items.py"] *}
|
||||
|
||||
/// tip | Подсказка
|
||||
|
||||
@@ -341,17 +303,13 @@ from ...dependencies import get_token_header
|
||||
|
||||
Мы даже можем объявить [глобальные зависимости](dependencies/global-dependencies.md){.internal-link target=_blank}, которые будут объединены с зависимостями для каждого отдельного маршрутизатора:
|
||||
|
||||
```Python hl_lines="1 3 7" title="app/main.py"
|
||||
{!../../docs_src/bigger_applications/app/main.py!}
|
||||
```
|
||||
{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[1,3,7] title["app/main.py"] *}
|
||||
|
||||
### Импорт `APIRouter` { #import-the-apirouter }
|
||||
|
||||
Теперь мы импортируем другие суб-модули, содержащие `APIRouter`:
|
||||
|
||||
```Python hl_lines="4-5" title="app/main.py"
|
||||
{!../../docs_src/bigger_applications/app/main.py!}
|
||||
```
|
||||
{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[4:5] title["app/main.py"] *}
|
||||
|
||||
Так как файлы `app/routers/users.py` и `app/routers/items.py` являются суб-модулями одного и того же Python-пакета `app`, то мы сможем их импортировать, воспользовавшись операцией относительного импорта `.`.
|
||||
|
||||
@@ -414,17 +372,13 @@ from .routers.users import router
|
||||
|
||||
Поэтому, для того чтобы использовать обе эти переменные в одном файле, мы импортировали соответствующие суб-модули:
|
||||
|
||||
```Python hl_lines="5" title="app/main.py"
|
||||
{!../../docs_src/bigger_applications/app/main.py!}
|
||||
```
|
||||
{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[5] title["app/main.py"] *}
|
||||
|
||||
### Подключение маршрутизаторов (`APIRouter`) для `users` и для `items` { #include-the-apirouters-for-users-and-items }
|
||||
|
||||
Давайте подключим маршрутизаторы (`router`) из суб-модулей `users` и `items`:
|
||||
|
||||
```Python hl_lines="10-11" title="app/main.py"
|
||||
{!../../docs_src/bigger_applications/app/main.py!}
|
||||
```
|
||||
{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[10:11] title["app/main.py"] *}
|
||||
|
||||
/// info | Примечание
|
||||
|
||||
@@ -465,17 +419,13 @@ from .routers.users import router
|
||||
В данном примере это сделать очень просто. Но давайте предположим, что поскольку файл используется для нескольких проектов,
|
||||
то мы не можем модифицировать его, добавляя префиксы (`prefix`), зависимости (`dependencies`), теги (`tags`), и т.д. непосредственно в `APIRouter`:
|
||||
|
||||
```Python hl_lines="3" title="app/internal/admin.py"
|
||||
{!../../docs_src/bigger_applications/app/internal/admin.py!}
|
||||
```
|
||||
{* ../../docs_src/bigger_applications/app_an_py39/internal/admin.py hl[3] title["app/internal/admin.py"] *}
|
||||
|
||||
Но, несмотря на это, мы хотим использовать кастомный префикс (`prefix`) для подключенного маршрутизатора (`APIRouter`), в результате чего, каждая *операция пути* будет начинаться с `/admin`. Также мы хотим защитить наш маршрутизатор с помощью зависимостей, созданных для нашего проекта. И ещё мы хотим включить теги (`tags`) и ответы (`responses`).
|
||||
|
||||
Мы можем применить все вышеперечисленные настройки, не изменяя начальный `APIRouter`. Нам всего лишь нужно передать нужные параметры в `app.include_router()`.
|
||||
|
||||
```Python hl_lines="14-17" title="app/main.py"
|
||||
{!../../docs_src/bigger_applications/app/main.py!}
|
||||
```
|
||||
{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[14:17] title["app/main.py"] *}
|
||||
|
||||
Таким образом, оригинальный `APIRouter` не будет модифицирован, и мы сможем использовать файл `app/internal/admin.py` сразу в нескольких проектах организации.
|
||||
|
||||
@@ -496,9 +446,7 @@ from .routers.users import router
|
||||
|
||||
Здесь мы это делаем ... просто, чтобы показать, что это возможно 🤷:
|
||||
|
||||
```Python hl_lines="21-23" title="app/main.py"
|
||||
{!../../docs_src/bigger_applications/app/main.py!}
|
||||
```
|
||||
{* ../../docs_src/bigger_applications/app_an_py39/main.py hl[21:23] title["app/main.py"] *}
|
||||
|
||||
и это будет работать корректно вместе с другими *эндпоинтами*, добавленными с помощью `app.include_router()`.
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
|
||||
Вы можете сконфигурировать Pydantic-модель так, чтобы запретить (`forbid`) любые дополнительные (`extra`) поля:
|
||||
|
||||
{* ../../docs_src/cookie_param_models/tutorial002_an_py39.py hl[10] *}
|
||||
{* ../../docs_src/cookie_param_models/tutorial002_an_py310.py hl[10] *}
|
||||
|
||||
Если клиент попробует отправить **дополнительные cookies**, то в ответ он получит **ошибку**.
|
||||
|
||||
|
||||
@@ -143,6 +143,42 @@ OpenAPI определяет схему API для вашего API. И эта
|
||||
|
||||
Вы также можете использовать её для автоматической генерации кода для клиентов, которые взаимодействуют с вашим API. Например, для фронтенд-, мобильных или IoT-приложений.
|
||||
|
||||
### Разверните приложение (необязательно) { #deploy-your-app-optional }
|
||||
|
||||
При желании вы можете развернуть своё приложение FastAPI в <a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>, перейдите и присоединитесь к списку ожидания, если ещё не сделали этого. 🚀
|
||||
|
||||
Если у вас уже есть аккаунт **FastAPI Cloud** (мы пригласили вас из списка ожидания 😉), вы можете развернуть приложение одной командой.
|
||||
|
||||
Перед развертыванием убедитесь, что вы вошли в систему:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ fastapi login
|
||||
|
||||
You are logged in to FastAPI Cloud 🚀
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
Затем разверните приложение:
|
||||
|
||||
<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>
|
||||
|
||||
Готово! Теперь вы можете открыть своё приложение по этому URL. ✨
|
||||
|
||||
## Рассмотрим поэтапно { #recap-step-by-step }
|
||||
|
||||
### Шаг 1: импортируйте `FastAPI` { #step-1-import-fastapi }
|
||||
@@ -314,6 +350,26 @@ https://example.com/items/foo
|
||||
|
||||
Многие другие объекты и модели будут автоматически преобразованы в JSON (включая ORM и т. п.). Попробуйте использовать те, что вам привычнее, с высокой вероятностью они уже поддерживаются.
|
||||
|
||||
### Шаг 6: разверните приложение { #step-6-deploy-it }
|
||||
|
||||
Разверните приложение в **<a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>** одной командой: `fastapi deploy`. 🎉
|
||||
|
||||
#### О FastAPI Cloud { #about-fastapi-cloud }
|
||||
|
||||
**<a href="https://fastapicloud.com" class="external-link" target="_blank">FastAPI Cloud</a>** создан тем же автором и командой, что и **FastAPI**.
|
||||
|
||||
Он упрощает процесс **создания образа**, **развертывания** и **доступа** к API с минимальными усилиями.
|
||||
|
||||
Он переносит тот же **опыт разработчика** при создании приложений с FastAPI на их **развертывание** в облаке. 🎉
|
||||
|
||||
FastAPI Cloud — основной спонсор и источник финансирования для open-source проектов «FastAPI и друзья». ✨
|
||||
|
||||
#### Развертывание у других облачных провайдеров { #deploy-to-other-cloud-providers }
|
||||
|
||||
FastAPI — open-source и основан на стандартах. Вы можете развернуть приложения FastAPI у любого облачного провайдера по вашему выбору.
|
||||
|
||||
Следуйте руководствам вашего облачного провайдера, чтобы развернуть приложения FastAPI у них. 🤓
|
||||
|
||||
## Резюме { #recap }
|
||||
|
||||
* Импортируйте `FastAPI`.
|
||||
@@ -321,3 +377,4 @@ https://example.com/items/foo
|
||||
* Напишите **декоратор операции пути**, например `@app.get("/")`.
|
||||
* Определите **функцию операции пути**; например, `def root(): ...`.
|
||||
* Запустите сервер разработки командой `fastapi dev`.
|
||||
* При желании разверните приложение командой `fastapi deploy`.
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
|
||||
## Установка пользовательских обработчиков исключений { #install-custom-exception-handlers }
|
||||
|
||||
Вы можете добавить пользовательские обработчики исключений с помощью <a href="https://www.starlette.dev/exceptions/" class="external-link" target="_blank">то же самое исключение - утилиты от Starlette</a>.
|
||||
Вы можете добавить пользовательские обработчики исключений с помощью <a href="https://www.starlette.dev/exceptions/" class="external-link" target="_blank">тех же утилит обработки исключений из Starlette</a>.
|
||||
|
||||
Допустим, у вас есть пользовательское исключение `UnicornException`, которое вы (или используемая вами библиотека) можете `вызвать`.
|
||||
|
||||
@@ -117,7 +117,7 @@
|
||||
|
||||
Вы можете переопределить эти обработчики исключений на свои собственные.
|
||||
|
||||
### Переопределение исключений проверки запроса { #override-request-validation-exceptions }
|
||||
### Переопределение обработчика исключений проверки запроса { #override-request-validation-exceptions }
|
||||
|
||||
Когда запрос содержит недопустимые данные, **FastAPI** внутренне вызывает ошибку `RequestValidationError`.
|
||||
|
||||
@@ -127,7 +127,7 @@
|
||||
|
||||
Обработчик исключения получит объект `Request` и исключение.
|
||||
|
||||
{* ../../docs_src/handling_errors/tutorial004.py hl[2,14:16] *}
|
||||
{* ../../docs_src/handling_errors/tutorial004.py hl[2,14:19] *}
|
||||
|
||||
Теперь, если перейти к `/items/foo`, то вместо стандартной JSON-ошибки с:
|
||||
|
||||
@@ -149,36 +149,17 @@
|
||||
вы получите текстовую версию:
|
||||
|
||||
```
|
||||
1 validation error
|
||||
path -> item_id
|
||||
value is not a valid integer (type=type_error.integer)
|
||||
Validation errors:
|
||||
Field: ('path', 'item_id'), Error: Input should be a valid integer, unable to parse string as an integer
|
||||
```
|
||||
|
||||
#### `RequestValidationError` или `ValidationError` { #requestvalidationerror-vs-validationerror }
|
||||
|
||||
/// warning | Внимание
|
||||
|
||||
Это технические детали, которые можно пропустить, если они не важны для вас сейчас.
|
||||
|
||||
///
|
||||
|
||||
`RequestValidationError` является подклассом Pydantic <a href="https://docs.pydantic.dev/latest/concepts/models/#error-handling" class="external-link" target="_blank">`ValidationError`</a>.
|
||||
|
||||
**FastAPI** использует его для того, чтобы, если вы используете Pydantic-модель в `response_model`, и ваши данные содержат ошибку, вы увидели ошибку в журнале.
|
||||
|
||||
Но клиент/пользователь этого не увидит. Вместо этого клиент получит сообщение "Internal Server Error" с кодом состояния HTTP `500`.
|
||||
|
||||
Так и должно быть, потому что если в вашем *ответе* или где-либо в вашем коде (не в *запросе* клиента) возникает Pydantic `ValidationError`, то это действительно ошибка в вашем коде.
|
||||
|
||||
И пока вы не устраните ошибку, ваши клиенты/пользователи не должны иметь доступа к внутренней информации о ней, так как это может привести к уязвимости в системе безопасности.
|
||||
|
||||
### Переопределите обработчик ошибок `HTTPException` { #override-the-httpexception-error-handler }
|
||||
|
||||
Аналогичным образом можно переопределить обработчик `HTTPException`.
|
||||
|
||||
Например, для этих ошибок можно вернуть обычный текстовый ответ вместо JSON:
|
||||
|
||||
{* ../../docs_src/handling_errors/tutorial004.py hl[3:4,9:11,22] *}
|
||||
{* ../../docs_src/handling_errors/tutorial004.py hl[3:4,9:11,25] *}
|
||||
|
||||
/// note | Технические детали
|
||||
|
||||
@@ -188,6 +169,14 @@ path -> item_id
|
||||
|
||||
///
|
||||
|
||||
/// warning | Внимание
|
||||
|
||||
Имейте в виду, что `RequestValidationError` содержит информацию об имени файла и строке, где произошла ошибка валидации, чтобы вы могли при желании отобразить её в логах с релевантными данными.
|
||||
|
||||
Но это означает, что если вы просто преобразуете её в строку и вернёте эту информацию напрямую, вы можете допустить небольшую утечку информации о своей системе, поэтому здесь код извлекает и показывает каждую ошибку отдельно.
|
||||
|
||||
///
|
||||
|
||||
### Используйте тело `RequestValidationError` { #use-the-requestvalidationerror-body }
|
||||
|
||||
Ошибка `RequestValidationError` содержит полученное `тело` с недопустимыми данными.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Настройка авторизации
|
||||
# Настройка авторизации { #security }
|
||||
|
||||
Существует множество способов обеспечения безопасности, аутентификации и авторизации.
|
||||
|
||||
@@ -10,11 +10,11 @@
|
||||
|
||||
Но сначала давайте рассмотрим некоторые небольшие концепции.
|
||||
|
||||
## Куда-то торопишься?
|
||||
## Куда-то торопишься? { #in-a-hurry }
|
||||
|
||||
Если вам не нужна информация о каких-либо из следующих терминов и вам просто нужно добавить защиту с аутентификацией на основе логина и пароля *прямо сейчас*, переходите к следующим главам.
|
||||
|
||||
## OAuth2
|
||||
## OAuth2 { #oauth2 }
|
||||
|
||||
OAuth2 - это протокол, который определяет несколько способов обработки аутентификации и авторизации.
|
||||
|
||||
@@ -24,7 +24,7 @@ OAuth2 включает в себя способы аутентификации
|
||||
|
||||
Это то, что используют под собой все кнопки "вход с помощью Facebook, Google, X (Twitter), GitHub" на страницах авторизации.
|
||||
|
||||
### OAuth 1
|
||||
### OAuth 1 { #oauth-1 }
|
||||
|
||||
Ранее использовался протокол OAuth 1, который сильно отличается от OAuth2 и является более сложным, поскольку он включал прямые описания того, как шифровать сообщение.
|
||||
|
||||
@@ -34,11 +34,11 @@ OAuth2 не указывает, как шифровать сообщение, о
|
||||
|
||||
/// tip | Подсказка
|
||||
|
||||
В разделе **Развертывание** вы увидите [как настроить протокол HTTPS бесплатно, используя Traefik и Let's Encrypt.](https://fastapi.tiangolo.com/ru/deployment/https/)
|
||||
В разделе **Развертывание** вы увидите как настроить протокол HTTPS бесплатно, используя Traefik и Let's Encrypt.
|
||||
|
||||
///
|
||||
|
||||
## OpenID Connect
|
||||
## OpenID Connect { #openid-connect }
|
||||
|
||||
OpenID Connect - это еще один протокол, основанный на **OAuth2**.
|
||||
|
||||
@@ -48,7 +48,7 @@ OpenID Connect - это еще один протокол, основанный
|
||||
|
||||
Но вход в Facebook не поддерживает OpenID Connect. У него есть собственная вариация OAuth2.
|
||||
|
||||
### OpenID (не "OpenID Connect")
|
||||
### OpenID (не "OpenID Connect") { #openid-not-openid-connect }
|
||||
|
||||
Также ранее использовался стандарт "OpenID", который пытался решить ту же проблему, что и **OpenID Connect**, но не был основан на OAuth2.
|
||||
|
||||
@@ -56,7 +56,7 @@ OpenID Connect - это еще один протокол, основанный
|
||||
|
||||
В настоящее время не очень популярен и не используется.
|
||||
|
||||
## OpenAPI
|
||||
## OpenAPI { #openapi }
|
||||
|
||||
OpenAPI (ранее известный как Swagger) - это открытая спецификация для создания API (в настоящее время является частью Linux Foundation).
|
||||
|
||||
@@ -97,7 +97,7 @@ OpenAPI может использовать следующие схемы авт
|
||||
|
||||
///
|
||||
|
||||
## Преимущества **FastAPI**
|
||||
## Преимущества **FastAPI** { #fastapi-utilities }
|
||||
|
||||
Fast API предоставляет несколько инструментов для каждой из этих схем безопасности в модуле `fastapi.security`, которые упрощают использование этих механизмов безопасности.
|
||||
|
||||
|
||||
@@ -63,9 +63,9 @@ $ pip install sqlmodel
|
||||
|
||||
* `table=True` сообщает SQLModel, что это *модель-таблица*, она должна представлять **таблицу** в SQL базе данных, это не просто *модель данных* (как обычный класс Pydantic).
|
||||
|
||||
* `Field(primary_key=True)` сообщает SQLModel, что `id` — это **первичный ключ** в SQL базе данных (подробнее о первичных ключах можно узнать в документации SQLModel).
|
||||
* `Field(primary_key=True)` сообщает SQLModel, что `id` — это **первичный ключ** в SQL базе данных (подробнее о первичных ключах SQL можно узнать в документации SQLModel).
|
||||
|
||||
Благодаря типу `int | None`, SQLModel будет знать, что этот столбец должен быть `INTEGER` в SQL базе данных и должен допускать значение `NULL`.
|
||||
**Примечание:** Мы используем `int | None` для поля первичного ключа, чтобы в Python-коде можно было *создать объект без `id`* (`id=None`), предполагая, что база данных *сгенерирует его при сохранении*. SQLModel понимает, что база данных предоставит `id`, и *определяет столбец как `INTEGER` (не `NULL`)* в схеме базы данных. См. <a href="https://sqlmodel.tiangolo.com/tutorial/create-db-and-table/#primary-key-id" class="external-link" target="_blank">документацию SQLModel о первичных ключах</a> для подробностей.
|
||||
|
||||
* `Field(index=True)` сообщает SQLModel, что нужно создать **SQL индекс** для этого столбца, что позволит быстрее выполнять выборки при чтении данных, отфильтрованных по этому столбцу.
|
||||
|
||||
@@ -107,7 +107,7 @@ $ pip install sqlmodel
|
||||
|
||||
Здесь мы создаём таблицы в обработчике события запуска приложения.
|
||||
|
||||
Для продакшна вы, вероятно, будете использовать скрипт миграций, который выполняется до запуска приложения. 🤓
|
||||
Для продакшн вы, вероятно, будете использовать скрипт миграций, который выполняется до запуска приложения. 🤓
|
||||
|
||||
/// tip | Подсказка
|
||||
|
||||
|
||||
@@ -121,63 +121,13 @@ $ pip install httpx
|
||||
|
||||
Обе *операции пути* требуют наличия в запросе заголовка `X-Token`.
|
||||
|
||||
//// tab | Python 3.10+
|
||||
|
||||
```Python
|
||||
{!> ../../docs_src/app_testing/app_b_an_py310/main.py!}
|
||||
```
|
||||
|
||||
////
|
||||
|
||||
//// tab | Python 3.9+
|
||||
|
||||
```Python
|
||||
{!> ../../docs_src/app_testing/app_b_an_py39/main.py!}
|
||||
```
|
||||
|
||||
////
|
||||
|
||||
//// tab | Python 3.8+
|
||||
|
||||
```Python
|
||||
{!> ../../docs_src/app_testing/app_b_an/main.py!}
|
||||
```
|
||||
|
||||
////
|
||||
|
||||
//// tab | Python 3.10+ без Annotated
|
||||
|
||||
/// tip | Подсказка
|
||||
|
||||
По возможности используйте версию с `Annotated`.
|
||||
|
||||
///
|
||||
|
||||
```Python
|
||||
{!> ../../docs_src/app_testing/app_b_py310/main.py!}
|
||||
```
|
||||
|
||||
////
|
||||
|
||||
//// tab | Python 3.8+ без Annotated
|
||||
|
||||
/// tip | Подсказка
|
||||
|
||||
По возможности используйте версию с `Annotated`.
|
||||
|
||||
///
|
||||
|
||||
```Python
|
||||
{!> ../../docs_src/app_testing/app_b/main.py!}
|
||||
```
|
||||
|
||||
////
|
||||
{* ../../docs_src/app_testing/app_b_an_py310/main.py *}
|
||||
|
||||
### Расширенный файл тестов { #extended-testing-file }
|
||||
|
||||
Теперь обновим файл `test_main.py`, добавив в него тестов:
|
||||
|
||||
{* ../../docs_src/app_testing/app_b/test_main.py *}
|
||||
{* ../../docs_src/app_testing/app_b_an_py310/test_main.py *}
|
||||
|
||||
|
||||
Если Вы не знаете, как передать информацию в запросе, можете воспользоваться поисковиком (погуглить) и задать вопрос: "Как передать информацию в запросе с помощью `httpx`", можно даже спросить: "Как передать информацию в запросе с помощью `requests`", поскольку дизайн HTTPX основан на дизайне Requests.
|
||||
|
||||
@@ -242,6 +242,26 @@ $ python -m pip install --upgrade pip
|
||||
|
||||
</div>
|
||||
|
||||
/// tip | Подсказка
|
||||
|
||||
Иногда при попытке обновить pip вы можете получить ошибку **`No module named pip`**.
|
||||
|
||||
Если это произошло, установите и обновите pip с помощью команды ниже:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ python -m ensurepip --upgrade
|
||||
|
||||
---> 100%
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
Эта команда установит pip, если он ещё не установлен, а также гарантирует, что установленная версия pip будет не старее, чем версия, доступная в `ensurepip`.
|
||||
|
||||
///
|
||||
|
||||
## Добавление `.gitignore` { #add-gitignore }
|
||||
|
||||
Если вы используете **Git** (а вам стоит его использовать), добавьте файл `.gitignore`, чтобы исключить из Git всё, что находится в вашей `.venv`.
|
||||
@@ -834,7 +854,7 @@ I solemnly swear 🐺
|
||||
* Управлять **виртуальным окружением** ваших проектов
|
||||
* Устанавливать **пакеты**
|
||||
* Управлять **зависимостями и версиями** пакетов вашего проекта
|
||||
* Обеспечивать наличие **точного** набора пакетов и версий к установке, включая их зависимости, чтобы вы были уверены, что сможете запускать проект в продакшне точно так же, как и на компьютере при разработке — это называется **locking**
|
||||
* Обеспечивать наличие **точного** набора пакетов и версий к установке, включая их зависимости, чтобы вы были уверены, что сможете запускать проект в продакшн точно так же, как и на компьютере при разработке — это называется **locking**
|
||||
* И многое другое
|
||||
|
||||
## Заключение { #conclusion }
|
||||
|
||||
@@ -12,8 +12,11 @@ async def http_exception_handler(request, exc):
|
||||
|
||||
|
||||
@app.exception_handler(RequestValidationError)
|
||||
async def validation_exception_handler(request, exc):
|
||||
return PlainTextResponse(str(exc), status_code=400)
|
||||
async def validation_exception_handler(request, exc: RequestValidationError):
|
||||
message = "Validation errors:"
|
||||
for error in exc.errors():
|
||||
message += f"\nField: {error['loc']}, Error: {error['msg']}"
|
||||
return PlainTextResponse(message, status_code=400)
|
||||
|
||||
|
||||
@app.get("/items/{item_id}")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
|
||||
|
||||
__version__ = "0.124.2"
|
||||
__version__ = "0.124.4"
|
||||
|
||||
from starlette import status as status
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ from typing import (
|
||||
from fastapi._compat import may_v1, shared
|
||||
from fastapi.openapi.constants import REF_TEMPLATE
|
||||
from fastapi.types import IncEx, ModelNameMap, UnionType
|
||||
from pydantic import BaseModel, ConfigDict, TypeAdapter, create_model
|
||||
from pydantic import BaseModel, ConfigDict, Field, TypeAdapter, create_model
|
||||
from pydantic import PydanticSchemaGenerationError as PydanticSchemaGenerationError
|
||||
from pydantic import PydanticUndefinedAnnotation as PydanticUndefinedAnnotation
|
||||
from pydantic import ValidationError as ValidationError
|
||||
@@ -50,6 +50,45 @@ UndefinedType = PydanticUndefinedType
|
||||
evaluate_forwardref = eval_type_lenient
|
||||
Validator = Any
|
||||
|
||||
# TODO: remove when dropping support for Pydantic < v2.12.3
|
||||
_Attrs = {
|
||||
"default": ...,
|
||||
"default_factory": None,
|
||||
"alias": None,
|
||||
"alias_priority": None,
|
||||
"validation_alias": None,
|
||||
"serialization_alias": None,
|
||||
"title": None,
|
||||
"field_title_generator": None,
|
||||
"description": None,
|
||||
"examples": None,
|
||||
"exclude": None,
|
||||
"exclude_if": None,
|
||||
"discriminator": None,
|
||||
"deprecated": None,
|
||||
"json_schema_extra": None,
|
||||
"frozen": None,
|
||||
"validate_default": None,
|
||||
"repr": True,
|
||||
"init": None,
|
||||
"init_var": None,
|
||||
"kw_only": None,
|
||||
}
|
||||
|
||||
|
||||
# TODO: remove when dropping support for Pydantic < v2.12.3
|
||||
def asdict(field_info: FieldInfo) -> Dict[str, Any]:
|
||||
attributes = {}
|
||||
for attr in _Attrs:
|
||||
value = getattr(field_info, attr, Undefined)
|
||||
if value is not Undefined:
|
||||
attributes[attr] = value
|
||||
return {
|
||||
"annotation": field_info.annotation,
|
||||
"metadata": field_info.metadata,
|
||||
"attributes": attributes,
|
||||
}
|
||||
|
||||
|
||||
class BaseConfig:
|
||||
pass
|
||||
@@ -71,6 +110,18 @@ class ModelField:
|
||||
a = self.field_info.alias
|
||||
return a if a is not None else self.name
|
||||
|
||||
@property
|
||||
def validation_alias(self) -> Union[str, None]:
|
||||
va = self.field_info.validation_alias
|
||||
if isinstance(va, str) and va:
|
||||
return va
|
||||
return None
|
||||
|
||||
@property
|
||||
def serialization_alias(self) -> Union[str, None]:
|
||||
sa = self.field_info.serialization_alias
|
||||
return sa or None
|
||||
|
||||
@property
|
||||
def required(self) -> bool:
|
||||
return self.field_info.is_required()
|
||||
@@ -95,10 +146,15 @@ class ModelField:
|
||||
warnings.simplefilter(
|
||||
"ignore", category=UnsupportedFieldAttributeWarning
|
||||
)
|
||||
# TODO: remove after dropping support for Python 3.8 and
|
||||
# setting the min Pydantic to v2.12.3 that adds asdict()
|
||||
field_dict = asdict(self.field_info)
|
||||
annotated_args = (
|
||||
self.field_info.annotation,
|
||||
*self.field_info.metadata,
|
||||
self.field_info,
|
||||
field_dict["annotation"],
|
||||
*field_dict["metadata"],
|
||||
# this FieldInfo needs to be created again so that it doesn't include
|
||||
# the old field info metadata and only the rest of the attributes
|
||||
Field(**field_dict["attributes"]),
|
||||
)
|
||||
self._type_adapter: TypeAdapter[Any] = TypeAdapter(
|
||||
Annotated[annotated_args],
|
||||
@@ -199,12 +255,18 @@ def get_schema_from_model_field(
|
||||
if (separate_input_output_schemas or _has_computed_fields(field))
|
||||
else "validation"
|
||||
)
|
||||
field_alias = (
|
||||
(field.validation_alias or field.alias)
|
||||
if field.mode == "validation"
|
||||
else (field.serialization_alias or field.alias)
|
||||
)
|
||||
|
||||
# This expects that GenerateJsonSchema was already used to generate the definitions
|
||||
json_schema = field_mapping[(field, override_mode or field.mode)]
|
||||
if "$ref" not in json_schema:
|
||||
# TODO remove when deprecating Pydantic v1
|
||||
# Ref: https://github.com/pydantic/pydantic/blob/d61792cc42c80b13b23e3ffa74bc37ec7c77f7d1/pydantic/schema.py#L207
|
||||
json_schema["title"] = field.field_info.title or field.alias.title().replace(
|
||||
json_schema["title"] = field.field_info.title or field_alias.title().replace(
|
||||
"_", " "
|
||||
)
|
||||
return json_schema
|
||||
|
||||
@@ -752,7 +752,7 @@ def _validate_value_with_model_field(
|
||||
def _get_multidict_value(
|
||||
field: ModelField, values: Mapping[str, Any], alias: Union[str, None] = None
|
||||
) -> Any:
|
||||
alias = alias or field.alias
|
||||
alias = alias or get_validation_alias(field)
|
||||
if is_sequence_field(field) and isinstance(values, (ImmutableMultiDict, Headers)):
|
||||
value = values.getlist(alias)
|
||||
else:
|
||||
@@ -809,15 +809,13 @@ def request_params_to_args(
|
||||
field.field_info, "convert_underscores", default_convert_underscores
|
||||
)
|
||||
if convert_underscores:
|
||||
alias = (
|
||||
field.alias
|
||||
if field.alias != field.name
|
||||
else field.name.replace("_", "-")
|
||||
)
|
||||
alias = get_validation_alias(field)
|
||||
if alias == field.name:
|
||||
alias = alias.replace("_", "-")
|
||||
value = _get_multidict_value(field, received_params, alias=alias)
|
||||
if value is not None:
|
||||
params_to_process[field.alias] = value
|
||||
processed_keys.add(alias or field.alias)
|
||||
params_to_process[get_validation_alias(field)] = value
|
||||
processed_keys.add(alias or get_validation_alias(field))
|
||||
|
||||
for key in received_params.keys():
|
||||
if key not in processed_keys:
|
||||
@@ -847,7 +845,7 @@ def request_params_to_args(
|
||||
assert isinstance(field_info, (params.Param, temp_pydantic_v1_params.Param)), (
|
||||
"Params must be subclasses of Param"
|
||||
)
|
||||
loc = (field_info.in_.value, field.alias)
|
||||
loc = (field_info.in_.value, get_validation_alias(field))
|
||||
v_, errors_ = _validate_value_with_model_field(
|
||||
field=field, value=value, values=values, loc=loc
|
||||
)
|
||||
@@ -936,8 +934,8 @@ async def _extract_form_body(
|
||||
tg.start_soon(process_fn, sub_value.read)
|
||||
value = serialize_sequence_value(field=field, value=results)
|
||||
if value is not None:
|
||||
values[field.alias] = value
|
||||
field_aliases = {field.alias for field in body_fields}
|
||||
values[get_validation_alias(field)] = value
|
||||
field_aliases = {get_validation_alias(field) for field in body_fields}
|
||||
for key in received_body.keys():
|
||||
if key not in field_aliases:
|
||||
param_values = received_body.getlist(key)
|
||||
@@ -979,11 +977,11 @@ async def request_body_to_args(
|
||||
)
|
||||
return {first_field.name: v_}, errors_
|
||||
for field in body_fields:
|
||||
loc = ("body", field.alias)
|
||||
loc = ("body", get_validation_alias(field))
|
||||
value: Optional[Any] = None
|
||||
if body_to_process is not None:
|
||||
try:
|
||||
value = body_to_process.get(field.alias)
|
||||
value = body_to_process.get(get_validation_alias(field))
|
||||
# If the received body is a list, not a dict
|
||||
except AttributeError:
|
||||
errors.append(get_missing_field_error(loc))
|
||||
@@ -1062,3 +1060,8 @@ def get_body_field(
|
||||
field_info=BodyFieldInfo(**BodyFieldInfo_kwargs),
|
||||
)
|
||||
return final_field
|
||||
|
||||
|
||||
def get_validation_alias(field: ModelField) -> str:
|
||||
va = getattr(field, "validation_alias", None)
|
||||
return va or field.alias
|
||||
|
||||
@@ -19,6 +19,7 @@ from fastapi.dependencies.utils import (
|
||||
_get_flat_fields_from_params,
|
||||
get_flat_dependant,
|
||||
get_flat_params,
|
||||
get_validation_alias,
|
||||
)
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.openapi.constants import METHODS_WITH_BODY, REF_PREFIX
|
||||
@@ -141,7 +142,7 @@ def _get_openapi_operation_parameters(
|
||||
field_mapping=field_mapping,
|
||||
separate_input_output_schemas=separate_input_output_schemas,
|
||||
)
|
||||
name = param.alias
|
||||
name = get_validation_alias(param)
|
||||
convert_underscores = getattr(
|
||||
param.field_info,
|
||||
"convert_underscores",
|
||||
@@ -149,7 +150,7 @@ def _get_openapi_operation_parameters(
|
||||
)
|
||||
if (
|
||||
param_type == ParamTypes.header
|
||||
and param.alias == param.name
|
||||
and name == param.name
|
||||
and convert_underscores
|
||||
):
|
||||
name = param.name.replace("_", "-")
|
||||
|
||||
@@ -115,6 +115,10 @@ class Param(FieldInfo): # type: ignore[misc]
|
||||
else:
|
||||
kwargs["deprecated"] = deprecated
|
||||
if PYDANTIC_V2:
|
||||
if serialization_alias in (_Unset, None) and isinstance(alias, str):
|
||||
serialization_alias = alias
|
||||
if validation_alias in (_Unset, None):
|
||||
validation_alias = alias
|
||||
kwargs.update(
|
||||
{
|
||||
"annotation": annotation,
|
||||
@@ -571,6 +575,10 @@ class Body(FieldInfo): # type: ignore[misc]
|
||||
else:
|
||||
kwargs["deprecated"] = deprecated
|
||||
if PYDANTIC_V2:
|
||||
if serialization_alias in (_Unset, None) and isinstance(alias, str):
|
||||
serialization_alias = alias
|
||||
if validation_alias in (_Unset, None):
|
||||
validation_alias = alias
|
||||
kwargs.update(
|
||||
{
|
||||
"annotation": annotation,
|
||||
|
||||
@@ -196,6 +196,7 @@ source = [
|
||||
"tests",
|
||||
"fastapi"
|
||||
]
|
||||
relative_files = true
|
||||
context = '${CONTEXT}'
|
||||
dynamic_context = "test_function"
|
||||
omit = [
|
||||
|
||||
0
tests/test_request_params/__init__.py
Normal file
0
tests/test_request_params/__init__.py
Normal file
0
tests/test_request_params/test_body/__init__.py
Normal file
0
tests/test_request_params/test_body/__init__.py
Normal file
483
tests/test_request_params/test_body/test_list.py
Normal file
483
tests/test_request_params/test_body/test_list.py
Normal file
@@ -0,0 +1,483 @@
|
||||
from typing import List, Union
|
||||
|
||||
import pytest
|
||||
from dirty_equals import IsDict, IsOneOf, IsPartialDict
|
||||
from fastapi import Body, FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel, Field
|
||||
from typing_extensions import Annotated
|
||||
|
||||
from tests.utils import needs_pydanticv2
|
||||
|
||||
from .utils import get_body_model_name
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# =====================================================================================
|
||||
# Without aliases
|
||||
|
||||
|
||||
@app.post("/required-list-str", operation_id="required_list_str")
|
||||
async def read_required_list_str(p: Annotated[List[str], Body(embed=True)]):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class BodyModelRequiredListStr(BaseModel):
|
||||
p: List[str]
|
||||
|
||||
|
||||
@app.post("/model-required-list-str", operation_id="model_required_list_str")
|
||||
def read_model_required_list_str(p: BodyModelRequiredListStr):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-list-str", "/model-required-list-str"],
|
||||
)
|
||||
def test_required_list_str_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == {
|
||||
"properties": {
|
||||
"p": {
|
||||
"items": {"type": "string"},
|
||||
"title": "P",
|
||||
"type": "array",
|
||||
},
|
||||
},
|
||||
"required": ["p"],
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("json", [None, {}])
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-list-str", "/model-required-list-str"],
|
||||
)
|
||||
def test_required_list_str_missing(path: str, json: Union[dict, None]):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json=json)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": IsOneOf(["body", "p"], ["body"]),
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {}),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": IsOneOf(["body", "p"], ["body"]),
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-list-str", "/model-required-list-str"],
|
||||
)
|
||||
def test_required_list_str(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={"p": ["hello", "world"]})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": ["hello", "world"]}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias
|
||||
|
||||
|
||||
@app.post("/required-list-alias", operation_id="required_list_alias")
|
||||
async def read_required_list_alias(
|
||||
p: Annotated[List[str], Body(embed=True, alias="p_alias")],
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class BodyModelRequiredListAlias(BaseModel):
|
||||
p: List[str] = Field(alias="p_alias")
|
||||
|
||||
|
||||
@app.post("/model-required-list-alias", operation_id="model_required_list_alias")
|
||||
async def read_model_required_list_alias(p: BodyModelRequiredListAlias):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-list-alias",
|
||||
"/model-required-list-alias",
|
||||
],
|
||||
)
|
||||
def test_required_list_str_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == {
|
||||
"properties": {
|
||||
"p_alias": {
|
||||
"items": {"type": "string"},
|
||||
"title": "P Alias",
|
||||
"type": "array",
|
||||
},
|
||||
},
|
||||
"required": ["p_alias"],
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("json", [None, {}])
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-list-alias", "/model-required-list-alias"],
|
||||
)
|
||||
def test_required_list_alias_missing(path: str, json: Union[dict, None]):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json=json)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": IsOneOf(["body", "p_alias"], ["body"]),
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {}),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": IsOneOf(["body", "p_alias"], ["body"]),
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-list-alias", "/model-required-list-alias"],
|
||||
)
|
||||
def test_required_list_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={"p": ["hello", "world"]})
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "p_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {"p": ["hello", "world"]}),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "p_alias"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-list-alias", "/model-required-list-alias"],
|
||||
)
|
||||
def test_required_list_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={"p_alias": ["hello", "world"]})
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"p": ["hello", "world"]}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Validation alias
|
||||
|
||||
|
||||
@app.post(
|
||||
"/required-list-validation-alias", operation_id="required_list_validation_alias"
|
||||
)
|
||||
def read_required_list_validation_alias(
|
||||
p: Annotated[List[str], Body(embed=True, validation_alias="p_val_alias")],
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class BodyModelRequiredListValidationAlias(BaseModel):
|
||||
p: List[str] = Field(validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.post(
|
||||
"/model-required-list-validation-alias",
|
||||
operation_id="model_required_list_validation_alias",
|
||||
)
|
||||
async def read_model_required_list_validation_alias(
|
||||
p: BodyModelRequiredListValidationAlias,
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-list-validation-alias", "/model-required-list-validation-alias"],
|
||||
)
|
||||
def test_required_list_validation_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == {
|
||||
"properties": {
|
||||
"p_val_alias": {
|
||||
"items": {"type": "string"},
|
||||
"title": "P Val Alias",
|
||||
"type": "array",
|
||||
},
|
||||
},
|
||||
"required": ["p_val_alias"],
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize("json", [None, {}])
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-list-validation-alias",
|
||||
"/model-required-list-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_list_validation_alias_missing(path: str, json: Union[dict, None]):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json=json)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": IsOneOf(["body"], ["body", "p_val_alias"]),
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {}),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-list-validation-alias",
|
||||
"/model-required-list-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_list_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={"p": ["hello", "world"]})
|
||||
assert response.status_code == 422, response.text
|
||||
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "p_val_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, IsPartialDict({"p": ["hello", "world"]})),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-list-validation-alias",
|
||||
"/model-required-list-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_list_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={"p_val_alias": ["hello", "world"]})
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"p": ["hello", "world"]}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias and validation alias
|
||||
|
||||
|
||||
@app.post(
|
||||
"/required-list-alias-and-validation-alias",
|
||||
operation_id="required_list_alias_and_validation_alias",
|
||||
)
|
||||
def read_required_list_alias_and_validation_alias(
|
||||
p: Annotated[
|
||||
List[str], Body(embed=True, alias="p_alias", validation_alias="p_val_alias")
|
||||
],
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class BodyModelRequiredListAliasAndValidationAlias(BaseModel):
|
||||
p: List[str] = Field(alias="p_alias", validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.post(
|
||||
"/model-required-list-alias-and-validation-alias",
|
||||
operation_id="model_required_list_alias_and_validation_alias",
|
||||
)
|
||||
def read_model_required_list_alias_and_validation_alias(
|
||||
p: BodyModelRequiredListAliasAndValidationAlias,
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-list-alias-and-validation-alias",
|
||||
"/model-required-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_list_alias_and_validation_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == {
|
||||
"properties": {
|
||||
"p_val_alias": {
|
||||
"items": {"type": "string"},
|
||||
"title": "P Val Alias",
|
||||
"type": "array",
|
||||
},
|
||||
},
|
||||
"required": ["p_val_alias"],
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize("json", [None, {}])
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-list-alias-and-validation-alias",
|
||||
"/model-required-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_list_alias_and_validation_alias_missing(path: str, json):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json=json)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": IsOneOf(["body"], ["body", "p_val_alias"]),
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {}),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-list-alias-and-validation-alias",
|
||||
"/model-required-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_list_alias_and_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={"p": ["hello", "world"]})
|
||||
assert response.status_code == 422
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": [
|
||||
"body",
|
||||
"p_val_alias",
|
||||
],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {"p": ["hello", "world"]}),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-list-alias-and-validation-alias",
|
||||
"/model-required-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_list_alias_and_validation_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={"p_alias": ["hello", "world"]})
|
||||
assert response.status_code == 422, response.text
|
||||
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "p_val_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {"p_alias": ["hello", "world"]}),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-list-alias-and-validation-alias",
|
||||
"/model-required-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_list_alias_and_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={"p_val_alias": ["hello", "world"]})
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"p": ["hello", "world"]}
|
||||
575
tests/test_request_params/test_body/test_optional_list.py
Normal file
575
tests/test_request_params/test_body/test_optional_list.py
Normal file
@@ -0,0 +1,575 @@
|
||||
from typing import List, Optional
|
||||
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi import Body, FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel, Field
|
||||
from typing_extensions import Annotated
|
||||
|
||||
from tests.utils import needs_pydanticv2
|
||||
|
||||
from .utils import get_body_model_name
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# =====================================================================================
|
||||
# Without aliases
|
||||
|
||||
|
||||
@app.post("/optional-list-str", operation_id="optional_list_str")
|
||||
async def read_optional_list_str(
|
||||
p: Annotated[Optional[List[str]], Body(embed=True)] = None,
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class BodyModelOptionalListStr(BaseModel):
|
||||
p: Optional[List[str]] = None
|
||||
|
||||
|
||||
@app.post("/model-optional-list-str", operation_id="model_optional_list_str")
|
||||
async def read_model_optional_list_str(p: BodyModelOptionalListStr):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-str", "/model-optional-list-str"],
|
||||
)
|
||||
def test_optional_list_str_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
|
||||
{
|
||||
"properties": {
|
||||
"p": {
|
||||
"anyOf": [
|
||||
{"items": {"type": "string"}, "type": "array"},
|
||||
{"type": "null"},
|
||||
],
|
||||
"title": "P",
|
||||
},
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"properties": {
|
||||
"p": {"items": {"type": "string"}, "type": "array", "title": "P"},
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_optional_list_str_missing():
|
||||
client = TestClient(app)
|
||||
response = client.post("/optional-list-str")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
def test_model_optional_list_str_missing():
|
||||
client = TestClient(app)
|
||||
response = client.post("/model-optional-list-str")
|
||||
assert response.status_code == 422, response.text
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"input": None,
|
||||
"loc": ["body"],
|
||||
"msg": "Field required",
|
||||
"type": "missing",
|
||||
},
|
||||
],
|
||||
}
|
||||
) | IsDict(
|
||||
{
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-str", "/model-optional-list-str"],
|
||||
)
|
||||
def test_optional_list_str_missing_empty_dict(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={})
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-str", "/model-optional-list-str"],
|
||||
)
|
||||
def test_optional_list_str(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={"p": ["hello", "world"]})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": ["hello", "world"]}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias
|
||||
|
||||
|
||||
@app.post("/optional-list-alias", operation_id="optional_list_alias")
|
||||
async def read_optional_list_alias(
|
||||
p: Annotated[Optional[List[str]], Body(embed=True, alias="p_alias")] = None,
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class BodyModelOptionalListAlias(BaseModel):
|
||||
p: Optional[List[str]] = Field(None, alias="p_alias")
|
||||
|
||||
|
||||
@app.post("/model-optional-list-alias", operation_id="model_optional_list_alias")
|
||||
async def read_model_optional_list_alias(p: BodyModelOptionalListAlias):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-alias",
|
||||
"/model-optional-list-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_str_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
|
||||
{
|
||||
"properties": {
|
||||
"p_alias": {
|
||||
"anyOf": [
|
||||
{"items": {"type": "string"}, "type": "array"},
|
||||
{"type": "null"},
|
||||
],
|
||||
"title": "P Alias",
|
||||
},
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"properties": {
|
||||
"p_alias": {
|
||||
"items": {"type": "string"},
|
||||
"type": "array",
|
||||
"title": "P Alias",
|
||||
},
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_optional_list_alias_missing():
|
||||
client = TestClient(app)
|
||||
response = client.post("/optional-list-alias")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
def test_model_optional_list_alias_missing():
|
||||
client = TestClient(app)
|
||||
response = client.post("/model-optional-list-alias")
|
||||
assert response.status_code == 422, response.text
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"input": None,
|
||||
"loc": ["body"],
|
||||
"msg": "Field required",
|
||||
"type": "missing",
|
||||
},
|
||||
],
|
||||
}
|
||||
) | IsDict(
|
||||
{
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-alias", "/model-optional-list-alias"],
|
||||
)
|
||||
def test_optional_list_alias_missing_empty_dict(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={})
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-alias", "/model-optional-list-alias"],
|
||||
)
|
||||
def test_optional_list_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={"p": ["hello", "world"]})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-alias", "/model-optional-list-alias"],
|
||||
)
|
||||
def test_optional_list_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={"p_alias": ["hello", "world"]})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": ["hello", "world"]}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Validation alias
|
||||
|
||||
|
||||
@app.post(
|
||||
"/optional-list-validation-alias", operation_id="optional_list_validation_alias"
|
||||
)
|
||||
def read_optional_list_validation_alias(
|
||||
p: Annotated[
|
||||
Optional[List[str]], Body(embed=True, validation_alias="p_val_alias")
|
||||
] = None,
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class BodyModelOptionalListValidationAlias(BaseModel):
|
||||
p: Optional[List[str]] = Field(None, validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.post(
|
||||
"/model-optional-list-validation-alias",
|
||||
operation_id="model_optional_list_validation_alias",
|
||||
)
|
||||
def read_model_optional_list_validation_alias(
|
||||
p: BodyModelOptionalListValidationAlias,
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-validation-alias", "/model-optional-list-validation-alias"],
|
||||
)
|
||||
def test_optional_list_validation_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
|
||||
{
|
||||
"properties": {
|
||||
"p_val_alias": {
|
||||
"anyOf": [
|
||||
{"items": {"type": "string"}, "type": "array"},
|
||||
{"type": "null"},
|
||||
],
|
||||
"title": "P Val Alias",
|
||||
},
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"properties": {
|
||||
"p_val_alias": {
|
||||
"items": {"type": "string"},
|
||||
"type": "array",
|
||||
"title": "P Val Alias",
|
||||
},
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_optional_list_validation_alias_missing():
|
||||
client = TestClient(app)
|
||||
response = client.post("/optional-list-validation-alias")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
def test_model_optional_list_validation_alias_missing():
|
||||
client = TestClient(app)
|
||||
response = client.post("/model-optional-list-validation-alias")
|
||||
assert response.status_code == 422, response.text
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"input": None,
|
||||
"loc": ["body"],
|
||||
"msg": "Field required",
|
||||
"type": "missing",
|
||||
},
|
||||
],
|
||||
}
|
||||
) | IsDict(
|
||||
{
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-validation-alias", "/model-optional-list-validation-alias"],
|
||||
)
|
||||
def test_optional_list_validation_alias_missing_empty_dict(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={})
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-validation-alias",
|
||||
"/model-optional-list-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={"p": ["hello", "world"]})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-validation-alias",
|
||||
"/model-optional-list-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={"p_val_alias": ["hello", "world"]})
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"p": ["hello", "world"]}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias and validation alias
|
||||
|
||||
|
||||
@app.post(
|
||||
"/optional-list-alias-and-validation-alias",
|
||||
operation_id="optional_list_alias_and_validation_alias",
|
||||
)
|
||||
def read_optional_list_alias_and_validation_alias(
|
||||
p: Annotated[
|
||||
Optional[List[str]],
|
||||
Body(embed=True, alias="p_alias", validation_alias="p_val_alias"),
|
||||
] = None,
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class BodyModelOptionalListAliasAndValidationAlias(BaseModel):
|
||||
p: Optional[List[str]] = Field(
|
||||
None, alias="p_alias", validation_alias="p_val_alias"
|
||||
)
|
||||
|
||||
|
||||
@app.post(
|
||||
"/model-optional-list-alias-and-validation-alias",
|
||||
operation_id="model_optional_list_alias_and_validation_alias",
|
||||
)
|
||||
def read_model_optional_list_alias_and_validation_alias(
|
||||
p: BodyModelOptionalListAliasAndValidationAlias,
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-alias-and-validation-alias",
|
||||
"/model-optional-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_alias_and_validation_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
|
||||
{
|
||||
"properties": {
|
||||
"p_val_alias": {
|
||||
"anyOf": [
|
||||
{"items": {"type": "string"}, "type": "array"},
|
||||
{"type": "null"},
|
||||
],
|
||||
"title": "P Val Alias",
|
||||
},
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"properties": {
|
||||
"p_val_alias": {
|
||||
"items": {"type": "string"},
|
||||
"type": "array",
|
||||
"title": "P Val Alias",
|
||||
},
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_optional_list_alias_and_validation_alias_missing():
|
||||
client = TestClient(app)
|
||||
response = client.post("/optional-list-alias-and-validation-alias")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
def test_model_optional_list_alias_and_validation_alias_missing():
|
||||
client = TestClient(app)
|
||||
response = client.post("/model-optional-list-alias-and-validation-alias")
|
||||
assert response.status_code == 422, response.text
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"input": None,
|
||||
"loc": ["body"],
|
||||
"msg": "Field required",
|
||||
"type": "missing",
|
||||
},
|
||||
],
|
||||
}
|
||||
) | IsDict(
|
||||
{
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-alias-and-validation-alias",
|
||||
"/model-optional-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_alias_and_validation_alias_missing_empty_dict(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={})
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-alias-and-validation-alias",
|
||||
"/model-optional-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_alias_and_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={"p": ["hello", "world"]})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-alias-and-validation-alias",
|
||||
"/model-optional-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_alias_and_validation_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={"p_alias": ["hello", "world"]})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-alias-and-validation-alias",
|
||||
"/model-optional-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_alias_and_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={"p_val_alias": ["hello", "world"]})
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {
|
||||
"p": [
|
||||
"hello",
|
||||
"world",
|
||||
]
|
||||
}
|
||||
544
tests/test_request_params/test_body/test_optional_str.py
Normal file
544
tests/test_request_params/test_body/test_optional_str.py
Normal file
@@ -0,0 +1,544 @@
|
||||
from typing import Optional
|
||||
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi import Body, FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel, Field
|
||||
from typing_extensions import Annotated
|
||||
|
||||
from tests.utils import needs_pydanticv2
|
||||
|
||||
from .utils import get_body_model_name
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# =====================================================================================
|
||||
# Without aliases
|
||||
|
||||
|
||||
@app.post("/optional-str", operation_id="optional_str")
|
||||
async def read_optional_str(p: Annotated[Optional[str], Body(embed=True)] = None):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class BodyModelOptionalStr(BaseModel):
|
||||
p: Optional[str] = None
|
||||
|
||||
|
||||
@app.post("/model-optional-str", operation_id="model_optional_str")
|
||||
async def read_model_optional_str(p: BodyModelOptionalStr):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-str", "/model-optional-str"],
|
||||
)
|
||||
def test_optional_str_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
|
||||
{
|
||||
"properties": {
|
||||
"p": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "P",
|
||||
},
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"properties": {
|
||||
"p": {"type": "string", "title": "P"},
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_optional_str_missing():
|
||||
client = TestClient(app)
|
||||
response = client.post("/optional-str")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
def test_model_optional_str_missing():
|
||||
client = TestClient(app)
|
||||
response = client.post("/model-optional-str")
|
||||
assert response.status_code == 422, response.text
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"input": None,
|
||||
"loc": ["body"],
|
||||
"msg": "Field required",
|
||||
"type": "missing",
|
||||
},
|
||||
],
|
||||
}
|
||||
) | IsDict(
|
||||
{
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-str", "/model-optional-str"],
|
||||
)
|
||||
def test_optional_str_missing_empty_dict(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={})
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-str", "/model-optional-str"],
|
||||
)
|
||||
def test_optional_str(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={"p": "hello"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": "hello"}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias
|
||||
|
||||
|
||||
@app.post("/optional-alias", operation_id="optional_alias")
|
||||
async def read_optional_alias(
|
||||
p: Annotated[Optional[str], Body(embed=True, alias="p_alias")] = None,
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class BodyModelOptionalAlias(BaseModel):
|
||||
p: Optional[str] = Field(None, alias="p_alias")
|
||||
|
||||
|
||||
@app.post("/model-optional-alias", operation_id="model_optional_alias")
|
||||
async def read_model_optional_alias(p: BodyModelOptionalAlias):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-alias",
|
||||
"/model-optional-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_str_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
|
||||
{
|
||||
"properties": {
|
||||
"p_alias": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "P Alias",
|
||||
},
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"properties": {
|
||||
"p_alias": {"type": "string", "title": "P Alias"},
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_optional_alias_missing():
|
||||
client = TestClient(app)
|
||||
response = client.post("/optional-alias")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
def test_model_optional_alias_missing():
|
||||
client = TestClient(app)
|
||||
response = client.post("/model-optional-alias")
|
||||
assert response.status_code == 422, response.text
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"input": None,
|
||||
"loc": ["body"],
|
||||
"msg": "Field required",
|
||||
"type": "missing",
|
||||
},
|
||||
],
|
||||
}
|
||||
) | IsDict(
|
||||
{
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-alias", "/model-optional-alias"],
|
||||
)
|
||||
def test_model_optional_alias_missing_empty_dict(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={})
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-alias", "/model-optional-alias"],
|
||||
)
|
||||
def test_optional_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={"p": "hello"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-alias", "/model-optional-alias"],
|
||||
)
|
||||
def test_optional_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={"p_alias": "hello"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": "hello"}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Validation alias
|
||||
|
||||
|
||||
@app.post("/optional-validation-alias", operation_id="optional_validation_alias")
|
||||
def read_optional_validation_alias(
|
||||
p: Annotated[
|
||||
Optional[str], Body(embed=True, validation_alias="p_val_alias")
|
||||
] = None,
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class BodyModelOptionalValidationAlias(BaseModel):
|
||||
p: Optional[str] = Field(None, validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.post(
|
||||
"/model-optional-validation-alias", operation_id="model_optional_validation_alias"
|
||||
)
|
||||
def read_model_optional_validation_alias(
|
||||
p: BodyModelOptionalValidationAlias,
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-validation-alias", "/model-optional-validation-alias"],
|
||||
)
|
||||
def test_optional_validation_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
|
||||
{
|
||||
"properties": {
|
||||
"p_val_alias": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "P Val Alias",
|
||||
},
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"properties": {
|
||||
"p_val_alias": {"type": "string", "title": "P Val Alias"},
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
def test_optional_validation_alias_missing():
|
||||
client = TestClient(app)
|
||||
response = client.post("/optional-validation-alias")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
def test_model_optional_validation_alias_missing():
|
||||
client = TestClient(app)
|
||||
response = client.post("/model-optional-validation-alias")
|
||||
assert response.status_code == 422, response.text
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"input": None,
|
||||
"loc": ["body"],
|
||||
"msg": "Field required",
|
||||
"type": "missing",
|
||||
},
|
||||
],
|
||||
}
|
||||
) | IsDict(
|
||||
{
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-validation-alias", "/model-optional-validation-alias"],
|
||||
)
|
||||
def test_model_optional_validation_alias_missing_empty_dict(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={})
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-validation-alias",
|
||||
"/model-optional-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={"p": "hello"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-validation-alias",
|
||||
"/model-optional-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={"p_val_alias": "hello"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": "hello"}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias and validation alias
|
||||
|
||||
|
||||
@app.post(
|
||||
"/optional-alias-and-validation-alias",
|
||||
operation_id="optional_alias_and_validation_alias",
|
||||
)
|
||||
def read_optional_alias_and_validation_alias(
|
||||
p: Annotated[
|
||||
Optional[str], Body(embed=True, alias="p_alias", validation_alias="p_val_alias")
|
||||
] = None,
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class BodyModelOptionalAliasAndValidationAlias(BaseModel):
|
||||
p: Optional[str] = Field(None, alias="p_alias", validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.post(
|
||||
"/model-optional-alias-and-validation-alias",
|
||||
operation_id="model_optional_alias_and_validation_alias",
|
||||
)
|
||||
def read_model_optional_alias_and_validation_alias(
|
||||
p: BodyModelOptionalAliasAndValidationAlias,
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-alias-and-validation-alias",
|
||||
"/model-optional-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_alias_and_validation_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
|
||||
{
|
||||
"properties": {
|
||||
"p_val_alias": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "P Val Alias",
|
||||
},
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"properties": {
|
||||
"p_val_alias": {"type": "string", "title": "P Val Alias"},
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
def test_optional_alias_and_validation_alias_missing():
|
||||
client = TestClient(app)
|
||||
response = client.post("/optional-alias-and-validation-alias")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
def test_model_optional_alias_and_validation_alias_missing():
|
||||
client = TestClient(app)
|
||||
response = client.post("/model-optional-alias-and-validation-alias")
|
||||
assert response.status_code == 422, response.text
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"input": None,
|
||||
"loc": ["body"],
|
||||
"msg": "Field required",
|
||||
"type": "missing",
|
||||
},
|
||||
],
|
||||
}
|
||||
) | IsDict(
|
||||
{
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-alias-and-validation-alias",
|
||||
"/model-optional-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_model_optional_alias_and_validation_alias_missing_empty_dict(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={})
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-alias-and-validation-alias",
|
||||
"/model-optional-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_alias_and_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={"p": "hello"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-alias-and-validation-alias",
|
||||
"/model-optional-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_alias_and_validation_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={"p_alias": "hello"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-alias-and-validation-alias",
|
||||
"/model-optional-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_alias_and_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={"p_val_alias": "hello"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": "hello"}
|
||||
472
tests/test_request_params/test_body/test_required_str.py
Normal file
472
tests/test_request_params/test_body/test_required_str.py
Normal file
@@ -0,0 +1,472 @@
|
||||
from typing import Any, Dict, Union
|
||||
|
||||
import pytest
|
||||
from dirty_equals import IsDict, IsOneOf
|
||||
from fastapi import Body, FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel, Field
|
||||
from typing_extensions import Annotated
|
||||
|
||||
from tests.utils import needs_pydanticv2
|
||||
|
||||
from .utils import get_body_model_name
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# =====================================================================================
|
||||
# Without aliases
|
||||
|
||||
|
||||
@app.post("/required-str", operation_id="required_str")
|
||||
async def read_required_str(p: Annotated[str, Body(embed=True)]):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class BodyModelRequiredStr(BaseModel):
|
||||
p: str
|
||||
|
||||
|
||||
@app.post("/model-required-str", operation_id="model_required_str")
|
||||
async def read_model_required_str(p: BodyModelRequiredStr):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-str", "/model-required-str"],
|
||||
)
|
||||
def test_required_str_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == {
|
||||
"properties": {
|
||||
"p": {"title": "P", "type": "string"},
|
||||
},
|
||||
"required": ["p"],
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("json", [None, {}])
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-str", "/model-required-str"],
|
||||
)
|
||||
def test_required_str_missing(path: str, json: Union[Dict[str, Any], None]):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json=json)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": IsOneOf(["body"], ["body", "p"]),
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {}),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": IsOneOf(["body"], ["body", "p"]),
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-str", "/model-required-str"],
|
||||
)
|
||||
def test_required_str(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={"p": "hello"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": "hello"}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias
|
||||
|
||||
|
||||
@app.post("/required-alias", operation_id="required_alias")
|
||||
async def read_required_alias(
|
||||
p: Annotated[str, Body(embed=True, alias="p_alias")],
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class BodyModelRequiredAlias(BaseModel):
|
||||
p: str = Field(alias="p_alias")
|
||||
|
||||
|
||||
@app.post("/model-required-alias", operation_id="model_required_alias")
|
||||
async def read_model_required_alias(p: BodyModelRequiredAlias):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-alias",
|
||||
"/model-required-alias",
|
||||
],
|
||||
)
|
||||
def test_required_str_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == {
|
||||
"properties": {
|
||||
"p_alias": {"title": "P Alias", "type": "string"},
|
||||
},
|
||||
"required": ["p_alias"],
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("json", [None, {}])
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-alias", "/model-required-alias"],
|
||||
)
|
||||
def test_required_alias_missing(path: str, json: Union[Dict[str, Any], None]):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json=json)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": IsOneOf(["body", "p_alias"], ["body"]),
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {}),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": IsOneOf(["body", "p_alias"], ["body"]),
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-alias", "/model-required-alias"],
|
||||
)
|
||||
def test_required_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={"p": "hello"})
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "p_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {"p": "hello"}),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": IsOneOf(["body", "p_alias"], ["body"]),
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-alias", "/model-required-alias"],
|
||||
)
|
||||
def test_required_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={"p_alias": "hello"})
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"p": "hello"}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Validation alias
|
||||
|
||||
|
||||
@app.post("/required-validation-alias", operation_id="required_validation_alias")
|
||||
def read_required_validation_alias(
|
||||
p: Annotated[str, Body(embed=True, validation_alias="p_val_alias")],
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class BodyModelRequiredValidationAlias(BaseModel):
|
||||
p: str = Field(validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.post(
|
||||
"/model-required-validation-alias", operation_id="model_required_validation_alias"
|
||||
)
|
||||
def read_model_required_validation_alias(
|
||||
p: BodyModelRequiredValidationAlias,
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-validation-alias", "/model-required-validation-alias"],
|
||||
)
|
||||
def test_required_validation_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == {
|
||||
"properties": {
|
||||
"p_val_alias": {"title": "P Val Alias", "type": "string"},
|
||||
},
|
||||
"required": ["p_val_alias"],
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize("json", [None, {}])
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-validation-alias",
|
||||
"/model-required-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_validation_alias_missing(
|
||||
path: str, json: Union[Dict[str, Any], None]
|
||||
):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json=json)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": IsOneOf(["body", "p_val_alias"], ["body"]),
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {}),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-validation-alias",
|
||||
"/model-required-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={"p": "hello"})
|
||||
assert response.status_code == 422, response.text
|
||||
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "p_val_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {"p": "hello"}),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-validation-alias",
|
||||
"/model-required-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={"p_val_alias": "hello"})
|
||||
assert response.status_code == 200, response.text
|
||||
|
||||
assert response.json() == {"p": "hello"}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias and validation alias
|
||||
|
||||
|
||||
@app.post(
|
||||
"/required-alias-and-validation-alias",
|
||||
operation_id="required_alias_and_validation_alias",
|
||||
)
|
||||
def read_required_alias_and_validation_alias(
|
||||
p: Annotated[
|
||||
str, Body(embed=True, alias="p_alias", validation_alias="p_val_alias")
|
||||
],
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class BodyModelRequiredAliasAndValidationAlias(BaseModel):
|
||||
p: str = Field(alias="p_alias", validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.post(
|
||||
"/model-required-alias-and-validation-alias",
|
||||
operation_id="model_required_alias_and_validation_alias",
|
||||
)
|
||||
def read_model_required_alias_and_validation_alias(
|
||||
p: BodyModelRequiredAliasAndValidationAlias,
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-alias-and-validation-alias",
|
||||
"/model-required-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_and_validation_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == {
|
||||
"properties": {
|
||||
"p_val_alias": {"title": "P Val Alias", "type": "string"},
|
||||
},
|
||||
"required": ["p_val_alias"],
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize("json", [None, {}])
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-alias-and-validation-alias",
|
||||
"/model-required-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_and_validation_alias_missing(
|
||||
path: str, json: Union[Dict[str, Any], None]
|
||||
):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json=json)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": IsOneOf(["body"], ["body", "p_val_alias"]),
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {}),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-alias-and-validation-alias",
|
||||
"/model-required-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_and_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={"p": "hello"})
|
||||
assert response.status_code == 422
|
||||
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": [
|
||||
"body",
|
||||
"p_val_alias",
|
||||
],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {"p": "hello"}),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-alias-and-validation-alias",
|
||||
"/model-required-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_and_validation_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={"p_alias": "hello"})
|
||||
assert response.status_code == 422, response.text
|
||||
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "p_val_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {"p_alias": "hello"}),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-alias-and-validation-alias",
|
||||
"/model-required-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_and_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={"p_val_alias": "hello"})
|
||||
assert response.status_code == 200, response.text
|
||||
|
||||
assert response.json() == {"p": "hello"}
|
||||
7
tests/test_request_params/test_body/utils.py
Normal file
7
tests/test_request_params/test_body/utils.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from typing import Any, Dict
|
||||
|
||||
|
||||
def get_body_model_name(openapi: Dict[str, Any], path: str) -> str:
|
||||
body = openapi["paths"][path]["post"]["requestBody"]
|
||||
body_schema = body["content"]["application/json"]["schema"]
|
||||
return body_schema.get("$ref", "").split("/")[-1]
|
||||
0
tests/test_request_params/test_cookie/__init__.py
Normal file
0
tests/test_request_params/test_cookie/__init__.py
Normal file
3
tests/test_request_params/test_cookie/test_list.py
Normal file
3
tests/test_request_params/test_cookie/test_list.py
Normal file
@@ -0,0 +1,3 @@
|
||||
# Currently, there is no way to pass multiple cookies with the same name.
|
||||
# The only way to pass multiple values for cookie params is to serialize them using
|
||||
# a comma as a delimiter, but this is not currently supported by Starlette.
|
||||
@@ -0,0 +1,3 @@
|
||||
# Currently, there is no way to pass multiple cookies with the same name.
|
||||
# The only way to pass multiple values for cookie params is to serialize them using
|
||||
# a comma as a delimiter, but this is not currently supported by Starlette.
|
||||
362
tests/test_request_params/test_cookie/test_optional_str.py
Normal file
362
tests/test_request_params/test_cookie/test_optional_str.py
Normal file
@@ -0,0 +1,362 @@
|
||||
from typing import Optional
|
||||
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi import Cookie, FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel, Field
|
||||
from typing_extensions import Annotated
|
||||
|
||||
from tests.utils import needs_pydanticv2
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# =====================================================================================
|
||||
# Without aliases
|
||||
|
||||
|
||||
@app.get("/optional-str")
|
||||
async def read_optional_str(p: Annotated[Optional[str], Cookie()] = None):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class CookieModelOptionalStr(BaseModel):
|
||||
p: Optional[str] = None
|
||||
|
||||
|
||||
@app.get("/model-optional-str")
|
||||
async def read_model_optional_str(p: Annotated[CookieModelOptionalStr, Cookie()]):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-str", "/model-optional-str"],
|
||||
)
|
||||
def test_optional_str_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
IsDict(
|
||||
{
|
||||
"required": False,
|
||||
"schema": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "P",
|
||||
},
|
||||
"name": "p",
|
||||
"in": "cookie",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"required": False,
|
||||
"schema": {"title": "P", "type": "string"},
|
||||
"name": "p",
|
||||
"in": "cookie",
|
||||
}
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-str", "/model-optional-str"],
|
||||
)
|
||||
def test_optional_str_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-str", "/model-optional-str"],
|
||||
)
|
||||
def test_optional_str(path: str):
|
||||
client = TestClient(app)
|
||||
client.cookies.set("p", "hello")
|
||||
response = client.get(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": "hello"}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias
|
||||
|
||||
|
||||
@app.get("/optional-alias")
|
||||
async def read_optional_alias(
|
||||
p: Annotated[Optional[str], Cookie(alias="p_alias")] = None,
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class CookieModelOptionalAlias(BaseModel):
|
||||
p: Optional[str] = Field(None, alias="p_alias")
|
||||
|
||||
|
||||
@app.get("/model-optional-alias")
|
||||
async def read_model_optional_alias(p: Annotated[CookieModelOptionalAlias, Cookie()]):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-alias", "/model-optional-alias"],
|
||||
)
|
||||
def test_optional_str_alias_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
IsDict(
|
||||
{
|
||||
"required": False,
|
||||
"schema": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "P Alias",
|
||||
},
|
||||
"name": "p_alias",
|
||||
"in": "cookie",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"required": False,
|
||||
"schema": {"title": "P Alias", "type": "string"},
|
||||
"name": "p_alias",
|
||||
"in": "cookie",
|
||||
}
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-alias", "/model-optional-alias"],
|
||||
)
|
||||
def test_optional_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-alias", "/model-optional-alias"],
|
||||
)
|
||||
def test_optional_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
client.cookies.set("p", "hello")
|
||||
response = client.get(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-alias",
|
||||
"/model-optional-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
client.cookies.set("p_alias", "hello")
|
||||
response = client.get(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": "hello"}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Validation alias
|
||||
|
||||
|
||||
@app.get("/optional-validation-alias")
|
||||
def read_optional_validation_alias(
|
||||
p: Annotated[Optional[str], Cookie(validation_alias="p_val_alias")] = None,
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class CookieModelOptionalValidationAlias(BaseModel):
|
||||
p: Optional[str] = Field(None, validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.get("/model-optional-validation-alias")
|
||||
def read_model_optional_validation_alias(
|
||||
p: Annotated[CookieModelOptionalValidationAlias, Cookie()],
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-validation-alias", "/model-optional-validation-alias"],
|
||||
)
|
||||
def test_optional_validation_alias_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
{
|
||||
"required": False,
|
||||
"schema": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "P Val Alias",
|
||||
},
|
||||
"name": "p_val_alias",
|
||||
"in": "cookie",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-validation-alias", "/model-optional-validation-alias"],
|
||||
)
|
||||
def test_optional_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-validation-alias",
|
||||
"/model-optional-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
client.cookies.set("p", "hello")
|
||||
response = client.get(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-validation-alias",
|
||||
"/model-optional-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
client.cookies.set("p_val_alias", "hello")
|
||||
response = client.get(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": "hello"}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias and validation alias
|
||||
|
||||
|
||||
@app.get("/optional-alias-and-validation-alias")
|
||||
def read_optional_alias_and_validation_alias(
|
||||
p: Annotated[
|
||||
Optional[str], Cookie(alias="p_alias", validation_alias="p_val_alias")
|
||||
] = None,
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class CookieModelOptionalAliasAndValidationAlias(BaseModel):
|
||||
p: Optional[str] = Field(None, alias="p_alias", validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.get("/model-optional-alias-and-validation-alias")
|
||||
def read_model_optional_alias_and_validation_alias(
|
||||
p: Annotated[CookieModelOptionalAliasAndValidationAlias, Cookie()],
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-alias-and-validation-alias",
|
||||
"/model-optional-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_alias_and_validation_alias_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
{
|
||||
"required": False,
|
||||
"schema": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "P Val Alias",
|
||||
},
|
||||
"name": "p_val_alias",
|
||||
"in": "cookie",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-alias-and-validation-alias",
|
||||
"/model-optional-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_alias_and_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-alias-and-validation-alias",
|
||||
"/model-optional-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_alias_and_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
client.cookies.set("p", "hello")
|
||||
response = client.get(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-alias-and-validation-alias",
|
||||
"/model-optional-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_alias_and_validation_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
client.cookies.set("p_alias", "hello")
|
||||
response = client.get(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-alias-and-validation-alias",
|
||||
"/model-optional-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_alias_and_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
client.cookies.set("p_val_alias", "hello")
|
||||
response = client.get(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": "hello"}
|
||||
462
tests/test_request_params/test_cookie/test_required_str.py
Normal file
462
tests/test_request_params/test_cookie/test_required_str.py
Normal file
@@ -0,0 +1,462 @@
|
||||
import pytest
|
||||
from dirty_equals import IsDict, IsOneOf
|
||||
from fastapi import Cookie, FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel, Field
|
||||
from typing_extensions import Annotated
|
||||
|
||||
from tests.utils import needs_pydanticv2
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# =====================================================================================
|
||||
# Without aliases
|
||||
|
||||
|
||||
@app.get("/required-str")
|
||||
async def read_required_str(p: Annotated[str, Cookie()]):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class CookieModelRequiredStr(BaseModel):
|
||||
p: str
|
||||
|
||||
|
||||
@app.get("/model-required-str")
|
||||
async def read_model_required_str(p: Annotated[CookieModelRequiredStr, Cookie()]):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-str", "/model-required-str"],
|
||||
)
|
||||
def test_required_str_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "P", "type": "string"},
|
||||
"name": "p",
|
||||
"in": "cookie",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-str", "/model-required-str"],
|
||||
)
|
||||
def test_required_str_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["cookie", "p"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {}),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["cookie", "p"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-str", "/model-required-str"],
|
||||
)
|
||||
def test_required_str(path: str):
|
||||
client = TestClient(app)
|
||||
client.cookies.set("p", "hello")
|
||||
response = client.get(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": "hello"}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias
|
||||
|
||||
|
||||
@app.get("/required-alias")
|
||||
async def read_required_alias(p: Annotated[str, Cookie(alias="p_alias")]):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class CookieModelRequiredAlias(BaseModel):
|
||||
p: str = Field(alias="p_alias")
|
||||
|
||||
|
||||
@app.get("/model-required-alias")
|
||||
async def read_model_required_alias(p: Annotated[CookieModelRequiredAlias, Cookie()]):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-alias", "/model-required-alias"],
|
||||
)
|
||||
def test_required_str_alias_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "P Alias", "type": "string"},
|
||||
"name": "p_alias",
|
||||
"in": "cookie",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-alias", "/model-required-alias"],
|
||||
)
|
||||
def test_required_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["cookie", "p_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {}),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["cookie", "p_alias"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-alias",
|
||||
"/model-required-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
client.cookies.set("p", "hello")
|
||||
response = client.get(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["cookie", "p_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(
|
||||
None,
|
||||
{"p": "hello"},
|
||||
),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["cookie", "p_alias"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-alias",
|
||||
"/model-required-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
client.cookies.set("p_alias", "hello")
|
||||
response = client.get(path)
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"p": "hello"}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Validation alias
|
||||
|
||||
|
||||
@app.get("/required-validation-alias")
|
||||
def read_required_validation_alias(
|
||||
p: Annotated[str, Cookie(validation_alias="p_val_alias")],
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class CookieModelRequiredValidationAlias(BaseModel):
|
||||
p: str = Field(validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.get("/model-required-validation-alias")
|
||||
def read_model_required_validation_alias(
|
||||
p: Annotated[CookieModelRequiredValidationAlias, Cookie()],
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-validation-alias", "/model-required-validation-alias"],
|
||||
)
|
||||
def test_required_validation_alias_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "P Val Alias", "type": "string"},
|
||||
"name": "p_val_alias",
|
||||
"in": "cookie",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-validation-alias",
|
||||
"/model-required-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": [
|
||||
"cookie",
|
||||
"p_val_alias",
|
||||
],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {}),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-validation-alias",
|
||||
"/model-required-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
client.cookies.set("p", "hello")
|
||||
response = client.get(path)
|
||||
assert response.status_code == 422, response.text
|
||||
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["cookie", "p_val_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {"p": "hello"}),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-validation-alias",
|
||||
"/model-required-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
client.cookies.set("p_val_alias", "hello")
|
||||
response = client.get(path)
|
||||
assert response.status_code == 200, response.text
|
||||
|
||||
assert response.json() == {"p": "hello"}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias and validation alias
|
||||
|
||||
|
||||
@app.get("/required-alias-and-validation-alias")
|
||||
def read_required_alias_and_validation_alias(
|
||||
p: Annotated[str, Cookie(alias="p_alias", validation_alias="p_val_alias")],
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class CookieModelRequiredAliasAndValidationAlias(BaseModel):
|
||||
p: str = Field(alias="p_alias", validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.get("/model-required-alias-and-validation-alias")
|
||||
def read_model_required_alias_and_validation_alias(
|
||||
p: Annotated[CookieModelRequiredAliasAndValidationAlias, Cookie()],
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-alias-and-validation-alias",
|
||||
"/model-required-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_and_validation_alias_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "P Val Alias", "type": "string"},
|
||||
"name": "p_val_alias",
|
||||
"in": "cookie",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-alias-and-validation-alias",
|
||||
"/model-required-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_and_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": [
|
||||
"cookie",
|
||||
"p_val_alias",
|
||||
],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {}),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-alias-and-validation-alias",
|
||||
"/model-required-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_and_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
client.cookies.set("p", "hello")
|
||||
response = client.get(path)
|
||||
assert response.status_code == 422
|
||||
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": [
|
||||
"cookie",
|
||||
"p_val_alias",
|
||||
],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(
|
||||
None,
|
||||
{"p": "hello"},
|
||||
),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-alias-and-validation-alias",
|
||||
"/model-required-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_and_validation_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
client.cookies.set("p_alias", "hello")
|
||||
response = client.get(path)
|
||||
assert response.status_code == 422
|
||||
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["cookie", "p_val_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(
|
||||
None,
|
||||
{"p_alias": "hello"},
|
||||
),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-alias-and-validation-alias",
|
||||
"/model-required-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_and_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
client.cookies.set("p_val_alias", "hello")
|
||||
response = client.get(path)
|
||||
assert response.status_code == 200, response.text
|
||||
|
||||
assert response.json() == {"p": "hello"}
|
||||
0
tests/test_request_params/test_file/__init__.py
Normal file
0
tests/test_request_params/test_file/__init__.py
Normal file
557
tests/test_request_params/test_file/test_list.py
Normal file
557
tests/test_request_params/test_file/test_list.py
Normal file
@@ -0,0 +1,557 @@
|
||||
from typing import List
|
||||
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi import FastAPI, File, UploadFile
|
||||
from fastapi.testclient import TestClient
|
||||
from typing_extensions import Annotated
|
||||
|
||||
from tests.utils import needs_pydanticv2
|
||||
|
||||
from .utils import get_body_model_name
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# =====================================================================================
|
||||
# Without aliases
|
||||
|
||||
|
||||
@app.post("/list-bytes", operation_id="list_bytes")
|
||||
async def read_list_bytes(p: Annotated[List[bytes], File()]):
|
||||
return {"file_size": [len(file) for file in p]}
|
||||
|
||||
|
||||
@app.post("/list-uploadfile", operation_id="list_uploadfile")
|
||||
async def read_list_uploadfile(p: Annotated[List[UploadFile], File()]):
|
||||
return {"file_size": [file.size for file in p]}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/list-bytes",
|
||||
"/list-uploadfile",
|
||||
],
|
||||
)
|
||||
def test_list_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == {
|
||||
"properties": {
|
||||
"p": (
|
||||
IsDict(
|
||||
{
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "array",
|
||||
"items": {"type": "string", "format": "binary"},
|
||||
},
|
||||
{"type": "null"},
|
||||
],
|
||||
"title": "P",
|
||||
},
|
||||
)
|
||||
| IsDict(
|
||||
{
|
||||
"type": "array",
|
||||
"items": {"type": "string", "format": "binary"},
|
||||
"title": "P",
|
||||
},
|
||||
)
|
||||
)
|
||||
},
|
||||
"required": ["p"],
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/list-bytes",
|
||||
"/list-uploadfile",
|
||||
],
|
||||
)
|
||||
def test_list_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "p"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "p"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/list-bytes",
|
||||
"/list-uploadfile",
|
||||
],
|
||||
)
|
||||
def test_list(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, files=[("p", b"hello"), ("p", b"world")])
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"file_size": [5, 5]}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias
|
||||
|
||||
|
||||
@app.post("/list-bytes-alias", operation_id="list_bytes_alias")
|
||||
async def read_list_bytes_alias(p: Annotated[List[bytes], File(alias="p_alias")]):
|
||||
return {"file_size": [len(file) for file in p]}
|
||||
|
||||
|
||||
@app.post("/list-uploadfile-alias", operation_id="list_uploadfile_alias")
|
||||
async def read_list_uploadfile_alias(
|
||||
p: Annotated[List[UploadFile], File(alias="p_alias")],
|
||||
):
|
||||
return {"file_size": [file.size for file in p]}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/list-bytes-alias",
|
||||
"/list-uploadfile-alias",
|
||||
],
|
||||
)
|
||||
def test_list_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == {
|
||||
"properties": {
|
||||
"p_alias": (
|
||||
IsDict(
|
||||
{
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "array",
|
||||
"items": {"type": "string", "format": "binary"},
|
||||
},
|
||||
{"type": "null"},
|
||||
],
|
||||
"title": "P Alias",
|
||||
},
|
||||
)
|
||||
| IsDict(
|
||||
{
|
||||
"type": "array",
|
||||
"items": {"type": "string", "format": "binary"},
|
||||
"title": "P Alias",
|
||||
},
|
||||
)
|
||||
)
|
||||
},
|
||||
"required": ["p_alias"],
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/list-bytes-alias",
|
||||
"/list-uploadfile-alias",
|
||||
],
|
||||
)
|
||||
def test_list_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "p_alias"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "p_alias"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/list-bytes-alias",
|
||||
"/list-uploadfile-alias",
|
||||
],
|
||||
)
|
||||
def test_list_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, files=[("p", b"hello"), ("p", b"world")])
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "p_alias"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "p_alias"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/list-bytes-alias",
|
||||
"/list-uploadfile-alias",
|
||||
],
|
||||
)
|
||||
def test_list_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, files=[("p_alias", b"hello"), ("p_alias", b"world")])
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"file_size": [5, 5]}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Validation alias
|
||||
|
||||
|
||||
@app.post("/list-bytes-validation-alias", operation_id="list_bytes_validation_alias")
|
||||
def read_list_bytes_validation_alias(
|
||||
p: Annotated[List[bytes], File(validation_alias="p_val_alias")],
|
||||
):
|
||||
return {"file_size": [len(file) for file in p]}
|
||||
|
||||
|
||||
@app.post(
|
||||
"/list-uploadfile-validation-alias",
|
||||
operation_id="list_uploadfile_validation_alias",
|
||||
)
|
||||
def read_list_uploadfile_validation_alias(
|
||||
p: Annotated[List[UploadFile], File(validation_alias="p_val_alias")],
|
||||
):
|
||||
return {"file_size": [file.size for file in p]}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/list-bytes-validation-alias",
|
||||
"/list-uploadfile-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_list_validation_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == {
|
||||
"properties": {
|
||||
"p_val_alias": (
|
||||
IsDict(
|
||||
{
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "array",
|
||||
"items": {"type": "string", "format": "binary"},
|
||||
},
|
||||
{"type": "null"},
|
||||
],
|
||||
"title": "P Val Alias",
|
||||
},
|
||||
)
|
||||
| IsDict(
|
||||
{
|
||||
"type": "array",
|
||||
"items": {"type": "string", "format": "binary"},
|
||||
"title": "P Val Alias",
|
||||
},
|
||||
)
|
||||
)
|
||||
},
|
||||
"required": ["p_val_alias"],
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/list-bytes-validation-alias",
|
||||
"/list-uploadfile-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_list_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": [
|
||||
"body",
|
||||
"p_val_alias",
|
||||
],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/list-bytes-validation-alias",
|
||||
"/list-uploadfile-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_list_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, files=[("p", b"hello"), ("p", b"world")])
|
||||
assert response.status_code == 422, response.text
|
||||
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "p_val_alias"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/list-bytes-validation-alias",
|
||||
"/list-uploadfile-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_list_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(
|
||||
path, files=[("p_val_alias", b"hello"), ("p_val_alias", b"world")]
|
||||
)
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"file_size": [5, 5]}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias and validation alias
|
||||
|
||||
|
||||
@app.post(
|
||||
"/list-bytes-alias-and-validation-alias",
|
||||
operation_id="list_bytes_alias_and_validation_alias",
|
||||
)
|
||||
def read_list_bytes_alias_and_validation_alias(
|
||||
p: Annotated[List[bytes], File(alias="p_alias", validation_alias="p_val_alias")],
|
||||
):
|
||||
return {"file_size": [len(file) for file in p]}
|
||||
|
||||
|
||||
@app.post(
|
||||
"/list-uploadfile-alias-and-validation-alias",
|
||||
operation_id="list_uploadfile_alias_and_validation_alias",
|
||||
)
|
||||
def read_list_uploadfile_alias_and_validation_alias(
|
||||
p: Annotated[
|
||||
List[UploadFile], File(alias="p_alias", validation_alias="p_val_alias")
|
||||
],
|
||||
):
|
||||
return {"file_size": [file.size for file in p]}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/list-bytes-alias-and-validation-alias",
|
||||
"/list-uploadfile-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_list_alias_and_validation_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == {
|
||||
"properties": {
|
||||
"p_val_alias": (
|
||||
IsDict(
|
||||
{
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "array",
|
||||
"items": {"type": "string", "format": "binary"},
|
||||
},
|
||||
{"type": "null"},
|
||||
],
|
||||
"title": "P Val Alias",
|
||||
},
|
||||
)
|
||||
| IsDict(
|
||||
{
|
||||
"type": "array",
|
||||
"items": {"type": "string", "format": "binary"},
|
||||
"title": "P Val Alias",
|
||||
},
|
||||
)
|
||||
)
|
||||
},
|
||||
"required": ["p_val_alias"],
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/list-bytes-alias-and-validation-alias",
|
||||
"/list-uploadfile-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_list_alias_and_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": [
|
||||
"body",
|
||||
"p_val_alias",
|
||||
],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/list-bytes-alias-and-validation-alias",
|
||||
"/list-uploadfile-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_list_alias_and_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, files=[("p", "hello"), ("p", "world")])
|
||||
assert response.status_code == 422
|
||||
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": [
|
||||
"body",
|
||||
"p_val_alias",
|
||||
],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/list-bytes-alias-and-validation-alias",
|
||||
"/list-uploadfile-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_list_alias_and_validation_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, files=[("p_alias", b"hello"), ("p_alias", b"world")])
|
||||
assert response.status_code == 422, response.text
|
||||
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "p_val_alias"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/list-bytes-alias-and-validation-alias",
|
||||
"/list-uploadfile-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_list_alias_and_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(
|
||||
path, files=[("p_val_alias", b"hello"), ("p_val_alias", b"world")]
|
||||
)
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"file_size": [5, 5]}
|
||||
408
tests/test_request_params/test_file/test_optional.py
Normal file
408
tests/test_request_params/test_file/test_optional.py
Normal file
@@ -0,0 +1,408 @@
|
||||
from typing import Optional
|
||||
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi import FastAPI, File, UploadFile
|
||||
from fastapi.testclient import TestClient
|
||||
from typing_extensions import Annotated
|
||||
|
||||
from tests.utils import needs_pydanticv2
|
||||
|
||||
from .utils import get_body_model_name
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# =====================================================================================
|
||||
# Without aliases
|
||||
|
||||
|
||||
@app.post("/optional-bytes", operation_id="optional_bytes")
|
||||
async def read_optional_bytes(p: Annotated[Optional[bytes], File()] = None):
|
||||
return {"file_size": len(p) if p else None}
|
||||
|
||||
|
||||
@app.post("/optional-uploadfile", operation_id="optional_uploadfile")
|
||||
async def read_optional_uploadfile(p: Annotated[Optional[UploadFile], File()] = None):
|
||||
return {"file_size": p.size if p else None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-bytes",
|
||||
"/optional-uploadfile",
|
||||
],
|
||||
)
|
||||
def test_optional_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == {
|
||||
"properties": {
|
||||
"p": (
|
||||
IsDict(
|
||||
{
|
||||
"anyOf": [
|
||||
{"type": "string", "format": "binary"},
|
||||
{"type": "null"},
|
||||
],
|
||||
"title": "P",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "P", "type": "string", "format": "binary"}
|
||||
)
|
||||
),
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-bytes",
|
||||
"/optional-uploadfile",
|
||||
],
|
||||
)
|
||||
def test_optional_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path)
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"file_size": None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-bytes",
|
||||
"/optional-uploadfile",
|
||||
],
|
||||
)
|
||||
def test_optional(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, files=[("p", b"hello")])
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"file_size": 5}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias
|
||||
|
||||
|
||||
@app.post("/optional-bytes-alias", operation_id="optional_bytes_alias")
|
||||
async def read_optional_bytes_alias(
|
||||
p: Annotated[Optional[bytes], File(alias="p_alias")] = None,
|
||||
):
|
||||
return {"file_size": len(p) if p else None}
|
||||
|
||||
|
||||
@app.post("/optional-uploadfile-alias", operation_id="optional_uploadfile_alias")
|
||||
async def read_optional_uploadfile_alias(
|
||||
p: Annotated[Optional[UploadFile], File(alias="p_alias")] = None,
|
||||
):
|
||||
return {"file_size": p.size if p else None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-bytes-alias",
|
||||
"/optional-uploadfile-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == {
|
||||
"properties": {
|
||||
"p_alias": (
|
||||
IsDict(
|
||||
{
|
||||
"anyOf": [
|
||||
{"type": "string", "format": "binary"},
|
||||
{"type": "null"},
|
||||
],
|
||||
"title": "P Alias",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "P Alias", "type": "string", "format": "binary"}
|
||||
)
|
||||
),
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-bytes-alias",
|
||||
"/optional-uploadfile-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"file_size": None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-bytes-alias",
|
||||
"/optional-uploadfile-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, files=[("p", b"hello")])
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"file_size": None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-bytes-alias",
|
||||
"/optional-uploadfile-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, files=[("p_alias", b"hello")])
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"file_size": 5}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Validation alias
|
||||
|
||||
|
||||
@app.post(
|
||||
"/optional-bytes-validation-alias", operation_id="optional_bytes_validation_alias"
|
||||
)
|
||||
def read_optional_bytes_validation_alias(
|
||||
p: Annotated[Optional[bytes], File(validation_alias="p_val_alias")] = None,
|
||||
):
|
||||
return {"file_size": len(p) if p else None}
|
||||
|
||||
|
||||
@app.post(
|
||||
"/optional-uploadfile-validation-alias",
|
||||
operation_id="optional_uploadfile_validation_alias",
|
||||
)
|
||||
def read_optional_uploadfile_validation_alias(
|
||||
p: Annotated[Optional[UploadFile], File(validation_alias="p_val_alias")] = None,
|
||||
):
|
||||
return {"file_size": p.size if p else None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-bytes-validation-alias",
|
||||
"/optional-uploadfile-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_validation_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == {
|
||||
"properties": {
|
||||
"p_val_alias": (
|
||||
IsDict(
|
||||
{
|
||||
"anyOf": [
|
||||
{"type": "string", "format": "binary"},
|
||||
{"type": "null"},
|
||||
],
|
||||
"title": "P Val Alias",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "P Val Alias", "type": "string", "format": "binary"}
|
||||
)
|
||||
),
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-bytes-validation-alias",
|
||||
"/optional-uploadfile-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"file_size": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-bytes-validation-alias",
|
||||
"/optional-uploadfile-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, files=[("p", b"hello")])
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"file_size": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-bytes-validation-alias",
|
||||
"/optional-uploadfile-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, files=[("p_val_alias", b"hello")])
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"file_size": 5}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias and validation alias
|
||||
|
||||
|
||||
@app.post(
|
||||
"/optional-bytes-alias-and-validation-alias",
|
||||
operation_id="optional_bytes_alias_and_validation_alias",
|
||||
)
|
||||
def read_optional_bytes_alias_and_validation_alias(
|
||||
p: Annotated[
|
||||
Optional[bytes], File(alias="p_alias", validation_alias="p_val_alias")
|
||||
] = None,
|
||||
):
|
||||
return {"file_size": len(p) if p else None}
|
||||
|
||||
|
||||
@app.post(
|
||||
"/optional-uploadfile-alias-and-validation-alias",
|
||||
operation_id="optional_uploadfile_alias_and_validation_alias",
|
||||
)
|
||||
def read_optional_uploadfile_alias_and_validation_alias(
|
||||
p: Annotated[
|
||||
Optional[UploadFile], File(alias="p_alias", validation_alias="p_val_alias")
|
||||
] = None,
|
||||
):
|
||||
return {"file_size": p.size if p else None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-bytes-alias-and-validation-alias",
|
||||
"/optional-uploadfile-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_alias_and_validation_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == {
|
||||
"properties": {
|
||||
"p_val_alias": (
|
||||
IsDict(
|
||||
{
|
||||
"anyOf": [
|
||||
{"type": "string", "format": "binary"},
|
||||
{"type": "null"},
|
||||
],
|
||||
"title": "P Val Alias",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "P Val Alias", "type": "string", "format": "binary"}
|
||||
)
|
||||
),
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-bytes-alias-and-validation-alias",
|
||||
"/optional-uploadfile-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_alias_and_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"file_size": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-bytes-alias-and-validation-alias",
|
||||
"/optional-uploadfile-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_alias_and_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, files={"p": "hello"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"file_size": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-bytes-alias-and-validation-alias",
|
||||
"/optional-uploadfile-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_alias_and_validation_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, files=[("p_alias", b"hello")])
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"file_size": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-bytes-alias-and-validation-alias",
|
||||
"/optional-uploadfile-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_alias_and_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, files=[("p_val_alias", b"hello")])
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"file_size": 5}
|
||||
434
tests/test_request_params/test_file/test_optional_list.py
Normal file
434
tests/test_request_params/test_file/test_optional_list.py
Normal file
@@ -0,0 +1,434 @@
|
||||
from typing import List, Optional
|
||||
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi import FastAPI, File, UploadFile
|
||||
from fastapi.testclient import TestClient
|
||||
from typing_extensions import Annotated
|
||||
|
||||
from tests.utils import needs_pydanticv2
|
||||
|
||||
from .utils import get_body_model_name
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# =====================================================================================
|
||||
# Without aliases
|
||||
|
||||
|
||||
@app.post("/optional-list-bytes")
|
||||
async def read_optional_list_bytes(p: Annotated[Optional[List[bytes]], File()] = None):
|
||||
return {"file_size": [len(file) for file in p] if p else None}
|
||||
|
||||
|
||||
@app.post("/optional-list-uploadfile")
|
||||
async def read_optional_list_uploadfile(
|
||||
p: Annotated[Optional[List[UploadFile]], File()] = None,
|
||||
):
|
||||
return {"file_size": [file.size for file in p] if p else None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-bytes",
|
||||
"/optional-list-uploadfile",
|
||||
],
|
||||
)
|
||||
def test_optional_list_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == {
|
||||
"properties": {
|
||||
"p": (
|
||||
IsDict(
|
||||
{
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "array",
|
||||
"items": {"type": "string", "format": "binary"},
|
||||
},
|
||||
{"type": "null"},
|
||||
],
|
||||
"title": "P",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"title": "P",
|
||||
"type": "array",
|
||||
"items": {"type": "string", "format": "binary"},
|
||||
},
|
||||
)
|
||||
),
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-bytes",
|
||||
"/optional-list-uploadfile",
|
||||
],
|
||||
)
|
||||
def test_optional_list_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path)
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"file_size": None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-bytes",
|
||||
"/optional-list-uploadfile",
|
||||
],
|
||||
)
|
||||
def test_optional_list(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, files=[("p", b"hello"), ("p", b"world")])
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"file_size": [5, 5]}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias
|
||||
|
||||
|
||||
@app.post("/optional-list-bytes-alias")
|
||||
async def read_optional_list_bytes_alias(
|
||||
p: Annotated[Optional[List[bytes]], File(alias="p_alias")] = None,
|
||||
):
|
||||
return {"file_size": [len(file) for file in p] if p else None}
|
||||
|
||||
|
||||
@app.post("/optional-list-uploadfile-alias")
|
||||
async def read_optional_list_uploadfile_alias(
|
||||
p: Annotated[Optional[List[UploadFile]], File(alias="p_alias")] = None,
|
||||
):
|
||||
return {"file_size": [file.size for file in p] if p else None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-bytes-alias",
|
||||
"/optional-list-uploadfile-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == {
|
||||
"properties": {
|
||||
"p_alias": (
|
||||
IsDict(
|
||||
{
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "array",
|
||||
"items": {"type": "string", "format": "binary"},
|
||||
},
|
||||
{"type": "null"},
|
||||
],
|
||||
"title": "P Alias",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"title": "P Alias",
|
||||
"type": "array",
|
||||
"items": {"type": "string", "format": "binary"},
|
||||
}
|
||||
)
|
||||
),
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-bytes-alias",
|
||||
"/optional-list-uploadfile-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"file_size": None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-bytes-alias",
|
||||
"/optional-list-uploadfile-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, files=[("p", b"hello"), ("p", b"world")])
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"file_size": None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-bytes-alias",
|
||||
"/optional-list-uploadfile-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, files=[("p_alias", b"hello"), ("p_alias", b"world")])
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"file_size": [5, 5]}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Validation alias
|
||||
|
||||
|
||||
@app.post("/optional-list-bytes-validation-alias")
|
||||
def read_optional_list_bytes_validation_alias(
|
||||
p: Annotated[Optional[List[bytes]], File(validation_alias="p_val_alias")] = None,
|
||||
):
|
||||
return {"file_size": [len(file) for file in p] if p else None}
|
||||
|
||||
|
||||
@app.post("/optional-list-uploadfile-validation-alias")
|
||||
def read_optional_list_uploadfile_validation_alias(
|
||||
p: Annotated[
|
||||
Optional[List[UploadFile]], File(validation_alias="p_val_alias")
|
||||
] = None,
|
||||
):
|
||||
return {"file_size": [file.size for file in p] if p else None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-bytes-validation-alias",
|
||||
"/optional-list-uploadfile-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_validation_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == {
|
||||
"properties": {
|
||||
"p_val_alias": (
|
||||
IsDict(
|
||||
{
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "array",
|
||||
"items": {"type": "string", "format": "binary"},
|
||||
},
|
||||
{"type": "null"},
|
||||
],
|
||||
"title": "P Val Alias",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"title": "P Val Alias",
|
||||
"type": "array",
|
||||
"items": {"type": "string", "format": "binary"},
|
||||
}
|
||||
)
|
||||
),
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-bytes-validation-alias",
|
||||
"/optional-list-uploadfile-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"file_size": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-bytes-validation-alias",
|
||||
"/optional-list-uploadfile-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, files=[("p", b"hello"), ("p", b"world")])
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"file_size": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-bytes-validation-alias",
|
||||
"/optional-list-uploadfile-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(
|
||||
path, files=[("p_val_alias", b"hello"), ("p_val_alias", b"world")]
|
||||
)
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"file_size": [5, 5]}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias and validation alias
|
||||
|
||||
|
||||
@app.post("/optional-list-bytes-alias-and-validation-alias")
|
||||
def read_optional_list_bytes_alias_and_validation_alias(
|
||||
p: Annotated[
|
||||
Optional[List[bytes]], File(alias="p_alias", validation_alias="p_val_alias")
|
||||
] = None,
|
||||
):
|
||||
return {"file_size": [len(file) for file in p] if p else None}
|
||||
|
||||
|
||||
@app.post("/optional-list-uploadfile-alias-and-validation-alias")
|
||||
def read_optional_list_uploadfile_alias_and_validation_alias(
|
||||
p: Annotated[
|
||||
Optional[List[UploadFile]],
|
||||
File(alias="p_alias", validation_alias="p_val_alias"),
|
||||
] = None,
|
||||
):
|
||||
return {"file_size": [file.size for file in p] if p else None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-bytes-alias-and-validation-alias",
|
||||
"/optional-list-uploadfile-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_alias_and_validation_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == {
|
||||
"properties": {
|
||||
"p_val_alias": (
|
||||
IsDict(
|
||||
{
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "array",
|
||||
"items": {"type": "string", "format": "binary"},
|
||||
},
|
||||
{"type": "null"},
|
||||
],
|
||||
"title": "P Val Alias",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"title": "P Val Alias",
|
||||
"type": "array",
|
||||
"items": {"type": "string", "format": "binary"},
|
||||
}
|
||||
)
|
||||
),
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-bytes-alias-and-validation-alias",
|
||||
"/optional-list-uploadfile-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_alias_and_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"file_size": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-bytes-alias-and-validation-alias",
|
||||
"/optional-list-uploadfile-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_alias_and_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, files={"p": "hello"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"file_size": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-bytes-alias-and-validation-alias",
|
||||
"/optional-list-uploadfile-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_alias_and_validation_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, files=[("p_alias", b"hello"), ("p_alias", b"world")])
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"file_size": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-bytes-alias-and-validation-alias",
|
||||
"/optional-list-uploadfile-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_alias_and_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(
|
||||
path, files=[("p_val_alias", b"hello"), ("p_val_alias", b"world")]
|
||||
)
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"file_size": [5, 5]}
|
||||
479
tests/test_request_params/test_file/test_required.py
Normal file
479
tests/test_request_params/test_file/test_required.py
Normal file
@@ -0,0 +1,479 @@
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi import FastAPI, File, UploadFile
|
||||
from fastapi.testclient import TestClient
|
||||
from typing_extensions import Annotated
|
||||
|
||||
from tests.utils import needs_pydanticv2
|
||||
|
||||
from .utils import get_body_model_name
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# =====================================================================================
|
||||
# Without aliases
|
||||
|
||||
|
||||
@app.post("/required-bytes", operation_id="required_bytes")
|
||||
async def read_required_bytes(p: Annotated[bytes, File()]):
|
||||
return {"file_size": len(p)}
|
||||
|
||||
|
||||
@app.post("/required-uploadfile", operation_id="required_uploadfile")
|
||||
async def read_required_uploadfile(p: Annotated[UploadFile, File()]):
|
||||
return {"file_size": p.size}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-bytes",
|
||||
"/required-uploadfile",
|
||||
],
|
||||
)
|
||||
def test_required_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == {
|
||||
"properties": {
|
||||
"p": {"title": "P", "type": "string", "format": "binary"},
|
||||
},
|
||||
"required": ["p"],
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-bytes",
|
||||
"/required-uploadfile",
|
||||
],
|
||||
)
|
||||
def test_required_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "p"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "p"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-bytes",
|
||||
"/required-uploadfile",
|
||||
],
|
||||
)
|
||||
def test_required(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, files=[("p", b"hello")])
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"file_size": 5}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias
|
||||
|
||||
|
||||
@app.post("/required-bytes-alias", operation_id="required_bytes_alias")
|
||||
async def read_required_bytes_alias(p: Annotated[bytes, File(alias="p_alias")]):
|
||||
return {"file_size": len(p)}
|
||||
|
||||
|
||||
@app.post("/required-uploadfile-alias", operation_id="required_uploadfile_alias")
|
||||
async def read_required_uploadfile_alias(
|
||||
p: Annotated[UploadFile, File(alias="p_alias")],
|
||||
):
|
||||
return {"file_size": p.size}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-bytes-alias",
|
||||
"/required-uploadfile-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == {
|
||||
"properties": {
|
||||
"p_alias": {"title": "P Alias", "type": "string", "format": "binary"},
|
||||
},
|
||||
"required": ["p_alias"],
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-bytes-alias",
|
||||
"/required-uploadfile-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "p_alias"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "p_alias"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-bytes-alias",
|
||||
"/required-uploadfile-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, files=[("p", b"hello")])
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "p_alias"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "p_alias"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-bytes-alias",
|
||||
"/required-uploadfile-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, files=[("p_alias", b"hello")])
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"file_size": 5}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Validation alias
|
||||
|
||||
|
||||
@app.post(
|
||||
"/required-bytes-validation-alias", operation_id="required_bytes_validation_alias"
|
||||
)
|
||||
def read_required_bytes_validation_alias(
|
||||
p: Annotated[bytes, File(validation_alias="p_val_alias")],
|
||||
):
|
||||
return {"file_size": len(p)}
|
||||
|
||||
|
||||
@app.post(
|
||||
"/required-uploadfile-validation-alias",
|
||||
operation_id="required_uploadfile_validation_alias",
|
||||
)
|
||||
def read_required_uploadfile_validation_alias(
|
||||
p: Annotated[UploadFile, File(validation_alias="p_val_alias")],
|
||||
):
|
||||
return {"file_size": p.size}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-bytes-validation-alias",
|
||||
"/required-uploadfile-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_validation_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == {
|
||||
"properties": {
|
||||
"p_val_alias": {
|
||||
"title": "P Val Alias",
|
||||
"type": "string",
|
||||
"format": "binary",
|
||||
},
|
||||
},
|
||||
"required": ["p_val_alias"],
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-bytes-validation-alias",
|
||||
"/required-uploadfile-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": [
|
||||
"body",
|
||||
"p_val_alias",
|
||||
],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-bytes-validation-alias",
|
||||
"/required-uploadfile-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, files=[("p", b"hello")])
|
||||
assert response.status_code == 422, response.text
|
||||
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "p_val_alias"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-bytes-validation-alias",
|
||||
"/required-uploadfile-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, files=[("p_val_alias", b"hello")])
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"file_size": 5}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias and validation alias
|
||||
|
||||
|
||||
@app.post(
|
||||
"/required-bytes-alias-and-validation-alias",
|
||||
operation_id="required_bytes_alias_and_validation_alias",
|
||||
)
|
||||
def read_required_bytes_alias_and_validation_alias(
|
||||
p: Annotated[bytes, File(alias="p_alias", validation_alias="p_val_alias")],
|
||||
):
|
||||
return {"file_size": len(p)}
|
||||
|
||||
|
||||
@app.post(
|
||||
"/required-uploadfile-alias-and-validation-alias",
|
||||
operation_id="required_uploadfile_alias_and_validation_alias",
|
||||
)
|
||||
def read_required_uploadfile_alias_and_validation_alias(
|
||||
p: Annotated[UploadFile, File(alias="p_alias", validation_alias="p_val_alias")],
|
||||
):
|
||||
return {"file_size": p.size}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-bytes-alias-and-validation-alias",
|
||||
"/required-uploadfile-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_and_validation_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == {
|
||||
"properties": {
|
||||
"p_val_alias": {
|
||||
"title": "P Val Alias",
|
||||
"type": "string",
|
||||
"format": "binary",
|
||||
},
|
||||
},
|
||||
"required": ["p_val_alias"],
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-bytes-alias-and-validation-alias",
|
||||
"/required-uploadfile-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_and_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": [
|
||||
"body",
|
||||
"p_val_alias",
|
||||
],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-bytes-alias-and-validation-alias",
|
||||
"/required-uploadfile-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_and_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, files={"p": "hello"})
|
||||
assert response.status_code == 422
|
||||
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": [
|
||||
"body",
|
||||
"p_val_alias",
|
||||
],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-bytes-alias-and-validation-alias",
|
||||
"/required-uploadfile-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_and_validation_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, files=[("p_alias", b"hello")])
|
||||
assert response.status_code == 422, response.text
|
||||
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "p_val_alias"],
|
||||
"msg": "Field required",
|
||||
"input": None,
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-bytes-alias-and-validation-alias",
|
||||
"/required-uploadfile-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_and_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, files=[("p_val_alias", b"hello")])
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"file_size": 5}
|
||||
7
tests/test_request_params/test_file/utils.py
Normal file
7
tests/test_request_params/test_file/utils.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from typing import Any, Dict
|
||||
|
||||
|
||||
def get_body_model_name(openapi: Dict[str, Any], path: str) -> str:
|
||||
body = openapi["paths"][path]["post"]["requestBody"]
|
||||
body_schema = body["content"]["multipart/form-data"]["schema"]
|
||||
return body_schema.get("$ref", "").split("/")[-1]
|
||||
0
tests/test_request_params/test_form/__init__.py
Normal file
0
tests/test_request_params/test_form/__init__.py
Normal file
486
tests/test_request_params/test_form/test_list.py
Normal file
486
tests/test_request_params/test_form/test_list.py
Normal file
@@ -0,0 +1,486 @@
|
||||
from typing import List
|
||||
|
||||
import pytest
|
||||
from dirty_equals import IsDict, IsOneOf, IsPartialDict
|
||||
from fastapi import FastAPI, Form
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel, Field
|
||||
from typing_extensions import Annotated
|
||||
|
||||
from tests.utils import needs_pydanticv2
|
||||
|
||||
from .utils import get_body_model_name
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# =====================================================================================
|
||||
# Without aliases
|
||||
|
||||
|
||||
@app.post("/required-list-str", operation_id="required_list_str")
|
||||
async def read_required_list_str(p: Annotated[List[str], Form()]):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class FormModelRequiredListStr(BaseModel):
|
||||
p: List[str]
|
||||
|
||||
|
||||
@app.post("/model-required-list-str", operation_id="model_required_list_str")
|
||||
def read_model_required_list_str(p: Annotated[FormModelRequiredListStr, Form()]):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-list-str", "/model-required-list-str"],
|
||||
)
|
||||
def test_required_list_str_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == {
|
||||
"properties": {
|
||||
"p": {
|
||||
"items": {"type": "string"},
|
||||
"title": "P",
|
||||
"type": "array",
|
||||
},
|
||||
},
|
||||
"required": ["p"],
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-list-str", "/model-required-list-str"],
|
||||
)
|
||||
def test_required_list_str_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "p"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {}),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "p"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-list-str", "/model-required-list-str"],
|
||||
)
|
||||
def test_required_list_str(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, data={"p": ["hello", "world"]})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": ["hello", "world"]}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias
|
||||
|
||||
|
||||
@app.post("/required-list-alias", operation_id="required_list_alias")
|
||||
async def read_required_list_alias(p: Annotated[List[str], Form(alias="p_alias")]):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class FormModelRequiredListAlias(BaseModel):
|
||||
p: List[str] = Field(alias="p_alias")
|
||||
|
||||
|
||||
@app.post("/model-required-list-alias", operation_id="model_required_list_alias")
|
||||
async def read_model_required_list_alias(
|
||||
p: Annotated[FormModelRequiredListAlias, Form()],
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-list-alias",
|
||||
"/model-required-list-alias",
|
||||
],
|
||||
)
|
||||
def test_required_list_str_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == {
|
||||
"properties": {
|
||||
"p_alias": {
|
||||
"items": {"type": "string"},
|
||||
"title": "P Alias",
|
||||
"type": "array",
|
||||
},
|
||||
},
|
||||
"required": ["p_alias"],
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-list-alias", "/model-required-list-alias"],
|
||||
)
|
||||
def test_required_list_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "p_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {}),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "p_alias"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-list-alias",
|
||||
"/model-required-list-alias",
|
||||
],
|
||||
)
|
||||
def test_required_list_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, data={"p": ["hello", "world"]})
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "p_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {"p": ["hello", "world"]}),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "p_alias"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-list-alias", "/model-required-list-alias"],
|
||||
)
|
||||
def test_required_list_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, data={"p_alias": ["hello", "world"]})
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"p": ["hello", "world"]}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Validation alias
|
||||
|
||||
|
||||
@app.post(
|
||||
"/required-list-validation-alias", operation_id="required_list_validation_alias"
|
||||
)
|
||||
def read_required_list_validation_alias(
|
||||
p: Annotated[List[str], Form(validation_alias="p_val_alias")],
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class FormModelRequiredListValidationAlias(BaseModel):
|
||||
p: List[str] = Field(validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.post(
|
||||
"/model-required-list-validation-alias",
|
||||
operation_id="model_required_list_validation_alias",
|
||||
)
|
||||
async def read_model_required_list_validation_alias(
|
||||
p: Annotated[FormModelRequiredListValidationAlias, Form()],
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-list-validation-alias", "/model-required-list-validation-alias"],
|
||||
)
|
||||
def test_required_list_validation_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == {
|
||||
"properties": {
|
||||
"p_val_alias": {
|
||||
"items": {"type": "string"},
|
||||
"title": "P Val Alias",
|
||||
"type": "array",
|
||||
},
|
||||
},
|
||||
"required": ["p_val_alias"],
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-list-validation-alias",
|
||||
"/model-required-list-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_list_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": [
|
||||
"body",
|
||||
"p_val_alias",
|
||||
],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {}),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-list-validation-alias",
|
||||
"/model-required-list-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_list_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, data={"p": ["hello", "world"]})
|
||||
assert response.status_code == 422, response.text
|
||||
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "p_val_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, IsPartialDict({"p": ["hello", "world"]})),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-list-validation-alias", "/model-required-list-validation-alias"],
|
||||
)
|
||||
def test_required_list_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, data={"p_val_alias": ["hello", "world"]})
|
||||
assert response.status_code == 200, response.text
|
||||
|
||||
assert response.json() == {"p": ["hello", "world"]}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias and validation alias
|
||||
|
||||
|
||||
@app.post(
|
||||
"/required-list-alias-and-validation-alias",
|
||||
operation_id="required_list_alias_and_validation_alias",
|
||||
)
|
||||
def read_required_list_alias_and_validation_alias(
|
||||
p: Annotated[List[str], Form(alias="p_alias", validation_alias="p_val_alias")],
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class FormModelRequiredListAliasAndValidationAlias(BaseModel):
|
||||
p: List[str] = Field(alias="p_alias", validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.post(
|
||||
"/model-required-list-alias-and-validation-alias",
|
||||
operation_id="model_required_list_alias_and_validation_alias",
|
||||
)
|
||||
def read_model_required_list_alias_and_validation_alias(
|
||||
p: Annotated[FormModelRequiredListAliasAndValidationAlias, Form()],
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-list-alias-and-validation-alias",
|
||||
"/model-required-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_list_alias_and_validation_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == {
|
||||
"properties": {
|
||||
"p_val_alias": {
|
||||
"items": {"type": "string"},
|
||||
"title": "P Val Alias",
|
||||
"type": "array",
|
||||
},
|
||||
},
|
||||
"required": ["p_val_alias"],
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-list-alias-and-validation-alias",
|
||||
"/model-required-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_list_alias_and_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": [
|
||||
"body",
|
||||
"p_val_alias",
|
||||
],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {}),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-list-alias-and-validation-alias",
|
||||
"/model-required-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_list_alias_and_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, data={"p": ["hello", "world"]})
|
||||
assert response.status_code == 422
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": [
|
||||
"body",
|
||||
"p_val_alias",
|
||||
],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(
|
||||
None,
|
||||
{"p": ["hello", "world"]},
|
||||
),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-list-alias-and-validation-alias",
|
||||
"/model-required-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_list_alias_and_validation_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, data={"p_alias": ["hello", "world"]})
|
||||
assert response.status_code == 422
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "p_val_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {"p_alias": ["hello", "world"]}),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-list-alias-and-validation-alias",
|
||||
"/model-required-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_list_alias_and_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, data={"p_val_alias": ["hello", "world"]})
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"p": ["hello", "world"]}
|
||||
429
tests/test_request_params/test_form/test_optional_list.py
Normal file
429
tests/test_request_params/test_form/test_optional_list.py
Normal file
@@ -0,0 +1,429 @@
|
||||
from typing import List, Optional
|
||||
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi import FastAPI, Form
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel, Field
|
||||
from typing_extensions import Annotated
|
||||
|
||||
from tests.utils import needs_pydanticv2
|
||||
|
||||
from .utils import get_body_model_name
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# =====================================================================================
|
||||
# Without aliases
|
||||
|
||||
|
||||
@app.post("/optional-list-str", operation_id="optional_list_str")
|
||||
async def read_optional_list_str(
|
||||
p: Annotated[Optional[List[str]], Form()] = None,
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class FormModelOptionalListStr(BaseModel):
|
||||
p: Optional[List[str]] = None
|
||||
|
||||
|
||||
@app.post("/model-optional-list-str", operation_id="model_optional_list_str")
|
||||
async def read_model_optional_list_str(p: Annotated[FormModelOptionalListStr, Form()]):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-str", "/model-optional-list-str"],
|
||||
)
|
||||
def test_optional_list_str_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
|
||||
{
|
||||
"properties": {
|
||||
"p": {
|
||||
"anyOf": [
|
||||
{"items": {"type": "string"}, "type": "array"},
|
||||
{"type": "null"},
|
||||
],
|
||||
"title": "P",
|
||||
},
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"properties": {
|
||||
"p": {"items": {"type": "string"}, "type": "array", "title": "P"},
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-str", "/model-optional-list-str"],
|
||||
)
|
||||
def test_optional_list_str_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path)
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-str", "/model-optional-list-str"],
|
||||
)
|
||||
def test_optional_list_str(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, data={"p": ["hello", "world"]})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": ["hello", "world"]}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias
|
||||
|
||||
|
||||
@app.post("/optional-list-alias", operation_id="optional_list_alias")
|
||||
async def read_optional_list_alias(
|
||||
p: Annotated[Optional[List[str]], Form(alias="p_alias")] = None,
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class FormModelOptionalListAlias(BaseModel):
|
||||
p: Optional[List[str]] = Field(None, alias="p_alias")
|
||||
|
||||
|
||||
@app.post("/model-optional-list-alias", operation_id="model_optional_list_alias")
|
||||
async def read_model_optional_list_alias(
|
||||
p: Annotated[FormModelOptionalListAlias, Form()],
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-alias",
|
||||
"/model-optional-list-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_str_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
|
||||
{
|
||||
"properties": {
|
||||
"p_alias": {
|
||||
"anyOf": [
|
||||
{"items": {"type": "string"}, "type": "array"},
|
||||
{"type": "null"},
|
||||
],
|
||||
"title": "P Alias",
|
||||
},
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"properties": {
|
||||
"p_alias": {
|
||||
"items": {"type": "string"},
|
||||
"type": "array",
|
||||
"title": "P Alias",
|
||||
},
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-alias", "/model-optional-list-alias"],
|
||||
)
|
||||
def test_optional_list_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-alias", "/model-optional-list-alias"],
|
||||
)
|
||||
def test_optional_list_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, data={"p": ["hello", "world"]})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-alias", "/model-optional-list-alias"],
|
||||
)
|
||||
def test_optional_list_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, data={"p_alias": ["hello", "world"]})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": ["hello", "world"]}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Validation alias
|
||||
|
||||
|
||||
@app.post(
|
||||
"/optional-list-validation-alias", operation_id="optional_list_validation_alias"
|
||||
)
|
||||
def read_optional_list_validation_alias(
|
||||
p: Annotated[Optional[List[str]], Form(validation_alias="p_val_alias")] = None,
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class FormModelOptionalListValidationAlias(BaseModel):
|
||||
p: Optional[List[str]] = Field(None, validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.post(
|
||||
"/model-optional-list-validation-alias",
|
||||
operation_id="model_optional_list_validation_alias",
|
||||
)
|
||||
def read_model_optional_list_validation_alias(
|
||||
p: Annotated[FormModelOptionalListValidationAlias, Form()],
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-validation-alias", "/model-optional-list-validation-alias"],
|
||||
)
|
||||
def test_optional_list_validation_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
|
||||
{
|
||||
"properties": {
|
||||
"p_val_alias": {
|
||||
"anyOf": [
|
||||
{"items": {"type": "string"}, "type": "array"},
|
||||
{"type": "null"},
|
||||
],
|
||||
"title": "P Val Alias",
|
||||
},
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"properties": {
|
||||
"p_val_alias": {
|
||||
"items": {"type": "string"},
|
||||
"type": "array",
|
||||
"title": "P Val Alias",
|
||||
},
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-validation-alias", "/model-optional-list-validation-alias"],
|
||||
)
|
||||
def test_optional_list_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-validation-alias",
|
||||
"/model-optional-list-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, data={"p": ["hello", "world"]})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-validation-alias", "/model-optional-list-validation-alias"],
|
||||
)
|
||||
def test_optional_list_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, data={"p_val_alias": ["hello", "world"]})
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"p": ["hello", "world"]}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias and validation alias
|
||||
|
||||
|
||||
@app.post(
|
||||
"/optional-list-alias-and-validation-alias",
|
||||
operation_id="optional_list_alias_and_validation_alias",
|
||||
)
|
||||
def read_optional_list_alias_and_validation_alias(
|
||||
p: Annotated[
|
||||
Optional[List[str]], Form(alias="p_alias", validation_alias="p_val_alias")
|
||||
] = None,
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class FormModelOptionalListAliasAndValidationAlias(BaseModel):
|
||||
p: Optional[List[str]] = Field(
|
||||
None, alias="p_alias", validation_alias="p_val_alias"
|
||||
)
|
||||
|
||||
|
||||
@app.post(
|
||||
"/model-optional-list-alias-and-validation-alias",
|
||||
operation_id="model_optional_list_alias_and_validation_alias",
|
||||
)
|
||||
def read_model_optional_list_alias_and_validation_alias(
|
||||
p: Annotated[FormModelOptionalListAliasAndValidationAlias, Form()],
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-alias-and-validation-alias",
|
||||
"/model-optional-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_alias_and_validation_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
|
||||
{
|
||||
"properties": {
|
||||
"p_val_alias": {
|
||||
"anyOf": [
|
||||
{"items": {"type": "string"}, "type": "array"},
|
||||
{"type": "null"},
|
||||
],
|
||||
"title": "P Val Alias",
|
||||
},
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"properties": {
|
||||
"p_val_alias": {
|
||||
"items": {"type": "string"},
|
||||
"type": "array",
|
||||
"title": "P Val Alias",
|
||||
},
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-alias-and-validation-alias",
|
||||
"/model-optional-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_alias_and_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-alias-and-validation-alias",
|
||||
"/model-optional-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_alias_and_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, data={"p": ["hello", "world"]})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-alias-and-validation-alias",
|
||||
"/model-optional-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_alias_and_validation_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, data={"p_alias": ["hello", "world"]})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-alias-and-validation-alias",
|
||||
"/model-optional-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_alias_and_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, data={"p_val_alias": ["hello", "world"]})
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {
|
||||
"p": [
|
||||
"hello",
|
||||
"world",
|
||||
]
|
||||
}
|
||||
394
tests/test_request_params/test_form/test_optional_str.py
Normal file
394
tests/test_request_params/test_form/test_optional_str.py
Normal file
@@ -0,0 +1,394 @@
|
||||
from typing import Optional
|
||||
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi import FastAPI, Form
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel, Field
|
||||
from typing_extensions import Annotated
|
||||
|
||||
from tests.utils import needs_pydanticv2
|
||||
|
||||
from .utils import get_body_model_name
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# =====================================================================================
|
||||
# Without aliases
|
||||
|
||||
|
||||
@app.post("/optional-str", operation_id="optional_str")
|
||||
async def read_optional_str(p: Annotated[Optional[str], Form()] = None):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class FormModelOptionalStr(BaseModel):
|
||||
p: Optional[str] = None
|
||||
|
||||
|
||||
@app.post("/model-optional-str", operation_id="model_optional_str")
|
||||
async def read_model_optional_str(p: Annotated[FormModelOptionalStr, Form()]):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-str", "/model-optional-str"],
|
||||
)
|
||||
def test_optional_str_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
|
||||
{
|
||||
"properties": {
|
||||
"p": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "P",
|
||||
},
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"properties": {
|
||||
"p": {"type": "string", "title": "P"},
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-str", "/model-optional-str"],
|
||||
)
|
||||
def test_optional_str_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-str", "/model-optional-str"],
|
||||
)
|
||||
def test_optional_str(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, data={"p": "hello"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": "hello"}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias
|
||||
|
||||
|
||||
@app.post("/optional-alias", operation_id="optional_alias")
|
||||
async def read_optional_alias(
|
||||
p: Annotated[Optional[str], Form(alias="p_alias")] = None,
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class FormModelOptionalAlias(BaseModel):
|
||||
p: Optional[str] = Field(None, alias="p_alias")
|
||||
|
||||
|
||||
@app.post("/model-optional-alias", operation_id="model_optional_alias")
|
||||
async def read_model_optional_alias(p: Annotated[FormModelOptionalAlias, Form()]):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-alias",
|
||||
"/model-optional-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_str_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
|
||||
{
|
||||
"properties": {
|
||||
"p_alias": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "P Alias",
|
||||
},
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"properties": {
|
||||
"p_alias": {"type": "string", "title": "P Alias"},
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-alias", "/model-optional-alias"],
|
||||
)
|
||||
def test_optional_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-alias", "/model-optional-alias"],
|
||||
)
|
||||
def test_optional_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, data={"p": "hello"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-alias", "/model-optional-alias"],
|
||||
)
|
||||
def test_optional_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, data={"p_alias": "hello"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": "hello"}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Validation alias
|
||||
|
||||
|
||||
@app.post("/optional-validation-alias", operation_id="optional_validation_alias")
|
||||
def read_optional_validation_alias(
|
||||
p: Annotated[Optional[str], Form(validation_alias="p_val_alias")] = None,
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class FormModelOptionalValidationAlias(BaseModel):
|
||||
p: Optional[str] = Field(None, validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.post(
|
||||
"/model-optional-validation-alias", operation_id="model_optional_validation_alias"
|
||||
)
|
||||
def read_model_optional_validation_alias(
|
||||
p: Annotated[FormModelOptionalValidationAlias, Form()],
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-validation-alias", "/model-optional-validation-alias"],
|
||||
)
|
||||
def test_optional_validation_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
|
||||
{
|
||||
"properties": {
|
||||
"p_val_alias": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "P Val Alias",
|
||||
},
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"properties": {
|
||||
"p_val_alias": {"type": "string", "title": "P Val Alias"},
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-validation-alias", "/model-optional-validation-alias"],
|
||||
)
|
||||
def test_optional_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-validation-alias",
|
||||
"/model-optional-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, data={"p": "hello"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-validation-alias",
|
||||
"/model-optional-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, data={"p_val_alias": "hello"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": "hello"}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias and validation alias
|
||||
|
||||
|
||||
@app.post(
|
||||
"/optional-alias-and-validation-alias",
|
||||
operation_id="optional_alias_and_validation_alias",
|
||||
)
|
||||
def read_optional_alias_and_validation_alias(
|
||||
p: Annotated[
|
||||
Optional[str], Form(alias="p_alias", validation_alias="p_val_alias")
|
||||
] = None,
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class FormModelOptionalAliasAndValidationAlias(BaseModel):
|
||||
p: Optional[str] = Field(None, alias="p_alias", validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.post(
|
||||
"/model-optional-alias-and-validation-alias",
|
||||
operation_id="model_optional_alias_and_validation_alias",
|
||||
)
|
||||
def read_model_optional_alias_and_validation_alias(
|
||||
p: Annotated[FormModelOptionalAliasAndValidationAlias, Form()],
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-alias-and-validation-alias",
|
||||
"/model-optional-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_alias_and_validation_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
|
||||
{
|
||||
"properties": {
|
||||
"p_val_alias": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "P Val Alias",
|
||||
},
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"properties": {
|
||||
"p_val_alias": {"type": "string", "title": "P Val Alias"},
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-alias-and-validation-alias",
|
||||
"/model-optional-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_alias_and_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-alias-and-validation-alias",
|
||||
"/model-optional-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_alias_and_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, data={"p": "hello"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-alias-and-validation-alias",
|
||||
"/model-optional-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_alias_and_validation_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, data={"p_alias": "hello"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-alias-and-validation-alias",
|
||||
"/model-optional-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_alias_and_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, data={"p_val_alias": "hello"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": "hello"}
|
||||
464
tests/test_request_params/test_form/test_required_str.py
Normal file
464
tests/test_request_params/test_form/test_required_str.py
Normal file
@@ -0,0 +1,464 @@
|
||||
import pytest
|
||||
from dirty_equals import IsDict, IsOneOf
|
||||
from fastapi import FastAPI, Form
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel, Field
|
||||
from typing_extensions import Annotated
|
||||
|
||||
from tests.utils import needs_pydanticv2
|
||||
|
||||
from .utils import get_body_model_name
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# =====================================================================================
|
||||
# Without aliases
|
||||
|
||||
|
||||
@app.post("/required-str", operation_id="required_str")
|
||||
async def read_required_str(p: Annotated[str, Form()]):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class FormModelRequiredStr(BaseModel):
|
||||
p: str
|
||||
|
||||
|
||||
@app.post("/model-required-str", operation_id="model_required_str")
|
||||
async def read_model_required_str(p: Annotated[FormModelRequiredStr, Form()]):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-str", "/model-required-str"],
|
||||
)
|
||||
def test_required_str_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == {
|
||||
"properties": {
|
||||
"p": {"title": "P", "type": "string"},
|
||||
},
|
||||
"required": ["p"],
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-str", "/model-required-str"],
|
||||
)
|
||||
def test_required_str_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "p"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {}),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "p"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-str", "/model-required-str"],
|
||||
)
|
||||
def test_required_str(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, data={"p": "hello"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": "hello"}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias
|
||||
|
||||
|
||||
@app.post("/required-alias", operation_id="required_alias")
|
||||
async def read_required_alias(p: Annotated[str, Form(alias="p_alias")]):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class FormModelRequiredAlias(BaseModel):
|
||||
p: str = Field(alias="p_alias")
|
||||
|
||||
|
||||
@app.post("/model-required-alias", operation_id="model_required_alias")
|
||||
async def read_model_required_alias(p: Annotated[FormModelRequiredAlias, Form()]):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-alias",
|
||||
"/model-required-alias",
|
||||
],
|
||||
)
|
||||
def test_required_str_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == {
|
||||
"properties": {
|
||||
"p_alias": {"title": "P Alias", "type": "string"},
|
||||
},
|
||||
"required": ["p_alias"],
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-alias", "/model-required-alias"],
|
||||
)
|
||||
def test_required_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "p_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {}),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "p_alias"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-alias", "/model-required-alias"],
|
||||
)
|
||||
def test_required_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, data={"p": "hello"})
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "p_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {"p": "hello"}),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "p_alias"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-alias", "/model-required-alias"],
|
||||
)
|
||||
def test_required_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, data={"p_alias": "hello"})
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"p": "hello"}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Validation alias
|
||||
|
||||
|
||||
@app.post("/required-validation-alias", operation_id="required_validation_alias")
|
||||
def read_required_validation_alias(
|
||||
p: Annotated[str, Form(validation_alias="p_val_alias")],
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class FormModelRequiredValidationAlias(BaseModel):
|
||||
p: str = Field(validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.post(
|
||||
"/model-required-validation-alias", operation_id="model_required_validation_alias"
|
||||
)
|
||||
def read_model_required_validation_alias(
|
||||
p: Annotated[FormModelRequiredValidationAlias, Form()],
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-validation-alias", "/model-required-validation-alias"],
|
||||
)
|
||||
def test_required_validation_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == {
|
||||
"properties": {
|
||||
"p_val_alias": {"title": "P Val Alias", "type": "string"},
|
||||
},
|
||||
"required": ["p_val_alias"],
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-validation-alias",
|
||||
"/model-required-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": [
|
||||
"body",
|
||||
"p_val_alias",
|
||||
],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {}),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-validation-alias",
|
||||
"/model-required-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, data={"p": "hello"})
|
||||
assert response.status_code == 422, response.text
|
||||
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "p_val_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {"p": "hello"}),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-validation-alias",
|
||||
"/model-required-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, data={"p_val_alias": "hello"})
|
||||
assert response.status_code == 200, response.text
|
||||
|
||||
assert response.json() == {"p": "hello"}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias and validation alias
|
||||
|
||||
|
||||
@app.post(
|
||||
"/required-alias-and-validation-alias",
|
||||
operation_id="required_alias_and_validation_alias",
|
||||
)
|
||||
def read_required_alias_and_validation_alias(
|
||||
p: Annotated[str, Form(alias="p_alias", validation_alias="p_val_alias")],
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class FormModelRequiredAliasAndValidationAlias(BaseModel):
|
||||
p: str = Field(alias="p_alias", validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.post(
|
||||
"/model-required-alias-and-validation-alias",
|
||||
operation_id="model_required_alias_and_validation_alias",
|
||||
)
|
||||
def read_model_required_alias_and_validation_alias(
|
||||
p: Annotated[FormModelRequiredAliasAndValidationAlias, Form()],
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-alias-and-validation-alias",
|
||||
"/model-required-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_and_validation_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == {
|
||||
"properties": {
|
||||
"p_val_alias": {"title": "P Val Alias", "type": "string"},
|
||||
},
|
||||
"required": ["p_val_alias"],
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-alias-and-validation-alias",
|
||||
"/model-required-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_and_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": [
|
||||
"body",
|
||||
"p_val_alias",
|
||||
],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {}),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-alias-and-validation-alias",
|
||||
"/model-required-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_and_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, data={"p": "hello"})
|
||||
assert response.status_code == 422
|
||||
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": [
|
||||
"body",
|
||||
"p_val_alias",
|
||||
],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {"p": "hello"}),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-alias-and-validation-alias",
|
||||
"/model-required-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_and_validation_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, data={"p_alias": "hello"})
|
||||
assert response.status_code == 422, response.text
|
||||
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "p_val_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {"p_alias": "hello"}),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-alias-and-validation-alias",
|
||||
"/model-required-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_and_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, data={"p_val_alias": "hello"})
|
||||
assert response.status_code == 200, response.text
|
||||
|
||||
assert response.json() == {"p": "hello"}
|
||||
7
tests/test_request_params/test_form/utils.py
Normal file
7
tests/test_request_params/test_form/utils.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from typing import Any, Dict
|
||||
|
||||
|
||||
def get_body_model_name(openapi: Dict[str, Any], path: str) -> str:
|
||||
body = openapi["paths"][path]["post"]["requestBody"]
|
||||
body_schema = body["content"]["application/x-www-form-urlencoded"]["schema"]
|
||||
return body_schema.get("$ref", "").split("/")[-1]
|
||||
0
tests/test_request_params/test_header/__init__.py
Normal file
0
tests/test_request_params/test_header/__init__.py
Normal file
468
tests/test_request_params/test_header/test_list.py
Normal file
468
tests/test_request_params/test_header/test_list.py
Normal file
@@ -0,0 +1,468 @@
|
||||
from typing import List
|
||||
|
||||
import pytest
|
||||
from dirty_equals import AnyThing, IsDict, IsOneOf, IsPartialDict
|
||||
from fastapi import FastAPI, Header
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel, Field
|
||||
from typing_extensions import Annotated
|
||||
|
||||
from tests.utils import needs_pydanticv2
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# =====================================================================================
|
||||
# Without aliases
|
||||
|
||||
|
||||
@app.get("/required-list-str")
|
||||
async def read_required_list_str(p: Annotated[List[str], Header()]):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class HeaderModelRequiredListStr(BaseModel):
|
||||
p: List[str]
|
||||
|
||||
|
||||
@app.get("/model-required-list-str")
|
||||
def read_model_required_list_str(p: Annotated[HeaderModelRequiredListStr, Header()]):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-list-str", "/model-required-list-str"],
|
||||
)
|
||||
def test_required_list_str_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {
|
||||
"title": "P",
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
"name": "p",
|
||||
"in": "header",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-list-str", "/model-required-list-str"],
|
||||
)
|
||||
def test_required_list_str_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["header", "p"],
|
||||
"msg": "Field required",
|
||||
"input": AnyThing,
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["header", "p"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-list-str", "/model-required-list-str"],
|
||||
)
|
||||
def test_required_list_str(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path, headers=[("p", "hello"), ("p", "world")])
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": ["hello", "world"]}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias
|
||||
|
||||
|
||||
@app.get("/required-list-alias")
|
||||
async def read_required_list_alias(p: Annotated[List[str], Header(alias="p_alias")]):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class HeaderModelRequiredListAlias(BaseModel):
|
||||
p: List[str] = Field(alias="p_alias")
|
||||
|
||||
|
||||
@app.get("/model-required-list-alias")
|
||||
async def read_model_required_list_alias(
|
||||
p: Annotated[HeaderModelRequiredListAlias, Header()],
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-list-alias", "/model-required-list-alias"],
|
||||
)
|
||||
def test_required_list_str_alias_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {
|
||||
"title": "P Alias",
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
"name": "p_alias",
|
||||
"in": "header",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-list-alias", "/model-required-list-alias"],
|
||||
)
|
||||
def test_required_list_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["header", "p_alias"],
|
||||
"msg": "Field required",
|
||||
"input": AnyThing,
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["header", "p_alias"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-list-alias",
|
||||
"/model-required-list-alias",
|
||||
],
|
||||
)
|
||||
def test_required_list_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path, headers=[("p", "hello"), ("p", "world")])
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["header", "p_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, IsPartialDict({"p": ["hello", "world"]})),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["header", "p_alias"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-list-alias",
|
||||
"/model-required-list-alias",
|
||||
],
|
||||
)
|
||||
def test_required_list_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path, headers=[("p_alias", "hello"), ("p_alias", "world")])
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"p": ["hello", "world"]}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Validation alias
|
||||
|
||||
|
||||
@app.get("/required-list-validation-alias")
|
||||
def read_required_list_validation_alias(
|
||||
p: Annotated[List[str], Header(validation_alias="p_val_alias")],
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class HeaderModelRequiredListValidationAlias(BaseModel):
|
||||
p: List[str] = Field(validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.get("/model-required-list-validation-alias")
|
||||
async def read_model_required_list_validation_alias(
|
||||
p: Annotated[HeaderModelRequiredListValidationAlias, Header()],
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-list-validation-alias", "/model-required-list-validation-alias"],
|
||||
)
|
||||
def test_required_list_validation_alias_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {
|
||||
"title": "P Val Alias",
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
"name": "p_val_alias",
|
||||
"in": "header",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-list-validation-alias",
|
||||
"/model-required-list-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_list_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": [
|
||||
"header",
|
||||
"p_val_alias",
|
||||
],
|
||||
"msg": "Field required",
|
||||
"input": AnyThing,
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-list-validation-alias",
|
||||
"/model-required-list-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_list_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path, headers=[("p", "hello"), ("p", "world")])
|
||||
assert response.status_code == 422
|
||||
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["header", "p_val_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, IsPartialDict({"p": ["hello", "world"]})),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-list-validation-alias", "/model-required-list-validation-alias"],
|
||||
)
|
||||
def test_required_list_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(
|
||||
path, headers=[("p_val_alias", "hello"), ("p_val_alias", "world")]
|
||||
)
|
||||
assert response.status_code == 200, response.text
|
||||
|
||||
assert response.json() == {"p": ["hello", "world"]}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias and validation alias
|
||||
|
||||
|
||||
@app.get("/required-list-alias-and-validation-alias")
|
||||
def read_required_list_alias_and_validation_alias(
|
||||
p: Annotated[List[str], Header(alias="p_alias", validation_alias="p_val_alias")],
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class HeaderModelRequiredListAliasAndValidationAlias(BaseModel):
|
||||
p: List[str] = Field(alias="p_alias", validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.get("/model-required-list-alias-and-validation-alias")
|
||||
def read_model_required_list_alias_and_validation_alias(
|
||||
p: Annotated[HeaderModelRequiredListAliasAndValidationAlias, Header()],
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-list-alias-and-validation-alias",
|
||||
"/model-required-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_list_alias_and_validation_alias_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {
|
||||
"title": "P Val Alias",
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
"name": "p_val_alias",
|
||||
"in": "header",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-list-alias-and-validation-alias",
|
||||
"/model-required-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_list_alias_and_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": [
|
||||
"header",
|
||||
"p_val_alias",
|
||||
],
|
||||
"msg": "Field required",
|
||||
"input": AnyThing,
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-list-alias-and-validation-alias",
|
||||
"/model-required-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_list_alias_and_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path, headers=[("p", "hello"), ("p", "world")])
|
||||
assert response.status_code == 422
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": [
|
||||
"header",
|
||||
"p_val_alias",
|
||||
],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(
|
||||
None,
|
||||
IsPartialDict({"p": ["hello", "world"]}),
|
||||
),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-list-alias-and-validation-alias",
|
||||
"/model-required-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_list_alias_and_validation_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path, headers=[("p_alias", "hello"), ("p_alias", "world")])
|
||||
assert response.status_code == 422
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["header", "p_val_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(
|
||||
None,
|
||||
IsPartialDict({"p_alias": ["hello", "world"]}),
|
||||
),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-list-alias-and-validation-alias",
|
||||
"/model-required-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_list_alias_and_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(
|
||||
path, headers=[("p_val_alias", "hello"), ("p_val_alias", "world")]
|
||||
)
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"p": ["hello", "world"]}
|
||||
384
tests/test_request_params/test_header/test_optional_list.py
Normal file
384
tests/test_request_params/test_header/test_optional_list.py
Normal file
@@ -0,0 +1,384 @@
|
||||
from typing import List, Optional
|
||||
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi import FastAPI, Header
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel, Field
|
||||
from typing_extensions import Annotated
|
||||
|
||||
from tests.utils import needs_pydanticv2
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# =====================================================================================
|
||||
# Without aliases
|
||||
|
||||
|
||||
@app.get("/optional-list-str")
|
||||
async def read_optional_list_str(
|
||||
p: Annotated[Optional[List[str]], Header()] = None,
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class HeaderModelOptionalListStr(BaseModel):
|
||||
p: Optional[List[str]] = None
|
||||
|
||||
|
||||
@app.get("/model-optional-list-str")
|
||||
async def read_model_optional_list_str(
|
||||
p: Annotated[HeaderModelOptionalListStr, Header()],
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-str", "/model-optional-list-str"],
|
||||
)
|
||||
def test_optional_list_str_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
IsDict(
|
||||
{
|
||||
"required": False,
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{"items": {"type": "string"}, "type": "array"},
|
||||
{"type": "null"},
|
||||
],
|
||||
"title": "P",
|
||||
},
|
||||
"name": "p",
|
||||
"in": "header",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"required": False,
|
||||
"schema": {"items": {"type": "string"}, "type": "array", "title": "P"},
|
||||
"name": "p",
|
||||
"in": "header",
|
||||
}
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-str", "/model-optional-list-str"],
|
||||
)
|
||||
def test_optional_list_str_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-str", "/model-optional-list-str"],
|
||||
)
|
||||
def test_optional_list_str(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path, headers=[("p", "hello"), ("p", "world")])
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": ["hello", "world"]}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias
|
||||
|
||||
|
||||
@app.get("/optional-list-alias")
|
||||
async def read_optional_list_alias(
|
||||
p: Annotated[Optional[List[str]], Header(alias="p_alias")] = None,
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class HeaderModelOptionalListAlias(BaseModel):
|
||||
p: Optional[List[str]] = Field(None, alias="p_alias")
|
||||
|
||||
|
||||
@app.get("/model-optional-list-alias")
|
||||
async def read_model_optional_list_alias(
|
||||
p: Annotated[HeaderModelOptionalListAlias, Header()],
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-alias", "/model-optional-list-alias"],
|
||||
)
|
||||
def test_optional_list_str_alias_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
IsDict(
|
||||
{
|
||||
"required": False,
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{"items": {"type": "string"}, "type": "array"},
|
||||
{"type": "null"},
|
||||
],
|
||||
"title": "P Alias",
|
||||
},
|
||||
"name": "p_alias",
|
||||
"in": "header",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"required": False,
|
||||
"schema": {
|
||||
"items": {"type": "string"},
|
||||
"type": "array",
|
||||
"title": "P Alias",
|
||||
},
|
||||
"name": "p_alias",
|
||||
"in": "header",
|
||||
}
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-alias", "/model-optional-list-alias"],
|
||||
)
|
||||
def test_optional_list_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-alias", "/model-optional-list-alias"],
|
||||
)
|
||||
def test_optional_list_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path, headers=[("p", "hello"), ("p", "world")])
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-alias",
|
||||
"/model-optional-list-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path, headers=[("p_alias", "hello"), ("p_alias", "world")])
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": ["hello", "world"]}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Validation alias
|
||||
|
||||
|
||||
@app.get("/optional-list-validation-alias")
|
||||
def read_optional_list_validation_alias(
|
||||
p: Annotated[Optional[List[str]], Header(validation_alias="p_val_alias")] = None,
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class HeaderModelOptionalListValidationAlias(BaseModel):
|
||||
p: Optional[List[str]] = Field(None, validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.get("/model-optional-list-validation-alias")
|
||||
def read_model_optional_list_validation_alias(
|
||||
p: Annotated[HeaderModelOptionalListValidationAlias, Header()],
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-validation-alias", "/model-optional-list-validation-alias"],
|
||||
)
|
||||
def test_optional_list_validation_alias_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
{
|
||||
"required": False,
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{"items": {"type": "string"}, "type": "array"},
|
||||
{"type": "null"},
|
||||
],
|
||||
"title": "P Val Alias",
|
||||
},
|
||||
"name": "p_val_alias",
|
||||
"in": "header",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-validation-alias", "/model-optional-list-validation-alias"],
|
||||
)
|
||||
def test_optional_list_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-validation-alias",
|
||||
"/model-optional-list-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path, headers=[("p", "hello"), ("p", "world")])
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-validation-alias", "/model-optional-list-validation-alias"],
|
||||
)
|
||||
def test_optional_list_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(
|
||||
path, headers=[("p_val_alias", "hello"), ("p_val_alias", "world")]
|
||||
)
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"p": ["hello", "world"]}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias and validation alias
|
||||
|
||||
|
||||
@app.get("/optional-list-alias-and-validation-alias")
|
||||
def read_optional_list_alias_and_validation_alias(
|
||||
p: Annotated[
|
||||
Optional[List[str]], Header(alias="p_alias", validation_alias="p_val_alias")
|
||||
] = None,
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class HeaderModelOptionalListAliasAndValidationAlias(BaseModel):
|
||||
p: Optional[List[str]] = Field(
|
||||
None, alias="p_alias", validation_alias="p_val_alias"
|
||||
)
|
||||
|
||||
|
||||
@app.get("/model-optional-list-alias-and-validation-alias")
|
||||
def read_model_optional_list_alias_and_validation_alias(
|
||||
p: Annotated[HeaderModelOptionalListAliasAndValidationAlias, Header()],
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-alias-and-validation-alias",
|
||||
"/model-optional-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_alias_and_validation_alias_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
{
|
||||
"required": False,
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{"items": {"type": "string"}, "type": "array"},
|
||||
{"type": "null"},
|
||||
],
|
||||
"title": "P Val Alias",
|
||||
},
|
||||
"name": "p_val_alias",
|
||||
"in": "header",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-alias-and-validation-alias",
|
||||
"/model-optional-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_alias_and_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-alias-and-validation-alias",
|
||||
"/model-optional-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_alias_and_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path, headers=[("p", "hello"), ("p", "world")])
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-alias-and-validation-alias",
|
||||
"/model-optional-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_alias_and_validation_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path, headers=[("p_alias", "hello"), ("p_alias", "world")])
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-alias-and-validation-alias",
|
||||
"/model-optional-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_alias_and_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(
|
||||
path, headers=[("p_val_alias", "hello"), ("p_val_alias", "world")]
|
||||
)
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {
|
||||
"p": [
|
||||
"hello",
|
||||
"world",
|
||||
]
|
||||
}
|
||||
354
tests/test_request_params/test_header/test_optional_str.py
Normal file
354
tests/test_request_params/test_header/test_optional_str.py
Normal file
@@ -0,0 +1,354 @@
|
||||
from typing import Optional
|
||||
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi import FastAPI, Header
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel, Field
|
||||
from typing_extensions import Annotated
|
||||
|
||||
from tests.utils import needs_pydanticv2
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# =====================================================================================
|
||||
# Without aliases
|
||||
|
||||
|
||||
@app.get("/optional-str")
|
||||
async def read_optional_str(p: Annotated[Optional[str], Header()] = None):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class HeaderModelOptionalStr(BaseModel):
|
||||
p: Optional[str] = None
|
||||
|
||||
|
||||
@app.get("/model-optional-str")
|
||||
async def read_model_optional_str(p: Annotated[HeaderModelOptionalStr, Header()]):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-str", "/model-optional-str"],
|
||||
)
|
||||
def test_optional_str_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
IsDict(
|
||||
{
|
||||
"required": False,
|
||||
"schema": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "P",
|
||||
},
|
||||
"name": "p",
|
||||
"in": "header",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"required": False,
|
||||
"schema": {"title": "P", "type": "string"},
|
||||
"name": "p",
|
||||
"in": "header",
|
||||
}
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-str", "/model-optional-str"],
|
||||
)
|
||||
def test_optional_str_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-str", "/model-optional-str"],
|
||||
)
|
||||
def test_optional_str(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path, headers={"p": "hello"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": "hello"}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias
|
||||
|
||||
|
||||
@app.get("/optional-alias")
|
||||
async def read_optional_alias(
|
||||
p: Annotated[Optional[str], Header(alias="p_alias")] = None,
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class HeaderModelOptionalAlias(BaseModel):
|
||||
p: Optional[str] = Field(None, alias="p_alias")
|
||||
|
||||
|
||||
@app.get("/model-optional-alias")
|
||||
async def read_model_optional_alias(p: Annotated[HeaderModelOptionalAlias, Header()]):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-alias", "/model-optional-alias"],
|
||||
)
|
||||
def test_optional_str_alias_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
IsDict(
|
||||
{
|
||||
"required": False,
|
||||
"schema": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "P Alias",
|
||||
},
|
||||
"name": "p_alias",
|
||||
"in": "header",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"required": False,
|
||||
"schema": {"title": "P Alias", "type": "string"},
|
||||
"name": "p_alias",
|
||||
"in": "header",
|
||||
}
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-alias", "/model-optional-alias"],
|
||||
)
|
||||
def test_optional_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-alias", "/model-optional-alias"],
|
||||
)
|
||||
def test_optional_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path, headers={"p": "hello"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-alias",
|
||||
"/model-optional-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path, headers={"p_alias": "hello"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": "hello"}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Validation alias
|
||||
|
||||
|
||||
@app.get("/optional-validation-alias")
|
||||
def read_optional_validation_alias(
|
||||
p: Annotated[Optional[str], Header(validation_alias="p_val_alias")] = None,
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class HeaderModelOptionalValidationAlias(BaseModel):
|
||||
p: Optional[str] = Field(None, validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.get("/model-optional-validation-alias")
|
||||
def read_model_optional_validation_alias(
|
||||
p: Annotated[HeaderModelOptionalValidationAlias, Header()],
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-validation-alias", "/model-optional-validation-alias"],
|
||||
)
|
||||
def test_optional_validation_alias_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
{
|
||||
"required": False,
|
||||
"schema": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "P Val Alias",
|
||||
},
|
||||
"name": "p_val_alias",
|
||||
"in": "header",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-validation-alias", "/model-optional-validation-alias"],
|
||||
)
|
||||
def test_optional_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-validation-alias",
|
||||
"/model-optional-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path, headers={"p": "hello"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-validation-alias",
|
||||
"/model-optional-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path, headers={"p_val_alias": "hello"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": "hello"}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias and validation alias
|
||||
|
||||
|
||||
@app.get("/optional-alias-and-validation-alias")
|
||||
def read_optional_alias_and_validation_alias(
|
||||
p: Annotated[
|
||||
Optional[str], Header(alias="p_alias", validation_alias="p_val_alias")
|
||||
] = None,
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class HeaderModelOptionalAliasAndValidationAlias(BaseModel):
|
||||
p: Optional[str] = Field(None, alias="p_alias", validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.get("/model-optional-alias-and-validation-alias")
|
||||
def read_model_optional_alias_and_validation_alias(
|
||||
p: Annotated[HeaderModelOptionalAliasAndValidationAlias, Header()],
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-alias-and-validation-alias",
|
||||
"/model-optional-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_alias_and_validation_alias_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
{
|
||||
"required": False,
|
||||
"schema": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "P Val Alias",
|
||||
},
|
||||
"name": "p_val_alias",
|
||||
"in": "header",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-alias-and-validation-alias",
|
||||
"/model-optional-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_alias_and_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-alias-and-validation-alias",
|
||||
"/model-optional-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_alias_and_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path, headers={"p": "hello"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-alias-and-validation-alias",
|
||||
"/model-optional-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_alias_and_validation_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path, headers={"p_alias": "hello"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-alias-and-validation-alias",
|
||||
"/model-optional-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_alias_and_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path, headers={"p_val_alias": "hello"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": "hello"}
|
||||
451
tests/test_request_params/test_header/test_required_str.py
Normal file
451
tests/test_request_params/test_header/test_required_str.py
Normal file
@@ -0,0 +1,451 @@
|
||||
import pytest
|
||||
from dirty_equals import AnyThing, IsDict, IsOneOf, IsPartialDict
|
||||
from fastapi import FastAPI, Header
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel, Field
|
||||
from typing_extensions import Annotated
|
||||
|
||||
from tests.utils import needs_pydanticv2
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# =====================================================================================
|
||||
# Without aliases
|
||||
|
||||
|
||||
@app.get("/required-str")
|
||||
async def read_required_str(p: Annotated[str, Header()]):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class HeaderModelRequiredStr(BaseModel):
|
||||
p: str
|
||||
|
||||
|
||||
@app.get("/model-required-str")
|
||||
async def read_model_required_str(p: Annotated[HeaderModelRequiredStr, Header()]):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-str", "/model-required-str"],
|
||||
)
|
||||
def test_required_str_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "P", "type": "string"},
|
||||
"name": "p",
|
||||
"in": "header",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-str", "/model-required-str"],
|
||||
)
|
||||
def test_required_str_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["header", "p"],
|
||||
"msg": "Field required",
|
||||
"input": AnyThing,
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["header", "p"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-str", "/model-required-str"],
|
||||
)
|
||||
def test_required_str(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path, headers={"p": "hello"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": "hello"}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias
|
||||
|
||||
|
||||
@app.get("/required-alias")
|
||||
async def read_required_alias(p: Annotated[str, Header(alias="p_alias")]):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class HeaderModelRequiredAlias(BaseModel):
|
||||
p: str = Field(alias="p_alias")
|
||||
|
||||
|
||||
@app.get("/model-required-alias")
|
||||
async def read_model_required_alias(p: Annotated[HeaderModelRequiredAlias, Header()]):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-alias", "/model-required-alias"],
|
||||
)
|
||||
def test_required_str_alias_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "P Alias", "type": "string"},
|
||||
"name": "p_alias",
|
||||
"in": "header",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-alias", "/model-required-alias"],
|
||||
)
|
||||
def test_required_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["header", "p_alias"],
|
||||
"msg": "Field required",
|
||||
"input": AnyThing,
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["header", "p_alias"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-alias",
|
||||
"/model-required-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path, headers={"p": "hello"})
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["header", "p_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, IsPartialDict({"p": "hello"})),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["header", "p_alias"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-alias",
|
||||
"/model-required-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path, headers={"p_alias": "hello"})
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"p": "hello"}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Validation alias
|
||||
|
||||
|
||||
@app.get("/required-validation-alias")
|
||||
def read_required_validation_alias(
|
||||
p: Annotated[str, Header(validation_alias="p_val_alias")],
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class HeaderModelRequiredValidationAlias(BaseModel):
|
||||
p: str = Field(validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.get("/model-required-validation-alias")
|
||||
def read_model_required_validation_alias(
|
||||
p: Annotated[HeaderModelRequiredValidationAlias, Header()],
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-validation-alias", "/model-required-validation-alias"],
|
||||
)
|
||||
def test_required_validation_alias_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "P Val Alias", "type": "string"},
|
||||
"name": "p_val_alias",
|
||||
"in": "header",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-validation-alias",
|
||||
"/model-required-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": [
|
||||
"header",
|
||||
"p_val_alias",
|
||||
],
|
||||
"msg": "Field required",
|
||||
"input": AnyThing,
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-validation-alias",
|
||||
"/model-required-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path, headers={"p": "hello"})
|
||||
assert response.status_code == 422, response.text
|
||||
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["header", "p_val_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, IsPartialDict({"p": "hello"})),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-validation-alias",
|
||||
"/model-required-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path, headers={"p_val_alias": "hello"})
|
||||
assert response.status_code == 200, response.text
|
||||
|
||||
assert response.json() == {"p": "hello"}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias and validation alias
|
||||
|
||||
|
||||
@app.get("/required-alias-and-validation-alias")
|
||||
def read_required_alias_and_validation_alias(
|
||||
p: Annotated[str, Header(alias="p_alias", validation_alias="p_val_alias")],
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class HeaderModelRequiredAliasAndValidationAlias(BaseModel):
|
||||
p: str = Field(alias="p_alias", validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.get("/model-required-alias-and-validation-alias")
|
||||
def read_model_required_alias_and_validation_alias(
|
||||
p: Annotated[HeaderModelRequiredAliasAndValidationAlias, Header()],
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-alias-and-validation-alias",
|
||||
"/model-required-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_and_validation_alias_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "P Val Alias", "type": "string"},
|
||||
"name": "p_val_alias",
|
||||
"in": "header",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-alias-and-validation-alias",
|
||||
"/model-required-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_and_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": [
|
||||
"header",
|
||||
"p_val_alias",
|
||||
],
|
||||
"msg": "Field required",
|
||||
"input": AnyThing,
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-alias-and-validation-alias",
|
||||
"/model-required-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_and_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path, headers={"p": "hello"})
|
||||
assert response.status_code == 422
|
||||
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": [
|
||||
"header",
|
||||
"p_val_alias",
|
||||
],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(
|
||||
None,
|
||||
IsPartialDict({"p": "hello"}),
|
||||
),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-alias-and-validation-alias",
|
||||
"/model-required-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_and_validation_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path, headers={"p_alias": "hello"})
|
||||
assert response.status_code == 422
|
||||
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["header", "p_val_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(
|
||||
None,
|
||||
IsPartialDict({"p_alias": "hello"}),
|
||||
),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-alias-and-validation-alias",
|
||||
"/model-required-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_and_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path, headers={"p_val_alias": "hello"})
|
||||
assert response.status_code == 200, response.text
|
||||
|
||||
assert response.json() == {"p": "hello"}
|
||||
0
tests/test_request_params/test_path/__init__.py
Normal file
0
tests/test_request_params/test_path/__init__.py
Normal file
1
tests/test_request_params/test_path/test_list.py
Normal file
1
tests/test_request_params/test_path/test_list.py
Normal file
@@ -0,0 +1 @@
|
||||
# FastAPI doesn't currently support non-scalar Path parameters
|
||||
@@ -0,0 +1 @@
|
||||
# Optional Path parameters are not supported
|
||||
1
tests/test_request_params/test_path/test_optional_str.py
Normal file
1
tests/test_request_params/test_path/test_optional_str.py
Normal file
@@ -0,0 +1 @@
|
||||
# Optional Path parameters are not supported
|
||||
90
tests/test_request_params/test_path/test_required_str.py
Normal file
90
tests/test_request_params/test_path/test_required_str.py
Normal file
@@ -0,0 +1,90 @@
|
||||
import pytest
|
||||
from fastapi import FastAPI, Path
|
||||
from fastapi.testclient import TestClient
|
||||
from typing_extensions import Annotated
|
||||
|
||||
from tests.utils import needs_pydanticv2
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.get("/required-str/{p}")
|
||||
async def read_required_str(p: Annotated[str, Path()]):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
@app.get("/required-alias/{p_alias}")
|
||||
async def read_required_alias(p: Annotated[str, Path(alias="p_alias")]):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
@app.get("/required-validation-alias/{p_val_alias}")
|
||||
def read_required_validation_alias(
|
||||
p: Annotated[str, Path(validation_alias="p_val_alias")],
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
@app.get("/required-alias-and-validation-alias/{p_val_alias}")
|
||||
def read_required_alias_and_validation_alias(
|
||||
p: Annotated[str, Path(alias="p_alias", validation_alias="p_val_alias")],
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("path", "expected_name", "expected_title"),
|
||||
[
|
||||
pytest.param("/required-str/{p}", "p", "P", id="required-str"),
|
||||
pytest.param(
|
||||
"/required-alias/{p_alias}", "p_alias", "P Alias", id="required-alias"
|
||||
),
|
||||
pytest.param(
|
||||
"/required-validation-alias/{p_val_alias}",
|
||||
"p_val_alias",
|
||||
"P Val Alias",
|
||||
id="required-validation-alias",
|
||||
marks=needs_pydanticv2,
|
||||
),
|
||||
pytest.param(
|
||||
"/required-alias-and-validation-alias/{p_val_alias}",
|
||||
"p_val_alias",
|
||||
"P Val Alias",
|
||||
id="required-alias-and-validation-alias",
|
||||
marks=needs_pydanticv2,
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_schema(path: str, expected_name: str, expected_title: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": expected_title, "type": "string"},
|
||||
"name": expected_name,
|
||||
"in": "path",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param("/required-str", id="required-str"),
|
||||
pytest.param("/required-alias", id="required-alias"),
|
||||
pytest.param(
|
||||
"/required-validation-alias",
|
||||
id="required-validation-alias",
|
||||
marks=needs_pydanticv2,
|
||||
),
|
||||
pytest.param(
|
||||
"/required-alias-and-validation-alias",
|
||||
id="required-alias-and-validation-alias",
|
||||
marks=needs_pydanticv2,
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_success(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(f"{path}/hello")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"p": "hello"}
|
||||
0
tests/test_request_params/test_query/__init__.py
Normal file
0
tests/test_request_params/test_query/__init__.py
Normal file
469
tests/test_request_params/test_query/test_list.py
Normal file
469
tests/test_request_params/test_query/test_list.py
Normal file
@@ -0,0 +1,469 @@
|
||||
from typing import List
|
||||
|
||||
import pytest
|
||||
from dirty_equals import IsDict, IsOneOf
|
||||
from fastapi import FastAPI, Query
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel, Field
|
||||
from typing_extensions import Annotated
|
||||
|
||||
from tests.utils import needs_pydanticv2
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# =====================================================================================
|
||||
# Without aliases
|
||||
|
||||
|
||||
@app.get("/required-list-str")
|
||||
async def read_required_list_str(p: Annotated[List[str], Query()]):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class QueryModelRequiredListStr(BaseModel):
|
||||
p: List[str]
|
||||
|
||||
|
||||
@app.get("/model-required-list-str")
|
||||
def read_model_required_list_str(p: Annotated[QueryModelRequiredListStr, Query()]):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-list-str", "/model-required-list-str"],
|
||||
)
|
||||
def test_required_list_str_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {
|
||||
"title": "P",
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
"name": "p",
|
||||
"in": "query",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-list-str", "/model-required-list-str"],
|
||||
)
|
||||
def test_required_list_str_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "p"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {}),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "p"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-list-str", "/model-required-list-str"],
|
||||
)
|
||||
def test_required_list_str(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(f"{path}?p=hello&p=world")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": ["hello", "world"]}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias
|
||||
|
||||
|
||||
@app.get("/required-list-alias")
|
||||
async def read_required_list_alias(p: Annotated[List[str], Query(alias="p_alias")]):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class QueryModelRequiredListAlias(BaseModel):
|
||||
p: List[str] = Field(alias="p_alias")
|
||||
|
||||
|
||||
@app.get("/model-required-list-alias")
|
||||
async def read_model_required_list_alias(
|
||||
p: Annotated[QueryModelRequiredListAlias, Query()],
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-list-alias", "/model-required-list-alias"],
|
||||
)
|
||||
def test_required_list_str_alias_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {
|
||||
"title": "P Alias",
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
"name": "p_alias",
|
||||
"in": "query",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-list-alias", "/model-required-list-alias"],
|
||||
)
|
||||
def test_required_list_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "p_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {}),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "p_alias"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-list-alias",
|
||||
"/model-required-list-alias",
|
||||
],
|
||||
)
|
||||
def test_required_list_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(f"{path}?p=hello&p=world")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "p_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {"p": ["hello", "world"]}),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "p_alias"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-list-alias",
|
||||
"/model-required-list-alias",
|
||||
],
|
||||
)
|
||||
def test_required_list_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(f"{path}?p_alias=hello&p_alias=world")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"p": ["hello", "world"]}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Validation alias
|
||||
|
||||
|
||||
@app.get("/required-list-validation-alias")
|
||||
def read_required_list_validation_alias(
|
||||
p: Annotated[List[str], Query(validation_alias="p_val_alias")],
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class QueryModelRequiredListValidationAlias(BaseModel):
|
||||
p: List[str] = Field(validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.get("/model-required-list-validation-alias")
|
||||
async def read_model_required_list_validation_alias(
|
||||
p: Annotated[QueryModelRequiredListValidationAlias, Query()],
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-list-validation-alias", "/model-required-list-validation-alias"],
|
||||
)
|
||||
def test_required_list_validation_alias_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {
|
||||
"title": "P Val Alias",
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
"name": "p_val_alias",
|
||||
"in": "query",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-list-validation-alias",
|
||||
"/model-required-list-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_list_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": [
|
||||
"query",
|
||||
"p_val_alias",
|
||||
],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {}),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-list-validation-alias",
|
||||
"/model-required-list-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_list_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(f"{path}?p=hello&p=world")
|
||||
assert response.status_code == 422
|
||||
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "p_val_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {"p": ["hello", "world"]}),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-list-validation-alias", "/model-required-list-validation-alias"],
|
||||
)
|
||||
def test_required_list_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(f"{path}?p_val_alias=hello&p_val_alias=world")
|
||||
assert response.status_code == 200, response.text
|
||||
|
||||
assert response.json() == {"p": ["hello", "world"]}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias and validation alias
|
||||
|
||||
|
||||
@app.get("/required-list-alias-and-validation-alias")
|
||||
def read_required_list_alias_and_validation_alias(
|
||||
p: Annotated[List[str], Query(alias="p_alias", validation_alias="p_val_alias")],
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class QueryModelRequiredListAliasAndValidationAlias(BaseModel):
|
||||
p: List[str] = Field(alias="p_alias", validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.get("/model-required-list-alias-and-validation-alias")
|
||||
def read_model_required_list_alias_and_validation_alias(
|
||||
p: Annotated[QueryModelRequiredListAliasAndValidationAlias, Query()],
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-list-alias-and-validation-alias",
|
||||
"/model-required-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_list_alias_and_validation_alias_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {
|
||||
"title": "P Val Alias",
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
"name": "p_val_alias",
|
||||
"in": "query",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-list-alias-and-validation-alias",
|
||||
"/model-required-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_list_alias_and_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": [
|
||||
"query",
|
||||
"p_val_alias",
|
||||
],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {}),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-list-alias-and-validation-alias",
|
||||
"/model-required-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_list_alias_and_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(f"{path}?p=hello&p=world")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": [
|
||||
"query",
|
||||
"p_val_alias",
|
||||
],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(
|
||||
None,
|
||||
{
|
||||
"p": [
|
||||
"hello",
|
||||
"world",
|
||||
]
|
||||
},
|
||||
),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-list-alias-and-validation-alias",
|
||||
"/model-required-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_list_alias_and_validation_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(f"{path}?p_alias=hello&p_alias=world")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "p_val_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(
|
||||
None,
|
||||
{"p_alias": ["hello", "world"]},
|
||||
),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-list-alias-and-validation-alias",
|
||||
"/model-required-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_list_alias_and_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(f"{path}?p_val_alias=hello&p_val_alias=world")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"p": ["hello", "world"]}
|
||||
380
tests/test_request_params/test_query/test_optional_list.py
Normal file
380
tests/test_request_params/test_query/test_optional_list.py
Normal file
@@ -0,0 +1,380 @@
|
||||
from typing import List, Optional
|
||||
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi import FastAPI, Query
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel, Field
|
||||
from typing_extensions import Annotated
|
||||
|
||||
from tests.utils import needs_pydanticv2
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# =====================================================================================
|
||||
# Without aliases
|
||||
|
||||
|
||||
@app.get("/optional-list-str")
|
||||
async def read_optional_list_str(
|
||||
p: Annotated[Optional[List[str]], Query()] = None,
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class QueryModelOptionalListStr(BaseModel):
|
||||
p: Optional[List[str]] = None
|
||||
|
||||
|
||||
@app.get("/model-optional-list-str")
|
||||
async def read_model_optional_list_str(
|
||||
p: Annotated[QueryModelOptionalListStr, Query()],
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-str", "/model-optional-list-str"],
|
||||
)
|
||||
def test_optional_list_str_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
IsDict(
|
||||
{
|
||||
"required": False,
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{"items": {"type": "string"}, "type": "array"},
|
||||
{"type": "null"},
|
||||
],
|
||||
"title": "P",
|
||||
},
|
||||
"name": "p",
|
||||
"in": "query",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"required": False,
|
||||
"schema": {"items": {"type": "string"}, "type": "array", "title": "P"},
|
||||
"name": "p",
|
||||
"in": "query",
|
||||
}
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-str", "/model-optional-list-str"],
|
||||
)
|
||||
def test_optional_list_str_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-str", "/model-optional-list-str"],
|
||||
)
|
||||
def test_optional_list_str(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(f"{path}?p=hello&p=world")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": ["hello", "world"]}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias
|
||||
|
||||
|
||||
@app.get("/optional-list-alias")
|
||||
async def read_optional_list_alias(
|
||||
p: Annotated[Optional[List[str]], Query(alias="p_alias")] = None,
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class QueryModelOptionalListAlias(BaseModel):
|
||||
p: Optional[List[str]] = Field(None, alias="p_alias")
|
||||
|
||||
|
||||
@app.get("/model-optional-list-alias")
|
||||
async def read_model_optional_list_alias(
|
||||
p: Annotated[QueryModelOptionalListAlias, Query()],
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-alias", "/model-optional-list-alias"],
|
||||
)
|
||||
def test_optional_list_str_alias_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
IsDict(
|
||||
{
|
||||
"required": False,
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{"items": {"type": "string"}, "type": "array"},
|
||||
{"type": "null"},
|
||||
],
|
||||
"title": "P Alias",
|
||||
},
|
||||
"name": "p_alias",
|
||||
"in": "query",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"required": False,
|
||||
"schema": {
|
||||
"items": {"type": "string"},
|
||||
"type": "array",
|
||||
"title": "P Alias",
|
||||
},
|
||||
"name": "p_alias",
|
||||
"in": "query",
|
||||
}
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-alias", "/model-optional-list-alias"],
|
||||
)
|
||||
def test_optional_list_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-alias", "/model-optional-list-alias"],
|
||||
)
|
||||
def test_optional_list_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(f"{path}?p=hello&p=world")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-alias",
|
||||
"/model-optional-list-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(f"{path}?p_alias=hello&p_alias=world")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": ["hello", "world"]}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Validation alias
|
||||
|
||||
|
||||
@app.get("/optional-list-validation-alias")
|
||||
def read_optional_list_validation_alias(
|
||||
p: Annotated[Optional[List[str]], Query(validation_alias="p_val_alias")] = None,
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class QueryModelOptionalListValidationAlias(BaseModel):
|
||||
p: Optional[List[str]] = Field(None, validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.get("/model-optional-list-validation-alias")
|
||||
def read_model_optional_list_validation_alias(
|
||||
p: Annotated[QueryModelOptionalListValidationAlias, Query()],
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-validation-alias", "/model-optional-list-validation-alias"],
|
||||
)
|
||||
def test_optional_list_validation_alias_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
{
|
||||
"required": False,
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{"items": {"type": "string"}, "type": "array"},
|
||||
{"type": "null"},
|
||||
],
|
||||
"title": "P Val Alias",
|
||||
},
|
||||
"name": "p_val_alias",
|
||||
"in": "query",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-validation-alias", "/model-optional-list-validation-alias"],
|
||||
)
|
||||
def test_optional_list_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-validation-alias",
|
||||
"/model-optional-list-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(f"{path}?p=hello&p=world")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-validation-alias", "/model-optional-list-validation-alias"],
|
||||
)
|
||||
def test_optional_list_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(f"{path}?p_val_alias=hello&p_val_alias=world")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"p": ["hello", "world"]}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias and validation alias
|
||||
|
||||
|
||||
@app.get("/optional-list-alias-and-validation-alias")
|
||||
def read_optional_list_alias_and_validation_alias(
|
||||
p: Annotated[
|
||||
Optional[List[str]], Query(alias="p_alias", validation_alias="p_val_alias")
|
||||
] = None,
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class QueryModelOptionalListAliasAndValidationAlias(BaseModel):
|
||||
p: Optional[List[str]] = Field(
|
||||
None, alias="p_alias", validation_alias="p_val_alias"
|
||||
)
|
||||
|
||||
|
||||
@app.get("/model-optional-list-alias-and-validation-alias")
|
||||
def read_model_optional_list_alias_and_validation_alias(
|
||||
p: Annotated[QueryModelOptionalListAliasAndValidationAlias, Query()],
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-alias-and-validation-alias",
|
||||
"/model-optional-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_alias_and_validation_alias_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
{
|
||||
"required": False,
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{"items": {"type": "string"}, "type": "array"},
|
||||
{"type": "null"},
|
||||
],
|
||||
"title": "P Val Alias",
|
||||
},
|
||||
"name": "p_val_alias",
|
||||
"in": "query",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-alias-and-validation-alias",
|
||||
"/model-optional-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_alias_and_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-alias-and-validation-alias",
|
||||
"/model-optional-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_alias_and_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(f"{path}?p=hello&p=world")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-alias-and-validation-alias",
|
||||
"/model-optional-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_alias_and_validation_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(f"{path}?p_alias=hello&p_alias=world")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-alias-and-validation-alias",
|
||||
"/model-optional-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_alias_and_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(f"{path}?p_val_alias=hello&p_val_alias=world")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {
|
||||
"p": [
|
||||
"hello",
|
||||
"world",
|
||||
]
|
||||
}
|
||||
354
tests/test_request_params/test_query/test_optional_str.py
Normal file
354
tests/test_request_params/test_query/test_optional_str.py
Normal file
@@ -0,0 +1,354 @@
|
||||
from typing import Optional
|
||||
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi import FastAPI, Query
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel, Field
|
||||
from typing_extensions import Annotated
|
||||
|
||||
from tests.utils import needs_pydanticv2
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# =====================================================================================
|
||||
# Without aliases
|
||||
|
||||
|
||||
@app.get("/optional-str")
|
||||
async def read_optional_str(p: Optional[str] = None):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class QueryModelOptionalStr(BaseModel):
|
||||
p: Optional[str] = None
|
||||
|
||||
|
||||
@app.get("/model-optional-str")
|
||||
async def read_model_optional_str(p: Annotated[QueryModelOptionalStr, Query()]):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-str", "/model-optional-str"],
|
||||
)
|
||||
def test_optional_str_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
IsDict(
|
||||
{
|
||||
"required": False,
|
||||
"schema": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "P",
|
||||
},
|
||||
"name": "p",
|
||||
"in": "query",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"required": False,
|
||||
"schema": {"title": "P", "type": "string"},
|
||||
"name": "p",
|
||||
"in": "query",
|
||||
}
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-str", "/model-optional-str"],
|
||||
)
|
||||
def test_optional_str_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-str", "/model-optional-str"],
|
||||
)
|
||||
def test_optional_str(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(f"{path}?p=hello")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": "hello"}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias
|
||||
|
||||
|
||||
@app.get("/optional-alias")
|
||||
async def read_optional_alias(
|
||||
p: Annotated[Optional[str], Query(alias="p_alias")] = None,
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class QueryModelOptionalAlias(BaseModel):
|
||||
p: Optional[str] = Field(None, alias="p_alias")
|
||||
|
||||
|
||||
@app.get("/model-optional-alias")
|
||||
async def read_model_optional_alias(p: Annotated[QueryModelOptionalAlias, Query()]):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-alias", "/model-optional-alias"],
|
||||
)
|
||||
def test_optional_str_alias_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
IsDict(
|
||||
{
|
||||
"required": False,
|
||||
"schema": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "P Alias",
|
||||
},
|
||||
"name": "p_alias",
|
||||
"in": "query",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"required": False,
|
||||
"schema": {"title": "P Alias", "type": "string"},
|
||||
"name": "p_alias",
|
||||
"in": "query",
|
||||
}
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-alias", "/model-optional-alias"],
|
||||
)
|
||||
def test_optional_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-alias", "/model-optional-alias"],
|
||||
)
|
||||
def test_optional_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(f"{path}?p=hello")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-alias",
|
||||
"/model-optional-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(f"{path}?p_alias=hello")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": "hello"}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Validation alias
|
||||
|
||||
|
||||
@app.get("/optional-validation-alias")
|
||||
def read_optional_validation_alias(
|
||||
p: Annotated[Optional[str], Query(validation_alias="p_val_alias")] = None,
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class QueryModelOptionalValidationAlias(BaseModel):
|
||||
p: Optional[str] = Field(None, validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.get("/model-optional-validation-alias")
|
||||
def read_model_optional_validation_alias(
|
||||
p: Annotated[QueryModelOptionalValidationAlias, Query()],
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-validation-alias", "/model-optional-validation-alias"],
|
||||
)
|
||||
def test_optional_validation_alias_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
{
|
||||
"required": False,
|
||||
"schema": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "P Val Alias",
|
||||
},
|
||||
"name": "p_val_alias",
|
||||
"in": "query",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-validation-alias", "/model-optional-validation-alias"],
|
||||
)
|
||||
def test_optional_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-validation-alias",
|
||||
"/model-optional-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(f"{path}?p=hello")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-validation-alias",
|
||||
"/model-optional-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(f"{path}?p_val_alias=hello")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": "hello"}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias and validation alias
|
||||
|
||||
|
||||
@app.get("/optional-alias-and-validation-alias")
|
||||
def read_optional_alias_and_validation_alias(
|
||||
p: Annotated[
|
||||
Optional[str], Query(alias="p_alias", validation_alias="p_val_alias")
|
||||
] = None,
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class QueryModelOptionalAliasAndValidationAlias(BaseModel):
|
||||
p: Optional[str] = Field(None, alias="p_alias", validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.get("/model-optional-alias-and-validation-alias")
|
||||
def read_model_optional_alias_and_validation_alias(
|
||||
p: Annotated[QueryModelOptionalAliasAndValidationAlias, Query()],
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-alias-and-validation-alias",
|
||||
"/model-optional-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_alias_and_validation_alias_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
{
|
||||
"required": False,
|
||||
"schema": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "P Val Alias",
|
||||
},
|
||||
"name": "p_val_alias",
|
||||
"in": "query",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-alias-and-validation-alias",
|
||||
"/model-optional-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_alias_and_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-alias-and-validation-alias",
|
||||
"/model-optional-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_alias_and_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(f"{path}?p=hello")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-alias-and-validation-alias",
|
||||
"/model-optional-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_alias_and_validation_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(f"{path}?p_alias=hello")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-alias-and-validation-alias",
|
||||
"/model-optional-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_alias_and_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(f"{path}?p_val_alias=hello")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": "hello"}
|
||||
454
tests/test_request_params/test_query/test_required_str.py
Normal file
454
tests/test_request_params/test_query/test_required_str.py
Normal file
@@ -0,0 +1,454 @@
|
||||
import pytest
|
||||
from dirty_equals import IsDict, IsOneOf
|
||||
from fastapi import FastAPI, Query
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel, Field
|
||||
from typing_extensions import Annotated
|
||||
|
||||
from tests.utils import needs_pydanticv2
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# =====================================================================================
|
||||
# Without aliases
|
||||
|
||||
|
||||
@app.get("/required-str")
|
||||
async def read_required_str(p: str):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class QueryModelRequiredStr(BaseModel):
|
||||
p: str
|
||||
|
||||
|
||||
@app.get("/model-required-str")
|
||||
async def read_model_required_str(p: Annotated[QueryModelRequiredStr, Query()]):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-str", "/model-required-str"],
|
||||
)
|
||||
def test_required_str_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "P", "type": "string"},
|
||||
"name": "p",
|
||||
"in": "query",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-str", "/model-required-str"],
|
||||
)
|
||||
def test_required_str_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "p"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {}),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "p"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-str", "/model-required-str"],
|
||||
)
|
||||
def test_required_str(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(f"{path}?p=hello")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": "hello"}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias
|
||||
|
||||
|
||||
@app.get("/required-alias")
|
||||
async def read_required_alias(p: Annotated[str, Query(alias="p_alias")]):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class QueryModelRequiredAlias(BaseModel):
|
||||
p: str = Field(alias="p_alias")
|
||||
|
||||
|
||||
@app.get("/model-required-alias")
|
||||
async def read_model_required_alias(p: Annotated[QueryModelRequiredAlias, Query()]):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-alias", "/model-required-alias"],
|
||||
)
|
||||
def test_required_str_alias_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "P Alias", "type": "string"},
|
||||
"name": "p_alias",
|
||||
"in": "query",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-alias", "/model-required-alias"],
|
||||
)
|
||||
def test_required_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "p_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {}),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "p_alias"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-alias",
|
||||
"/model-required-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(f"{path}?p=hello")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "p_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(
|
||||
None,
|
||||
{"p": "hello"},
|
||||
),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "p_alias"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-alias",
|
||||
"/model-required-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(f"{path}?p_alias=hello")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"p": "hello"}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Validation alias
|
||||
|
||||
|
||||
@app.get("/required-validation-alias")
|
||||
def read_required_validation_alias(
|
||||
p: Annotated[str, Query(validation_alias="p_val_alias")],
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class QueryModelRequiredValidationAlias(BaseModel):
|
||||
p: str = Field(validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.get("/model-required-validation-alias")
|
||||
def read_model_required_validation_alias(
|
||||
p: Annotated[QueryModelRequiredValidationAlias, Query()],
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-validation-alias", "/model-required-validation-alias"],
|
||||
)
|
||||
def test_required_validation_alias_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "P Val Alias", "type": "string"},
|
||||
"name": "p_val_alias",
|
||||
"in": "query",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-validation-alias",
|
||||
"/model-required-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": [
|
||||
"query",
|
||||
"p_val_alias",
|
||||
],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {}),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-validation-alias",
|
||||
"/model-required-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(f"{path}?p=hello")
|
||||
assert response.status_code == 422, response.text
|
||||
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "p_val_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {"p": "hello"}),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-validation-alias",
|
||||
"/model-required-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(f"{path}?p_val_alias=hello")
|
||||
assert response.status_code == 200, response.text
|
||||
|
||||
assert response.json() == {"p": "hello"}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias and validation alias
|
||||
|
||||
|
||||
@app.get("/required-alias-and-validation-alias")
|
||||
def read_required_alias_and_validation_alias(
|
||||
p: Annotated[str, Query(alias="p_alias", validation_alias="p_val_alias")],
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class QueryModelRequiredAliasAndValidationAlias(BaseModel):
|
||||
p: str = Field(alias="p_alias", validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.get("/model-required-alias-and-validation-alias")
|
||||
def read_model_required_alias_and_validation_alias(
|
||||
p: Annotated[QueryModelRequiredAliasAndValidationAlias, Query()],
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-alias-and-validation-alias",
|
||||
"/model-required-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_and_validation_alias_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "P Val Alias", "type": "string"},
|
||||
"name": "p_val_alias",
|
||||
"in": "query",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-alias-and-validation-alias",
|
||||
"/model-required-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_and_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": [
|
||||
"query",
|
||||
"p_val_alias",
|
||||
],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {}),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-alias-and-validation-alias",
|
||||
"/model-required-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_and_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(f"{path}?p=hello")
|
||||
assert response.status_code == 422
|
||||
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": [
|
||||
"query",
|
||||
"p_val_alias",
|
||||
],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(
|
||||
None,
|
||||
{"p": "hello"},
|
||||
),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-alias-and-validation-alias",
|
||||
"/model-required-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_and_validation_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(f"{path}?p_alias=hello")
|
||||
assert response.status_code == 422
|
||||
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "p_val_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(
|
||||
None,
|
||||
{"p_alias": "hello"},
|
||||
),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-alias-and-validation-alias",
|
||||
"/model-required-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_and_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(f"{path}?p_val_alias=hello")
|
||||
assert response.status_code == 200, response.text
|
||||
|
||||
assert response.json() == {"p": "hello"}
|
||||
@@ -8,18 +8,8 @@ client = TestClient(app)
|
||||
def test_get_validation_error():
|
||||
response = client.get("/items/foo")
|
||||
assert response.status_code == 400, response.text
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
assert (
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
"path -> item_id" in response.text
|
||||
or "'loc': ('path', 'item_id')" in response.text
|
||||
)
|
||||
assert (
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
"value is not a valid integer" in response.text
|
||||
or "Input should be a valid integer, unable to parse string as an integer"
|
||||
in response.text
|
||||
)
|
||||
assert "Validation errors:" in response.text
|
||||
assert "Field: ('path', 'item_id')" in response.text
|
||||
|
||||
|
||||
def test_get_http_error():
|
||||
|
||||
207
tests/test_union_body_discriminator_annotated.py
Normal file
207
tests/test_union_body_discriminator_annotated.py
Normal file
@@ -0,0 +1,207 @@
|
||||
# Ref: https://github.com/fastapi/fastapi/discussions/14495
|
||||
|
||||
from typing import Union
|
||||
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
from inline_snapshot import snapshot
|
||||
from pydantic import BaseModel
|
||||
from typing_extensions import Annotated
|
||||
|
||||
from .utils import needs_pydanticv2
|
||||
|
||||
|
||||
@pytest.fixture(name="client")
|
||||
def client_fixture() -> TestClient:
|
||||
from fastapi import Body
|
||||
from pydantic import Discriminator, Tag
|
||||
|
||||
class Cat(BaseModel):
|
||||
pet_type: str = "cat"
|
||||
meows: int
|
||||
|
||||
class Dog(BaseModel):
|
||||
pet_type: str = "dog"
|
||||
barks: float
|
||||
|
||||
def get_pet_type(v):
|
||||
assert isinstance(v, dict)
|
||||
return v.get("pet_type", "")
|
||||
|
||||
Pet = Annotated[
|
||||
Union[Annotated[Cat, Tag("cat")], Annotated[Dog, Tag("dog")]],
|
||||
Discriminator(get_pet_type),
|
||||
]
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
@app.post("/pet/assignment")
|
||||
async def create_pet_assignment(pet: Pet = Body()):
|
||||
return pet
|
||||
|
||||
@app.post("/pet/annotated")
|
||||
async def create_pet_annotated(pet: Annotated[Pet, Body()]):
|
||||
return pet
|
||||
|
||||
client = TestClient(app)
|
||||
return client
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
def test_union_body_discriminator_assignment(client: TestClient) -> None:
|
||||
response = client.post("/pet/assignment", json={"pet_type": "cat", "meows": 5})
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"pet_type": "cat", "meows": 5}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
def test_union_body_discriminator_annotated(client: TestClient) -> None:
|
||||
response = client.post("/pet/annotated", json={"pet_type": "dog", "barks": 3.5})
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"pet_type": "dog", "barks": 3.5}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
def test_openapi_schema(client: TestClient) -> None:
|
||||
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": {
|
||||
"/pet/assignment": {
|
||||
"post": {
|
||||
"summary": "Create Pet Assignment",
|
||||
"operationId": "create_pet_assignment_pet_assignment_post",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{"$ref": "#/components/schemas/Cat"},
|
||||
{"$ref": "#/components/schemas/Dog"},
|
||||
],
|
||||
"title": "Pet",
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": True,
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
"/pet/annotated": {
|
||||
"post": {
|
||||
"summary": "Create Pet Annotated",
|
||||
"operationId": "create_pet_annotated_pet_annotated_post",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"oneOf": [
|
||||
{"$ref": "#/components/schemas/Cat"},
|
||||
{"$ref": "#/components/schemas/Dog"},
|
||||
],
|
||||
"title": "Pet",
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": True,
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"Cat": {
|
||||
"properties": {
|
||||
"pet_type": {
|
||||
"type": "string",
|
||||
"title": "Pet Type",
|
||||
"default": "cat",
|
||||
},
|
||||
"meows": {"type": "integer", "title": "Meows"},
|
||||
},
|
||||
"type": "object",
|
||||
"required": ["meows"],
|
||||
"title": "Cat",
|
||||
},
|
||||
"Dog": {
|
||||
"properties": {
|
||||
"pet_type": {
|
||||
"type": "string",
|
||||
"title": "Pet Type",
|
||||
"default": "dog",
|
||||
},
|
||||
"barks": {"type": "number", "title": "Barks"},
|
||||
},
|
||||
"type": "object",
|
||||
"required": ["barks"],
|
||||
"title": "Dog",
|
||||
},
|
||||
"HTTPValidationError": {
|
||||
"properties": {
|
||||
"detail": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ValidationError"
|
||||
},
|
||||
"type": "array",
|
||||
"title": "Detail",
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"title": "HTTPValidationError",
|
||||
},
|
||||
"ValidationError": {
|
||||
"properties": {
|
||||
"loc": {
|
||||
"items": {
|
||||
"anyOf": [{"type": "string"}, {"type": "integer"}]
|
||||
},
|
||||
"type": "array",
|
||||
"title": "Location",
|
||||
},
|
||||
"msg": {"type": "string", "title": "Message"},
|
||||
"type": {"type": "string", "title": "Error Type"},
|
||||
},
|
||||
"type": "object",
|
||||
"required": ["loc", "msg", "type"],
|
||||
"title": "ValidationError",
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
Reference in New Issue
Block a user