committed by
GitHub
56 changed files with 5181 additions and 242 deletions
@ -2,7 +2,7 @@ name: Issue Manager |
|||
|
|||
on: |
|||
schedule: |
|||
- cron: "10 3 * * *" |
|||
- cron: "13 22 * * *" |
|||
issue_comment: |
|||
types: |
|||
- created |
|||
@ -16,6 +16,7 @@ on: |
|||
|
|||
permissions: |
|||
issues: write |
|||
pull-requests: write |
|||
|
|||
jobs: |
|||
issue-manager: |
|||
@ -26,7 +27,7 @@ jobs: |
|||
env: |
|||
GITHUB_CONTEXT: ${{ toJson(github) }} |
|||
run: echo "$GITHUB_CONTEXT" |
|||
- uses: tiangolo/[email protected].0 |
|||
- uses: tiangolo/[email protected].1 |
|||
with: |
|||
token: ${{ secrets.GITHUB_TOKEN }} |
|||
config: > |
|||
@ -35,8 +36,8 @@ jobs: |
|||
"delay": 864000, |
|||
"message": "Assuming the original need was handled, this will be automatically closed now. But feel free to add more comments or create new issues or PRs." |
|||
}, |
|||
"changes-requested": { |
|||
"waiting": { |
|||
"delay": 2628000, |
|||
"message": "As this PR had requested changes to be applied but has been inactive for a while, it's now going to be closed. But if there's anyone interested, feel free to create a new PR." |
|||
"message": "As this PR has been waiting for the original user for a while but seems to be inactive, it's now going to be closed. But if there's anyone interested, feel free to create a new PR." |
|||
} |
|||
} |
|||
|
After Width: | Height: | Size: 43 KiB |
@ -0,0 +1,134 @@ |
|||
# Form Models |
|||
|
|||
You can use **Pydantic models** to declare **form fields** in FastAPI. |
|||
|
|||
/// info |
|||
|
|||
To use forms, first install <a href="https://github.com/Kludex/python-multipart" class="external-link" target="_blank">`python-multipart`</a>. |
|||
|
|||
Make sure you create a [virtual environment](../virtual-environments.md){.internal-link target=_blank}, activate it, and then install it, for example: |
|||
|
|||
```console |
|||
$ pip install python-multipart |
|||
``` |
|||
|
|||
/// |
|||
|
|||
/// note |
|||
|
|||
This is supported since FastAPI version `0.113.0`. 🤓 |
|||
|
|||
/// |
|||
|
|||
## Pydantic Models for Forms |
|||
|
|||
You just need to declare a **Pydantic model** with the fields you want to receive as **form fields**, and then declare the parameter as `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 |
|||
|
|||
Prefer to use the `Annotated` version if possible. |
|||
|
|||
/// |
|||
|
|||
```Python hl_lines="7-9 13" |
|||
{!> ../../../docs_src/request_form_models/tutorial001.py!} |
|||
``` |
|||
|
|||
//// |
|||
|
|||
**FastAPI** will **extract** the data for **each field** from the **form data** in the request and give you the Pydantic model you defined. |
|||
|
|||
## Check the Docs |
|||
|
|||
You can verify it in the docs UI at `/docs`: |
|||
|
|||
<div class="screenshot"> |
|||
<img src="/img/tutorial/request-form-models/image01.png"> |
|||
</div> |
|||
|
|||
## Forbid Extra Form Fields |
|||
|
|||
In some special use cases (probably not very common), you might want to **restrict** the form fields to only those declared in the Pydantic model. And **forbid** any **extra** fields. |
|||
|
|||
/// note |
|||
|
|||
This is supported since FastAPI version `0.114.0`. 🤓 |
|||
|
|||
/// |
|||
|
|||
You can use Pydantic's model configuration to `forbid` any `extra` fields: |
|||
|
|||
//// 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 |
|||
|
|||
Prefer to use the `Annotated` version if possible. |
|||
|
|||
/// |
|||
|
|||
```Python hl_lines="10" |
|||
{!> ../../../docs_src/request_form_models/tutorial002.py!} |
|||
``` |
|||
|
|||
//// |
|||
|
|||
If a client tries to send some extra data, they will receive an **error** response. |
|||
|
|||
For example, if the client tries to send the form fields: |
|||
|
|||
* `username`: `Rick` |
|||
* `password`: `Portal Gun` |
|||
* `extra`: `Mr. Poopybutthole` |
|||
|
|||
They will receive an error response telling them that the field `extra` is not allowed: |
|||
|
|||
```json |
|||
{ |
|||
"detail": [ |
|||
{ |
|||
"type": "extra_forbidden", |
|||
"loc": ["body", "extra"], |
|||
"msg": "Extra inputs are not permitted", |
|||
"input": "Mr. Poopybutthole" |
|||
} |
|||
] |
|||
} |
|||
``` |
|||
|
|||
## Summary |
|||
|
|||
You can use Pydantic models to declare form fields in FastAPI. 😎 |
@ -0,0 +1,28 @@ |
|||
# Full Stack FastAPI 템플릿 |
|||
|
|||
템플릿은 일반적으로 특정 설정과 함께 제공되지만, 유연하고 커스터마이징이 가능하게 디자인 되었습니다. 이 특성들은 여러분이 프로젝트의 요구사항에 맞춰 수정, 적용을 할 수 있게 해주고, 템플릿이 완벽한 시작점이 되게 해줍니다. 🏁 |
|||
|
|||
많은 초기 설정, 보안, 데이터베이스 및 일부 API 엔드포인트가 이미 준비되어 있으므로, 여러분은 이 템플릿을 (프로젝트를) 시작하는 데 사용할 수 있습니다. |
|||
|
|||
GitHub 저장소: <a href="https://github.com/tiangolo/full-stack-fastapi-template" class="external-link" target="_blank">Full Stack FastAPI 템플릿</a> |
|||
|
|||
## Full Stack 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): 프론트엔드 컴포넌트. |
|||
- 🤖 자동으로 생성된 프론트엔드 클라이언트. |
|||
- 🧪 E2E 테스트를 위한 Playwright. |
|||
- 🦇 다크 모드 지원. |
|||
- 🐋 [Docker Compose](https://www.docker.com): 개발 환경과 프로덕션(운영). |
|||
- 🔒 기본으로 지원되는 안전한 비밀번호 해싱. |
|||
- 🔑 JWT 토큰 인증. |
|||
- 📫 이메일 기반 비밀번호 복구. |
|||
- ✅ [Pytest]를 이용한 테스트(https://pytest.org). |
|||
- 📞 [Traefik](https://traefik.io): 리버스 프록시 / 로드 밸런서. |
|||
- 🚢 Docker Compose를 이용한 배포 지침: 자동 HTTPS 인증서를 처리하기 위한 프론트엔드 Traefik 프록시 설정 방법을 포함. |
|||
- 🏭 GitHub Actions를 기반으로 CI (지속적인 통합) 및 CD (지속적인 배포). |
@ -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 <abbr title="bijvoorbeeld: str, int, float, bool">type</abbr> 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()`. |
|||
`` |
|||
* <abbr title="Voegt ze samen, als één. Met de inhoud van de een na de ander.">Voeg samen</abbr> 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: |
|||
|
|||
<img src="/img/python-types/image01.png"> |
|||
|
|||
### 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: |
|||
|
|||
<img src="/img/python-types/image02.png"> |
|||
|
|||
Nu kun je de opties bekijken en er doorheen scrollen totdat je de optie vindt die “een belletje doet rinkelen”: |
|||
|
|||
<img src="/img/python-types/image03.png"> |
|||
|
|||
### 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: |
|||
|
|||
<img src="/img/python-types/image04.png"> |
|||
|
|||
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: |
|||
|
|||
<img src="/img/python-types/image05.png"> |
|||
|
|||
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 <abbr title='ook wel "bitwise of operator" genoemd, maar die betekenis is hier niet relevant'>verticale balk (`|`)</abbr>. |
|||
|
|||
//// 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 <abbr title='ook wel "bitwise or operator" genoemd, maar die betekenis is hier niet relevant'>verticale lijn (`|`)</abbr> 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: |
|||
|
|||
<img src="/img/python-types/image06.png"> |
|||
|
|||
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 |
|||
|
|||
<a href="https://docs.pydantic.dev/" class="external-link" target="_blank">Pydantic</a> 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 <a href="https://docs.pydantic.dev/" class="external-link" target="_blank">Pydantic, bekijk de documentatie</a>. |
|||
|
|||
/// |
|||
|
|||
**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 <a href="https://docs.pydantic.dev/2.3/usage/models/#required-fields" class="external-link" target="_blank">Verplichte optionele velden</a>. |
|||
|
|||
/// |
|||
|
|||
## Type Hints met Metadata Annotaties |
|||
|
|||
Python heeft ook een functie waarmee je **extra <abbr title="Data over de data, in dit geval informatie over het type, bijvoorbeeld een beschrijving.">metadata</abbr>** 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 <a href="https://mypy.readthedocs.io/en/latest/cheat_sheet_py3.html" class="external-link" target="_blank">het "cheat sheet" van `mypy`</a>. |
|||
|
|||
/// |
@ -0,0 +1,298 @@ |
|||
# Variáveis de Ambiente |
|||
|
|||
/// tip | "Dica" |
|||
|
|||
Se você já sabe o que são "variáveis de ambiente" e como usá-las, pode pular esta seção. |
|||
|
|||
/// |
|||
|
|||
Uma variável de ambiente (também conhecida como "**env var**") é uma variável que existe **fora** do código Python, no **sistema operacional**, e pode ser lida pelo seu código Python (ou por outros programas também). |
|||
|
|||
Variáveis de ambiente podem ser úteis para lidar com **configurações** do aplicativo, como parte da **instalação** do Python, etc. |
|||
|
|||
## Criar e Usar Variáveis de Ambiente |
|||
|
|||
Você pode **criar** e usar variáveis de ambiente no **shell (terminal)**, sem precisar do Python: |
|||
|
|||
//// tab | Linux, macOS, Windows Bash |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
// Você pode criar uma variável de ambiente MY_NAME com |
|||
$ export MY_NAME="Wade Wilson" |
|||
|
|||
// Então você pode usá-la com outros programas, como |
|||
$ echo "Hello $MY_NAME" |
|||
|
|||
Hello Wade Wilson |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
//// |
|||
|
|||
//// tab | Windows PowerShell |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
// Criar uma variável de ambiente MY_NAME |
|||
$ $Env:MY_NAME = "Wade Wilson" |
|||
|
|||
// Usá-la com outros programas, como |
|||
$ echo "Hello $Env:MY_NAME" |
|||
|
|||
Hello Wade Wilson |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
//// |
|||
|
|||
## Ler Variáveis de Ambiente no Python |
|||
|
|||
Você também pode criar variáveis de ambiente **fora** do Python, no terminal (ou com qualquer outro método) e depois **lê-las no Python**. |
|||
|
|||
Por exemplo, você poderia ter um arquivo `main.py` com: |
|||
|
|||
```Python hl_lines="3" |
|||
import os |
|||
|
|||
name = os.getenv("MY_NAME", "World") |
|||
print(f"Hello {name} from Python") |
|||
``` |
|||
|
|||
/// tip | "Dica" |
|||
|
|||
O segundo argumento para <a href="https://docs.python.org/3.8/library/os.html#os.getenv" class="external-link" target="_blank">`os.getenv()`</a> é o valor padrão a ser retornado. |
|||
|
|||
Se não for fornecido, é `None` por padrão, Aqui fornecemos `"World"` como o valor padrão a ser usado. |
|||
|
|||
/// |
|||
|
|||
Então você poderia chamar esse programa Python: |
|||
|
|||
//// tab | Linux, macOS, Windows Bash |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
// Aqui ainda não definimos a variável de ambiente |
|||
$ python main.py |
|||
|
|||
// Como não definimos a variável de ambiente, obtemos o valor padrão |
|||
|
|||
Hello World from Python |
|||
|
|||
// Mas se criarmos uma variável de ambiente primeiro |
|||
$ export MY_NAME="Wade Wilson" |
|||
|
|||
// E então chamar o programa novamente |
|||
$ python main.py |
|||
|
|||
// Agora ele pode ler a variável de ambiente |
|||
|
|||
Hello Wade Wilson from Python |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
//// |
|||
|
|||
//// tab | Windows PowerShell |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
// Aqui ainda não definimos a variável de ambiente |
|||
$ python main.py |
|||
|
|||
// Como não definimos a variável de ambiente, obtemos o valor padrão |
|||
|
|||
Hello World from Python |
|||
|
|||
// Mas se criarmos uma variável de ambiente primeiro |
|||
$ $Env:MY_NAME = "Wade Wilson" |
|||
|
|||
// E então chamar o programa novamente |
|||
$ python main.py |
|||
|
|||
// Agora ele pode ler a variável de ambiente |
|||
|
|||
Hello Wade Wilson from Python |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
//// |
|||
|
|||
Como as variáveis de ambiente podem ser definidas fora do código, mas podem ser lidas pelo código e não precisam ser armazenadas (com versão no `git`) com o restante dos arquivos, é comum usá-las para configurações ou **definições**. |
|||
|
|||
Você também pode criar uma variável de ambiente apenas para uma **invocação específica do programa**, que só está disponível para aquele programa e apenas pela duração dele. |
|||
|
|||
Para fazer isso, crie-a na mesma linha, antes do próprio programa: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
// Criar uma variável de ambiente MY_NAME para esta chamada de programa |
|||
$ MY_NAME="Wade Wilson" python main.py |
|||
|
|||
// Agora ele pode ler a variável de ambiente |
|||
|
|||
Hello Wade Wilson from Python |
|||
|
|||
// A variável de ambiente não existe mais depois |
|||
$ python main.py |
|||
|
|||
Hello World from Python |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
/// tip | "Dica" |
|||
|
|||
Você pode ler mais sobre isso em <a href="https://12factor.net/config" class="external-link" target="_blank">The Twelve-Factor App: Config</a>. |
|||
|
|||
/// |
|||
|
|||
## Tipos e Validação |
|||
|
|||
Essas variáveis de ambiente só podem lidar com **strings de texto**, pois são externas ao Python e precisam ser compatíveis com outros programas e com o resto do sistema (e até mesmo com diferentes sistemas operacionais, como Linux, Windows, macOS). |
|||
|
|||
Isso significa que **qualquer valor** lido em Python de uma variável de ambiente **será uma `str`**, e qualquer conversão para um tipo diferente ou qualquer validação precisa ser feita no código. |
|||
|
|||
Você aprenderá mais sobre como usar variáveis de ambiente para lidar com **configurações do aplicativo** no [Guia do Usuário Avançado - Configurações e Variáveis de Ambiente](./advanced/settings.md){.internal-link target=_blank}. |
|||
|
|||
## Variável de Ambiente `PATH` |
|||
|
|||
Existe uma variável de ambiente **especial** chamada **`PATH`** que é usada pelos sistemas operacionais (Linux, macOS, Windows) para encontrar programas para executar. |
|||
|
|||
O valor da variável `PATH` é uma longa string composta por diretórios separados por dois pontos `:` no Linux e macOS, e por ponto e vírgula `;` no Windows. |
|||
|
|||
Por exemplo, a variável de ambiente `PATH` poderia ter esta aparência: |
|||
|
|||
//// tab | Linux, macOS |
|||
|
|||
```plaintext |
|||
/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin |
|||
``` |
|||
|
|||
Isso significa que o sistema deve procurar programas nos diretórios: |
|||
|
|||
* `/usr/local/bin` |
|||
* `/usr/bin` |
|||
* `/bin` |
|||
* `/usr/sbin` |
|||
* `/sbin` |
|||
|
|||
//// |
|||
|
|||
//// tab | Windows |
|||
|
|||
```plaintext |
|||
C:\Program Files\Python312\Scripts;C:\Program Files\Python312;C:\Windows\System32 |
|||
``` |
|||
|
|||
Isso significa que o sistema deve procurar programas nos diretórios: |
|||
|
|||
* `C:\Program Files\Python312\Scripts` |
|||
* `C:\Program Files\Python312` |
|||
* `C:\Windows\System32` |
|||
|
|||
//// |
|||
|
|||
Quando você digita um **comando** no terminal, o sistema operacional **procura** o programa em **cada um dos diretórios** listados na variável de ambiente `PATH`. |
|||
|
|||
Por exemplo, quando você digita `python` no terminal, o sistema operacional procura um programa chamado `python` no **primeiro diretório** dessa lista. |
|||
|
|||
Se ele o encontrar, então ele o **usará**. Caso contrário, ele continua procurando nos **outros diretórios**. |
|||
|
|||
### Instalando o Python e Atualizando o `PATH` |
|||
|
|||
Durante a instalação do Python, você pode ser questionado sobre a atualização da variável de ambiente `PATH`. |
|||
|
|||
//// tab | Linux, macOS |
|||
|
|||
Vamos supor que você instale o Python e ele fique em um diretório `/opt/custompython/bin`. |
|||
|
|||
Se você concordar em atualizar a variável de ambiente `PATH`, o instalador adicionará `/opt/custompython/bin` para a variável de ambiente `PATH`. |
|||
|
|||
Poderia parecer assim: |
|||
|
|||
```plaintext |
|||
/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/custompython/bin |
|||
``` |
|||
|
|||
Dessa forma, ao digitar `python` no terminal, o sistema encontrará o programa Python em `/opt/custompython/bin` (último diretório) e o utilizará. |
|||
|
|||
//// |
|||
|
|||
//// tab | Windows |
|||
|
|||
Digamos que você instala o Python e ele acaba em um diretório `C:\opt\custompython\bin`. |
|||
|
|||
Se você disser sim para atualizar a variável de ambiente `PATH`, o instalador adicionará `C:\opt\custompython\bin` à variável de ambiente `PATH`. |
|||
|
|||
```plaintext |
|||
C:\Program Files\Python312\Scripts;C:\Program Files\Python312;C:\Windows\System32;C:\opt\custompython\bin |
|||
``` |
|||
|
|||
Dessa forma, quando você digitar `python` no terminal, o sistema encontrará o programa Python em `C:\opt\custompython\bin` (o último diretório) e o utilizará. |
|||
|
|||
//// |
|||
|
|||
Então, se você digitar: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ python |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
//// tab | Linux, macOS |
|||
|
|||
O sistema **encontrará** o programa `python` em `/opt/custompython/bin` e o executará. |
|||
|
|||
Seria aproximadamente equivalente a digitar: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ /opt/custompython/bin/python |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
//// |
|||
|
|||
//// tab | Windows |
|||
|
|||
O sistema **encontrará** o programa `python` em `C:\opt\custompython\bin\python` e o executará. |
|||
|
|||
Seria aproximadamente equivalente a digitar: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ C:\opt\custompython\bin\python |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
//// |
|||
|
|||
Essas informações serão úteis ao aprender sobre [Ambientes Virtuais](virtual-environments.md){.internal-link target=_blank}. |
|||
|
|||
## Conclusão |
|||
|
|||
Com isso, você deve ter uma compreensão básica do que são **variáveis de ambiente** e como usá-las em Python. |
|||
|
|||
Você também pode ler mais sobre elas na <a href="https://en.wikipedia.org/wiki/Environment_variable" class="external-link" target="_blank">Wikipedia para Variáveis de Ambiente</a>. |
|||
|
|||
Em muitos casos, não é muito óbvio como as variáveis de ambiente seriam úteis e aplicáveis imediatamente. Mas elas continuam aparecendo em muitos cenários diferentes quando você está desenvolvendo, então é bom saber sobre elas. |
|||
|
|||
Por exemplo, você precisará dessas informações na próxima seção, sobre [Ambientes Virtuais](virtual-environments.md). |
@ -0,0 +1,115 @@ |
|||
# Depuração |
|||
|
|||
Você pode conectar o depurador no seu editor, por exemplo, com o Visual Studio Code ou PyCharm. |
|||
|
|||
## Chamar `uvicorn` |
|||
|
|||
Em seu aplicativo FastAPI, importe e execute `uvicorn` diretamente: |
|||
|
|||
```Python hl_lines="1 15" |
|||
{!../../../docs_src/debugging/tutorial001.py!} |
|||
``` |
|||
|
|||
### Sobre `__name__ == "__main__"` |
|||
|
|||
O objetivo principal de `__name__ == "__main__"` é ter algum código que seja executado quando seu arquivo for chamado com: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ python myapp.py |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
mas não é chamado quando outro arquivo o importa, como em: |
|||
|
|||
```Python |
|||
from myapp import app |
|||
``` |
|||
|
|||
#### Mais detalhes |
|||
|
|||
Digamos que seu arquivo se chama `myapp.py`. |
|||
|
|||
Se você executá-lo com: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ python myapp.py |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
então a variável interna `__name__` no seu arquivo, criada automaticamente pelo Python, terá como valor a string `"__main__"`. |
|||
|
|||
Então, a seção: |
|||
|
|||
```Python |
|||
uvicorn.run(app, host="0.0.0.0", port=8000) |
|||
``` |
|||
|
|||
vai executar. |
|||
|
|||
--- |
|||
|
|||
Isso não acontecerá se você importar esse módulo (arquivo). |
|||
|
|||
Então, se você tiver outro arquivo `importer.py` com: |
|||
|
|||
```Python |
|||
from myapp import app |
|||
|
|||
# Mais um pouco de código |
|||
``` |
|||
|
|||
nesse caso, a variável criada automaticamente dentro de `myapp.py` não terá a variável `__name__` com o valor `"__main__"`. |
|||
|
|||
Então, a linha: |
|||
|
|||
```Python |
|||
uvicorn.run(app, host="0.0.0.0", port=8000) |
|||
``` |
|||
|
|||
não será executada. |
|||
|
|||
/// info | "Informação" |
|||
|
|||
Para mais informações, consulte <a href="https://docs.python.org/3/library/__main__.html" class="external-link" target="_blank">a documentação oficial do Python</a>. |
|||
|
|||
/// |
|||
|
|||
## Execute seu código com seu depurador |
|||
|
|||
Como você está executando o servidor Uvicorn diretamente do seu código, você pode chamar seu programa Python (seu aplicativo FastAPI) diretamente do depurador. |
|||
|
|||
--- |
|||
|
|||
Por exemplo, no Visual Studio Code, você pode: |
|||
|
|||
* Ir para o painel "Debug". |
|||
* "Add configuration...". |
|||
* Selecionar "Python" |
|||
* Executar o depurador com a opção "`Python: Current File (Integrated Terminal)`". |
|||
|
|||
Em seguida, ele iniciará o servidor com seu código **FastAPI**, parará em seus pontos de interrupção, etc. |
|||
|
|||
Veja como pode parecer: |
|||
|
|||
<img src="/img/tutorial/debugging/image01.png"> |
|||
|
|||
--- |
|||
|
|||
Se você usar o Pycharm, você pode: |
|||
|
|||
* Abrir o menu "Executar". |
|||
* Selecionar a opção "Depurar...". |
|||
* Então um menu de contexto aparece. |
|||
* Selecionar o arquivo para depurar (neste caso, `main.py`). |
|||
|
|||
Em seguida, ele iniciará o servidor com seu código **FastAPI**, parará em seus pontos de interrupção, etc. |
|||
|
|||
Veja como pode parecer: |
|||
|
|||
<img src="/img/tutorial/debugging/image02.png"> |
@ -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 <a href="https://github.com/Kludex/python-multipart" class="external-link" target="_blank">`python-multipart`</a>. |
|||
|
|||
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`: |
|||
|
|||
<div class="screenshot"> |
|||
<img src="/img/tutorial/request-form-models/image01.png"> |
|||
</div> |
|||
|
|||
## 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. 😎 |
@ -0,0 +1,249 @@ |
|||
# Testando |
|||
|
|||
Graças ao <a href="https://www.starlette.io/testclient/" class="external-link" target="_blank">Starlette</a>, testar aplicativos **FastAPI** é fácil e agradável. |
|||
|
|||
Ele é baseado no <a href="https://www.python-httpx.org" class="external-link" target="_blank">HTTPX</a>, que por sua vez é projetado com base em Requests, por isso é muito familiar e intuitivo. |
|||
|
|||
Com ele, você pode usar o <a href="https://docs.pytest.org/" class="external-link" target="_blank">pytest</a> diretamente com **FastAPI**. |
|||
|
|||
## Usando `TestClient` |
|||
|
|||
/// info | "Informação" |
|||
|
|||
Para usar o `TestClient`, primeiro instale o <a href="https://www.python-httpx.org" class="external-link" target="_blank">`httpx`</a>. |
|||
|
|||
Certifique-se de criar um [ambiente virtual](../virtual-environments.md){.internal-link target=_blank}, ativá-lo e instalá-lo, por exemplo: |
|||
|
|||
```console |
|||
$ pip install httpx |
|||
``` |
|||
|
|||
/// |
|||
|
|||
Importe `TestClient`. |
|||
|
|||
Crie um `TestClient` passando seu aplicativo **FastAPI** para ele. |
|||
|
|||
Crie funções com um nome que comece com `test_` (essa é a convenção padrão do `pytest`). |
|||
|
|||
Use o objeto `TestClient` da mesma forma que você faz com `httpx`. |
|||
|
|||
Escreva instruções `assert` simples com as expressões Python padrão que você precisa verificar (novamente, `pytest` padrão). |
|||
|
|||
```Python hl_lines="2 12 15-18" |
|||
{!../../../docs_src/app_testing/tutorial001.py!} |
|||
``` |
|||
|
|||
/// tip | "Dica" |
|||
|
|||
Observe que as funções de teste são `def` normais, não `async def`. |
|||
|
|||
E as chamadas para o cliente também são chamadas normais, não usando `await`. |
|||
|
|||
Isso permite que você use `pytest` diretamente sem complicações. |
|||
|
|||
/// |
|||
|
|||
/// note | "Detalhes técnicos" |
|||
|
|||
Você também pode usar `from starlette.testclient import TestClient`. |
|||
|
|||
**FastAPI** fornece o mesmo `starlette.testclient` que `fastapi.testclient` apenas como uma conveniência para você, o desenvolvedor. Mas ele vem diretamente da Starlette. |
|||
|
|||
/// |
|||
|
|||
/// tip | "Dica" |
|||
|
|||
Se você quiser chamar funções `async` em seus testes além de enviar solicitações ao seu aplicativo FastAPI (por exemplo, funções de banco de dados assíncronas), dê uma olhada em [Testes assíncronos](../advanced/async-tests.md){.internal-link target=_blank} no tutorial avançado. |
|||
|
|||
/// |
|||
|
|||
## Separando testes |
|||
|
|||
Em uma aplicação real, você provavelmente teria seus testes em um arquivo diferente. |
|||
|
|||
E seu aplicativo **FastAPI** também pode ser composto de vários arquivos/módulos, etc. |
|||
|
|||
### Arquivo do aplicativo **FastAPI** |
|||
|
|||
Digamos que você tenha uma estrutura de arquivo conforme descrito em [Aplicativos maiores](bigger-applications.md){.internal-link target=_blank}: |
|||
|
|||
``` |
|||
. |
|||
├── app |
|||
│ ├── __init__.py |
|||
│ └── main.py |
|||
``` |
|||
|
|||
No arquivo `main.py` você tem seu aplicativo **FastAPI**: |
|||
|
|||
|
|||
```Python |
|||
{!../../../docs_src/app_testing/main.py!} |
|||
``` |
|||
|
|||
### Arquivo de teste |
|||
|
|||
Então você poderia ter um arquivo `test_main.py` com seus testes. Ele poderia estar no mesmo pacote Python (o mesmo diretório com um arquivo `__init__.py`): |
|||
|
|||
``` hl_lines="5" |
|||
. |
|||
├── app |
|||
│ ├── __init__.py |
|||
│ ├── main.py |
|||
│ └── test_main.py |
|||
``` |
|||
|
|||
Como esse arquivo está no mesmo pacote, você pode usar importações relativas para importar o objeto `app` do módulo `main` (`main.py`): |
|||
|
|||
```Python hl_lines="3" |
|||
{!../../../docs_src/app_testing/test_main.py!} |
|||
``` |
|||
|
|||
...e ter o código para os testes como antes. |
|||
|
|||
## Testando: exemplo estendido |
|||
|
|||
Agora vamos estender este exemplo e adicionar mais detalhes para ver como testar diferentes partes. |
|||
|
|||
### Arquivo de aplicativo **FastAPI** estendido |
|||
|
|||
Vamos continuar com a mesma estrutura de arquivo de antes: |
|||
|
|||
``` |
|||
. |
|||
├── app |
|||
│ ├── __init__.py |
|||
│ ├── main.py |
|||
│ └── test_main.py |
|||
``` |
|||
|
|||
Digamos que agora o arquivo `main.py` com seu aplicativo **FastAPI** tenha algumas outras **operações de rotas**. |
|||
|
|||
Ele tem uma operação `GET` que pode retornar um erro. |
|||
|
|||
Ele tem uma operação `POST` que pode retornar vários erros. |
|||
|
|||
Ambas as *operações de rotas* requerem um cabeçalho `X-Token`. |
|||
|
|||
//// tab | Python 3.10+ |
|||
|
|||
```Python |
|||
{!> ../../../docs_src/app_testing/app_b_an_py310/main.py!} |
|||
``` |
|||
|
|||
//// |
|||
|
|||
//// tab | Python 3.9+ |
|||
|
|||
```Python |
|||
{!> ../../../docs_src/app_testing/app_b_an_py39/main.py!} |
|||
``` |
|||
|
|||
//// |
|||
|
|||
//// tab | Python 3.8+ |
|||
|
|||
```Python |
|||
{!> ../../../docs_src/app_testing/app_b_an/main.py!} |
|||
``` |
|||
|
|||
//// |
|||
|
|||
//// tab | Python 3.10+ non-Annotated |
|||
|
|||
/// tip | "Dica" |
|||
|
|||
Prefira usar a versão `Annotated` se possível. |
|||
|
|||
/// |
|||
|
|||
```Python |
|||
{!> ../../../docs_src/app_testing/app_b_py310/main.py!} |
|||
``` |
|||
|
|||
//// |
|||
|
|||
//// tab | Python 3.8+ non-Annotated |
|||
|
|||
/// tip | "Dica" |
|||
|
|||
Prefira usar a versão `Annotated` se possível. |
|||
|
|||
/// |
|||
|
|||
```Python |
|||
{!> ../../../docs_src/app_testing/app_b/main.py!} |
|||
``` |
|||
|
|||
//// |
|||
|
|||
### Arquivo de teste estendido |
|||
|
|||
Você pode então atualizar `test_main.py` com os testes estendidos: |
|||
|
|||
```Python |
|||
{!> ../../../docs_src/app_testing/app_b/test_main.py!} |
|||
``` |
|||
|
|||
Sempre que você precisar que o cliente passe informações na requisição e não souber como, você pode pesquisar (no Google) como fazer isso no `httpx`, ou até mesmo como fazer isso com `requests`, já que o design do HTTPX é baseado no design do Requests. |
|||
|
|||
Depois é só fazer o mesmo nos seus testes. |
|||
|
|||
Por exemplo: |
|||
|
|||
* Para passar um parâmetro *path* ou *query*, adicione-o à própria URL. |
|||
* Para passar um corpo JSON, passe um objeto Python (por exemplo, um `dict`) para o parâmetro `json`. |
|||
* Se você precisar enviar *Dados de Formulário* em vez de JSON, use o parâmetro `data`. |
|||
* Para passar *headers*, use um `dict` no parâmetro `headers`. |
|||
* Para *cookies*, um `dict` no parâmetro `cookies`. |
|||
|
|||
Para mais informações sobre como passar dados para o backend (usando `httpx` ou `TestClient`), consulte a <a href="https://www.python-httpx.org" class="external-link" target="_blank">documentação do HTTPX</a>. |
|||
|
|||
/// info | "Informação" |
|||
|
|||
Observe que o `TestClient` recebe dados que podem ser convertidos para JSON, não para modelos Pydantic. |
|||
|
|||
Se você tiver um modelo Pydantic em seu teste e quiser enviar seus dados para o aplicativo durante o teste, poderá usar o `jsonable_encoder` descrito em [Codificador compatível com JSON](encoder.md){.internal-link target=_blank}. |
|||
|
|||
/// |
|||
|
|||
## Execute-o |
|||
|
|||
Depois disso, você só precisa instalar o `pytest`. |
|||
|
|||
Certifique-se de criar um [ambiente virtual](../virtual-environments.md){.internal-link target=_blank}, ativá-lo e instalá-lo, por exemplo: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ pip install pytest |
|||
|
|||
---> 100% |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
Ele detectará os arquivos e os testes automaticamente, os executará e informará os resultados para você. |
|||
|
|||
Execute os testes com: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ pytest |
|||
|
|||
================ test session starts ================ |
|||
platform linux -- Python 3.6.9, pytest-5.3.5, py-1.8.1, pluggy-0.13.1 |
|||
rootdir: /home/user/code/superawesome-cli/app |
|||
plugins: forked-1.1.3, xdist-1.31.0, cov-2.8.1 |
|||
collected 6 items |
|||
|
|||
---> 100% |
|||
|
|||
test_main.py <span style="color: green; white-space: pre;">...... [100%]</span> |
|||
|
|||
<span style="color: green;">================= 1 passed in 0.03s =================</span> |
|||
``` |
|||
|
|||
</div> |
@ -0,0 +1,844 @@ |
|||
# Ambientes Virtuais |
|||
|
|||
Ao trabalhar em projetos Python, você provavelmente deve usar um **ambiente virtual** (ou um mecanismo similar) para isolar os pacotes que você instala para cada projeto. |
|||
|
|||
/// info | "Informação" |
|||
|
|||
Se você já sabe sobre ambientes virtuais, como criá-los e usá-los, talvez seja melhor pular esta seção. 🤓 |
|||
|
|||
/// |
|||
|
|||
/// tip | "Dica" |
|||
|
|||
Um **ambiente virtual** é diferente de uma **variável de ambiente**. |
|||
|
|||
Uma **variável de ambiente** é uma variável no sistema que pode ser usada por programas. |
|||
|
|||
Um **ambiente virtual** é um diretório com alguns arquivos. |
|||
|
|||
/// |
|||
|
|||
/// info | "Informação" |
|||
|
|||
Esta página lhe ensinará como usar **ambientes virtuais** e como eles funcionam. |
|||
|
|||
Se você estiver pronto para adotar uma **ferramenta que gerencia tudo** para você (incluindo a instalação do Python), experimente <a href="https://github.com/astral-sh/uv" class="external-link" target="_blank">uv</a>. |
|||
|
|||
/// |
|||
|
|||
## Criar um Projeto |
|||
|
|||
Primeiro, crie um diretório para seu projeto. |
|||
|
|||
O que normalmente faço é criar um diretório chamado `code` dentro do meu diretório home/user. |
|||
|
|||
E dentro disso eu crio um diretório por projeto. |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
// Vá para o diretório inicial |
|||
$ cd |
|||
// Crie um diretório para todos os seus projetos de código |
|||
$ mkdir code |
|||
// Entre nesse diretório de código |
|||
$ cd code |
|||
// Crie um diretório para este projeto |
|||
$ mkdir awesome-project |
|||
// Entre no diretório do projeto |
|||
$ cd awesome-project |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
## Crie um ambiente virtual |
|||
|
|||
Ao começar a trabalhar em um projeto Python **pela primeira vez**, crie um ambiente virtual **<abbr title="existem outras opções, esta é uma diretriz simples">dentro do seu projeto</abbr>**. |
|||
|
|||
/// tip | "Dica" |
|||
|
|||
Você só precisa fazer isso **uma vez por projeto**, não toda vez que trabalhar. |
|||
|
|||
/// |
|||
|
|||
//// tab | `venv` |
|||
|
|||
Para criar um ambiente virtual, você pode usar o módulo `venv` que vem com o Python. |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ python -m venv .venv |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
/// details | O que esse comando significa |
|||
|
|||
* `python`: usa o programa chamado `python` |
|||
* `-m`: chama um módulo como um script, nós diremos a ele qual módulo vem em seguida |
|||
* `venv`: usa o módulo chamado `venv` que normalmente vem instalado com o Python |
|||
* `.venv`: cria o ambiente virtual no novo diretório `.venv` |
|||
|
|||
/// |
|||
|
|||
//// |
|||
|
|||
//// tab | `uv` |
|||
|
|||
Se você tiver o <a href="https://github.com/astral-sh/uv" class="external-link" target="_blank">`uv`</a> instalado, poderá usá-lo para criar um ambiente virtual. |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ uv venv |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
/// tip | "Dica" |
|||
|
|||
Por padrão, `uv` criará um ambiente virtual em um diretório chamado `.venv`. |
|||
|
|||
Mas você pode personalizá-lo passando um argumento adicional com o nome do diretório. |
|||
|
|||
/// |
|||
|
|||
//// |
|||
|
|||
Esse comando cria um novo ambiente virtual em um diretório chamado `.venv`. |
|||
|
|||
/// details | `.venv` ou outro nome |
|||
|
|||
Você pode criar o ambiente virtual em um diretório diferente, mas há uma convenção para chamá-lo de `.venv`. |
|||
|
|||
/// |
|||
|
|||
## Ative o ambiente virtual |
|||
|
|||
Ative o novo ambiente virtual para que qualquer comando Python que você executar ou pacote que você instalar o utilize. |
|||
|
|||
/// tip | "Dica" |
|||
|
|||
Faça isso **toda vez** que iniciar uma **nova sessão de terminal** para trabalhar no projeto. |
|||
|
|||
/// |
|||
|
|||
//// tab | Linux, macOS |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ source .venv/bin/activate |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
//// |
|||
|
|||
//// tab | Windows PowerShell |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ .venv\Scripts\Activate.ps1 |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
//// |
|||
|
|||
//// tab | Windows Bash |
|||
|
|||
Ou se você usa o Bash para Windows (por exemplo, <a href="https://gitforwindows.org/" class="external-link" target="_blank">Git Bash</a>): |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ source .venv/Scripts/activate |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
//// |
|||
|
|||
/// tip | "Dica" |
|||
|
|||
Toda vez que você instalar um **novo pacote** naquele ambiente, **ative** o ambiente novamente. |
|||
|
|||
Isso garante que, se você usar um **programa de terminal (<abbr title="interface de linha de comando">CLI</abbr>)** instalado por esse pacote, você usará aquele do seu ambiente virtual e não qualquer outro que possa ser instalado globalmente, provavelmente com uma versão diferente do que você precisa. |
|||
|
|||
/// |
|||
|
|||
## Verifique se o ambiente virtual está ativo |
|||
|
|||
Verifique se o ambiente virtual está ativo (o comando anterior funcionou). |
|||
|
|||
/// tip | "Dica" |
|||
|
|||
Isso é **opcional**, mas é uma boa maneira de **verificar** se tudo está funcionando conforme o esperado e se você está usando o ambiente virtual pretendido. |
|||
|
|||
/// |
|||
|
|||
//// tab | Linux, macOS, Windows Bash |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ which python |
|||
|
|||
/home/user/code/awesome-project/.venv/bin/python |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
Se ele mostrar o binário `python` em `.venv/bin/python`, dentro do seu projeto (neste caso `awesome-project`), então funcionou. 🎉 |
|||
|
|||
//// |
|||
|
|||
//// tab | Windows PowerShell |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ Get-Command python |
|||
|
|||
C:\Users\user\code\awesome-project\.venv\Scripts\python |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
Se ele mostrar o binário `python` em `.venv\Scripts\python`, dentro do seu projeto (neste caso `awesome-project`), então funcionou. 🎉 |
|||
|
|||
//// |
|||
|
|||
## Atualizar `pip` |
|||
|
|||
/// tip | "Dica" |
|||
|
|||
Se você usar <a href="https://github.com/astral-sh/uv" class="external-link" target="_blank">`uv`</a>, você o usará para instalar coisas em vez do `pip`, então não precisará atualizar o `pip`. 😎 |
|||
|
|||
/// |
|||
|
|||
Se você estiver usando `pip` para instalar pacotes (ele vem por padrão com o Python), você deve **atualizá-lo** para a versão mais recente. |
|||
|
|||
Muitos erros exóticos durante a instalação de um pacote são resolvidos apenas atualizando o `pip` primeiro. |
|||
|
|||
/// tip | "Dica" |
|||
|
|||
Normalmente, você faria isso **uma vez**, logo após criar o ambiente virtual. |
|||
|
|||
/// |
|||
|
|||
Certifique-se de que o ambiente virtual esteja ativo (com o comando acima) e execute: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ python -m pip install --upgrade pip |
|||
|
|||
---> 100% |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
## Adicionar `.gitignore` |
|||
|
|||
Se você estiver usando **Git** (você deveria), adicione um arquivo `.gitignore` para excluir tudo em seu `.venv` do Git. |
|||
|
|||
/// tip | "Dica" |
|||
|
|||
Se você usou <a href="https://github.com/astral-sh/uv" class="external-link" target="_blank">`uv`</a> para criar o ambiente virtual, ele já fez isso para você, você pode pular esta etapa. 😎 |
|||
|
|||
/// |
|||
|
|||
/// tip | "Dica" |
|||
|
|||
Faça isso **uma vez**, logo após criar o ambiente virtual. |
|||
|
|||
/// |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ echo "*" > .venv/.gitignore |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
/// details | O que esse comando significa |
|||
|
|||
* `echo "*"`: irá "imprimir" o texto `*` no terminal (a próxima parte muda isso um pouco) |
|||
* `>`: qualquer coisa impressa no terminal pelo comando à esquerda de `>` não deve ser impressa, mas sim escrita no arquivo que vai à direita de `>` |
|||
* `.gitignore`: o nome do arquivo onde o texto deve ser escrito |
|||
|
|||
E `*` para Git significa "tudo". Então, ele ignorará tudo no diretório `.venv`. |
|||
|
|||
Esse comando criará um arquivo `.gitignore` com o conteúdo: |
|||
|
|||
```gitignore |
|||
* |
|||
``` |
|||
|
|||
/// |
|||
|
|||
## Instalar Pacotes |
|||
|
|||
Após ativar o ambiente, você pode instalar pacotes nele. |
|||
|
|||
/// tip | "Dica" |
|||
|
|||
Faça isso **uma vez** ao instalar ou atualizar os pacotes que seu projeto precisa. |
|||
|
|||
Se precisar atualizar uma versão ou adicionar um novo pacote, você **fará isso novamente**. |
|||
|
|||
/// |
|||
|
|||
### Instalar pacotes diretamente |
|||
|
|||
Se estiver com pressa e não quiser usar um arquivo para declarar os requisitos de pacote do seu projeto, você pode instalá-los diretamente. |
|||
|
|||
/// tip | "Dica" |
|||
|
|||
É uma (muito) boa ideia colocar os pacotes e versões que seu programa precisa em um arquivo (por exemplo `requirements.txt` ou `pyproject.toml`). |
|||
|
|||
/// |
|||
|
|||
//// tab | `pip` |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ pip install "fastapi[standard]" |
|||
|
|||
---> 100% |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
//// |
|||
|
|||
//// tab | `uv` |
|||
|
|||
Se você tem o <a href="https://github.com/astral-sh/uv" class="external-link" target="_blank">`uv`</a>: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ uv pip install "fastapi[standard]" |
|||
---> 100% |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
//// |
|||
|
|||
### Instalar a partir de `requirements.txt` |
|||
|
|||
Se você tiver um `requirements.txt`, agora poderá usá-lo para instalar seus pacotes. |
|||
|
|||
//// tab | `pip` |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ pip install -r requirements.txt |
|||
---> 100% |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
//// |
|||
|
|||
//// tab | `uv` |
|||
|
|||
Se você tem o <a href="https://github.com/astral-sh/uv" class="external-link" target="_blank">`uv`</a>: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ uv pip install -r requirements.txt |
|||
---> 100% |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
//// |
|||
|
|||
/// details | `requirements.txt` |
|||
|
|||
Um `requirements.txt` com alguns pacotes poderia se parecer com: |
|||
|
|||
```requirements.txt |
|||
fastapi[standard]==0.113.0 |
|||
pydantic==2.8.0 |
|||
``` |
|||
|
|||
/// |
|||
|
|||
## Execute seu programa |
|||
|
|||
Depois de ativar o ambiente virtual, você pode executar seu programa, e ele usará o Python dentro do seu ambiente virtual com os pacotes que você instalou lá. |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ python main.py |
|||
|
|||
Hello World |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
## Configure seu editor |
|||
|
|||
Você provavelmente usaria um editor. Certifique-se de configurá-lo para usar o mesmo ambiente virtual que você criou (ele provavelmente o detectará automaticamente) para que você possa obter erros de preenchimento automático e em linha. |
|||
|
|||
Por exemplo: |
|||
|
|||
* <a href="https://code.visualstudio.com/docs/python/environments#_select-and-activate-an-environment" class="external-link" target="_blank">VS Code</a> |
|||
* <a href="https://www.jetbrains.com/help/pycharm/creating-virtual-environment.html" class="external-link" target="_blank">PyCharm</a> |
|||
|
|||
/// tip | "Dica" |
|||
|
|||
Normalmente, você só precisa fazer isso **uma vez**, ao criar o ambiente virtual. |
|||
|
|||
/// |
|||
|
|||
## Desativar o ambiente virtual |
|||
|
|||
Quando terminar de trabalhar no seu projeto, você pode **desativar** o ambiente virtual. |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ deactivate |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
Dessa forma, quando você executar `python`, ele não tentará executá-lo naquele ambiente virtual com os pacotes instalados nele. |
|||
|
|||
## Pronto para trabalhar |
|||
|
|||
Agora você está pronto para começar a trabalhar no seu projeto. |
|||
|
|||
|
|||
|
|||
/// tip | "Dica" |
|||
|
|||
Você quer entender o que é tudo isso acima? |
|||
|
|||
Continue lendo. 👇🤓 |
|||
|
|||
/// |
|||
|
|||
## Por que ambientes virtuais |
|||
|
|||
Para trabalhar com o FastAPI, você precisa instalar o <a href="https://www.python.org/" class="external-link" target="_blank">Python</a>. |
|||
|
|||
Depois disso, você precisará **instalar** o FastAPI e quaisquer outros **pacotes** que queira usar. |
|||
|
|||
Para instalar pacotes, você normalmente usaria o comando `pip` que vem com o Python (ou alternativas semelhantes). |
|||
|
|||
No entanto, se você usar `pip` diretamente, os pacotes serão instalados no seu **ambiente Python global** (a instalação global do Python). |
|||
|
|||
### O Problema |
|||
|
|||
Então, qual é o problema em instalar pacotes no ambiente global do Python? |
|||
|
|||
Em algum momento, você provavelmente acabará escrevendo muitos programas diferentes que dependem de **pacotes diferentes**. E alguns desses projetos em que você trabalha dependerão de **versões diferentes** do mesmo pacote. 😱 |
|||
|
|||
Por exemplo, você pode criar um projeto chamado `philosophers-stone`, este programa depende de outro pacote chamado **`harry`, usando a versão `1`**. Então, você precisa instalar `harry`. |
|||
|
|||
```mermaid |
|||
flowchart LR |
|||
stone(philosophers-stone) -->|requires| harry-1[harry v1] |
|||
``` |
|||
|
|||
Então, em algum momento depois, você cria outro projeto chamado `prisoner-of-azkaban`, e esse projeto também depende de `harry`, mas esse projeto precisa do **`harry` versão `3`**. |
|||
|
|||
```mermaid |
|||
flowchart LR |
|||
azkaban(prisoner-of-azkaban) --> |requires| harry-3[harry v3] |
|||
``` |
|||
|
|||
Mas agora o problema é que, se você instalar os pacotes globalmente (no ambiente global) em vez de em um **ambiente virtual** local, você terá que escolher qual versão do `harry` instalar. |
|||
|
|||
Se você quiser executar `philosophers-stone`, precisará primeiro instalar `harry` versão `1`, por exemplo com: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ pip install "harry==1" |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
E então você acabaria com `harry` versão `1` instalado em seu ambiente Python global. |
|||
|
|||
```mermaid |
|||
flowchart LR |
|||
subgraph global[global env] |
|||
harry-1[harry v1] |
|||
end |
|||
subgraph stone-project[philosophers-stone project] |
|||
stone(philosophers-stone) -->|requires| harry-1 |
|||
end |
|||
``` |
|||
|
|||
Mas se você quiser executar `prisoner-of-azkaban`, você precisará desinstalar `harry` versão `1` e instalar `harry` versão `3` (ou apenas instalar a versão `3` desinstalaria automaticamente a versão `1`). |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ pip install "harry==3" |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
E então você acabaria com `harry` versão `3` instalado em seu ambiente Python global. |
|||
|
|||
E se você tentar executar `philosophers-stone` novamente, há uma chance de que **não funcione** porque ele precisa de `harry` versão `1`. |
|||
|
|||
```mermaid |
|||
flowchart LR |
|||
subgraph global[global env] |
|||
harry-1[<strike>harry v1</strike>] |
|||
style harry-1 fill:#ccc,stroke-dasharray: 5 5 |
|||
harry-3[harry v3] |
|||
end |
|||
subgraph stone-project[philosophers-stone project] |
|||
stone(philosophers-stone) -.-x|⛔️| harry-1 |
|||
end |
|||
subgraph azkaban-project[prisoner-of-azkaban project] |
|||
azkaban(prisoner-of-azkaban) --> |requires| harry-3 |
|||
end |
|||
``` |
|||
|
|||
/// tip | "Dica" |
|||
|
|||
É muito comum em pacotes Python tentar ao máximo **evitar alterações drásticas** em **novas versões**, mas é melhor prevenir do que remediar e instalar versões mais recentes intencionalmente e, quando possível, executar os testes para verificar se tudo está funcionando corretamente. |
|||
|
|||
/// |
|||
|
|||
Agora, imagine isso com **muitos** outros **pacotes** dos quais todos os seus **projetos dependem**. Isso é muito difícil de gerenciar. E você provavelmente acabaria executando alguns projetos com algumas **versões incompatíveis** dos pacotes, e não saberia por que algo não está funcionando. |
|||
|
|||
Além disso, dependendo do seu sistema operacional (por exemplo, Linux, Windows, macOS), ele pode ter vindo com o Python já instalado. E, nesse caso, provavelmente tinha alguns pacotes pré-instalados com algumas versões específicas **necessárias para o seu sistema**. Se você instalar pacotes no ambiente global do Python, poderá acabar **quebrando** alguns dos programas que vieram com seu sistema operacional. |
|||
|
|||
## Onde os pacotes são instalados |
|||
|
|||
Quando você instala o Python, ele cria alguns diretórios com alguns arquivos no seu computador. |
|||
|
|||
Alguns desses diretórios são os responsáveis por ter todos os pacotes que você instala. |
|||
|
|||
Quando você executa: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
// Não execute isso agora, é apenas um exemplo 🤓 |
|||
$ pip install "fastapi[standard]" |
|||
---> 100% |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
Isso fará o download de um arquivo compactado com o código FastAPI, normalmente do <a href="https://pypi.org/project/fastapi/" class="external-link" target="_blank">PyPI</a>. |
|||
|
|||
Ele também fará o **download** de arquivos para outros pacotes dos quais o FastAPI depende. |
|||
|
|||
Em seguida, ele **extrairá** todos esses arquivos e os colocará em um diretório no seu computador. |
|||
|
|||
Por padrão, ele colocará os arquivos baixados e extraídos no diretório que vem com a instalação do Python, que é o **ambiente global**. |
|||
|
|||
## O que são ambientes virtuais |
|||
|
|||
A solução para os problemas de ter todos os pacotes no ambiente global é usar um **ambiente virtual para cada projeto** em que você trabalha. |
|||
|
|||
Um ambiente virtual é um **diretório**, muito semelhante ao global, onde você pode instalar os pacotes para um projeto. |
|||
|
|||
Dessa forma, cada projeto terá seu próprio ambiente virtual (diretório `.venv`) com seus próprios pacotes. |
|||
|
|||
```mermaid |
|||
flowchart TB |
|||
subgraph stone-project[philosophers-stone project] |
|||
stone(philosophers-stone) --->|requires| harry-1 |
|||
subgraph venv1[.venv] |
|||
harry-1[harry v1] |
|||
end |
|||
end |
|||
subgraph azkaban-project[prisoner-of-azkaban project] |
|||
azkaban(prisoner-of-azkaban) --->|requires| harry-3 |
|||
subgraph venv2[.venv] |
|||
harry-3[harry v3] |
|||
end |
|||
end |
|||
stone-project ~~~ azkaban-project |
|||
``` |
|||
|
|||
## O que significa ativar um ambiente virtual |
|||
|
|||
Quando você ativa um ambiente virtual, por exemplo com: |
|||
|
|||
//// tab | Linux, macOS |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ source .venv/bin/activate |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
//// |
|||
|
|||
//// tab | Windows PowerShell |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ .venv\Scripts\Activate.ps1 |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
//// |
|||
|
|||
//// tab | Windows Bash |
|||
|
|||
Ou se você usa o Bash para Windows (por exemplo, <a href="https://gitforwindows.org/" class="external-link" target="_blank">Git Bash</a>): |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ source .venv/Scripts/activate |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
//// |
|||
|
|||
Esse comando criará ou modificará algumas [variáveis de ambiente](environment-variables.md){.internal-link target=_blank} que estarão disponíveis para os próximos comandos. |
|||
|
|||
Uma dessas variáveis é a variável `PATH`. |
|||
|
|||
/// tip | "Dica" |
|||
|
|||
Você pode aprender mais sobre a variável de ambiente `PATH` na seção [Variáveis de ambiente](environment-variables.md#path-environment-variable){.internal-link target=_blank}. |
|||
|
|||
/// |
|||
|
|||
A ativação de um ambiente virtual adiciona seu caminho `.venv/bin` (no Linux e macOS) ou `.venv\Scripts` (no Windows) à variável de ambiente `PATH`. |
|||
|
|||
Digamos que antes de ativar o ambiente, a variável `PATH` estava assim: |
|||
|
|||
//// tab | Linux, macOS |
|||
|
|||
```plaintext |
|||
/usr/bin:/bin:/usr/sbin:/sbin |
|||
``` |
|||
|
|||
Isso significa que o sistema procuraria programas em: |
|||
|
|||
* `/usr/bin` |
|||
* `/bin` |
|||
* `/usr/sbin` |
|||
* `/sbin` |
|||
|
|||
//// |
|||
|
|||
//// tab | Windows |
|||
|
|||
```plaintext |
|||
C:\Windows\System32 |
|||
``` |
|||
|
|||
Isso significa que o sistema procuraria programas em: |
|||
|
|||
* `C:\Windows\System32` |
|||
|
|||
//// |
|||
|
|||
Após ativar o ambiente virtual, a variável `PATH` ficaria mais ou menos assim: |
|||
|
|||
//// tab | Linux, macOS |
|||
|
|||
```plaintext |
|||
/home/user/code/awesome-project/.venv/bin:/usr/bin:/bin:/usr/sbin:/sbin |
|||
``` |
|||
|
|||
Isso significa que o sistema agora começará a procurar primeiro por programas em: |
|||
|
|||
```plaintext |
|||
/home/user/code/awesome-project/.venv/bin |
|||
``` |
|||
|
|||
antes de procurar nos outros diretórios. |
|||
|
|||
Então, quando você digita `python` no terminal, o sistema encontrará o programa Python em |
|||
|
|||
```plaintext |
|||
/home/user/code/awesome-project/.venv/bin/python |
|||
``` |
|||
|
|||
e usa esse. |
|||
|
|||
//// |
|||
|
|||
//// tab | Windows |
|||
|
|||
```plaintext |
|||
C:\Users\user\code\awesome-project\.venv\Scripts;C:\Windows\System32 |
|||
``` |
|||
|
|||
Isso significa que o sistema agora começará a procurar primeiro por programas em: |
|||
|
|||
```plaintext |
|||
C:\Users\user\code\awesome-project\.venv\Scripts |
|||
``` |
|||
|
|||
antes de procurar nos outros diretórios. |
|||
|
|||
Então, quando você digita `python` no terminal, o sistema encontrará o programa Python em |
|||
|
|||
```plaintext |
|||
C:\Users\user\code\awesome-project\.venv\Scripts\python |
|||
``` |
|||
|
|||
e usa esse. |
|||
|
|||
//// |
|||
|
|||
Um detalhe importante é que ele colocará o caminho do ambiente virtual no **início** da variável `PATH`. O sistema o encontrará **antes** de encontrar qualquer outro Python disponível. Dessa forma, quando você executar `python`, ele usará o Python **do ambiente virtual** em vez de qualquer outro `python` (por exemplo, um `python` de um ambiente global). |
|||
|
|||
Ativar um ambiente virtual também muda algumas outras coisas, mas esta é uma das mais importantes. |
|||
|
|||
## Verificando um ambiente virtual |
|||
|
|||
Ao verificar se um ambiente virtual está ativo, por exemplo com: |
|||
|
|||
//// tab | Linux, macOS, Windows Bash |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ which python |
|||
|
|||
/home/user/code/awesome-project/.venv/bin/python |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
//// |
|||
|
|||
//// tab | Windows PowerShell |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ Get-Command python |
|||
|
|||
C:\Users\user\code\awesome-project\.venv\Scripts\python |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
//// |
|||
|
|||
Isso significa que o programa `python` que será usado é aquele **no ambiente virtual**. |
|||
|
|||
você usa `which` no Linux e macOS e `Get-Command` no Windows PowerShell. |
|||
|
|||
A maneira como esse comando funciona é que ele vai e verifica na variável de ambiente `PATH`, passando por **cada caminho em ordem**, procurando pelo programa chamado `python`. Uma vez que ele o encontre, ele **mostrará o caminho** para esse programa. |
|||
|
|||
A parte mais importante é que quando você chama ``python`, esse é exatamente o "`python`" que será executado. |
|||
|
|||
Assim, você pode confirmar se está no ambiente virtual correto. |
|||
|
|||
/// tip | "Dica" |
|||
|
|||
É fácil ativar um ambiente virtual, obter um Python e então **ir para outro projeto**. |
|||
|
|||
E o segundo projeto **não funcionaria** porque você está usando o **Python incorreto**, de um ambiente virtual para outro projeto. |
|||
|
|||
É útil poder verificar qual `python` está sendo usado. 🤓 |
|||
|
|||
/// |
|||
|
|||
## Por que desativar um ambiente virtual |
|||
|
|||
Por exemplo, você pode estar trabalhando em um projeto `philosophers-stone`, **ativar esse ambiente virtual**, instalar pacotes e trabalhar com esse ambiente. |
|||
|
|||
E então você quer trabalhar em **outro projeto** `prisoner-of-azkaban`. |
|||
|
|||
Você vai para aquele projeto: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ cd ~/code/prisoner-of-azkaban |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
Se você não desativar o ambiente virtual para `philosophers-stone`, quando você executar `python` no terminal, ele tentará usar o Python de `philosophers-stone`. |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ cd ~/code/prisoner-of-azkaban |
|||
|
|||
$ python main.py |
|||
|
|||
// Erro ao importar o Sirius, ele não está instalado 😱 |
|||
Traceback (most recent call last): |
|||
File "main.py", line 1, in <module> |
|||
import sirius |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
Mas se você desativar o ambiente virtual e ativar o novo para `prisoner-of-askaban`, quando você executar `python`, ele usará o Python do ambiente virtual em `prisoner-of-azkaban`. |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ cd ~/code/prisoner-of-azkaban |
|||
|
|||
// Você não precisa estar no diretório antigo para desativar, você pode fazer isso de onde estiver, mesmo depois de ir para o outro projeto 😎 |
|||
$ deactivate |
|||
|
|||
// Ative o ambiente virtual em prisoner-of-azkaban/.venv 🚀 |
|||
$ source .venv/bin/activate |
|||
|
|||
// Agora, quando você executar o python, ele encontrará o pacote sirius instalado neste ambiente virtual ✨ |
|||
$ python main.py |
|||
|
|||
Eu juro solenemente 🐺 |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
## Alternativas |
|||
|
|||
Este é um guia simples para você começar e lhe ensinar como tudo funciona **por baixo**. |
|||
|
|||
Existem muitas **alternativas** para gerenciar ambientes virtuais, dependências de pacotes (requisitos) e projetos. |
|||
|
|||
Quando estiver pronto e quiser usar uma ferramenta para **gerenciar todo o projeto**, dependências de pacotes, ambientes virtuais, etc., sugiro que você experimente o <a href="https://github.com/astral-sh/uv" class="external-link" target="_blank">uv</a>. |
|||
|
|||
`uv` pode fazer muitas coisas, ele pode: |
|||
|
|||
* **Instalar o Python** para você, incluindo versões diferentes |
|||
* Gerenciar o **ambiente virtual** para seus projetos |
|||
* Instalar **pacotes** |
|||
* Gerenciar **dependências e versões** de pacotes para seu projeto |
|||
* Certifique-se de ter um conjunto **exato** de pacotes e versões para instalar, incluindo suas dependências, para que você possa ter certeza de que pode executar seu projeto em produção exatamente da mesma forma que em seu computador durante o desenvolvimento, isso é chamado de **bloqueio** |
|||
* E muitas outras coisas |
|||
|
|||
## Conclusão |
|||
|
|||
Se você leu e entendeu tudo isso, agora **você sabe muito mais** sobre ambientes virtuais do que muitos desenvolvedores por aí. 🤓 |
|||
|
|||
Saber esses detalhes provavelmente será útil no futuro, quando você estiver depurando algo que parece complexo, mas você saberá **como tudo funciona**. 😎 |
@ -1,84 +1,28 @@ |
|||
# 项目生成 - 模板 |
|||
|
|||
项目生成器一般都会提供很多初始设置、安全措施、数据库,甚至还准备好了第一个 API 端点,能帮助您快速上手。 |
|||
|
|||
项目生成器的设置通常都很主观,您可以按需更新或修改,但对于您的项目来说,它是非常好的起点。 |
|||
|
|||
## 全栈 FastAPI + PostgreSQL |
|||
|
|||
GitHub:<a href="https://github.com/tiangolo/full-stack-fastapi-postgresql" class="external-link" target="_blank">https://github.com/tiangolo/full-stack-fastapi-postgresql</a> |
|||
|
|||
### 全栈 FastAPI + PostgreSQL - 功能 |
|||
|
|||
* 完整的 **Docker** 集成(基于 Docker) |
|||
* Docker Swarm 开发模式 |
|||
* **Docker Compose** 本地开发集成与优化 |
|||
* **生产可用**的 Python 网络服务器,使用 Uvicorn 或 Gunicorn |
|||
* Python <a href="https://github.com/fastapi/fastapi" class="external-link" target="_blank">**FastAPI**</a> 后端: |
|||
* * **速度快**:可与 **NodeJS** 和 **Go** 比肩的极高性能(归功于 Starlette 和 Pydantic) |
|||
* **直观**:强大的编辑器支持,处处皆可<abbr title="也叫自动完成、智能感知">自动补全</abbr>,减少调试时间 |
|||
* **简单**:易学、易用,阅读文档所需时间更短 |
|||
* **简短**:代码重复最小化,每次参数声明都可以实现多个功能 |
|||
* **健壮**: 生产级别的代码,还有自动交互文档 |
|||
* **基于标准**:完全兼容并基于 API 开放标准:<a href="https://github.com/OAI/OpenAPI-Specification" class="external-link" target="_blank">OpenAPI</a> 和 <a href="https://json-schema.org/" class="external-link" target="_blank">JSON Schema</a> |
|||
* <a href="https://fastapi.tiangolo.com/features/" class="external-link" target="_blank">**更多功能**</a>包括自动验证、序列化、交互文档、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:<a href="https://github.com/tiangolo/full-stack-fastapi-couchbase" class="external-link" target="_blank">https://github.com/tiangolo/full-stack-fastapi-couchbase</a> |
|||
|
|||
⚠️ **警告** ⚠️ |
|||
|
|||
如果您想从头开始创建新项目,建议使用以下备选方案。 |
|||
|
|||
例如,项目生成器<a href="https://github.com/tiangolo/full-stack-fastapi-postgresql" class="external-link" target="_blank">全栈 FastAPI + PostgreSQL </a>会更适用,这个项目的维护积极,用的人也多,还包括了所有新功能和改进内容。 |
|||
|
|||
当然,您也可以放心使用这个基于 Couchbase 的生成器,它也能正常使用。就算用它生成项目也没有任何问题(为了更好地满足需求,您可以自行更新这个项目)。 |
|||
|
|||
详见资源仓库中的文档。 |
|||
|
|||
## 全栈 FastAPI + MongoDB |
|||
|
|||
……敬请期待,得看我有没有时间做这个项目。😅 🎉 |
|||
|
|||
## FastAPI + spaCy 机器学习模型 |
|||
|
|||
GitHub:<a href="https://github.com/microsoft/cookiecutter-spacy-fastapi" class="external-link" target="_blank">https://github.com/microsoft/cookiecutter-spacy-fastapi</a> |
|||
|
|||
### FastAPI + spaCy 机器学习模型 - 功能 |
|||
|
|||
* 集成 **spaCy** NER 模型 |
|||
* 内置 **Azure 认知搜索**请求格式 |
|||
* **生产可用**的 Python 网络服务器,使用 Uvicorn 与 Gunicorn |
|||
* 内置 **Azure DevOps** Kubernetes (AKS) CI/CD 开发 |
|||
* **多语**支持,可在项目设置时选择 spaCy 内置的语言 |
|||
* 不仅局限于 spaCy,可**轻松扩展**至其它模型框架(Pytorch、TensorFlow) |
|||
# FastAPI全栈模板 |
|||
|
|||
模板通常带有特定的设置,而且被设计为灵活和可定制的。这允许您根据项目的需求修改和调整它们,使它们成为一个很好的起点。🏁 |
|||
|
|||
您可以使用此模板开始,因为它包含了许多已经为您完成的初始设置、安全性、数据库和一些API端点。 |
|||
|
|||
代码仓: <a href="https://github.com/fastapi/full-stack-fastapi-template" class="external-link" target="_blank">Full Stack FastAPI Template</a> |
|||
|
|||
## 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。 |
|||
|
@ -0,0 +1,14 @@ |
|||
from fastapi import FastAPI, Form |
|||
from pydantic import BaseModel |
|||
|
|||
app = FastAPI() |
|||
|
|||
|
|||
class FormData(BaseModel): |
|||
username: str |
|||
password: str |
|||
|
|||
|
|||
@app.post("/login/") |
|||
async def login(data: FormData = Form()): |
|||
return data |
@ -0,0 +1,15 @@ |
|||
from fastapi import FastAPI, Form |
|||
from pydantic import BaseModel |
|||
from typing_extensions import Annotated |
|||
|
|||
app = FastAPI() |
|||
|
|||
|
|||
class FormData(BaseModel): |
|||
username: str |
|||
password: str |
|||
|
|||
|
|||
@app.post("/login/") |
|||
async def login(data: Annotated[FormData, Form()]): |
|||
return data |
@ -0,0 +1,16 @@ |
|||
from typing import Annotated |
|||
|
|||
from fastapi import FastAPI, Form |
|||
from pydantic import BaseModel |
|||
|
|||
app = FastAPI() |
|||
|
|||
|
|||
class FormData(BaseModel): |
|||
username: str |
|||
password: str |
|||
|
|||
|
|||
@app.post("/login/") |
|||
async def login(data: Annotated[FormData, Form()]): |
|||
return data |
@ -0,0 +1,15 @@ |
|||
from fastapi import FastAPI, Form |
|||
from pydantic import BaseModel |
|||
|
|||
app = FastAPI() |
|||
|
|||
|
|||
class FormData(BaseModel): |
|||
username: str |
|||
password: str |
|||
model_config = {"extra": "forbid"} |
|||
|
|||
|
|||
@app.post("/login/") |
|||
async def login(data: FormData = Form()): |
|||
return data |
@ -0,0 +1,16 @@ |
|||
from fastapi import FastAPI, Form |
|||
from pydantic import BaseModel |
|||
from typing_extensions import Annotated |
|||
|
|||
app = FastAPI() |
|||
|
|||
|
|||
class FormData(BaseModel): |
|||
username: str |
|||
password: str |
|||
model_config = {"extra": "forbid"} |
|||
|
|||
|
|||
@app.post("/login/") |
|||
async def login(data: Annotated[FormData, Form()]): |
|||
return data |
@ -0,0 +1,17 @@ |
|||
from typing import Annotated |
|||
|
|||
from fastapi import FastAPI, Form |
|||
from pydantic import BaseModel |
|||
|
|||
app = FastAPI() |
|||
|
|||
|
|||
class FormData(BaseModel): |
|||
username: str |
|||
password: str |
|||
model_config = {"extra": "forbid"} |
|||
|
|||
|
|||
@app.post("/login/") |
|||
async def login(data: Annotated[FormData, Form()]): |
|||
return data |
@ -0,0 +1,17 @@ |
|||
from fastapi import FastAPI, Form |
|||
from pydantic import BaseModel |
|||
|
|||
app = FastAPI() |
|||
|
|||
|
|||
class FormData(BaseModel): |
|||
username: str |
|||
password: str |
|||
|
|||
class Config: |
|||
extra = "forbid" |
|||
|
|||
|
|||
@app.post("/login/") |
|||
async def login(data: FormData = Form()): |
|||
return data |
@ -0,0 +1,18 @@ |
|||
from fastapi import FastAPI, Form |
|||
from pydantic import BaseModel |
|||
from typing_extensions import Annotated |
|||
|
|||
app = FastAPI() |
|||
|
|||
|
|||
class FormData(BaseModel): |
|||
username: str |
|||
password: str |
|||
|
|||
class Config: |
|||
extra = "forbid" |
|||
|
|||
|
|||
@app.post("/login/") |
|||
async def login(data: Annotated[FormData, Form()]): |
|||
return data |
@ -0,0 +1,19 @@ |
|||
from typing import Annotated |
|||
|
|||
from fastapi import FastAPI, Form |
|||
from pydantic import BaseModel |
|||
|
|||
app = FastAPI() |
|||
|
|||
|
|||
class FormData(BaseModel): |
|||
username: str |
|||
password: str |
|||
|
|||
class Config: |
|||
extra = "forbid" |
|||
|
|||
|
|||
@app.post("/login/") |
|||
async def login(data: Annotated[FormData, Form()]): |
|||
return data |
@ -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="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") |
|||
|
|||
# --------------------- |
|||
context.close() |
|||
browser.close() |
|||
|
|||
|
|||
process = subprocess.Popen( |
|||
["fastapi", "run", "docs_src/request_form_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() |
@ -0,0 +1,133 @@ |
|||
from typing import List, Optional |
|||
|
|||
from dirty_equals import IsDict |
|||
from fastapi import FastAPI, Form |
|||
from fastapi.testclient import TestClient |
|||
from pydantic import BaseModel, Field |
|||
from typing_extensions import Annotated |
|||
|
|||
app = FastAPI() |
|||
|
|||
|
|||
class FormModel(BaseModel): |
|||
username: str |
|||
lastname: str |
|||
age: Optional[int] = None |
|||
tags: List[str] = ["foo", "bar"] |
|||
alias_with: str = Field(alias="with", default="nothing") |
|||
|
|||
|
|||
@app.post("/form/") |
|||
def post_form(user: Annotated[FormModel, Form()]): |
|||
return user |
|||
|
|||
|
|||
client = TestClient(app) |
|||
|
|||
|
|||
def test_send_all_data(): |
|||
response = client.post( |
|||
"/form/", |
|||
data={ |
|||
"username": "Rick", |
|||
"lastname": "Sanchez", |
|||
"age": "70", |
|||
"tags": ["plumbus", "citadel"], |
|||
"with": "something", |
|||
}, |
|||
) |
|||
assert response.status_code == 200, response.text |
|||
assert response.json() == { |
|||
"username": "Rick", |
|||
"lastname": "Sanchez", |
|||
"age": 70, |
|||
"tags": ["plumbus", "citadel"], |
|||
"with": "something", |
|||
} |
|||
|
|||
|
|||
def test_defaults(): |
|||
response = client.post("/form/", data={"username": "Rick", "lastname": "Sanchez"}) |
|||
assert response.status_code == 200, response.text |
|||
assert response.json() == { |
|||
"username": "Rick", |
|||
"lastname": "Sanchez", |
|||
"age": None, |
|||
"tags": ["foo", "bar"], |
|||
"with": "nothing", |
|||
} |
|||
|
|||
|
|||
def test_invalid_data(): |
|||
response = client.post( |
|||
"/form/", |
|||
data={ |
|||
"username": "Rick", |
|||
"lastname": "Sanchez", |
|||
"age": "seventy", |
|||
"tags": ["plumbus", "citadel"], |
|||
}, |
|||
) |
|||
assert response.status_code == 422, response.text |
|||
assert response.json() == IsDict( |
|||
{ |
|||
"detail": [ |
|||
{ |
|||
"type": "int_parsing", |
|||
"loc": ["body", "age"], |
|||
"msg": "Input should be a valid integer, unable to parse string as an integer", |
|||
"input": "seventy", |
|||
} |
|||
] |
|||
} |
|||
) | IsDict( |
|||
# TODO: remove when deprecating Pydantic v1 |
|||
{ |
|||
"detail": [ |
|||
{ |
|||
"loc": ["body", "age"], |
|||
"msg": "value is not a valid integer", |
|||
"type": "type_error.integer", |
|||
} |
|||
] |
|||
} |
|||
) |
|||
|
|||
|
|||
def test_no_data(): |
|||
response = client.post("/form/") |
|||
assert response.status_code == 422, response.text |
|||
assert response.json() == IsDict( |
|||
{ |
|||
"detail": [ |
|||
{ |
|||
"type": "missing", |
|||
"loc": ["body", "username"], |
|||
"msg": "Field required", |
|||
"input": {"tags": ["foo", "bar"], "with": "nothing"}, |
|||
}, |
|||
{ |
|||
"type": "missing", |
|||
"loc": ["body", "lastname"], |
|||
"msg": "Field required", |
|||
"input": {"tags": ["foo", "bar"], "with": "nothing"}, |
|||
}, |
|||
] |
|||
} |
|||
) | IsDict( |
|||
# TODO: remove when deprecating Pydantic v1 |
|||
{ |
|||
"detail": [ |
|||
{ |
|||
"loc": ["body", "username"], |
|||
"msg": "field required", |
|||
"type": "value_error.missing", |
|||
}, |
|||
{ |
|||
"loc": ["body", "lastname"], |
|||
"msg": "field required", |
|||
"type": "value_error.missing", |
|||
}, |
|||
] |
|||
} |
|||
) |
@ -0,0 +1,99 @@ |
|||
from fastapi import FastAPI, Form |
|||
from fastapi.testclient import TestClient |
|||
from typing_extensions import Annotated |
|||
|
|||
app = FastAPI() |
|||
|
|||
|
|||
@app.post("/form/") |
|||
def post_form(username: Annotated[str, Form()]): |
|||
return username |
|||
|
|||
|
|||
client = TestClient(app) |
|||
|
|||
|
|||
def test_single_form_field(): |
|||
response = client.post("/form/", data={"username": "Rick"}) |
|||
assert response.status_code == 200, response.text |
|||
assert response.json() == "Rick" |
|||
|
|||
|
|||
def test_openapi_schema(): |
|||
response = client.get("/openapi.json") |
|||
assert response.status_code == 200, response.text |
|||
assert response.json() == { |
|||
"openapi": "3.1.0", |
|||
"info": {"title": "FastAPI", "version": "0.1.0"}, |
|||
"paths": { |
|||
"/form/": { |
|||
"post": { |
|||
"summary": "Post Form", |
|||
"operationId": "post_form_form__post", |
|||
"requestBody": { |
|||
"content": { |
|||
"application/x-www-form-urlencoded": { |
|||
"schema": { |
|||
"$ref": "#/components/schemas/Body_post_form_form__post" |
|||
} |
|||
} |
|||
}, |
|||
"required": True, |
|||
}, |
|||
"responses": { |
|||
"200": { |
|||
"description": "Successful Response", |
|||
"content": {"application/json": {"schema": {}}}, |
|||
}, |
|||
"422": { |
|||
"description": "Validation Error", |
|||
"content": { |
|||
"application/json": { |
|||
"schema": { |
|||
"$ref": "#/components/schemas/HTTPValidationError" |
|||
} |
|||
} |
|||
}, |
|||
}, |
|||
}, |
|||
} |
|||
} |
|||
}, |
|||
"components": { |
|||
"schemas": { |
|||
"Body_post_form_form__post": { |
|||
"properties": {"username": {"type": "string", "title": "Username"}}, |
|||
"type": "object", |
|||
"required": ["username"], |
|||
"title": "Body_post_form_form__post", |
|||
}, |
|||
"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", |
|||
}, |
|||
} |
|||
}, |
|||
} |
@ -0,0 +1,232 @@ |
|||
import pytest |
|||
from dirty_equals import IsDict |
|||
from fastapi.testclient import TestClient |
|||
|
|||
|
|||
@pytest.fixture(name="client") |
|||
def get_client(): |
|||
from docs_src.request_form_models.tutorial001 import app |
|||
|
|||
client = TestClient(app) |
|||
return client |
|||
|
|||
|
|||
def test_post_body_form(client: TestClient): |
|||
response = client.post("/login/", data={"username": "Foo", "password": "secret"}) |
|||
assert response.status_code == 200 |
|||
assert response.json() == {"username": "Foo", "password": "secret"} |
|||
|
|||
|
|||
def test_post_body_form_no_password(client: TestClient): |
|||
response = client.post("/login/", data={"username": "Foo"}) |
|||
assert response.status_code == 422 |
|||
assert response.json() == IsDict( |
|||
{ |
|||
"detail": [ |
|||
{ |
|||
"type": "missing", |
|||
"loc": ["body", "password"], |
|||
"msg": "Field required", |
|||
"input": {"username": "Foo"}, |
|||
} |
|||
] |
|||
} |
|||
) | IsDict( |
|||
# TODO: remove when deprecating Pydantic v1 |
|||
{ |
|||
"detail": [ |
|||
{ |
|||
"loc": ["body", "password"], |
|||
"msg": "field required", |
|||
"type": "value_error.missing", |
|||
} |
|||
] |
|||
} |
|||
) |
|||
|
|||
|
|||
def test_post_body_form_no_username(client: TestClient): |
|||
response = client.post("/login/", data={"password": "secret"}) |
|||
assert response.status_code == 422 |
|||
assert response.json() == IsDict( |
|||
{ |
|||
"detail": [ |
|||
{ |
|||
"type": "missing", |
|||
"loc": ["body", "username"], |
|||
"msg": "Field required", |
|||
"input": {"password": "secret"}, |
|||
} |
|||
] |
|||
} |
|||
) | IsDict( |
|||
# TODO: remove when deprecating Pydantic v1 |
|||
{ |
|||
"detail": [ |
|||
{ |
|||
"loc": ["body", "username"], |
|||
"msg": "field required", |
|||
"type": "value_error.missing", |
|||
} |
|||
] |
|||
} |
|||
) |
|||
|
|||
|
|||
def test_post_body_form_no_data(client: TestClient): |
|||
response = client.post("/login/") |
|||
assert response.status_code == 422 |
|||
assert response.json() == IsDict( |
|||
{ |
|||
"detail": [ |
|||
{ |
|||
"type": "missing", |
|||
"loc": ["body", "username"], |
|||
"msg": "Field required", |
|||
"input": {}, |
|||
}, |
|||
{ |
|||
"type": "missing", |
|||
"loc": ["body", "password"], |
|||
"msg": "Field required", |
|||
"input": {}, |
|||
}, |
|||
] |
|||
} |
|||
) | IsDict( |
|||
# TODO: remove when deprecating Pydantic v1 |
|||
{ |
|||
"detail": [ |
|||
{ |
|||
"loc": ["body", "username"], |
|||
"msg": "field required", |
|||
"type": "value_error.missing", |
|||
}, |
|||
{ |
|||
"loc": ["body", "password"], |
|||
"msg": "field required", |
|||
"type": "value_error.missing", |
|||
}, |
|||
] |
|||
} |
|||
) |
|||
|
|||
|
|||
def test_post_body_json(client: TestClient): |
|||
response = client.post("/login/", json={"username": "Foo", "password": "secret"}) |
|||
assert response.status_code == 422, response.text |
|||
assert response.json() == IsDict( |
|||
{ |
|||
"detail": [ |
|||
{ |
|||
"type": "missing", |
|||
"loc": ["body", "username"], |
|||
"msg": "Field required", |
|||
"input": {}, |
|||
}, |
|||
{ |
|||
"type": "missing", |
|||
"loc": ["body", "password"], |
|||
"msg": "Field required", |
|||
"input": {}, |
|||
}, |
|||
] |
|||
} |
|||
) | IsDict( |
|||
# TODO: remove when deprecating Pydantic v1 |
|||
{ |
|||
"detail": [ |
|||
{ |
|||
"loc": ["body", "username"], |
|||
"msg": "field required", |
|||
"type": "value_error.missing", |
|||
}, |
|||
{ |
|||
"loc": ["body", "password"], |
|||
"msg": "field required", |
|||
"type": "value_error.missing", |
|||
}, |
|||
] |
|||
} |
|||
) |
|||
|
|||
|
|||
def test_openapi_schema(client: TestClient): |
|||
response = client.get("/openapi.json") |
|||
assert response.status_code == 200, response.text |
|||
assert response.json() == { |
|||
"openapi": "3.1.0", |
|||
"info": {"title": "FastAPI", "version": "0.1.0"}, |
|||
"paths": { |
|||
"/login/": { |
|||
"post": { |
|||
"responses": { |
|||
"200": { |
|||
"description": "Successful Response", |
|||
"content": {"application/json": {"schema": {}}}, |
|||
}, |
|||
"422": { |
|||
"description": "Validation Error", |
|||
"content": { |
|||
"application/json": { |
|||
"schema": { |
|||
"$ref": "#/components/schemas/HTTPValidationError" |
|||
} |
|||
} |
|||
}, |
|||
}, |
|||
}, |
|||
"summary": "Login", |
|||
"operationId": "login_login__post", |
|||
"requestBody": { |
|||
"content": { |
|||
"application/x-www-form-urlencoded": { |
|||
"schema": {"$ref": "#/components/schemas/FormData"} |
|||
} |
|||
}, |
|||
"required": True, |
|||
}, |
|||
} |
|||
} |
|||
}, |
|||
"components": { |
|||
"schemas": { |
|||
"FormData": { |
|||
"properties": { |
|||
"username": {"type": "string", "title": "Username"}, |
|||
"password": {"type": "string", "title": "Password"}, |
|||
}, |
|||
"type": "object", |
|||
"required": ["username", "password"], |
|||
"title": "FormData", |
|||
}, |
|||
"ValidationError": { |
|||
"title": "ValidationError", |
|||
"required": ["loc", "msg", "type"], |
|||
"type": "object", |
|||
"properties": { |
|||
"loc": { |
|||
"title": "Location", |
|||
"type": "array", |
|||
"items": { |
|||
"anyOf": [{"type": "string"}, {"type": "integer"}] |
|||
}, |
|||
}, |
|||
"msg": {"title": "Message", "type": "string"}, |
|||
"type": {"title": "Error Type", "type": "string"}, |
|||
}, |
|||
}, |
|||
"HTTPValidationError": { |
|||
"title": "HTTPValidationError", |
|||
"type": "object", |
|||
"properties": { |
|||
"detail": { |
|||
"title": "Detail", |
|||
"type": "array", |
|||
"items": {"$ref": "#/components/schemas/ValidationError"}, |
|||
} |
|||
}, |
|||
}, |
|||
} |
|||
}, |
|||
} |
@ -0,0 +1,232 @@ |
|||
import pytest |
|||
from dirty_equals import IsDict |
|||
from fastapi.testclient import TestClient |
|||
|
|||
|
|||
@pytest.fixture(name="client") |
|||
def get_client(): |
|||
from docs_src.request_form_models.tutorial001_an import app |
|||
|
|||
client = TestClient(app) |
|||
return client |
|||
|
|||
|
|||
def test_post_body_form(client: TestClient): |
|||
response = client.post("/login/", data={"username": "Foo", "password": "secret"}) |
|||
assert response.status_code == 200 |
|||
assert response.json() == {"username": "Foo", "password": "secret"} |
|||
|
|||
|
|||
def test_post_body_form_no_password(client: TestClient): |
|||
response = client.post("/login/", data={"username": "Foo"}) |
|||
assert response.status_code == 422 |
|||
assert response.json() == IsDict( |
|||
{ |
|||
"detail": [ |
|||
{ |
|||
"type": "missing", |
|||
"loc": ["body", "password"], |
|||
"msg": "Field required", |
|||
"input": {"username": "Foo"}, |
|||
} |
|||
] |
|||
} |
|||
) | IsDict( |
|||
# TODO: remove when deprecating Pydantic v1 |
|||
{ |
|||
"detail": [ |
|||
{ |
|||
"loc": ["body", "password"], |
|||
"msg": "field required", |
|||
"type": "value_error.missing", |
|||
} |
|||
] |
|||
} |
|||
) |
|||
|
|||
|
|||
def test_post_body_form_no_username(client: TestClient): |
|||
response = client.post("/login/", data={"password": "secret"}) |
|||
assert response.status_code == 422 |
|||
assert response.json() == IsDict( |
|||
{ |
|||
"detail": [ |
|||
{ |
|||
"type": "missing", |
|||
"loc": ["body", "username"], |
|||
"msg": "Field required", |
|||
"input": {"password": "secret"}, |
|||
} |
|||
] |
|||
} |
|||
) | IsDict( |
|||
# TODO: remove when deprecating Pydantic v1 |
|||
{ |
|||
"detail": [ |
|||
{ |
|||
"loc": ["body", "username"], |
|||
"msg": "field required", |
|||
"type": "value_error.missing", |
|||
} |
|||
] |
|||
} |
|||
) |
|||
|
|||
|
|||
def test_post_body_form_no_data(client: TestClient): |
|||
response = client.post("/login/") |
|||
assert response.status_code == 422 |
|||
assert response.json() == IsDict( |
|||
{ |
|||
"detail": [ |
|||
{ |
|||
"type": "missing", |
|||
"loc": ["body", "username"], |
|||
"msg": "Field required", |
|||
"input": {}, |
|||
}, |
|||
{ |
|||
"type": "missing", |
|||
"loc": ["body", "password"], |
|||
"msg": "Field required", |
|||
"input": {}, |
|||
}, |
|||
] |
|||
} |
|||
) | IsDict( |
|||
# TODO: remove when deprecating Pydantic v1 |
|||
{ |
|||
"detail": [ |
|||
{ |
|||
"loc": ["body", "username"], |
|||
"msg": "field required", |
|||
"type": "value_error.missing", |
|||
}, |
|||
{ |
|||
"loc": ["body", "password"], |
|||
"msg": "field required", |
|||
"type": "value_error.missing", |
|||
}, |
|||
] |
|||
} |
|||
) |
|||
|
|||
|
|||
def test_post_body_json(client: TestClient): |
|||
response = client.post("/login/", json={"username": "Foo", "password": "secret"}) |
|||
assert response.status_code == 422, response.text |
|||
assert response.json() == IsDict( |
|||
{ |
|||
"detail": [ |
|||
{ |
|||
"type": "missing", |
|||
"loc": ["body", "username"], |
|||
"msg": "Field required", |
|||
"input": {}, |
|||
}, |
|||
{ |
|||
"type": "missing", |
|||
"loc": ["body", "password"], |
|||
"msg": "Field required", |
|||
"input": {}, |
|||
}, |
|||
] |
|||
} |
|||
) | IsDict( |
|||
# TODO: remove when deprecating Pydantic v1 |
|||
{ |
|||
"detail": [ |
|||
{ |
|||
"loc": ["body", "username"], |
|||
"msg": "field required", |
|||
"type": "value_error.missing", |
|||
}, |
|||
{ |
|||
"loc": ["body", "password"], |
|||
"msg": "field required", |
|||
"type": "value_error.missing", |
|||
}, |
|||
] |
|||
} |
|||
) |
|||
|
|||
|
|||
def test_openapi_schema(client: TestClient): |
|||
response = client.get("/openapi.json") |
|||
assert response.status_code == 200, response.text |
|||
assert response.json() == { |
|||
"openapi": "3.1.0", |
|||
"info": {"title": "FastAPI", "version": "0.1.0"}, |
|||
"paths": { |
|||
"/login/": { |
|||
"post": { |
|||
"responses": { |
|||
"200": { |
|||
"description": "Successful Response", |
|||
"content": {"application/json": {"schema": {}}}, |
|||
}, |
|||
"422": { |
|||
"description": "Validation Error", |
|||
"content": { |
|||
"application/json": { |
|||
"schema": { |
|||
"$ref": "#/components/schemas/HTTPValidationError" |
|||
} |
|||
} |
|||
}, |
|||
}, |
|||
}, |
|||
"summary": "Login", |
|||
"operationId": "login_login__post", |
|||
"requestBody": { |
|||
"content": { |
|||
"application/x-www-form-urlencoded": { |
|||
"schema": {"$ref": "#/components/schemas/FormData"} |
|||
} |
|||
}, |
|||
"required": True, |
|||
}, |
|||
} |
|||
} |
|||
}, |
|||
"components": { |
|||
"schemas": { |
|||
"FormData": { |
|||
"properties": { |
|||
"username": {"type": "string", "title": "Username"}, |
|||
"password": {"type": "string", "title": "Password"}, |
|||
}, |
|||
"type": "object", |
|||
"required": ["username", "password"], |
|||
"title": "FormData", |
|||
}, |
|||
"ValidationError": { |
|||
"title": "ValidationError", |
|||
"required": ["loc", "msg", "type"], |
|||
"type": "object", |
|||
"properties": { |
|||
"loc": { |
|||
"title": "Location", |
|||
"type": "array", |
|||
"items": { |
|||
"anyOf": [{"type": "string"}, {"type": "integer"}] |
|||
}, |
|||
}, |
|||
"msg": {"title": "Message", "type": "string"}, |
|||
"type": {"title": "Error Type", "type": "string"}, |
|||
}, |
|||
}, |
|||
"HTTPValidationError": { |
|||
"title": "HTTPValidationError", |
|||
"type": "object", |
|||
"properties": { |
|||
"detail": { |
|||
"title": "Detail", |
|||
"type": "array", |
|||
"items": {"$ref": "#/components/schemas/ValidationError"}, |
|||
} |
|||
}, |
|||
}, |
|||
} |
|||
}, |
|||
} |
@ -0,0 +1,240 @@ |
|||
import pytest |
|||
from dirty_equals import IsDict |
|||
from fastapi.testclient import TestClient |
|||
|
|||
from tests.utils import needs_py39 |
|||
|
|||
|
|||
@pytest.fixture(name="client") |
|||
def get_client(): |
|||
from docs_src.request_form_models.tutorial001_an_py39 import app |
|||
|
|||
client = TestClient(app) |
|||
return client |
|||
|
|||
|
|||
@needs_py39 |
|||
def test_post_body_form(client: TestClient): |
|||
response = client.post("/login/", data={"username": "Foo", "password": "secret"}) |
|||
assert response.status_code == 200 |
|||
assert response.json() == {"username": "Foo", "password": "secret"} |
|||
|
|||
|
|||
@needs_py39 |
|||
def test_post_body_form_no_password(client: TestClient): |
|||
response = client.post("/login/", data={"username": "Foo"}) |
|||
assert response.status_code == 422 |
|||
assert response.json() == IsDict( |
|||
{ |
|||
"detail": [ |
|||
{ |
|||
"type": "missing", |
|||
"loc": ["body", "password"], |
|||
"msg": "Field required", |
|||
"input": {"username": "Foo"}, |
|||
} |
|||
] |
|||
} |
|||
) | IsDict( |
|||
# TODO: remove when deprecating Pydantic v1 |
|||
{ |
|||
"detail": [ |
|||
{ |
|||
"loc": ["body", "password"], |
|||
"msg": "field required", |
|||
"type": "value_error.missing", |
|||
} |
|||
] |
|||
} |
|||
) |
|||
|
|||
|
|||
@needs_py39 |
|||
def test_post_body_form_no_username(client: TestClient): |
|||
response = client.post("/login/", data={"password": "secret"}) |
|||
assert response.status_code == 422 |
|||
assert response.json() == IsDict( |
|||
{ |
|||
"detail": [ |
|||
{ |
|||
"type": "missing", |
|||
"loc": ["body", "username"], |
|||
"msg": "Field required", |
|||
"input": {"password": "secret"}, |
|||
} |
|||
] |
|||
} |
|||
) | IsDict( |
|||
# TODO: remove when deprecating Pydantic v1 |
|||
{ |
|||
"detail": [ |
|||
{ |
|||
"loc": ["body", "username"], |
|||
"msg": "field required", |
|||
"type": "value_error.missing", |
|||
} |
|||
] |
|||
} |
|||
) |
|||
|
|||
|
|||
@needs_py39 |
|||
def test_post_body_form_no_data(client: TestClient): |
|||
response = client.post("/login/") |
|||
assert response.status_code == 422 |
|||
assert response.json() == IsDict( |
|||
{ |
|||
"detail": [ |
|||
{ |
|||
"type": "missing", |
|||
"loc": ["body", "username"], |
|||
"msg": "Field required", |
|||
"input": {}, |
|||
}, |
|||
{ |
|||
"type": "missing", |
|||
"loc": ["body", "password"], |
|||
"msg": "Field required", |
|||
"input": {}, |
|||
}, |
|||
] |
|||
} |
|||
) | IsDict( |
|||
# TODO: remove when deprecating Pydantic v1 |
|||
{ |
|||
"detail": [ |
|||
{ |
|||
"loc": ["body", "username"], |
|||
"msg": "field required", |
|||
"type": "value_error.missing", |
|||
}, |
|||
{ |
|||
"loc": ["body", "password"], |
|||
"msg": "field required", |
|||
"type": "value_error.missing", |
|||
}, |
|||
] |
|||
} |
|||
) |
|||
|
|||
|
|||
@needs_py39 |
|||
def test_post_body_json(client: TestClient): |
|||
response = client.post("/login/", json={"username": "Foo", "password": "secret"}) |
|||
assert response.status_code == 422, response.text |
|||
assert response.json() == IsDict( |
|||
{ |
|||
"detail": [ |
|||
{ |
|||
"type": "missing", |
|||
"loc": ["body", "username"], |
|||
"msg": "Field required", |
|||
"input": {}, |
|||
}, |
|||
{ |
|||
"type": "missing", |
|||
"loc": ["body", "password"], |
|||
"msg": "Field required", |
|||
"input": {}, |
|||
}, |
|||
] |
|||
} |
|||
) | IsDict( |
|||
# TODO: remove when deprecating Pydantic v1 |
|||
{ |
|||
"detail": [ |
|||
{ |
|||
"loc": ["body", "username"], |
|||
"msg": "field required", |
|||
"type": "value_error.missing", |
|||
}, |
|||
{ |
|||
"loc": ["body", "password"], |
|||
"msg": "field required", |
|||
"type": "value_error.missing", |
|||
}, |
|||
] |
|||
} |
|||
) |
|||
|
|||
|
|||
@needs_py39 |
|||
def test_openapi_schema(client: TestClient): |
|||
response = client.get("/openapi.json") |
|||
assert response.status_code == 200, response.text |
|||
assert response.json() == { |
|||
"openapi": "3.1.0", |
|||
"info": {"title": "FastAPI", "version": "0.1.0"}, |
|||
"paths": { |
|||
"/login/": { |
|||
"post": { |
|||
"responses": { |
|||
"200": { |
|||
"description": "Successful Response", |
|||
"content": {"application/json": {"schema": {}}}, |
|||
}, |
|||
"422": { |
|||
"description": "Validation Error", |
|||
"content": { |
|||
"application/json": { |
|||
"schema": { |
|||
"$ref": "#/components/schemas/HTTPValidationError" |
|||
} |
|||
} |
|||
}, |
|||
}, |
|||
}, |
|||
"summary": "Login", |
|||
"operationId": "login_login__post", |
|||
"requestBody": { |
|||
"content": { |
|||
"application/x-www-form-urlencoded": { |
|||
"schema": {"$ref": "#/components/schemas/FormData"} |
|||
} |
|||
}, |
|||
"required": True, |
|||
}, |
|||
} |
|||
} |
|||
}, |
|||
"components": { |
|||
"schemas": { |
|||
"FormData": { |
|||
"properties": { |
|||
"username": {"type": "string", "title": "Username"}, |
|||
"password": {"type": "string", "title": "Password"}, |
|||
}, |
|||
"type": "object", |
|||
"required": ["username", "password"], |
|||
"title": "FormData", |
|||
}, |
|||
"ValidationError": { |
|||
"title": "ValidationError", |
|||
"required": ["loc", "msg", "type"], |
|||
"type": "object", |
|||
"properties": { |
|||
"loc": { |
|||
"title": "Location", |
|||
"type": "array", |
|||
"items": { |
|||
"anyOf": [{"type": "string"}, {"type": "integer"}] |
|||
}, |
|||
}, |
|||
"msg": {"title": "Message", "type": "string"}, |
|||
"type": {"title": "Error Type", "type": "string"}, |
|||
}, |
|||
}, |
|||
"HTTPValidationError": { |
|||
"title": "HTTPValidationError", |
|||
"type": "object", |
|||
"properties": { |
|||
"detail": { |
|||
"title": "Detail", |
|||
"type": "array", |
|||
"items": {"$ref": "#/components/schemas/ValidationError"}, |
|||
} |
|||
}, |
|||
}, |
|||
} |
|||
}, |
|||
} |
@ -0,0 +1,196 @@ |
|||
import pytest |
|||
from fastapi.testclient import TestClient |
|||
|
|||
from tests.utils import needs_pydanticv2 |
|||
|
|||
|
|||
@pytest.fixture(name="client") |
|||
def get_client(): |
|||
from docs_src.request_form_models.tutorial002 import app |
|||
|
|||
client = TestClient(app) |
|||
return client |
|||
|
|||
|
|||
@needs_pydanticv2 |
|||
def test_post_body_form(client: TestClient): |
|||
response = client.post("/login/", data={"username": "Foo", "password": "secret"}) |
|||
assert response.status_code == 200 |
|||
assert response.json() == {"username": "Foo", "password": "secret"} |
|||
|
|||
|
|||
@needs_pydanticv2 |
|||
def test_post_body_extra_form(client: TestClient): |
|||
response = client.post( |
|||
"/login/", data={"username": "Foo", "password": "secret", "extra": "extra"} |
|||
) |
|||
assert response.status_code == 422 |
|||
assert response.json() == { |
|||
"detail": [ |
|||
{ |
|||
"type": "extra_forbidden", |
|||
"loc": ["body", "extra"], |
|||
"msg": "Extra inputs are not permitted", |
|||
"input": "extra", |
|||
} |
|||
] |
|||
} |
|||
|
|||
|
|||
@needs_pydanticv2 |
|||
def test_post_body_form_no_password(client: TestClient): |
|||
response = client.post("/login/", data={"username": "Foo"}) |
|||
assert response.status_code == 422 |
|||
assert response.json() == { |
|||
"detail": [ |
|||
{ |
|||
"type": "missing", |
|||
"loc": ["body", "password"], |
|||
"msg": "Field required", |
|||
"input": {"username": "Foo"}, |
|||
} |
|||
] |
|||
} |
|||
|
|||
|
|||
@needs_pydanticv2 |
|||
def test_post_body_form_no_username(client: TestClient): |
|||
response = client.post("/login/", data={"password": "secret"}) |
|||
assert response.status_code == 422 |
|||
assert response.json() == { |
|||
"detail": [ |
|||
{ |
|||
"type": "missing", |
|||
"loc": ["body", "username"], |
|||
"msg": "Field required", |
|||
"input": {"password": "secret"}, |
|||
} |
|||
] |
|||
} |
|||
|
|||
|
|||
@needs_pydanticv2 |
|||
def test_post_body_form_no_data(client: TestClient): |
|||
response = client.post("/login/") |
|||
assert response.status_code == 422 |
|||
assert response.json() == { |
|||
"detail": [ |
|||
{ |
|||
"type": "missing", |
|||
"loc": ["body", "username"], |
|||
"msg": "Field required", |
|||
"input": {}, |
|||
}, |
|||
{ |
|||
"type": "missing", |
|||
"loc": ["body", "password"], |
|||
"msg": "Field required", |
|||
"input": {}, |
|||
}, |
|||
] |
|||
} |
|||
|
|||
|
|||
@needs_pydanticv2 |
|||
def test_post_body_json(client: TestClient): |
|||
response = client.post("/login/", json={"username": "Foo", "password": "secret"}) |
|||
assert response.status_code == 422, response.text |
|||
assert response.json() == { |
|||
"detail": [ |
|||
{ |
|||
"type": "missing", |
|||
"loc": ["body", "username"], |
|||
"msg": "Field required", |
|||
"input": {}, |
|||
}, |
|||
{ |
|||
"type": "missing", |
|||
"loc": ["body", "password"], |
|||
"msg": "Field required", |
|||
"input": {}, |
|||
}, |
|||
] |
|||
} |
|||
|
|||
|
|||
@needs_pydanticv2 |
|||
def test_openapi_schema(client: TestClient): |
|||
response = client.get("/openapi.json") |
|||
assert response.status_code == 200, response.text |
|||
assert response.json() == { |
|||
"openapi": "3.1.0", |
|||
"info": {"title": "FastAPI", "version": "0.1.0"}, |
|||
"paths": { |
|||
"/login/": { |
|||
"post": { |
|||
"responses": { |
|||
"200": { |
|||
"description": "Successful Response", |
|||
"content": {"application/json": {"schema": {}}}, |
|||
}, |
|||
"422": { |
|||
"description": "Validation Error", |
|||
"content": { |
|||
"application/json": { |
|||
"schema": { |
|||
"$ref": "#/components/schemas/HTTPValidationError" |
|||
} |
|||
} |
|||
}, |
|||
}, |
|||
}, |
|||
"summary": "Login", |
|||
"operationId": "login_login__post", |
|||
"requestBody": { |
|||
"content": { |
|||
"application/x-www-form-urlencoded": { |
|||
"schema": {"$ref": "#/components/schemas/FormData"} |
|||
} |
|||
}, |
|||
"required": True, |
|||
}, |
|||
} |
|||
} |
|||
}, |
|||
"components": { |
|||
"schemas": { |
|||
"FormData": { |
|||
"properties": { |
|||
"username": {"type": "string", "title": "Username"}, |
|||
"password": {"type": "string", "title": "Password"}, |
|||
}, |
|||
"additionalProperties": False, |
|||
"type": "object", |
|||
"required": ["username", "password"], |
|||
"title": "FormData", |
|||
}, |
|||
"ValidationError": { |
|||
"title": "ValidationError", |
|||
"required": ["loc", "msg", "type"], |
|||
"type": "object", |
|||
"properties": { |
|||
"loc": { |
|||
"title": "Location", |
|||
"type": "array", |
|||
"items": { |
|||
"anyOf": [{"type": "string"}, {"type": "integer"}] |
|||
}, |
|||
}, |
|||
"msg": {"title": "Message", "type": "string"}, |
|||
"type": {"title": "Error Type", "type": "string"}, |
|||
}, |
|||
}, |
|||
"HTTPValidationError": { |
|||
"title": "HTTPValidationError", |
|||
"type": "object", |
|||
"properties": { |
|||
"detail": { |
|||
"title": "Detail", |
|||
"type": "array", |
|||
"items": {"$ref": "#/components/schemas/ValidationError"}, |
|||
} |
|||
}, |
|||
}, |
|||
} |
|||
}, |
|||
} |
@ -0,0 +1,196 @@ |
|||
import pytest |
|||
from fastapi.testclient import TestClient |
|||
|
|||
from tests.utils import needs_pydanticv2 |
|||
|
|||
|
|||
@pytest.fixture(name="client") |
|||
def get_client(): |
|||
from docs_src.request_form_models.tutorial002_an import app |
|||
|
|||
client = TestClient(app) |
|||
return client |
|||
|
|||
|
|||
@needs_pydanticv2 |
|||
def test_post_body_form(client: TestClient): |
|||
response = client.post("/login/", data={"username": "Foo", "password": "secret"}) |
|||
assert response.status_code == 200 |
|||
assert response.json() == {"username": "Foo", "password": "secret"} |
|||
|
|||
|
|||
@needs_pydanticv2 |
|||
def test_post_body_extra_form(client: TestClient): |
|||
response = client.post( |
|||
"/login/", data={"username": "Foo", "password": "secret", "extra": "extra"} |
|||
) |
|||
assert response.status_code == 422 |
|||
assert response.json() == { |
|||
"detail": [ |
|||
{ |
|||
"type": "extra_forbidden", |
|||
"loc": ["body", "extra"], |
|||
"msg": "Extra inputs are not permitted", |
|||
"input": "extra", |
|||
} |
|||
] |
|||
} |
|||
|
|||
|
|||
@needs_pydanticv2 |
|||
def test_post_body_form_no_password(client: TestClient): |
|||
response = client.post("/login/", data={"username": "Foo"}) |
|||
assert response.status_code == 422 |
|||
assert response.json() == { |
|||
"detail": [ |
|||
{ |
|||
"type": "missing", |
|||
"loc": ["body", "password"], |
|||
"msg": "Field required", |
|||
"input": {"username": "Foo"}, |
|||
} |
|||
] |
|||
} |
|||
|
|||
|
|||
@needs_pydanticv2 |
|||
def test_post_body_form_no_username(client: TestClient): |
|||
response = client.post("/login/", data={"password": "secret"}) |
|||
assert response.status_code == 422 |
|||
assert response.json() == { |
|||
"detail": [ |
|||
{ |
|||
"type": "missing", |
|||
"loc": ["body", "username"], |
|||
"msg": "Field required", |
|||
"input": {"password": "secret"}, |
|||
} |
|||
] |
|||
} |
|||
|
|||
|
|||
@needs_pydanticv2 |
|||
def test_post_body_form_no_data(client: TestClient): |
|||
response = client.post("/login/") |
|||
assert response.status_code == 422 |
|||
assert response.json() == { |
|||
"detail": [ |
|||
{ |
|||
"type": "missing", |
|||
"loc": ["body", "username"], |
|||
"msg": "Field required", |
|||
"input": {}, |
|||
}, |
|||
{ |
|||
"type": "missing", |
|||
"loc": ["body", "password"], |
|||
"msg": "Field required", |
|||
"input": {}, |
|||
}, |
|||
] |
|||
} |
|||
|
|||
|
|||
@needs_pydanticv2 |
|||
def test_post_body_json(client: TestClient): |
|||
response = client.post("/login/", json={"username": "Foo", "password": "secret"}) |
|||
assert response.status_code == 422, response.text |
|||
assert response.json() == { |
|||
"detail": [ |
|||
{ |
|||
"type": "missing", |
|||
"loc": ["body", "username"], |
|||
"msg": "Field required", |
|||
"input": {}, |
|||
}, |
|||
{ |
|||
"type": "missing", |
|||
"loc": ["body", "password"], |
|||
"msg": "Field required", |
|||
"input": {}, |
|||
}, |
|||
] |
|||
} |
|||
|
|||
|
|||
@needs_pydanticv2 |
|||
def test_openapi_schema(client: TestClient): |
|||
response = client.get("/openapi.json") |
|||
assert response.status_code == 200, response.text |
|||
assert response.json() == { |
|||
"openapi": "3.1.0", |
|||
"info": {"title": "FastAPI", "version": "0.1.0"}, |
|||
"paths": { |
|||
"/login/": { |
|||
"post": { |
|||
"responses": { |
|||
"200": { |
|||
"description": "Successful Response", |
|||
"content": {"application/json": {"schema": {}}}, |
|||
}, |
|||
"422": { |
|||
"description": "Validation Error", |
|||
"content": { |
|||
"application/json": { |
|||
"schema": { |
|||
"$ref": "#/components/schemas/HTTPValidationError" |
|||
} |
|||
} |
|||
}, |
|||
}, |
|||
}, |
|||
"summary": "Login", |
|||
"operationId": "login_login__post", |
|||
"requestBody": { |
|||
"content": { |
|||
"application/x-www-form-urlencoded": { |
|||
"schema": {"$ref": "#/components/schemas/FormData"} |
|||
} |
|||
}, |
|||
"required": True, |
|||
}, |
|||
} |
|||
} |
|||
}, |
|||
"components": { |
|||
"schemas": { |
|||
"FormData": { |
|||
"properties": { |
|||
"username": {"type": "string", "title": "Username"}, |
|||
"password": {"type": "string", "title": "Password"}, |
|||
}, |
|||
"additionalProperties": False, |
|||
"type": "object", |
|||
"required": ["username", "password"], |
|||
"title": "FormData", |
|||
}, |
|||
"ValidationError": { |
|||
"title": "ValidationError", |
|||
"required": ["loc", "msg", "type"], |
|||
"type": "object", |
|||
"properties": { |
|||
"loc": { |
|||
"title": "Location", |
|||
"type": "array", |
|||
"items": { |
|||
"anyOf": [{"type": "string"}, {"type": "integer"}] |
|||
}, |
|||
}, |
|||
"msg": {"title": "Message", "type": "string"}, |
|||
"type": {"title": "Error Type", "type": "string"}, |
|||
}, |
|||
}, |
|||
"HTTPValidationError": { |
|||
"title": "HTTPValidationError", |
|||
"type": "object", |
|||
"properties": { |
|||
"detail": { |
|||
"title": "Detail", |
|||
"type": "array", |
|||
"items": {"$ref": "#/components/schemas/ValidationError"}, |
|||
} |
|||
}, |
|||
}, |
|||
} |
|||
}, |
|||
} |
@ -0,0 +1,203 @@ |
|||
import pytest |
|||
from fastapi.testclient import TestClient |
|||
|
|||
from tests.utils import needs_py39, needs_pydanticv2 |
|||
|
|||
|
|||
@pytest.fixture(name="client") |
|||
def get_client(): |
|||
from docs_src.request_form_models.tutorial002_an_py39 import app |
|||
|
|||
client = TestClient(app) |
|||
return client |
|||
|
|||
|
|||
@needs_pydanticv2 |
|||
@needs_py39 |
|||
def test_post_body_form(client: TestClient): |
|||
response = client.post("/login/", data={"username": "Foo", "password": "secret"}) |
|||
assert response.status_code == 200 |
|||
assert response.json() == {"username": "Foo", "password": "secret"} |
|||
|
|||
|
|||
@needs_pydanticv2 |
|||
@needs_py39 |
|||
def test_post_body_extra_form(client: TestClient): |
|||
response = client.post( |
|||
"/login/", data={"username": "Foo", "password": "secret", "extra": "extra"} |
|||
) |
|||
assert response.status_code == 422 |
|||
assert response.json() == { |
|||
"detail": [ |
|||
{ |
|||
"type": "extra_forbidden", |
|||
"loc": ["body", "extra"], |
|||
"msg": "Extra inputs are not permitted", |
|||
"input": "extra", |
|||
} |
|||
] |
|||
} |
|||
|
|||
|
|||
@needs_pydanticv2 |
|||
@needs_py39 |
|||
def test_post_body_form_no_password(client: TestClient): |
|||
response = client.post("/login/", data={"username": "Foo"}) |
|||
assert response.status_code == 422 |
|||
assert response.json() == { |
|||
"detail": [ |
|||
{ |
|||
"type": "missing", |
|||
"loc": ["body", "password"], |
|||
"msg": "Field required", |
|||
"input": {"username": "Foo"}, |
|||
} |
|||
] |
|||
} |
|||
|
|||
|
|||
@needs_pydanticv2 |
|||
@needs_py39 |
|||
def test_post_body_form_no_username(client: TestClient): |
|||
response = client.post("/login/", data={"password": "secret"}) |
|||
assert response.status_code == 422 |
|||
assert response.json() == { |
|||
"detail": [ |
|||
{ |
|||
"type": "missing", |
|||
"loc": ["body", "username"], |
|||
"msg": "Field required", |
|||
"input": {"password": "secret"}, |
|||
} |
|||
] |
|||
} |
|||
|
|||
|
|||
@needs_pydanticv2 |
|||
@needs_py39 |
|||
def test_post_body_form_no_data(client: TestClient): |
|||
response = client.post("/login/") |
|||
assert response.status_code == 422 |
|||
assert response.json() == { |
|||
"detail": [ |
|||
{ |
|||
"type": "missing", |
|||
"loc": ["body", "username"], |
|||
"msg": "Field required", |
|||
"input": {}, |
|||
}, |
|||
{ |
|||
"type": "missing", |
|||
"loc": ["body", "password"], |
|||
"msg": "Field required", |
|||
"input": {}, |
|||
}, |
|||
] |
|||
} |
|||
|
|||
|
|||
@needs_pydanticv2 |
|||
@needs_py39 |
|||
def test_post_body_json(client: TestClient): |
|||
response = client.post("/login/", json={"username": "Foo", "password": "secret"}) |
|||
assert response.status_code == 422, response.text |
|||
assert response.json() == { |
|||
"detail": [ |
|||
{ |
|||
"type": "missing", |
|||
"loc": ["body", "username"], |
|||
"msg": "Field required", |
|||
"input": {}, |
|||
}, |
|||
{ |
|||
"type": "missing", |
|||
"loc": ["body", "password"], |
|||
"msg": "Field required", |
|||
"input": {}, |
|||
}, |
|||
] |
|||
} |
|||
|
|||
|
|||
@needs_pydanticv2 |
|||
@needs_py39 |
|||
def test_openapi_schema(client: TestClient): |
|||
response = client.get("/openapi.json") |
|||
assert response.status_code == 200, response.text |
|||
assert response.json() == { |
|||
"openapi": "3.1.0", |
|||
"info": {"title": "FastAPI", "version": "0.1.0"}, |
|||
"paths": { |
|||
"/login/": { |
|||
"post": { |
|||
"responses": { |
|||
"200": { |
|||
"description": "Successful Response", |
|||
"content": {"application/json": {"schema": {}}}, |
|||
}, |
|||
"422": { |
|||
"description": "Validation Error", |
|||
"content": { |
|||
"application/json": { |
|||
"schema": { |
|||
"$ref": "#/components/schemas/HTTPValidationError" |
|||
} |
|||
} |
|||
}, |
|||
}, |
|||
}, |
|||
"summary": "Login", |
|||
"operationId": "login_login__post", |
|||
"requestBody": { |
|||
"content": { |
|||
"application/x-www-form-urlencoded": { |
|||
"schema": {"$ref": "#/components/schemas/FormData"} |
|||
} |
|||
}, |
|||
"required": True, |
|||
}, |
|||
} |
|||
} |
|||
}, |
|||
"components": { |
|||
"schemas": { |
|||
"FormData": { |
|||
"properties": { |
|||
"username": {"type": "string", "title": "Username"}, |
|||
"password": {"type": "string", "title": "Password"}, |
|||
}, |
|||
"additionalProperties": False, |
|||
"type": "object", |
|||
"required": ["username", "password"], |
|||
"title": "FormData", |
|||
}, |
|||
"ValidationError": { |
|||
"title": "ValidationError", |
|||
"required": ["loc", "msg", "type"], |
|||
"type": "object", |
|||
"properties": { |
|||
"loc": { |
|||
"title": "Location", |
|||
"type": "array", |
|||
"items": { |
|||
"anyOf": [{"type": "string"}, {"type": "integer"}] |
|||
}, |
|||
}, |
|||
"msg": {"title": "Message", "type": "string"}, |
|||
"type": {"title": "Error Type", "type": "string"}, |
|||
}, |
|||
}, |
|||
"HTTPValidationError": { |
|||
"title": "HTTPValidationError", |
|||
"type": "object", |
|||
"properties": { |
|||
"detail": { |
|||
"title": "Detail", |
|||
"type": "array", |
|||
"items": {"$ref": "#/components/schemas/ValidationError"}, |
|||
} |
|||
}, |
|||
}, |
|||
} |
|||
}, |
|||
} |
@ -0,0 +1,189 @@ |
|||
import pytest |
|||
from fastapi.testclient import TestClient |
|||
|
|||
from tests.utils import needs_pydanticv1 |
|||
|
|||
|
|||
@pytest.fixture(name="client") |
|||
def get_client(): |
|||
from docs_src.request_form_models.tutorial002_pv1 import app |
|||
|
|||
client = TestClient(app) |
|||
return client |
|||
|
|||
|
|||
@needs_pydanticv1 |
|||
def test_post_body_form(client: TestClient): |
|||
response = client.post("/login/", data={"username": "Foo", "password": "secret"}) |
|||
assert response.status_code == 200 |
|||
assert response.json() == {"username": "Foo", "password": "secret"} |
|||
|
|||
|
|||
@needs_pydanticv1 |
|||
def test_post_body_extra_form(client: TestClient): |
|||
response = client.post( |
|||
"/login/", data={"username": "Foo", "password": "secret", "extra": "extra"} |
|||
) |
|||
assert response.status_code == 422 |
|||
assert response.json() == { |
|||
"detail": [ |
|||
{ |
|||
"type": "value_error.extra", |
|||
"loc": ["body", "extra"], |
|||
"msg": "extra fields not permitted", |
|||
} |
|||
] |
|||
} |
|||
|
|||
|
|||
@needs_pydanticv1 |
|||
def test_post_body_form_no_password(client: TestClient): |
|||
response = client.post("/login/", data={"username": "Foo"}) |
|||
assert response.status_code == 422 |
|||
assert response.json() == { |
|||
"detail": [ |
|||
{ |
|||
"type": "value_error.missing", |
|||
"loc": ["body", "password"], |
|||
"msg": "field required", |
|||
} |
|||
] |
|||
} |
|||
|
|||
|
|||
@needs_pydanticv1 |
|||
def test_post_body_form_no_username(client: TestClient): |
|||
response = client.post("/login/", data={"password": "secret"}) |
|||
assert response.status_code == 422 |
|||
assert response.json() == { |
|||
"detail": [ |
|||
{ |
|||
"type": "value_error.missing", |
|||
"loc": ["body", "username"], |
|||
"msg": "field required", |
|||
} |
|||
] |
|||
} |
|||
|
|||
|
|||
@needs_pydanticv1 |
|||
def test_post_body_form_no_data(client: TestClient): |
|||
response = client.post("/login/") |
|||
assert response.status_code == 422 |
|||
assert response.json() == { |
|||
"detail": [ |
|||
{ |
|||
"type": "value_error.missing", |
|||
"loc": ["body", "username"], |
|||
"msg": "field required", |
|||
}, |
|||
{ |
|||
"type": "value_error.missing", |
|||
"loc": ["body", "password"], |
|||
"msg": "field required", |
|||
}, |
|||
] |
|||
} |
|||
|
|||
|
|||
@needs_pydanticv1 |
|||
def test_post_body_json(client: TestClient): |
|||
response = client.post("/login/", json={"username": "Foo", "password": "secret"}) |
|||
assert response.status_code == 422, response.text |
|||
assert response.json() == { |
|||
"detail": [ |
|||
{ |
|||
"type": "value_error.missing", |
|||
"loc": ["body", "username"], |
|||
"msg": "field required", |
|||
}, |
|||
{ |
|||
"type": "value_error.missing", |
|||
"loc": ["body", "password"], |
|||
"msg": "field required", |
|||
}, |
|||
] |
|||
} |
|||
|
|||
|
|||
@needs_pydanticv1 |
|||
def test_openapi_schema(client: TestClient): |
|||
response = client.get("/openapi.json") |
|||
assert response.status_code == 200, response.text |
|||
assert response.json() == { |
|||
"openapi": "3.1.0", |
|||
"info": {"title": "FastAPI", "version": "0.1.0"}, |
|||
"paths": { |
|||
"/login/": { |
|||
"post": { |
|||
"responses": { |
|||
"200": { |
|||
"description": "Successful Response", |
|||
"content": {"application/json": {"schema": {}}}, |
|||
}, |
|||
"422": { |
|||
"description": "Validation Error", |
|||
"content": { |
|||
"application/json": { |
|||
"schema": { |
|||
"$ref": "#/components/schemas/HTTPValidationError" |
|||
} |
|||
} |
|||
}, |
|||
}, |
|||
}, |
|||
"summary": "Login", |
|||
"operationId": "login_login__post", |
|||
"requestBody": { |
|||
"content": { |
|||
"application/x-www-form-urlencoded": { |
|||
"schema": {"$ref": "#/components/schemas/FormData"} |
|||
} |
|||
}, |
|||
"required": True, |
|||
}, |
|||
} |
|||
} |
|||
}, |
|||
"components": { |
|||
"schemas": { |
|||
"FormData": { |
|||
"properties": { |
|||
"username": {"type": "string", "title": "Username"}, |
|||
"password": {"type": "string", "title": "Password"}, |
|||
}, |
|||
"additionalProperties": False, |
|||
"type": "object", |
|||
"required": ["username", "password"], |
|||
"title": "FormData", |
|||
}, |
|||
"ValidationError": { |
|||
"title": "ValidationError", |
|||
"required": ["loc", "msg", "type"], |
|||
"type": "object", |
|||
"properties": { |
|||
"loc": { |
|||
"title": "Location", |
|||
"type": "array", |
|||
"items": { |
|||
"anyOf": [{"type": "string"}, {"type": "integer"}] |
|||
}, |
|||
}, |
|||
"msg": {"title": "Message", "type": "string"}, |
|||
"type": {"title": "Error Type", "type": "string"}, |
|||
}, |
|||
}, |
|||
"HTTPValidationError": { |
|||
"title": "HTTPValidationError", |
|||
"type": "object", |
|||
"properties": { |
|||
"detail": { |
|||
"title": "Detail", |
|||
"type": "array", |
|||
"items": {"$ref": "#/components/schemas/ValidationError"}, |
|||
} |
|||
}, |
|||
}, |
|||
} |
|||
}, |
|||
} |
@ -0,0 +1,196 @@ |
|||
import pytest |
|||
from fastapi.testclient import TestClient |
|||
|
|||
from tests.utils import needs_pydanticv1 |
|||
|
|||
|
|||
@pytest.fixture(name="client") |
|||
def get_client(): |
|||
from docs_src.request_form_models.tutorial002_pv1_an import app |
|||
|
|||
client = TestClient(app) |
|||
return client |
|||
|
|||
|
|||
# TODO: remove when deprecating Pydantic v1 |
|||
@needs_pydanticv1 |
|||
def test_post_body_form(client: TestClient): |
|||
response = client.post("/login/", data={"username": "Foo", "password": "secret"}) |
|||
assert response.status_code == 200 |
|||
assert response.json() == {"username": "Foo", "password": "secret"} |
|||
|
|||
|
|||
# TODO: remove when deprecating Pydantic v1 |
|||
@needs_pydanticv1 |
|||
def test_post_body_extra_form(client: TestClient): |
|||
response = client.post( |
|||
"/login/", data={"username": "Foo", "password": "secret", "extra": "extra"} |
|||
) |
|||
assert response.status_code == 422 |
|||
assert response.json() == { |
|||
"detail": [ |
|||
{ |
|||
"type": "value_error.extra", |
|||
"loc": ["body", "extra"], |
|||
"msg": "extra fields not permitted", |
|||
} |
|||
] |
|||
} |
|||
|
|||
|
|||
# TODO: remove when deprecating Pydantic v1 |
|||
@needs_pydanticv1 |
|||
def test_post_body_form_no_password(client: TestClient): |
|||
response = client.post("/login/", data={"username": "Foo"}) |
|||
assert response.status_code == 422 |
|||
assert response.json() == { |
|||
"detail": [ |
|||
{ |
|||
"type": "value_error.missing", |
|||
"loc": ["body", "password"], |
|||
"msg": "field required", |
|||
} |
|||
] |
|||
} |
|||
|
|||
|
|||
# TODO: remove when deprecating Pydantic v1 |
|||
@needs_pydanticv1 |
|||
def test_post_body_form_no_username(client: TestClient): |
|||
response = client.post("/login/", data={"password": "secret"}) |
|||
assert response.status_code == 422 |
|||
assert response.json() == { |
|||
"detail": [ |
|||
{ |
|||
"type": "value_error.missing", |
|||
"loc": ["body", "username"], |
|||
"msg": "field required", |
|||
} |
|||
] |
|||
} |
|||
|
|||
|
|||
# TODO: remove when deprecating Pydantic v1 |
|||
@needs_pydanticv1 |
|||
def test_post_body_form_no_data(client: TestClient): |
|||
response = client.post("/login/") |
|||
assert response.status_code == 422 |
|||
assert response.json() == { |
|||
"detail": [ |
|||
{ |
|||
"type": "value_error.missing", |
|||
"loc": ["body", "username"], |
|||
"msg": "field required", |
|||
}, |
|||
{ |
|||
"type": "value_error.missing", |
|||
"loc": ["body", "password"], |
|||
"msg": "field required", |
|||
}, |
|||
] |
|||
} |
|||
|
|||
|
|||
# TODO: remove when deprecating Pydantic v1 |
|||
@needs_pydanticv1 |
|||
def test_post_body_json(client: TestClient): |
|||
response = client.post("/login/", json={"username": "Foo", "password": "secret"}) |
|||
assert response.status_code == 422, response.text |
|||
assert response.json() == { |
|||
"detail": [ |
|||
{ |
|||
"type": "value_error.missing", |
|||
"loc": ["body", "username"], |
|||
"msg": "field required", |
|||
}, |
|||
{ |
|||
"type": "value_error.missing", |
|||
"loc": ["body", "password"], |
|||
"msg": "field required", |
|||
}, |
|||
] |
|||
} |
|||
|
|||
|
|||
# TODO: remove when deprecating Pydantic v1 |
|||
@needs_pydanticv1 |
|||
def test_openapi_schema(client: TestClient): |
|||
response = client.get("/openapi.json") |
|||
assert response.status_code == 200, response.text |
|||
assert response.json() == { |
|||
"openapi": "3.1.0", |
|||
"info": {"title": "FastAPI", "version": "0.1.0"}, |
|||
"paths": { |
|||
"/login/": { |
|||
"post": { |
|||
"responses": { |
|||
"200": { |
|||
"description": "Successful Response", |
|||
"content": {"application/json": {"schema": {}}}, |
|||
}, |
|||
"422": { |
|||
"description": "Validation Error", |
|||
"content": { |
|||
"application/json": { |
|||
"schema": { |
|||
"$ref": "#/components/schemas/HTTPValidationError" |
|||
} |
|||
} |
|||
}, |
|||
}, |
|||
}, |
|||
"summary": "Login", |
|||
"operationId": "login_login__post", |
|||
"requestBody": { |
|||
"content": { |
|||
"application/x-www-form-urlencoded": { |
|||
"schema": {"$ref": "#/components/schemas/FormData"} |
|||
} |
|||
}, |
|||
"required": True, |
|||
}, |
|||
} |
|||
} |
|||
}, |
|||
"components": { |
|||
"schemas": { |
|||
"FormData": { |
|||
"properties": { |
|||
"username": {"type": "string", "title": "Username"}, |
|||
"password": {"type": "string", "title": "Password"}, |
|||
}, |
|||
"additionalProperties": False, |
|||
"type": "object", |
|||
"required": ["username", "password"], |
|||
"title": "FormData", |
|||
}, |
|||
"ValidationError": { |
|||
"title": "ValidationError", |
|||
"required": ["loc", "msg", "type"], |
|||
"type": "object", |
|||
"properties": { |
|||
"loc": { |
|||
"title": "Location", |
|||
"type": "array", |
|||
"items": { |
|||
"anyOf": [{"type": "string"}, {"type": "integer"}] |
|||
}, |
|||
}, |
|||
"msg": {"title": "Message", "type": "string"}, |
|||
"type": {"title": "Error Type", "type": "string"}, |
|||
}, |
|||
}, |
|||
"HTTPValidationError": { |
|||
"title": "HTTPValidationError", |
|||
"type": "object", |
|||
"properties": { |
|||
"detail": { |
|||
"title": "Detail", |
|||
"type": "array", |
|||
"items": {"$ref": "#/components/schemas/ValidationError"}, |
|||
} |
|||
}, |
|||
}, |
|||
} |
|||
}, |
|||
} |
@ -0,0 +1,203 @@ |
|||
import pytest |
|||
from fastapi.testclient import TestClient |
|||
|
|||
from tests.utils import needs_py39, needs_pydanticv1 |
|||
|
|||
|
|||
@pytest.fixture(name="client") |
|||
def get_client(): |
|||
from docs_src.request_form_models.tutorial002_pv1_an_py39 import app |
|||
|
|||
client = TestClient(app) |
|||
return client |
|||
|
|||
|
|||
# TODO: remove when deprecating Pydantic v1 |
|||
@needs_pydanticv1 |
|||
@needs_py39 |
|||
def test_post_body_form(client: TestClient): |
|||
response = client.post("/login/", data={"username": "Foo", "password": "secret"}) |
|||
assert response.status_code == 200 |
|||
assert response.json() == {"username": "Foo", "password": "secret"} |
|||
|
|||
|
|||
# TODO: remove when deprecating Pydantic v1 |
|||
@needs_pydanticv1 |
|||
@needs_py39 |
|||
def test_post_body_extra_form(client: TestClient): |
|||
response = client.post( |
|||
"/login/", data={"username": "Foo", "password": "secret", "extra": "extra"} |
|||
) |
|||
assert response.status_code == 422 |
|||
assert response.json() == { |
|||
"detail": [ |
|||
{ |
|||
"type": "value_error.extra", |
|||
"loc": ["body", "extra"], |
|||
"msg": "extra fields not permitted", |
|||
} |
|||
] |
|||
} |
|||
|
|||
|
|||
# TODO: remove when deprecating Pydantic v1 |
|||
@needs_pydanticv1 |
|||
@needs_py39 |
|||
def test_post_body_form_no_password(client: TestClient): |
|||
response = client.post("/login/", data={"username": "Foo"}) |
|||
assert response.status_code == 422 |
|||
assert response.json() == { |
|||
"detail": [ |
|||
{ |
|||
"type": "value_error.missing", |
|||
"loc": ["body", "password"], |
|||
"msg": "field required", |
|||
} |
|||
] |
|||
} |
|||
|
|||
|
|||
# TODO: remove when deprecating Pydantic v1 |
|||
@needs_pydanticv1 |
|||
@needs_py39 |
|||
def test_post_body_form_no_username(client: TestClient): |
|||
response = client.post("/login/", data={"password": "secret"}) |
|||
assert response.status_code == 422 |
|||
assert response.json() == { |
|||
"detail": [ |
|||
{ |
|||
"type": "value_error.missing", |
|||
"loc": ["body", "username"], |
|||
"msg": "field required", |
|||
} |
|||
] |
|||
} |
|||
|
|||
|
|||
# TODO: remove when deprecating Pydantic v1 |
|||
@needs_pydanticv1 |
|||
@needs_py39 |
|||
def test_post_body_form_no_data(client: TestClient): |
|||
response = client.post("/login/") |
|||
assert response.status_code == 422 |
|||
assert response.json() == { |
|||
"detail": [ |
|||
{ |
|||
"type": "value_error.missing", |
|||
"loc": ["body", "username"], |
|||
"msg": "field required", |
|||
}, |
|||
{ |
|||
"type": "value_error.missing", |
|||
"loc": ["body", "password"], |
|||
"msg": "field required", |
|||
}, |
|||
] |
|||
} |
|||
|
|||
|
|||
# TODO: remove when deprecating Pydantic v1 |
|||
@needs_pydanticv1 |
|||
@needs_py39 |
|||
def test_post_body_json(client: TestClient): |
|||
response = client.post("/login/", json={"username": "Foo", "password": "secret"}) |
|||
assert response.status_code == 422, response.text |
|||
assert response.json() == { |
|||
"detail": [ |
|||
{ |
|||
"type": "value_error.missing", |
|||
"loc": ["body", "username"], |
|||
"msg": "field required", |
|||
}, |
|||
{ |
|||
"type": "value_error.missing", |
|||
"loc": ["body", "password"], |
|||
"msg": "field required", |
|||
}, |
|||
] |
|||
} |
|||
|
|||
|
|||
# TODO: remove when deprecating Pydantic v1 |
|||
@needs_pydanticv1 |
|||
@needs_py39 |
|||
def test_openapi_schema(client: TestClient): |
|||
response = client.get("/openapi.json") |
|||
assert response.status_code == 200, response.text |
|||
assert response.json() == { |
|||
"openapi": "3.1.0", |
|||
"info": {"title": "FastAPI", "version": "0.1.0"}, |
|||
"paths": { |
|||
"/login/": { |
|||
"post": { |
|||
"responses": { |
|||
"200": { |
|||
"description": "Successful Response", |
|||
"content": {"application/json": {"schema": {}}}, |
|||
}, |
|||
"422": { |
|||
"description": "Validation Error", |
|||
"content": { |
|||
"application/json": { |
|||
"schema": { |
|||
"$ref": "#/components/schemas/HTTPValidationError" |
|||
} |
|||
} |
|||
}, |
|||
}, |
|||
}, |
|||
"summary": "Login", |
|||
"operationId": "login_login__post", |
|||
"requestBody": { |
|||
"content": { |
|||
"application/x-www-form-urlencoded": { |
|||
"schema": {"$ref": "#/components/schemas/FormData"} |
|||
} |
|||
}, |
|||
"required": True, |
|||
}, |
|||
} |
|||
} |
|||
}, |
|||
"components": { |
|||
"schemas": { |
|||
"FormData": { |
|||
"properties": { |
|||
"username": {"type": "string", "title": "Username"}, |
|||
"password": {"type": "string", "title": "Password"}, |
|||
}, |
|||
"additionalProperties": False, |
|||
"type": "object", |
|||
"required": ["username", "password"], |
|||
"title": "FormData", |
|||
}, |
|||
"ValidationError": { |
|||
"title": "ValidationError", |
|||
"required": ["loc", "msg", "type"], |
|||
"type": "object", |
|||
"properties": { |
|||
"loc": { |
|||
"title": "Location", |
|||
"type": "array", |
|||
"items": { |
|||
"anyOf": [{"type": "string"}, {"type": "integer"}] |
|||
}, |
|||
}, |
|||
"msg": {"title": "Message", "type": "string"}, |
|||
"type": {"title": "Error Type", "type": "string"}, |
|||
}, |
|||
}, |
|||
"HTTPValidationError": { |
|||
"title": "HTTPValidationError", |
|||
"type": "object", |
|||
"properties": { |
|||
"detail": { |
|||
"title": "Detail", |
|||
"type": "array", |
|||
"items": {"$ref": "#/components/schemas/ValidationError"}, |
|||
} |
|||
}, |
|||
}, |
|||
} |
|||
}, |
|||
} |
Loading…
Reference in new issue