mirror of
https://github.com/fastapi/fastapi.git
synced 2025-12-27 16:21:06 -05:00
Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
212fd5e247 | ||
|
|
8dc882f751 | ||
|
|
b0eedbb580 | ||
|
|
74451189f6 | ||
|
|
a4c5f7f62f | ||
|
|
eb45bade63 | ||
|
|
944b6e507e | ||
|
|
e69ba26386 | ||
|
|
a4a7925045 | ||
|
|
73d4f347df | ||
|
|
80e2cd1274 | ||
|
|
bc715d55bc | ||
|
|
fc601bcb4b | ||
|
|
da4670cf77 | ||
|
|
a67167dce3 | ||
|
|
c49c4e7df8 | ||
|
|
270aef71c4 | ||
|
|
3a4431b6fe | ||
|
|
ec2a508292 | ||
|
|
b501fc6daf | ||
|
|
edb584199f | ||
|
|
4b9e5b3a74 | ||
|
|
b60d36e753 | ||
|
|
bde12faea2 | ||
|
|
74842f0a60 | ||
|
|
e68d8c60fb | ||
|
|
4ff22a0c41 | ||
|
|
a11e392f5f | ||
|
|
4633b1bca9 | ||
|
|
1b06b53267 | ||
|
|
c411b81c29 | ||
|
|
d86f660302 | ||
|
|
179f838c36 | ||
|
|
afdda4e50b | ||
|
|
e787f854dd | ||
|
|
7bad7c0975 | ||
|
|
965fc8301e |
9
.github/workflows/issue-manager.yml
vendored
9
.github/workflows/issue-manager.yml
vendored
@@ -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/issue-manager@0.5.0
|
||||
- uses: tiangolo/issue-manager@0.5.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."
|
||||
}
|
||||
}
|
||||
|
||||
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
@@ -37,7 +37,7 @@ jobs:
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
run: pip install -r requirements-tests.txt
|
||||
- name: Install Pydantic v2
|
||||
run: pip install "pydantic>=2.0.2,<3.0.0"
|
||||
run: pip install --upgrade "pydantic>=2.0.2,<3.0.0"
|
||||
- name: Lint
|
||||
run: bash scripts/lint.sh
|
||||
|
||||
@@ -79,7 +79,7 @@ jobs:
|
||||
run: pip install "pydantic>=1.10.0,<2.0.0"
|
||||
- name: Install Pydantic v2
|
||||
if: matrix.pydantic-version == 'pydantic-v2'
|
||||
run: pip install "pydantic>=2.0.2,<3.0.0"
|
||||
run: pip install --upgrade "pydantic>=2.0.2,<3.0.0"
|
||||
- run: mkdir coverage
|
||||
- name: Test
|
||||
run: bash scripts/test.sh
|
||||
|
||||
@@ -14,7 +14,7 @@ repos:
|
||||
- id: end-of-file-fixer
|
||||
- id: trailing-whitespace
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.6.3
|
||||
rev: v0.6.4
|
||||
hooks:
|
||||
- id: ruff
|
||||
args:
|
||||
|
||||
@@ -52,7 +52,7 @@ The key features are:
|
||||
<a href="https://bump.sh/fastapi?utm_source=fastapi&utm_medium=referral&utm_campaign=sponsor" target="_blank" title="Automate FastAPI documentation generation with Bump.sh"><img src="https://fastapi.tiangolo.com/img/sponsors/bump-sh.svg"></a>
|
||||
<a href="https://github.com/scalar/scalar/?utm_source=fastapi&utm_medium=website&utm_campaign=main-badge" target="_blank" title="Scalar: Beautiful Open-Source API References from Swagger/OpenAPI files"><img src="https://fastapi.tiangolo.com/img/sponsors/scalar.svg"></a>
|
||||
<a href="https://www.propelauth.com/?utm_source=fastapi&utm_campaign=1223&utm_medium=mainbadge" target="_blank" title="Auth, user management and more for your B2B product"><img src="https://fastapi.tiangolo.com/img/sponsors/propelauth.png"></a>
|
||||
<a href="https://docs.withcoherence.com/coherence-templates/full-stack-template/#fastapi?utm_medium=advertising&utm_source=fastapi&utm_campaign=docs" target="_blank" title="Coherence"><img src="https://fastapi.tiangolo.com/img/sponsors/coherence.png"></a>
|
||||
<a href="https://www.withcoherence.com/?utm_medium=advertising&utm_source=fastapi&utm_campaign=website" target="_blank" title="Coherence"><img src="https://fastapi.tiangolo.com/img/sponsors/coherence.png"></a>
|
||||
<a href="https://www.mongodb.com/developer/languages/python/python-quickstart-fastapi/?utm_campaign=fastapi_framework&utm_source=fastapi_sponsorship&utm_medium=web_referral" target="_blank" title="Simplify Full Stack Development with FastAPI & MongoDB"><img src="https://fastapi.tiangolo.com/img/sponsors/mongodb.png"></a>
|
||||
<a href="https://zuplo.link/fastapi-gh" target="_blank" title="Zuplo: Scale, Protect, Document, and Monetize your FastAPI"><img src="https://fastapi.tiangolo.com/img/sponsors/zuplo.png"></a>
|
||||
<a href="https://fine.dev?ref=fastapibadge" target="_blank" title="Fine's AI FastAPI Workflow: Effortlessly Deploy and Integrate FastAPI into Your Project"><img src="https://fastapi.tiangolo.com/img/sponsors/fine.png"></a>
|
||||
|
||||
@@ -17,7 +17,7 @@ gold:
|
||||
- url: https://www.propelauth.com/?utm_source=fastapi&utm_campaign=1223&utm_medium=mainbadge
|
||||
title: Auth, user management and more for your B2B product
|
||||
img: https://fastapi.tiangolo.com/img/sponsors/propelauth.png
|
||||
- url: https://docs.withcoherence.com/coherence-templates/full-stack-template/#fastapi?utm_medium=advertising&utm_source=fastapi&utm_campaign=docs
|
||||
- url: https://www.withcoherence.com/?utm_medium=advertising&utm_source=fastapi&utm_campaign=website
|
||||
title: Coherence
|
||||
img: https://fastapi.tiangolo.com/img/sponsors/coherence.png
|
||||
- url: https://www.mongodb.com/developer/languages/python/python-quickstart-fastapi/?utm_campaign=fastapi_framework&utm_source=fastapi_sponsorship&utm_medium=web_referral
|
||||
|
||||
@@ -14,4 +14,4 @@ You might want to try their services and follow their guides:
|
||||
|
||||
* <a href="https://docs.platform.sh/languages/python.html?utm_source=fastapi-signup&utm_medium=banner&utm_campaign=FastAPI-signup-June-2023" class="external-link" target="_blank">Platform.sh</a>
|
||||
* <a href="https://docs.porter.run/language-specific-guides/fastapi" class="external-link" target="_blank">Porter</a>
|
||||
* <a href="https://docs.withcoherence.com/coherence-templates/full-stack-template/#fastapi?utm_medium=advertising&utm_source=fastapi&utm_campaign=docs" class="external-link" target="_blank">Coherence</a>
|
||||
* <a href="https://www.withcoherence.com/?utm_medium=advertising&utm_source=fastapi&utm_campaign=website" class="external-link" target="_blank">Coherence</a>
|
||||
|
||||
@@ -243,8 +243,6 @@ This way, when you type `python` in the terminal, the system will find the Pytho
|
||||
|
||||
////
|
||||
|
||||
This way, when you type `python` in the terminal, the system will find the Python program in `/opt/custompython/bin` (the last directory) and use that one.
|
||||
|
||||
So, if you type:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
BIN
docs/en/docs/img/tutorial/request-form-models/image01.png
Normal file
BIN
docs/en/docs/img/tutorial/request-form-models/image01.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 43 KiB |
@@ -7,6 +7,102 @@ hide:
|
||||
|
||||
## Latest Changes
|
||||
|
||||
## 0.114.1
|
||||
|
||||
### Refactors
|
||||
|
||||
* ⚡️ Improve performance in request body parsing with a cache for internal model fields. PR [#12184](https://github.com/fastapi/fastapi/pull/12184) by [@tiangolo](https://github.com/tiangolo).
|
||||
|
||||
### Docs
|
||||
|
||||
* 📝 Remove duplicate line in docs for `docs/en/docs/environment-variables.md`. PR [#12169](https://github.com/fastapi/fastapi/pull/12169) by [@prometek](https://github.com/prometek).
|
||||
|
||||
### Translations
|
||||
|
||||
* 🌐 Add Portuguese translation for `docs/pt/docs/virtual-environments.md`. PR [#12163](https://github.com/fastapi/fastapi/pull/12163) by [@marcelomarkus](https://github.com/marcelomarkus).
|
||||
* 🌐 Add Portuguese translation for `docs/pt/docs/environment-variables.md`. PR [#12162](https://github.com/fastapi/fastapi/pull/12162) by [@marcelomarkus](https://github.com/marcelomarkus).
|
||||
* 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/testing.md`. PR [#12164](https://github.com/fastapi/fastapi/pull/12164) by [@marcelomarkus](https://github.com/marcelomarkus).
|
||||
* 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/debugging.md`. PR [#12165](https://github.com/fastapi/fastapi/pull/12165) by [@marcelomarkus](https://github.com/marcelomarkus).
|
||||
* 🌐 Add Korean translation for `docs/ko/docs/project-generation.md`. PR [#12157](https://github.com/fastapi/fastapi/pull/12157) by [@BORA040126](https://github.com/BORA040126).
|
||||
|
||||
### Internal
|
||||
|
||||
* ⬆ Bump tiangolo/issue-manager from 0.5.0 to 0.5.1. PR [#12173](https://github.com/fastapi/fastapi/pull/12173) by [@dependabot[bot]](https://github.com/apps/dependabot).
|
||||
* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#12176](https://github.com/fastapi/fastapi/pull/12176) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci).
|
||||
* 👷 Update `issue-manager.yml`. PR [#12159](https://github.com/fastapi/fastapi/pull/12159) by [@tiangolo](https://github.com/tiangolo).
|
||||
* ✏️ Fix typo in `fastapi/params.py`. PR [#12143](https://github.com/fastapi/fastapi/pull/12143) by [@surreal30](https://github.com/surreal30).
|
||||
|
||||
## 0.114.0
|
||||
|
||||
You can restrict form fields to only include those declared in a Pydantic model and forbid any extra field sent in the request using Pydantic's `model_config = {"extra": "forbid"}`:
|
||||
|
||||
```python
|
||||
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
|
||||
```
|
||||
|
||||
Read the new docs: [Form Models - Forbid Extra Form Fields](https://fastapi.tiangolo.com/tutorial/request-form-models/#forbid-extra-form-fields).
|
||||
|
||||
### Features
|
||||
|
||||
* ✨ Add support for forbidding extra form fields with Pydantic models. PR [#12134](https://github.com/fastapi/fastapi/pull/12134) by [@tiangolo](https://github.com/tiangolo).
|
||||
|
||||
### Docs
|
||||
|
||||
* 📝 Update docs, Form Models section title, to match config name. PR [#12152](https://github.com/fastapi/fastapi/pull/12152) by [@tiangolo](https://github.com/tiangolo).
|
||||
|
||||
### Internal
|
||||
|
||||
* ✅ Update internal tests for latest Pydantic, including CI tweaks to install the latest Pydantic. PR [#12147](https://github.com/fastapi/fastapi/pull/12147) by [@tiangolo](https://github.com/tiangolo).
|
||||
|
||||
## 0.113.0
|
||||
|
||||
Now you can declare form fields with Pydantic models:
|
||||
|
||||
```python
|
||||
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
|
||||
```
|
||||
|
||||
Read the new docs: [Form Models](https://fastapi.tiangolo.com/tutorial/request-form-models/).
|
||||
|
||||
### Features
|
||||
|
||||
* ✨ Add support for Pydantic models in `Form` parameters. PR [#12129](https://github.com/fastapi/fastapi/pull/12129) by [@tiangolo](https://github.com/tiangolo).
|
||||
|
||||
### Internal
|
||||
|
||||
* 🔧 Update sponsors: Coherence link. PR [#12130](https://github.com/fastapi/fastapi/pull/12130) by [@tiangolo](https://github.com/tiangolo).
|
||||
|
||||
## 0.112.4
|
||||
|
||||
This release is mainly a big internal refactor to enable adding support for Pydantic models for `Form` fields, but that feature comes in the next release.
|
||||
@@ -19,8 +115,8 @@ This release shouldn't affect apps using FastAPI in any way. You don't even have
|
||||
|
||||
### Internal
|
||||
|
||||
* ⏪️ Temporarily revert "✨ Add support for Pydantic models in `Form` parameters" to make a checkpoint release. PR [#12128](https://github.com/fastapi/fastapi/pull/12128) by [@tiangolo](https://github.com/tiangolo).
|
||||
* ✨ Add support for Pydantic models in `Form` parameters. PR [#12127](https://github.com/fastapi/fastapi/pull/12127) by [@tiangolo](https://github.com/tiangolo). Reverted to make a checkpoint release with only refactors.
|
||||
* ⏪️ Temporarily revert "✨ Add support for Pydantic models in `Form` parameters" to make a checkpoint release. PR [#12128](https://github.com/fastapi/fastapi/pull/12128) by [@tiangolo](https://github.com/tiangolo). Restored by PR [#12129](https://github.com/fastapi/fastapi/pull/12129).
|
||||
* ✨ Add support for Pydantic models in `Form` parameters. PR [#12127](https://github.com/fastapi/fastapi/pull/12127) by [@tiangolo](https://github.com/tiangolo). Reverted by PR [#12128](https://github.com/fastapi/fastapi/pull/12128) to make a checkpoint release with only refactors. Restored by PR [#12129](https://github.com/fastapi/fastapi/pull/12129).
|
||||
|
||||
## 0.112.3
|
||||
|
||||
|
||||
134
docs/en/docs/tutorial/request-form-models.md
Normal file
134
docs/en/docs/tutorial/request-form-models.md
Normal file
@@ -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. 😎
|
||||
@@ -129,6 +129,7 @@ nav:
|
||||
- tutorial/extra-models.md
|
||||
- tutorial/response-status-code.md
|
||||
- tutorial/request-forms.md
|
||||
- tutorial/request-form-models.md
|
||||
- tutorial/request-files.md
|
||||
- tutorial/request-forms-and-files.md
|
||||
- tutorial/handling-errors.md
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
</a>
|
||||
</div>
|
||||
<div class="item">
|
||||
<a title="Coherence" style="display: block; position: relative;" href="https://docs.withcoherence.com/coherence-templates/full-stack-template/#fastapi?utm_medium=advertising&utm_source=fastapi&utm_campaign=docs" target="_blank">
|
||||
<a title="Coherence" style="display: block; position: relative;" href="https://www.withcoherence.com/?utm_medium=advertising&utm_source=fastapi&utm_campaign=website" target="_blank">
|
||||
<span class="sponsor-badge">sponsor</span>
|
||||
<img class="sponsor-image" src="/img/sponsors/coherence-banner.png" />
|
||||
</a>
|
||||
|
||||
28
docs/ko/docs/project-generation.md
Normal file
28
docs/ko/docs/project-generation.md
Normal file
@@ -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 (지속적인 배포).
|
||||
298
docs/pt/docs/environment-variables.md
Normal file
298
docs/pt/docs/environment-variables.md
Normal file
@@ -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).
|
||||
115
docs/pt/docs/tutorial/debugging.md
Normal file
115
docs/pt/docs/tutorial/debugging.md
Normal file
@@ -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">
|
||||
249
docs/pt/docs/tutorial/testing.md
Normal file
249
docs/pt/docs/tutorial/testing.md
Normal file
@@ -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>
|
||||
844
docs/pt/docs/virtual-environments.md
Normal file
844
docs/pt/docs/virtual-environments.md
Normal file
@@ -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**. 😎
|
||||
14
docs_src/request_form_models/tutorial001.py
Normal file
14
docs_src/request_form_models/tutorial001.py
Normal file
@@ -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
|
||||
15
docs_src/request_form_models/tutorial001_an.py
Normal file
15
docs_src/request_form_models/tutorial001_an.py
Normal file
@@ -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
|
||||
16
docs_src/request_form_models/tutorial001_an_py39.py
Normal file
16
docs_src/request_form_models/tutorial001_an_py39.py
Normal file
@@ -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
|
||||
15
docs_src/request_form_models/tutorial002.py
Normal file
15
docs_src/request_form_models/tutorial002.py
Normal file
@@ -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
|
||||
16
docs_src/request_form_models/tutorial002_an.py
Normal file
16
docs_src/request_form_models/tutorial002_an.py
Normal file
@@ -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
|
||||
17
docs_src/request_form_models/tutorial002_an_py39.py
Normal file
17
docs_src/request_form_models/tutorial002_an_py39.py
Normal file
@@ -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
|
||||
17
docs_src/request_form_models/tutorial002_pv1.py
Normal file
17
docs_src/request_form_models/tutorial002_pv1.py
Normal file
@@ -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
|
||||
18
docs_src/request_form_models/tutorial002_pv1_an.py
Normal file
18
docs_src/request_form_models/tutorial002_pv1_an.py
Normal file
@@ -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
|
||||
19
docs_src/request_form_models/tutorial002_pv1_an_py39.py
Normal file
19
docs_src/request_form_models/tutorial002_pv1_an_py39.py
Normal file
@@ -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
|
||||
@@ -1,6 +1,6 @@
|
||||
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
|
||||
|
||||
__version__ = "0.112.4"
|
||||
__version__ = "0.114.1"
|
||||
|
||||
from starlette import status as status
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ from collections import deque
|
||||
from copy import copy
|
||||
from dataclasses import dataclass, is_dataclass
|
||||
from enum import Enum
|
||||
from functools import lru_cache
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
@@ -649,3 +650,8 @@ def is_uploadfile_sequence_annotation(annotation: Any) -> bool:
|
||||
is_uploadfile_or_nonable_uploadfile_annotation(sub_annotation)
|
||||
for sub_annotation in get_args(annotation)
|
||||
)
|
||||
|
||||
|
||||
@lru_cache
|
||||
def get_cached_model_fields(model: Type[BaseModel]) -> List[ModelField]:
|
||||
return get_model_fields(model)
|
||||
|
||||
@@ -32,6 +32,7 @@ from fastapi._compat import (
|
||||
evaluate_forwardref,
|
||||
field_annotation_is_scalar,
|
||||
get_annotation_from_field_info,
|
||||
get_cached_model_fields,
|
||||
get_missing_field_error,
|
||||
is_bytes_field,
|
||||
is_bytes_sequence_field,
|
||||
@@ -56,6 +57,7 @@ from fastapi.security.base import SecurityBase
|
||||
from fastapi.security.oauth2 import OAuth2, SecurityScopes
|
||||
from fastapi.security.open_id_connect_url import OpenIdConnect
|
||||
from fastapi.utils import create_model_field, get_path_param_names
|
||||
from pydantic import BaseModel
|
||||
from pydantic.fields import FieldInfo
|
||||
from starlette.background import BackgroundTasks as StarletteBackgroundTasks
|
||||
from starlette.concurrency import run_in_threadpool
|
||||
@@ -743,7 +745,9 @@ def _should_embed_body_fields(fields: List[ModelField]) -> bool:
|
||||
return True
|
||||
# If it's a Form (or File) field, it has to be a BaseModel to be top level
|
||||
# otherwise it has to be embedded, so that the key value pair can be extracted
|
||||
if isinstance(first_field.field_info, params.Form):
|
||||
if isinstance(first_field.field_info, params.Form) and not lenient_issubclass(
|
||||
first_field.type_, BaseModel
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -783,7 +787,11 @@ async def _extract_form_body(
|
||||
for sub_value in value:
|
||||
tg.start_soon(process_fn, sub_value.read)
|
||||
value = serialize_sequence_value(field=field, value=results)
|
||||
values[field.name] = value
|
||||
if value is not None:
|
||||
values[field.name] = value
|
||||
for key, value in received_body.items():
|
||||
if key not in values:
|
||||
values[key] = value
|
||||
return values
|
||||
|
||||
|
||||
@@ -798,8 +806,14 @@ async def request_body_to_args(
|
||||
single_not_embedded_field = len(body_fields) == 1 and not embed_body_fields
|
||||
first_field = body_fields[0]
|
||||
body_to_process = received_body
|
||||
|
||||
fields_to_extract: List[ModelField] = body_fields
|
||||
|
||||
if single_not_embedded_field and lenient_issubclass(first_field.type_, BaseModel):
|
||||
fields_to_extract = get_cached_model_fields(first_field.type_)
|
||||
|
||||
if isinstance(received_body, FormData):
|
||||
body_to_process = await _extract_form_body(body_fields, received_body)
|
||||
body_to_process = await _extract_form_body(fields_to_extract, received_body)
|
||||
|
||||
if single_not_embedded_field:
|
||||
loc: Tuple[str, ...] = ("body",)
|
||||
|
||||
@@ -556,7 +556,7 @@ class Body(FieldInfo):
|
||||
kwargs["examples"] = examples
|
||||
if regex is not None:
|
||||
warnings.warn(
|
||||
"`regex` has been depreacated, please use `pattern` instead",
|
||||
"`regex` has been deprecated, please use `pattern` instead",
|
||||
category=DeprecationWarning,
|
||||
stacklevel=4,
|
||||
)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
pytest >=7.1.3,<8.0.0
|
||||
coverage[toml] >= 6.5.0,< 8.0
|
||||
mypy ==1.8.0
|
||||
ruff ==0.6.3
|
||||
ruff ==0.6.4
|
||||
dirty-equals ==0.6.0
|
||||
# TODO: once removing databases from tutorial, upgrade SQLAlchemy
|
||||
# probably when including SQLModel
|
||||
|
||||
36
scripts/playwright/request_form_models/image01.py
Normal file
36
scripts/playwright/request_form_models/image01.py
Normal file
@@ -0,0 +1,36 @@
|
||||
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)
|
||||
context = browser.new_context()
|
||||
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()
|
||||
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()
|
||||
@@ -5,6 +5,7 @@ from fastapi._compat import (
|
||||
ModelField,
|
||||
Undefined,
|
||||
_get_model_config,
|
||||
get_cached_model_fields,
|
||||
get_model_fields,
|
||||
is_bytes_sequence_annotation,
|
||||
is_scalar_field,
|
||||
@@ -102,3 +103,18 @@ def test_is_pv1_scalar_field():
|
||||
|
||||
fields = get_model_fields(Model)
|
||||
assert not is_scalar_field(fields[0])
|
||||
|
||||
|
||||
def test_get_model_fields_cached():
|
||||
class Model(BaseModel):
|
||||
foo: str
|
||||
|
||||
non_cached_fields = get_model_fields(Model)
|
||||
non_cached_fields2 = get_model_fields(Model)
|
||||
cached_fields = get_cached_model_fields(Model)
|
||||
cached_fields2 = get_cached_model_fields(Model)
|
||||
for f1, f2 in zip(cached_fields, cached_fields2):
|
||||
assert f1 is f2
|
||||
|
||||
assert non_cached_fields is not non_cached_fields2
|
||||
assert cached_fields is cached_fields2
|
||||
|
||||
129
tests/test_forms_single_model.py
Normal file
129
tests/test_forms_single_model.py
Normal file
@@ -0,0 +1,129 @@
|
||||
from typing import List, Optional
|
||||
|
||||
from dirty_equals import IsDict
|
||||
from fastapi import FastAPI, Form
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel
|
||||
from typing_extensions import Annotated
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
class FormModel(BaseModel):
|
||||
username: str
|
||||
lastname: str
|
||||
age: Optional[int] = None
|
||||
tags: List[str] = ["foo", "bar"]
|
||||
|
||||
|
||||
@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"],
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {
|
||||
"username": "Rick",
|
||||
"lastname": "Sanchez",
|
||||
"age": 70,
|
||||
"tags": ["plumbus", "citadel"],
|
||||
}
|
||||
|
||||
|
||||
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"],
|
||||
}
|
||||
|
||||
|
||||
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"]},
|
||||
},
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "lastname"],
|
||||
"msg": "Field required",
|
||||
"input": {"tags": ["foo", "bar"]},
|
||||
},
|
||||
]
|
||||
}
|
||||
) | 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",
|
||||
},
|
||||
]
|
||||
}
|
||||
)
|
||||
@@ -155,13 +155,26 @@ def test_openapi_schema():
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"allOf": [{"$ref": "#/components/schemas/Item"}],
|
||||
"title": "Item",
|
||||
"examples": [
|
||||
{"data": "Data in Body examples, example1"}
|
||||
],
|
||||
},
|
||||
"schema": IsDict(
|
||||
{
|
||||
"$ref": "#/components/schemas/Item",
|
||||
"examples": [
|
||||
{"data": "Data in Body examples, example1"}
|
||||
],
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
{
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
"allOf": [
|
||||
{"$ref": "#/components/schemas/Item"}
|
||||
],
|
||||
"title": "Item",
|
||||
"examples": [
|
||||
{"data": "Data in Body examples, example1"}
|
||||
],
|
||||
}
|
||||
),
|
||||
"examples": {
|
||||
"Example One": {
|
||||
"summary": "Example One Summary",
|
||||
|
||||
232
tests/test_tutorial/test_request_form_models/test_tutorial001.py
Normal file
232
tests/test_tutorial/test_request_form_models/test_tutorial001.py
Normal file
@@ -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"},
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
196
tests/test_tutorial/test_request_form_models/test_tutorial002.py
Normal file
196
tests/test_tutorial/test_request_form_models/test_tutorial002.py
Normal file
@@ -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"},
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user