From b0eedbb5804a6ac32e4ee8d029d462d950ff8848 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 11 Sep 2024 09:45:30 +0200 Subject: [PATCH 01/24] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20Improve=20performanc?= =?UTF-8?q?e=20in=20request=20body=20parsing=20with=20a=20cache=20for=20in?= =?UTF-8?q?ternal=20model=20fields=20(#12184)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/_compat.py | 6 ++++++ fastapi/dependencies/utils.py | 4 ++-- tests/test_compat.py | 16 ++++++++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/fastapi/_compat.py b/fastapi/_compat.py index f940d6597..4b07b44fa 100644 --- a/fastapi/_compat.py +++ b/fastapi/_compat.py @@ -2,6 +2,7 @@ from collections import deque from copy import copy from dataclasses import dataclass, is_dataclass from enum import Enum +from functools import lru_cache from typing import ( Any, Callable, @@ -649,3 +650,8 @@ def is_uploadfile_sequence_annotation(annotation: Any) -> bool: is_uploadfile_or_nonable_uploadfile_annotation(sub_annotation) for sub_annotation in get_args(annotation) ) + + +@lru_cache +def get_cached_model_fields(model: Type[BaseModel]) -> List[ModelField]: + return get_model_fields(model) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 6083b7319..f18eace9d 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -32,8 +32,8 @@ from fastapi._compat import ( evaluate_forwardref, field_annotation_is_scalar, get_annotation_from_field_info, + get_cached_model_fields, get_missing_field_error, - get_model_fields, is_bytes_field, is_bytes_sequence_field, is_scalar_field, @@ -810,7 +810,7 @@ async def request_body_to_args( fields_to_extract: List[ModelField] = body_fields if single_not_embedded_field and lenient_issubclass(first_field.type_, BaseModel): - fields_to_extract = get_model_fields(first_field.type_) + fields_to_extract = get_cached_model_fields(first_field.type_) if isinstance(received_body, FormData): body_to_process = await _extract_form_body(fields_to_extract, received_body) diff --git a/tests/test_compat.py b/tests/test_compat.py index 270475bf3..f4a3093c5 100644 --- a/tests/test_compat.py +++ b/tests/test_compat.py @@ -5,6 +5,7 @@ from fastapi._compat import ( ModelField, Undefined, _get_model_config, + get_cached_model_fields, get_model_fields, is_bytes_sequence_annotation, is_scalar_field, @@ -102,3 +103,18 @@ def test_is_pv1_scalar_field(): fields = get_model_fields(Model) assert not is_scalar_field(fields[0]) + + +def test_get_model_fields_cached(): + class Model(BaseModel): + foo: str + + non_cached_fields = get_model_fields(Model) + non_cached_fields2 = get_model_fields(Model) + cached_fields = get_cached_model_fields(Model) + cached_fields2 = get_cached_model_fields(Model) + for f1, f2 in zip(cached_fields, cached_fields2): + assert f1 is f2 + + assert non_cached_fields is not non_cached_fields2 + assert cached_fields is cached_fields2 From 8dc882f75121414eb44db590efae83fbddf43f72 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 11 Sep 2024 07:45:49 +0000 Subject: [PATCH 02/24] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index a72775416..647b51b19 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Refactors + +* ⚡️ Improve performance in request body parsing with a cache for internal model fields. PR [#12184](https://github.com/fastapi/fastapi/pull/12184) by [@tiangolo](https://github.com/tiangolo). + ### Docs * 📝 Remove duplicate line in docs for `docs/en/docs/environment-variables.md`. PR [#12169](https://github.com/fastapi/fastapi/pull/12169) by [@prometek](https://github.com/prometek). From 212fd5e247279073dceaba346fd4afc52f627232 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 11 Sep 2024 09:46:34 +0200 Subject: [PATCH 03/24] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.114.?= =?UTF-8?q?1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 647b51b19..97f472815 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +## 0.114.1 + ### Refactors * ⚡️ Improve performance in request body parsing with a cache for internal model fields. PR [#12184](https://github.com/fastapi/fastapi/pull/12184) by [@tiangolo](https://github.com/tiangolo). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index dce17360f..c2ed4859a 100644 --- a/fastapi/__init__.py +++ b/fastapi/__init__.py @@ -1,6 +1,6 @@ """FastAPI framework, high performance, easy to learn, fast to code, ready for production""" -__version__ = "0.114.0" +__version__ = "0.114.1" from starlette import status as status From 24b8f2668beb773895a93040a2ae284898dc58b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 12 Sep 2024 00:49:55 +0200 Subject: [PATCH 04/24] =?UTF-8?q?=E2=9E=95=20Add=20inline-snapshot=20for?= =?UTF-8?q?=20tests=20(#12189)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 4 ++++ requirements-tests.txt | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index bb87be470..1be2817a1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -241,3 +241,7 @@ known-third-party = ["fastapi", "pydantic", "starlette"] [tool.ruff.lint.pyupgrade] # Preserve types, even if a file imports `from __future__ import annotations`. keep-runtime-typing = true + +[tool.inline-snapshot] +# default-flags=["fix"] +# default-flags=["create"] diff --git a/requirements-tests.txt b/requirements-tests.txt index 809a19c0c..2f2576dd5 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -14,7 +14,7 @@ anyio[trio] >=3.2.1,<4.0.0 PyJWT==2.8.0 pyyaml >=5.3.1,<7.0.0 passlib[bcrypt] >=1.7.2,<2.0.0 - +inline-snapshot==0.13.0 # types types-ujson ==5.7.0.1 types-orjson ==3.6.2 From ba0bb6212e553e779f75f973d07a4db112b43cf0 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 11 Sep 2024 22:50:18 +0000 Subject: [PATCH 05/24] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 97f472815..01c9fb225 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Internal + +* ➕ Add inline-snapshot for tests. PR [#12189](https://github.com/fastapi/fastapi/pull/12189) by [@tiangolo](https://github.com/tiangolo). + ## 0.114.1 ### Refactors From c8e644d19e688e00e51cdca2c1bb15d274d70801 Mon Sep 17 00:00:00 2001 From: Max Scheijen <47034840+maxscheijen@users.noreply.github.com> Date: Thu, 12 Sep 2024 19:00:36 +0200 Subject: [PATCH 06/24] =?UTF-8?q?=F0=9F=8C=90=20Add=20Dutch=20translation?= =?UTF-8?q?=20for=20`docs/nl/docs/python-types.md`=20(#12158)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/nl/docs/python-types.md | 597 +++++++++++++++++++++++++++++++++++ 1 file changed, 597 insertions(+) create mode 100644 docs/nl/docs/python-types.md diff --git a/docs/nl/docs/python-types.md b/docs/nl/docs/python-types.md new file mode 100644 index 000000000..a5562b795 --- /dev/null +++ b/docs/nl/docs/python-types.md @@ -0,0 +1,597 @@ +# Introductie tot Python Types + +Python biedt ondersteuning voor optionele "type hints" (ook wel "type annotaties" genoemd). + +Deze **"type hints"** of annotaties zijn een speciale syntax waarmee het type van een variabele kan worden gedeclareerd. + +Door types voor je variabelen te declareren, kunnen editors en hulpmiddelen je beter ondersteunen. + +Dit is slechts een **korte tutorial/opfrisser** over Python type hints. Het behandelt enkel het minimum dat nodig is om ze te gebruiken met **FastAPI**... en dat is relatief weinig. + +**FastAPI** is helemaal gebaseerd op deze type hints, ze geven veel voordelen. + +Maar zelfs als je **FastAPI** nooit gebruikt, heb je er baat bij om er iets over te leren. + +/// note + +Als je een Python expert bent en alles al weet over type hints, sla dan dit hoofdstuk over. + +/// + +## Motivatie + +Laten we beginnen met een eenvoudig voorbeeld: + +```Python +{!../../../docs_src/python_types/tutorial001.py!} +``` + +Het aanroepen van dit programma leidt tot het volgende resultaat: + +``` +John Doe +``` + +De functie voert het volgende uit: + +* Neem een `first_name` en een `last_name` +* Converteer de eerste letter van elk naar een hoofdletter met `title()`. +`` +* Voeg samen met een spatie in het midden. + +```Python hl_lines="2" +{!../../../docs_src/python_types/tutorial001.py!} +``` + +### Bewerk het + +Dit is een heel eenvoudig programma. + +Maar stel je nu voor dat je het vanaf nul zou moeten maken. + +Op een gegeven moment zou je aan de definitie van de functie zijn begonnen, je had de parameters klaar... + +Maar dan moet je “die methode die de eerste letter naar hoofdletters converteert” aanroepen. + +Was het `upper`? Was het `uppercase`? `first_uppercase`? `capitalize`? + +Dan roep je de hulp in van je oude programmeursvriend, (automatische) code aanvulling in je editor. + +Je typt de eerste parameter van de functie, `first_name`, dan een punt (`.`) en drukt dan op `Ctrl+Spatie` om de aanvulling te activeren. + +Maar helaas krijg je niets bruikbaars: + + + +### Types toevoegen + +Laten we een enkele regel uit de vorige versie aanpassen. + +We zullen precies dit fragment, de parameters van de functie, wijzigen van: + +```Python + first_name, last_name +``` + +naar: + +```Python + first_name: str, last_name: str +``` + +Dat is alles. + +Dat zijn de "type hints": + +```Python hl_lines="1" +{!../../../docs_src/python_types/tutorial002.py!} +``` + +Dit is niet hetzelfde als het declareren van standaardwaarden zoals bij: + +```Python + first_name="john", last_name="doe" +``` + +Het is iets anders. + +We gebruiken dubbele punten (`:`), geen gelijkheidstekens (`=`). + +Het toevoegen van type hints verandert normaal gesproken niet wat er gebeurt in je programma t.o.v. wat er zonder type hints zou gebeuren. + +Maar stel je voor dat je weer bezig bent met het maken van een functie, maar deze keer met type hints. + +Op hetzelfde moment probeer je de automatische aanvulling te activeren met `Ctrl+Spatie` en je ziet: + + + +Nu kun je de opties bekijken en er doorheen scrollen totdat je de optie vindt die “een belletje doet rinkelen”: + + + +### Meer motivatie + +Bekijk deze functie, deze heeft al type hints: + +```Python hl_lines="1" +{!../../../docs_src/python_types/tutorial003.py!} +``` + +Omdat de editor de types van de variabelen kent, krijgt u niet alleen aanvulling, maar ook controles op fouten: + + + +Nu weet je hoe je het moet oplossen, converteer `age` naar een string met `str(age)`: + +```Python hl_lines="2" +{!../../../docs_src/python_types/tutorial004.py!} +``` + +## Types declareren + +Je hebt net de belangrijkste plek om type hints te declareren gezien. Namelijk als functieparameters. + +Dit is ook de belangrijkste plek waar je ze gebruikt met **FastAPI**. + +### Eenvoudige types + +Je kunt alle standaard Python types declareren, niet alleen `str`. + +Je kunt bijvoorbeeld het volgende gebruiken: + +* `int` +* `float` +* `bool` +* `bytes` + +```Python hl_lines="1" +{!../../../docs_src/python_types/tutorial005.py!} +``` + +### Generieke types met typeparameters + +Er zijn enkele datastructuren die andere waarden kunnen bevatten, zoals `dict`, `list`, `set` en `tuple` en waar ook de interne waarden hun eigen type kunnen hebben. + +Deze types die interne types hebben worden “**generieke**” types genoemd. Het is mogelijk om ze te declareren, zelfs met hun interne types. + +Om deze types en de interne types te declareren, kun je de standaard Python module `typing` gebruiken. Deze module is speciaal gemaakt om deze type hints te ondersteunen. + +#### Nieuwere versies van Python + +De syntax met `typing` is **verenigbaar** met alle versies, van Python 3.6 tot aan de nieuwste, inclusief Python 3.9, Python 3.10, enz. + +Naarmate Python zich ontwikkelt, worden **nieuwere versies**, met verbeterde ondersteuning voor deze type annotaties, beschikbaar. In veel gevallen hoef je niet eens de `typing` module te importeren en te gebruiken om de type annotaties te declareren. + +Als je een recentere versie van Python kunt kiezen voor je project, kun je profiteren van die extra eenvoud. + +In alle documentatie staan voorbeelden die compatibel zijn met elke versie van Python (als er een verschil is). + +Bijvoorbeeld “**Python 3.6+**” betekent dat het compatibel is met Python 3.6 of hoger (inclusief 3.7, 3.8, 3.9, 3.10, etc). En “**Python 3.9+**” betekent dat het compatibel is met Python 3.9 of hoger (inclusief 3.10, etc). + +Als je de **laatste versies van Python** kunt gebruiken, gebruik dan de voorbeelden voor de laatste versie, die hebben de **beste en eenvoudigste syntax**, bijvoorbeeld “**Python 3.10+**”. + +#### List + +Laten we bijvoorbeeld een variabele definiëren als een `list` van `str`. + +//// tab | Python 3.9+ + +Declareer de variabele met dezelfde dubbele punt (`:`) syntax. + +Als type, vul `list` in. + +Doordat de list een type is dat enkele interne types bevat, zet je ze tussen vierkante haakjes: + +```Python hl_lines="1" +{!> ../../../docs_src/python_types/tutorial006_py39.py!} +``` + +//// + +//// tab | Python 3.8+ + +Van `typing`, importeer `List` (met een hoofdletter `L`): + +```Python hl_lines="1" +{!> ../../../docs_src/python_types/tutorial006.py!} +``` + +Declareer de variabele met dezelfde dubbele punt (`:`) syntax. + +Zet als type de `List` die je hebt geïmporteerd uit `typing`. + +Doordat de list een type is dat enkele interne types bevat, zet je ze tussen vierkante haakjes: + +```Python hl_lines="4" +{!> ../../../docs_src/python_types/tutorial006.py!} +``` + +//// + +/// info + +De interne types tussen vierkante haakjes worden “typeparameters” genoemd. + +In dit geval is `str` de typeparameter die wordt doorgegeven aan `List` (of `list` in Python 3.9 en hoger). + +/// + +Dat betekent: “de variabele `items` is een `list`, en elk van de items in deze list is een `str`”. + +/// tip + +Als je Python 3.9 of hoger gebruikt, hoef je `List` niet te importeren uit `typing`, je kunt in plaats daarvan hetzelfde reguliere `list` type gebruiken. + +/// + +Door dat te doen, kan je editor ondersteuning bieden, zelfs tijdens het verwerken van items uit de list: + + + +Zonder types is dat bijna onmogelijk om te bereiken. + +Merk op dat de variabele `item` een van de elementen is in de lijst `items`. + +Toch weet de editor dat het een `str` is, en biedt daar vervolgens ondersteuning voor aan. + +#### Tuple en Set + +Je kunt hetzelfde doen om `tuple`s en `set`s te declareren: + +//// tab | Python 3.9+ + +```Python hl_lines="1" +{!> ../../../docs_src/python_types/tutorial007_py39.py!} +``` + +//// + +//// tab | Python 3.8+ + +```Python hl_lines="1 4" +{!> ../../../docs_src/python_types/tutorial007.py!} +``` + +//// + +Dit betekent: + +* De variabele `items_t` is een `tuple` met 3 items, een `int`, nog een `int`, en een `str`. +* De variabele `items_s` is een `set`, en elk van de items is van het type `bytes`. + +#### Dict + +Om een `dict` te definiëren, geef je 2 typeparameters door, gescheiden door komma's. + +De eerste typeparameter is voor de sleutels (keys) van de `dict`. + +De tweede typeparameter is voor de waarden (values) van het `dict`: + +//// tab | Python 3.9+ + +```Python hl_lines="1" +{!> ../../../docs_src/python_types/tutorial008_py39.py!} +``` + +//// + +//// tab | Python 3.8+ + +```Python hl_lines="1 4" +{!> ../../../docs_src/python_types/tutorial008.py!} +``` + +//// + +Dit betekent: + +* De variabele `prices` is een `dict`: + * De sleutels van dit `dict` zijn van het type `str` (bijvoorbeeld de naam van elk item). + * De waarden van dit `dict` zijn van het type `float` (bijvoorbeeld de prijs van elk item). + +#### Union + +Je kunt een variable declareren die van **verschillende types** kan zijn, bijvoorbeeld een `int` of een `str`. + +In Python 3.6 en hoger (inclusief Python 3.10) kun je het `Union`-type van `typing` gebruiken en de mogelijke types die je wilt accepteren, tussen de vierkante haakjes zetten. + +In Python 3.10 is er ook een **nieuwe syntax** waarin je de mogelijke types kunt scheiden door een verticale balk (`|`). + +//// tab | Python 3.10+ + +```Python hl_lines="1" +{!> ../../../docs_src/python_types/tutorial008b_py310.py!} +``` + +//// + +//// tab | Python 3.8+ + +```Python hl_lines="1 4" +{!> ../../../docs_src/python_types/tutorial008b.py!} +``` + +//// + +In beide gevallen betekent dit dat `item` een `int` of een `str` kan zijn. + +#### Mogelijk `None` + +Je kunt declareren dat een waarde een type kan hebben, zoals `str`, maar dat het ook `None` kan zijn. + +In Python 3.6 en hoger (inclusief Python 3.10) kun je het declareren door `Optional` te importeren en te gebruiken vanuit de `typing`-module. + +```Python hl_lines="1 4" +{!../../../docs_src/python_types/tutorial009.py!} +``` + +Door `Optional[str]` te gebruiken in plaats van alleen `str`, kan de editor je helpen fouten te detecteren waarbij je ervan uit zou kunnen gaan dat een waarde altijd een `str` is, terwijl het in werkelijkheid ook `None` zou kunnen zijn. + +`Optional[EenType]` is eigenlijk een snelkoppeling voor `Union[EenType, None]`, ze zijn equivalent. + +Dit betekent ook dat je in Python 3.10 `EenType | None` kunt gebruiken: + +//// tab | Python 3.10+ + +```Python hl_lines="1" +{!> ../../../docs_src/python_types/tutorial009_py310.py!} +``` + +//// + +//// tab | Python 3.8+ + +```Python hl_lines="1 4" +{!> ../../../docs_src/python_types/tutorial009.py!} +``` + +//// + +//// tab | Python 3.8+ alternative + +```Python hl_lines="1 4" +{!> ../../../docs_src/python_types/tutorial009b.py!} +``` + +//// + +#### Gebruik van `Union` of `Optional` + +Als je een Python versie lager dan 3.10 gebruikt, is dit een tip vanuit mijn **subjectieve** standpunt: + +* 🚨 Vermijd het gebruik van `Optional[EenType]`. +* Gebruik in plaats daarvan **`Union[EenType, None]`** ✨. + +Beide zijn gelijkwaardig en onderliggend zijn ze hetzelfde, maar ik zou `Union` aanraden in plaats van `Optional` omdat het woord “**optional**” lijkt te impliceren dat de waarde optioneel is, en het eigenlijk betekent “het kan `None` zijn”, zelfs als het niet optioneel is en nog steeds vereist is. + +Ik denk dat `Union[SomeType, None]` explicieter is over wat het betekent. + +Het gaat alleen om de woorden en naamgeving. Maar die naamgeving kan invloed hebben op hoe jij en je teamgenoten over de code denken. + +Laten we als voorbeeld deze functie nemen: + +```Python hl_lines="1 4" +{!../../../docs_src/python_types/tutorial009c.py!} +``` + +De parameter `name` is gedefinieerd als `Optional[str]`, maar is **niet optioneel**, je kunt de functie niet aanroepen zonder de parameter: + +```Python +say_hi() # Oh, nee, dit geeft een foutmelding! 😱 +``` + +De `name` parameter is **nog steeds vereist** (niet *optioneel*) omdat het geen standaardwaarde heeft. Toch accepteert `name` `None` als waarde: + +```Python +say_hi(name=None) # Dit werkt, None is geldig 🎉 +``` + +Het goede nieuws is dat als je eenmaal Python 3.10 gebruikt, je je daar geen zorgen meer over hoeft te maken, omdat je dan gewoon `|` kunt gebruiken om unions van types te definiëren: + +```Python hl_lines="1 4" +{!../../../docs_src/python_types/tutorial009c_py310.py!} +``` + +Dan hoef je je geen zorgen te maken over namen als `Optional` en `Union`. 😎 + +#### Generieke typen + +De types die typeparameters in vierkante haakjes gebruiken, worden **Generieke types** of **Generics** genoemd, bijvoorbeeld: + +//// tab | Python 3.10+ + +Je kunt dezelfde ingebouwde types gebruiken als generics (met vierkante haakjes en types erin): + +* `list` +* `tuple` +* `set` +* `dict` + +Hetzelfde als bij Python 3.8, uit de `typing`-module: + +* `Union` +* `Optional` (hetzelfde als bij Python 3.8) +* ...en anderen. + +In Python 3.10 kun je , als alternatief voor de generieke `Union` en `Optional`, de verticale lijn (`|`) gebruiken om unions van typen te voorzien, dat is veel beter en eenvoudiger. + +//// + +//// tab | Python 3.9+ + +Je kunt dezelfde ingebouwde types gebruiken als generieke types (met vierkante haakjes en types erin): + +* `list` +* `tuple` +* `set` +* `dict` + +En hetzelfde als met Python 3.8, vanuit de `typing`-module: + +* `Union` +* `Optional` +* ...en anderen. + +//// + +//// tab | Python 3.8+ + +* `List` +* `Tuple` +* `Set` +* `Dict` +* `Union` +* `Optional` +* ...en anderen. + +//// + +### Klassen als types + +Je kunt een klasse ook declareren als het type van een variabele. + +Stel dat je een klasse `Person` hebt, met een naam: + +```Python hl_lines="1-3" +{!../../../docs_src/python_types/tutorial010.py!} +``` + +Vervolgens kun je een variabele van het type `Persoon` declareren: + +```Python hl_lines="6" +{!../../../docs_src/python_types/tutorial010.py!} +``` + +Dan krijg je ook nog eens volledige editorondersteuning: + + + +Merk op dat dit betekent dat "`one_person` een **instantie** is van de klasse `Person`". + +Dit betekent niet dat `one_person` de **klasse** is met de naam `Person`. + +## Pydantic modellen + +Pydantic is een Python-pakket voor het uitvoeren van datavalidatie. + +Je declareert de "vorm" van de data als klassen met attributen. + +Elk attribuut heeft een type. + +Vervolgens maak je een instantie van die klasse met een aantal waarden en het valideert de waarden, converteert ze naar het juiste type (als dat het geval is) en geeft je een object met alle data terug. + +Daarnaast krijg je volledige editorondersteuning met dat resulterende object. + +Een voorbeeld uit de officiële Pydantic-documentatie: + +//// tab | Python 3.10+ + +```Python +{!> ../../../docs_src/python_types/tutorial011_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!> ../../../docs_src/python_types/tutorial011_py39.py!} +``` + +//// + +//// tab | Python 3.8+ + +```Python +{!> ../../../docs_src/python_types/tutorial011.py!} +``` + +//// + +/// info + +Om meer te leren over Pydantic, bekijk de documentatie. + +/// + +**FastAPI** is volledig gebaseerd op Pydantic. + +Je zult veel meer van dit alles in de praktijk zien in de [Tutorial - Gebruikershandleiding](tutorial/index.md){.internal-link target=_blank}. + +/// tip + +Pydantic heeft een speciaal gedrag wanneer je `Optional` of `Union[EenType, None]` gebruikt zonder een standaardwaarde, je kunt er meer over lezen in de Pydantic-documentatie over Verplichte optionele velden. + +/// + +## Type Hints met Metadata Annotaties + +Python heeft ook een functie waarmee je **extra metadata** in deze type hints kunt toevoegen met behulp van `Annotated`. + +//// tab | Python 3.9+ + +In Python 3.9 is `Annotated` onderdeel van de standaardpakket, dus je kunt het importeren vanuit `typing`. + +```Python hl_lines="1 4" +{!> ../../../docs_src/python_types/tutorial013_py39.py!} +``` + +//// + +//// tab | Python 3.8+ + +In versies lager dan Python 3.9 importeer je `Annotated` vanuit `typing_extensions`. + +Het wordt al geïnstalleerd met **FastAPI**. + +```Python hl_lines="1 4" +{!> ../../../docs_src/python_types/tutorial013.py!} +``` + +//// + +Python zelf doet niets met deze `Annotated` en voor editors en andere hulpmiddelen is het type nog steeds een `str`. + +Maar je kunt deze ruimte in `Annotated` gebruiken om **FastAPI** te voorzien van extra metadata over hoe je wilt dat je applicatie zich gedraagt. + +Het belangrijkste om te onthouden is dat **de eerste *typeparameter*** die je doorgeeft aan `Annotated` het **werkelijke type** is. De rest is gewoon metadata voor andere hulpmiddelen. + +Voor nu hoef je alleen te weten dat `Annotated` bestaat en dat het standaard Python is. 😎 + +Later zul je zien hoe **krachtig** het kan zijn. + +/// tip + +Het feit dat dit **standaard Python** is, betekent dat je nog steeds de **best mogelijke ontwikkelaarservaring** krijgt in je editor, met de hulpmiddelen die je gebruikt om je code te analyseren en te refactoren, enz. ✨ + +Daarnaast betekent het ook dat je code zeer verenigbaar zal zijn met veel andere Python-hulpmiddelen en -pakketten. 🚀 + +/// + +## Type hints in **FastAPI** + +**FastAPI** maakt gebruik van type hints om verschillende dingen te doen. + +Met **FastAPI** declareer je parameters met type hints en krijg je: + +* **Editor ondersteuning**. +* **Type checks**. + +...en **FastAPI** gebruikt dezelfde declaraties om: + +* **Vereisten te definïeren **: van request pad parameters, query parameters, headers, bodies, dependencies, enz. +* **Data te converteren**: van de request naar het vereiste type. +* **Data te valideren**: afkomstig van elke request: + * **Automatische foutmeldingen** te genereren die naar de client worden geretourneerd wanneer de data ongeldig is. +* De API met OpenAPI te **documenteren**: + * die vervolgens wordt gebruikt door de automatische interactieve documentatie gebruikersinterfaces. + +Dit klinkt misschien allemaal abstract. Maak je geen zorgen. Je ziet dit allemaal in actie in de [Tutorial - Gebruikershandleiding](tutorial/index.md){.internal-link target=_blank}. + +Het belangrijkste is dat door standaard Python types te gebruiken, op één plek (in plaats van meer klassen, decorators, enz. toe te voegen), **FastAPI** een groot deel van het werk voor je doet. + +/// info + +Als je de hele tutorial al hebt doorgenomen en terug bent gekomen om meer te weten te komen over types, is een goede bron het "cheat sheet" van `mypy`. + +/// From 492943fdb1f726800ab7a42ead08e297813c8e68 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 12 Sep 2024 17:01:01 +0000 Subject: [PATCH 07/24] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 01c9fb225..c3bf2bb8d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Translations + +* 🌐 Add Dutch translation for `docs/nl/docs/python-types.md`. PR [#12158](https://github.com/fastapi/fastapi/pull/12158) by [@maxscheijen](https://github.com/maxscheijen). + ### Internal * ➕ Add inline-snapshot for tests. PR [#12189](https://github.com/fastapi/fastapi/pull/12189) by [@tiangolo](https://github.com/tiangolo). From 4a94fe3c8249e2c13999964ac9f707ab0ca069ee Mon Sep 17 00:00:00 2001 From: Waket Zheng Date: Fri, 13 Sep 2024 01:01:54 +0800 Subject: [PATCH 08/24] =?UTF-8?q?=F0=9F=8C=90=20Add=20Chinese=20translatio?= =?UTF-8?q?n=20for=20`docs/zh/docs/project-generation.md`=20(#12170)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/zh/docs/project-generation.md | 112 ++++++++--------------------- 1 file changed, 28 insertions(+), 84 deletions(-) diff --git a/docs/zh/docs/project-generation.md b/docs/zh/docs/project-generation.md index 0655cb0a9..9b3735539 100644 --- a/docs/zh/docs/project-generation.md +++ b/docs/zh/docs/project-generation.md @@ -1,84 +1,28 @@ -# 项目生成 - 模板 - -项目生成器一般都会提供很多初始设置、安全措施、数据库,甚至还准备好了第一个 API 端点,能帮助您快速上手。 - -项目生成器的设置通常都很主观,您可以按需更新或修改,但对于您的项目来说,它是非常好的起点。 - -## 全栈 FastAPI + PostgreSQL - -GitHub:https://github.com/tiangolo/full-stack-fastapi-postgresql - -### 全栈 FastAPI + PostgreSQL - 功能 - -* 完整的 **Docker** 集成(基于 Docker) -* Docker Swarm 开发模式 -* **Docker Compose** 本地开发集成与优化 -* **生产可用**的 Python 网络服务器,使用 Uvicorn 或 Gunicorn -* Python **FastAPI** 后端: -* * **速度快**:可与 **NodeJS** 和 **Go** 比肩的极高性能(归功于 Starlette 和 Pydantic) - * **直观**:强大的编辑器支持,处处皆可自动补全,减少调试时间 - * **简单**:易学、易用,阅读文档所需时间更短 - * **简短**:代码重复最小化,每次参数声明都可以实现多个功能 - * **健壮**: 生产级别的代码,还有自动交互文档 - * **基于标准**:完全兼容并基于 API 开放标准:OpenAPIJSON Schema - * **更多功能**包括自动验证、序列化、交互文档、OAuth2 JWT 令牌身份验证等 -* **安全密码**,默认使用密码哈希 -* **JWT 令牌**身份验证 -* **SQLAlchemy** 模型(独立于 Flask 扩展,可直接用于 Celery Worker) -* 基础的用户模型(可按需修改或删除) -* **Alembic** 迁移 -* **CORS**(跨域资源共享) -* **Celery** Worker 可从后端其它部分有选择地导入并使用模型和代码 -* REST 后端测试基于 Pytest,并与 Docker 集成,可独立于数据库实现完整的 API 交互测试。因为是在 Docker 中运行,每次都可从头构建新的数据存储(使用 ElasticSearch、MongoDB、CouchDB 等数据库,仅测试 API 运行) -* Python 与 **Jupyter Kernels** 集成,用于远程或 Docker 容器内部开发,使用 Atom Hydrogen 或 Visual Studio Code 的 Jupyter 插件 -* **Vue** 前端: - * 由 Vue CLI 生成 - * **JWT 身份验证**处理 - * 登录视图 - * 登录后显示主仪表盘视图 - * 主仪表盘支持用户创建与编辑 - * 用户信息编辑 - * **Vuex** - * **Vue-router** - * **Vuetify** 美化组件 - * **TypeScript** - * 基于 **Nginx** 的 Docker 服务器(优化了 Vue-router 配置) - * Docker 多阶段构建,无需保存或提交编译的代码 - * 在构建时运行前端测试(可禁用) - * 尽量模块化,开箱即用,但仍可使用 Vue CLI 重新生成或创建所需项目,或复用所需内容 -* 使用 **PGAdmin** 管理 PostgreSQL 数据库,可轻松替换为 PHPMyAdmin 或 MySQL -* 使用 **Flower** 监控 Celery 任务 -* 使用 **Traefik** 处理前后端负载平衡,可把前后端放在同一个域下,按路径分隔,但在不同容器中提供服务 -* Traefik 集成,包括自动生成 Let's Encrypt **HTTPS** 凭证 -* GitLab **CI**(持续集成),包括前后端测试 - -## 全栈 FastAPI + Couchbase - -GitHub:https://github.com/tiangolo/full-stack-fastapi-couchbase - -⚠️ **警告** ⚠️ - -如果您想从头开始创建新项目,建议使用以下备选方案。 - -例如,项目生成器全栈 FastAPI + PostgreSQL 会更适用,这个项目的维护积极,用的人也多,还包括了所有新功能和改进内容。 - -当然,您也可以放心使用这个基于 Couchbase 的生成器,它也能正常使用。就算用它生成项目也没有任何问题(为了更好地满足需求,您可以自行更新这个项目)。 - -详见资源仓库中的文档。 - -## 全栈 FastAPI + MongoDB - -……敬请期待,得看我有没有时间做这个项目。😅 🎉 - -## FastAPI + spaCy 机器学习模型 - -GitHub:https://github.com/microsoft/cookiecutter-spacy-fastapi - -### FastAPI + spaCy 机器学习模型 - 功能 - -* 集成 **spaCy** NER 模型 -* 内置 **Azure 认知搜索**请求格式 -* **生产可用**的 Python 网络服务器,使用 Uvicorn 与 Gunicorn -* 内置 **Azure DevOps** Kubernetes (AKS) CI/CD 开发 -* **多语**支持,可在项目设置时选择 spaCy 内置的语言 -* 不仅局限于 spaCy,可**轻松扩展**至其它模型框架(Pytorch、TensorFlow) +# FastAPI全栈模板 + +模板通常带有特定的设置,而且被设计为灵活和可定制的。这允许您根据项目的需求修改和调整它们,使它们成为一个很好的起点。🏁 + +您可以使用此模板开始,因为它包含了许多已经为您完成的初始设置、安全性、数据库和一些API端点。 + +代码仓: Full Stack FastAPI Template + +## FastAPI全栈模板 - 技术栈和特性 + +- ⚡ [**FastAPI**](https://fastapi.tiangolo.com) 用于Python后端API. + - 🧰 [SQLModel](https://sqlmodel.tiangolo.com) 用于Python和SQL数据库的集成(ORM)。 + - 🔍 [Pydantic](https://docs.pydantic.dev) FastAPI的依赖项之一,用于数据验证和配置管理。 + - 💾 [PostgreSQL](https://www.postgresql.org) 作为SQL数据库。 +- 🚀 [React](https://react.dev) 用于前端。 + - 💃 使用了TypeScript、hooks、Vite和其他一些现代化的前端技术栈。 + - 🎨 [Chakra UI](https://chakra-ui.com) 用于前端组件。 + - 🤖 一个自动化生成的前端客户端。 + - 🧪 Playwright用于端到端测试。 + - 🦇 支持暗黑主题(Dark mode)。 +- 🐋 [Docker Compose](https://www.docker.com) 用于开发环境和生产环境。 +- 🔒 默认使用密码哈希来保证安全。 +- 🔑 JWT令牌用于权限验证。 +- 📫 使用邮箱来进行密码恢复。 +- ✅ 单元测试用了[Pytest](https://pytest.org). +- 📞 [Traefik](https://traefik.io) 用于反向代理和负载均衡。 +- 🚢 部署指南(Docker Compose)包含了如何起一个Traefik前端代理来自动化HTTPS认证。 +- 🏭 CI(持续集成)和 CD(持续部署)基于GitHub Actions。 From 93e50e373b0651c22a6743a4e907dafbadc8d27e Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 12 Sep 2024 17:03:26 +0000 Subject: [PATCH 09/24] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index c3bf2bb8d..ac2398759 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Translations +* 🌐 Add Chinese translation for `docs/zh/docs/project-generation.md`. PR [#12170](https://github.com/fastapi/fastapi/pull/12170) by [@waketzheng](https://github.com/waketzheng). * 🌐 Add Dutch translation for `docs/nl/docs/python-types.md`. PR [#12158](https://github.com/fastapi/fastapi/pull/12158) by [@maxscheijen](https://github.com/maxscheijen). ### Internal From e50facaf227f4725d64c7166c2fe3438367705c7 Mon Sep 17 00:00:00 2001 From: Rafael de Oliveira Marques Date: Thu, 12 Sep 2024 14:03:48 -0300 Subject: [PATCH 10/24] =?UTF-8?q?=F0=9F=8C=90=20Add=20Portuguese=20transla?= =?UTF-8?q?tion=20for=20`docs/pt/docs/tutorial/request-form-models.md`=20(?= =?UTF-8?q?#12175)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/pt/docs/tutorial/request-form-models.md | 134 +++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 docs/pt/docs/tutorial/request-form-models.md diff --git a/docs/pt/docs/tutorial/request-form-models.md b/docs/pt/docs/tutorial/request-form-models.md new file mode 100644 index 000000000..a9db18e9d --- /dev/null +++ b/docs/pt/docs/tutorial/request-form-models.md @@ -0,0 +1,134 @@ +# Modelos de Formulários + +Você pode utilizar **Modelos Pydantic** para declarar **campos de formulários** no FastAPI. + +/// info | "Informação" + +Para utilizar formulários, instale primeiramente o `python-multipart`. + +Certifique-se de criar um [ambiente virtual](../virtual-environments.md){.internal-link target=_blank}, ativá-lo, e então instalar. Por exemplo: + +```console +$ pip install python-multipart +``` + +/// + +/// note | "Nota" + +Isto é suportado desde a versão `0.113.0` do FastAPI. 🤓 + +/// + +## Modelos Pydantic para Formulários + +Você precisa apenas declarar um **modelo Pydantic** com os campos que deseja receber como **campos de formulários**, e então declarar o parâmetro como um `Form`: + +//// tab | Python 3.9+ + +```Python hl_lines="9-11 15" +{!> ../../../docs_src/request_form_models/tutorial001_an_py39.py!} +``` + +//// + +//// tab | Python 3.8+ + +```Python hl_lines="8-10 14" +{!> ../../../docs_src/request_form_models/tutorial001_an.py!} +``` + +//// + +//// tab | Python 3.8+ non-Annotated + +/// tip | "Dica" + +Prefira utilizar a versão `Annotated` se possível. + +/// + +```Python hl_lines="7-9 13" +{!> ../../../docs_src/request_form_models/tutorial001.py!} +``` + +//// + +O **FastAPI** irá **extrair** as informações para **cada campo** dos **dados do formulário** na requisição e dar para você o modelo Pydantic que você definiu. + +## Confira os Documentos + +Você pode verificar na UI de documentação em `/docs`: + +
+ +
+ +## Proibir Campos Extras de Formulários + +Em alguns casos de uso especiais (provavelmente não muito comum), você pode desejar **restringir** os campos do formulário para aceitar apenas os declarados no modelo Pydantic. E **proibir** qualquer campo **extra**. + +/// note | "Nota" + +Isso é suportado deste a versão `0.114.0` do FastAPI. 🤓 + +/// + +Você pode utilizar a configuração de modelo do Pydantic para `proibir` qualquer campo `extra`: + +//// tab | Python 3.9+ + +```Python hl_lines="12" +{!> ../../../docs_src/request_form_models/tutorial002_an_py39.py!} +``` + +//// + +//// tab | Python 3.8+ + +```Python hl_lines="11" +{!> ../../../docs_src/request_form_models/tutorial002_an.py!} +``` + +//// + +//// tab | Python 3.8+ non-Annotated + +/// tip + +Prefira utilizar a versão `Annotated` se possível. + +/// + +```Python hl_lines="10" +{!> ../../../docs_src/request_form_models/tutorial002.py!} +``` + +//// + +Caso um cliente tente enviar informações adicionais, ele receberá um retorno de **erro**. + +Por exemplo, se o cliente tentar enviar os campos de formulário: + +* `username`: `Rick` +* `password`: `Portal Gun` +* `extra`: `Mr. Poopybutthole` + +Ele receberá um retorno de erro informando-o que o campo `extra` não é permitido: + +```json +{ + "detail": [ + { + "type": "extra_forbidden", + "loc": ["body", "extra"], + "msg": "Extra inputs are not permitted", + "input": "Mr. Poopybutthole" + } + ] +} +``` + +## Resumo + +Você pode utilizar modelos Pydantic para declarar campos de formulários no FastAPI. 😎 From ed66d705139b67665db1742797fdbeba7490c0e2 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 12 Sep 2024 17:06:34 +0000 Subject: [PATCH 11/24] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index ac2398759..6534adb03 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Translations +* 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/request-form-models.md`. PR [#12175](https://github.com/fastapi/fastapi/pull/12175) by [@ceb10n](https://github.com/ceb10n). * 🌐 Add Chinese translation for `docs/zh/docs/project-generation.md`. PR [#12170](https://github.com/fastapi/fastapi/pull/12170) by [@waketzheng](https://github.com/waketzheng). * 🌐 Add Dutch translation for `docs/nl/docs/python-types.md`. PR [#12158](https://github.com/fastapi/fastapi/pull/12158) by [@maxscheijen](https://github.com/maxscheijen). From 2a4351105ed968002ad15530dec35c6bb453a042 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 13 Sep 2024 11:14:46 +0200 Subject: [PATCH 12/24] =?UTF-8?q?=F0=9F=92=A1=20Add=20comments=20with=20in?= =?UTF-8?q?structions=20for=20Playwright=20screenshot=20scripts=20(#12193)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/playwright/request_form_models/image01.py | 4 +++- scripts/playwright/separate_openapi_schemas/image01.py | 3 +++ scripts/playwright/separate_openapi_schemas/image02.py | 3 +++ scripts/playwright/separate_openapi_schemas/image03.py | 3 +++ scripts/playwright/separate_openapi_schemas/image04.py | 3 +++ scripts/playwright/separate_openapi_schemas/image05.py | 3 +++ 6 files changed, 18 insertions(+), 1 deletion(-) diff --git a/scripts/playwright/request_form_models/image01.py b/scripts/playwright/request_form_models/image01.py index 15bd3858c..fe4da32fc 100644 --- a/scripts/playwright/request_form_models/image01.py +++ b/scripts/playwright/request_form_models/image01.py @@ -8,11 +8,13 @@ from playwright.sync_api import Playwright, sync_playwright # Run playwright codegen to generate the code below, copy paste the sections in run() def run(playwright: Playwright) -> None: browser = playwright.chromium.launch(headless=False) - context = browser.new_context() + # Update the viewport manually + context = browser.new_context(viewport={"width": 960, "height": 1080}) page = context.new_page() page.goto("http://localhost:8000/docs") page.get_by_role("button", name="POST /login/ Login").click() page.get_by_role("button", name="Try it out").click() + # Manually add the screenshot page.screenshot(path="docs/en/docs/img/tutorial/request-form-models/image01.png") # --------------------- diff --git a/scripts/playwright/separate_openapi_schemas/image01.py b/scripts/playwright/separate_openapi_schemas/image01.py index 0b40f3bbc..0eb55fb73 100644 --- a/scripts/playwright/separate_openapi_schemas/image01.py +++ b/scripts/playwright/separate_openapi_schemas/image01.py @@ -3,13 +3,16 @@ import subprocess from playwright.sync_api import Playwright, sync_playwright +# Run playwright codegen to generate the code below, copy paste the sections in run() def run(playwright: Playwright) -> None: browser = playwright.chromium.launch(headless=False) + # Update the viewport manually context = browser.new_context(viewport={"width": 960, "height": 1080}) page = context.new_page() page.goto("http://localhost:8000/docs") page.get_by_text("POST/items/Create Item").click() page.get_by_role("tab", name="Schema").first.click() + # Manually add the screenshot page.screenshot( path="docs/en/docs/img/tutorial/separate-openapi-schemas/image01.png" ) diff --git a/scripts/playwright/separate_openapi_schemas/image02.py b/scripts/playwright/separate_openapi_schemas/image02.py index f76af7ee2..0eb6c3c79 100644 --- a/scripts/playwright/separate_openapi_schemas/image02.py +++ b/scripts/playwright/separate_openapi_schemas/image02.py @@ -3,14 +3,17 @@ import subprocess from playwright.sync_api import Playwright, sync_playwright +# Run playwright codegen to generate the code below, copy paste the sections in run() def run(playwright: Playwright) -> None: browser = playwright.chromium.launch(headless=False) + # Update the viewport manually context = browser.new_context(viewport={"width": 960, "height": 1080}) page = context.new_page() page.goto("http://localhost:8000/docs") page.get_by_text("GET/items/Read Items").click() page.get_by_role("button", name="Try it out").click() page.get_by_role("button", name="Execute").click() + # Manually add the screenshot page.screenshot( path="docs/en/docs/img/tutorial/separate-openapi-schemas/image02.png" ) diff --git a/scripts/playwright/separate_openapi_schemas/image03.py b/scripts/playwright/separate_openapi_schemas/image03.py index 127f5c428..b68e9d7db 100644 --- a/scripts/playwright/separate_openapi_schemas/image03.py +++ b/scripts/playwright/separate_openapi_schemas/image03.py @@ -3,14 +3,17 @@ import subprocess from playwright.sync_api import Playwright, sync_playwright +# Run playwright codegen to generate the code below, copy paste the sections in run() def run(playwright: Playwright) -> None: browser = playwright.chromium.launch(headless=False) + # Update the viewport manually context = browser.new_context(viewport={"width": 960, "height": 1080}) page = context.new_page() page.goto("http://localhost:8000/docs") page.get_by_text("GET/items/Read Items").click() page.get_by_role("tab", name="Schema").click() page.get_by_label("Schema").get_by_role("button", name="Expand all").click() + # Manually add the screenshot page.screenshot( path="docs/en/docs/img/tutorial/separate-openapi-schemas/image03.png" ) diff --git a/scripts/playwright/separate_openapi_schemas/image04.py b/scripts/playwright/separate_openapi_schemas/image04.py index 208eaf8a0..a36c2f6b2 100644 --- a/scripts/playwright/separate_openapi_schemas/image04.py +++ b/scripts/playwright/separate_openapi_schemas/image04.py @@ -3,14 +3,17 @@ import subprocess from playwright.sync_api import Playwright, sync_playwright +# Run playwright codegen to generate the code below, copy paste the sections in run() def run(playwright: Playwright) -> None: browser = playwright.chromium.launch(headless=False) + # Update the viewport manually context = browser.new_context(viewport={"width": 960, "height": 1080}) page = context.new_page() page.goto("http://localhost:8000/docs") page.get_by_role("button", name="Item-Input").click() page.get_by_role("button", name="Item-Output").click() page.set_viewport_size({"width": 960, "height": 820}) + # Manually add the screenshot page.screenshot( path="docs/en/docs/img/tutorial/separate-openapi-schemas/image04.png" ) diff --git a/scripts/playwright/separate_openapi_schemas/image05.py b/scripts/playwright/separate_openapi_schemas/image05.py index 83966b449..0da5db0cf 100644 --- a/scripts/playwright/separate_openapi_schemas/image05.py +++ b/scripts/playwright/separate_openapi_schemas/image05.py @@ -3,13 +3,16 @@ import subprocess from playwright.sync_api import Playwright, sync_playwright +# Run playwright codegen to generate the code below, copy paste the sections in run() def run(playwright: Playwright) -> None: browser = playwright.chromium.launch(headless=False) + # Update the viewport manually context = browser.new_context(viewport={"width": 960, "height": 1080}) page = context.new_page() page.goto("http://localhost:8000/docs") page.get_by_role("button", name="Item", exact=True).click() page.set_viewport_size({"width": 960, "height": 700}) + # Manually add the screenshot page.screenshot( path="docs/en/docs/img/tutorial/separate-openapi-schemas/image05.png" ) From 0fc6e34135b2436a8749f5aa3b8f8ad92da106d5 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 13 Sep 2024 09:15:10 +0000 Subject: [PATCH 13/24] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 6534adb03..f00c3fed3 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -15,6 +15,7 @@ hide: ### Internal +* 💡 Add comments with instructions for Playwright screenshot scripts. PR [#12193](https://github.com/fastapi/fastapi/pull/12193) by [@tiangolo](https://github.com/tiangolo). * ➕ Add inline-snapshot for tests. PR [#12189](https://github.com/fastapi/fastapi/pull/12189) by [@tiangolo](https://github.com/tiangolo). ## 0.114.1 From 88d4f2cb1814392f54011b2bbd3fe55c5f2a3278 Mon Sep 17 00:00:00 2001 From: Nico Tonnhofer Date: Fri, 13 Sep 2024 11:51:00 +0200 Subject: [PATCH 14/24] =?UTF-8?q?=F0=9F=90=9B=20Fix=20form=20field=20regre?= =?UTF-8?q?ssion=20(#12194)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/dependencies/utils.py | 2 +- tests/test_forms_single_model.py | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index f18eace9d..7548cf0c7 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -788,7 +788,7 @@ 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.name] = value + values[field.alias] = value for key, value in received_body.items(): if key not in values: values[key] = value diff --git a/tests/test_forms_single_model.py b/tests/test_forms_single_model.py index 7ed3ba3a2..880ab3820 100644 --- a/tests/test_forms_single_model.py +++ b/tests/test_forms_single_model.py @@ -3,7 +3,7 @@ from typing import List, Optional from dirty_equals import IsDict from fastapi import FastAPI, Form from fastapi.testclient import TestClient -from pydantic import BaseModel +from pydantic import BaseModel, Field from typing_extensions import Annotated app = FastAPI() @@ -14,6 +14,7 @@ class FormModel(BaseModel): lastname: str age: Optional[int] = None tags: List[str] = ["foo", "bar"] + alias_with: str = Field(alias="with", default="nothing") @app.post("/form/") @@ -32,6 +33,7 @@ def test_send_all_data(): "lastname": "Sanchez", "age": "70", "tags": ["plumbus", "citadel"], + "with": "something", }, ) assert response.status_code == 200, response.text @@ -40,6 +42,7 @@ def test_send_all_data(): "lastname": "Sanchez", "age": 70, "tags": ["plumbus", "citadel"], + "with": "something", } @@ -51,6 +54,7 @@ def test_defaults(): "lastname": "Sanchez", "age": None, "tags": ["foo", "bar"], + "with": "nothing", } @@ -100,13 +104,13 @@ def test_no_data(): "type": "missing", "loc": ["body", "username"], "msg": "Field required", - "input": {"tags": ["foo", "bar"]}, + "input": {"tags": ["foo", "bar"], "with": "nothing"}, }, { "type": "missing", "loc": ["body", "lastname"], "msg": "Field required", - "input": {"tags": ["foo", "bar"]}, + "input": {"tags": ["foo", "bar"], "with": "nothing"}, }, ] } From 3a5fd71f5596ad7437394597fc09f1b8e8ec73f2 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 13 Sep 2024 09:51:26 +0000 Subject: [PATCH 15/24] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index f00c3fed3..9370a1f3f 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Fixes + +* 🐛 Fix form field regression with `alias`. PR [#12194](https://github.com/fastapi/fastapi/pull/12194) by [@Wurstnase](https://github.com/Wurstnase). + ### Translations * 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/request-form-models.md`. PR [#12175](https://github.com/fastapi/fastapi/pull/12175) by [@ceb10n](https://github.com/ceb10n). From 2ada1615a338a415a0ad7a9b879a1e7c09b9cce6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 13 Sep 2024 22:46:33 +0200 Subject: [PATCH 16/24] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.114.?= =?UTF-8?q?2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 9370a1f3f..3f0b60fd3 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +## 0.114.2 + ### Fixes * 🐛 Fix form field regression with `alias`. PR [#12194](https://github.com/fastapi/fastapi/pull/12194) by [@Wurstnase](https://github.com/Wurstnase). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index c2ed4859a..3925d3603 100644 --- a/fastapi/__init__.py +++ b/fastapi/__init__.py @@ -1,6 +1,6 @@ """FastAPI framework, high performance, easy to learn, fast to code, ready for production""" -__version__ = "0.114.1" +__version__ = "0.114.2" from starlette import status as status From 8eb3c5621ff2b946e7dd7a1a7ffd709e27de9ac6 Mon Sep 17 00:00:00 2001 From: Rafael de Oliveira Marques Date: Sun, 15 Sep 2024 16:04:17 -0300 Subject: [PATCH 17/24] =?UTF-8?q?=F0=9F=8C=90=20Add=20Portuguese=20transla?= =?UTF-8?q?tion=20for=20`docs/pt/docs/advanced/security/http-basic-auth.md?= =?UTF-8?q?`=20(#12195)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../docs/advanced/security/http-basic-auth.md | 192 ++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 docs/pt/docs/advanced/security/http-basic-auth.md diff --git a/docs/pt/docs/advanced/security/http-basic-auth.md b/docs/pt/docs/advanced/security/http-basic-auth.md new file mode 100644 index 000000000..12b8ab01c --- /dev/null +++ b/docs/pt/docs/advanced/security/http-basic-auth.md @@ -0,0 +1,192 @@ +# HTTP Basic Auth + +Para os casos mais simples, você pode utilizar o HTTP Basic Auth. + +No HTTP Basic Auth, a aplicação espera um cabeçalho que contém um usuário e uma senha. + +Caso ela não receba, ela retorna um erro HTTP 401 "Unauthorized" (*Não Autorizado*). + +E retorna um cabeçalho `WWW-Authenticate` com o valor `Basic`, e um parâmetro opcional `realm`. + +Isso sinaliza ao navegador para mostrar o prompt integrado para um usuário e senha. + +Então, quando você digitar o usuário e senha, o navegador os envia automaticamente no cabeçalho. + +## HTTP Basic Auth Simples + +* Importe `HTTPBasic` e `HTTPBasicCredentials`. +* Crie um "esquema `security`" utilizando `HTTPBasic`. +* Utilize o `security` com uma dependência em sua *operação de rota*. +* Isso retorna um objeto do tipo `HTTPBasicCredentials`: + * Isto contém o `username` e o `password` enviado. + +//// tab | Python 3.9+ + +```Python hl_lines="4 8 12" +{!> ../../../docs_src/security/tutorial006_an_py39.py!} +``` + +//// + +//// tab | Python 3.8+ + +```Python hl_lines="2 7 11" +{!> ../../../docs_src/security/tutorial006_an.py!} +``` + +//// + +//// tab | Python 3.8+ non-Annotated + +/// tip | "Dica" + +Prefira utilizar a versão `Annotated` se possível. + +/// + +```Python hl_lines="2 6 10" +{!> ../../../docs_src/security/tutorial006.py!} +``` + +//// + +Quando você tentar abrir a URL pela primeira vez (ou clicar no botão "Executar" nos documentos) o navegador vai pedir pelo seu usuário e senha: + + + +## Verifique o usuário + +Aqui está um exemplo mais completo. + +Utilize uma dependência para verificar se o usuário e a senha estão corretos. + +Para isso, utilize o módulo padrão do Python `secrets` para verificar o usuário e senha. + +O `secrets.compare_digest()` necessita receber `bytes` ou `str` que possuem apenas caracteres ASCII (os em Inglês). Isso significa que não funcionaria com caracteres como o `á`, como em `Sebastián`. + +Para lidar com isso, primeiramente nós convertemos o `username` e o `password` para `bytes`, codificando-os com UTF-8. + +Então nós podemos utilizar o `secrets.compare_digest()` para garantir que o `credentials.username` é `"stanleyjobson"`, e que o `credentials.password` é `"swordfish"`. + +//// tab | Python 3.9+ + +```Python hl_lines="1 12-24" +{!> ../../../docs_src/security/tutorial007_an_py39.py!} +``` + +//// + +//// tab | Python 3.8+ + +```Python hl_lines="1 12-24" +{!> ../../../docs_src/security/tutorial007_an.py!} +``` + +//// + +//// tab | Python 3.8+ non-Annotated + +/// tip | "Dica" + +Prefira utilizar a versão `Annotated` se possível. + +/// + +```Python hl_lines="1 11-21" +{!> ../../../docs_src/security/tutorial007.py!} +``` + +//// + +Isso seria parecido com: + +```Python +if not (credentials.username == "stanleyjobson") or not (credentials.password == "swordfish"): + # Return some error + ... +``` + +Porém ao utilizar o `secrets.compare_digest()`, isso estará seguro contra um tipo de ataque chamado "ataque de temporização (timing attacks)". + +### Ataques de Temporização + +Mas o que é um "ataque de temporização"? + +Vamos imaginar que alguns invasores estão tentando adivinhar o usuário e a senha. + +E eles enviam uma requisição com um usuário `johndoe` e uma senha `love123`. + +Então o código Python em sua aplicação seria equivalente a algo como: + +```Python +if "johndoe" == "stanleyjobson" and "love123" == "swordfish": + ... +``` + +Mas no exato momento que o Python compara o primeiro `j` em `johndoe` contra o primeiro `s` em `stanleyjobson`, ele retornará `False`, porque ele já sabe que aquelas duas strings não são a mesma, pensando que "não existe a necessidade de desperdiçar mais poder computacional comparando o resto das letras". E a sua aplicação dirá "Usuário ou senha incorretos". + +Mas então os invasores vão tentar com o usuário `stanleyjobsox` e a senha `love123`. + +E a sua aplicação faz algo como: + +```Python +if "stanleyjobsox" == "stanleyjobson" and "love123" == "swordfish": + ... +``` + +O Python terá que comparar todo o `stanleyjobso` tanto em `stanleyjobsox` como em `stanleyjobson` antes de perceber que as strings não são a mesma. Então isso levará alguns microsegundos a mais para retornar "Usuário ou senha incorretos". + +#### O tempo para responder ajuda os invasores + +Neste ponto, ao perceber que o servidor demorou alguns microsegundos a mais para enviar o retorno "Usuário ou senha incorretos", os invasores irão saber que eles acertaram _alguma coisa_, algumas das letras iniciais estavam certas. + +E eles podem tentar de novo sabendo que provavelmente é algo mais parecido com `stanleyjobsox` do que com `johndoe`. + +#### Um ataque "profissional" + +Claro, os invasores não tentariam tudo isso de forma manual, eles escreveriam um programa para fazer isso, possivelmente com milhares ou milhões de testes por segundo. E obteriam apenas uma letra a mais por vez. + +Mas fazendo isso, em alguns minutos ou horas os invasores teriam adivinhado o usuário e senha corretos, com a "ajuda" da nossa aplicação, apenas usando o tempo levado para responder. + +#### Corrija com o `secrets.compare_digest()` + +Mas em nosso código nós estamos utilizando o `secrets.compare_digest()`. + +Resumindo, levará o mesmo tempo para comparar `stanleyjobsox` com `stanleyjobson` do que comparar `johndoe` com `stanleyjobson`. E o mesmo para a senha. + +Deste modo, ao utilizar `secrets.compare_digest()` no código de sua aplicação, ela esterá a salvo contra toda essa gama de ataques de segurança. + + +### Retorne o erro + +Depois de detectar que as credenciais estão incorretas, retorne um `HTTPException` com o status 401 (o mesmo retornado quando nenhuma credencial foi informada) e adicione o cabeçalho `WWW-Authenticate` para fazer com que o navegador mostre o prompt de login novamente: + +//// tab | Python 3.9+ + +```Python hl_lines="26-30" +{!> ../../../docs_src/security/tutorial007_an_py39.py!} +``` + +//// + +//// tab | Python 3.8+ + +```Python hl_lines="26-30" +{!> ../../../docs_src/security/tutorial007_an.py!} +``` + +//// + +//// tab | Python 3.8+ non-Annotated + +/// tip | "Dica" + +Prefira utilizar a versão `Annotated` se possível. + +/// + +```Python hl_lines="23-27" +{!> ../../../docs_src/security/tutorial007.py!} +``` + +//// From 35df20c79c8ce482c314934098e8875cf011c56e Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 15 Sep 2024 19:04:38 +0000 Subject: [PATCH 18/24] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 3f0b60fd3..7f5e86b30 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Translations + +* 🌐 Add Portuguese translation for `docs/pt/docs/advanced/security/http-basic-auth.md`. PR [#12195](https://github.com/fastapi/fastapi/pull/12195) by [@ceb10n](https://github.com/ceb10n). + ## 0.114.2 ### Fixes From 4b2b14a8e89a46a3c37dbf49746c5cd0e6f678f2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 17 Sep 2024 00:00:09 +0200 Subject: [PATCH 19/24] =?UTF-8?q?=E2=AC=86=20[pre-commit.ci]=20pre-commit?= =?UTF-8?q?=20autoupdate=20(#12204)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.6.4 → v0.6.5](https://github.com/astral-sh/ruff-pre-commit/compare/v0.6.4...v0.6.5) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f74816f12..4b1b10a68 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.6.4 + rev: v0.6.5 hooks: - id: ruff args: From 0903da78c9940b094e13732264b5e462a426e5cc Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 16 Sep 2024 22:00:35 +0000 Subject: [PATCH 20/24] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 7f5e86b30..d6d2a05b3 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -11,6 +11,10 @@ hide: * 🌐 Add Portuguese translation for `docs/pt/docs/advanced/security/http-basic-auth.md`. PR [#12195](https://github.com/fastapi/fastapi/pull/12195) by [@ceb10n](https://github.com/ceb10n). +### Internal + +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#12204](https://github.com/fastapi/fastapi/pull/12204) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). + ## 0.114.2 ### Fixes From 55035f440bf852f739e3ccd71b67034016ae9bba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 17 Sep 2024 20:54:10 +0200 Subject: [PATCH 21/24] =?UTF-8?q?=E2=9C=A8=20Add=20support=20for=20Pydanti?= =?UTF-8?q?c=20models=20for=20parameters=20using=20`Query`,=20`Cookie`,=20?= =?UTF-8?q?`Header`=20(#12199)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tutorial/cookie-param-models/image01.png | Bin 0 -> 45217 bytes .../tutorial/header-param-models/image01.png | Bin 0 -> 62257 bytes .../tutorial/query-param-models/image01.png | Bin 0 -> 45571 bytes docs/en/docs/tutorial/cookie-param-models.md | 154 ++++++++++ docs/en/docs/tutorial/header-param-models.md | 184 ++++++++++++ docs/en/docs/tutorial/query-param-models.md | 196 ++++++++++++ docs/en/mkdocs.yml | 3 + docs_src/cookie_param_models/tutorial001.py | 17 ++ .../cookie_param_models/tutorial001_an.py | 18 ++ .../tutorial001_an_py310.py | 17 ++ .../tutorial001_an_py39.py | 17 ++ .../cookie_param_models/tutorial001_py310.py | 15 + docs_src/cookie_param_models/tutorial002.py | 19 ++ .../cookie_param_models/tutorial002_an.py | 20 ++ .../tutorial002_an_py310.py | 19 ++ .../tutorial002_an_py39.py | 19 ++ .../cookie_param_models/tutorial002_pv1.py | 20 ++ .../cookie_param_models/tutorial002_pv1_an.py | 21 ++ .../tutorial002_pv1_an_py310.py | 20 ++ .../tutorial002_pv1_an_py39.py | 20 ++ .../tutorial002_pv1_py310.py | 18 ++ .../cookie_param_models/tutorial002_py310.py | 17 ++ docs_src/header_param_models/tutorial001.py | 19 ++ .../header_param_models/tutorial001_an.py | 20 ++ .../tutorial001_an_py310.py | 19 ++ .../tutorial001_an_py39.py | 19 ++ .../header_param_models/tutorial001_py310.py | 17 ++ .../header_param_models/tutorial001_py39.py | 19 ++ docs_src/header_param_models/tutorial002.py | 21 ++ .../header_param_models/tutorial002_an.py | 22 ++ .../tutorial002_an_py310.py | 21 ++ .../tutorial002_an_py39.py | 21 ++ .../header_param_models/tutorial002_pv1.py | 22 ++ .../header_param_models/tutorial002_pv1_an.py | 23 ++ .../tutorial002_pv1_an_py310.py | 22 ++ .../tutorial002_pv1_an_py39.py | 22 ++ .../tutorial002_pv1_py310.py | 20 ++ .../tutorial002_pv1_py39.py | 22 ++ .../header_param_models/tutorial002_py310.py | 19 ++ .../header_param_models/tutorial002_py39.py | 21 ++ docs_src/query_param_models/tutorial001.py | 19 ++ docs_src/query_param_models/tutorial001_an.py | 19 ++ .../tutorial001_an_py310.py | 18 ++ .../query_param_models/tutorial001_an_py39.py | 17 ++ .../query_param_models/tutorial001_py310.py | 18 ++ .../query_param_models/tutorial001_py39.py | 17 ++ docs_src/query_param_models/tutorial002.py | 21 ++ docs_src/query_param_models/tutorial002_an.py | 21 ++ .../tutorial002_an_py310.py | 20 ++ .../query_param_models/tutorial002_an_py39.py | 19 ++ .../query_param_models/tutorial002_pv1.py | 22 ++ .../query_param_models/tutorial002_pv1_an.py | 22 ++ .../tutorial002_pv1_an_py310.py | 21 ++ .../tutorial002_pv1_an_py39.py | 20 ++ .../tutorial002_pv1_py310.py | 21 ++ .../tutorial002_pv1_py39.py | 20 ++ .../query_param_models/tutorial002_py310.py | 20 ++ .../query_param_models/tutorial002_py39.py | 19 ++ fastapi/dependencies/utils.py | 90 +++++- fastapi/openapi/utils.py | 86 +++--- .../playwright/cookie_param_models/image01.py | 39 +++ .../playwright/header_param_models/image01.py | 38 +++ .../playwright/query_param_models/image01.py | 41 +++ .../test_cookie_param_models/__init__.py | 0 .../test_tutorial001.py | 205 +++++++++++++ .../test_tutorial002.py | 233 +++++++++++++++ .../test_header_param_models/__init__.py | 0 .../test_tutorial001.py | 238 +++++++++++++++ .../test_tutorial002.py | 249 ++++++++++++++++ .../test_query_param_models/__init__.py | 0 .../test_tutorial001.py | 260 ++++++++++++++++ .../test_tutorial002.py | 282 ++++++++++++++++++ 72 files changed, 3253 insertions(+), 45 deletions(-) create mode 100644 docs/en/docs/img/tutorial/cookie-param-models/image01.png create mode 100644 docs/en/docs/img/tutorial/header-param-models/image01.png create mode 100644 docs/en/docs/img/tutorial/query-param-models/image01.png create mode 100644 docs/en/docs/tutorial/cookie-param-models.md create mode 100644 docs/en/docs/tutorial/header-param-models.md create mode 100644 docs/en/docs/tutorial/query-param-models.md create mode 100644 docs_src/cookie_param_models/tutorial001.py create mode 100644 docs_src/cookie_param_models/tutorial001_an.py create mode 100644 docs_src/cookie_param_models/tutorial001_an_py310.py create mode 100644 docs_src/cookie_param_models/tutorial001_an_py39.py create mode 100644 docs_src/cookie_param_models/tutorial001_py310.py create mode 100644 docs_src/cookie_param_models/tutorial002.py create mode 100644 docs_src/cookie_param_models/tutorial002_an.py create mode 100644 docs_src/cookie_param_models/tutorial002_an_py310.py create mode 100644 docs_src/cookie_param_models/tutorial002_an_py39.py create mode 100644 docs_src/cookie_param_models/tutorial002_pv1.py create mode 100644 docs_src/cookie_param_models/tutorial002_pv1_an.py create mode 100644 docs_src/cookie_param_models/tutorial002_pv1_an_py310.py create mode 100644 docs_src/cookie_param_models/tutorial002_pv1_an_py39.py create mode 100644 docs_src/cookie_param_models/tutorial002_pv1_py310.py create mode 100644 docs_src/cookie_param_models/tutorial002_py310.py create mode 100644 docs_src/header_param_models/tutorial001.py create mode 100644 docs_src/header_param_models/tutorial001_an.py create mode 100644 docs_src/header_param_models/tutorial001_an_py310.py create mode 100644 docs_src/header_param_models/tutorial001_an_py39.py create mode 100644 docs_src/header_param_models/tutorial001_py310.py create mode 100644 docs_src/header_param_models/tutorial001_py39.py create mode 100644 docs_src/header_param_models/tutorial002.py create mode 100644 docs_src/header_param_models/tutorial002_an.py create mode 100644 docs_src/header_param_models/tutorial002_an_py310.py create mode 100644 docs_src/header_param_models/tutorial002_an_py39.py create mode 100644 docs_src/header_param_models/tutorial002_pv1.py create mode 100644 docs_src/header_param_models/tutorial002_pv1_an.py create mode 100644 docs_src/header_param_models/tutorial002_pv1_an_py310.py create mode 100644 docs_src/header_param_models/tutorial002_pv1_an_py39.py create mode 100644 docs_src/header_param_models/tutorial002_pv1_py310.py create mode 100644 docs_src/header_param_models/tutorial002_pv1_py39.py create mode 100644 docs_src/header_param_models/tutorial002_py310.py create mode 100644 docs_src/header_param_models/tutorial002_py39.py create mode 100644 docs_src/query_param_models/tutorial001.py create mode 100644 docs_src/query_param_models/tutorial001_an.py create mode 100644 docs_src/query_param_models/tutorial001_an_py310.py create mode 100644 docs_src/query_param_models/tutorial001_an_py39.py create mode 100644 docs_src/query_param_models/tutorial001_py310.py create mode 100644 docs_src/query_param_models/tutorial001_py39.py create mode 100644 docs_src/query_param_models/tutorial002.py create mode 100644 docs_src/query_param_models/tutorial002_an.py create mode 100644 docs_src/query_param_models/tutorial002_an_py310.py create mode 100644 docs_src/query_param_models/tutorial002_an_py39.py create mode 100644 docs_src/query_param_models/tutorial002_pv1.py create mode 100644 docs_src/query_param_models/tutorial002_pv1_an.py create mode 100644 docs_src/query_param_models/tutorial002_pv1_an_py310.py create mode 100644 docs_src/query_param_models/tutorial002_pv1_an_py39.py create mode 100644 docs_src/query_param_models/tutorial002_pv1_py310.py create mode 100644 docs_src/query_param_models/tutorial002_pv1_py39.py create mode 100644 docs_src/query_param_models/tutorial002_py310.py create mode 100644 docs_src/query_param_models/tutorial002_py39.py create mode 100644 scripts/playwright/cookie_param_models/image01.py create mode 100644 scripts/playwright/header_param_models/image01.py create mode 100644 scripts/playwright/query_param_models/image01.py create mode 100644 tests/test_tutorial/test_cookie_param_models/__init__.py create mode 100644 tests/test_tutorial/test_cookie_param_models/test_tutorial001.py create mode 100644 tests/test_tutorial/test_cookie_param_models/test_tutorial002.py create mode 100644 tests/test_tutorial/test_header_param_models/__init__.py create mode 100644 tests/test_tutorial/test_header_param_models/test_tutorial001.py create mode 100644 tests/test_tutorial/test_header_param_models/test_tutorial002.py create mode 100644 tests/test_tutorial/test_query_param_models/__init__.py create mode 100644 tests/test_tutorial/test_query_param_models/test_tutorial001.py create mode 100644 tests/test_tutorial/test_query_param_models/test_tutorial002.py diff --git a/docs/en/docs/img/tutorial/cookie-param-models/image01.png b/docs/en/docs/img/tutorial/cookie-param-models/image01.png new file mode 100644 index 0000000000000000000000000000000000000000..85c370f80a42e52768a412c066e686e4f2773ccd GIT binary patch literal 45217 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

CT>@y??8 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 TT)vm_&h-i@{l=?q3#J@QQqf?x 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 vlA(&L8i`zQ6{%`DkNA)w<%j ../../../docs_src/cookie_param_models/tutorial001_an_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="9-12 16" +{!> ../../../docs_src/cookie_param_models/tutorial001_an_py39.py!} +``` + +//// + +//// tab | Python 3.8+ + +```Python hl_lines="10-13 17" +{!> ../../../docs_src/cookie_param_models/tutorial001_an.py!} +``` + +//// + +//// tab | Python 3.10+ non-Annotated + +/// tip + +Prefer to use the `Annotated` version if possible. + +/// + +```Python hl_lines="7-10 14" +{!> ../../../docs_src/cookie_param_models/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.8+ non-Annotated + +/// tip + +Prefer to use the `Annotated` version if possible. + +/// + +```Python hl_lines="9-12 16" +{!> ../../../docs_src/cookie_param_models/tutorial001.py!} +``` + +//// + +**FastAPI** will **extract** the data for **each field** from the **cookies** received in the request and give you the Pydantic model you defined. + +## Check the Docs + +You can see the defined cookies in the docs UI at `/docs`: + +

+ +/// info + +Have in mind that, as **browsers handle cookies** in special ways and behind the scenes, they **don't** easily allow **JavaScript** to touch them. + +If you go to the **API docs UI** at `/docs` you will be able to see the **documentation** for cookies for your *path operations*. + +But even if you **fill the data** and click "Execute", because the docs UI works with **JavaScript**, the cookies won't be sent, and you will see an **error** message as if you didn't write any values. + +/// + +## Forbid Extra Cookies + +In some special use cases (probably not very common), you might want to **restrict** the cookies that you want to receive. + +Your API now has the power to control its own cookie consent. 🤪🍪 + +You can use Pydantic's model configuration to `forbid` any `extra` fields: + +//// tab | Python 3.9+ + +```Python hl_lines="10" +{!> ../../../docs_src/cookie_param_models/tutorial002_an_py39.py!} +``` + +//// + +//// tab | Python 3.8+ + +```Python hl_lines="11" +{!> ../../../docs_src/cookie_param_models/tutorial002_an.py!} +``` + +//// + +//// tab | Python 3.8+ non-Annotated + +/// tip + +Prefer to use the `Annotated` version if possible. + +/// + +```Python hl_lines="10" +{!> ../../../docs_src/cookie_param_models/tutorial002.py!} +``` + +//// + +If a client tries to send some **extra cookies**, they will receive an **error** response. + +Poor cookie banners with all their effort to get your consent for the API to reject it. 🍪 + +For example, if the client tries to send a `santa_tracker` cookie with a value of `good-list-please`, the client will receive an **error** response telling them that the `santa_tracker` cookie is not allowed: + +```json +{ + "detail": [ + { + "type": "extra_forbidden", + "loc": ["cookie", "santa_tracker"], + "msg": "Extra inputs are not permitted", + "input": "good-list-please", + } + ] +} +``` + +## Summary + +You can use **Pydantic models** to declare **cookies** in **FastAPI**. 😎 diff --git a/docs/en/docs/tutorial/header-param-models.md b/docs/en/docs/tutorial/header-param-models.md new file mode 100644 index 000000000..8deb0a455 --- /dev/null +++ b/docs/en/docs/tutorial/header-param-models.md @@ -0,0 +1,184 @@ +# Header Parameter Models + +If you have a group of related **header parameters**, you can create a **Pydantic model** to declare them. + +This would allow you to **re-use the model** in **multiple places** and also to declare validations and metadata for all the parameters at once. 😎 + +/// note + +This is supported since FastAPI version `0.115.0`. 🤓 + +/// + +## Header Parameters with a Pydantic Model + +Declare the **header parameters** that you need in a **Pydantic model**, and then declare the parameter as `Header`: + +//// tab | Python 3.10+ + +```Python hl_lines="9-14 18" +{!> ../../../docs_src/header_param_models/tutorial001_an_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="9-14 18" +{!> ../../../docs_src/header_param_models/tutorial001_an_py39.py!} +``` + +//// + +//// tab | Python 3.8+ + +```Python hl_lines="10-15 19" +{!> ../../../docs_src/header_param_models/tutorial001_an.py!} +``` + +//// + +//// tab | Python 3.10+ non-Annotated + +/// tip + +Prefer to use the `Annotated` version if possible. + +/// + +```Python hl_lines="7-12 16" +{!> ../../../docs_src/header_param_models/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.9+ non-Annotated + +/// tip + +Prefer to use the `Annotated` version if possible. + +/// + +```Python hl_lines="9-14 18" +{!> ../../../docs_src/header_param_models/tutorial001_py39.py!} +``` + +//// + +//// tab | Python 3.8+ non-Annotated + +/// tip + +Prefer to use the `Annotated` version if possible. + +/// + +```Python hl_lines="7-12 16" +{!> ../../../docs_src/header_param_models/tutorial001_py310.py!} +``` + +//// + +**FastAPI** will **extract** the data for **each field** from the **headers** in the request and give you the Pydantic model you defined. + +## Check the Docs + +You can see the required headers in the docs UI at `/docs`: + +
+ +
+ +## Forbid Extra Headers + +In some special use cases (probably not very common), you might want to **restrict** the headers that you want to receive. + +You can use Pydantic's model configuration to `forbid` any `extra` fields: + +//// tab | Python 3.10+ + +```Python hl_lines="10" +{!> ../../../docs_src/header_param_models/tutorial002_an_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="10" +{!> ../../../docs_src/header_param_models/tutorial002_an_py39.py!} +``` + +//// + +//// tab | Python 3.8+ + +```Python hl_lines="11" +{!> ../../../docs_src/header_param_models/tutorial002_an.py!} +``` + +//// + +//// tab | Python 3.10+ non-Annotated + +/// tip + +Prefer to use the `Annotated` version if possible. + +/// + +```Python hl_lines="8" +{!> ../../../docs_src/header_param_models/tutorial002_py310.py!} +``` + +//// + +//// tab | Python 3.9+ non-Annotated + +/// tip + +Prefer to use the `Annotated` version if possible. + +/// + +```Python hl_lines="10" +{!> ../../../docs_src/header_param_models/tutorial002_py39.py!} +``` + +//// + +//// tab | Python 3.8+ non-Annotated + +/// tip + +Prefer to use the `Annotated` version if possible. + +/// + +```Python hl_lines="10" +{!> ../../../docs_src/header_param_models/tutorial002.py!} +``` + +//// + +If a client tries to send some **extra headers**, they will receive an **error** response. + +For example, if the client tries to send a `tool` header with a value of `plumbus`, they will receive an **error** response telling them that the header parameter `tool` is not allowed: + +```json +{ + "detail": [ + { + "type": "extra_forbidden", + "loc": ["header", "tool"], + "msg": "Extra inputs are not permitted", + "input": "plumbus", + } + ] +} +``` + +## Summary + +You can use **Pydantic models** to declare **headers** in **FastAPI**. 😎 diff --git a/docs/en/docs/tutorial/query-param-models.md b/docs/en/docs/tutorial/query-param-models.md new file mode 100644 index 000000000..02e36dc0f --- /dev/null +++ b/docs/en/docs/tutorial/query-param-models.md @@ -0,0 +1,196 @@ +# Query Parameter Models + +If you have a group of **query parameters** that are related, you can create a **Pydantic model** to declare them. + +This would allow you to **re-use the model** in **multiple places** and also to declare validations and metadata for all the parameters at once. 😎 + +/// note + +This is supported since FastAPI version `0.115.0`. 🤓 + +/// + +## Query Parameters with a Pydantic Model + +Declare the **query parameters** that you need in a **Pydantic model**, and then declare the parameter as `Query`: + +//// tab | Python 3.10+ + +```Python hl_lines="9-13 17" +{!> ../../../docs_src/query_param_models/tutorial001_an_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="8-12 16" +{!> ../../../docs_src/query_param_models/tutorial001_an_py39.py!} +``` + +//// + +//// tab | Python 3.8+ + +```Python hl_lines="10-14 18" +{!> ../../../docs_src/query_param_models/tutorial001_an.py!} +``` + +//// + +//// tab | Python 3.10+ non-Annotated + +/// tip + +Prefer to use the `Annotated` version if possible. + +/// + +```Python hl_lines="9-13 17" +{!> ../../../docs_src/query_param_models/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.9+ non-Annotated + +/// tip + +Prefer to use the `Annotated` version if possible. + +/// + +```Python hl_lines="8-12 16" +{!> ../../../docs_src/query_param_models/tutorial001_py39.py!} +``` + +//// + +//// tab | Python 3.8+ non-Annotated + +/// tip + +Prefer to use the `Annotated` version if possible. + +/// + +```Python hl_lines="9-13 17" +{!> ../../../docs_src/query_param_models/tutorial001_py310.py!} +``` + +//// + +**FastAPI** will **extract** the data for **each field** from the **query parameters** in the request and give you the Pydantic model you defined. + +## Check the Docs + +You can see the query parameters in the docs UI at `/docs`: + +
+ +
+ +## Forbid Extra Query Parameters + +In some special use cases (probably not very common), you might want to **restrict** the query parameters that you want to receive. + +You can use Pydantic's model configuration to `forbid` any `extra` fields: + +//// tab | Python 3.10+ + +```Python hl_lines="10" +{!> ../../../docs_src/query_param_models/tutorial002_an_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="9" +{!> ../../../docs_src/query_param_models/tutorial002_an_py39.py!} +``` + +//// + +//// tab | Python 3.8+ + +```Python hl_lines="11" +{!> ../../../docs_src/query_param_models/tutorial002_an.py!} +``` + +//// + +//// tab | Python 3.10+ non-Annotated + +/// tip + +Prefer to use the `Annotated` version if possible. + +/// + +```Python hl_lines="10" +{!> ../../../docs_src/query_param_models/tutorial002_py310.py!} +``` + +//// + +//// tab | Python 3.9+ non-Annotated + +/// tip + +Prefer to use the `Annotated` version if possible. + +/// + +```Python hl_lines="9" +{!> ../../../docs_src/query_param_models/tutorial002_py39.py!} +``` + +//// + +//// tab | Python 3.8+ non-Annotated + +/// tip + +Prefer to use the `Annotated` version if possible. + +/// + +```Python hl_lines="11" +{!> ../../../docs_src/query_param_models/tutorial002.py!} +``` + +//// + +If a client tries to send some **extra** data in the **query parameters**, they will receive an **error** response. + +For example, if the client tries to send a `tool` query parameter with a value of `plumbus`, like: + +```http +https://example.com/items/?limit=10&tool=plumbus +``` + +They will receive an **error** response telling them that the query parameter `tool` is not allowed: + +```json +{ + "detail": [ + { + "type": "extra_forbidden", + "loc": ["query", "tool"], + "msg": "Extra inputs are not permitted", + "input": "plumbus" + } + ] +} +``` + +## Summary + +You can use **Pydantic models** to declare **query parameters** in **FastAPI**. 😎 + +/// tip + +Spoiler alert: you can also use Pydantic models to declare cookies and headers, but you will read about that later in the tutorial. 🤫 + +/// diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index 7c810c2d7..5161b891b 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -118,6 +118,7 @@ nav: - tutorial/body.md - tutorial/query-params-str-validations.md - tutorial/path-params-numeric-validations.md + - tutorial/query-param-models.md - tutorial/body-multiple-params.md - tutorial/body-fields.md - tutorial/body-nested-models.md @@ -125,6 +126,8 @@ nav: - tutorial/extra-data-types.md - tutorial/cookie-params.md - tutorial/header-params.md + - tutorial/cookie-param-models.md + - tutorial/header-param-models.md - tutorial/response-model.md - tutorial/extra-models.md - tutorial/response-status-code.md diff --git a/docs_src/cookie_param_models/tutorial001.py b/docs_src/cookie_param_models/tutorial001.py new file mode 100644 index 000000000..cc65c43e1 --- /dev/null +++ b/docs_src/cookie_param_models/tutorial001.py @@ -0,0 +1,17 @@ +from typing import Union + +from fastapi import Cookie, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Cookies(BaseModel): + session_id: str + fatebook_tracker: Union[str, None] = None + googall_tracker: Union[str, None] = None + + +@app.get("/items/") +async def read_items(cookies: Cookies = Cookie()): + return cookies diff --git a/docs_src/cookie_param_models/tutorial001_an.py b/docs_src/cookie_param_models/tutorial001_an.py new file mode 100644 index 000000000..e5839ffd5 --- /dev/null +++ b/docs_src/cookie_param_models/tutorial001_an.py @@ -0,0 +1,18 @@ +from typing import Union + +from fastapi import Cookie, FastAPI +from pydantic import BaseModel +from typing_extensions import Annotated + +app = FastAPI() + + +class Cookies(BaseModel): + session_id: str + fatebook_tracker: Union[str, None] = None + googall_tracker: Union[str, None] = None + + +@app.get("/items/") +async def read_items(cookies: Annotated[Cookies, Cookie()]): + return cookies diff --git a/docs_src/cookie_param_models/tutorial001_an_py310.py b/docs_src/cookie_param_models/tutorial001_an_py310.py new file mode 100644 index 000000000..24cc889a9 --- /dev/null +++ b/docs_src/cookie_param_models/tutorial001_an_py310.py @@ -0,0 +1,17 @@ +from typing import Annotated + +from fastapi import Cookie, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Cookies(BaseModel): + session_id: str + fatebook_tracker: str | None = None + googall_tracker: str | None = None + + +@app.get("/items/") +async def read_items(cookies: Annotated[Cookies, Cookie()]): + return cookies diff --git a/docs_src/cookie_param_models/tutorial001_an_py39.py b/docs_src/cookie_param_models/tutorial001_an_py39.py new file mode 100644 index 000000000..3d90c2007 --- /dev/null +++ b/docs_src/cookie_param_models/tutorial001_an_py39.py @@ -0,0 +1,17 @@ +from typing import Annotated, Union + +from fastapi import Cookie, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Cookies(BaseModel): + session_id: str + fatebook_tracker: Union[str, None] = None + googall_tracker: Union[str, None] = None + + +@app.get("/items/") +async def read_items(cookies: Annotated[Cookies, Cookie()]): + return cookies diff --git a/docs_src/cookie_param_models/tutorial001_py310.py b/docs_src/cookie_param_models/tutorial001_py310.py new file mode 100644 index 000000000..7cdee5a92 --- /dev/null +++ b/docs_src/cookie_param_models/tutorial001_py310.py @@ -0,0 +1,15 @@ +from fastapi import Cookie, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Cookies(BaseModel): + session_id: str + fatebook_tracker: str | None = None + googall_tracker: str | None = None + + +@app.get("/items/") +async def read_items(cookies: Cookies = Cookie()): + return cookies diff --git a/docs_src/cookie_param_models/tutorial002.py b/docs_src/cookie_param_models/tutorial002.py new file mode 100644 index 000000000..9679e890f --- /dev/null +++ b/docs_src/cookie_param_models/tutorial002.py @@ -0,0 +1,19 @@ +from typing import Union + +from fastapi import Cookie, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Cookies(BaseModel): + model_config = {"extra": "forbid"} + + session_id: str + fatebook_tracker: Union[str, None] = None + googall_tracker: Union[str, None] = None + + +@app.get("/items/") +async def read_items(cookies: Cookies = Cookie()): + return cookies diff --git a/docs_src/cookie_param_models/tutorial002_an.py b/docs_src/cookie_param_models/tutorial002_an.py new file mode 100644 index 000000000..ce5644b7b --- /dev/null +++ b/docs_src/cookie_param_models/tutorial002_an.py @@ -0,0 +1,20 @@ +from typing import Union + +from fastapi import Cookie, FastAPI +from pydantic import BaseModel +from typing_extensions import Annotated + +app = FastAPI() + + +class Cookies(BaseModel): + model_config = {"extra": "forbid"} + + session_id: str + fatebook_tracker: Union[str, None] = None + googall_tracker: Union[str, None] = None + + +@app.get("/items/") +async def read_items(cookies: Annotated[Cookies, Cookie()]): + return cookies diff --git a/docs_src/cookie_param_models/tutorial002_an_py310.py b/docs_src/cookie_param_models/tutorial002_an_py310.py new file mode 100644 index 000000000..7fa70fe92 --- /dev/null +++ b/docs_src/cookie_param_models/tutorial002_an_py310.py @@ -0,0 +1,19 @@ +from typing import Annotated + +from fastapi import Cookie, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Cookies(BaseModel): + model_config = {"extra": "forbid"} + + session_id: str + fatebook_tracker: str | None = None + googall_tracker: str | None = None + + +@app.get("/items/") +async def read_items(cookies: Annotated[Cookies, Cookie()]): + return cookies diff --git a/docs_src/cookie_param_models/tutorial002_an_py39.py b/docs_src/cookie_param_models/tutorial002_an_py39.py new file mode 100644 index 000000000..a906ce6a1 --- /dev/null +++ b/docs_src/cookie_param_models/tutorial002_an_py39.py @@ -0,0 +1,19 @@ +from typing import Annotated, Union + +from fastapi import Cookie, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Cookies(BaseModel): + model_config = {"extra": "forbid"} + + session_id: str + fatebook_tracker: Union[str, None] = None + googall_tracker: Union[str, None] = None + + +@app.get("/items/") +async def read_items(cookies: Annotated[Cookies, Cookie()]): + return cookies diff --git a/docs_src/cookie_param_models/tutorial002_pv1.py b/docs_src/cookie_param_models/tutorial002_pv1.py new file mode 100644 index 000000000..13f78b850 --- /dev/null +++ b/docs_src/cookie_param_models/tutorial002_pv1.py @@ -0,0 +1,20 @@ +from typing import Union + +from fastapi import Cookie, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Cookies(BaseModel): + class Config: + extra = "forbid" + + session_id: str + fatebook_tracker: Union[str, None] = None + googall_tracker: Union[str, None] = None + + +@app.get("/items/") +async def read_items(cookies: Cookies = Cookie()): + return cookies diff --git a/docs_src/cookie_param_models/tutorial002_pv1_an.py b/docs_src/cookie_param_models/tutorial002_pv1_an.py new file mode 100644 index 000000000..ddfda9b6f --- /dev/null +++ b/docs_src/cookie_param_models/tutorial002_pv1_an.py @@ -0,0 +1,21 @@ +from typing import Union + +from fastapi import Cookie, FastAPI +from pydantic import BaseModel +from typing_extensions import Annotated + +app = FastAPI() + + +class Cookies(BaseModel): + class Config: + extra = "forbid" + + session_id: str + fatebook_tracker: Union[str, None] = None + googall_tracker: Union[str, None] = None + + +@app.get("/items/") +async def read_items(cookies: Annotated[Cookies, Cookie()]): + return cookies diff --git a/docs_src/cookie_param_models/tutorial002_pv1_an_py310.py b/docs_src/cookie_param_models/tutorial002_pv1_an_py310.py new file mode 100644 index 000000000..ac00360b6 --- /dev/null +++ b/docs_src/cookie_param_models/tutorial002_pv1_an_py310.py @@ -0,0 +1,20 @@ +from typing import Annotated + +from fastapi import Cookie, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Cookies(BaseModel): + class Config: + extra = "forbid" + + session_id: str + fatebook_tracker: str | None = None + googall_tracker: str | None = None + + +@app.get("/items/") +async def read_items(cookies: Annotated[Cookies, Cookie()]): + return cookies diff --git a/docs_src/cookie_param_models/tutorial002_pv1_an_py39.py b/docs_src/cookie_param_models/tutorial002_pv1_an_py39.py new file mode 100644 index 000000000..573caea4b --- /dev/null +++ b/docs_src/cookie_param_models/tutorial002_pv1_an_py39.py @@ -0,0 +1,20 @@ +from typing import Annotated, Union + +from fastapi import Cookie, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Cookies(BaseModel): + class Config: + extra = "forbid" + + session_id: str + fatebook_tracker: Union[str, None] = None + googall_tracker: Union[str, None] = None + + +@app.get("/items/") +async def read_items(cookies: Annotated[Cookies, Cookie()]): + return cookies diff --git a/docs_src/cookie_param_models/tutorial002_pv1_py310.py b/docs_src/cookie_param_models/tutorial002_pv1_py310.py new file mode 100644 index 000000000..2c59aad12 --- /dev/null +++ b/docs_src/cookie_param_models/tutorial002_pv1_py310.py @@ -0,0 +1,18 @@ +from fastapi import Cookie, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Cookies(BaseModel): + class Config: + extra = "forbid" + + session_id: str + fatebook_tracker: str | None = None + googall_tracker: str | None = None + + +@app.get("/items/") +async def read_items(cookies: Cookies = Cookie()): + return cookies diff --git a/docs_src/cookie_param_models/tutorial002_py310.py b/docs_src/cookie_param_models/tutorial002_py310.py new file mode 100644 index 000000000..f011aa1af --- /dev/null +++ b/docs_src/cookie_param_models/tutorial002_py310.py @@ -0,0 +1,17 @@ +from fastapi import Cookie, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Cookies(BaseModel): + model_config = {"extra": "forbid"} + + session_id: str + fatebook_tracker: str | None = None + googall_tracker: str | None = None + + +@app.get("/items/") +async def read_items(cookies: Cookies = Cookie()): + return cookies diff --git a/docs_src/header_param_models/tutorial001.py b/docs_src/header_param_models/tutorial001.py new file mode 100644 index 000000000..4caaba87b --- /dev/null +++ b/docs_src/header_param_models/tutorial001.py @@ -0,0 +1,19 @@ +from typing import List, Union + +from fastapi import FastAPI, Header +from pydantic import BaseModel + +app = FastAPI() + + +class CommonHeaders(BaseModel): + host: str + save_data: bool + if_modified_since: Union[str, None] = None + traceparent: Union[str, None] = None + x_tag: List[str] = [] + + +@app.get("/items/") +async def read_items(headers: CommonHeaders = Header()): + return headers diff --git a/docs_src/header_param_models/tutorial001_an.py b/docs_src/header_param_models/tutorial001_an.py new file mode 100644 index 000000000..b55c6b56b --- /dev/null +++ b/docs_src/header_param_models/tutorial001_an.py @@ -0,0 +1,20 @@ +from typing import List, Union + +from fastapi import FastAPI, Header +from pydantic import BaseModel +from typing_extensions import Annotated + +app = FastAPI() + + +class CommonHeaders(BaseModel): + host: str + save_data: bool + if_modified_since: Union[str, None] = None + traceparent: Union[str, None] = None + x_tag: List[str] = [] + + +@app.get("/items/") +async def read_items(headers: Annotated[CommonHeaders, Header()]): + return headers diff --git a/docs_src/header_param_models/tutorial001_an_py310.py b/docs_src/header_param_models/tutorial001_an_py310.py new file mode 100644 index 000000000..acfb6b9bf --- /dev/null +++ b/docs_src/header_param_models/tutorial001_an_py310.py @@ -0,0 +1,19 @@ +from typing import Annotated + +from fastapi import FastAPI, Header +from pydantic import BaseModel + +app = FastAPI() + + +class CommonHeaders(BaseModel): + host: str + save_data: bool + if_modified_since: str | None = None + traceparent: str | None = None + x_tag: list[str] = [] + + +@app.get("/items/") +async def read_items(headers: Annotated[CommonHeaders, Header()]): + return headers diff --git a/docs_src/header_param_models/tutorial001_an_py39.py b/docs_src/header_param_models/tutorial001_an_py39.py new file mode 100644 index 000000000..51a5f94fc --- /dev/null +++ b/docs_src/header_param_models/tutorial001_an_py39.py @@ -0,0 +1,19 @@ +from typing import Annotated, Union + +from fastapi import FastAPI, Header +from pydantic import BaseModel + +app = FastAPI() + + +class CommonHeaders(BaseModel): + host: str + save_data: bool + if_modified_since: Union[str, None] = None + traceparent: Union[str, None] = None + x_tag: list[str] = [] + + +@app.get("/items/") +async def read_items(headers: Annotated[CommonHeaders, Header()]): + return headers diff --git a/docs_src/header_param_models/tutorial001_py310.py b/docs_src/header_param_models/tutorial001_py310.py new file mode 100644 index 000000000..7239c64ce --- /dev/null +++ b/docs_src/header_param_models/tutorial001_py310.py @@ -0,0 +1,17 @@ +from fastapi import FastAPI, Header +from pydantic import BaseModel + +app = FastAPI() + + +class CommonHeaders(BaseModel): + host: str + save_data: bool + if_modified_since: str | None = None + traceparent: str | None = None + x_tag: list[str] = [] + + +@app.get("/items/") +async def read_items(headers: CommonHeaders = Header()): + return headers diff --git a/docs_src/header_param_models/tutorial001_py39.py b/docs_src/header_param_models/tutorial001_py39.py new file mode 100644 index 000000000..4c1137813 --- /dev/null +++ b/docs_src/header_param_models/tutorial001_py39.py @@ -0,0 +1,19 @@ +from typing import Union + +from fastapi import FastAPI, Header +from pydantic import BaseModel + +app = FastAPI() + + +class CommonHeaders(BaseModel): + host: str + save_data: bool + if_modified_since: Union[str, None] = None + traceparent: Union[str, None] = None + x_tag: list[str] = [] + + +@app.get("/items/") +async def read_items(headers: CommonHeaders = Header()): + return headers diff --git a/docs_src/header_param_models/tutorial002.py b/docs_src/header_param_models/tutorial002.py new file mode 100644 index 000000000..3f9aac58d --- /dev/null +++ b/docs_src/header_param_models/tutorial002.py @@ -0,0 +1,21 @@ +from typing import List, Union + +from fastapi import FastAPI, Header +from pydantic import BaseModel + +app = FastAPI() + + +class CommonHeaders(BaseModel): + model_config = {"extra": "forbid"} + + host: str + save_data: bool + if_modified_since: Union[str, None] = None + traceparent: Union[str, None] = None + x_tag: List[str] = [] + + +@app.get("/items/") +async def read_items(headers: CommonHeaders = Header()): + return headers diff --git a/docs_src/header_param_models/tutorial002_an.py b/docs_src/header_param_models/tutorial002_an.py new file mode 100644 index 000000000..771135d77 --- /dev/null +++ b/docs_src/header_param_models/tutorial002_an.py @@ -0,0 +1,22 @@ +from typing import List, Union + +from fastapi import FastAPI, Header +from pydantic import BaseModel +from typing_extensions import Annotated + +app = FastAPI() + + +class CommonHeaders(BaseModel): + model_config = {"extra": "forbid"} + + host: str + save_data: bool + if_modified_since: Union[str, None] = None + traceparent: Union[str, None] = None + x_tag: List[str] = [] + + +@app.get("/items/") +async def read_items(headers: Annotated[CommonHeaders, Header()]): + return headers diff --git a/docs_src/header_param_models/tutorial002_an_py310.py b/docs_src/header_param_models/tutorial002_an_py310.py new file mode 100644 index 000000000..e9535f045 --- /dev/null +++ b/docs_src/header_param_models/tutorial002_an_py310.py @@ -0,0 +1,21 @@ +from typing import Annotated + +from fastapi import FastAPI, Header +from pydantic import BaseModel + +app = FastAPI() + + +class CommonHeaders(BaseModel): + model_config = {"extra": "forbid"} + + host: str + save_data: bool + if_modified_since: str | None = None + traceparent: str | None = None + x_tag: list[str] = [] + + +@app.get("/items/") +async def read_items(headers: Annotated[CommonHeaders, Header()]): + return headers diff --git a/docs_src/header_param_models/tutorial002_an_py39.py b/docs_src/header_param_models/tutorial002_an_py39.py new file mode 100644 index 000000000..ca5208c9d --- /dev/null +++ b/docs_src/header_param_models/tutorial002_an_py39.py @@ -0,0 +1,21 @@ +from typing import Annotated, Union + +from fastapi import FastAPI, Header +from pydantic import BaseModel + +app = FastAPI() + + +class CommonHeaders(BaseModel): + model_config = {"extra": "forbid"} + + host: str + save_data: bool + if_modified_since: Union[str, None] = None + traceparent: Union[str, None] = None + x_tag: list[str] = [] + + +@app.get("/items/") +async def read_items(headers: Annotated[CommonHeaders, Header()]): + return headers diff --git a/docs_src/header_param_models/tutorial002_pv1.py b/docs_src/header_param_models/tutorial002_pv1.py new file mode 100644 index 000000000..7e56cd993 --- /dev/null +++ b/docs_src/header_param_models/tutorial002_pv1.py @@ -0,0 +1,22 @@ +from typing import List, Union + +from fastapi import FastAPI, Header +from pydantic import BaseModel + +app = FastAPI() + + +class CommonHeaders(BaseModel): + class Config: + extra = "forbid" + + host: str + save_data: bool + if_modified_since: Union[str, None] = None + traceparent: Union[str, None] = None + x_tag: List[str] = [] + + +@app.get("/items/") +async def read_items(headers: CommonHeaders = Header()): + return headers diff --git a/docs_src/header_param_models/tutorial002_pv1_an.py b/docs_src/header_param_models/tutorial002_pv1_an.py new file mode 100644 index 000000000..236778231 --- /dev/null +++ b/docs_src/header_param_models/tutorial002_pv1_an.py @@ -0,0 +1,23 @@ +from typing import List, Union + +from fastapi import FastAPI, Header +from pydantic import BaseModel +from typing_extensions import Annotated + +app = FastAPI() + + +class CommonHeaders(BaseModel): + class Config: + extra = "forbid" + + host: str + save_data: bool + if_modified_since: Union[str, None] = None + traceparent: Union[str, None] = None + x_tag: List[str] = [] + + +@app.get("/items/") +async def read_items(headers: Annotated[CommonHeaders, Header()]): + return headers diff --git a/docs_src/header_param_models/tutorial002_pv1_an_py310.py b/docs_src/header_param_models/tutorial002_pv1_an_py310.py new file mode 100644 index 000000000..e99e24ea5 --- /dev/null +++ b/docs_src/header_param_models/tutorial002_pv1_an_py310.py @@ -0,0 +1,22 @@ +from typing import Annotated + +from fastapi import FastAPI, Header +from pydantic import BaseModel + +app = FastAPI() + + +class CommonHeaders(BaseModel): + class Config: + extra = "forbid" + + host: str + save_data: bool + if_modified_since: str | None = None + traceparent: str | None = None + x_tag: list[str] = [] + + +@app.get("/items/") +async def read_items(headers: Annotated[CommonHeaders, Header()]): + return headers diff --git a/docs_src/header_param_models/tutorial002_pv1_an_py39.py b/docs_src/header_param_models/tutorial002_pv1_an_py39.py new file mode 100644 index 000000000..18398b726 --- /dev/null +++ b/docs_src/header_param_models/tutorial002_pv1_an_py39.py @@ -0,0 +1,22 @@ +from typing import Annotated, Union + +from fastapi import FastAPI, Header +from pydantic import BaseModel + +app = FastAPI() + + +class CommonHeaders(BaseModel): + class Config: + extra = "forbid" + + host: str + save_data: bool + if_modified_since: Union[str, None] = None + traceparent: Union[str, None] = None + x_tag: list[str] = [] + + +@app.get("/items/") +async def read_items(headers: Annotated[CommonHeaders, Header()]): + return headers diff --git a/docs_src/header_param_models/tutorial002_pv1_py310.py b/docs_src/header_param_models/tutorial002_pv1_py310.py new file mode 100644 index 000000000..3dbff9d7b --- /dev/null +++ b/docs_src/header_param_models/tutorial002_pv1_py310.py @@ -0,0 +1,20 @@ +from fastapi import FastAPI, Header +from pydantic import BaseModel + +app = FastAPI() + + +class CommonHeaders(BaseModel): + class Config: + extra = "forbid" + + host: str + save_data: bool + if_modified_since: str | None = None + traceparent: str | None = None + x_tag: list[str] = [] + + +@app.get("/items/") +async def read_items(headers: CommonHeaders = Header()): + return headers diff --git a/docs_src/header_param_models/tutorial002_pv1_py39.py b/docs_src/header_param_models/tutorial002_pv1_py39.py new file mode 100644 index 000000000..86e19be0d --- /dev/null +++ b/docs_src/header_param_models/tutorial002_pv1_py39.py @@ -0,0 +1,22 @@ +from typing import Union + +from fastapi import FastAPI, Header +from pydantic import BaseModel + +app = FastAPI() + + +class CommonHeaders(BaseModel): + class Config: + extra = "forbid" + + host: str + save_data: bool + if_modified_since: Union[str, None] = None + traceparent: Union[str, None] = None + x_tag: list[str] = [] + + +@app.get("/items/") +async def read_items(headers: CommonHeaders = Header()): + return headers diff --git a/docs_src/header_param_models/tutorial002_py310.py b/docs_src/header_param_models/tutorial002_py310.py new file mode 100644 index 000000000..3d2296345 --- /dev/null +++ b/docs_src/header_param_models/tutorial002_py310.py @@ -0,0 +1,19 @@ +from fastapi import FastAPI, Header +from pydantic import BaseModel + +app = FastAPI() + + +class CommonHeaders(BaseModel): + model_config = {"extra": "forbid"} + + host: str + save_data: bool + if_modified_since: str | None = None + traceparent: str | None = None + x_tag: list[str] = [] + + +@app.get("/items/") +async def read_items(headers: CommonHeaders = Header()): + return headers diff --git a/docs_src/header_param_models/tutorial002_py39.py b/docs_src/header_param_models/tutorial002_py39.py new file mode 100644 index 000000000..f8ce559a7 --- /dev/null +++ b/docs_src/header_param_models/tutorial002_py39.py @@ -0,0 +1,21 @@ +from typing import Union + +from fastapi import FastAPI, Header +from pydantic import BaseModel + +app = FastAPI() + + +class CommonHeaders(BaseModel): + model_config = {"extra": "forbid"} + + host: str + save_data: bool + if_modified_since: Union[str, None] = None + traceparent: Union[str, None] = None + x_tag: list[str] = [] + + +@app.get("/items/") +async def read_items(headers: CommonHeaders = Header()): + return headers diff --git a/docs_src/query_param_models/tutorial001.py b/docs_src/query_param_models/tutorial001.py new file mode 100644 index 000000000..0c0ab315e --- /dev/null +++ b/docs_src/query_param_models/tutorial001.py @@ -0,0 +1,19 @@ +from typing import List + +from fastapi import FastAPI, Query +from pydantic import BaseModel, Field +from typing_extensions import Literal + +app = FastAPI() + + +class FilterParams(BaseModel): + limit: int = Field(100, gt=0, le=100) + offset: int = Field(0, ge=0) + order_by: Literal["created_at", "updated_at"] = "created_at" + tags: List[str] = [] + + +@app.get("/items/") +async def read_items(filter_query: FilterParams = Query()): + return filter_query diff --git a/docs_src/query_param_models/tutorial001_an.py b/docs_src/query_param_models/tutorial001_an.py new file mode 100644 index 000000000..28375057c --- /dev/null +++ b/docs_src/query_param_models/tutorial001_an.py @@ -0,0 +1,19 @@ +from typing import List + +from fastapi import FastAPI, Query +from pydantic import BaseModel, Field +from typing_extensions import Annotated, Literal + +app = FastAPI() + + +class FilterParams(BaseModel): + limit: int = Field(100, gt=0, le=100) + offset: int = Field(0, ge=0) + order_by: Literal["created_at", "updated_at"] = "created_at" + tags: List[str] = [] + + +@app.get("/items/") +async def read_items(filter_query: Annotated[FilterParams, Query()]): + return filter_query diff --git a/docs_src/query_param_models/tutorial001_an_py310.py b/docs_src/query_param_models/tutorial001_an_py310.py new file mode 100644 index 000000000..71427acae --- /dev/null +++ b/docs_src/query_param_models/tutorial001_an_py310.py @@ -0,0 +1,18 @@ +from typing import Annotated, Literal + +from fastapi import FastAPI, Query +from pydantic import BaseModel, Field + +app = FastAPI() + + +class FilterParams(BaseModel): + limit: int = Field(100, gt=0, le=100) + offset: int = Field(0, ge=0) + order_by: Literal["created_at", "updated_at"] = "created_at" + tags: list[str] = [] + + +@app.get("/items/") +async def read_items(filter_query: Annotated[FilterParams, Query()]): + return filter_query diff --git a/docs_src/query_param_models/tutorial001_an_py39.py b/docs_src/query_param_models/tutorial001_an_py39.py new file mode 100644 index 000000000..ba690d3e3 --- /dev/null +++ b/docs_src/query_param_models/tutorial001_an_py39.py @@ -0,0 +1,17 @@ +from fastapi import FastAPI, Query +from pydantic import BaseModel, Field +from typing_extensions import Annotated, Literal + +app = FastAPI() + + +class FilterParams(BaseModel): + limit: int = Field(100, gt=0, le=100) + offset: int = Field(0, ge=0) + order_by: Literal["created_at", "updated_at"] = "created_at" + tags: list[str] = [] + + +@app.get("/items/") +async def read_items(filter_query: Annotated[FilterParams, Query()]): + return filter_query diff --git a/docs_src/query_param_models/tutorial001_py310.py b/docs_src/query_param_models/tutorial001_py310.py new file mode 100644 index 000000000..3ebf9f4d7 --- /dev/null +++ b/docs_src/query_param_models/tutorial001_py310.py @@ -0,0 +1,18 @@ +from typing import Literal + +from fastapi import FastAPI, Query +from pydantic import BaseModel, Field + +app = FastAPI() + + +class FilterParams(BaseModel): + limit: int = Field(100, gt=0, le=100) + offset: int = Field(0, ge=0) + order_by: Literal["created_at", "updated_at"] = "created_at" + tags: list[str] = [] + + +@app.get("/items/") +async def read_items(filter_query: FilterParams = Query()): + return filter_query diff --git a/docs_src/query_param_models/tutorial001_py39.py b/docs_src/query_param_models/tutorial001_py39.py new file mode 100644 index 000000000..54b52a054 --- /dev/null +++ b/docs_src/query_param_models/tutorial001_py39.py @@ -0,0 +1,17 @@ +from fastapi import FastAPI, Query +from pydantic import BaseModel, Field +from typing_extensions import Literal + +app = FastAPI() + + +class FilterParams(BaseModel): + limit: int = Field(100, gt=0, le=100) + offset: int = Field(0, ge=0) + order_by: Literal["created_at", "updated_at"] = "created_at" + tags: list[str] = [] + + +@app.get("/items/") +async def read_items(filter_query: FilterParams = Query()): + return filter_query diff --git a/docs_src/query_param_models/tutorial002.py b/docs_src/query_param_models/tutorial002.py new file mode 100644 index 000000000..1633bc464 --- /dev/null +++ b/docs_src/query_param_models/tutorial002.py @@ -0,0 +1,21 @@ +from typing import List + +from fastapi import FastAPI, Query +from pydantic import BaseModel, Field +from typing_extensions import Literal + +app = FastAPI() + + +class FilterParams(BaseModel): + model_config = {"extra": "forbid"} + + limit: int = Field(100, gt=0, le=100) + offset: int = Field(0, ge=0) + order_by: Literal["created_at", "updated_at"] = "created_at" + tags: List[str] = [] + + +@app.get("/items/") +async def read_items(filter_query: FilterParams = Query()): + return filter_query diff --git a/docs_src/query_param_models/tutorial002_an.py b/docs_src/query_param_models/tutorial002_an.py new file mode 100644 index 000000000..69705d4b4 --- /dev/null +++ b/docs_src/query_param_models/tutorial002_an.py @@ -0,0 +1,21 @@ +from typing import List + +from fastapi import FastAPI, Query +from pydantic import BaseModel, Field +from typing_extensions import Annotated, Literal + +app = FastAPI() + + +class FilterParams(BaseModel): + model_config = {"extra": "forbid"} + + limit: int = Field(100, gt=0, le=100) + offset: int = Field(0, ge=0) + order_by: Literal["created_at", "updated_at"] = "created_at" + tags: List[str] = [] + + +@app.get("/items/") +async def read_items(filter_query: Annotated[FilterParams, Query()]): + return filter_query diff --git a/docs_src/query_param_models/tutorial002_an_py310.py b/docs_src/query_param_models/tutorial002_an_py310.py new file mode 100644 index 000000000..975956502 --- /dev/null +++ b/docs_src/query_param_models/tutorial002_an_py310.py @@ -0,0 +1,20 @@ +from typing import Annotated, Literal + +from fastapi import FastAPI, Query +from pydantic import BaseModel, Field + +app = FastAPI() + + +class FilterParams(BaseModel): + model_config = {"extra": "forbid"} + + limit: int = Field(100, gt=0, le=100) + offset: int = Field(0, ge=0) + order_by: Literal["created_at", "updated_at"] = "created_at" + tags: list[str] = [] + + +@app.get("/items/") +async def read_items(filter_query: Annotated[FilterParams, Query()]): + return filter_query diff --git a/docs_src/query_param_models/tutorial002_an_py39.py b/docs_src/query_param_models/tutorial002_an_py39.py new file mode 100644 index 000000000..2d4c1a62b --- /dev/null +++ b/docs_src/query_param_models/tutorial002_an_py39.py @@ -0,0 +1,19 @@ +from fastapi import FastAPI, Query +from pydantic import BaseModel, Field +from typing_extensions import Annotated, Literal + +app = FastAPI() + + +class FilterParams(BaseModel): + model_config = {"extra": "forbid"} + + limit: int = Field(100, gt=0, le=100) + offset: int = Field(0, ge=0) + order_by: Literal["created_at", "updated_at"] = "created_at" + tags: list[str] = [] + + +@app.get("/items/") +async def read_items(filter_query: Annotated[FilterParams, Query()]): + return filter_query diff --git a/docs_src/query_param_models/tutorial002_pv1.py b/docs_src/query_param_models/tutorial002_pv1.py new file mode 100644 index 000000000..71ccd961d --- /dev/null +++ b/docs_src/query_param_models/tutorial002_pv1.py @@ -0,0 +1,22 @@ +from typing import List + +from fastapi import FastAPI, Query +from pydantic import BaseModel, Field +from typing_extensions import Literal + +app = FastAPI() + + +class FilterParams(BaseModel): + class Config: + extra = "forbid" + + limit: int = Field(100, gt=0, le=100) + offset: int = Field(0, ge=0) + order_by: Literal["created_at", "updated_at"] = "created_at" + tags: List[str] = [] + + +@app.get("/items/") +async def read_items(filter_query: FilterParams = Query()): + return filter_query diff --git a/docs_src/query_param_models/tutorial002_pv1_an.py b/docs_src/query_param_models/tutorial002_pv1_an.py new file mode 100644 index 000000000..1dd29157a --- /dev/null +++ b/docs_src/query_param_models/tutorial002_pv1_an.py @@ -0,0 +1,22 @@ +from typing import List + +from fastapi import FastAPI, Query +from pydantic import BaseModel, Field +from typing_extensions import Annotated, Literal + +app = FastAPI() + + +class FilterParams(BaseModel): + class Config: + extra = "forbid" + + limit: int = Field(100, gt=0, le=100) + offset: int = Field(0, ge=0) + order_by: Literal["created_at", "updated_at"] = "created_at" + tags: List[str] = [] + + +@app.get("/items/") +async def read_items(filter_query: Annotated[FilterParams, Query()]): + return filter_query diff --git a/docs_src/query_param_models/tutorial002_pv1_an_py310.py b/docs_src/query_param_models/tutorial002_pv1_an_py310.py new file mode 100644 index 000000000..d635aae88 --- /dev/null +++ b/docs_src/query_param_models/tutorial002_pv1_an_py310.py @@ -0,0 +1,21 @@ +from typing import Annotated, Literal + +from fastapi import FastAPI, Query +from pydantic import BaseModel, Field + +app = FastAPI() + + +class FilterParams(BaseModel): + class Config: + extra = "forbid" + + limit: int = Field(100, gt=0, le=100) + offset: int = Field(0, ge=0) + order_by: Literal["created_at", "updated_at"] = "created_at" + tags: list[str] = [] + + +@app.get("/items/") +async def read_items(filter_query: Annotated[FilterParams, Query()]): + return filter_query diff --git a/docs_src/query_param_models/tutorial002_pv1_an_py39.py b/docs_src/query_param_models/tutorial002_pv1_an_py39.py new file mode 100644 index 000000000..494fef11f --- /dev/null +++ b/docs_src/query_param_models/tutorial002_pv1_an_py39.py @@ -0,0 +1,20 @@ +from fastapi import FastAPI, Query +from pydantic import BaseModel, Field +from typing_extensions import Annotated, Literal + +app = FastAPI() + + +class FilterParams(BaseModel): + class Config: + extra = "forbid" + + limit: int = Field(100, gt=0, le=100) + offset: int = Field(0, ge=0) + order_by: Literal["created_at", "updated_at"] = "created_at" + tags: list[str] = [] + + +@app.get("/items/") +async def read_items(filter_query: Annotated[FilterParams, Query()]): + return filter_query diff --git a/docs_src/query_param_models/tutorial002_pv1_py310.py b/docs_src/query_param_models/tutorial002_pv1_py310.py new file mode 100644 index 000000000..9ffdeefc0 --- /dev/null +++ b/docs_src/query_param_models/tutorial002_pv1_py310.py @@ -0,0 +1,21 @@ +from typing import Literal + +from fastapi import FastAPI, Query +from pydantic import BaseModel, Field + +app = FastAPI() + + +class FilterParams(BaseModel): + class Config: + extra = "forbid" + + limit: int = Field(100, gt=0, le=100) + offset: int = Field(0, ge=0) + order_by: Literal["created_at", "updated_at"] = "created_at" + tags: list[str] = [] + + +@app.get("/items/") +async def read_items(filter_query: FilterParams = Query()): + return filter_query diff --git a/docs_src/query_param_models/tutorial002_pv1_py39.py b/docs_src/query_param_models/tutorial002_pv1_py39.py new file mode 100644 index 000000000..7fa456a79 --- /dev/null +++ b/docs_src/query_param_models/tutorial002_pv1_py39.py @@ -0,0 +1,20 @@ +from fastapi import FastAPI, Query +from pydantic import BaseModel, Field +from typing_extensions import Literal + +app = FastAPI() + + +class FilterParams(BaseModel): + class Config: + extra = "forbid" + + limit: int = Field(100, gt=0, le=100) + offset: int = Field(0, ge=0) + order_by: Literal["created_at", "updated_at"] = "created_at" + tags: list[str] = [] + + +@app.get("/items/") +async def read_items(filter_query: FilterParams = Query()): + return filter_query diff --git a/docs_src/query_param_models/tutorial002_py310.py b/docs_src/query_param_models/tutorial002_py310.py new file mode 100644 index 000000000..6ec418499 --- /dev/null +++ b/docs_src/query_param_models/tutorial002_py310.py @@ -0,0 +1,20 @@ +from typing import Literal + +from fastapi import FastAPI, Query +from pydantic import BaseModel, Field + +app = FastAPI() + + +class FilterParams(BaseModel): + model_config = {"extra": "forbid"} + + limit: int = Field(100, gt=0, le=100) + offset: int = Field(0, ge=0) + order_by: Literal["created_at", "updated_at"] = "created_at" + tags: list[str] = [] + + +@app.get("/items/") +async def read_items(filter_query: FilterParams = Query()): + return filter_query diff --git a/docs_src/query_param_models/tutorial002_py39.py b/docs_src/query_param_models/tutorial002_py39.py new file mode 100644 index 000000000..f9bba028c --- /dev/null +++ b/docs_src/query_param_models/tutorial002_py39.py @@ -0,0 +1,19 @@ +from fastapi import FastAPI, Query +from pydantic import BaseModel, Field +from typing_extensions import Literal + +app = FastAPI() + + +class FilterParams(BaseModel): + model_config = {"extra": "forbid"} + + limit: int = Field(100, gt=0, le=100) + offset: int = Field(0, ge=0) + order_by: Literal["created_at", "updated_at"] = "created_at" + tags: list[str] = [] + + +@app.get("/items/") +async def read_items(filter_query: FilterParams = Query()): + return filter_query diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 7548cf0c7..5cebbf00f 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -201,14 +201,23 @@ def get_flat_dependant( return flat_dependant +def _get_flat_fields_from_params(fields: List[ModelField]) -> List[ModelField]: + if not fields: + return fields + first_field = fields[0] + if len(fields) == 1 and lenient_issubclass(first_field.type_, BaseModel): + fields_to_extract = get_cached_model_fields(first_field.type_) + return fields_to_extract + return fields + + def get_flat_params(dependant: Dependant) -> List[ModelField]: flat_dependant = get_flat_dependant(dependant, skip_repeats=True) - return ( - flat_dependant.path_params - + flat_dependant.query_params - + flat_dependant.header_params - + flat_dependant.cookie_params - ) + path_params = _get_flat_fields_from_params(flat_dependant.path_params) + query_params = _get_flat_fields_from_params(flat_dependant.query_params) + header_params = _get_flat_fields_from_params(flat_dependant.header_params) + cookie_params = _get_flat_fields_from_params(flat_dependant.cookie_params) + return path_params + query_params + header_params + cookie_params def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature: @@ -479,7 +488,15 @@ def analyze_param( field=field ), "Path params must be of one of the supported types" elif isinstance(field_info, params.Query): - assert is_scalar_field(field) or is_scalar_sequence_field(field) + assert ( + is_scalar_field(field) + or is_scalar_sequence_field(field) + or ( + lenient_issubclass(field.type_, BaseModel) + # For Pydantic v1 + and getattr(field, "shape", 1) == 1 + ) + ) return ParamDetails(type_annotation=type_annotation, depends=depends, field=field) @@ -686,11 +703,14 @@ def _validate_value_with_model_field( return v_, [] -def _get_multidict_value(field: ModelField, values: Mapping[str, Any]) -> Any: +def _get_multidict_value( + field: ModelField, values: Mapping[str, Any], alias: Union[str, None] = None +) -> Any: + alias = alias or field.alias if is_sequence_field(field) and isinstance(values, (ImmutableMultiDict, Headers)): - value = values.getlist(field.alias) + value = values.getlist(alias) else: - value = values.get(field.alias, None) + value = values.get(alias, None) if ( value is None or ( @@ -712,7 +732,55 @@ def request_params_to_args( received_params: Union[Mapping[str, Any], QueryParams, Headers], ) -> Tuple[Dict[str, Any], List[Any]]: values: Dict[str, Any] = {} - errors = [] + errors: List[Dict[str, Any]] = [] + + if not fields: + return values, errors + + first_field = fields[0] + fields_to_extract = fields + single_not_embedded_field = False + if len(fields) == 1 and lenient_issubclass(first_field.type_, BaseModel): + fields_to_extract = get_cached_model_fields(first_field.type_) + single_not_embedded_field = True + + params_to_process: Dict[str, Any] = {} + + processed_keys = set() + + for field in fields_to_extract: + alias = None + if isinstance(received_params, Headers): + # Handle fields extracted from a Pydantic Model for a header, each field + # doesn't have a FieldInfo of type Header with the default convert_underscores=True + convert_underscores = getattr(field.field_info, "convert_underscores", True) + if convert_underscores: + alias = ( + field.alias + if field.alias != field.name + else field.name.replace("_", "-") + ) + value = _get_multidict_value(field, received_params, alias=alias) + if value is not None: + params_to_process[field.name] = value + processed_keys.add(alias or field.alias) + processed_keys.add(field.name) + + for key, value in received_params.items(): + if key not in processed_keys: + params_to_process[key] = value + + if single_not_embedded_field: + field_info = first_field.field_info + assert isinstance( + field_info, params.Param + ), "Params must be subclasses of Param" + loc: Tuple[str, ...] = (field_info.in_.value,) + v_, errors_ = _validate_value_with_model_field( + field=first_field, value=params_to_process, values=values, loc=loc + ) + return {first_field.name: v_}, errors_ + for field in fields: value = _get_multidict_value(field, received_params) field_info = field.field_info diff --git a/fastapi/openapi/utils.py b/fastapi/openapi/utils.py index 79ad9f83f..947eca948 100644 --- a/fastapi/openapi/utils.py +++ b/fastapi/openapi/utils.py @@ -16,11 +16,15 @@ from fastapi._compat import ( ) from fastapi.datastructures import DefaultPlaceholder from fastapi.dependencies.models import Dependant -from fastapi.dependencies.utils import get_flat_dependant, get_flat_params +from fastapi.dependencies.utils import ( + _get_flat_fields_from_params, + get_flat_dependant, + get_flat_params, +) from fastapi.encoders import jsonable_encoder from fastapi.openapi.constants import METHODS_WITH_BODY, REF_PREFIX, REF_TEMPLATE from fastapi.openapi.models import OpenAPI -from fastapi.params import Body, Param +from fastapi.params import Body, ParamTypes from fastapi.responses import Response from fastapi.types import ModelNameMap from fastapi.utils import ( @@ -87,9 +91,9 @@ def get_openapi_security_definitions( return security_definitions, operation_security -def get_openapi_operation_parameters( +def _get_openapi_operation_parameters( *, - all_route_params: Sequence[ModelField], + dependant: Dependant, schema_generator: GenerateJsonSchema, model_name_map: ModelNameMap, field_mapping: Dict[ @@ -98,33 +102,47 @@ def get_openapi_operation_parameters( separate_input_output_schemas: bool = True, ) -> List[Dict[str, Any]]: parameters = [] - for param in all_route_params: - field_info = param.field_info - field_info = cast(Param, field_info) - if not field_info.include_in_schema: - continue - param_schema = get_schema_from_model_field( - field=param, - schema_generator=schema_generator, - model_name_map=model_name_map, - field_mapping=field_mapping, - separate_input_output_schemas=separate_input_output_schemas, - ) - parameter = { - "name": param.alias, - "in": field_info.in_.value, - "required": param.required, - "schema": param_schema, - } - if field_info.description: - parameter["description"] = field_info.description - if field_info.openapi_examples: - parameter["examples"] = jsonable_encoder(field_info.openapi_examples) - elif field_info.example != Undefined: - parameter["example"] = jsonable_encoder(field_info.example) - if field_info.deprecated: - parameter["deprecated"] = True - parameters.append(parameter) + flat_dependant = get_flat_dependant(dependant, skip_repeats=True) + path_params = _get_flat_fields_from_params(flat_dependant.path_params) + query_params = _get_flat_fields_from_params(flat_dependant.query_params) + header_params = _get_flat_fields_from_params(flat_dependant.header_params) + cookie_params = _get_flat_fields_from_params(flat_dependant.cookie_params) + parameter_groups = [ + (ParamTypes.path, path_params), + (ParamTypes.query, query_params), + (ParamTypes.header, header_params), + (ParamTypes.cookie, cookie_params), + ] + for param_type, param_group in parameter_groups: + for param in param_group: + field_info = param.field_info + # field_info = cast(Param, field_info) + if not getattr(field_info, "include_in_schema", True): + continue + param_schema = get_schema_from_model_field( + field=param, + schema_generator=schema_generator, + model_name_map=model_name_map, + field_mapping=field_mapping, + separate_input_output_schemas=separate_input_output_schemas, + ) + parameter = { + "name": param.alias, + "in": param_type.value, + "required": param.required, + "schema": param_schema, + } + if field_info.description: + parameter["description"] = field_info.description + openapi_examples = getattr(field_info, "openapi_examples", None) + example = getattr(field_info, "example", None) + if openapi_examples: + parameter["examples"] = jsonable_encoder(openapi_examples) + elif example != Undefined: + parameter["example"] = jsonable_encoder(example) + if getattr(field_info, "deprecated", None): + parameter["deprecated"] = True + parameters.append(parameter) return parameters @@ -247,9 +265,8 @@ def get_openapi_path( operation.setdefault("security", []).extend(operation_security) if security_definitions: security_schemes.update(security_definitions) - all_route_params = get_flat_params(route.dependant) - operation_parameters = get_openapi_operation_parameters( - all_route_params=all_route_params, + operation_parameters = _get_openapi_operation_parameters( + dependant=route.dependant, schema_generator=schema_generator, model_name_map=model_name_map, field_mapping=field_mapping, @@ -379,6 +396,7 @@ def get_openapi_path( deep_dict_update(openapi_response, process_response) openapi_response["description"] = description http422 = str(HTTP_422_UNPROCESSABLE_ENTITY) + all_route_params = get_flat_params(route.dependant) if (all_route_params or route.body_field) and not any( status in operation["responses"] for status in [http422, "4XX", "default"] diff --git a/scripts/playwright/cookie_param_models/image01.py b/scripts/playwright/cookie_param_models/image01.py new file mode 100644 index 000000000..77c91bfe2 --- /dev/null +++ b/scripts/playwright/cookie_param_models/image01.py @@ -0,0 +1,39 @@ +import subprocess +import time + +import httpx +from playwright.sync_api import Playwright, sync_playwright + + +# Run playwright codegen to generate the code below, copy paste the sections in run() +def run(playwright: Playwright) -> None: + browser = playwright.chromium.launch(headless=False) + # Update the viewport manually + context = browser.new_context(viewport={"width": 960, "height": 1080}) + browser = playwright.chromium.launch(headless=False) + context = browser.new_context() + page = context.new_page() + page.goto("http://localhost:8000/docs") + page.get_by_role("link", name="/items/").click() + # Manually add the screenshot + page.screenshot(path="docs/en/docs/img/tutorial/cookie-param-models/image01.png") + + # --------------------- + context.close() + browser.close() + + +process = subprocess.Popen( + ["fastapi", "run", "docs_src/cookie_param_models/tutorial001.py"] +) +try: + for _ in range(3): + try: + response = httpx.get("http://localhost:8000/docs") + except httpx.ConnectError: + time.sleep(1) + break + with sync_playwright() as playwright: + run(playwright) +finally: + process.terminate() diff --git a/scripts/playwright/header_param_models/image01.py b/scripts/playwright/header_param_models/image01.py new file mode 100644 index 000000000..53914251e --- /dev/null +++ b/scripts/playwright/header_param_models/image01.py @@ -0,0 +1,38 @@ +import subprocess +import time + +import httpx +from playwright.sync_api import Playwright, sync_playwright + + +# Run playwright codegen to generate the code below, copy paste the sections in run() +def run(playwright: Playwright) -> None: + browser = playwright.chromium.launch(headless=False) + # Update the viewport manually + context = browser.new_context(viewport={"width": 960, "height": 1080}) + page = context.new_page() + page.goto("http://localhost:8000/docs") + page.get_by_role("button", name="GET /items/ Read Items").click() + page.get_by_role("button", name="Try it out").click() + # Manually add the screenshot + page.screenshot(path="docs/en/docs/img/tutorial/header-param-models/image01.png") + + # --------------------- + context.close() + browser.close() + + +process = subprocess.Popen( + ["fastapi", "run", "docs_src/header_param_models/tutorial001.py"] +) +try: + for _ in range(3): + try: + response = httpx.get("http://localhost:8000/docs") + except httpx.ConnectError: + time.sleep(1) + break + with sync_playwright() as playwright: + run(playwright) +finally: + process.terminate() diff --git a/scripts/playwright/query_param_models/image01.py b/scripts/playwright/query_param_models/image01.py new file mode 100644 index 000000000..0ea1d0df4 --- /dev/null +++ b/scripts/playwright/query_param_models/image01.py @@ -0,0 +1,41 @@ +import subprocess +import time + +import httpx +from playwright.sync_api import Playwright, sync_playwright + + +# Run playwright codegen to generate the code below, copy paste the sections in run() +def run(playwright: Playwright) -> None: + browser = playwright.chromium.launch(headless=False) + # Update the viewport manually + context = browser.new_context(viewport={"width": 960, "height": 1080}) + browser = playwright.chromium.launch(headless=False) + context = browser.new_context() + page = context.new_page() + page.goto("http://localhost:8000/docs") + page.get_by_role("button", name="GET /items/ Read Items").click() + page.get_by_role("button", name="Try it out").click() + page.get_by_role("heading", name="Servers").click() + # Manually add the screenshot + page.screenshot(path="docs/en/docs/img/tutorial/query-param-models/image01.png") + + # --------------------- + context.close() + browser.close() + + +process = subprocess.Popen( + ["fastapi", "run", "docs_src/query_param_models/tutorial001.py"] +) +try: + for _ in range(3): + try: + response = httpx.get("http://localhost:8000/docs") + except httpx.ConnectError: + time.sleep(1) + break + with sync_playwright() as playwright: + run(playwright) +finally: + process.terminate() diff --git a/tests/test_tutorial/test_cookie_param_models/__init__.py b/tests/test_tutorial/test_cookie_param_models/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_tutorial/test_cookie_param_models/test_tutorial001.py b/tests/test_tutorial/test_cookie_param_models/test_tutorial001.py new file mode 100644 index 000000000..60643185a --- /dev/null +++ b/tests/test_tutorial/test_cookie_param_models/test_tutorial001.py @@ -0,0 +1,205 @@ +import importlib + +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient +from inline_snapshot import snapshot + +from tests.utils import needs_py39, needs_py310 + + +@pytest.fixture( + name="client", + params=[ + "tutorial001", + pytest.param("tutorial001_py310", marks=needs_py310), + "tutorial001_an", + pytest.param("tutorial001_an_py39", marks=needs_py39), + pytest.param("tutorial001_an_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.cookie_param_models.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_cookie_param_model(client: TestClient): + with client as c: + c.cookies.set("session_id", "123") + c.cookies.set("fatebook_tracker", "456") + c.cookies.set("googall_tracker", "789") + response = c.get("/items/") + assert response.status_code == 200 + assert response.json() == { + "session_id": "123", + "fatebook_tracker": "456", + "googall_tracker": "789", + } + + +def test_cookie_param_model_defaults(client: TestClient): + with client as c: + c.cookies.set("session_id", "123") + response = c.get("/items/") + assert response.status_code == 200 + assert response.json() == { + "session_id": "123", + "fatebook_tracker": None, + "googall_tracker": None, + } + + +def test_cookie_param_model_invalid(client: TestClient): + response = client.get("/items/") + assert response.status_code == 422 + assert response.json() == snapshot( + IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["cookie", "session_id"], + "msg": "Field required", + "input": {}, + } + ] + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "type": "value_error.missing", + "loc": ["cookie", "session_id"], + "msg": "field required", + } + ] + } + ) + ) + + +def test_cookie_param_model_extra(client: TestClient): + with client as c: + c.cookies.set("session_id", "123") + c.cookies.set("extra", "track-me-here-too") + response = c.get("/items/") + assert response.status_code == 200 + assert response.json() == snapshot( + {"session_id": "123", "fatebook_tracker": None, "googall_tracker": None} + ) + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "name": "session_id", + "in": "cookie", + "required": True, + "schema": {"type": "string", "title": "Session Id"}, + }, + { + "name": "fatebook_tracker", + "in": "cookie", + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Fatebook Tracker", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "type": "string", + "title": "Fatebook Tracker", + } + ), + }, + { + "name": "googall_tracker", + "in": "cookie", + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Googall Tracker", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "type": "string", + "title": "Googall Tracker", + } + ), + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "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", + }, + } + }, + } + ) diff --git a/tests/test_tutorial/test_cookie_param_models/test_tutorial002.py b/tests/test_tutorial/test_cookie_param_models/test_tutorial002.py new file mode 100644 index 000000000..30adadc8a --- /dev/null +++ b/tests/test_tutorial/test_cookie_param_models/test_tutorial002.py @@ -0,0 +1,233 @@ +import importlib + +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient +from inline_snapshot import snapshot + +from tests.utils import needs_py39, needs_py310, needs_pydanticv1, needs_pydanticv2 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial002", marks=needs_pydanticv2), + pytest.param("tutorial002_py310", marks=[needs_py310, needs_pydanticv2]), + pytest.param("tutorial002_an", marks=needs_pydanticv2), + pytest.param("tutorial002_an_py39", marks=[needs_py39, needs_pydanticv2]), + pytest.param("tutorial002_an_py310", marks=[needs_py310, needs_pydanticv2]), + pytest.param("tutorial002_pv1", marks=[needs_pydanticv1, needs_pydanticv1]), + pytest.param("tutorial002_pv1_py310", marks=[needs_py310, needs_pydanticv1]), + pytest.param("tutorial002_pv1_an", marks=[needs_pydanticv1]), + pytest.param("tutorial002_pv1_an_py39", marks=[needs_py39, needs_pydanticv1]), + pytest.param("tutorial002_pv1_an_py310", marks=[needs_py310, needs_pydanticv1]), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.cookie_param_models.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_cookie_param_model(client: TestClient): + with client as c: + c.cookies.set("session_id", "123") + c.cookies.set("fatebook_tracker", "456") + c.cookies.set("googall_tracker", "789") + response = c.get("/items/") + assert response.status_code == 200 + assert response.json() == { + "session_id": "123", + "fatebook_tracker": "456", + "googall_tracker": "789", + } + + +def test_cookie_param_model_defaults(client: TestClient): + with client as c: + c.cookies.set("session_id", "123") + response = c.get("/items/") + assert response.status_code == 200 + assert response.json() == { + "session_id": "123", + "fatebook_tracker": None, + "googall_tracker": None, + } + + +def test_cookie_param_model_invalid(client: TestClient): + response = client.get("/items/") + assert response.status_code == 422 + assert response.json() == snapshot( + IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["cookie", "session_id"], + "msg": "Field required", + "input": {}, + } + ] + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "type": "value_error.missing", + "loc": ["cookie", "session_id"], + "msg": "field required", + } + ] + } + ) + ) + + +def test_cookie_param_model_extra(client: TestClient): + with client as c: + c.cookies.set("session_id", "123") + c.cookies.set("extra", "track-me-here-too") + response = c.get("/items/") + assert response.status_code == 422 + assert response.json() == snapshot( + IsDict( + { + "detail": [ + { + "type": "extra_forbidden", + "loc": ["cookie", "extra"], + "msg": "Extra inputs are not permitted", + "input": "track-me-here-too", + } + ] + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "type": "value_error.extra", + "loc": ["cookie", "extra"], + "msg": "extra fields not permitted", + } + ] + } + ) + ) + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "name": "session_id", + "in": "cookie", + "required": True, + "schema": {"type": "string", "title": "Session Id"}, + }, + { + "name": "fatebook_tracker", + "in": "cookie", + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Fatebook Tracker", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "type": "string", + "title": "Fatebook Tracker", + } + ), + }, + { + "name": "googall_tracker", + "in": "cookie", + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Googall Tracker", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "type": "string", + "title": "Googall Tracker", + } + ), + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "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", + }, + } + }, + } + ) diff --git a/tests/test_tutorial/test_header_param_models/__init__.py b/tests/test_tutorial/test_header_param_models/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_tutorial/test_header_param_models/test_tutorial001.py b/tests/test_tutorial/test_header_param_models/test_tutorial001.py new file mode 100644 index 000000000..06b2404cf --- /dev/null +++ b/tests/test_tutorial/test_header_param_models/test_tutorial001.py @@ -0,0 +1,238 @@ +import importlib + +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient +from inline_snapshot import snapshot + +from tests.utils import needs_py39, needs_py310 + + +@pytest.fixture( + name="client", + params=[ + "tutorial001", + pytest.param("tutorial001_py39", marks=needs_py39), + pytest.param("tutorial001_py310", marks=needs_py310), + "tutorial001_an", + pytest.param("tutorial001_an_py39", marks=needs_py39), + pytest.param("tutorial001_an_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.header_param_models.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_header_param_model(client: TestClient): + response = client.get( + "/items/", + headers=[ + ("save-data", "true"), + ("if-modified-since", "yesterday"), + ("traceparent", "123"), + ("x-tag", "one"), + ("x-tag", "two"), + ], + ) + assert response.status_code == 200 + assert response.json() == { + "host": "testserver", + "save_data": True, + "if_modified_since": "yesterday", + "traceparent": "123", + "x_tag": ["one", "two"], + } + + +def test_header_param_model_defaults(client: TestClient): + response = client.get("/items/", headers=[("save-data", "true")]) + assert response.status_code == 200 + assert response.json() == { + "host": "testserver", + "save_data": True, + "if_modified_since": None, + "traceparent": None, + "x_tag": [], + } + + +def test_header_param_model_invalid(client: TestClient): + response = client.get("/items/") + assert response.status_code == 422 + assert response.json() == snapshot( + { + "detail": [ + IsDict( + { + "type": "missing", + "loc": ["header", "save_data"], + "msg": "Field required", + "input": { + "x_tag": [], + "host": "testserver", + "accept": "*/*", + "accept-encoding": "gzip, deflate", + "connection": "keep-alive", + "user-agent": "testclient", + }, + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "type": "value_error.missing", + "loc": ["header", "save_data"], + "msg": "field required", + } + ) + ] + } + ) + + +def test_header_param_model_extra(client: TestClient): + response = client.get( + "/items/", headers=[("save-data", "true"), ("tool", "plumbus")] + ) + assert response.status_code == 200, response.text + assert response.json() == snapshot( + { + "host": "testserver", + "save_data": True, + "if_modified_since": None, + "traceparent": None, + "x_tag": [], + } + ) + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "name": "host", + "in": "header", + "required": True, + "schema": {"type": "string", "title": "Host"}, + }, + { + "name": "save_data", + "in": "header", + "required": True, + "schema": {"type": "boolean", "title": "Save Data"}, + }, + { + "name": "if_modified_since", + "in": "header", + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "If Modified Since", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "type": "string", + "title": "If Modified Since", + } + ), + }, + { + "name": "traceparent", + "in": "header", + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Traceparent", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "type": "string", + "title": "Traceparent", + } + ), + }, + { + "name": "x_tag", + "in": "header", + "required": False, + "schema": { + "type": "array", + "items": {"type": "string"}, + "default": [], + "title": "X Tag", + }, + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "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", + }, + } + }, + } + ) diff --git a/tests/test_tutorial/test_header_param_models/test_tutorial002.py b/tests/test_tutorial/test_header_param_models/test_tutorial002.py new file mode 100644 index 000000000..e07655a0c --- /dev/null +++ b/tests/test_tutorial/test_header_param_models/test_tutorial002.py @@ -0,0 +1,249 @@ +import importlib + +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient +from inline_snapshot import snapshot + +from tests.utils import needs_py39, needs_py310, needs_pydanticv1, needs_pydanticv2 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial002", marks=needs_pydanticv2), + pytest.param("tutorial002_py310", marks=[needs_py310, needs_pydanticv2]), + pytest.param("tutorial002_an", marks=needs_pydanticv2), + pytest.param("tutorial002_an_py39", marks=[needs_py39, needs_pydanticv2]), + pytest.param("tutorial002_an_py310", marks=[needs_py310, needs_pydanticv2]), + pytest.param("tutorial002_pv1", marks=[needs_pydanticv1, needs_pydanticv1]), + pytest.param("tutorial002_pv1_py310", marks=[needs_py310, needs_pydanticv1]), + pytest.param("tutorial002_pv1_an", marks=[needs_pydanticv1]), + pytest.param("tutorial002_pv1_an_py39", marks=[needs_py39, needs_pydanticv1]), + pytest.param("tutorial002_pv1_an_py310", marks=[needs_py310, needs_pydanticv1]), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.header_param_models.{request.param}") + + client = TestClient(mod.app) + client.headers.clear() + return client + + +def test_header_param_model(client: TestClient): + response = client.get( + "/items/", + headers=[ + ("save-data", "true"), + ("if-modified-since", "yesterday"), + ("traceparent", "123"), + ("x-tag", "one"), + ("x-tag", "two"), + ], + ) + assert response.status_code == 200, response.text + assert response.json() == { + "host": "testserver", + "save_data": True, + "if_modified_since": "yesterday", + "traceparent": "123", + "x_tag": ["one", "two"], + } + + +def test_header_param_model_defaults(client: TestClient): + response = client.get("/items/", headers=[("save-data", "true")]) + assert response.status_code == 200 + assert response.json() == { + "host": "testserver", + "save_data": True, + "if_modified_since": None, + "traceparent": None, + "x_tag": [], + } + + +def test_header_param_model_invalid(client: TestClient): + response = client.get("/items/") + assert response.status_code == 422 + assert response.json() == snapshot( + { + "detail": [ + IsDict( + { + "type": "missing", + "loc": ["header", "save_data"], + "msg": "Field required", + "input": {"x_tag": [], "host": "testserver"}, + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "type": "value_error.missing", + "loc": ["header", "save_data"], + "msg": "field required", + } + ) + ] + } + ) + + +def test_header_param_model_extra(client: TestClient): + response = client.get( + "/items/", headers=[("save-data", "true"), ("tool", "plumbus")] + ) + assert response.status_code == 422, response.text + assert response.json() == snapshot( + { + "detail": [ + IsDict( + { + "type": "extra_forbidden", + "loc": ["header", "tool"], + "msg": "Extra inputs are not permitted", + "input": "plumbus", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "type": "value_error.extra", + "loc": ["header", "tool"], + "msg": "extra fields not permitted", + } + ) + ] + } + ) + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "name": "host", + "in": "header", + "required": True, + "schema": {"type": "string", "title": "Host"}, + }, + { + "name": "save_data", + "in": "header", + "required": True, + "schema": {"type": "boolean", "title": "Save Data"}, + }, + { + "name": "if_modified_since", + "in": "header", + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "If Modified Since", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "type": "string", + "title": "If Modified Since", + } + ), + }, + { + "name": "traceparent", + "in": "header", + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Traceparent", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "type": "string", + "title": "Traceparent", + } + ), + }, + { + "name": "x_tag", + "in": "header", + "required": False, + "schema": { + "type": "array", + "items": {"type": "string"}, + "default": [], + "title": "X Tag", + }, + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "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", + }, + } + }, + } + ) diff --git a/tests/test_tutorial/test_query_param_models/__init__.py b/tests/test_tutorial/test_query_param_models/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_tutorial/test_query_param_models/test_tutorial001.py b/tests/test_tutorial/test_query_param_models/test_tutorial001.py new file mode 100644 index 000000000..5b7bc7b42 --- /dev/null +++ b/tests/test_tutorial/test_query_param_models/test_tutorial001.py @@ -0,0 +1,260 @@ +import importlib + +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient +from inline_snapshot import snapshot + +from tests.utils import needs_py39, needs_py310 + + +@pytest.fixture( + name="client", + params=[ + "tutorial001", + pytest.param("tutorial001_py39", marks=needs_py39), + pytest.param("tutorial001_py310", marks=needs_py310), + "tutorial001_an", + pytest.param("tutorial001_an_py39", marks=needs_py39), + pytest.param("tutorial001_an_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.query_param_models.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_query_param_model(client: TestClient): + response = client.get( + "/items/", + params={ + "limit": 10, + "offset": 5, + "order_by": "updated_at", + "tags": ["tag1", "tag2"], + }, + ) + assert response.status_code == 200 + assert response.json() == { + "limit": 10, + "offset": 5, + "order_by": "updated_at", + "tags": ["tag1", "tag2"], + } + + +def test_query_param_model_defaults(client: TestClient): + response = client.get("/items/") + assert response.status_code == 200 + assert response.json() == { + "limit": 100, + "offset": 0, + "order_by": "created_at", + "tags": [], + } + + +def test_query_param_model_invalid(client: TestClient): + response = client.get( + "/items/", + params={ + "limit": 150, + "offset": -1, + "order_by": "invalid", + }, + ) + assert response.status_code == 422 + assert response.json() == snapshot( + IsDict( + { + "detail": [ + { + "type": "less_than_equal", + "loc": ["query", "limit"], + "msg": "Input should be less than or equal to 100", + "input": "150", + "ctx": {"le": 100}, + }, + { + "type": "greater_than_equal", + "loc": ["query", "offset"], + "msg": "Input should be greater than or equal to 0", + "input": "-1", + "ctx": {"ge": 0}, + }, + { + "type": "literal_error", + "loc": ["query", "order_by"], + "msg": "Input should be 'created_at' or 'updated_at'", + "input": "invalid", + "ctx": {"expected": "'created_at' or 'updated_at'"}, + }, + ] + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "type": "value_error.number.not_le", + "loc": ["query", "limit"], + "msg": "ensure this value is less than or equal to 100", + "ctx": {"limit_value": 100}, + }, + { + "type": "value_error.number.not_ge", + "loc": ["query", "offset"], + "msg": "ensure this value is greater than or equal to 0", + "ctx": {"limit_value": 0}, + }, + { + "type": "value_error.const", + "loc": ["query", "order_by"], + "msg": "unexpected value; permitted: 'created_at', 'updated_at'", + "ctx": { + "given": "invalid", + "permitted": ["created_at", "updated_at"], + }, + }, + ] + } + ) + ) + + +def test_query_param_model_extra(client: TestClient): + response = client.get( + "/items/", + params={ + "limit": 10, + "offset": 5, + "order_by": "updated_at", + "tags": ["tag1", "tag2"], + "tool": "plumbus", + }, + ) + assert response.status_code == 200 + assert response.json() == { + "limit": 10, + "offset": 5, + "order_by": "updated_at", + "tags": ["tag1", "tag2"], + } + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "name": "limit", + "in": "query", + "required": False, + "schema": { + "type": "integer", + "maximum": 100, + "exclusiveMinimum": 0, + "default": 100, + "title": "Limit", + }, + }, + { + "name": "offset", + "in": "query", + "required": False, + "schema": { + "type": "integer", + "minimum": 0, + "default": 0, + "title": "Offset", + }, + }, + { + "name": "order_by", + "in": "query", + "required": False, + "schema": { + "enum": ["created_at", "updated_at"], + "type": "string", + "default": "created_at", + "title": "Order By", + }, + }, + { + "name": "tags", + "in": "query", + "required": False, + "schema": { + "type": "array", + "items": {"type": "string"}, + "default": [], + "title": "Tags", + }, + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "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", + }, + } + }, + } + ) diff --git a/tests/test_tutorial/test_query_param_models/test_tutorial002.py b/tests/test_tutorial/test_query_param_models/test_tutorial002.py new file mode 100644 index 000000000..4432c9d8a --- /dev/null +++ b/tests/test_tutorial/test_query_param_models/test_tutorial002.py @@ -0,0 +1,282 @@ +import importlib + +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient +from inline_snapshot import snapshot + +from tests.utils import needs_py39, needs_py310, needs_pydanticv1, needs_pydanticv2 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial002", marks=needs_pydanticv2), + pytest.param("tutorial002_py39", marks=[needs_py39, needs_pydanticv2]), + pytest.param("tutorial002_py310", marks=[needs_py310, needs_pydanticv2]), + pytest.param("tutorial002_an", marks=needs_pydanticv2), + pytest.param("tutorial002_an_py39", marks=[needs_py39, needs_pydanticv2]), + pytest.param("tutorial002_an_py310", marks=[needs_py310, needs_pydanticv2]), + pytest.param("tutorial002_pv1", marks=[needs_pydanticv1, needs_pydanticv1]), + pytest.param("tutorial002_pv1_py39", marks=[needs_py39, needs_pydanticv1]), + pytest.param("tutorial002_pv1_py310", marks=[needs_py310, needs_pydanticv1]), + pytest.param("tutorial002_pv1_an", marks=[needs_pydanticv1]), + pytest.param("tutorial002_pv1_an_py39", marks=[needs_py39, needs_pydanticv1]), + pytest.param("tutorial002_pv1_an_py310", marks=[needs_py310, needs_pydanticv1]), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.query_param_models.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_query_param_model(client: TestClient): + response = client.get( + "/items/", + params={ + "limit": 10, + "offset": 5, + "order_by": "updated_at", + "tags": ["tag1", "tag2"], + }, + ) + assert response.status_code == 200 + assert response.json() == { + "limit": 10, + "offset": 5, + "order_by": "updated_at", + "tags": ["tag1", "tag2"], + } + + +def test_query_param_model_defaults(client: TestClient): + response = client.get("/items/") + assert response.status_code == 200 + assert response.json() == { + "limit": 100, + "offset": 0, + "order_by": "created_at", + "tags": [], + } + + +def test_query_param_model_invalid(client: TestClient): + response = client.get( + "/items/", + params={ + "limit": 150, + "offset": -1, + "order_by": "invalid", + }, + ) + assert response.status_code == 422 + assert response.json() == snapshot( + IsDict( + { + "detail": [ + { + "type": "less_than_equal", + "loc": ["query", "limit"], + "msg": "Input should be less than or equal to 100", + "input": "150", + "ctx": {"le": 100}, + }, + { + "type": "greater_than_equal", + "loc": ["query", "offset"], + "msg": "Input should be greater than or equal to 0", + "input": "-1", + "ctx": {"ge": 0}, + }, + { + "type": "literal_error", + "loc": ["query", "order_by"], + "msg": "Input should be 'created_at' or 'updated_at'", + "input": "invalid", + "ctx": {"expected": "'created_at' or 'updated_at'"}, + }, + ] + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "type": "value_error.number.not_le", + "loc": ["query", "limit"], + "msg": "ensure this value is less than or equal to 100", + "ctx": {"limit_value": 100}, + }, + { + "type": "value_error.number.not_ge", + "loc": ["query", "offset"], + "msg": "ensure this value is greater than or equal to 0", + "ctx": {"limit_value": 0}, + }, + { + "type": "value_error.const", + "loc": ["query", "order_by"], + "msg": "unexpected value; permitted: 'created_at', 'updated_at'", + "ctx": { + "given": "invalid", + "permitted": ["created_at", "updated_at"], + }, + }, + ] + } + ) + ) + + +def test_query_param_model_extra(client: TestClient): + response = client.get( + "/items/", + params={ + "limit": 10, + "offset": 5, + "order_by": "updated_at", + "tags": ["tag1", "tag2"], + "tool": "plumbus", + }, + ) + assert response.status_code == 422 + assert response.json() == snapshot( + { + "detail": [ + IsDict( + { + "type": "extra_forbidden", + "loc": ["query", "tool"], + "msg": "Extra inputs are not permitted", + "input": "plumbus", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "type": "value_error.extra", + "loc": ["query", "tool"], + "msg": "extra fields not permitted", + } + ) + ] + } + ) + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "name": "limit", + "in": "query", + "required": False, + "schema": { + "type": "integer", + "maximum": 100, + "exclusiveMinimum": 0, + "default": 100, + "title": "Limit", + }, + }, + { + "name": "offset", + "in": "query", + "required": False, + "schema": { + "type": "integer", + "minimum": 0, + "default": 0, + "title": "Offset", + }, + }, + { + "name": "order_by", + "in": "query", + "required": False, + "schema": { + "enum": ["created_at", "updated_at"], + "type": "string", + "default": "created_at", + "title": "Order By", + }, + }, + { + "name": "tags", + "in": "query", + "required": False, + "schema": { + "type": "array", + "items": {"type": "string"}, + "default": [], + "title": "Tags", + }, + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "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", + }, + } + }, + } + ) From 7eadeb69bdc579f7de92d0e7762a7825a5da192a Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 17 Sep 2024 18:54:35 +0000 Subject: [PATCH 22/24] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index d6d2a05b3..405d70c37 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Features + +* ✨ Add support for Pydantic models for parameters using `Query`, `Cookie`, `Header`. PR [#12199](https://github.com/fastapi/fastapi/pull/12199) by [@tiangolo](https://github.com/tiangolo). + ### Translations * 🌐 Add Portuguese translation for `docs/pt/docs/advanced/security/http-basic-auth.md`. PR [#12195](https://github.com/fastapi/fastapi/pull/12195) by [@ceb10n](https://github.com/ceb10n). From b36047b54ae4f965d03426872dbc1fa1f32c746b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 17 Sep 2024 21:06:26 +0200 Subject: [PATCH 23/24] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 120 ++++++++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 405d70c37..722bc5008 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,126 @@ hide: ## Latest Changes +### Highlights + +Now you can declare `Query`, `Header`, and `Cookie` parameters with Pydantic models. 🎉 + +#### `Query` Parameter Models + +Use Pydantic models for `Query` parameters: + +```python +from typing import Annotated, Literal + +from fastapi import FastAPI, Query +from pydantic import BaseModel, Field + +app = FastAPI() + + +class FilterParams(BaseModel): + limit: int = Field(100, gt=0, le=100) + offset: int = Field(0, ge=0) + order_by: Literal["created_at", "updated_at"] = "created_at" + tags: list[str] = [] + + +@app.get("/items/") +async def read_items(filter_query: Annotated[FilterParams, Query()]): + return filter_query +``` + +Read the new docs: [Query Parameter Models](https://fastapi.tiangolo.com/tutorial/query-param-models/). + +#### `Header` Parameter Models + +Use Pydantic models for `Header` parameters: + +```python +from typing import Annotated + +from fastapi import FastAPI, Header +from pydantic import BaseModel + +app = FastAPI() + + +class CommonHeaders(BaseModel): + host: str + save_data: bool + if_modified_since: str | None = None + traceparent: str | None = None + x_tag: list[str] = [] + + +@app.get("/items/") +async def read_items(headers: Annotated[CommonHeaders, Header()]): + return headers +``` + +Read the new docs: [Header Parameter Models](https://fastapi.tiangolo.com/tutorial/header-param-models/). + +#### `Cookie` Parameter Models + +Use Pydantic models for `Cookie` parameters: + +```python +from typing import Annotated + +from fastapi import Cookie, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Cookies(BaseModel): + session_id: str + fatebook_tracker: str | None = None + googall_tracker: str | None = None + + +@app.get("/items/") +async def read_items(cookies: Annotated[Cookies, Cookie()]): + return cookies +``` + +Read the new docs: [Cookie Parameter Models](https://fastapi.tiangolo.com/tutorial/cookie-param-models/). + +#### Forbid Extra Query (Cookie, Header) Parameters + +Use Pydantic models to restrict extra values for `Query` parameters (also applies to `Header` and `Cookie` parameters). + +To achieve it, use Pydantic's `model_config = {"extra": "forbid"}`: + +```python +from typing import Annotated, Literal + +from fastapi import FastAPI, Query +from pydantic import BaseModel, Field + +app = FastAPI() + + +class FilterParams(BaseModel): + model_config = {"extra": "forbid"} + + limit: int = Field(100, gt=0, le=100) + offset: int = Field(0, ge=0) + order_by: Literal["created_at", "updated_at"] = "created_at" + tags: list[str] = [] + + +@app.get("/items/") +async def read_items(filter_query: Annotated[FilterParams, Query()]): + return filter_query +``` + +This applies to `Query`, `Header`, and `Cookie` parameters, read the new docs: + +* [Forbid Extra Query Parameters](https://fastapi.tiangolo.com/tutorial/query-param-models/#forbid-extra-query-parameters) +* [Forbid Extra Headers](https://fastapi.tiangolo.com/tutorial/header-param-models/#forbid-extra-headers) +* [Forbid Extra Cookies](https://fastapi.tiangolo.com/tutorial/cookie-param-models/#forbid-extra-cookies) + ### Features * ✨ Add support for Pydantic models for parameters using `Query`, `Cookie`, `Header`. PR [#12199](https://github.com/fastapi/fastapi/pull/12199) by [@tiangolo](https://github.com/tiangolo). From 40e33e492dbf4af6172997f4e3238a32e56cbe26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 17 Sep 2024 21:07:35 +0200 Subject: [PATCH 24/24] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.115.?= =?UTF-8?q?0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 722bc5008..ea7ac9215 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +## 0.115.0 + ### Highlights Now you can declare `Query`, `Header`, and `Cookie` parameters with Pydantic models. 🎉 diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 3925d3603..7dd74c28f 100644 --- a/fastapi/__init__.py +++ b/fastapi/__init__.py @@ -1,6 +1,6 @@ """FastAPI framework, high performance, easy to learn, fast to code, ready for production""" -__version__ = "0.114.2" +__version__ = "0.115.0" from starlette import status as status