committed by
GitHub
66 changed files with 7189 additions and 93 deletions
@ -29,7 +29,7 @@ jobs: |
|||
with: |
|||
python-version: "3.11" |
|||
- name: Setup uv |
|||
uses: astral-sh/setup-uv@v3 |
|||
uses: astral-sh/setup-uv@v4 |
|||
with: |
|||
version: "0.4.15" |
|||
enable-cache: true |
|||
@ -64,7 +64,7 @@ jobs: |
|||
BRANCH: ${{ ( github.event.workflow_run.head_repository.full_name == github.repository && github.event.workflow_run.head_branch == 'master' && 'main' ) || ( github.event.workflow_run.head_sha ) }} |
|||
# TODO: Use v3 when it's fixed, probably in v3.11 |
|||
# https://github.com/cloudflare/wrangler-action/issues/307 |
|||
uses: cloudflare/[email protected]2 |
|||
uses: cloudflare/[email protected]3 |
|||
# uses: cloudflare/wrangler-action@v3 |
|||
with: |
|||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} |
|||
|
@ -35,7 +35,7 @@ jobs: |
|||
TIANGOLO_BUILD_PACKAGE: ${{ matrix.package }} |
|||
run: python -m build |
|||
- name: Publish |
|||
uses: pypa/[email protected].2 |
|||
uses: pypa/[email protected].3 |
|||
- name: Dump GitHub context |
|||
env: |
|||
GITHUB_CONTEXT: ${{ toJson(github) }} |
|||
|
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 13 KiB |
@ -0,0 +1,332 @@ |
|||
# Langkah Pertama |
|||
|
|||
File FastAPI yang paling sederhana bisa seperti berikut: |
|||
|
|||
{* ../../docs_src/first_steps/tutorial001.py *} |
|||
|
|||
Salin file tersebut ke `main.py`. |
|||
|
|||
Jalankan di server: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ <font color="#4E9A06">fastapi</font> dev <u style="text-decoration-style:single">main.py</u> |
|||
<font color="#3465A4">INFO </font> Using path <font color="#3465A4">main.py</font> |
|||
<font color="#3465A4">INFO </font> Resolved absolute path <font color="#75507B">/home/user/code/awesomeapp/</font><font color="#AD7FA8">main.py</font> |
|||
<font color="#3465A4">INFO </font> Searching for package file structure from directories with <font color="#3465A4">__init__.py</font> files |
|||
<font color="#3465A4">INFO </font> Importing from <font color="#75507B">/home/user/code/</font><font color="#AD7FA8">awesomeapp</font> |
|||
|
|||
╭─ <font color="#8AE234"><b>Python module file</b></font> ─╮ |
|||
│ │ |
|||
│ 🐍 main.py │ |
|||
│ │ |
|||
╰──────────────────────╯ |
|||
|
|||
<font color="#3465A4">INFO </font> Importing module <font color="#4E9A06">main</font> |
|||
<font color="#3465A4">INFO </font> Found importable FastAPI app |
|||
|
|||
╭─ <font color="#8AE234"><b>Importable FastAPI app</b></font> ─╮ |
|||
│ │ |
|||
│ <span style="background-color:#272822"><font color="#FF4689">from</font></span><span style="background-color:#272822"><font color="#F8F8F2"> main </font></span><span style="background-color:#272822"><font color="#FF4689">import</font></span><span style="background-color:#272822"><font color="#F8F8F2"> app</font></span><span style="background-color:#272822"> </span> │ |
|||
│ │ |
|||
╰──────────────────────────╯ |
|||
|
|||
<font color="#3465A4">INFO </font> Using import string <font color="#8AE234"><b>main:app</b></font> |
|||
|
|||
<span style="background-color:#C4A000"><font color="#2E3436">╭────────── FastAPI CLI - Development mode ───────────╮</font></span> |
|||
<span style="background-color:#C4A000"><font color="#2E3436">│ │</font></span> |
|||
<span style="background-color:#C4A000"><font color="#2E3436">│ Serving at: http://127.0.0.1:8000 │</font></span> |
|||
<span style="background-color:#C4A000"><font color="#2E3436">│ │</font></span> |
|||
<span style="background-color:#C4A000"><font color="#2E3436">│ API docs: http://127.0.0.1:8000/docs │</font></span> |
|||
<span style="background-color:#C4A000"><font color="#2E3436">│ │</font></span> |
|||
<span style="background-color:#C4A000"><font color="#2E3436">│ Running in development mode, for production use: │</font></span> |
|||
<span style="background-color:#C4A000"><font color="#2E3436">│ │</font></span> |
|||
<span style="background-color:#C4A000"><font color="#2E3436">│ </font></span><span style="background-color:#C4A000"><font color="#555753"><b>fastapi run</b></font></span><span style="background-color:#C4A000"><font color="#2E3436"> │</font></span> |
|||
<span style="background-color:#C4A000"><font color="#2E3436">│ │</font></span> |
|||
<span style="background-color:#C4A000"><font color="#2E3436">╰─────────────────────────────────────────────────────╯</font></span> |
|||
|
|||
<font color="#4E9A06">INFO</font>: Will watch for changes in these directories: ['/home/user/code/awesomeapp'] |
|||
<font color="#4E9A06">INFO</font>: Uvicorn running on <b>http://127.0.0.1:8000</b> (Press CTRL+C to quit) |
|||
<font color="#4E9A06">INFO</font>: Started reloader process [<font color="#34E2E2"><b>2265862</b></font>] using <font color="#34E2E2"><b>WatchFiles</b></font> |
|||
<font color="#4E9A06">INFO</font>: Started server process [<font color="#06989A">2265873</font>] |
|||
<font color="#4E9A06">INFO</font>: Waiting for application startup. |
|||
<font color="#4E9A06">INFO</font>: Application startup complete. |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
Di output, terdapat sebaris pesan: |
|||
|
|||
```hl_lines="4" |
|||
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) |
|||
``` |
|||
|
|||
Baris tersebut menunjukan URL dimana app aktif di komputer anda. |
|||
|
|||
|
|||
### Mencoba aplikasi |
|||
|
|||
Buka browser di <a href="http://127.0.0.1:8000" class="external-link" target="_blank">http://127.0.0.1:8000</a>. |
|||
|
|||
Anda akan melihat response JSON sebagai berikut: |
|||
|
|||
```JSON |
|||
{"message": "Hello World"} |
|||
``` |
|||
|
|||
### Dokumen API interaktif |
|||
|
|||
Sekarang kunjungi <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>. |
|||
|
|||
Anda akan melihat dokumentasi API interaktif otomatis (dibuat oleh <a href="https://github.com/swagger-api/swagger-ui" class="external-link" target="_blank">Swagger UI</a>): |
|||
|
|||
 |
|||
|
|||
### Dokumen API alternatif |
|||
|
|||
Dan sekarang, kunjungi <a href="http://127.0.0.1:8000/redoc" class="external-link" target="_blank">http://127.0.0.1:8000/redoc</a>. |
|||
|
|||
Anda akan melihat dokumentasi alternatif otomatis (dibuat oleh <a href="https://github.com/Rebilly/ReDoc" class="external-link" target="_blank">ReDoc</a>): |
|||
|
|||
 |
|||
|
|||
### OpenAPI |
|||
|
|||
**FastAPI** membuat sebuah "schema" dimana semua API anda menggunakan standar **OpenAPI** untuk mendefinisikan API. |
|||
|
|||
#### "Schema" |
|||
|
|||
"schema" adalah suatu definisi atau deskripsi dari sesuatu. Bukan kode yang mengimplementasi definisi tersebut. Ini hanyalah sebuah deskripsi abstrak. |
|||
|
|||
#### "schema" API |
|||
|
|||
Dalam hal ini, <a href="https://github.com/OAI/OpenAPI-Specification" class="external-link" target="_blank">OpenAPI</a> adalah spesifikasi yang menunjukan bagaimana untuk mendefinisikan sebuah skema di API anda. |
|||
|
|||
Definisi skema ini termasuk jalur API anda, parameter yang bisa diterima, dll. |
|||
|
|||
#### "schema" Data |
|||
|
|||
Istilah "schema" bisa juga merujuk ke struktur data, seperti konten JSON. |
|||
|
|||
Dalam kondisi ini, ini berarti attribut JSON dan tipe data yang dimiliki, dll. |
|||
|
|||
#### Schema OpenAPI and JSON |
|||
|
|||
"schema" OpenAPI mendefinisikan skema API dari API yang anda buat. Skema tersebut termasuk definisi (atau "schema") dari data yang dikirim atau diterima oleh API dari **JSON Schema**, skema data standar JSON. |
|||
|
|||
#### Lihat `openapi.json` |
|||
|
|||
Jika anda penasaran bagaimana skema OpenAPI polos seperti apa, FastAPI secara otomatis membuat JSON (schema) dengan deksripsi API anda. |
|||
|
|||
anda bisa melihatnya di: <a href="http://127.0.0.1:8000/openapi.json" class="external-link" target="_blank">http://127.0.0.1:8000/openapi.json</a>. |
|||
|
|||
Anda akan melihat JSON yang dimulai seperti: |
|||
|
|||
```JSON |
|||
{ |
|||
"openapi": "3.1.0", |
|||
"info": { |
|||
"title": "FastAPI", |
|||
"version": "0.1.0" |
|||
}, |
|||
"paths": { |
|||
"/items/": { |
|||
"get": { |
|||
"responses": { |
|||
"200": { |
|||
"description": "Successful Response", |
|||
"content": { |
|||
"application/json": { |
|||
|
|||
|
|||
|
|||
... |
|||
``` |
|||
|
|||
#### Kegunaan OpenAPI |
|||
|
|||
Skema OpenAPI adalah tulang punggung dua sistem dokumentasi API interaktif yang ada di FastAPI. |
|||
|
|||
Ada banyak alternatif sistem dokumentasi lainnya yang semuanya berdasarkan OpenAPI. Anda bisa menambahkannya ke aplikasi **FastAPI** anda. |
|||
|
|||
Anda juga bisa menggunakan OpenAPI untuk membuat kode secara otomatis, untuk klien yang menggunakan API anda. Sebagai contoh, frontend, aplikasi mobile atau IoT. |
|||
|
|||
## Ringkasan, secara bertahap |
|||
|
|||
### Langkah 1: impor `FastAPI` |
|||
|
|||
{* ../../docs_src/first_steps/tutorial001.py hl[1] *} |
|||
|
|||
`FastAPI` adalah class Python yang menyediakan semua fungsionalitas API anda. |
|||
|
|||
/// note | Detail Teknis |
|||
|
|||
`FastAPI` adalah class turunan langsung dari `Starlette`. |
|||
|
|||
Anda bisa menggunakan semua fungsionalitas <a href="https://www.starlette.io/" class="external-link" target="_blank">Starlette</a> dengan `FastAPI` juga. |
|||
|
|||
/// |
|||
|
|||
### Langkah 2: buat "instance" dari `FastAPI` |
|||
|
|||
{* ../../docs_src/first_steps/tutorial001.py hl[3] *} |
|||
|
|||
Di sini variabel `app` akan menjadi sebuah "instance" dari class `FastAPI`. |
|||
|
|||
Ini akan menjadi gerbang utama untuk membuat semua API anda. |
|||
|
|||
### Langkah 3: Buat *operasi path* |
|||
|
|||
#### Path |
|||
|
|||
"Path" atau jalur di sini merujuk ke bagian URL terakhir dimulai dari `/` pertama. |
|||
|
|||
Sehingga, URL seperti: |
|||
|
|||
``` |
|||
https://example.com/items/foo |
|||
``` |
|||
|
|||
...path-nya adalah: |
|||
|
|||
``` |
|||
/items/foo |
|||
``` |
|||
|
|||
/// info |
|||
|
|||
"path" juga biasa disebut "endpoint" atau "route". |
|||
|
|||
/// |
|||
|
|||
ketika membuat API, "path" adalah jalan utama untuk memisahkan "concern" dan "resources". |
|||
|
|||
#### Operasi |
|||
|
|||
"Operasi" di sini merujuk ke salah satu dari metode HTTP berikut. |
|||
|
|||
Salah satu dari: |
|||
|
|||
* `POST` |
|||
* `GET` |
|||
* `PUT` |
|||
* `DELETE` |
|||
|
|||
...dan operasi lainnya yang unik: |
|||
|
|||
* `OPTIONS` |
|||
* `HEAD` |
|||
* `PATCH` |
|||
* `TRACE` |
|||
|
|||
Dalam protokol HTTP, anda bisa berkomunikasi ke setiap path menggunakan satu (atau lebih) metode di atas. |
|||
|
|||
--- |
|||
|
|||
Ketika membuat API, anda umumnya menggunakan metode HTTP tertentu untuk proses tertentu. |
|||
|
|||
Umumnya menggunakan: |
|||
|
|||
* `POST`: untuk membuat data. |
|||
* `GET`: untuk membaca data. |
|||
* `PUT`: untuk memperbarui data. |
|||
* `DELETE`: untuk menghapus data. |
|||
|
|||
Sehingga, di OpanAPI, setiap metode HTTP ini disebut sebuah "operasi". |
|||
|
|||
Kita akan menyebut mereka juga "**operasi**". |
|||
|
|||
#### Mendefinisikan *dekorator operasi path* |
|||
|
|||
{* ../../docs_src/first_steps/tutorial001.py hl[6] *} |
|||
|
|||
`@app.get("/")` memberitahu **FastAPI** bahwa fungsi di bawahnya mengurusi request yang menuju ke: |
|||
|
|||
* path `/` |
|||
* menggunakan <abbr title="an HTTP GET method">operasi <code>get</code></abbr> |
|||
|
|||
/// info | `@decorator` Info |
|||
|
|||
Sintaksis `@sesuatu` di Python disebut "dekorator". |
|||
|
|||
Dekorator ditempatkan di atas fungsi. Seperti sebuah topi cantik (Saya pikir istilah ini berasal dari situ). |
|||
|
|||
"dekorator" memanggil dan bekerja dengan fungsi yang ada di bawahnya |
|||
|
|||
Pada kondisi ini, dekorator ini memberi tahu **FastAPI** bahwa fungsi di bawah nya berhubungan dengan **path** `/` dengan **operasi** `get`. |
|||
|
|||
Sehingga disebut **dekorator operasi path**. |
|||
|
|||
/// |
|||
|
|||
Operasi lainnya yang bisa digunakan: |
|||
|
|||
* `@app.post()` |
|||
* `@app.put()` |
|||
* `@app.delete()` |
|||
|
|||
Dan operasi unik lainnya: |
|||
|
|||
* `@app.options()` |
|||
* `@app.head()` |
|||
* `@app.patch()` |
|||
* `@app.trace()` |
|||
|
|||
/// tip | Tips |
|||
|
|||
Jika anda bisa menggunakan operasi apa saja (metode HTTP). |
|||
|
|||
**FastAPI** tidak mengharuskan anda menggunakan operasi tertentu. |
|||
|
|||
Informasi di sini hanyalah sebagai panduan, bukan keharusan. |
|||
|
|||
Sebagai contoh, ketika menggunakan GraphQL, semua operasi umumnya hanya menggunakan `POST`. |
|||
|
|||
/// |
|||
|
|||
### Langkah 4: mendefinisikan **fungsi operasi path** |
|||
|
|||
Ini "**fungsi operasi path**" kita: |
|||
|
|||
* **path**: adalah `/`. |
|||
* **operasi**: adalah `get`. |
|||
* **fungsi**: adalah fungsi yang ada di bawah dekorator (di bawah `@app.get("/")`). |
|||
|
|||
{* ../../docs_src/first_steps/tutorial001.py hl[7] *} |
|||
|
|||
Ini adalah fungsi Python. |
|||
|
|||
Fungsi ini dipanggil **FastAPI** setiap kali menerima request ke URL "`/`" dengan operasi `GET`. |
|||
|
|||
Di kondisi ini, ini adalah sebuah fungsi `async`. |
|||
|
|||
--- |
|||
|
|||
Anda bisa mendefinisikan fungsi ini sebagai fungsi normal daripada `async def`: |
|||
|
|||
{* ../../docs_src/first_steps/tutorial003.py hl[7] *} |
|||
|
|||
/// note | Catatan |
|||
|
|||
Jika anda tidak tahu perbedaannya, kunjungi [Async: *"Panduan cepat"*](../async.md#in-a-hurry){.internal-link target=_blank}. |
|||
|
|||
/// |
|||
|
|||
### Langkah 5: hasilkan konten |
|||
|
|||
{* ../../docs_src/first_steps/tutorial001.py hl[8] *} |
|||
|
|||
Anda bisa menghasilkan `dict`, `list`, nilai singular seperti `str`, `int`, dll. |
|||
|
|||
Anda juga bisa menghasilkan model Pydantic (anda akan belajar mengenai ini nanti). |
|||
|
|||
Ada banyak objek dan model yang secara otomatis dikonversi ke JSON (termasuk ORM, dll). Anda bisa menggunakan yang anda suka, kemungkinan sudah didukung. |
|||
|
|||
## Ringkasan |
|||
|
|||
* Impor `FastAPI`. |
|||
* Buat sebuah instance `app`. |
|||
* Tulis **dekorator operasi path** menggunakan dekorator seperti `@app.get("/")`. |
|||
* Definisikan **fungsi operasi path**; sebagai contoh, `def root(): ...`. |
|||
* Jalankan server development dengan perintah `fastapi dev`. |
@ -0,0 +1,41 @@ |
|||
# 추가 상태 코드 |
|||
|
|||
기본적으로 **FastAPI**는 응답을 `JSONResponse`를 사용하여 반환하며, *경로 작업(path operation)*에서 반환한 내용을 해당 `JSONResponse` 안에 넣어 반환합니다. |
|||
|
|||
기본 상태 코드 또는 *경로 작업*에서 설정한 상태 코드를 사용합니다. |
|||
|
|||
## 추가 상태 코드 |
|||
|
|||
기본 상태 코드와 별도로 추가 상태 코드를 반환하려면 `JSONResponse`와 같이 `Response`를 직접 반환하고 추가 상태 코드를 직접 설정할 수 있습니다. |
|||
|
|||
예를 들어 항목을 업데이트할 수 있는 *경로 작업*이 있고 성공 시 200 “OK”의 HTTP 상태 코드를 반환한다고 가정해 보겠습니다. |
|||
|
|||
하지만 새로운 항목을 허용하기를 원할 것입니다. 항목이 이전에 존재하지 않았다면 이를 생성하고 HTTP 상태 코드 201 "Created"를 반환합니다. |
|||
|
|||
이를 위해서는 `JSONResponse`를 가져와서 원하는 `status_code`를 설정하여 콘텐츠를 직접 반환합니다: |
|||
|
|||
{* ../../docs_src/additional_status_codes/tutorial001_an_py310.py hl[4,25] *} |
|||
|
|||
/// warning | 경고 |
|||
|
|||
위의 예제처럼 `Response`를 직접 반환하면 바로 반환됩니다. |
|||
|
|||
모델 등과 함께 직렬화되지 않습니다. |
|||
|
|||
원하는 데이터가 있는지, 값이 유효한 JSON인지 확인합니다(`JSONResponse`를 사용하는 경우). |
|||
|
|||
/// |
|||
|
|||
/// note | 기술적 세부 정보 |
|||
|
|||
`from starlette.responses import JSONResponse`를 사용할 수도 있습니다. |
|||
|
|||
**FastAPI**는 개발자 여러분을 위한 편의성으로 `fastapi.responses`와 동일한 `starlette.responses`를 제공합니다. 그러나 사용 가능한 응답의 대부분은 Starlette에서 직접 제공됩니다. `status` 또한 마찬가지입니다. |
|||
|
|||
/// |
|||
|
|||
## OpenAPI 및 API 문서 |
|||
|
|||
추가 상태 코드와 응답을 직접 반환하는 경우, FastAPI는 반환할 내용을 미리 알 수 있는 방법이 없기 때문에 OpenAPI 스키마(API 문서)에 포함되지 않습니다. |
|||
|
|||
하지만 다음을 사용하여 코드에 이를 문서화할 수 있습니다: [추가 응답](additional-responses.md){.internal-link target=_blank}. |
@ -0,0 +1,108 @@ |
|||
# 비동기 테스트 코드 작성 |
|||
|
|||
이전 장에서 `TestClient` 를 이용해 **FastAPI** 어플리케이션 테스트를 작성하는 법을 배우셨을텐데요. |
|||
지금까지는 `async` 키워드 사용없이 동기 함수의 테스트 코드를 작성하는 법만 익혔습니다. |
|||
|
|||
하지만 비동기 함수를 사용하여 테스트 코드를 작성하는 것은 매우 유용할 수 있습니다. |
|||
예를 들면 데이터베이스에 비동기로 쿼리하는 경우를 생각해봅시다. |
|||
FastAPI 애플리케이션에 요청을 보내고, 비동기 데이터베이스 라이브러리를 사용하여 백엔드가 데이터베이스에 올바르게 데이터를 기록했는지 확인하고 싶을 때가 있을 겁니다. |
|||
|
|||
이런 경우의 테스트 코드를 어떻게 비동기로 작성하는지 알아봅시다. |
|||
|
|||
## pytest.mark.anyio |
|||
|
|||
앞에서 작성한 테스트 함수에서 비동기 함수를 호출하고 싶다면, 테스트 코드도 비동기 함수여야합니다. |
|||
AnyIO는 특정 테스트 함수를 비동기 함수로 호출 할 수 있는 깔끔한 플러그인을 제공합니다. |
|||
|
|||
|
|||
## HTTPX |
|||
|
|||
**FastAPI** 애플리케이션이 `async def` 대신 `def` 키워드로 선언된 함수를 사용하더라도, 내부적으로는 여전히 `비동기` 애플리케이션입니다. |
|||
|
|||
`TestClient`는 pytest 표준을 사용하여 비동기 FastAPI 애플리케이션을 일반적인 `def` 테스트 함수 내에서 호출할 수 있도록 내부에서 마술을 부립니다. 하지만 이 마술은 비동기 함수 내부에서 사용할 때는 더 이상 작동하지 않습니다. 테스트를 비동기로 실행하면, 더 이상 테스트 함수 내부에서 `TestClient`를 사용할 수 없습니다. |
|||
|
|||
`TestClient`는 <a href="https://www.python-httpx.org" class="external-link" target="_blank">HTTPX</a>를 기반으로 하고 있으며, 다행히 이를 직접 사용하여 API를 테스트할 수 있습니다. |
|||
|
|||
## 예시 |
|||
|
|||
간단한 예시를 위해 [더 큰 어플리케이션 만들기](../ko/tutorial/bigger-applications.md){.internal-link target=_blank} 와 [테스트](../ko/tutorial/testing.md){.internal-link target=_blank}:에서 다룬 파일 구조와 비슷한 형태를 확인해봅시다: |
|||
|
|||
``` |
|||
. |
|||
├── app |
|||
│ ├── __init__.py |
|||
│ ├── main.py |
|||
│ └── test_main.py |
|||
``` |
|||
|
|||
`main.py`는 아래와 같아야 합니다: |
|||
|
|||
{* ../../docs_src/async_tests/main.py *} |
|||
|
|||
`test_main.py` 파일은 `main.py`에 대한 테스트가 있을 텐데, 다음과 같을 수 있습니다: |
|||
|
|||
{* ../../docs_src/async_tests/test_main.py *} |
|||
|
|||
## 실행하기 |
|||
|
|||
아래의 명령어로 테스트 코드를 실행합니다: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ pytest |
|||
|
|||
---> 100% |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
## 자세히 보기 |
|||
|
|||
`@pytest.mark.anyio` 마커는 pytest에게 이 테스트 함수가 비동기로 호출되어야 함을 알려줍니다: |
|||
|
|||
{* ../../docs_src/async_tests/test_main.py hl[7] *} |
|||
|
|||
/// tip | 팁 |
|||
|
|||
테스트 함수가 이제 `TestClient`를 사용할 때처럼 단순히 `def`가 아니라 `async def`로 작성된 점에 주목해주세요. |
|||
|
|||
/// |
|||
|
|||
그 다음에 `AsyncClient` 로 앱을 만들고 비동기 요청을 `await` 키워드로 보낼 수 있습니다: |
|||
|
|||
{* ../../docs_src/async_tests/test_main.py hl[9:12] *} |
|||
|
|||
위의 코드는: |
|||
|
|||
```Python |
|||
response = client.get('/') |
|||
``` |
|||
|
|||
`TestClient` 에 요청을 보내던 것과 동일합니다. |
|||
|
|||
/// tip | 팁 |
|||
|
|||
새로운 `AsyncClient`를 사용할 때 async/await를 사용하고 있다는 점에 주목하세요. 이 요청은 비동기적으로 처리됩니다. |
|||
|
|||
/// |
|||
|
|||
/// warning | 경고 |
|||
|
|||
만약의 어플리케이션이 Lifespan 이벤트에 의존성을 갖고 있다면 `AsyncClient` 가 이러한 이벤트를 실행시키지 않습니다. |
|||
`AsyncClient` 가 테스트를 실행시켰다는 것을 확인하기 위해 |
|||
`LifespanManager` from <a href="https://github.com/florimondmanca/asgi-lifespan#usage" class="external-link" target="_blank">florimondmanca/asgi-lifespan</a>.확인해주세요. |
|||
|
|||
|
|||
/// |
|||
|
|||
## 그 외의 비동기 함수 호출 |
|||
|
|||
테스트 함수가 이제 비동기 함수이므로, FastAPI 애플리케이션에 요청을 보내는 것 외에도 다른 `async` 함수를 호출하고 `await` 키워드를 사용 할 수 있습니다. |
|||
|
|||
/// tip | 팁 |
|||
|
|||
테스트에 비동기 함수 호출을 통합할 때 (예: <a href="https://stackoverflow.com/questions/41584243/runtimeerror-task-attached-to-a-different-loop" class="external-link" target="_blank">MongoDB의 MotorClient</a>를 사용할 때) `RuntimeError: Task attached to a different loop` 오류가 발생한다면, 이벤트 루프가 필요한 객체는 반드시 비동기 함수 내에서만 인스턴스화해야 한다는 점을 주의하세요! |
|||
예를 들어 `@app.on_event("startup")` 콜백 내에서 인스턴스화하는 것이 좋습니다. |
|||
|
|||
/// |
@ -0,0 +1,96 @@ |
|||
# 고급 미들웨어 |
|||
|
|||
메인 튜토리얼에서 [Custom Middleware](../tutorial/middleware.md){.internal-link target=_blank}를 응용프로그램에 추가하는 방법을 읽으셨습니다. |
|||
|
|||
그리고 [CORS with the `CORSMiddleware`](){.internal-link target=_blank}하는 방법도 보셨습니다. |
|||
|
|||
이 섹션에서는 다른 미들웨어들을 사용하는 방법을 알아보겠습니다. |
|||
|
|||
## ASGI 미들웨어 추가하기 |
|||
|
|||
**FastAPI**는 Starlette을 기반으로 하고 있으며, <abbr title="Asynchronous Server Gateway Interface">ASGI</abbr> 사양을 구현하므로 ASGI 미들웨어를 사용할 수 있습니다. |
|||
|
|||
미들웨어가 FastAPI나 Starlette용으로 만들어지지 않아도 ASGI 사양을 준수하는 한 동작할 수 있습니다. |
|||
|
|||
일반적으로 ASGI 미들웨어는 첫 번째 인수로 ASGI 앱을 받는 클래스들입니다. |
|||
|
|||
따라서 타사 ASGI 미들웨어 문서에서 일반적으로 다음과 같이 사용하도록 안내할 것입니다. |
|||
|
|||
```Python |
|||
from unicorn import UnicornMiddleware |
|||
|
|||
app = SomeASGIApp() |
|||
|
|||
new_app = UnicornMiddleware(app, some_config="rainbow") |
|||
``` |
|||
|
|||
하지만 내부 미들웨어가 서버 오류를 처리하고 사용자 정의 예외 처리기가 제대로 작동하도록 하는 더 간단한 방법을 제공하는 FastAPI(실제로는 Starlette)가 있습니다. |
|||
|
|||
이를 위해 `app.add_middleware()`를 사용합니다(CORS의 예에서와 같이). |
|||
|
|||
```Python |
|||
from fastapi import FastAPI |
|||
from unicorn import UnicornMiddleware |
|||
|
|||
app = FastAPI() |
|||
|
|||
app.add_middleware(UnicornMiddleware, some_config="rainbow") |
|||
``` |
|||
|
|||
`app.add_middleware()`는 첫 번째 인수로 미들웨어 클래스와 미들웨어에 전달할 추가 인수를 받습니다. |
|||
|
|||
## 통합 미들웨어 |
|||
|
|||
**FastAPI**에는 일반적인 사용 사례를 위한 여러 미들웨어가 포함되어 있으며, 사용 방법은 다음에서 살펴보겠습니다. |
|||
|
|||
/// note | 기술 세부 사항 |
|||
|
|||
다음 예제에서는 `from starlette.middleware.something import SomethingMiddleware`를 사용할 수도 있습니다. |
|||
|
|||
**FastAPI**는 개발자의 편의를 위해 `fastapi.middleware`에 여러 미들웨어를 제공합니다. 그러나 사용 가능한 대부분의 미들웨어는 Starlette에서 직접 제공합니다. |
|||
|
|||
/// |
|||
|
|||
## `HTTPSRedirectMiddleware` |
|||
|
|||
들어오는 모든 요청이 `https` 또는 `wss`여야 합니다. |
|||
|
|||
`http` 또는 `ws`로 들어오는 모든 요청은 대신 보안 체계로 리디렉션됩니다. |
|||
|
|||
{* ../../docs_src/advanced_middleware/tutorial001.py hl[2,6] *} |
|||
|
|||
## `TrustedHostMiddleware` |
|||
|
|||
HTTP 호스트 헤더 공격을 방지하기 위해 모든 수신 요청에 올바르게 설정된 `Host` 헤더를 갖도록 강제합니다. |
|||
|
|||
{* ../../docs_src/advanced_middleware/tutorial002.py hl[2,6:8] *} |
|||
|
|||
다음 인수가 지원됩니다: |
|||
|
|||
* `allowed_hosts` - 호스트 이름으로 허용해야 하는 도메인 이름 목록입니다. 일치하는 하위 도메인에 대해 `*.example.com`과 같은 와일드카드 도메인이 지원됩니다. 모든 호스트 이름을 허용하려면 `allowed_hosts=[“*”]`를 사용하거나 미들웨어를 생략하세요. |
|||
|
|||
수신 요청의 유효성이 올바르게 확인되지 않으면 `400`이라는 응답이 전송됩니다. |
|||
|
|||
## `GZipMiddleware` |
|||
|
|||
`Accept-Encoding` 헤더에 `“gzip”`이 포함된 모든 요청에 대해 GZip 응답을 처리합니다. |
|||
|
|||
미들웨어는 표준 응답과 스트리밍 응답을 모두 처리합니다. |
|||
|
|||
{* ../../docs_src/advanced_middleware/tutorial003.py hl[2,6] *} |
|||
|
|||
지원되는 인수는 다음과 같습니다: |
|||
|
|||
* `minimum_size` - 이 최소 크기(바이트)보다 작은 응답은 GZip하지 않습니다. 기본값은 `500`입니다. |
|||
* `compresslevel` - GZip 압축 중에 사용됩니다. 1에서 9 사이의 정수입니다. 기본값은 `9`입니다. 값이 낮을수록 압축 속도는 빨라지지만 파일 크기는 커지고, 값이 높을수록 압축 속도는 느려지지만 파일 크기는 작아집니다. |
|||
|
|||
## 기타 미들웨어 |
|||
|
|||
다른 많은 ASGI 미들웨어가 있습니다. |
|||
|
|||
예를 들어: |
|||
|
|||
<a href=“https://github.com/encode/uvicorn/blob/master/uvicorn/middleware/proxy_headers.py” class=“external-link” target=“_blank”>유비콘의 `ProxyHeadersMiddleware`></a> |
|||
<a href=“https://github.com/florimondmanca/msgpack-asgi” class=“external-link” target=“_blank”>MessagePack</a> |
|||
|
|||
사용 가능한 다른 미들웨어를 확인하려면 <a href=“https://www.starlette.io/middleware/” class=“external-link” target=“_blank”>스타렛의 미들웨어 문서</a> 및 <a href=“https://github.com/florimondmanca/awesome-asgi” class=“external-link” target=“_blank”>ASGI Awesome List</a>를 참조하세요. |
@ -0,0 +1,127 @@ |
|||
# 템플릿 |
|||
|
|||
**FastAPI**와 함께 원하는 어떤 템플릿 엔진도 사용할 수 있습니다. |
|||
|
|||
일반적인 선택은 Jinja2로, Flask와 다른 도구에서도 사용됩니다. |
|||
|
|||
설정을 쉽게 할 수 있는 유틸리티가 있으며, 이를 **FastAPI** 애플리케이션에서 직접 사용할 수 있습니다(Starlette 제공). |
|||
|
|||
## 의존성 설치 |
|||
|
|||
가상 환경을 생성하고(virtual environment{.internal-link target=_blank}), 활성화한 후 jinja2를 설치해야 합니다: |
|||
|
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ pip install jinja2 |
|||
|
|||
---> 100% |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
## 사용하기 `Jinja2Templates` |
|||
|
|||
* `Jinja2Templates`를 가져옵니다. |
|||
* 나중에 재사용할 수 있는 `templates` 객체를 생성합니다. |
|||
* 템플릿을 반환할 경로 작업에 `Request` 매개변수를 선언합니다. |
|||
* 생성한 `templates`를 사용하여 `TemplateResponse`를 렌더링하고 반환합니다. 템플릿의 이름, 요청 객체 및 Jinja2 템플릿 내에서 사용될 키-값 쌍이 포함된 "컨텍스트" 딕셔너리도 전달합니다. |
|||
|
|||
|
|||
```Python hl_lines="4 11 15-18" |
|||
{!../../docs_src/templates/tutorial001.py!} |
|||
``` |
|||
|
|||
/// note | 참고 |
|||
|
|||
FastAPI 0.108.0 이전과 Starlette 0.29.0에서는 `name`이 첫 번째 매개변수였습니다. |
|||
|
|||
또한 이전 버전에서는 `request` 객체가 Jinja2의 컨텍스트에서 키-값 쌍의 일부로 전달되었습니다. |
|||
|
|||
/// |
|||
|
|||
/// tip | 팁 |
|||
|
|||
`response_class=HTMLResponse`를 선언하면 문서 UI 응답이 HTML임을 알 수 있습니다. |
|||
|
|||
/// |
|||
|
|||
/// note | 기술 세부 사항 |
|||
`from starlette.templating import Jinja2Templates`를 사용할 수도 있습니다. |
|||
|
|||
**FastAPI**는 개발자를 위한 편리함으로 `fastapi.templating` 대신 `starlette.templating`을 제공합니다. 하지만 대부분의 사용 가능한 응답은 Starlette에서 직접 옵니다. `Request` 및 `StaticFiles`도 마찬가지입니다. |
|||
/// |
|||
|
|||
## 템플릿 작성하기 |
|||
|
|||
그런 다음 `templates/item.html`에 템플릿을 작성할 수 있습니다. 예를 들면: |
|||
|
|||
```jinja hl_lines="7" |
|||
{!../../docs_src/templates/templates/item.html!} |
|||
``` |
|||
|
|||
### 템플릿 컨텍스트 값 |
|||
|
|||
다음과 같은 HTML에서: |
|||
|
|||
{% raw %} |
|||
|
|||
```jinja |
|||
Item ID: {{ id }} |
|||
``` |
|||
|
|||
{% endraw %} |
|||
|
|||
...이는 전달한 "컨텍스트" `dict`에서 가져온 `id`를 표시합니다: |
|||
|
|||
```Python |
|||
{"id": id} |
|||
``` |
|||
|
|||
예를 들어, ID가 `42`일 경우, 이는 다음과 같이 렌더링됩니다: |
|||
|
|||
```html |
|||
Item ID: 42 |
|||
``` |
|||
|
|||
### 템플릿 `url_for` 인수 |
|||
|
|||
템플릿 내에서 `url_for()`를 사용할 수도 있으며, 이는 *경로 작업 함수*에서 사용될 인수와 동일한 인수를 받습니다. |
|||
|
|||
따라서 다음과 같은 부분에서: |
|||
|
|||
{% raw %} |
|||
|
|||
```jinja |
|||
<a href="{{ url_for('read_item', id=id) }}"> |
|||
``` |
|||
|
|||
{% endraw %} |
|||
|
|||
...이는 *경로 작업 함수* `read_item(id=id)`가 처리할 동일한 URL로 링크를 생성합니다. |
|||
|
|||
예를 들어, ID가 `42`일 경우, 이는 다음과 같이 렌더링됩니다: |
|||
```html |
|||
<a href="/items/42"> |
|||
``` |
|||
|
|||
## 템플릿과 정적 파일 |
|||
|
|||
템플릿 내에서 `url_for()`를 사용할 수 있으며, 예를 들어 `name="static"`으로 마운트한 `StaticFiles`와 함께 사용할 수 있습니다. |
|||
|
|||
```jinja hl_lines="4" |
|||
{!../../docs_src/templates/templates/item.html!} |
|||
``` |
|||
|
|||
이 예제에서는 `static/styles.css`에 있는 CSS 파일에 연결될 것입니다: |
|||
|
|||
```CSS hl_lines="4" |
|||
{!../../docs_src/templates/static/styles.css!} |
|||
``` |
|||
|
|||
그리고 `StaticFiles`를 사용하고 있으므로, 해당 CSS 파일은 **FastAPI** 애플리케이션에서 `/static/styles.css` URL로 자동 제공됩니다. |
|||
|
|||
## 더 많은 세부 사항 |
|||
|
|||
템플릿 테스트를 포함한 더 많은 세부 사항은 <a href="https://www.starlette.io/templates/" class="external-link" target="_blank">Starlette의 템플릿 문서</a>를 확인하세요. |
@ -0,0 +1,53 @@ |
|||
# 테스트 의존성 오버라이드 |
|||
|
|||
## 테스트 중 의존성 오버라이드하기 |
|||
|
|||
테스트를 진행하다 보면 의존성을 오버라이드해야 하는 경우가 있습니다. |
|||
|
|||
원래 의존성을 실행하고 싶지 않을 수도 있습니다(또는 그 의존성이 가지고 있는 하위 의존성까지도 실행되지 않길 원할 수 있습니다). |
|||
|
|||
대신, 테스트 동안(특정 테스트에서만) 사용될 다른 의존성을 제공하고, 원래 의존성이 사용되던 곳에서 사용할 수 있는 값을 제공하기를 원할 수 있습니다. |
|||
|
|||
### 사용 사례: 외부 서비스 |
|||
|
|||
예를 들어, 외부 인증 제공자를 호출해야 하는 경우를 생각해봅시다. |
|||
|
|||
토큰을 보내면 인증된 사용자를 반환합니다. |
|||
|
|||
제공자는 요청당 요금을 부과할 수 있으며, 테스트를 위해 고정된 모의 사용자가 있는 경우보다 호출하는 데 시간이 더 걸릴 수 있습니다. |
|||
|
|||
외부 제공자를 한 번만 테스트하고 싶을 수도 있지만 테스트를 실행할 때마다 반드시 호출할 필요는 없습니다. |
|||
|
|||
이 경우 해당 공급자를 호출하는 종속성을 오버라이드하고 테스트에 대해서만 모의 사용자를 반환하는 사용자 지정 종속성을 사용할 수 있습니다. |
|||
|
|||
### `app.dependency_overrides` 속성 사용하기 |
|||
|
|||
이런 경우를 위해 **FastAPI** 응용 프로그램에는 `app.dependency_overrides`라는 속성이 있습니다. 이는 간단한 `dict`입니다. |
|||
|
|||
테스트를 위해 의존성을 오버라이드하려면, 원래 의존성(함수)을 키로 설정하고 오버라이드할 의존성(다른 함수)을 값으로 설정합니다. |
|||
|
|||
그럼 **FastAPI**는 원래 의존성 대신 오버라이드된 의존성을 호출합니다. |
|||
|
|||
{* ../../docs_src/dependency_testing/tutorial001_an_py310.py hl[26:27,30] *} |
|||
|
|||
/// tip | 팁 |
|||
|
|||
**FastAPI** 애플리케이션 어디에서든 사용된 의존성에 대해 오버라이드를 설정할 수 있습니다. |
|||
|
|||
원래 의존성은 *경로 동작 함수*, *경로 동작 데코레이터*(반환값을 사용하지 않는 경우), `.include_router()` 호출 등에서 사용될 수 있습니다. |
|||
|
|||
FastAPI는 여전히 이를 오버라이드할 수 있습니다. |
|||
|
|||
/// |
|||
|
|||
그런 다음, `app.dependency_overrides`를 빈 `dict`로 설정하여 오버라이드를 재설정(제거)할 수 있습니다: |
|||
|
|||
```python |
|||
app.dependency_overrides = {} |
|||
``` |
|||
|
|||
/// tip | 팁 |
|||
|
|||
특정 테스트에서만 의존성을 오버라이드하고 싶다면, 테스트 시작 시(테스트 함수 내부) 오버라이드를 설정하고 테스트 종료 시(테스트 함수 끝부분) 재설정하면 됩니다. |
|||
|
|||
/// |
@ -0,0 +1,186 @@ |
|||
# WebSockets |
|||
|
|||
여러분은 **FastAPI**에서 <a href="https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API" class="external-link" target="_blank">WebSockets</a>를 사용할 수 있습니다. |
|||
|
|||
## `WebSockets` 설치 |
|||
|
|||
[가상 환경](../virtual-environments.md){.internal-link target=_blank)를 생성하고 활성화한 다음, `websockets`를 설치하세요: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ pip install websockets |
|||
|
|||
---> 100% |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
## WebSockets 클라이언트 |
|||
|
|||
### 프로덕션 환경에서 |
|||
|
|||
여러분의 프로덕션 시스템에서는 React, Vue.js 또는 Angular와 같은 최신 프레임워크로 생성된 프런트엔드를 사용하고 있을 가능성이 높습니다. |
|||
|
|||
백엔드와 WebSockets을 사용해 통신하려면 아마도 프런트엔드의 유틸리티를 사용할 것입니다. |
|||
|
|||
또는 네이티브 코드로 WebSocket 백엔드와 직접 통신하는 네이티브 모바일 응용 프로그램을 가질 수도 있습니다. |
|||
|
|||
혹은 WebSocket 엔드포인트와 통신할 수 있는 다른 방법이 있을 수도 있습니다. |
|||
|
|||
--- |
|||
|
|||
하지만 이번 예제에서는 일부 자바스크립트를 포함한 간단한 HTML 문서를 사용하겠습니다. 모든 것을 긴 문자열 안에 넣습니다. |
|||
|
|||
물론, 이는 최적의 방법이 아니며 프로덕션 환경에서는 사용하지 않을 것입니다. |
|||
|
|||
프로덕션 환경에서는 위에서 설명한 옵션 중 하나를 사용하는 것이 좋습니다. |
|||
|
|||
그러나 이는 WebSockets의 서버 측에 집중하고 동작하는 예제를 제공하는 가장 간단한 방법입니다: |
|||
|
|||
{* ../../docs_src/websockets/tutorial001.py hl[2,6:38,41:43] *} |
|||
|
|||
## `websocket` 생성하기 |
|||
|
|||
**FastAPI** 응용 프로그램에서 `websocket`을 생성합니다: |
|||
|
|||
{* ../../docs_src/websockets/tutorial001.py hl[1,46:47] *} |
|||
|
|||
/// note | 기술적 세부사항 |
|||
|
|||
`from starlette.websockets import WebSocket`을 사용할 수도 있습니다. |
|||
|
|||
**FastAPI**는 개발자를 위한 편의를 위해 동일한 `WebSocket`을 직접 제공합니다. 하지만 이는 Starlette에서 가져옵니다. |
|||
|
|||
/// |
|||
|
|||
## 메시지를 대기하고 전송하기 |
|||
|
|||
WebSocket 경로에서 메시지를 대기(`await`)하고 전송할 수 있습니다. |
|||
|
|||
{* ../../docs_src/websockets/tutorial001.py hl[48:52] *} |
|||
|
|||
여러분은 이진 데이터, 텍스트, JSON 데이터를 받을 수 있고 전송할 수 있습니다. |
|||
|
|||
## 시도해보기 |
|||
|
|||
파일 이름이 `main.py`라고 가정하고 응용 프로그램을 실행합니다: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ fastapi dev main.py |
|||
|
|||
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
브라우저에서 <a href="http://127.0.0.1:8000" class="external-link" target="_blank">http://127.0.0.1:8000</a>을 열어보세요. |
|||
|
|||
간단한 페이지가 나타날 것입니다: |
|||
|
|||
<img src="/img/tutorial/websockets/image01.png"> |
|||
|
|||
입력창에 메시지를 입력하고 전송할 수 있습니다: |
|||
|
|||
<img src="/img/tutorial/websockets/image02.png"> |
|||
|
|||
**FastAPI** WebSocket 응용 프로그램이 응답을 돌려줄 것입니다: |
|||
|
|||
<img src="/img/tutorial/websockets/image03.png"> |
|||
|
|||
여러 메시지를 전송(그리고 수신)할 수 있습니다: |
|||
|
|||
<img src="/img/tutorial/websockets/image04.png"> |
|||
|
|||
모든 메시지는 동일한 WebSocket 연결을 사용합니다. |
|||
|
|||
## `Depends` 및 기타 사용하기 |
|||
|
|||
WebSocket 엔드포인트에서 `fastapi`에서 다음을 가져와 사용할 수 있습니다: |
|||
|
|||
* `Depends` |
|||
* `Security` |
|||
* `Cookie` |
|||
* `Header` |
|||
* `Path` |
|||
* `Query` |
|||
|
|||
이들은 다른 FastAPI 엔드포인트/*경로 작동*과 동일하게 동작합니다: |
|||
|
|||
{* ../../docs_src/websockets/tutorial002_an_py310.py hl[68:69,82] *} |
|||
|
|||
/// info | 정보 |
|||
|
|||
WebSocket에서는 `HTTPException`을 발생시키는 것이 적합하지 않습니다. 대신 `WebSocketException`을 발생시킵니다. |
|||
|
|||
명세서에 정의된 <a href="https://tools.ietf.org/html/rfc6455#section-7.4.1" class="external-link" target="_blank">유효한 코드</a>를 사용하여 종료 코드를 설정할 수 있습니다. |
|||
|
|||
/// |
|||
|
|||
### 종속성을 가진 WebSockets 테스트 |
|||
|
|||
파일 이름이 `main.py`라고 가정하고 응용 프로그램을 실행합니다: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ fastapi dev main.py |
|||
|
|||
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
브라우저에서 <a href="http://127.0.0.1:8000" class="external-link" target="_blank">http://127.0.0.1:8000</a>을 열어보세요. |
|||
|
|||
다음과 같은 값을 설정할 수 있습니다: |
|||
|
|||
* 경로에 사용된 "Item ID". |
|||
* 쿼리 매개변수로 사용된 "Token". |
|||
|
|||
/// tip | 팁 |
|||
|
|||
쿼리 `token`은 종속성에 의해 처리됩니다. |
|||
|
|||
/// |
|||
|
|||
이제 WebSocket에 연결하고 메시지를 전송 및 수신할 수 있습니다: |
|||
|
|||
<img src="/img/tutorial/websockets/image05.png"> |
|||
|
|||
## 연결 해제 및 다중 클라이언트 처리 |
|||
|
|||
WebSocket 연결이 닫히면, `await websocket.receive_text()`가 `WebSocketDisconnect` 예외를 발생시킵니다. 이를 잡아 처리할 수 있습니다: |
|||
|
|||
{* ../../docs_src/websockets/tutorial003_py39.py hl[79:81] *} |
|||
|
|||
테스트해보기: |
|||
|
|||
* 여러 브라우저 탭에서 앱을 엽니다. |
|||
* 각 탭에서 메시지를 작성합니다. |
|||
* 한 탭을 닫아보세요. |
|||
|
|||
`WebSocketDisconnect` 예외가 발생하며, 다른 모든 클라이언트가 다음과 같은 메시지를 수신합니다: |
|||
|
|||
``` |
|||
Client #1596980209979 left the chat |
|||
``` |
|||
|
|||
/// tip | 팁 |
|||
|
|||
위 응용 프로그램은 여러 WebSocket 연결에 메시지를 브로드캐스트하는 방법을 보여주는 간단한 예제입니다. |
|||
|
|||
그러나 모든 것을 메모리의 단일 리스트로 처리하므로, 프로세스가 실행 중인 동안만 동작하며 단일 프로세스에서만 작동합니다. |
|||
|
|||
FastAPI와 쉽게 통합할 수 있으면서 더 견고하고 Redis, PostgreSQL 등을 지원하는 도구를 찾고 있다면, <a href="https://github.com/encode/broadcaster" class="external-link" target="_blank">encode/broadcaster</a>를 확인하세요. |
|||
|
|||
/// |
|||
|
|||
## 추가 정보 |
|||
|
|||
다음 옵션에 대한 자세한 내용을 보려면 Starlette의 문서를 확인하세요: |
|||
|
|||
* <a href="https://www.starlette.io/websockets/" class="external-link" target="_blank">`WebSocket` 클래스</a>. |
|||
* <a href="https://www.starlette.io/endpoints/#websocketendpoint" class="external-link" target="_blank">클래스 기반 WebSocket 처리</a>. |
@ -0,0 +1,70 @@ |
|||
# Swagger UI 구성 |
|||
|
|||
추가적인 <a href="https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration/" class="external-link" target="_blank">Swagger UI 매개변수</a>를 구성할 수 있습니다. |
|||
|
|||
구성을 하려면, `FastAPI()` 앱 객체를 생성할 때 또는 `get_swagger_ui_html()` 함수에 `swagger_ui_parameters` 인수를 전달하십시오. |
|||
|
|||
`swagger_ui_parameters`는 Swagger UI에 직접 전달된 구성을 포함하는 딕셔너리를 받습니다. |
|||
|
|||
FastAPI는 이 구성을 **JSON** 형식으로 변환하여 JavaScript와 호환되도록 합니다. 이는 Swagger UI에서 필요로 하는 형식입니다. |
|||
|
|||
## 구문 강조 비활성화 |
|||
|
|||
예를 들어, Swagger UI에서 구문 강조 기능을 비활성화할 수 있습니다. |
|||
|
|||
설정을 변경하지 않으면, 기본적으로 구문 강조 기능이 활성화되어 있습니다: |
|||
|
|||
<img src="/img/tutorial/extending-openapi/image02.png"> |
|||
|
|||
그러나 `syntaxHighlight`를 `False`로 설정하여 구문 강조 기능을 비활성화할 수 있습니다: |
|||
|
|||
{* ../../docs_src/configure_swagger_ui/tutorial001.py hl[3] *} |
|||
|
|||
...그럼 Swagger UI에서 더 이상 구문 강조 기능이 표시되지 않습니다: |
|||
|
|||
<img src="/img/tutorial/extending-openapi/image03.png"> |
|||
|
|||
## 테마 변경 |
|||
|
|||
동일한 방식으로 `"syntaxHighlight.theme"` 키를 사용하여 구문 강조 테마를 설정할 수 있습니다 (중간에 점이 포함된 것을 참고하십시오). |
|||
|
|||
{* ../../docs_src/configure_swagger_ui/tutorial002.py hl[3] *} |
|||
|
|||
이 설정은 구문 강조 색상 테마를 변경합니다: |
|||
|
|||
<img src="/img/tutorial/extending-openapi/image04.png"> |
|||
|
|||
## 기본 Swagger UI 매개변수 변경 |
|||
|
|||
FastAPI는 대부분의 사용 사례에 적합한 몇 가지 기본 구성 매개변수를 포함하고 있습니다. |
|||
|
|||
기본 구성에는 다음이 포함됩니다: |
|||
|
|||
{* ../../fastapi/openapi/docs.py ln[8:23] hl[17:23] *} |
|||
|
|||
`swagger_ui_parameters` 인수에 다른 값을 설정하여 이러한 기본값 중 일부를 재정의할 수 있습니다. |
|||
|
|||
예를 들어, `deepLinking`을 비활성화하려면 `swagger_ui_parameters`에 다음 설정을 전달할 수 있습니다: |
|||
|
|||
{* ../../docs_src/configure_swagger_ui/tutorial003.py hl[3] *} |
|||
|
|||
## 기타 Swagger UI 매개변수 |
|||
|
|||
사용할 수 있는 다른 모든 구성 옵션을 확인하려면, Swagger UI 매개변수에 대한 공식 <a href="https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration/" class="external-link" target="_blank">문서</a>를 참조하십시오. |
|||
|
|||
## JavaScript 전용 설정 |
|||
|
|||
Swagger UI는 **JavaScript 전용** 객체(예: JavaScript 함수)로 다른 구성을 허용하기도 합니다. |
|||
|
|||
FastAPI는 이러한 JavaScript 전용 `presets` 설정을 포함하고 있습니다: |
|||
|
|||
```JavaScript |
|||
presets: [ |
|||
SwaggerUIBundle.presets.apis, |
|||
SwaggerUIBundle.SwaggerUIStandalonePreset |
|||
] |
|||
``` |
|||
|
|||
이들은 문자열이 아닌 **JavaScript** 객체이므로 Python 코드에서 직접 전달할 수 없습니다. |
|||
|
|||
이와 같은 JavaScript 전용 구성을 사용해야 하는 경우, 위의 방법 중 하나를 사용하여 모든 Swagger UI 경로 작업을 재정의하고 필요한 JavaScript를 수동으로 작성할 수 있습니다. |
@ -0,0 +1,55 @@ |
|||
# OpenAPI 웹훅(Webhooks) |
|||
|
|||
API **사용자**에게 특정 **이벤트**가 발생할 때 *그들*의 앱(시스템)에 요청을 보내 **알림**을 전달할 수 있다는 것을 알리고 싶은 경우가 있습니다. |
|||
|
|||
즉, 일반적으로 사용자가 API에 요청을 보내는 것과는 반대로, **API**(또는 앱)가 **사용자의 시스템**(그들의 API나 앱)으로 **요청을 보내는** 상황을 의미합니다. |
|||
|
|||
이를 흔히 **웹훅(Webhook)**이라고 부릅니다. |
|||
|
|||
## 웹훅 스텝 |
|||
|
|||
**코드에서** 웹훅으로 보낼 메시지, 즉 요청의 **바디(body)**를 정의하는 것이 일반적인 프로세스입니다. |
|||
|
|||
앱에서 해당 요청이나 이벤트를 전송할 **시점**을 정의합니다. |
|||
|
|||
**사용자**는 앱이 해당 요청을 보낼 **URL**을 정의합니다. (예: 웹 대시보드에서 설정) |
|||
|
|||
웹훅의 URL을 등록하는 방법과 이러한 요청을 실제로 전송하는 코드에 대한 모든 로직은 여러분에게 달려 있습니다. 원하는대로 **고유의 코드**를 작성하면 됩니다. |
|||
|
|||
## **FastAPI**와 OpenAPI로 웹훅 문서화하기 |
|||
|
|||
**FastAPI**를 사용하여 OpenAPI와 함께 웹훅의 이름, 앱이 보낼 수 있는 HTTP 작업 유형(예: `POST`, `PUT` 등), 그리고 보낼 요청의 **바디**를 정의할 수 있습니다. |
|||
|
|||
이를 통해 사용자가 **웹훅** 요청을 수신할 **API 구현**을 훨씬 쉽게 할 수 있으며, 경우에 따라 사용자 API 코드의 일부를 자동 생성할 수도 있습니다. |
|||
|
|||
/// info |
|||
|
|||
웹훅은 OpenAPI 3.1.0 이상에서 지원되며, FastAPI `0.99.0` 이상 버전에서 사용할 수 있습니다. |
|||
|
|||
/// |
|||
|
|||
## 웹훅이 포함된 앱 만들기 |
|||
|
|||
**FastAPI** 애플리케이션을 만들 때, `webhooks` 속성을 사용하여 *웹훅*을 정의할 수 있습니다. 이는 `@app.webhooks.post()`와 같은 방식으로 *경로(path) 작업*을 정의하는 것과 비슷합니다. |
|||
|
|||
{* ../../docs_src/openapi_webhooks/tutorial001.py hl[9:13,36:53] *} |
|||
|
|||
이렇게 정의한 웹훅은 **OpenAPI** 스키마와 자동 **문서화 UI**에 표시됩니다. |
|||
|
|||
/// info |
|||
|
|||
`app.webhooks` 객체는 사실 `APIRouter`일 뿐이며, 여러 파일로 앱을 구성할 때 사용하는 것과 동일한 타입입니다. |
|||
|
|||
/// |
|||
|
|||
웹훅에서는 실제 **경로(path)** (예: `/items/`)를 선언하지 않는 점에 유의해야 합니다. 여기서 전달하는 텍스트는 **식별자**로, 웹훅의 이름(이벤트 이름)입니다. 예를 들어, `@app.webhooks.post("new-subscription")`에서 웹훅 이름은 `new-subscription`입니다. |
|||
|
|||
이는 실제 **URL 경로**는 **사용자**가 다른 방법(예: 웹 대시보드)을 통해 지정하도록 기대되기 때문입니다. |
|||
|
|||
### 문서 확인하기 |
|||
|
|||
이제 앱을 시작하고 <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>로 이동해 봅시다. |
|||
|
|||
문서에서 기존 *경로 작업*뿐만 아니라 **웹훅**도 표시된 것을 확인할 수 있습니다: |
|||
|
|||
<img src="/img/tutorial/openapi-webhooks/image01.png"> |
@ -0,0 +1,3 @@ |
|||
# 리소스 |
|||
|
|||
추가 리소스, 외부 링크, 기사 등. ✈️ |
@ -0,0 +1,76 @@ |
|||
# 쿠키 매개변수 모델 |
|||
|
|||
관련있는 **쿠키**들의 그룹이 있는 경우, **Pydantic 모델**을 생성하여 선언할 수 있습니다. 🍪 |
|||
|
|||
이를 통해 **여러 위치**에서 **모델을 재사용** 할 수 있고 모든 매개변수에 대한 유효성 검사 및 메타데이터를 한 번에 선언할 수도 있습니다. 😍 |
|||
|
|||
/// note | 참고 |
|||
|
|||
이 기능은 FastAPI 버전 `0.115.0` 이후부터 지원됩니다. 🤓 |
|||
|
|||
/// |
|||
|
|||
/// tip | 팁 |
|||
|
|||
동일한 기술이 `Query`, `Cookie`, 그리고 `Header`에 적용됩니다. 😎 |
|||
|
|||
/// |
|||
|
|||
## Pydantic 모델을 사용한 쿠키 |
|||
|
|||
**Pydantic 모델**에 필요한 **쿠키** 매개변수를 선언한 다음, 해당 매개변수를 `Cookie`로 선언합니다: |
|||
|
|||
{* ../../docs_src/cookie_param_models/tutorial001_an_py310.py hl[9:12,16] *} |
|||
|
|||
**FastAPI**는 요청에서 받은 **쿠키**에서 **각 필드**에 대한 데이터를 **추출**하고 정의한 Pydantic 모델을 줍니다. |
|||
|
|||
## 문서 확인하기 |
|||
|
|||
문서 UI `/docs`에서 정의한 쿠키를 볼 수 있습니다: |
|||
|
|||
<div class="screenshot"> |
|||
<img src="/img/tutorial/cookie-param-models/image01.png"> |
|||
</div> |
|||
|
|||
/// info | 정보 |
|||
|
|||
명심하세요, 내부적으로 **브라우저는 쿠키를 특별한 방식으로 처리**하기 때문에 **자바스크립트**가 쉽게 쿠키를 건드릴 수 **없습니다**. |
|||
|
|||
`/docs`에서 **API 문서 UI**로 이동하면 *경로 작업*에 대한 쿠키의 **문서**를 볼 수 있습니다. |
|||
|
|||
하지만 아무리 **데이터를 입력**하고 "실행(Execute)"을 클릭해도, 문서 UI는 **자바스크립트**로 작동하기 때문에 쿠키는 전송되지 않고, 아무 값도 쓰지 않은 것처럼 **오류** 메시지를 보게 됩니다. |
|||
|
|||
/// |
|||
|
|||
## 추가 쿠키 금지하기 |
|||
|
|||
일부 특별한 사용 사례(흔하지는 않겠지만)에서는 수신하려는 쿠키를 **제한**할 수 있습니다. |
|||
|
|||
이제 API는 자신의 <abbr title="농담입니다, 혹시나 해서요. 쿠키 동의와 관련해서 할 수 있는 것은 없지만, 이제 API조차도 잘못된 쿠키를 거부할 수 있다는 점이 재밌습니다. 쿠키 드세요. 🍪">쿠키 동의</abbr>를 제어할 수 있는 권한을 갖게 되었습니다. 🤪🍪 |
|||
|
|||
Pydantic의 모델 구성을 사용하여 추가(`extra`) 필드를 금지(`forbid`)할 수 있습니다: |
|||
|
|||
{* ../../docs_src/cookie_param_models/tutorial002_an_py39.py hl[10] *} |
|||
|
|||
클라이언트가 **추가 쿠키**를 보내려고 시도하면, **오류** 응답을 받게 됩니다. |
|||
|
|||
<abbr title="이건 또 다른 농담입니다. 제 말에 귀 기울이지 마세요. 커피랑 쿠키 좀 드세요. ☕">API가 거부</abbr>하는데도 동의를 얻기 위해 애쓰는 불쌍한 쿠키 배너(팝업)들. 🍪 |
|||
|
|||
예를 들어, 클라이언트가 `good-list-please` 값으로 `santa_tracker` 쿠키를 보내려고 하면 클라이언트는 `santa_tracker` <abbr title="산타는 쿠키가 부족한 것을 못마땅해합니다. 🎅 알겠습니다, 쿠키 농담은 이제 없습니다.">쿠키가 허용되지 않는다</abbr>는 **오류** 응답을 받게 됩니다: |
|||
|
|||
```json |
|||
{ |
|||
"detail": [ |
|||
{ |
|||
"type": "extra_forbidden", |
|||
"loc": ["cookie", "santa_tracker"], |
|||
"msg": "Extra inputs are not permitted", |
|||
"input": "good-list-please", |
|||
} |
|||
] |
|||
} |
|||
``` |
|||
|
|||
## 요약 |
|||
|
|||
**Pydantic 모델**을 사용하여 **FastAPI**에서 <abbr title="가기 전에 마지막 쿠키를 드세요. 🍪">**쿠키**</abbr>를 선언할 수 있습니다. 😍 |
@ -0,0 +1,56 @@ |
|||
# 헤더 매개변수 모델 |
|||
|
|||
관련 있는 **헤더 매개변수** 그룹이 있는 경우, **Pydantic 모델**을 생성하여 선언할 수 있습니다. |
|||
|
|||
이를 통해 **여러 위치**에서 **모델을 재사용** 할 수 있고 모든 매개변수에 대한 유효성 검사 및 메타데이터를 한 번에 선언할 수도 있습니다. 😎 |
|||
|
|||
/// note | 참고 |
|||
|
|||
이 기능은 FastAPI 버전 `0.115.0` 이후부터 지원됩니다. 🤓 |
|||
|
|||
/// |
|||
|
|||
## Pydantic 모델을 사용한 헤더 매개변수 |
|||
|
|||
**Pydantic 모델**에 필요한 **헤더 매개변수**를 선언한 다음, 해당 매개변수를 `Header`로 선언합니다: |
|||
|
|||
{* ../../docs_src/header_param_models/tutorial001_an_py310.py hl[9:14,18] *} |
|||
|
|||
**FastAPI**는 요청에서 받은 **헤더**에서 **각 필드**에 대한 데이터를 **추출**하고 정의한 Pydantic 모델을 줍니다. |
|||
|
|||
## 문서 확인하기 |
|||
|
|||
문서 UI `/docs`에서 필요한 헤더를 볼 수 있습니다: |
|||
|
|||
<div class="screenshot"> |
|||
<img src="/img/tutorial/header-param-models/image01.png"> |
|||
</div> |
|||
|
|||
## 추가 헤더 금지하기 |
|||
|
|||
일부 특별한 사용 사례(흔하지는 않겠지만)에서는 수신하려는 헤더를 **제한**할 수 있습니다. |
|||
|
|||
Pydantic의 모델 구성을 사용하여 추가(`extra`) 필드를 금지(`forbid`)할 수 있습니다: |
|||
|
|||
{* ../../docs_src/header_param_models/tutorial002_an_py310.py hl[10] *} |
|||
|
|||
클라이언트가 **추가 헤더**를 보내려고 시도하면, **오류** 응답을 받게 됩니다. |
|||
|
|||
예를 들어, 클라이언트가 `plumbus` 값으로 `tool` 헤더를 보내려고 하면, 클라이언트는 헤더 매개변수 `tool`이 허용 되지 않는다는 **오류** 응답을 받게 됩니다: |
|||
|
|||
```json |
|||
{ |
|||
"detail": [ |
|||
{ |
|||
"type": "extra_forbidden", |
|||
"loc": ["header", "tool"], |
|||
"msg": "Extra inputs are not permitted", |
|||
"input": "plumbus", |
|||
} |
|||
] |
|||
} |
|||
``` |
|||
|
|||
## 요약 |
|||
|
|||
**Pydantic 모델**을 사용하여 **FastAPI**에서 **헤더**를 선언할 수 있습니다. 😎 |
@ -0,0 +1,78 @@ |
|||
# 폼 모델 |
|||
|
|||
FastAPI에서 **Pydantic 모델**을 이용하여 **폼 필드**를 선언할 수 있습니다. |
|||
|
|||
/// info | 정보 |
|||
|
|||
폼(Form)을 사용하려면, 먼저 <a href="https://github.com/Kludex/python-multipart" class="external-link" target="_blank">`python-multipart`</a>를 설치하세요. |
|||
|
|||
[가상 환경](../virtual-environments.md){.internal-link target=_blank}을 생성하고 활성화한 다음, 아래와 같이 설치할 수 있습니다: |
|||
|
|||
```console |
|||
$ pip install python-multipart |
|||
``` |
|||
|
|||
/// |
|||
|
|||
/// note | 참고 |
|||
|
|||
이 기능은 FastAPI 버전 `0.113.0` 이후부터 지원됩니다. 🤓 |
|||
|
|||
/// |
|||
|
|||
## Pydantic 모델을 사용한 폼 |
|||
|
|||
**폼 필드**로 받고 싶은 필드를 **Pydantic 모델**로 선언한 다음, 매개변수를 `Form`으로 선언하면 됩니다: |
|||
|
|||
{* ../../docs_src/request_form_models/tutorial001_an_py39.py hl[9:11,15] *} |
|||
|
|||
**FastAPI**는 요청에서 받은 **폼 데이터**에서 **각 필드**에 대한 데이터를 **추출**하고 정의한 Pydantic 모델을 줍니다. |
|||
|
|||
## 문서 확인하기 |
|||
|
|||
문서 UI `/docs`에서 확인할 수 있습니다: |
|||
|
|||
<div class="screenshot"> |
|||
<img src="/img/tutorial/request-form-models/image01.png"> |
|||
</div> |
|||
|
|||
## 추가 폼 필드 금지하기 |
|||
|
|||
일부 특별한 사용 사례(흔하지는 않겠지만)에서는 Pydantic 모델에서 정의한 폼 필드를 **제한**하길 원할 수도 있습니다. 그리고 **추가** 필드를 **금지**할 수도 있습니다. |
|||
|
|||
/// note | 참고 |
|||
|
|||
이 기능은 FastAPI 버전 `0.114.0` 이후부터 지원됩니다. 🤓 |
|||
|
|||
/// |
|||
|
|||
Pydantic의 모델 구성을 사용하여 추가(`extra`) 필드를 금지(`forbid`)할 수 있습니다: |
|||
|
|||
{* ../../docs_src/request_form_models/tutorial002_an_py39.py hl[12] *} |
|||
|
|||
클라이언트가 추가 데이터를 보내려고 하면 **오류** 응답을 받게 됩니다. |
|||
|
|||
예를 들어, 클라이언트가 폼 필드를 보내려고 하면: |
|||
|
|||
* `username`: `Rick` |
|||
* `password`: `Portal Gun` |
|||
* `extra`: `Mr. Poopybutthole` |
|||
|
|||
`extra` 필드가 허용되지 않는다는 오류 응답을 받게 됩니다: |
|||
|
|||
```json |
|||
{ |
|||
"detail": [ |
|||
{ |
|||
"type": "extra_forbidden", |
|||
"loc": ["body", "extra"], |
|||
"msg": "Extra inputs are not permitted", |
|||
"input": "Mr. Poopybutthole" |
|||
} |
|||
] |
|||
} |
|||
``` |
|||
|
|||
## 요약 |
|||
|
|||
Pydantic 모델을 사용하여 FastAPI에서 폼 필드를 선언할 수 있습니다. 😎 |
@ -0,0 +1,74 @@ |
|||
# 폼 데이터 |
|||
|
|||
JSON 대신 폼 필드를 받아야 하는 경우 `Form`을 사용할 수 있습니다. |
|||
|
|||
/// info | 정보 |
|||
|
|||
폼을 사용하려면, 먼저 <a href="https://github.com/Kludex/python-multipart" class="external-link" target="_blank">`python-multipart`</a>를 설치하세요. |
|||
|
|||
[가상 환경](../virtual-environments.md){.internal-link target=_blank}을 생성하고 활성화한 다음, 아래와 같이 설치할 수 있습니다: |
|||
|
|||
```console |
|||
$ pip install python-multipart |
|||
``` |
|||
|
|||
/// |
|||
|
|||
## `Form` 임포트하기 |
|||
|
|||
`fastapi`에서 `Form`을 임포트합니다: |
|||
|
|||
{* ../../docs_src/request_forms/tutorial001_an_py39.py hl[3] *} |
|||
|
|||
## `Form` 매개변수 정의하기 |
|||
|
|||
`Body` 또는 `Query`와 동일한 방식으로 폼 매개변수를 만듭니다: |
|||
|
|||
{* ../../docs_src/request_forms/tutorial001_an_py39.py hl[9] *} |
|||
|
|||
예를 들어, OAuth2 사양을 사용할 수 있는 방법 중 하나("패스워드 플로우"라고 함)로 `username`과 `password`를 폼 필드로 보내야 합니다. |
|||
|
|||
<abbr title="specification">사양</abbr>에서는 필드 이름이 `username` 및 `password`로 정확하게 명명되어야 하고, JSON이 아닌 폼 필드로 전송해야 합니다. |
|||
|
|||
`Form`을 사용하면 유효성 검사, 예제, 별칭(예: `username` 대신 `user-name`) 등을 포함하여 `Body`(및 `Query`, `Path`, `Cookie`)와 동일한 구성을 선언할 수 있습니다. |
|||
|
|||
/// info | 정보 |
|||
|
|||
`Form`은 `Body`에서 직접 상속되는 클래스입니다. |
|||
|
|||
/// |
|||
|
|||
/// tip | 팁 |
|||
|
|||
폼 본문을 선언할 때, 폼이 없으면 매개변수가 쿼리 매개변수나 본문(JSON) 매개변수로 해석(interpret)되기 때문에 `Form`을 명시적으로 사용해야 합니다. |
|||
|
|||
/// |
|||
|
|||
## "폼 필드"에 대해 |
|||
|
|||
HTML 폼(`<form></form>`)이 데이터를 서버로 보내는 방식은 일반적으로 해당 데이터에 대해 "특수" 인코딩을 사용하며, 이는 JSON과 다릅니다. |
|||
|
|||
**FastAPI**는 JSON 대신 올바른 위치에서 해당 데이터를 읽습니다. |
|||
|
|||
/// note | 기술 세부사항 |
|||
|
|||
폼의 데이터는 일반적으로 "미디어 유형(media type)" `application/x-www-form-urlencoded`를 사용하여 인코딩합니다. |
|||
|
|||
그러나 폼에 파일이 포함된 경우, `multipart/form-data`로 인코딩합니다. 다음 장에서 파일 처리에 대해 읽을 겁니다. |
|||
|
|||
|
|||
이러한 인코딩 및 폼 필드에 대해 더 읽고 싶다면, <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST" class="external-link" target="_blank"><code>POST</code>에 대한 <abbr title="Mozilla Developer Network">MDN</a> 웹 문서를 참조하세요. |
|||
|
|||
/// |
|||
|
|||
/// warning | 경고 |
|||
|
|||
*경로 작업*에서 여러 `Form` 매개변수를 선언할 수 있지만, JSON으로 수신할 것으로 예상되는 `Body` 필드와 함께 선언할 수 없습니다. 요청 본문은 `application/json` 대신에 `application/x-www-form-urlencoded`를 사용하여 인코딩되기 때문입니다. |
|||
|
|||
이는 **FastAPI**의 제한 사항이 아니며 HTTP 프로토콜의 일부입니다. |
|||
|
|||
/// |
|||
|
|||
## 요약 |
|||
|
|||
폼 데이터 입력 매개변수를 선언하려면 `Form`을 사용하세요. |
@ -0,0 +1,243 @@ |
|||
# 테스팅 |
|||
|
|||
<a href="https://www.starlette.io/testclient/" class="external-link" target="_blank">Starlette</a> 덕분에 **FastAPI** 를 테스트하는 일은 쉽고 즐거운 일이 되었습니다. |
|||
|
|||
Starlette는 <a href="https://www.python-httpx.org\" class="external-link" target="_blank">HTTPX</a>를 기반으로 하며, 이는 Requests를 기반으로 설계되었기 때문에 매우 친숙하고 직관적입니다. |
|||
|
|||
이를 사용하면 FastAPI에서 <a href="https://docs.pytest.org/" class="external-link" target="_blank">pytest</a>를 직접 사용할 수 있습니다. |
|||
|
|||
## `TestClient` 사용하기 |
|||
|
|||
/// info | 정보 |
|||
|
|||
`TestClient` 사용하려면, 우선 <a href="https://www.python-httpx.org" class="external-link" target="_blank">`httpx`</a> 를 설치해야 합니다. |
|||
|
|||
[virtual environment](../virtual-environments.md){.internal-link target=_blank} 를 만들고, 활성화 시킨 뒤에 설치하세요. 예시: |
|||
|
|||
```console |
|||
$ pip install httpx |
|||
``` |
|||
|
|||
/// |
|||
|
|||
`TestClient` 를 임포트하세요. |
|||
|
|||
**FastAPI** 어플리케이션을 전달하여 `TestClient` 를 만드세요. |
|||
|
|||
이름이 `test_` 로 시작하는 함수를 만드세요(`pytest` 의 표준적인 관례입니다). |
|||
|
|||
`httpx` 를 사용하는 것과 같은 방식으로 `TestClient` 객체를 사용하세요. |
|||
|
|||
표준적인 파이썬 문법을 이용하여 확인이 필요한 곳에 간단한 `assert` 문장을 작성하세요(역시 표준적인 `pytest` 관례입니다). |
|||
|
|||
{* ../../docs_src/app_testing/tutorial001.py hl[2,12,15:18] *} |
|||
|
|||
/// tip | 팁 |
|||
|
|||
테스트를 위한 함수는 `async def` 가 아니라 `def` 로 작성됨에 주의하세요. |
|||
|
|||
그리고 클라이언트에 대한 호출도 `await` 를 사용하지 않는 일반 호출입니다. |
|||
|
|||
이렇게 하여 복잡한 과정 없이 `pytest` 를 직접적으로 사용할 수 있습니다. |
|||
|
|||
/// |
|||
|
|||
/// note | 기술 세부사항 |
|||
|
|||
`from starlette.testclient import TestClient` 역시 사용할 수 있습니다. |
|||
|
|||
**FastAPI** 는 개발자의 편의를 위해 `starlette.testclient` 를 `fastapi.testclient` 로도 제공할 뿐입니다. 이는 단지 `Starlette` 에서 직접 가져오는지의 차이일 뿐입니다. |
|||
|
|||
/// |
|||
|
|||
/// tip | 팁 |
|||
|
|||
FastAPI 애플리케이션에 요청을 보내는 것 외에도 테스트에서 `async` 함수를 호출하고 싶다면 (예: 비동기 데이터베이스 함수), 심화 튜토리얼의 [Async Tests](../advanced/async-tests.md){.internal-link target=_blank} 를 참조하세요. |
|||
|
|||
/// |
|||
|
|||
## 테스트 분리하기 |
|||
|
|||
실제 애플리케이션에서는 테스트를 별도의 파일로 나누는 경우가 많습니다. |
|||
|
|||
|
|||
그리고 **FastAPI** 애플리케이션도 여러 파일이나 모듈 등으로 구성될 수 있습니다. |
|||
|
|||
### **FastAPI** app 파일 |
|||
|
|||
[Bigger Applications](bigger-applications.md){.internal-link target=_blank} 에 묘사된 파일 구조를 가지고 있는 것으로 가정해봅시다. |
|||
|
|||
``` |
|||
. |
|||
├── app |
|||
│ ├── __init__.py |
|||
│ └── main.py |
|||
``` |
|||
|
|||
`main.py` 파일 안에 **FastAPI** app 을 만들었습니다: |
|||
|
|||
{* ../../docs_src/app_testing/main.py *} |
|||
|
|||
### 테스트 파일 |
|||
|
|||
테스트를 위해 `test_main.py` 라는 파일을 생성할 수 있습니다. 이 파일은 동일한 Python 패키지(즉, `__init__.py` 파일이 있는 동일한 디렉터리)에 위치할 수 있습니다. |
|||
|
|||
``` hl_lines="5" |
|||
. |
|||
├── app |
|||
│ ├── __init__.py |
|||
│ ├── main.py |
|||
│ └── test_main.py |
|||
``` |
|||
|
|||
파일들이 동일한 패키지에 위치해 있으므로, 상대 참조를 사용하여 `main` 에서 `app` 객체를 임포트 해올 수 있습니다. |
|||
|
|||
{* ../../docs_src/app_testing/test_main.py hl[3] *} |
|||
|
|||
|
|||
...그리고 이전에 작성했던 것과 같은 테스트 코드를 작성할 수 있습니다. |
|||
|
|||
## 테스트: 확장된 예시 |
|||
|
|||
이제 위의 예시를 확장하고 더 많은 세부 사항을 추가하여 다양한 부분을 어떻게 테스트하는지 살펴보겠습니다. |
|||
|
|||
### 확장된 FastAPI 애플리케이션 파일 |
|||
|
|||
이전과 같은 파일 구조를 계속 사용해 보겠습니다. |
|||
|
|||
``` |
|||
. |
|||
├── app |
|||
│ ├── __init__.py |
|||
│ ├── main.py |
|||
│ └── test_main.py |
|||
``` |
|||
|
|||
이제 **FastAPI** 앱이 있는 `main.py` 파일에 몇 가지 다른 **경로 작업** 이 추가된 경우를 생각해봅시다. |
|||
|
|||
단일 오류를 반환할 수 있는 `GET` 작업이 있습니다. |
|||
|
|||
여러 다른 오류를 반환할 수 있는 `POST` 작업이 있습니다. |
|||
|
|||
두 *경로 작업* 모두 `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 | 팁 |
|||
|
|||
될 수 있으면 `Annotated` 버전 사용을 권장합나다. |
|||
|
|||
/// |
|||
|
|||
```Python |
|||
{!> ../../docs_src/app_testing/app_b_py310/main.py!} |
|||
``` |
|||
|
|||
//// |
|||
|
|||
//// tab | Python 3.8+ non-Annotated |
|||
|
|||
/// tip | 팁 |
|||
|
|||
될 수 있으면 `Annotated` 버전 사용을 권장합나다. |
|||
|
|||
/// |
|||
|
|||
```Python |
|||
{!> ../../docs_src/app_testing/app_b/main.py!} |
|||
``` |
|||
|
|||
//// |
|||
|
|||
### 확장된 테스트 파일 |
|||
|
|||
이제는 `test_main.py` 를 확장된 테스트들로 수정할 수 있습니다: |
|||
|
|||
{* ../../docs_src/app_testing/app_b/test_main.py *} |
|||
|
|||
|
|||
클라이언트가 요청에 정보를 전달해야 하는데 방법을 모르겠다면, `httpx`에서 해당 작업을 수행하는 방법을 검색(Google)하거나, `requests`에서의 방법을 검색해보세요. HTTPX는 Requests의 디자인을 기반으로 설계되었습니다. |
|||
|
|||
그 후, 테스트에서도 동일하게 적용하면 됩니다. |
|||
|
|||
예시: |
|||
|
|||
* *경로* 혹은 *쿼리* 매개변수를 전달하려면, URL 자체에 추가한다. |
|||
* JSON 본문을 전달하려면, 파이썬 객체 (예를들면 `dict`) 를 `json` 파라미터로 전달한다. |
|||
* JSON 대신 *폼 데이터* 를 보내야한다면, `data` 파라미터를 대신 전달한다. |
|||
* *헤더* 를 전달하려면, `headers` 파라미터에 `dict` 를 전달한다. |
|||
* *쿠키* 를 전달하려면, `cookies` 파라미터에 `dict` 를 전달한다. |
|||
|
|||
백엔드로 데이터를 어떻게 보내는지 정보를 더 얻으려면 (`httpx` 혹은 `TestClient` 를 이용해서) <a href="https://www.python-httpx.org" class="external-link" target="_blank">HTTPX documentation</a> 를 확인하세요. |
|||
|
|||
/// info | 정보 |
|||
|
|||
`TestClient` 는 Pydantic 모델이 아니라 JSON 으로 변환될 수 있는 데이터를 받습니다. |
|||
|
|||
만약 테스트중 Pydantic 모델을 어플리케이션으로에 보내고 싶다면, [JSON 호환 가능 인코더](encoder.md){.internal-link target=_blank} 에 설명되어 있는 `jsonable_encoder` 를 사용할 수 있습니다. |
|||
|
|||
/// |
|||
|
|||
## 실행하기 |
|||
|
|||
테스트 코드를 작성하고, `pytest` 를 설치해야합니다. |
|||
|
|||
[virtual environment](../virtual-environments.md){.internal-link target=_blank} 를 만들고, 활성화 시킨 뒤에 설치하세요. 예시: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ pip install pytest |
|||
|
|||
---> 100% |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
`pytest` 파일과 테스트를 자동으로 감지하고 실행한 다음, 결과를 보고할 것입니다. |
|||
|
|||
테스트를 다음 명령어로 실행하세요. |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ pytest |
|||
|
|||
================ test session starts ================ |
|||
platform linux -- Python 3.6.9, pytest-5.3.5, py-1.8.1, pluggy-0.13.1 |
|||
rootdir: /home/user/code/superawesome-cli/app |
|||
plugins: forked-1.1.3, xdist-1.31.0, cov-2.8.1 |
|||
collected 6 items |
|||
|
|||
---> 100% |
|||
|
|||
test_main.py <span style="color: green; white-space: pre;">...... [100%]</span> |
|||
|
|||
<span style="color: green;">================= 1 passed in 0.03s =================</span> |
|||
``` |
|||
|
|||
</div> |
@ -0,0 +1,261 @@ |
|||
# Generate Clients |
|||
|
|||
Como o **FastAPI** é baseado na especificação **OpenAPI**, você obtém compatibilidade automática com muitas ferramentas, incluindo a documentação automática da API (fornecida pelo Swagger UI). |
|||
|
|||
Uma vantagem particular que nem sempre é óbvia é que você pode **gerar clientes** (às vezes chamados de <abbr title="Software Development Kits">**SDKs**</abbr>) para a sua API, para muitas **linguagens de programação** diferentes. |
|||
|
|||
## Geradores de Clientes OpenAPI |
|||
|
|||
Existem muitas ferramentas para gerar clientes a partir do **OpenAPI**. |
|||
|
|||
Uma ferramenta comum é o <a href="https://openapi-generator.tech/" class="external-link" target="_blank">OpenAPI Generator</a>. |
|||
|
|||
Se voce está construindo um **frontend**, uma alternativa muito interessante é o <a href="https://github.com/hey-api/openapi-ts" class="external-link" target="_blank">openapi-ts</a>. |
|||
|
|||
## Geradores de Clientes e SDKs - Patrocinadores |
|||
|
|||
Existem também alguns geradores de clientes e SDKs baseados na OpenAPI (FastAPI) **patrocinados por empresas**, em alguns casos eles podem oferecer **recursos adicionais** além de SDKs/clientes gerados de alta qualidade. |
|||
|
|||
Alguns deles também ✨ [**patrocinam o FastAPI**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨, isso garante o **desenvolvimento** contínuo e saudável do FastAPI e seu **ecossistema**. |
|||
|
|||
E isso mostra o verdadeiro compromisso deles com o FastAPI e sua **comunidade** (você), pois eles não apenas querem fornecer um **bom serviço**, mas também querem garantir que você tenha um **framework bom e saudável**, o FastAPI. 🙇 |
|||
|
|||
Por exemplo, você pode querer experimentar: |
|||
|
|||
* <a href="https://speakeasy.com/?utm_source=fastapi+repo&utm_medium=github+sponsorship" class="external-link" target="_blank">Speakeasy</a> |
|||
* <a href="https://www.stainlessapi.com/?utm_source=fastapi&utm_medium=referral" class="external-link" target="_blank">Stainless</a> |
|||
* <a href="https://developers.liblab.com/tutorials/sdk-for-fastapi/?utm_source=fastapi" class="external-link" target="_blank">liblab</a> |
|||
|
|||
Existem também várias outras empresas que oferecem serviços semelhantes que você pode pesquisar e encontrar online. 🤓 |
|||
|
|||
## Gerar um Cliente Frontend TypeScript |
|||
|
|||
Vamos começar com um aplicativo **FastAPI** simples: |
|||
|
|||
{* ../../docs_src/generate_clients/tutorial001_py39.py hl[7:9,12:13,16:17,21] *} |
|||
|
|||
Note que as *operações de rota* definem os modelos que usam para o corpo da requisição e o corpo da resposta, usando os modelos `Item` e `ResponseMessage`. |
|||
|
|||
### Documentação da API |
|||
|
|||
Se você acessar a documentação da API, verá que ela tem os **schemas** para os dados a serem enviados nas requisições e recebidos nas respostas: |
|||
|
|||
<img src="/img/tutorial/generate-clients/image01.png"> |
|||
|
|||
Você pode ver esses schemas porque eles foram declarados com os modelos no app. |
|||
|
|||
Essas informações estão disponíveis no **OpenAPI schema** do app e são mostradas na documentação da API (pelo Swagger UI). |
|||
|
|||
E essas mesmas informações dos modelos que estão incluídas no OpenAPI são o que pode ser usado para **gerar o código do cliente**. |
|||
|
|||
### Gerar um Cliente TypeScript |
|||
|
|||
Agora que temos o app com os modelos, podemos gerar o código do cliente para o frontend. |
|||
|
|||
#### Instalar o `openapi-ts` |
|||
|
|||
Você pode instalar o `openapi-ts` no seu código frontend com: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ npm install @hey-api/openapi-ts --save-dev |
|||
|
|||
---> 100% |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
#### Gerar o Código do Cliente |
|||
|
|||
Para gerar o código do cliente, você pode usar a aplicação de linha de comando `openapi-ts` que agora está instalada. |
|||
|
|||
Como ela está instalada no projeto local, você provavelmente não conseguiria chamar esse comando diretamente, mas você o colocaria no seu arquivo `package.json`. |
|||
|
|||
Poderia ser assim: |
|||
|
|||
```JSON hl_lines="7" |
|||
{ |
|||
"name": "frontend-app", |
|||
"version": "1.0.0", |
|||
"description": "", |
|||
"main": "index.js", |
|||
"scripts": { |
|||
"generate-client": "openapi-ts --input http://localhost:8000/openapi.json --output ./src/client --client axios" |
|||
}, |
|||
"author": "", |
|||
"license": "", |
|||
"devDependencies": { |
|||
"@hey-api/openapi-ts": "^0.27.38", |
|||
"typescript": "^4.6.2" |
|||
} |
|||
} |
|||
``` |
|||
|
|||
Depois de ter esse script NPM `generate-client` lá, você pode executá-lo com: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ npm run generate-client |
|||
|
|||
[email protected] generate-client /home/user/code/frontend-app |
|||
> openapi-ts --input http://localhost:8000/openapi.json --output ./src/client --client axios |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
Esse comando gerará o código em `./src/client` e usará o `axios` (a biblioteca HTTP frontend) internamente. |
|||
|
|||
### Experimente o Código do Cliente |
|||
|
|||
Agora você pode importar e usar o código do cliente, ele poderia ser assim, observe que você obtém preenchimento automático para os métodos: |
|||
|
|||
<img src="/img/tutorial/generate-clients/image02.png"> |
|||
|
|||
Você também obterá preenchimento automático para o corpo a ser enviado: |
|||
|
|||
<img src="/img/tutorial/generate-clients/image03.png"> |
|||
|
|||
/// tip | Dica |
|||
|
|||
Observe o preenchimento automático para `name` e `price`, que foi definido no aplicativo FastAPI, no modelo `Item`. |
|||
|
|||
/// |
|||
|
|||
Você terá erros em linha para os dados que você envia: |
|||
|
|||
<img src="/img/tutorial/generate-clients/image04.png"> |
|||
|
|||
O objeto de resposta também terá preenchimento automático: |
|||
|
|||
<img src="/img/tutorial/generate-clients/image05.png"> |
|||
|
|||
## App FastAPI com Tags |
|||
|
|||
Em muitos casos seu app FastAPI será maior, e você provavelmente usará tags para separar diferentes grupos de *operações de rota*. |
|||
|
|||
Por exemplo, você poderia ter uma seção para **items** e outra seção para **users**, e elas poderiam ser separadas por tags: |
|||
|
|||
{* ../../docs_src/generate_clients/tutorial002_py39.py hl[21,26,34] *} |
|||
|
|||
### Gerar um Cliente TypeScript com Tags |
|||
|
|||
Se você gerar um cliente para um app FastAPI usando tags, normalmente também separará o código do cliente com base nas tags. |
|||
|
|||
Dessa forma, você poderá ter as coisas ordenadas e agrupadas corretamente para o código do cliente: |
|||
|
|||
<img src="/img/tutorial/generate-clients/image06.png"> |
|||
|
|||
Nesse caso você tem: |
|||
|
|||
* `ItemsService` |
|||
* `UsersService` |
|||
|
|||
### Nomes dos Métodos do Cliente |
|||
|
|||
Agora os nomes dos métodos gerados como `createItemItemsPost` não parecem muito "limpos": |
|||
|
|||
```TypeScript |
|||
ItemsService.createItemItemsPost({name: "Plumbus", price: 5}) |
|||
``` |
|||
|
|||
...isto ocorre porque o gerador de clientes usa o **operation ID** interno do OpenAPI para cada *operação de rota*. |
|||
|
|||
O OpenAPI exige que cada operation ID seja único em todas as *operações de rota*, então o FastAPI usa o **nome da função**, o **caminho** e o **método/operacao HTTP** para gerar esse operation ID, porque dessa forma ele pode garantir que os operation IDs sejam únicos. |
|||
|
|||
Mas eu vou te mostrar como melhorar isso a seguir. 🤓 |
|||
|
|||
### IDs de Operação Personalizados e Melhores Nomes de Método |
|||
|
|||
Você pode **modificar** a maneira como esses IDs de operação são **gerados** para torná-los mais simples e ter **nomes de método mais simples** nos clientes. |
|||
|
|||
Neste caso, você terá que garantir que cada ID de operação seja **único** de alguma outra maneira. |
|||
|
|||
Por exemplo, você poderia garantir que cada *operação de rota* tenha uma tag, e então gerar o ID da operação com base na **tag** e no **nome** da *operação de rota* (o nome da função). |
|||
|
|||
### Função Personalizada para Gerar IDs de Operação Únicos |
|||
|
|||
O FastAPI usa um **ID único** para cada *operação de rota*, ele é usado para o **ID da operação** e também para os nomes de quaisquer modelos personalizados necessários, para requisições ou respostas. |
|||
|
|||
Você pode personalizar essa função. Ela recebe uma `APIRoute` e gera uma string. |
|||
|
|||
Por exemplo, aqui está usando a primeira tag (você provavelmente terá apenas uma tag) e o nome da *operação de rota* (o nome da função). |
|||
|
|||
Você pode então passar essa função personalizada para o **FastAPI** como o parâmetro `generate_unique_id_function`: |
|||
|
|||
{* ../../docs_src/generate_clients/tutorial003_py39.py hl[6:7,10] *} |
|||
|
|||
### Gerar um Cliente TypeScript com IDs de Operação Personalizados |
|||
|
|||
Agora, se você gerar o cliente novamente, verá que ele tem os nomes dos métodos melhorados: |
|||
|
|||
<img src="/img/tutorial/generate-clients/image07.png"> |
|||
|
|||
Como você pode ver, os nomes dos métodos agora têm a tag e, em seguida, o nome da função. Agora eles não incluem informações do caminho da URL e da operação HTTP. |
|||
|
|||
### Pré-processar a Especificação OpenAPI para o Gerador de Clientes |
|||
|
|||
O código gerado ainda tem algumas **informações duplicadas**. |
|||
|
|||
Nós já sabemos que esse método está relacionado aos **items** porque essa palavra está no `ItemsService` (retirada da tag), mas ainda temos o nome da tag prefixado no nome do método também. 😕 |
|||
|
|||
Provavelmente ainda queremos mantê-lo para o OpenAPI em geral, pois isso garantirá que os IDs de operação sejam **únicos**. |
|||
|
|||
Mas para o cliente gerado, poderíamos **modificar** os IDs de operação do OpenAPI logo antes de gerar os clientes, apenas para tornar esses nomes de método mais **simples**. |
|||
|
|||
Poderíamos baixar o JSON do OpenAPI para um arquivo `openapi.json` e então poderíamos **remover essa tag prefixada** com um script como este: |
|||
|
|||
{* ../../docs_src/generate_clients/tutorial004.py *} |
|||
|
|||
//// tab | Node.js |
|||
|
|||
```Javascript |
|||
{!> ../../docs_src/generate_clients/tutorial004.js!} |
|||
``` |
|||
|
|||
//// |
|||
|
|||
Com isso, os IDs de operação seriam renomeados de coisas como `items-get_items` para apenas `get_items`, dessa forma o gerador de clientes pode gerar nomes de métodos mais simples. |
|||
|
|||
### Gerar um Cliente TypeScript com o OpenAPI Pré-processado |
|||
|
|||
Agora, como o resultado final está em um arquivo `openapi.json`, você modificaria o `package.json` para usar esse arquivo local, por exemplo: |
|||
|
|||
```JSON hl_lines="7" |
|||
{ |
|||
"name": "frontend-app", |
|||
"version": "1.0.0", |
|||
"description": "", |
|||
"main": "index.js", |
|||
"scripts": { |
|||
"generate-client": "openapi-ts --input ./openapi.json --output ./src/client --client axios" |
|||
}, |
|||
"author": "", |
|||
"license": "", |
|||
"devDependencies": { |
|||
"@hey-api/openapi-ts": "^0.27.38", |
|||
"typescript": "^4.6.2" |
|||
} |
|||
} |
|||
``` |
|||
|
|||
Depois de gerar o novo cliente, você teria agora **nomes de métodos "limpos"**, com todo o **preenchimento automático**, **erros em linha**, etc: |
|||
|
|||
<img src="/img/tutorial/generate-clients/image08.png"> |
|||
|
|||
## Benefícios |
|||
|
|||
Ao usar os clientes gerados automaticamente, você teria **preenchimento automático** para: |
|||
|
|||
* Métodos. |
|||
* Corpo de requisições, parâmetros da query, etc. |
|||
* Corpo de respostas. |
|||
|
|||
Você também teria **erros em linha** para tudo. |
|||
|
|||
E sempre que você atualizar o código do backend, e **regenerar** o frontend, ele teria quaisquer novas *operações de rota* disponíveis como métodos, as antigas removidas, e qualquer outra alteração seria refletida no código gerado. 🤓 |
|||
|
|||
Isso também significa que se algo mudar, será **refletido** no código do cliente automaticamente. E se você **construir** o cliente, ele dará erro se houver alguma **incompatibilidade** nos dados usados. |
|||
|
|||
Então, você **detectaria vários erros** muito cedo no ciclo de desenvolvimento, em vez de ter que esperar que os erros apareçam para seus usuários finais em produção e então tentar depurar onde está o problema. ✨ |
@ -0,0 +1,357 @@ |
|||
# Modelo de resposta - Tipo de retorno |
|||
|
|||
Você pode declarar o tipo usado para a resposta anotando o **tipo de retorno** *da função de operação de rota*. |
|||
|
|||
Você pode usar **anotações de tipo** da mesma forma que usaria para dados de entrada em **parâmetros** de função, você pode usar modelos Pydantic, listas, dicionários, valores escalares como inteiros, booleanos, etc. |
|||
|
|||
{* ../../docs_src/response_model/tutorial001_01_py310.py hl[16,21] *} |
|||
|
|||
O FastAPI usará este tipo de retorno para: |
|||
|
|||
* **Validar** os dados retornados. |
|||
* Se os dados forem inválidos (por exemplo, se estiver faltando um campo), significa que o código do *seu* aplicativo está quebrado, não retornando o que deveria, e retornará um erro de servidor em vez de retornar dados incorretos. Dessa forma, você e seus clientes podem ter certeza de que receberão os dados e o formato de dados esperados. |
|||
* Adicionar um **Esquema JSON** para a resposta, na *operação de rota* do OpenAPI. |
|||
* Isso será usado pela **documentação automática**. |
|||
* Também será usado por ferramentas de geração automática de código do cliente. |
|||
|
|||
Mas o mais importante: |
|||
|
|||
* Ele **limitará e filtrará** os dados de saída para o que está definido no tipo de retorno. |
|||
* Isso é particularmente importante para a **segurança**, veremos mais sobre isso abaixo. |
|||
|
|||
## Parâmetro `response_model` |
|||
|
|||
Existem alguns casos em que você precisa ou deseja retornar alguns dados que não são exatamente o que o tipo declara. |
|||
|
|||
Por exemplo, você pode querer **retornar um dicionário** ou um objeto de banco de dados, mas **declará-lo como um modelo Pydantic**. Dessa forma, o modelo Pydantic faria toda a documentação de dados, validação, etc. para o objeto que você retornou (por exemplo, um dicionário ou objeto de banco de dados). |
|||
|
|||
Se você adicionasse a anotação do tipo de retorno, ferramentas e editores reclamariam com um erro (correto) informando que sua função está retornando um tipo (por exemplo, um dict) diferente do que você declarou (por exemplo, um modelo Pydantic). |
|||
|
|||
Nesses casos, você pode usar o parâmetro `response_model` do *decorador de operação de rota* em vez do tipo de retorno. |
|||
|
|||
Você pode usar o parâmetro `response_model` em qualquer uma das *operações de rota*: |
|||
|
|||
* `@app.get()` |
|||
* `@app.post()` |
|||
* `@app.put()` |
|||
* `@app.delete()` |
|||
* etc. |
|||
|
|||
{* ../../docs_src/response_model/tutorial001_py310.py hl[17,22,24:27] *} |
|||
|
|||
/// note | Nota |
|||
|
|||
Observe que `response_model` é um parâmetro do método "decorator" (`get`, `post`, etc). Não da sua *função de operação de rota*, como todos os parâmetros e corpo. |
|||
|
|||
/// |
|||
|
|||
`response_model` recebe o mesmo tipo que você declararia para um campo de modelo Pydantic, então, pode ser um modelo Pydantic, mas também pode ser, por exemplo, uma `lista` de modelos Pydantic, como `List[Item]`. |
|||
|
|||
O FastAPI usará este `response_model` para fazer toda a documentação de dados, validação, etc. e também para **converter e filtrar os dados de saída** para sua declaração de tipo. |
|||
|
|||
/// tip | Dica |
|||
|
|||
Se você tiver verificações de tipo rigorosas em seu editor, mypy, etc, você pode declarar o tipo de retorno da função como `Any`. |
|||
|
|||
Dessa forma, você diz ao editor que está retornando qualquer coisa intencionalmente. Mas o FastAPI ainda fará a documentação de dados, validação, filtragem, etc. com o `response_model`. |
|||
|
|||
/// |
|||
|
|||
### Prioridade `response_model` |
|||
|
|||
Se você declarar tanto um tipo de retorno quanto um `response_model`, o `response_model` terá prioridade e será usado pelo FastAPI. |
|||
|
|||
Dessa forma, você pode adicionar anotações de tipo corretas às suas funções, mesmo quando estiver retornando um tipo diferente do modelo de resposta, para ser usado pelo editor e ferramentas como mypy. E ainda assim você pode fazer com que o FastAPI faça a validação de dados, documentação, etc. usando o `response_model`. |
|||
|
|||
Você também pode usar `response_model=None` para desabilitar a criação de um modelo de resposta para essa *operação de rota*, você pode precisar fazer isso se estiver adicionando anotações de tipo para coisas que não são campos Pydantic válidos, você verá um exemplo disso em uma das seções abaixo. |
|||
|
|||
## Retorna os mesmos dados de entrada |
|||
|
|||
Aqui estamos declarando um modelo `UserIn`, ele conterá uma senha em texto simples: |
|||
|
|||
{* ../../docs_src/response_model/tutorial002_py310.py hl[7,9] *} |
|||
|
|||
/// info | Informação |
|||
|
|||
Para usar `EmailStr`, primeiro instale <a href="https://github.com/JoshData/python-email-validator" class="external-link" target="_blank">`email-validator`</a>. |
|||
|
|||
Certifique-se de criar um [ambiente virtual](../virtual-environments.md){.internal-link target=_blank}, ative-o e instale-o, por exemplo: |
|||
|
|||
```console |
|||
$ pip install email-validator |
|||
``` |
|||
|
|||
ou com: |
|||
|
|||
```console |
|||
$ pip install "pydantic[email]" |
|||
``` |
|||
|
|||
/// |
|||
|
|||
E estamos usando este modelo para declarar nossa entrada e o mesmo modelo para declarar nossa saída: |
|||
|
|||
{* ../../docs_src/response_model/tutorial002_py310.py hl[16] *} |
|||
|
|||
Agora, sempre que um navegador estiver criando um usuário com uma senha, a API retornará a mesma senha na resposta. |
|||
|
|||
Neste caso, pode não ser um problema, porque é o mesmo usuário enviando a senha. |
|||
|
|||
Mas se usarmos o mesmo modelo para outra *operação de rota*, poderíamos estar enviando as senhas dos nossos usuários para todos os clientes. |
|||
|
|||
/// danger | Perigo |
|||
|
|||
Nunca armazene a senha simples de um usuário ou envie-a em uma resposta como esta, a menos que você saiba todas as ressalvas e saiba o que está fazendo. |
|||
|
|||
/// |
|||
|
|||
## Adicionar um modelo de saída |
|||
|
|||
Podemos, em vez disso, criar um modelo de entrada com a senha em texto simples e um modelo de saída sem ela: |
|||
|
|||
{* ../../docs_src/response_model/tutorial003_py310.py hl[9,11,16] *} |
|||
|
|||
Aqui, embora nossa *função de operação de rota* esteja retornando o mesmo usuário de entrada que contém a senha: |
|||
|
|||
{* ../../docs_src/response_model/tutorial003_py310.py hl[24] *} |
|||
|
|||
...declaramos o `response_model` como nosso modelo `UserOut`, que não inclui a senha: |
|||
|
|||
{* ../../docs_src/response_model/tutorial003_py310.py hl[22] *} |
|||
|
|||
Então, **FastAPI** cuidará de filtrar todos os dados que não são declarados no modelo de saída (usando Pydantic). |
|||
|
|||
### `response_model` ou Tipo de Retorno |
|||
|
|||
Neste caso, como os dois modelos são diferentes, se anotássemos o tipo de retorno da função como `UserOut`, o editor e as ferramentas reclamariam que estamos retornando um tipo inválido, pois são classes diferentes. |
|||
|
|||
É por isso que neste exemplo temos que declará-lo no parâmetro `response_model`. |
|||
|
|||
...mas continue lendo abaixo para ver como superar isso. |
|||
|
|||
## Tipo de Retorno e Filtragem de Dados |
|||
|
|||
Vamos continuar do exemplo anterior. Queríamos **anotar a função com um tipo**, mas queríamos poder retornar da função algo que realmente incluísse **mais dados**. |
|||
|
|||
Queremos que o FastAPI continue **filtrando** os dados usando o modelo de resposta. Para que, embora a função retorne mais dados, a resposta inclua apenas os campos declarados no modelo de resposta. |
|||
|
|||
No exemplo anterior, como as classes eram diferentes, tivemos que usar o parâmetro `response_model`. Mas isso também significa que não temos suporte do editor e das ferramentas verificando o tipo de retorno da função. |
|||
|
|||
Mas na maioria dos casos em que precisamos fazer algo assim, queremos que o modelo apenas **filtre/remova** alguns dados como neste exemplo. |
|||
|
|||
E nesses casos, podemos usar classes e herança para aproveitar as **anotações de tipo** de função para obter melhor suporte no editor e nas ferramentas, e ainda obter a **filtragem de dados** FastAPI. |
|||
|
|||
{* ../../docs_src/response_model/tutorial003_01_py310.py hl[7:10,13:14,18] *} |
|||
|
|||
Com isso, temos suporte de ferramentas, de editores e mypy, pois este código está correto em termos de tipos, mas também obtemos a filtragem de dados do FastAPI. |
|||
|
|||
Como isso funciona? Vamos verificar. 🤓 |
|||
|
|||
### Anotações de tipo e ferramentas |
|||
|
|||
Primeiro, vamos ver como editores, mypy e outras ferramentas veriam isso. |
|||
|
|||
`BaseUser` tem os campos base. Então `UserIn` herda de `BaseUser` e adiciona o campo `password`, então, ele incluirá todos os campos de ambos os modelos. |
|||
|
|||
Anotamos o tipo de retorno da função como `BaseUser`, mas na verdade estamos retornando uma instância `UserIn`. |
|||
|
|||
O editor, mypy e outras ferramentas não reclamarão disso porque, em termos de digitação, `UserIn` é uma subclasse de `BaseUser`, o que significa que é um tipo *válido* quando o que é esperado é qualquer coisa que seja um `BaseUser`. |
|||
|
|||
### Filtragem de dados FastAPI |
|||
|
|||
Agora, para FastAPI, ele verá o tipo de retorno e garantirá que o que você retornar inclua **apenas** os campos que são declarados no tipo. |
|||
|
|||
O FastAPI faz várias coisas internamente com o Pydantic para garantir que essas mesmas regras de herança de classe não sejam usadas para a filtragem de dados retornados, caso contrário, você pode acabar retornando muito mais dados do que o esperado. |
|||
|
|||
Dessa forma, você pode obter o melhor dos dois mundos: anotações de tipo com **suporte a ferramentas** e **filtragem de dados**. |
|||
|
|||
## Veja na documentação |
|||
|
|||
Quando você vê a documentação automática, pode verificar se o modelo de entrada e o modelo de saída terão seus próprios esquemas JSON: |
|||
|
|||
<img src="/img/tutorial/response-model/image01.png"> |
|||
|
|||
E ambos os modelos serão usados para a documentação interativa da API: |
|||
|
|||
<img src="/img/tutorial/response-model/image02.png"> |
|||
|
|||
## Outras anotações de tipo de retorno |
|||
|
|||
Pode haver casos em que você retorna algo que não é um campo Pydantic válido e anota na função, apenas para obter o suporte fornecido pelas ferramentas (o editor, mypy, etc). |
|||
|
|||
### Retornar uma resposta diretamente |
|||
|
|||
O caso mais comum seria [retornar uma resposta diretamente, conforme explicado posteriormente na documentação avançada](../advanced/response-directly.md){.internal-link target=_blank}. |
|||
|
|||
{* ../../docs_src/response_model/tutorial003_02.py hl[8,10:11] *} |
|||
|
|||
Este caso simples é tratado automaticamente pelo FastAPI porque a anotação do tipo de retorno é a classe (ou uma subclasse de) `Response`. |
|||
|
|||
E as ferramentas também ficarão felizes porque `RedirectResponse` e `JSONResponse` são subclasses de `Response`, então a anotação de tipo está correta. |
|||
|
|||
### Anotar uma subclasse de resposta |
|||
|
|||
Você também pode usar uma subclasse de `Response` na anotação de tipo: |
|||
|
|||
{* ../../docs_src/response_model/tutorial003_03.py hl[8:9] *} |
|||
|
|||
Isso também funcionará porque `RedirectResponse` é uma subclasse de `Response`, e o FastAPI tratará automaticamente este caso simples. |
|||
|
|||
### Anotações de Tipo de Retorno Inválido |
|||
|
|||
Mas quando você retorna algum outro objeto arbitrário que não é um tipo Pydantic válido (por exemplo, um objeto de banco de dados) e você o anota dessa forma na função, o FastAPI tentará criar um modelo de resposta Pydantic a partir dessa anotação de tipo e falhará. |
|||
|
|||
O mesmo aconteceria se você tivesse algo como uma <abbr title='Uma união entre vários tipos significa "qualquer um desses tipos".'>união</abbr> entre tipos diferentes onde um ou mais deles não são tipos Pydantic válidos, por exemplo, isso falharia 💥: |
|||
|
|||
{* ../../docs_src/response_model/tutorial003_04_py310.py hl[8] *} |
|||
|
|||
... isso falha porque a anotação de tipo não é um tipo Pydantic e não é apenas uma única classe ou subclasse `Response`, é uma união (qualquer uma das duas) entre um `Response` e um `dict`. |
|||
|
|||
### Desabilitar modelo de resposta |
|||
|
|||
Continuando com o exemplo acima, você pode não querer ter a validação de dados padrão, documentação, filtragem, etc. que é realizada pelo FastAPI. |
|||
|
|||
Mas você pode querer manter a anotação do tipo de retorno na função para obter o suporte de ferramentas como editores e verificadores de tipo (por exemplo, mypy). |
|||
|
|||
Neste caso, você pode desabilitar a geração do modelo de resposta definindo `response_model=None`: |
|||
|
|||
{* ../../docs_src/response_model/tutorial003_05_py310.py hl[7] *} |
|||
|
|||
Isso fará com que o FastAPI pule a geração do modelo de resposta e, dessa forma, você pode ter quaisquer anotações de tipo de retorno que precisar sem afetar seu aplicativo FastAPI. 🤓 |
|||
|
|||
## Parâmetros de codificação do modelo de resposta |
|||
|
|||
Seu modelo de resposta pode ter valores padrão, como: |
|||
|
|||
{* ../../docs_src/response_model/tutorial004_py310.py hl[9,11:12] *} |
|||
|
|||
* `description: Union[str, None] = None` (ou `str | None = None` no Python 3.10) tem um padrão de `None`. |
|||
* `tax: float = 10.5` tem um padrão de `10.5`. |
|||
* `tags: List[str] = []` tem um padrão de uma lista vazia: `[]`. |
|||
|
|||
mas você pode querer omiti-los do resultado se eles não foram realmente armazenados. |
|||
|
|||
Por exemplo, se você tem modelos com muitos atributos opcionais em um banco de dados NoSQL, mas não quer enviar respostas JSON muito longas cheias de valores padrão. |
|||
|
|||
### Usar o parâmetro `response_model_exclude_unset` |
|||
|
|||
Você pode definir o parâmetro `response_model_exclude_unset=True` do *decorador de operação de rota* : |
|||
|
|||
{* ../../docs_src/response_model/tutorial004_py310.py hl[22] *} |
|||
|
|||
e esses valores padrão não serão incluídos na resposta, apenas os valores realmente definidos. |
|||
|
|||
Então, se você enviar uma solicitação para essa *operação de rota* para o item com ID `foo`, a resposta (sem incluir valores padrão) será: |
|||
|
|||
```JSON |
|||
{ |
|||
"name": "Foo", |
|||
"price": 50.2 |
|||
} |
|||
``` |
|||
|
|||
/// info | Informação |
|||
|
|||
No Pydantic v1, o método era chamado `.dict()`, ele foi descontinuado (mas ainda suportado) no Pydantic v2 e renomeado para `.model_dump()`. |
|||
|
|||
Os exemplos aqui usam `.dict()` para compatibilidade com Pydantic v1, mas você deve usar `.model_dump()` em vez disso se puder usar Pydantic v2. |
|||
|
|||
/// |
|||
|
|||
/// info | Informação |
|||
|
|||
O FastAPI usa `.dict()` do modelo Pydantic com <a href="https://docs.pydantic.dev/1.10/usage/exporting_models/#modeldict" class="external-link" target="_blank">seu parâmetro `exclude_unset`</a> para chegar a isso. |
|||
|
|||
/// |
|||
|
|||
/// info | Informação |
|||
|
|||
Você também pode usar: |
|||
|
|||
* `response_model_exclude_defaults=True` |
|||
* `response_model_exclude_none=True` |
|||
|
|||
conforme descrito na <a href="https://docs.pydantic.dev/1.10/usage/exporting_models/#modeldict" class="external-link" target="_blank">documentação do Pydantic</a> para `exclude_defaults` e `exclude_none`. |
|||
|
|||
/// |
|||
|
|||
#### Dados com valores para campos com padrões |
|||
|
|||
Mas se seus dados tiverem valores para os campos do modelo com valores padrões, como o item com ID `bar`: |
|||
|
|||
```Python hl_lines="3 5" |
|||
{ |
|||
"name": "Bar", |
|||
"description": "The bartenders", |
|||
"price": 62, |
|||
"tax": 20.2 |
|||
} |
|||
``` |
|||
|
|||
eles serão incluídos na resposta. |
|||
|
|||
#### Dados com os mesmos valores que os padrões |
|||
|
|||
Se os dados tiverem os mesmos valores que os padrões, como o item com ID `baz`: |
|||
|
|||
```Python hl_lines="3 5-6" |
|||
{ |
|||
"name": "Baz", |
|||
"description": None, |
|||
"price": 50.2, |
|||
"tax": 10.5, |
|||
"tags": [] |
|||
} |
|||
``` |
|||
|
|||
O FastAPI é inteligente o suficiente (na verdade, o Pydantic é inteligente o suficiente) para perceber que, embora `description`, `tax` e `tags` tenham os mesmos valores que os padrões, eles foram definidos explicitamente (em vez de retirados dos padrões). |
|||
|
|||
Portanto, eles serão incluídos na resposta JSON. |
|||
|
|||
/// tip | Dica |
|||
|
|||
Observe que os valores padrão podem ser qualquer coisa, não apenas `None`. |
|||
|
|||
Eles podem ser uma lista (`[]`), um `float` de `10.5`, etc. |
|||
|
|||
/// |
|||
|
|||
### `response_model_include` e `response_model_exclude` |
|||
|
|||
Você também pode usar os parâmetros `response_model_include` e `response_model_exclude` do *decorador de operação de rota*. |
|||
|
|||
Eles pegam um `set` de `str` com o nome dos atributos para incluir (omitindo o resto) ou para excluir (incluindo o resto). |
|||
|
|||
Isso pode ser usado como um atalho rápido se você tiver apenas um modelo Pydantic e quiser remover alguns dados da saída. |
|||
|
|||
/// tip | Dica |
|||
|
|||
Mas ainda é recomendado usar as ideias acima, usando várias classes, em vez desses parâmetros. |
|||
|
|||
Isso ocorre porque o Schema JSON gerado no OpenAPI do seu aplicativo (e a documentação) ainda será o único para o modelo completo, mesmo que você use `response_model_include` ou `response_model_exclude` para omitir alguns atributos. |
|||
|
|||
Isso também se aplica ao `response_model_by_alias` que funciona de forma semelhante. |
|||
|
|||
/// |
|||
|
|||
{* ../../docs_src/response_model/tutorial005_py310.py hl[29,35] *} |
|||
|
|||
/// tip | Dica |
|||
|
|||
A sintaxe `{"nome", "descrição"}` cria um `conjunto` com esses dois valores. |
|||
|
|||
É equivalente a `set(["nome", "descrição"])`. |
|||
|
|||
/// |
|||
|
|||
#### Usando `list`s em vez de `set`s |
|||
|
|||
Se você esquecer de usar um `set` e usar uma `lista` ou `tupla` em vez disso, o FastAPI ainda o converterá em um `set` e funcionará corretamente: |
|||
|
|||
{* ../../docs_src/response_model/tutorial006_py310.py hl[29,35] *} |
|||
|
|||
## Recapitulação |
|||
|
|||
Use o parâmetro `response_model` do *decorador de operação de rota* para definir modelos de resposta e, especialmente, para garantir que dados privados sejam filtrados. |
|||
|
|||
Use `response_model_exclude_unset` para retornar apenas os valores definidos explicitamente. |
@ -0,0 +1,75 @@ |
|||
# FastAPI CLI |
|||
|
|||
**FastAPI CLI** это программа командной строки, которую вы можете использовать для запуска вашего FastAPI приложения, для управления FastAPI-проектом, а также для многих других вещей. |
|||
|
|||
`fastapi-cli` устанавливается вместе со стандартным пакетом FastAPI (при запуске команды `pip install "fastapi[standard]"`). Данный пакет предоставляет доступ к программе `fastapi` через терминал. |
|||
|
|||
Чтобы запустить приложение FastAPI в режиме разработки, вы можете использовать команду `fastapi dev`: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ <font color="#4E9A06">fastapi</font> dev <u style="text-decoration-style:solid">main.py</u> |
|||
|
|||
<span style="background-color:#009485"><font color="#D3D7CF"> FastAPI </font></span> Starting development server 🚀 |
|||
|
|||
Searching for package file structure from directories with |
|||
<font color="#3465A4">__init__.py</font> files |
|||
Importing from <font color="#75507B">/home/user/code/</font><font color="#AD7FA8">awesomeapp</font> |
|||
|
|||
<span style="background-color:#007166"><font color="#D3D7CF"> module </font></span> 🐍 main.py |
|||
|
|||
<span style="background-color:#007166"><font color="#D3D7CF"> code </font></span> Importing the FastAPI app object from the module with the |
|||
following code: |
|||
|
|||
<u style="text-decoration-style:solid">from </u><u style="text-decoration-style:solid"><b>main</b></u><u style="text-decoration-style:solid"> import </u><u style="text-decoration-style:solid"><b>app</b></u> |
|||
|
|||
<span style="background-color:#007166"><font color="#D3D7CF"> app </font></span> Using import string: <font color="#3465A4">main:app</font> |
|||
|
|||
<span style="background-color:#007166"><font color="#D3D7CF"> server </font></span> Server started at <font color="#729FCF"><u style="text-decoration-style:solid">http://127.0.0.1:8000</u></font> |
|||
<span style="background-color:#007166"><font color="#D3D7CF"> server </font></span> Documentation at <font color="#729FCF"><u style="text-decoration-style:solid">http://127.0.0.1:8000/docs</u></font> |
|||
|
|||
<span style="background-color:#007166"><font color="#D3D7CF"> tip </font></span> Running in development mode, for production use: |
|||
<b>fastapi run</b> |
|||
|
|||
Logs: |
|||
|
|||
<span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Will watch for changes in these directories: |
|||
<b>[</b><font color="#4E9A06">'/home/user/code/awesomeapp'</font><b>]</b> |
|||
<span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Uvicorn running on <font color="#729FCF"><u style="text-decoration-style:solid">http://127.0.0.1:8000</u></font> <b>(</b>Press CTRL+C to |
|||
quit<b>)</b> |
|||
<span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Started reloader process <b>[</b><font color="#34E2E2"><b>383138</b></font><b>]</b> using WatchFiles |
|||
<span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Started server process <b>[</b><font color="#34E2E2"><b>383153</b></font><b>]</b> |
|||
<span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Waiting for application startup. |
|||
<span style="background-color:#007166"><font color="#D3D7CF"> INFO </font></span> Application startup complete. |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
Приложение командной строки `fastapi` это и есть **FastAPI CLI**. |
|||
|
|||
FastAPI CLI берет путь к вашей Python-программе (напр. `main.py`) и автоматически находит объект `FastAPI` (обычно это `app`), затем определяет правильный процесс импорта и запускает сервер приложения. |
|||
|
|||
Для работы в production окружении вместо `fastapi dev` нужно использовать `fastapi run`. 🚀 |
|||
|
|||
Внутри **FastAPI CLI** используется <a href="https://www.uvicorn.org" class="external-link" target="_blank">Uvicorn</a>, высокопроизводительный, готовый к работе в production сервер ASGI. 😎 |
|||
|
|||
## `fastapi dev` |
|||
|
|||
Вызов `fastapi dev` запускает режим разработки. |
|||
|
|||
По умолчанию включена автоматическая перезагрузка (**auto-reload**), благодаря этому при изменении кода происходит перезагрузка сервера приложения. Эта установка требует значительных ресурсов и делает систему менее стабильной. Используйте её только при разработке. Приложение слушает входящие подключения на IP `127.0.0.1`. Это IP адрес вашей машины, предназначенный для внутренних коммуникаций (`localhost`). |
|||
|
|||
## `fastapi run` |
|||
|
|||
Вызов `fastapi run` по умолчанию запускает FastAPI в режиме production. |
|||
|
|||
По умолчанию функция перезагрузки **auto-reload** отключена. Приложение слушает входящие подключения на IP `0.0.0.0`, т.е. на всех доступных адресах компьютера. Таким образом, приложение будет находиться в публичном доступе для любого, кто может подсоединиться к вашей машине. Продуктовые приложения запускаются именно так, например, с помощью контейнеров. |
|||
|
|||
В большинстве случаев вы будете (и должны) использовать прокси-сервер ("termination proxy"), который будет поддерживать HTTPS поверх вашего приложения. Всё будет зависеть от того, как вы развертываете приложение: за вас это либо сделает ваш провайдер, либо вам придется сделать настройки самостоятельно. |
|||
|
|||
/// tip | Подсказка |
|||
|
|||
Вы можете больше узнать об этом в документации по развертыванию приложений [deployment documentation](deployment/index.md){.internal-link target=_blank}. |
|||
|
|||
/// |
@ -0,0 +1,68 @@ |
|||
# Модели Query-Параметров |
|||
|
|||
Если у вас есть группа связанных **query-параметров**, то вы можете объединить их в одну **Pydantic-модель**. |
|||
|
|||
Это позволит вам **переиспользовать модель** в **разных местах**, устанавливать валидаторы и метаданные, в том числе для сразу всех параметров, в одном месте. 😎 |
|||
|
|||
/// note | Заметка |
|||
|
|||
Этот функционал доступен с версии `0.115.0`. 🤓 |
|||
|
|||
/// |
|||
|
|||
## Pydantic-Модель для Query-Параметров |
|||
|
|||
Объявите нужные **query-параметры** в **Pydantic-модели**, а после аннотируйте параметр как `Query`: |
|||
|
|||
{* ../../docs_src/query_param_models/tutorial001_an_py310.py hl[9:13,17] *} |
|||
|
|||
**FastAPI извлечёт** данные соответствующие **каждому полю модели** из **query-параметров** запроса и выдаст вам объявленную Pydantic-модель заполненную ими. |
|||
|
|||
## Проверьте Сгенерированную Документацию |
|||
|
|||
Вы можете посмотреть query-параметры в графическом интерфейсе сгенерированной документации по пути `/docs`: |
|||
|
|||
<div class="screenshot"> |
|||
<img src="/img/tutorial/query-param-models/image01.png"> |
|||
</div> |
|||
|
|||
## Запретить Дополнительные Query-Параметры |
|||
|
|||
В некоторых случаях (не особо часто встречающихся) вам может понадобиться **ограничить** query-параметры, которые вы хотите получить. |
|||
|
|||
Вы можете сконфигурировать Pydantic-модель так, чтобы запретить (`forbid`) все дополнительные (`extra`) поля. |
|||
|
|||
{* ../../docs_src/query_param_models/tutorial002_an_py310.py hl[10] *} |
|||
|
|||
Если клиент попробует отправить **дополнительные** данные в **query-параметрах**, то в ответ он получит **ошибку**. |
|||
|
|||
Например, если клиент попытается отправить query-параметр `tool` с значением `plumbus`, в виде: |
|||
|
|||
```http |
|||
https://example.com/items/?limit=10&tool=plumbus |
|||
``` |
|||
|
|||
То в ответ он получит **ошибку**, сообщающую ему, что query-параметр `tool` не разрешен: |
|||
|
|||
```json |
|||
{ |
|||
"detail": [ |
|||
{ |
|||
"type": "extra_forbidden", |
|||
"loc": ["query", "tool"], |
|||
"msg": "Extra inputs are not permitted", |
|||
"input": "plumbus" |
|||
} |
|||
] |
|||
} |
|||
``` |
|||
|
|||
## Заключение |
|||
|
|||
Вы можете использовать **Pydantic-модели** для объявления **query-параметров** в **FastAPI**. 😎 |
|||
|
|||
/// tip | Совет |
|||
|
|||
Спойлер: вы также можете использовать Pydantic-модели для группировки кук (cookies) и заголовков (headers), но об этом вы прочитаете позже. 🤫 |
|||
|
|||
/// |
@ -0,0 +1,99 @@ |
|||
# Данные текущего пользователя |
|||
|
|||
В предыдущей главе система безопасности (основанная на системе внедрения зависимостей) передавала *функции, обрабатывающей эндпоинт,* `токен` в виде `строки`: |
|||
|
|||
{* ../../docs_src/security/tutorial001_an_py39.py hl[12] *} |
|||
|
|||
Это пока что не слишком нам полезно. Давайте изменим код так, чтобы он возвращал нам данные пользователя, отправившего запрос. |
|||
|
|||
## Создание модели пользователя |
|||
|
|||
Сначала создадим Pydantic-модель пользователя. |
|||
|
|||
Точно так же, как мы использовали Pydantic для объявления тел запросов, мы можем использовать его где угодно: |
|||
|
|||
{* ../../docs_src/security/tutorial002_an_py310.py hl[5,12:6] *} |
|||
|
|||
## Создание зависимости `get_current_user` |
|||
|
|||
Давайте создадим зависимость `get_current_user`. |
|||
|
|||
Помните, что у зависимостей могут быть подзависимости? |
|||
|
|||
`get_current_user` как раз будет иметь подзависимость `oauth2_scheme`, которую мы создали ранее. |
|||
|
|||
Аналогично тому, как мы делали это ранее в *обработчике эндпоинта* наша новая зависимость `get_current_user` будет получать `token` в виде `строки` от подзависимости `oauth2_scheme`: |
|||
|
|||
{* ../../docs_src/security/tutorial002_an_py310.py hl[25] *} |
|||
|
|||
## Получение данных пользователя |
|||
|
|||
`get_current_user` будет использовать созданную нами (ненастоящую) служебную функцию, которая принимает токен в виде `строки` и возвращает нашу Pydantic-модель `User`: |
|||
|
|||
{* ../../docs_src/security/tutorial002_an_py310.py hl[19:22,26:27] *} |
|||
|
|||
## Внедрение зависимости текущего пользователя |
|||
|
|||
Теперь мы можем использовать тот же `Depends` с нашей зависимостью `get_current_user` в *функции обрабатывающей эндпоинт*: |
|||
|
|||
{* ../../docs_src/security/tutorial002_an_py310.py hl[31] *} |
|||
|
|||
Обратите внимание, что мы объявляем тип переменной `current_user` как Pydantic-модель `User`. |
|||
|
|||
Это поможет выполнить внутри функции все проверки автозаполнения и типа. |
|||
|
|||
/// tip | Подсказка |
|||
Возможно, вы помните, что тело запроса также объявляется с помощью Pydantic-моделей. |
|||
|
|||
В этом месте у **FastAPI** не возникнет проблем, потому что вы используете `Depends`. |
|||
/// |
|||
|
|||
/// check | Заметка |
|||
То, как устроена эта система зависимостей, позволяет нам иметь различные зависимости, которые возвращают модель `User`. |
|||
|
|||
Мы не ограничены наличием только одной зависимости, которая может возвращать данные такого типа. |
|||
/// |
|||
|
|||
## Другие модели |
|||
|
|||
Теперь вы можете получать информацию о текущем пользователе непосредственно в *функции обрабатывающей эндпоинт* и работать с механизмами безопасности на уровне **Внедрения Зависимостей**, используя `Depends`. |
|||
|
|||
Причем для обеспечения требований безопасности можно использовать любую модель или данные (в данном случае - Pydantic-модель `User`). |
|||
|
|||
Но вы не ограничены использованием какой-то конкретной моделью данных, классом или типом. |
|||
|
|||
Вы хотите использовать в модели `id` и `email`, а `username` вам не нужен? Ну разумеется. Воспользуйтесь тем же инструментарием. |
|||
|
|||
Вам нужны только `строки`? Или только `словари`? Или непосредственно экземпляр модели класса базы данных? Все это работает точно также. |
|||
|
|||
У вас нет пользователей, которые входят в ваше приложение, а только роботы, боты или другие системы, у которых есть только токен доступа? Опять же, все работает одинаково. |
|||
|
|||
Просто используйте любую модель, любой класс, любую базу данных, которые нужны для вашего приложения. Система внедрения зависимостей **FastAPI** поможет вам в этом. |
|||
|
|||
## Размер кода |
|||
|
|||
Этот пример может показаться многословным. Следует иметь в виду, что в одном файле мы смешиваем безопасность, модели данных, служебные функции и *эндпоинты*. |
|||
|
|||
Но вот ключевой момент: |
|||
|
|||
Все, что касается безопасности и внедрения зависимостей, пишется один раз. |
|||
|
|||
И вы можете сделать его настолько сложным, насколько захотите. И все это будет написано только один раз, в одном месте, со всей своей гибкостью. |
|||
|
|||
И у вас могут быть тысячи конечных точек (*эндпоинтов*), использующих одну и ту же систему безопасности. |
|||
|
|||
И все они (или любая их часть по вашему желанию) могут воспользоваться преимуществами повторного использования этих зависимостей или любых других зависимостей, которые вы создадите. |
|||
|
|||
И все эти тысячи *эндпоинтов* могут составлять всего 3 строки: |
|||
|
|||
{* ../../docs_src/security/tutorial002_an_py310.py hl[30:32] *} |
|||
|
|||
## Резюме |
|||
|
|||
Теперь вы можете получать данные о текущем пользователе непосредственно в своей *функции обработчике эндпоинта*. |
|||
|
|||
Мы уже на полпути к этому. |
|||
|
|||
Осталось лишь добавить *эндпоинт* для отправки пользователем/клиентом своих `имени пользователя` и `пароля`. |
|||
|
|||
Это будет рассмотрено в следующем разделе. |
@ -0,0 +1,261 @@ |
|||
# OAuth2 с паролем (и хешированием), Bearer с JWT-токенами |
|||
|
|||
Теперь, когда у нас определен процесс обеспечения безопасности, давайте сделаем приложение действительно безопасным, используя токены <abbr title="JSON Web Tokens">JWT</abbr> и безопасное хеширование паролей. |
|||
|
|||
Этот код можно реально использовать в своем приложении, сохранять хэши паролей в базе данных и т.д. |
|||
|
|||
Мы продолжим разбираться, начиная с того места, на котором остановились в предыдущей главе. |
|||
|
|||
## Про JWT |
|||
|
|||
JWT означает "JSON Web Tokens". |
|||
|
|||
Это стандарт для кодирования JSON-объекта в виде длинной строки без пробелов. Выглядит это следующим образом: |
|||
|
|||
``` |
|||
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c |
|||
``` |
|||
|
|||
Он не зашифрован, поэтому любой человек может восстановить информацию из его содержимого. |
|||
|
|||
Но он подписан. Следовательно, когда вы получаете токен, который вы эмитировали (выдавали), вы можете убедиться, что это именно вы его эмитировали. |
|||
|
|||
Таким образом, можно создать токен со сроком действия, скажем, 1 неделя. А когда пользователь вернется на следующий день с тем же токеном, вы будете знать, что он все еще авторизирован в вашей системе. |
|||
|
|||
Через неделю срок действия токена истечет, пользователь не будет авторизован и ему придется заново входить в систему, чтобы получить новый токен. А если пользователь (или третье лицо) попытается модифицировать токен, чтобы изменить срок действия, вы сможете это обнаружить, поскольку подписи не будут совпадать. |
|||
|
|||
Если вы хотите поиграть с JWT-токенами и посмотреть, как они работают, посмотрите <a href="https://jwt.io/" class="external-link" target="_blank">https://jwt.io</a>. |
|||
|
|||
## Установка `PyJWT` |
|||
|
|||
Нам необходимо установить `pyjwt` для генерации и проверки JWT-токенов на языке Python. |
|||
|
|||
Убедитесь, что вы создали [виртуальное окружение](../../virtual-environments.md){.internal-link target=_blank}, активируйте его, а затем установите `pyjwt`: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ pip install pyjwt |
|||
|
|||
---> 100% |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
/// info | Дополнительная информация |
|||
Если вы планируете использовать алгоритмы цифровой подписи, такие как RSA или ECDSA, вам следует установить зависимость библиотеки криптографии `pyjwt[crypto]`. |
|||
|
|||
Подробнее об этом можно прочитать в <a href=«https://pyjwt.readthedocs.io/en/latest/installation.html» class=«external-link» target=«_blank»>документации по установке PyJWT</a>. |
|||
/// |
|||
|
|||
## Хеширование паролей |
|||
|
|||
"Хеширование" означает преобразование некоторого содержимого (в данном случае пароля) в последовательность байтов (просто строку), которая выглядит как тарабарщина. |
|||
|
|||
Каждый раз, когда вы передаете точно такое же содержимое (точно такой же пароль), вы получаете точно такую же тарабарщину. |
|||
|
|||
Но преобразовать тарабарщину обратно в пароль невозможно. |
|||
|
|||
### Для чего нужно хеширование паролей |
|||
|
|||
Если ваша база данных будет украдена, то вор не получит пароли пользователей в открытом виде, а только их хэши. |
|||
|
|||
Таким образом, вор не сможет использовать этот пароль в другой системе (поскольку многие пользователи везде используют один и тот же пароль, это было бы опасно). |
|||
|
|||
## Установка `passlib` |
|||
|
|||
PassLib - это отличный пакет Python для работы с хэшами паролей. |
|||
|
|||
Он поддерживает множество безопасных алгоритмов хеширования и утилит для работы с ними. |
|||
|
|||
Рекомендуемый алгоритм - "Bcrypt". |
|||
|
|||
Убедитесь, что вы создали и активировали виртуальное окружение, и затем установите PassLib вместе с Bcrypt: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ pip install "passlib[bcrypt]" |
|||
|
|||
---> 100% |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
/// tip | Подсказка |
|||
С помощью `passlib` можно даже настроить его на чтение паролей, созданных **Django**, плагином безопасности **Flask** или многими другими библиотеками. |
|||
|
|||
Таким образом, вы сможете, например, совместно использовать одни и те же данные из приложения Django в базе данных с приложением FastAPI. Или постепенно мигрировать Django-приложение, используя ту же базу данных. |
|||
|
|||
При этом пользователи смогут одновременно входить в систему как из приложения Django, так и из приложения **FastAPI**. |
|||
/// |
|||
|
|||
## Хеширование и проверка паролей |
|||
|
|||
Импортируйте необходимые инструменты из `passlib`. |
|||
|
|||
Создайте "контекст" PassLib. Именно он будет использоваться для хэширования и проверки паролей. |
|||
|
|||
/// tip | Подсказка |
|||
Контекст PassLib также имеет функциональность для использования различных алгоритмов хеширования, в том числе и устаревших, только для возможности их проверки и т.д. |
|||
|
|||
Например, вы можете использовать его для чтения и проверки паролей, сгенерированных другой системой (например, Django), но хэшировать все новые пароли другим алгоритмом, например Bcrypt. |
|||
|
|||
И при этом быть совместимым со всеми этими системами. |
|||
/// |
|||
|
|||
Создайте служебную функцию для хэширования пароля, поступающего от пользователя. |
|||
|
|||
А затем создайте другую - для проверки соответствия полученного пароля и хранимого хэша. |
|||
|
|||
И еще одну - для аутентификации и возврата пользователя. |
|||
|
|||
{* ../../docs_src/security/tutorial004_an_py310.py hl[8,49,56:57,60:61,70:76] *} |
|||
|
|||
/// note | Технические детали |
|||
Если проверить новую (фальшивую) базу данных `fake_users_db`, то можно увидеть, как теперь выглядит хэшированный пароль: `"$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW"`. |
|||
/// |
|||
|
|||
## Работа с JWT токенами |
|||
|
|||
Импортируйте установленные модули. |
|||
|
|||
Создайте случайный секретный ключ, который будет использоваться для подписи JWT-токенов. |
|||
|
|||
Для генерации безопасного случайного секретного ключа используйте команду: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ openssl rand -hex 32 |
|||
|
|||
09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7 |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
И скопируйте полученный результат в переменную `SECRET_KEY` (не используйте тот, что в примере). |
|||
|
|||
Создайте переменную `ALGORITHM` с алгоритмом, используемым для подписи JWT-токена, и установите для нее значение `"HS256"`. |
|||
|
|||
Создайте переменную для срока действия токена. |
|||
|
|||
Определите Pydantic Model, которая будет использоваться для формирования ответа на запрос на получение токена. |
|||
|
|||
Создайте служебную функцию для генерации нового токена доступа. |
|||
|
|||
{* ../../docs_src/security/tutorial004_an_py310.py hl[4,7,13:15,29:31,79:87] *} |
|||
|
|||
## Обновление зависимостей |
|||
|
|||
Обновите `get_current_user` для получения того же токена, что и раньше, но на этот раз с использованием JWT-токенов. |
|||
|
|||
Декодируйте полученный токен, проверьте его и верните текущего пользователя. |
|||
|
|||
Если токен недействителен, то сразу же верните HTTP-ошибку. |
|||
|
|||
{* ../../docs_src/security/tutorial004_an_py310.py hl[90:107] *} |
|||
|
|||
## Обновление *операции пути* `/token` |
|||
|
|||
Создайте `timedelta` со временем истечения срока действия токена. |
|||
|
|||
Создайте реальный токен доступа JWT и верните его |
|||
|
|||
{* ../../docs_src/security/tutorial004_an_py310.py hl[118:133] *} |
|||
|
|||
### Технические подробности о JWT ключе `sub` |
|||
|
|||
В спецификации JWT говорится, что существует ключ `sub`, содержащий субъект токена. |
|||
|
|||
Его использование необязательно, но это именно то место, куда вы должны поместить идентификатор пользователя, и поэтому мы здесь его и используем. |
|||
|
|||
JWT может использоваться и для других целей, помимо идентификации пользователя и предоставления ему возможности выполнять операции непосредственно в вашем API. |
|||
|
|||
Например, вы могли бы определить "автомобиль" или "запись в блоге". |
|||
|
|||
Затем вы могли бы добавить права доступа к этой сущности, например "управлять" (для автомобиля) или "редактировать" (для блога). |
|||
|
|||
Затем вы могли бы передать этот JWT-токен пользователю (или боту), и они использовали бы его для выполнения определенных действий (управление автомобилем или редактирование запись в блоге), даже не имея учетной записи, просто используя JWT-токен, сгенерированный вашим API. |
|||
|
|||
Используя эти идеи, JWT можно применять для гораздо более сложных сценариев. |
|||
|
|||
В отдельных случаях несколько сущностей могут иметь один и тот же идентификатор, скажем, `foo` (пользователь `foo`, автомобиль `foo` и запись в блоге `foo`). |
|||
|
|||
Поэтому, чтобы избежать коллизий идентификаторов, при создании JWT-токена для пользователя можно добавить префикс `username` к значению ключа `sub`. Таким образом, в данном примере значение `sub` было бы `username:johndoe`. |
|||
|
|||
Важно помнить, что ключ `sub` должен иметь уникальный идентификатор для всего приложения и представлять собой строку. |
|||
|
|||
## Проверка в действии |
|||
|
|||
Запустите сервер и перейдите к документации: <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>. |
|||
|
|||
Вы увидите пользовательский интерфейс вида: |
|||
|
|||
<img src="/img/tutorial/security/image07.png"> |
|||
|
|||
Пройдите авторизацию так же, как делали раньше. |
|||
|
|||
Используя учетные данные пользователя: |
|||
|
|||
Username: `johndoe` |
|||
Password: `secret` |
|||
|
|||
/// check | Заметка |
|||
Обратите внимание, что нигде в коде не используется открытый текст пароля "`secret`", мы используем только его хэшированную версию. |
|||
/// |
|||
|
|||
<img src="/img/tutorial/security/image08.png"> |
|||
|
|||
Вызвав эндпоинт `/users/me/`, вы получите ответ в виде: |
|||
|
|||
```JSON |
|||
{ |
|||
"username": "johndoe", |
|||
"email": "[email protected]", |
|||
"full_name": "John Doe", |
|||
"disabled": false |
|||
} |
|||
``` |
|||
|
|||
<img src="/img/tutorial/security/image09.png"> |
|||
|
|||
Если открыть инструменты разработчика, то можно увидеть, что передаваемые данные включают только токен, пароль передается только в первом запросе для аутентификации пользователя и получения токена доступа, но не в последующих: |
|||
|
|||
<img src="/img/tutorial/security/image10.png"> |
|||
|
|||
/// note | Техническая информация |
|||
Обратите внимание на заголовок `Authorization`, значение которого начинается с `Bearer`. |
|||
/// |
|||
|
|||
## Продвинутое использование `scopes` |
|||
|
|||
В OAuth2 существует понятие "диапазоны" ("`scopes`"). |
|||
|
|||
С их помощью можно добавить определенный набор разрешений к JWT-токену. |
|||
|
|||
Затем вы можете передать этот токен непосредственно пользователю или третьей стороне для взаимодействия с вашим API с определенным набором ограничений. |
|||
|
|||
О том, как их использовать и как они интегрированы в **FastAPI**, читайте далее в **Руководстве пользователя**. |
|||
|
|||
## Резюме |
|||
|
|||
С учетом того, что вы видели до сих пор, вы можете создать безопасное приложение **FastAPI**, используя такие стандарты, как OAuth2 и JWT. |
|||
|
|||
Практически в любом фреймворке работа с безопасностью довольно быстро превращается в сложную тему. |
|||
|
|||
Многие пакеты, сильно упрощающие эту задачу, вынуждены идти на многочисленные компромиссы с моделью данных, с базой данных и с доступным функционалом. Некоторые из этих пакетов, которые пытаются уж слишком все упростить, имеют даже "дыры" в системе безопасности. |
|||
|
|||
--- |
|||
|
|||
**FastAPI** не делает уступок ни одной базе данных, модели данных или инструментарию. |
|||
|
|||
Он предоставляет вам полную свободу действий, позволяя выбирать то, что лучше всего подходит для вашего проекта. |
|||
|
|||
Вы можете напрямую использовать многие хорошо поддерживаемые и широко распространенные пакеты, такие как `passlib` и `PyJWT`, поскольку **FastAPI** не требует сложных механизмов для интеграции внешних пакетов. |
|||
|
|||
Напротив, он предоставляет инструменты, позволяющие максимально упростить этот процесс без ущерба для гибкости, надежности и безопасности. |
|||
|
|||
При этом вы можете использовать и реализовывать безопасные стандартные протоколы, такие как OAuth2, относительно простым способом. |
|||
|
|||
В **Руководстве пользователя** вы можете узнать больше о том, как использовать "диапазоны" ("`scopes`") OAuth2 для создания более точно настроенной системы разрешений в соответствии с теми же стандартами. OAuth2 с диапазонами - это механизм, используемый многими крупными провайдерами сервиса аутентификации, такими как Facebook, Google, GitHub, Microsoft, Twitter и др., для авторизации сторонних приложений на взаимодействие с их API от имени их пользователей. |
@ -0,0 +1,272 @@ |
|||
# Простая авторизация по протоколу OAuth2 с токеном типа Bearer |
|||
|
|||
Теперь, отталкиваясь от предыдущей главы, добавим недостающие части, чтобы получить безопасную систему. |
|||
|
|||
## Получение `имени пользователя` и `пароля` |
|||
|
|||
Для получения `имени пользователя` и `пароля` мы будем использовать утилиты безопасности **FastAPI**. |
|||
|
|||
Протокол OAuth2 определяет, что при использовании "аутентификации по паролю" (которую мы и используем) клиент/пользователь должен передавать поля `username` и `password` в полях формы. |
|||
|
|||
В спецификации сказано, что поля должны быть названы именно так. Поэтому `user-name` или `email` работать не будут. |
|||
|
|||
Но не волнуйтесь, вы можете показать его конечным пользователям во фронтенде в том виде, в котором хотите. |
|||
|
|||
А ваши модели баз данных могут использовать любые другие имена. |
|||
|
|||
Но при авторизации согласно спецификации, требуется использовать именно эти имена, что даст нам возможность воспользоваться встроенной системой документации API. |
|||
|
|||
В спецификации также указано, что `username` и `password` должны передаваться в виде данных формы (так что никакого JSON здесь нет). |
|||
|
|||
### Oбласть видимости (scope) |
|||
|
|||
В спецификации также говорится, что клиент может передать еще одно поле формы "`scope`". |
|||
|
|||
Имя поля формы - `scope` (в единственном числе), но на самом деле это длинная строка, состоящая из отдельных областей видимости (scopes), разделенных пробелами. |
|||
|
|||
Каждая "область видимости" (scope) - это просто строка (без пробелов). |
|||
|
|||
Обычно они используются для указания уровней доступа, например: |
|||
|
|||
* `users:read` или `users:write` являются распространенными примерами. |
|||
* `instagram_basic` используется Facebook / Instagram. |
|||
* `https://www.googleapis.com/auth/drive` используется компанией Google. |
|||
|
|||
/// info | Дополнительнаяя информация |
|||
В OAuth2 "scope" - это просто строка, которая уточняет уровень доступа. |
|||
|
|||
Не имеет значения, содержит ли он другие символы, например `:`, или является ли он URL. |
|||
|
|||
Эти детали зависят от конкретной реализации. |
|||
|
|||
Для OAuth2 это просто строки. |
|||
/// |
|||
|
|||
## Код получения `имени пользователя` и `пароля` |
|||
|
|||
Для решения задачи давайте воспользуемся утилитами, предоставляемыми **FastAPI**. |
|||
|
|||
### `OAuth2PasswordRequestForm` |
|||
|
|||
Сначала импортируйте `OAuth2PasswordRequestForm` и затем используйте ее как зависимость с `Depends` в *эндпоинте* `/token`: |
|||
|
|||
|
|||
{* ../../docs_src/security/tutorial003_an_py310.py hl[4,78] *} |
|||
|
|||
`OAuth2PasswordRequestForm` - это класс для использования в качестве зависимости для *функции обрабатывающей эндпоинт*, который определяет тело формы со следующими полями: |
|||
|
|||
* `username`. |
|||
* `password`. |
|||
* Необязательное поле `scope` в виде большой строки, состоящей из строк, разделенных пробелами. |
|||
* Необязательное поле `grant_type`. |
|||
|
|||
/// tip | Подсказка |
|||
По спецификации OAuth2 поле `grant_type` является обязательным и содержит фиксированное значение `password`, но `OAuth2PasswordRequestForm` не обеспечивает этого. |
|||
|
|||
Если вам необходимо использовать `grant_type`, воспользуйтесь `OAuth2PasswordRequestFormStrict` вместо `OAuth2PasswordRequestForm`. |
|||
/// |
|||
|
|||
* Необязательное поле `client_id` (в нашем примере он не нужен). |
|||
* Необязательное поле `client_secret` (в нашем примере он не нужен). |
|||
|
|||
/// info | Дополнительная информация |
|||
Форма `OAuth2PasswordRequestForm` не является специальным классом для **FastAPI**, как `OAuth2PasswordBearer`. |
|||
|
|||
`OAuth2PasswordBearer` указывает **FastAPI**, что это схема безопасности. Следовательно, она будет добавлена в OpenAPI. |
|||
|
|||
Но `OAuth2PasswordRequestForm` - это всего лишь класс зависимости, который вы могли бы написать самостоятельно или вы могли бы объявить параметры `Form` напрямую. |
|||
|
|||
Но, поскольку это распространённый вариант использования, он предоставляется **FastAPI** напрямую, просто чтобы облегчить задачу. |
|||
/// |
|||
|
|||
### Использование данных формы |
|||
|
|||
/// tip | Подсказка |
|||
В экземпляре зависимого класса `OAuth2PasswordRequestForm` атрибут `scope`, состоящий из одной длинной строки, разделенной пробелами, заменен на атрибут `scopes`, состоящий из списка отдельных строк, каждая из которых соответствует определенному уровню доступа. |
|||
|
|||
В данном примере мы не используем `scopes`, но если вам это необходимо, то такая функциональность имеется. |
|||
/// |
|||
|
|||
Теперь получим данные о пользователе из (ненастоящей) базы данных, используя `username` из поля формы. |
|||
|
|||
Если такого пользователя нет, то мы возвращаем ошибку "неверное имя пользователя или пароль". |
|||
|
|||
Для ошибки мы используем исключение `HTTPException`: |
|||
|
|||
{* ../../docs_src/security/tutorial003_an_py310.py hl[3,79:81] *} |
|||
|
|||
### Проверка пароля |
|||
|
|||
На данный момент у нас есть данные о пользователе из нашей базы данных, но мы еще не проверили пароль. |
|||
|
|||
Давайте сначала поместим эти данные в модель Pydantic `UserInDB`. |
|||
|
|||
Ни в коем случае нельзя сохранять пароли в открытом виде, поэтому мы будем использовать (пока что ненастоящую) систему хеширования паролей. |
|||
|
|||
Если пароли не совпадают, мы возвращаем ту же ошибку. |
|||
|
|||
#### Хеширование паролей |
|||
|
|||
"Хеширование" означает: преобразование некоторого содержимого (в данном случае пароля) в последовательность байтов (просто строку), которая выглядит как тарабарщина. |
|||
|
|||
Каждый раз, когда вы передаете точно такое же содержимое (точно такой же пароль), вы получаете точно такую же тарабарщину. |
|||
|
|||
Но преобразовать тарабарщину обратно в пароль невозможно. |
|||
|
|||
##### Зачем использовать хеширование паролей |
|||
|
|||
Если ваша база данных будет украдена, то у вора не будет паролей пользователей в открытом виде, только хэши. |
|||
|
|||
Таким образом, вор не сможет использовать эти же пароли в другой системе (поскольку многие пользователи используют одни и те же пароли повсеместно, это было бы опасно). |
|||
|
|||
{* ../../docs_src/security/tutorial003_an_py310.py hl[82:85] *} |
|||
|
|||
#### Про `**user_dict` |
|||
|
|||
`UserInDB(**user_dict)` означает: |
|||
|
|||
*Передавать ключи и значения `user_dict` непосредственно в качестве аргументов ключ-значение, что эквивалентно:* |
|||
|
|||
```Python |
|||
UserInDB( |
|||
username = user_dict["username"], |
|||
email = user_dict["email"], |
|||
full_name = user_dict["full_name"], |
|||
disabled = user_dict["disabled"], |
|||
hashed_password = user_dict["hashed_password"], |
|||
) |
|||
``` |
|||
|
|||
/// info | Дополнительная информация |
|||
Более полное объяснение `**user_dict` можно найти в [документации к **Дополнительным моделям**](../extra-models.md#about-user_indict){.internal-link target=_blank}. |
|||
/// |
|||
|
|||
## Возврат токена |
|||
|
|||
Ответ эндпоинта `token` должен представлять собой объект в формате JSON. |
|||
|
|||
Он должен иметь `token_type`. В нашем случае, поскольку мы используем токены типа "Bearer", тип токена должен быть "`bearer`". |
|||
|
|||
И в нем должна быть строка `access_token`, содержащая наш токен доступа. |
|||
|
|||
В этом простом примере мы нарушим все правила безопасности, и будем считать, что имя пользователя (username) полностью соответствует токену (token) |
|||
|
|||
/// tip | Подсказка |
|||
В следующей главе мы рассмотрим реальную защищенную реализацию с хешированием паролей и токенами <abbr title="JSON Web Tokens">JWT</abbr>. |
|||
|
|||
Но пока давайте остановимся на необходимых нам деталях. |
|||
/// |
|||
|
|||
{* ../../docs_src/security/tutorial003_an_py310.py hl[87] *} |
|||
|
|||
/// tip | Подсказка |
|||
Согласно спецификации, вы должны возвращать JSON с `access_token` и `token_type`, как в данном примере. |
|||
|
|||
Это то, что вы должны сделать сами в своем коде и убедиться, что вы используете эти JSON-ключи. |
|||
|
|||
Это практически единственное, что нужно не забывать делать самостоятельно, чтобы следовать требованиям спецификации. |
|||
|
|||
Все остальное за вас сделает **FastAPI**. |
|||
/// |
|||
|
|||
## Обновление зависимостей |
|||
|
|||
Теперь мы обновим наши зависимости. |
|||
|
|||
Мы хотим получить значение `current_user` *только* если этот пользователь активен. |
|||
|
|||
Поэтому мы создаем дополнительную зависимость `get_current_active_user`, которая, в свою очередь, использует в качестве зависимости `get_current_user`. |
|||
|
|||
Обе эти зависимости просто вернут HTTP-ошибку, если пользователь не существует или неактивен. |
|||
|
|||
Таким образом, в нашем эндпоинте мы получим пользователя только в том случае, если он существует, правильно аутентифицирован и активен: |
|||
|
|||
{* ../../docs_src/security/tutorial003_an_py310.py hl[58:66,69:74,94] *} |
|||
|
|||
/// info | Дополнительная информация |
|||
Дополнительный заголовок `WWW-Authenticate` со значением `Bearer`, который мы здесь возвращаем, также является частью спецификации. |
|||
|
|||
Ответ сервера с HTTP-кодом 401 "UNAUTHORIZED" должен также возвращать заголовок `WWW-Authenticate`. |
|||
|
|||
В случае с bearer-токенами (наш случай) значение этого заголовка должно быть `Bearer`. |
|||
|
|||
На самом деле этот дополнительный заголовок можно пропустить и все будет работать. |
|||
|
|||
Но он приведён здесь для соответствия спецификации. |
|||
|
|||
Кроме того, могут существовать инструменты, которые ожидают его и могут использовать, и это может быть полезно для вас или ваших пользователей сейчас или в будущем. |
|||
|
|||
В этом и заключается преимущество стандартов... |
|||
/// |
|||
|
|||
## Посмотим как это работает |
|||
|
|||
Откроем интерактивную документацию: <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>. |
|||
|
|||
### Аутентификация |
|||
|
|||
Нажмите кнопку "Авторизация". |
|||
|
|||
Используйте учётные данные: |
|||
|
|||
Пользователь: `johndoe` |
|||
|
|||
Пароль: `secret` |
|||
|
|||
<img src="/img/tutorial/security/image04.png"> |
|||
|
|||
После авторизации в системе вы увидите следующее: |
|||
|
|||
<img src="/img/tutorial/security/image05.png"> |
|||
|
|||
### Получение собственных пользовательских данных |
|||
|
|||
Теперь, используя операцию `GET` с путем `/users/me`, вы получите данные пользователя, например: |
|||
|
|||
```JSON |
|||
{ |
|||
"username": "johndoe", |
|||
"email": "[email protected]", |
|||
"full_name": "John Doe", |
|||
"disabled": false, |
|||
"hashed_password": "fakehashedsecret" |
|||
} |
|||
``` |
|||
|
|||
<img src="/img/tutorial/security/image06.png"> |
|||
|
|||
Если щелкнуть на значке замка и выйти из системы, а затем попытаться выполнить ту же операцию ещё раз, то будет выдана ошибка HTTP 401: |
|||
|
|||
```JSON |
|||
{ |
|||
"detail": "Not authenticated" |
|||
} |
|||
``` |
|||
|
|||
### Неактивный пользователь |
|||
|
|||
Теперь попробуйте пройти аутентификацию с неактивным пользователем: |
|||
|
|||
Пользователь: `alice` |
|||
|
|||
Пароль: `secret2` |
|||
|
|||
И попробуйте использовать операцию `GET` с путем `/users/me`. |
|||
|
|||
Вы получите ошибку "Inactive user", как тут: |
|||
|
|||
```JSON |
|||
{ |
|||
"detail": "Inactive user" |
|||
} |
|||
``` |
|||
|
|||
## Резюме |
|||
|
|||
Теперь у вас есть инструменты для реализации полноценной системы безопасности на основе `имени пользователя` и `пароля` для вашего API. |
|||
|
|||
Используя эти средства, можно сделать систему безопасности совместимой с любой базой данных, с любым пользователем или моделью данных. |
|||
|
|||
Единственным недостатком нашей системы является то, что она всё ещё не защищена. |
|||
|
|||
В следующей главе вы увидите, как использовать библиотеку безопасного хеширования паролей и токены <abbr title="JSON Web Tokens">JWT</abbr>. |
@ -0,0 +1,358 @@ |
|||
# SQL (реляционные) базы данных |
|||
|
|||
**FastAPI** не требует использования реляционной базы данных. Вы можете воспользоваться любой базой данных, которой хотите. |
|||
|
|||
В этом разделе мы продемонстрируем, как работать с <a href="https://sqlmodel.tiangolo.com/" class="external-link" target="_blank">SQLModel</a>. |
|||
|
|||
Библиотека **SQLModel** построена на основе <a href="https://www.sqlalchemy.org/" class="external-link" target="_blank">SQLAlchemy</a> и Pydantic. Она была разработана автором **FastAPI** специально для приложений на основе FastAPI, которые используют **реляционные базы данных**. |
|||
|
|||
/// tip | Подсказка |
|||
|
|||
Вы можете воспользоваться любой библиотекой для работы с реляционными (SQL) или нереляционными (NoSQL) базами данных. (Их ещё называют <abbr title="ORM = Object Relational Mapper, этот термин для библиотеки, в которой классы представляют SQL-таблицы, а экземпляры классов представляют строки в этих таблицах.">**ORM**</abbr> библиотеками). FastAPI не принуждает вас к использованию чего-либо конкретного. 😎 |
|||
|
|||
/// |
|||
|
|||
В основе SQLModel лежит SQLAlchemy, поэтому вы спокойно можете использовать любую базу данных, поддерживаемую SQLAlchemy (и, соответственно, поддерживаемую SQLModel), например: |
|||
|
|||
* PostgreSQL |
|||
* MySQL |
|||
* SQLite |
|||
* Oracle |
|||
* Microsoft SQL Server, и т.д. |
|||
|
|||
В данном примере мы будем использовать базу данных **SQLite**, т.к. она состоит из единственного файла и поддерживается встроенными библиотеками Python. Таким образом, вы сможете скопировать данный пример и запустить его как он есть. |
|||
|
|||
В дальнейшем, для продакшн-версии вашего приложения, возможно, вам стоит использовать серверную базу данных, например, **PostgreSQL**. |
|||
|
|||
/// tip | Подсказка |
|||
|
|||
Существует официальный генератор проектов на **FastAPI** и **PostgreSQL**, который также включает frontend и дополнительные инструменты <a href="https://github.com/fastapi/full-stack-fastapi-template" class="external-link" target="_blank">https://github.com/fastapi/full-stack-fastapi-template</a> |
|||
|
|||
/// |
|||
|
|||
Это очень простое и короткое руководство, поэтому, если вы хотите узнать о базах данных в целом, об SQL, разобраться с более продвинутым функционалом, то воспользуйтесь <a href="https://sqlmodel.tiangolo.com/" class="external-link" target="_blank">документацией SQLModel</a>. |
|||
|
|||
## Установка `SQLModel` |
|||
|
|||
Создайте виртуальное окружение [virtual environment](../virtual-environments.md){.internal-link target=_blank}, активируйте его и установите `sqlmodel`: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ pip install sqlmodel |
|||
---> 100% |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
## Создание приложения с единственной моделью |
|||
|
|||
Мы начнем с создания наиболее простой первой версии нашего приложения с одной единственной моделью **SQLModel**. |
|||
|
|||
В дальнейшем с помощью **дополнительных моделей** мы его улучшим и сделаем более безопасным и универсальным. 🤓 |
|||
|
|||
### Создание моделей |
|||
|
|||
Импортируйте `SQLModel` и создайте модель базы данных: |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[1:11] hl[7:11] *} |
|||
|
|||
Класс `Hero` очень напоминает модель Pydantic (фактически, под капотом, *это и есть модель Pydantic*). |
|||
|
|||
Но есть и некоторые различия |
|||
|
|||
* `table=True` для SQLModel означает, что это *модель-таблица*, которая должна представлять **таблицу** в реляционной базе данных. Это не просто *модель данных* (в отличие от обычного класса в Pydantic). |
|||
|
|||
* `Field(primary_key=True)` для SQLModel означает, что поле `id` является первичным ключом в таблице базы данных (вы можете подробнее узнать о первичных ключах баз данных в документации по SQLModel). |
|||
|
|||
Тип `int | None` сигнализирует для SQLModel, что столбец таблицы базы данных должен иметь тип `INTEGER`, или иметь пустое значение `NULL`. |
|||
|
|||
* `Field(index=True)` для SQLModel означает, что нужно создать **SQL индекс** для данного столбца. Это обеспечит более быстрый поиск при чтении данных, отфильтрованных по данному столбцу. |
|||
|
|||
SQLModel будет знать, что данные типа `str`, будут представлены в базе данных как `TEXT` (или `VARCHAR`, в зависимости от типа базы данных). |
|||
|
|||
### Создание соединения с базой данных (Engine) |
|||
|
|||
В SQLModel объект соединения `engine` (по сути это `Engine` из SQLAlchemy) **содержит пул соединений** к базе данных. |
|||
|
|||
Для обеспечения всех подключений приложения к одной базе данных нужен только один объект соединения `engine`. |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[14:18] hl[14:15,17:18] *} |
|||
|
|||
Использование настройки `check_same_thread=False` позволяет FastAPI использовать одну и ту же SQLite базу данных в различных потоках (threads). Это необходимо, когда **один запрос** использует **более одного потока** (например, в зависимостях). |
|||
|
|||
Не беспокойтесь, учитывая структуру кода, мы позже позаботимся о том, чтобы использовать **отдельную SQLModel-сессию на каждый отдельный запрос**, это как раз то, что пытается обеспечить `check_same_thread`. |
|||
|
|||
### Создание таблиц |
|||
|
|||
Далее мы добавляем функцию, использующую `SQLModel.metadata.create_all(engine)`, для того, чтобы создать **таблицы** для каждой из **моделей таблицы**. |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[21:22] hl[21:22] *} |
|||
|
|||
### Создание зависимости Session |
|||
|
|||
Сессия базы данных (**`Session`**) хранит **объекты в памяти** и отслеживает любые необходимые изменения в данных, а затем **использует `engine`** для коммуникации с базой данных. |
|||
|
|||
Мы создадим FastAPI-**зависимость** с помощью `yield`, которая будет создавать новую сессию (Session) для каждого запроса. Это как раз и обеспечит использование отдельной сессии на каждый отдельный запрос. 🤓 |
|||
|
|||
Затем мы создадим объявленную (`Annotated`) зависимость `SessionDep`. Мы сделаем это для того, чтобы упростить остальной код, который будет использовать эту зависимость. |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[25:30] hl[25:27,30] *} |
|||
|
|||
### Создание таблиц базы данных при запуске приложения |
|||
|
|||
Мы будем создавать таблицы базы данных при запуске приложения. |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[32:37] hl[35:37] *} |
|||
|
|||
В данном примере мы создаем таблицы при наступлении события запуска приложения. |
|||
|
|||
В продуктовом приложении вы, скорее всего, будете использовать скрипт для миграции базы данных, который выполняется перед запуском приложения. 🤓 |
|||
|
|||
/// tip | Подсказка |
|||
|
|||
В SQLModel будут включены утилиты миграции, входящие в состав Alembic, но на данный момент вы просто можете использовать |
|||
<a href="https://alembic.sqlalchemy.org/en/latest/" class="external-link" target="_blank">Alembic</a> напрямую. |
|||
|
|||
/// |
|||
|
|||
### Создание героя (Hero) |
|||
|
|||
Каждая модель в SQLModel является также моделью Pydantic, поэтому вы можете использовать её при **объявлении типов**, точно также, как и модели Pydantic. |
|||
|
|||
Например, при объявлении параметра типа `Hero`, она будет считана из **тела JSON**. |
|||
|
|||
Точно также, вы можете использовать её при объявлении типа значения, возвращаемого функцией, и тогда структурированные данные будут отображены через пользовательский интерфейс автоматически сгенерированной документации FastAPI. |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[40:45] hl[40:45] *} |
|||
|
|||
Мы используем зависимость `SessionDep` (сессию базы данных) для того, чтобы добавить нового героя `Hero` в объект сессии (`Session`), сохранить изменения в базе данных, обновить данные героя и затем вернуть их. |
|||
|
|||
### Чтение данных о героях |
|||
|
|||
Мы можем **читать** данные героев из базы данных с помощью `select()`. Мы можем включить `limit` и `offset` для постраничного считывания результатов. |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[48:55] hl[51:52,54] *} |
|||
|
|||
### Чтение данных отдельного героя |
|||
|
|||
Мы можем прочитать данные отдельного героя (`Hero`). |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[58:63] hl[60] *} |
|||
|
|||
### Удаление данных героя |
|||
|
|||
Мы также можем удалить героя `Hero` из базы данных. |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[66:73] hl[71] *} |
|||
|
|||
### Запуск приложения |
|||
|
|||
Вы можете запустить приложение следующим образом: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ fastapi dev main.py |
|||
|
|||
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
Далее перейдите в пользовательский интерфейс API `/docs`. Вы увидите, что **FastAPI** использует модели для создания документации API. Эти же модели используются для сериализации и проверки данных. |
|||
|
|||
<div class="screenshot"> |
|||
<img src="/img/tutorial/sql-databases/image01.png"> |
|||
</div> |
|||
|
|||
## Добавление в приложение дополнительных (вспомогательных) моделей |
|||
|
|||
Теперь давайте проведём **рефакторинг** нашего приложения, чтобы сделать его более безопасным и более универсальным. |
|||
|
|||
Обратите внимание, что на данном этапе наше приложение позволяет на уровне клиента определять `id` создаваемого героя (`Hero`). 😱 |
|||
|
|||
Мы не можем этого допустить, т.к. существует риск переписать уже присвоенные `id` в базе данных. Присвоение `id` должно происходить **на уровне бэкэнда (backend)** или **на уровне базы данных**, но никак **не на уровне клиента**. |
|||
|
|||
Кроме того, мы создаем секретное имя `secret_name` для героя, но пока что, мы возвращаем его повсеместно, и это слабо напоминает **секретность**... 😅 |
|||
|
|||
Мы поправим это с помощью нескольких дополнительных (вспомогательных) моделей. Вот где SQLModel по-настоящему покажет себя. ✨ |
|||
|
|||
### Создание дополнительных моделей |
|||
|
|||
В **SQLModel**, любая модель с параметром `table=True` является **моделью таблицы**. |
|||
|
|||
Любая модель, не содержащая `table=True` является **моделью данных**, это по сути обычные модели Pydantic (с несколько расширенным функционалом). 🤓 |
|||
|
|||
С помощью SQLModel мы можем использовать **наследование**, что поможет нам **избежать дублирования** всех полей. |
|||
|
|||
#### Базовый класс `HeroBase` |
|||
|
|||
Давайте начнём с модели `HeroBase`, которая содержит поля, общие для всех моделей: |
|||
|
|||
* `name` |
|||
* `age` |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:9] hl[7:9] *} |
|||
|
|||
#### Модель таблицы `Hero` |
|||
|
|||
Далее давайте создадим **модель таблицы** `Hero` с дополнительными полями, которых может не быть в других моделях: |
|||
|
|||
* `id` |
|||
* `secret_name` |
|||
|
|||
Модель `Hero` наследует от `HeroBase`, и поэтому включает также поля из `HeroBase`. Таким образом, все поля, содержащиеся в `Hero`, будут следующими: |
|||
|
|||
* `id` |
|||
* `name` |
|||
* `age` |
|||
* `secret_name` |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:14] hl[12:14] *} |
|||
|
|||
#### Публичная модель данных `HeroPublic` |
|||
|
|||
Далее мы создадим модель `HeroPublic`. Мы будем возвращать её клиентам API. |
|||
|
|||
Она включает в себя те же поля, что и `HeroBase`, и, соответственно, поле `secret_name` в ней отсутствует. |
|||
|
|||
Наконец-то личность наших героев защищена! 🥷 |
|||
|
|||
В модели `HeroPublic` также объявляется поле `id: int`. Мы как бы заключаем договоренность с API клиентом, на то, что передаваемые данные всегда должны содержать поле `id`, и это поле должно содержать целое число (и никогда не содержать `None`). |
|||
|
|||
/// tip | Подсказка |
|||
|
|||
Модель ответа, гарантирующая наличие поля со значением типа `int` (не `None`), очень полезна при разработке API клиентов. Определенность в передаваемых данных может обеспечить написание более простого кода. |
|||
|
|||
Также **автоматически генерируемые клиенты** будут иметь более простой интерфейс. И в результате жизнь разработчиков, использующих ваш API, станет значительно легче. 😎 |
|||
|
|||
/// |
|||
|
|||
`HeroPublic` содержит все поля `HeroBase`, а также поле `id`, объявленное как `int` (не `None`): |
|||
|
|||
* `id` |
|||
* `name` |
|||
* `age` |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:18] hl[17:18] *} |
|||
|
|||
#### Модель для создания героя `HeroCreate` |
|||
|
|||
Сейчас мы создадим модель `HeroCreate`. Эта модель будет использоваться для проверки данных, переданных клиентом. |
|||
|
|||
Она содержит те же поля, что и `HeroBase`, а также поле `secret_name`. |
|||
|
|||
Теперь, при создании нового героя, клиенты будут передавать секретное имя `secret_name`, которое будет сохранено в базе данных, но не будет возвращено в ответе API клиентам. |
|||
|
|||
/// tip | Подсказка |
|||
|
|||
Вот как нужно работать с **паролями**: получайте их, но не возвращайте их через API. |
|||
|
|||
Также хэшируйте значения паролей перед тем, как их сохранить. Ни в коем случае не храните пароли в открытом виде, как обычный текст. |
|||
|
|||
/// |
|||
|
|||
Поля модели `HeroCreate`: |
|||
|
|||
* `name` |
|||
* `age` |
|||
* `secret_name` |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:22] hl[21:22] *} |
|||
|
|||
#### Модель для обновления данных героя `HeroUpdate` |
|||
|
|||
В предыдущих версиях нашей программы мы не могли обновить данные героя, теперь, воспользовавшись дополнительными моделями, мы сможем это сделать. 🎉 |
|||
|
|||
Модель данных `HeroUpdate` в некотором смысле особенная. Она содержит все те же поля, что и модель создания героя, но все поля модели являются **необязательными**. (Все они имеют значение по умолчанию.) Таким образом, при обновлении данных героя, вам достаточно передать только те поля, которые требуют изменения. |
|||
|
|||
Поскольку **все поля по сути меняются** (теперь тип каждого поля допускает значение `None` и значение по умолчанию `None`), мы должны их **объявить заново**. |
|||
|
|||
Фактически, нам не нужно наследоваться от `HeroBase`, потому что мы будем заново объявлять все поля. Я оставлю наследование просто для поддержания общего стиля, но оно (наследование) здесь необязательно. 🤷 |
|||
|
|||
Поля `HeroUpdate`: |
|||
|
|||
* `name` |
|||
* `age` |
|||
* `secret_name` |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:28] hl[25:28] *} |
|||
|
|||
### Создание героя с помощью `HeroCreate` и возвращение результатов с помощью `HeroPublic` |
|||
|
|||
Теперь, когда у нас есть дополнительные модели, мы можем обновить те части приложения, которые их используют. |
|||
|
|||
Вместе c запросом на создание героя мы получаем объект данных `HeroCreate`, и создаем на его основе объект модели таблицы `Hero`. |
|||
|
|||
Созданный объект *модели таблицы* `Hero` будет иметь все поля, переданные клиентом, а также поле `id`, сгенерированное базой данных. |
|||
|
|||
Далее функция вернёт объект *модели таблицы* `Hero`. Но поскольку, мы объявили `HeroPublic` как модель ответа, то **FastAPI** будет использовать именно её для проверки и сериализации данных. |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[56:62] hl[56:58] *} |
|||
|
|||
/// tip | Подсказка |
|||
|
|||
Теперь мы используем модель ответа `response_model=HeroPublic`, вместо того, чтобы объявить тип возвращаемого значения как `-> HeroPublic`. Мы это делаем потому, что тип возвращаемого значения не относится к `HeroPublic`. |
|||
|
|||
Если бы мы объявили тип возвращаемого значения как `-> HeroPublic`, то редактор и линтер начали бы ругаться (и вполне справедливо), что возвращаемое значение принадлежит типу `Hero`, а совсем не `HeroPublic`. |
|||
|
|||
Объявляя модель ответа в `response_model`, мы как бы говорим **FastAPI**: делай свое дело, не вмешиваясь в аннотацию типов и не полагаясь на помощь редактора или других инструментов. |
|||
|
|||
/// |
|||
|
|||
### Чтение данных героев с помощью `HeroPublic` |
|||
|
|||
Мы можем проделать то же самое **для чтения данных** героев. Мы применим модель ответа `response_model=list[HeroPublic]`, и тем самым обеспечим правильную проверку и сериализацию данных. |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[65:72] hl[65] *} |
|||
|
|||
### Чтение данных отдельного героя с помощью `HeroPublic` |
|||
|
|||
Мы можем **прочитать** данные отдельного героя: |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[75:80] hl[77] *} |
|||
|
|||
### Обновление данных героя с помощью `HeroUpdate` |
|||
|
|||
Мы можем **обновить данные героя**. Для этого мы воспользуемся HTTP методом `PATCH`. |
|||
|
|||
В коде мы получаем объект словаря `dict` с данными, переданными клиентом (т.е. **только c данными, переданными клиентом**, исключая любые значения, которые могли бы быть там только потому, что они являются значениями по умолчанию). Для того чтобы сделать это, мы воспользуемся опцией `exclude_unset=True`. В этом главная хитрость. 🪄 |
|||
|
|||
Затем мы применим `hero_db.sqlmodel_update(hero_data)`, и обновим `hero_db`, использовав данные `hero_data`. |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[83:93] hl[83:84,88:89] *} |
|||
|
|||
### Удалим героя ещё раз |
|||
|
|||
Операция **удаления** героя практически не меняется. |
|||
|
|||
В данном случае желание *`отрефакторить всё`* остаётся неудовлетворенным. 😅 |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[96:103] hl[101] *} |
|||
|
|||
### Снова запустим приложение |
|||
|
|||
Вы можете снова запустить приложение: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ fastapi dev main.py |
|||
|
|||
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
Если вы перейдете в пользовательский интерфейс API `/docs`, то вы увидите, что он был обновлен, и больше не принимает параметра `id` от клиента при создании нового героя, и т.д. |
|||
|
|||
<div class="screenshot"> |
|||
<img src="/img/tutorial/sql-databases/image02.png"> |
|||
</div> |
|||
|
|||
## Резюме |
|||
|
|||
Вы можете использовать <a href="https://sqlmodel.tiangolo.com/" class="external-link" target="_blank">**SQLModel**</a> для взаимодействия с реляционными базами данных, а также для упрощения работы с **моделями данных** и **моделями таблиц**. |
|||
|
|||
Вы можете узнать гораздо больше информации в документации по **SQLModel**. Там вы найдете более подробное <a href="https://sqlmodel.tiangolo.com/tutorial/fastapi/" class="external-link" target="_blank">мини-руководство по использованию SQLModel с **FastAPI**</a>. 🚀 |
@ -0,0 +1,839 @@ |
|||
# Виртуальная среда |
|||
|
|||
При работе с проектами в Python рекомендуется использовать **виртуальную среду разработки** (или какой-нибудь другой подобный механизм). Это нужно для того, чтобы изолировать устанавливаемые пакеты для каждого отдельного проекта. |
|||
|
|||
/// info | Дополнительная информация |
|||
|
|||
Если вы уже знакомы с виртуальными средами разработки, знаете как их создавать и использовать, то вы можете свободно пропустить данный раздел. 🤓 |
|||
|
|||
/// |
|||
|
|||
/// tip | Подсказка |
|||
|
|||
**Виртуальная среда** и **переменная окружения** это две разные вещи. |
|||
|
|||
**Переменная окружения** это системная переменная, которую могут использовать программы. |
|||
|
|||
**Виртуальная среда** это папка, содержащая файлы. |
|||
|
|||
/// |
|||
|
|||
/// info | Дополнительная информация |
|||
|
|||
В этом разделе мы научим вас пользоваться виртуальными средами разработки и расскажем, как они работают. |
|||
|
|||
Если же вы готовы воспользоваться инструментом, **который умеет управлять всем, что касается Python-проектов**, |
|||
(включая установку Python), то попробуйте <a href="https://github.com/astral-sh/uv" class="external-link" target="_blank">uv</a>. |
|||
|
|||
/// |
|||
|
|||
## Создание проекта |
|||
|
|||
В первую очередь, создайте директорию для вашего проекта. |
|||
|
|||
Я обычно создаю папку под названием `code` внутри моего домашнего каталога `/home/user`. |
|||
|
|||
Затем внутри данной папки я создаю отдельную директорию под каждый свой проект. |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
// Перейдите в домашний каталог |
|||
$ cd |
|||
// Создайте отдельную папку под все будущие программные проекты (code) |
|||
$ mkdir code |
|||
// Войдите в директорию code |
|||
$ cd code |
|||
// Создайте директрорию под данный проект (awesome-project) |
|||
$ mkdir awesome-project |
|||
// Перейдите в созданную директорию проекта |
|||
$ cd awesome-project |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
## Создание виртуальной среды разработки |
|||
|
|||
Начиная работу с Python-проектом, сразу же создавайте виртуальную среду разработки |
|||
**<abbr title="есть и другие опции, но мы рассмотрим наиболее простой вариант">внутри вашего проекта</abbr>**. |
|||
|
|||
/// tip | Подсказка |
|||
|
|||
Виртуальная среда разработки создается один раз, и в дальнейшем, работая с проектом, этого больше делать не придется. |
|||
|
|||
/// |
|||
|
|||
//// tab | `venv` |
|||
|
|||
Для создания виртуальной среды вы можете воспользоваться модулем `venv`, который является частью встроенной библиотеки Python. |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ python -m venv .venv |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
/// details | Что делает эта команда? |
|||
|
|||
* `python`: использовать программу под именем `python` |
|||
* `-m`: вызывать модуль как скрипт, в следующей инструкции мы скажем какой именно модуль вызвать |
|||
* `venv`: использовать модуль под названием `venv`, который обычно устанавливается вместе с Python |
|||
* `.venv`: создать виртуальную среду разработки в новой директории `.venv` |
|||
|
|||
/// |
|||
|
|||
//// |
|||
|
|||
//// tab | `uv` |
|||
|
|||
Если вы установили <a href="https://github.com/astral-sh/uv" class="external-link" target="_blank">`uv`</a>, то вы можете им воспользоваться для создания виртуальной среды разработки. |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ uv venv |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
/// tip | Подсказка |
|||
|
|||
По умолчанию `uv` создаст виртуальную среду разработки в папке под названием `.venv`. |
|||
|
|||
Но вы можете это изменить, передав дополнительный аргумент с именем директории. |
|||
|
|||
/// |
|||
|
|||
//// |
|||
|
|||
Данная команда создаст новую виртуальную среду разработки в папке `.venv`. |
|||
|
|||
/// details | `.venv` или другое имя? |
|||
|
|||
Вы можете поместить виртуальную среду разработки в папку с другим именем, но традиционным (конвенциональным) названием является `.venv` . |
|||
|
|||
/// |
|||
|
|||
## Активация виртуальной среды разработки |
|||
|
|||
Активируйте виртуальную среду разработки, и тогда любая запускаемая Python-команда или устанавливаемый пакет будут ее использовать. |
|||
|
|||
/// tip | Подсказка |
|||
|
|||
При работе над проектом делайте это **каждый раз** при запуске **новой сессии в терминале**. |
|||
|
|||
/// |
|||
|
|||
//// 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 |
|||
|
|||
Или при использовании Bash для Windows (напр. <a href="https://gitforwindows.org/" class="external-link" target="_blank">Git Bash</a>): |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ source .venv/Scripts/activate |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
//// |
|||
|
|||
## Проверка активации виртуальной среды |
|||
|
|||
Проверьте, активна ли виртуальная среда (удостоверимся, что предыдущая команда сработала). |
|||
|
|||
/// tip | Подсказка |
|||
|
|||
Убедитесь в том, что все работает так, как нужно и вы используете именно ту виртуальную среду разработки, которую нужно. Делать это необязательно, но желательно. |
|||
|
|||
/// |
|||
|
|||
//// tab | Linux, macOS, Windows Bash |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ which python |
|||
|
|||
/home/user/code/awesome-project/.venv/bin/python |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
Если данная команда показывает, что исполняемый файл `python` (`.venv\bin\python`), находится внутри виртуальной среды вашего проекта (у нас это `awesome-project`), значит все отработало как нужно. 🎉 |
|||
|
|||
//// |
|||
|
|||
//// tab | Windows PowerShell |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ Get-Command python |
|||
|
|||
C:\Users\user\code\awesome-project\.venv\Scripts\python |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
Если данная команда показывает, что исполняемый файл `python` (`.venv\Scripts\python`), находится внутри виртуальной среды вашего проекта (у нас это `awesome-project`), значит все отработало как нужно. 🎉 |
|||
|
|||
//// |
|||
|
|||
## Обновление `pip` |
|||
|
|||
/// tip | Подсказка |
|||
|
|||
Если вы используете <a href="https://github.com/astral-sh/uv" class="external-link" target="_blank">`uv`</a>, то вы должны будете его использовать для установки пакетов вместо `pip`, поэтому обновлять `pip` вам ненужно. 😎 |
|||
|
|||
/// |
|||
|
|||
Если для установки пакетов вы используете `pip` (он устанавливается по умолчанию вместе с Python), то обновите `pip` до последней версии. |
|||
|
|||
Большинство экзотических ошибок, возникающих при установке пакетов, устраняется предварительным обновлением `pip`. |
|||
|
|||
/// tip | Подсказка |
|||
|
|||
Обычно это делается только один раз, сразу после создания виртуальной среды разработки. |
|||
|
|||
/// |
|||
|
|||
Убедитесь в том, что виртуальная среда активирована (с помощью вышеуказанной команды) и запустите следующую команду: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ python -m pip install --upgrade pip |
|||
|
|||
---> 100% |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
## Добавление `.gitignore` |
|||
|
|||
Если вы используете **Git** (а вы должны его использовать), то добавьте файл `.gitignore` и исключите из Git всё, что находится в папке `.venv`. |
|||
|
|||
/// tip | Подсказка |
|||
|
|||
Если для создания виртуальной среды вы используете <a href="https://github.com/astral-sh/uv" class="external-link" target="_blank">`uv`</a>, то для вас все уже сделано и вы можете пропустить этот шаг. 😎 |
|||
|
|||
/// |
|||
|
|||
/// tip | Подсказка |
|||
|
|||
Это делается один раз, сразу после создания виртуальной среды разработки. |
|||
|
|||
/// |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ echo "*" > .venv/.gitignore |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
/// details | Что делает эта команда? |
|||
|
|||
* `echo "*"`: напечатать `*` в консоли (следующий шаг это слегка изменит) |
|||
* `>`: все что находится слева от `>` не печатать в консоль, но записать в файл находящийся справа от `>` |
|||
* `.gitignore`: имя файла, в который нужно записать текст. |
|||
|
|||
`*` в Git означает "всё". Т.е. будет проигнорировано всё, что содержится в папке `.venv`. |
|||
|
|||
Данная команда создаст файл `.gitignore` следующего содержания: |
|||
|
|||
```gitignore |
|||
* |
|||
``` |
|||
|
|||
/// |
|||
|
|||
## Установка пакетов |
|||
|
|||
После установки виртуальной среды, вы можете устанавливать в нее пакеты. |
|||
|
|||
/// tip | Подсказка |
|||
|
|||
Сделайте это **один раз**, при установке или обновлении пакетов, нужных вашему проекту. |
|||
|
|||
Если вам понадобится обновить версию пакета или добавить новый пакет, то вы должны будете **сделать это снова**. |
|||
|
|||
/// |
|||
|
|||
### Установка пакетов напрямую |
|||
|
|||
Если вы торопитесь и не хотите объявлять зависимости проекта в отдельном файле, то вы можете установить их напрямую. |
|||
|
|||
/// tip | Подсказка |
|||
|
|||
Объявление пакетов, которые использует ваш проект, и их версий в отдельном файле (например, в `requirements.txt` или в `pyproject.toml`) - это отличная идея. |
|||
|
|||
/// |
|||
|
|||
//// tab | `pip` |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ pip install "fastapi[standard]" |
|||
|
|||
---> 100% |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
//// |
|||
|
|||
//// tab | `uv` |
|||
|
|||
Если вы используете <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> |
|||
|
|||
//// |
|||
|
|||
### Установка из `requirements.txt` |
|||
|
|||
Если у вас есть `requirements.txt`, то вы можете использовать его для установки пакетов. |
|||
|
|||
//// tab | `pip` |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ pip install -r requirements.txt |
|||
---> 100% |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
//// |
|||
|
|||
//// tab | `uv` |
|||
|
|||
Если вы используете <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` |
|||
|
|||
`requirements.txt` с парочкой пакетов внутри выглядит приблизительно так: |
|||
|
|||
```requirements.txt |
|||
fastapi[standard]==0.113.0 |
|||
pydantic==2.8.0 |
|||
``` |
|||
|
|||
/// |
|||
|
|||
## Запуск программы |
|||
|
|||
После активации виртуальной среды разработки вы можете запустить свою программу и она будет использовать версию Python и пакеты, установленные в виртуальной среде. |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ python main.py |
|||
|
|||
Hello World |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
## Настройка редактора |
|||
|
|||
Вероятно, вы захотите воспользоваться редактором. Убедитесь, что вы настроили его на использование той самой виртуальной среды, которую вы создали. (Скорее всего, она автоматически будет обнаружена). Это позволит вам использовать авто-завершение и выделение ошибок в редакторе. |
|||
|
|||
Например: |
|||
|
|||
* <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 | Подсказка |
|||
|
|||
Обычно это делается один раз, при создании виртуальной среды разработки. |
|||
|
|||
/// |
|||
|
|||
## Деактивация виртуальной среды разработки |
|||
|
|||
По окончании работы над проектом вы можете деактивировать виртуальную среду. |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ deactivate |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
Таким образом, при запуске `python`, будет использована версия `python` установленная глобально, а не из этой виртуальной среды вместе с установленными в ней пакетами. |
|||
|
|||
## Все готово к работе |
|||
|
|||
Теперь вы готовы к тому, чтобы начать работу над своим проектом. |
|||
|
|||
|
|||
|
|||
/// tip | Подсказка |
|||
|
|||
Хотите разобраться со всем, что написано выше? |
|||
|
|||
Продолжайте читать. 👇🤓 |
|||
|
|||
/// |
|||
|
|||
## Зачем использовать виртуальную среду? |
|||
|
|||
Для работы с FastAPI вам потребуется установить <a href="https://www.python.org/" class="external-link" target="_blank">Python</a>. |
|||
|
|||
После этого вам нужно будет **установить** FastAPI и другие **пакеты**, которые вы собираетесь использовать. |
|||
|
|||
Для установки пакетов обычно используют `pip`, который устанавливается вместе с Python (или же используют альтернативные решения). |
|||
|
|||
Тем не менее, если вы просто используете `pip` напрямую, то пакеты будут установлены в **глобальное Python-окружение** (глобально установленный Python). |
|||
|
|||
### Проблема |
|||
|
|||
Так в чем же проблема с установкой пакетов в глобальную среду Python? |
|||
|
|||
В какой-то момент вам, вероятно, придется писать множество разных программ, которые используют различные пакеты. 😱 |
|||
|
|||
Например, вы создаете проект `philosophers-stone`, который зависит от пакета под названием **`harry`, версии `1`**. Таким образом, вам нужно установить `harry`. |
|||
|
|||
```mermaid |
|||
flowchart LR |
|||
stone(philosophers-stone) -->|requires| harry-1[harry v1] |
|||
``` |
|||
|
|||
Затем, в какой-то момент, вы создаете другой проект под названием `prisoner-of-azkaban`, и этот проект тоже зависит от `harry`, но он уже использует **`harry` версии `3`**. |
|||
|
|||
```mermaid |
|||
flowchart LR |
|||
azkaban(prisoner-of-azkaban) --> |requires| harry-3[harry v3] |
|||
``` |
|||
|
|||
Проблема заключается в том, что при установке в глобальное окружение, а не в локальную виртуальную среду разработки, вам нужно будет выбирать, какую версию пакета `harry` устанавливать. |
|||
|
|||
Если вам нужен `philosophers-stone`, то вам нужно сначала установить `harry` версии `1`: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ pip install "harry==1" |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
И тогда в вашем глобальном окружении Python будет установлен `harry` версии `1`: |
|||
|
|||
```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 |
|||
``` |
|||
|
|||
Но если позднее вы захотите запустить `prisoner-of-azkaban`, то вам нужно будет удалить `harry` версии 1, и установить `harry` версии `3` (при установке пакета версии `3` поверх пакета версии `1`, пакет версии `1` удаляется автоматически). |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ pip install "harry==3" |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
И тогда, в вашей глобальной среде окружения Python, будет установлен пакет `harry` версии `3`. |
|||
|
|||
И когда вы снова попытаетесь запустить `philosophers-stone`, то существует вероятность того, что он не будет работать, так как он использует `harry` версии `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 | Подсказка |
|||
|
|||
В пакетах Python очень часто стараются изо всех сил избегать внесения критических изменений в новые версии, но лучше перестраховаться и планово устанавливать новые версии, а затем запускать тесты, чтобы проверить, все ли работает правильно. |
|||
|
|||
/// |
|||
|
|||
Теперь представьте, что это происходит со многими другими пакетами, которые используются в ваших проектах. С этим очень сложно справиться. И скорее всего, в конечном итоге вы будете запускать некоторые проекты с некоторыми несовместимыми зависимостями и не будете знать, почему что-то не работает. |
|||
|
|||
Кроме того, в зависимости от вашей операционной системы (напр. Linux, Windows, macOS), она может поставляться с уже установленным Python. Вероятно, что в этом случае в ней уже установлены системные пакеты определенных версий. Если вы устанавливаете пакеты глобально, то вы можете **поломать** программы, являющиеся частью ОС. |
|||
|
|||
## Куда устанавливаются пакеты? |
|||
|
|||
Когда вы устанавливаете Python, то на вашей машине создается некоторое количество директорий, содержащих некоторое количество файлов. |
|||
|
|||
Среди них есть каталоги, отвечающие за хранение всех устанавливаемых вами пакетов. |
|||
|
|||
Когда вы запустите команду: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
// Не запускайте эту команду, это просто пример 🤓 |
|||
$ pip install "fastapi[standard]" |
|||
---> 100% |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
То будет скачан сжатый файл, содержащий код FastAPI, обычно скачивание происходит с <a href="https://pypi.org/project/fastapi/" class="external-link" target="_blank">PyPI</a>. |
|||
|
|||
Также будут скачаны файлы, содержащие пакеты, которые использует FastAPI. |
|||
|
|||
Затем все файлы будут извлечены и помещены в директорию на вашем компьютере. |
|||
|
|||
По умолчанию эти файлы будут загружены и извлечены в один из каталогов установки Python, т.е. в глобальную среду. |
|||
|
|||
## Что такое виртуальная среда разработки? |
|||
|
|||
Решением проблемы размещения всех пакетов в глобальной среде будет использование отдельной виртуальной среды под каждый проект, над которым вы работаете. |
|||
|
|||
Виртуальная среда это обычная папка, очень похожая на глобальную, куда вы можете устанавливать пакеты для вашего проекта. |
|||
|
|||
Таким образом, каждый проект будет иметь свою отдельную виртуальную среду разработки (в директории `.venv`) вместе со своими пакетами. |
|||
|
|||
```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 |
|||
``` |
|||
|
|||
## Что означает активация виртуальной среды? |
|||
|
|||
Когда вы активируете виртуальную среду разработки, например, так: |
|||
|
|||
//// 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 |
|||
|
|||
Или если вы воспользуетесь Bash под Windows (напр. <a href="https://gitforwindows.org/" class="external-link" target="_blank">Git Bash</a>): |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ source .venv/Scripts/activate |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
//// |
|||
|
|||
|
|||
Эта команда создаст или изменит некоторые [переменные окружения](environment-variables.md){.internal-link target=_blank}, которые будут доступны для последующих команд. |
|||
|
|||
Одной из таких переменных является `PATH`. |
|||
|
|||
/// tip | Подсказка |
|||
|
|||
Вы можете узнать больше о переменной окружения `PATH` в разделе [Переменные окружения](environment-variables.md#path-environment-variable){.internal-link target=_blank}. |
|||
|
|||
/// |
|||
|
|||
При активации виртуальной среды путь `.venv/bin` (для Linux и macOS) или `.venv\Scripts` (для Windows) добавляется в переменную окружения `PATH`. |
|||
|
|||
Предположим, что до активации виртуальной среды переменная `PATH` выглядела так: |
|||
|
|||
//// tab | Linux, macOS |
|||
|
|||
```plaintext |
|||
/usr/bin:/bin:/usr/sbin:/sbin |
|||
``` |
|||
|
|||
Это означает, что система ищет программы в следующих каталогах: |
|||
|
|||
* `/usr/bin` |
|||
* `/bin` |
|||
* `/usr/sbin` |
|||
* `/sbin` |
|||
|
|||
//// |
|||
|
|||
//// tab | Windows |
|||
|
|||
```plaintext |
|||
C:\Windows\System32 |
|||
``` |
|||
|
|||
Это означает, что система ищет программы в: |
|||
|
|||
* `C:\Windows\System32` |
|||
|
|||
//// |
|||
|
|||
После активации виртуальной среды переменная окружение `PATH` будет выглядеть примерно так: |
|||
|
|||
//// tab | Linux, macOS |
|||
|
|||
```plaintext |
|||
/home/user/code/awesome-project/.venv/bin:/usr/bin:/bin:/usr/sbin:/sbin |
|||
``` |
|||
|
|||
Это означает, что система теперь будет искать программы в: |
|||
|
|||
```plaintext |
|||
/home/user/code/awesome-project/.venv/bin |
|||
``` |
|||
|
|||
прежде чем начать искать в других каталогах. |
|||
|
|||
Таким образом, когда вы введете в консоль `python`, система будет искать Python в |
|||
|
|||
```plaintext |
|||
/home/user/code/awesome-project/.venv/bin/python |
|||
``` |
|||
|
|||
и будет использовать именно его. |
|||
|
|||
//// |
|||
|
|||
//// tab | Windows |
|||
|
|||
```plaintext |
|||
C:\Users\user\code\awesome-project\.venv\Scripts;C:\Windows\System32 |
|||
``` |
|||
|
|||
Это означает, что система в первую очередь начнет искать программы в: |
|||
|
|||
```plaintext |
|||
C:\Users\user\code\awesome-project\.venv\Scripts |
|||
``` |
|||
|
|||
прежде чем начать искать в других директориях. |
|||
|
|||
Таким образом, если вы введете в консоль команду `python`, то система найдет Python в: |
|||
|
|||
```plaintext |
|||
C:\Users\user\code\awesome-project\.venv\Scripts\python |
|||
``` |
|||
|
|||
и использует его. |
|||
|
|||
//// |
|||
|
|||
Очень важной деталью является то, что путь к виртуальной среде будет помещен в самое начало переменной `PATH`. Система обнаружит данный путь к Python раньше, чем какой-либо другой. Таким образом, при запуске команды `python`, будет использован именно Python из виртуальной среды разработки, а не какой-нибудь другой (например, Python из глобальной среды) |
|||
|
|||
Активация виртуальной среды разработки также меняет и несколько других вещей, но данная функция является основной. |
|||
|
|||
## Проверка виртуальной среды |
|||
|
|||
Когда вы проверяете активна ли виртуальная среда разработки, например, так: |
|||
|
|||
//// 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> |
|||
|
|||
//// |
|||
|
|||
Это означает, что будет использоваться `python` **из виртуальной среды разработки**. |
|||
|
|||
Вы используете `which` для Linux и macOS и `Get-Command` для Windows PowerShell. |
|||
|
|||
Эта команда работает следующим образом: она проверяет переменную окружения `PATH`, проходя по очереди каждый указанный путь в поисках программы под названием `python`. И когда она её находит, то возвращает путь к данной программе. |
|||
|
|||
Основной момент при вызове команды `python` состоит в том, какой именно "`python`" будет запущен. |
|||
|
|||
Таким образом, вы можете убедиться, что используете правильную виртуальную среду разработки. |
|||
|
|||
/// tip | Подсказка |
|||
|
|||
Легко активировать одну виртуальную среду, вызвать один Python и **перейти к следующему проекту**. |
|||
|
|||
И следующий проект не будет работать потому, что вы используете **неправильный Python** из виртуальной среды другого проекта. |
|||
|
|||
Так что, будет нелишним проверить, какой `python` вы используете. 🤓 |
|||
|
|||
/// |
|||
|
|||
## Зачем деактивируют виртуальную среду? |
|||
|
|||
Предположим, что вы работаете над проектом `philosophers-stone`, **активируете виртуальную среду разработки**, устанавливаете пакеты и работаете с данной средой. |
|||
|
|||
И позже вам понадобилось поработать с **другим проектом** `prisoner-of-azkaban`. |
|||
|
|||
Вы переходите к этому проекту: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ cd ~/code/prisoner-of-azkaban |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
Если вы не деактивировали виртуальное окружение проекта `philosophers-stone`, то при запуске `python` через консоль будет вызван Python из `philosophers-stone` |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ cd ~/code/prisoner-of-azkaban |
|||
|
|||
$ python main.py |
|||
|
|||
// Error importing sirius, it's not installed 😱 |
|||
Traceback (most recent call last): |
|||
File "main.py", line 1, in <module> |
|||
import sirius |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
Но если вы деактивируете виртуальную среду разработки и активируете новую среду для `prisoner-of-askaban`, то вы тогда запустите Python из виртуального окружения `prisoner-of-azkaban`. |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ cd ~/code/prisoner-of-azkaban |
|||
|
|||
// Вам не требуется находится в старой директории для деактивации среды разработки, вы можете это сделать откуда угодно, даже из каталога другого проекта, в который вы перешли. 😎 |
|||
$ deactivate |
|||
|
|||
// Активируйте виртуальную среду разработки в prisoner-of-azkaban/.venv 🚀 |
|||
$ source .venv/bin/activate |
|||
|
|||
// Тепреь, когда вы запустите python, он найдет пакет sirius, установленный в виртуальной среде ✨ |
|||
$ python main.py |
|||
|
|||
Я торжественно клянусь в этом! 🐺 |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
## Альтернативы |
|||
|
|||
Это простое руководство поможет вам начать работу и научит тому, как всё работает **изнутри**. |
|||
|
|||
Существует много альтернативных решений для работы с виртуальными средами разработки, с программными зависимостями, а также с проектами. |
|||
|
|||
Когда вы будете готовы использовать единый инструмент для управления проектом, программными зависимостями, виртуальными средами разработки и т.д., то я рекомендую вам попробовать <a href="https://github.com/astral-sh/uv" class="external-link" target="_blank">uv</a>. |
|||
|
|||
`uv` может очень многое. Он умеет: |
|||
|
|||
* **Устанавливать Python**, включая установку различных версий |
|||
* Управлять средой виртуального окружения вашего проекта |
|||
* Устанавливать **пакеты** |
|||
* Управлять пакетами и их версиями внутри вашего проекта |
|||
* Удостовериться, что вы используете **точный** набор пакетов и версий при установке, включая зависимости. Таким образом, вы можете быть уверенны, что проект, запускается в production, точно также, как и при разработке, этот механизм называется *locking* |
|||
* Многие другие вещи |
|||
|
|||
## Заключение |
|||
|
|||
Если вы прочитали и поняли всё это, то теперь вы знаете **гораздо больше** о виртуальных средах разработки, чем многие другие разработчики. 🤓 |
|||
|
|||
Скорее всего, знание этих деталей будет полезно вам в будущем. Когда вы будете отлаживать что-то, кажущееся сложным, вы будете знать, **как это работает под капотом**. 😎 |
@ -0,0 +1,83 @@ |
|||
# FastAPI CLI |
|||
|
|||
**FastAPI CLI** це програма командного рядка, яку Ви можете використовувати, щоб обслуговувати Ваш додаток FastAPI, керувати Вашими FastApi проектами, тощо. |
|||
|
|||
Коли Ви встановлюєте FastApi (тобто виконуєте `pip install "fastapi[standard]"`), Ви також встановлюєте пакунок `fastapi-cli`, цей пакунок надає команду `fastapi` в терміналі. |
|||
|
|||
Для запуску Вашого FastAPI проекту для розробки, Ви можете скористатись командою `fastapi dev`: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ <font color="#4E9A06">fastapi</font> dev <u style="text-decoration-style:single">main.py</u> |
|||
<font color="#3465A4">INFO </font> Using path <font color="#3465A4">main.py</font> |
|||
<font color="#3465A4">INFO </font> Resolved absolute path <font color="#75507B">/home/user/code/awesomeapp/</font><font color="#AD7FA8">main.py</font> |
|||
<font color="#3465A4">INFO </font> Searching for package file structure from directories with <font color="#3465A4">__init__.py</font> files |
|||
<font color="#3465A4">INFO </font> Importing from <font color="#75507B">/home/user/code/</font><font color="#AD7FA8">awesomeapp</font> |
|||
|
|||
╭─ <font color="#8AE234"><b>Python module file</b></font> ─╮ |
|||
│ │ |
|||
│ 🐍 main.py │ |
|||
│ │ |
|||
╰──────────────────────╯ |
|||
|
|||
<font color="#3465A4">INFO </font> Importing module <font color="#4E9A06">main</font> |
|||
<font color="#3465A4">INFO </font> Found importable FastAPI app |
|||
|
|||
╭─ <font color="#8AE234"><b>Importable FastAPI app</b></font> ─╮ |
|||
│ │ |
|||
│ <span style="background-color:#272822"><font color="#FF4689">from</font></span><span style="background-color:#272822"><font color="#F8F8F2"> main </font></span><span style="background-color:#272822"><font color="#FF4689">import</font></span><span style="background-color:#272822"><font color="#F8F8F2"> app</font></span><span style="background-color:#272822"> </span> │ |
|||
│ │ |
|||
╰──────────────────────────╯ |
|||
|
|||
<font color="#3465A4">INFO </font> Using import string <font color="#8AE234"><b>main:app</b></font> |
|||
|
|||
<span style="background-color:#C4A000"><font color="#2E3436">╭────────── FastAPI CLI - Development mode ───────────╮</font></span> |
|||
<span style="background-color:#C4A000"><font color="#2E3436">│ │</font></span> |
|||
<span style="background-color:#C4A000"><font color="#2E3436">│ Serving at: http://127.0.0.1:8000 │</font></span> |
|||
<span style="background-color:#C4A000"><font color="#2E3436">│ │</font></span> |
|||
<span style="background-color:#C4A000"><font color="#2E3436">│ API docs: http://127.0.0.1:8000/docs │</font></span> |
|||
<span style="background-color:#C4A000"><font color="#2E3436">│ │</font></span> |
|||
<span style="background-color:#C4A000"><font color="#2E3436">│ Running in development mode, for production use: │</font></span> |
|||
<span style="background-color:#C4A000"><font color="#2E3436">│ │</font></span> |
|||
<span style="background-color:#C4A000"><font color="#2E3436">│ </font></span><span style="background-color:#C4A000"><font color="#555753"><b>fastapi run</b></font></span><span style="background-color:#C4A000"><font color="#2E3436"> │</font></span> |
|||
<span style="background-color:#C4A000"><font color="#2E3436">│ │</font></span> |
|||
<span style="background-color:#C4A000"><font color="#2E3436">╰─────────────────────────────────────────────────────╯</font></span> |
|||
|
|||
<font color="#4E9A06">INFO</font>: Will watch for changes in these directories: ['/home/user/code/awesomeapp'] |
|||
<font color="#4E9A06">INFO</font>: Uvicorn running on <b>http://127.0.0.1:8000</b> (Press CTRL+C to quit) |
|||
<font color="#4E9A06">INFO</font>: Started reloader process [<font color="#34E2E2"><b>2265862</b></font>] using <font color="#34E2E2"><b>WatchFiles</b></font> |
|||
<font color="#4E9A06">INFO</font>: Started server process [<font color="#06989A">2265873</font>] |
|||
<font color="#4E9A06">INFO</font>: Waiting for application startup. |
|||
<font color="#4E9A06">INFO</font>: Application startup complete. |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
Програма командного рядка `fastapi` це **FastAPI CLI**. |
|||
|
|||
FastAPI CLI приймає шлях до Вашої Python програми (напр. `main.py`) і автоматично виявляє екземпляр `FastAPI` (зазвичай названий `app`), обирає коректний процес імпорту, а потім обслуговує його. |
|||
|
|||
Натомість, для запуску у продакшн використовуйте `fastapi run`. 🚀 |
|||
|
|||
Всередині **FastAPI CLI** використовує <a href="https://www.uvicorn.org" class="external-link" target="_blank">Uvicorn</a>, високопродуктивний, production-ready, ASGI cервер. 😎 |
|||
|
|||
## `fastapi dev` |
|||
|
|||
Використання `fastapi dev` ініціює режим розробки. |
|||
|
|||
За замовчуванням, **автоматичне перезавантаження** увімкнене, автоматично перезавантажуючи сервер кожного разу, коли Ви змінюєте Ваш код. Це ресурсо-затратно, та може бути менш стабільним, ніж коли воно вимкнене. Ви повинні використовувати його тільки під час розробки. Воно також слухає IP-адресу `127.0.0.1`, що є IP Вашого девайсу для самостійної комунікації з самим собою (`localhost`). |
|||
|
|||
## `fastapi run` |
|||
|
|||
Виконання `fastapi run` запустить FastAPI у продакшн-режимі за замовчуванням. |
|||
|
|||
За замовчуванням, **автоматичне перезавантаження** вимкнене. Воно також прослуховує IP-адресу `0.0.0.0`, що означає всі доступні IP адреси, тим самим даючи змогу будь-кому комунікувати з девайсом. Так Ви зазвичай будете запускати його у продакшн, наприклад у контейнері. |
|||
|
|||
В більшості випадків Ви можете (і маєте) мати "termination proxy", який обробляє HTTPS для Вас, це залежить від способу розгортання вашого додатку, Ваш провайдер може зробити це для Вас, або Вам потрібно налаштувати його самостійно. |
|||
|
|||
/// tip |
|||
|
|||
Ви можете дізнатись більше про це у [документації про розгортування](deployment/index.md){.internal-link target=_blank}. |
|||
|
|||
/// |
@ -0,0 +1,442 @@ |
|||
# 並行與 async / await |
|||
|
|||
有關*路徑操作函式*的 `async def` 語法的細節與非同步 (asynchronous) 程式碼、並行 (concurrency) 與平行 (parallelism) 的一些背景知識。 |
|||
|
|||
## 趕時間嗎? |
|||
|
|||
<abbr title="too long; didn't read(文長慎入)"><strong>TL;DR:</strong></abbr> |
|||
|
|||
如果你正在使用要求你以 `await` 語法呼叫的第三方函式庫,例如: |
|||
|
|||
```Python |
|||
results = await some_library() |
|||
``` |
|||
|
|||
然後,使用 `async def` 宣告你的*路徑操作函式*: |
|||
|
|||
|
|||
```Python hl_lines="2" |
|||
@app.get('/') |
|||
async def read_results(): |
|||
results = await some_library() |
|||
return results |
|||
``` |
|||
|
|||
/// note | 注意 |
|||
|
|||
你只能在 `async def` 建立的函式內使用 `await`。 |
|||
|
|||
/// |
|||
|
|||
--- |
|||
|
|||
如果你使用的是第三方函式庫並且它需要與某些外部資源(例如資料庫、API、檔案系統等)進行通訊,但不支援 `await`(目前大多數資料庫函式庫都是這樣),在這種情況下,你可以像平常一樣使用 `def` 宣告*路徑操作函式*,如下所示: |
|||
|
|||
```Python hl_lines="2" |
|||
@app.get('/') |
|||
def results(): |
|||
results = some_library() |
|||
return results |
|||
``` |
|||
|
|||
--- |
|||
|
|||
如果你的應用程式不需要與外部資源進行任何通訊並等待其回應,請使用 `async def`。 |
|||
|
|||
--- |
|||
|
|||
如果你不確定該用哪個,直接用 `def` 就好。 |
|||
|
|||
--- |
|||
|
|||
**注意**:你可以在*路徑操作函式*中混合使用 `def` 和 `async def` ,並使用最適合你需求的方式來定義每個函式。FastAPI 會幫你做正確的處理。 |
|||
|
|||
無論如何,在上述哪種情況下,FastAPI 仍將以非同步方式運行,並且速度非常快。 |
|||
|
|||
但透過遵循上述步驟,它將能進行一些效能最佳化。 |
|||
|
|||
## 技術細節 |
|||
|
|||
現代版本的 Python 支援使用 **「協程」** 的 **`async` 和 `await`** 語法來寫 **「非同步程式碼」**。 |
|||
|
|||
接下來我們逐一介紹: |
|||
|
|||
* **非同步程式碼** |
|||
* **`async` 和 `await`** |
|||
* **協程** |
|||
|
|||
## 非同步程式碼 |
|||
|
|||
非同步程式碼僅意味著程式語言 💬 有辦法告訴電腦/程式 🤖 在程式碼中的某個點,它 🤖 需要等待某些事情完成。讓我們假設這些事情被稱為「慢速檔案」📝。 |
|||
|
|||
因此,在等待「慢速檔案」📝 完成的這段時間,電腦可以去處理一些其他工作。 |
|||
|
|||
接著程式 🤖 會在有空檔時回來查看是否有等待的工作已經完成,並執行必要的後續操作。 |
|||
|
|||
接下來,它 🤖 完成第一個工作(例如我們的「慢速檔案」📝)並繼續執行相關的所有操作。 |
|||
這個「等待其他事情」通常指的是一些相對較慢的(與處理器和 RAM 記憶體的速度相比)的 <abbr title="Input and Output">I/O</abbr> 操作,比如說: |
|||
|
|||
* 透過網路傳送來自用戶端的資料 |
|||
* 從網路接收來自用戶端的資料 |
|||
* 從磁碟讀取檔案內容 |
|||
* 將內容寫入磁碟 |
|||
* 遠端 API 操作 |
|||
* 資料庫操作 |
|||
* 資料庫查詢 |
|||
* 等等 |
|||
|
|||
由於大部分的執行時間都消耗在等待 <abbr title="輸入與輸出">I/O</abbr> 操作上,因此這些操作被稱為 "I/O 密集型" 操作。 |
|||
|
|||
之所以稱為「非同步」,是因為電腦/程式不需要與那些耗時的任務「同步」,等待任務完成的精確時間,然後才能取得結果並繼續工作。 |
|||
|
|||
相反地,非同步系統在任務完成後,可以讓任務稍微等一下(幾微秒),等待電腦/程式完成手頭上的其他工作,然後再回來取得結果繼續進行。 |
|||
|
|||
相對於「非同步」(asynchronous),「同步」(synchronous)也常被稱作「順序性」(sequential),因為電腦/程式會依序執行所有步驟,即便這些步驟涉及等待,才會切換到其他任務。 |
|||
|
|||
### 並行與漢堡 |
|||
|
|||
上述非同步程式碼的概念有時也被稱為「並行」,它不同於「平行」。 |
|||
|
|||
並行和平行都與 "不同的事情或多或少同時發生" 有關。 |
|||
|
|||
但並行和平行之間的細節是完全不同的。 |
|||
|
|||
為了理解差異,請想像以下有關漢堡的故事: |
|||
|
|||
### 並行漢堡 |
|||
|
|||
你和你的戀人去速食店,排隊等候時,收銀員正在幫排在你前面的人點餐。😍 |
|||
|
|||
<img src="/img/async/concurrent-burgers/concurrent-burgers-01.png" class="illustration"> |
|||
|
|||
輪到你了,你給你與你的戀人點了兩個豪華漢堡。🍔🍔 |
|||
|
|||
<img src="/img/async/concurrent-burgers/concurrent-burgers-02.png" class="illustration"> |
|||
|
|||
收銀員通知廚房準備你的漢堡(儘管他們還在為前面其他顧客準備食物)。 |
|||
|
|||
<img src="/img/async/concurrent-burgers/concurrent-burgers-03.png" class="illustration"> |
|||
|
|||
之後你完成付款。💸 |
|||
|
|||
收銀員給你一個號碼牌。 |
|||
|
|||
<img src="/img/async/concurrent-burgers/concurrent-burgers-04.png" class="illustration"> |
|||
|
|||
在等待漢堡的同時,你可以與戀人選一張桌子,然後坐下來聊很長一段時間(因為漢堡十分豪華,準備特別費工。) |
|||
|
|||
這段時間,你還能欣賞你的戀人有多麼的可愛、聰明與迷人。✨😍✨ |
|||
|
|||
<img src="/img/async/concurrent-burgers/concurrent-burgers-05.png" class="illustration"> |
|||
|
|||
當你和戀人邊聊天邊等待時,你會不時地查看櫃檯上的顯示的號碼,確認是否已經輪到你了。 |
|||
|
|||
然後在某個時刻,終於輪到你了。你走到櫃檯,拿了漢堡,然後回到桌子上。 |
|||
|
|||
<img src="/img/async/concurrent-burgers/concurrent-burgers-06.png" class="illustration"> |
|||
|
|||
你和戀人享用這頓大餐,整個過程十分開心✨ |
|||
|
|||
<img src="/img/async/concurrent-burgers/concurrent-burgers-07.png" class="illustration"> |
|||
|
|||
/// info |
|||
|
|||
漂亮的插畫來自 <a href="https://www.instagram.com/ketrinadrawsalot" class="external-link" target="_blank">Ketrina Thompson</a>. 🎨 |
|||
|
|||
/// |
|||
|
|||
--- |
|||
|
|||
想像你是故事中的電腦或程式 🤖。 |
|||
|
|||
當你排隊時,你在放空😴,等待輪到你,沒有做任何「生產性」的事情。但這沒關係,因為收銀員只是接單(而不是準備食物),所以排隊速度很快。 |
|||
|
|||
然後,當輪到你時,你開始做真正「有生產力」的工作,處理菜單,決定你想要什麼,替戀人選擇餐點,付款,確認你給了正確的帳單或信用卡,檢查你是否被正確收費,確認訂單中的項目是否正確等等。 |
|||
|
|||
但是,即使你還沒有拿到漢堡,你與收銀員的工作已經「暫停」了 ⏸,因為你必須等待 🕙 漢堡準備好。 |
|||
|
|||
但當你離開櫃檯,坐到桌子旁,拿著屬於你的號碼等待時,你可以把注意力 🔀 轉移到戀人身上,並開始「工作」⏯ 🤓——也就是和戀人調情 😍。這時你又開始做一些非常「有生產力」的事情。 |
|||
|
|||
接著,收銀員 💁 將你的號碼顯示在櫃檯螢幕上,並告訴你「漢堡已經做好了」。但你不會瘋狂地立刻跳起來,因為顯示的號碼變成了你的。你知道沒有人會搶走你的漢堡,因為你有自己的號碼,他們也有他們的號碼。 |
|||
|
|||
所以你會等戀人講完故事(完成當前的工作 ⏯/正在進行的任務 🤓),然後微笑著溫柔地說你要去拿漢堡了 ⏸。 |
|||
|
|||
然後你走向櫃檯 🔀,回到已經完成的最初任務 ⏯,拿起漢堡,說聲謝謝,並帶回桌上。這就結束了與櫃檯的互動步驟/任務 ⏹,接下來會產生一個新的任務,「吃漢堡」 🔀 ⏯,而先前的「拿漢堡」任務已經完成了 ⏹。 |
|||
|
|||
### 平行漢堡 |
|||
|
|||
現在,讓我們來想像這裡不是「並行漢堡」,而是「平行漢堡」。 |
|||
|
|||
你和戀人一起去吃平行的速食餐。 |
|||
|
|||
你們站在隊伍中,前面有幾位(假設有 8 位)既是收銀員又是廚師的員工,他們同時接單並準備餐點。 |
|||
|
|||
所有排在你前面的人都在等著他們的漢堡準備好後才會離開櫃檯,因為每位收銀員在接完單後,馬上會去準備漢堡,然後才回來處理下一個訂單。 |
|||
|
|||
<img src="/img/async/parallel-burgers/parallel-burgers-01.png" class="illustration"> |
|||
|
|||
終於輪到你了,你為你和你的戀人點了兩個非常豪華的漢堡。 |
|||
|
|||
你付款了 💸。 |
|||
|
|||
<img src="/img/async/parallel-burgers/parallel-burgers-02.png" class="illustration"> |
|||
|
|||
收銀員走進廚房準備食物。 |
|||
|
|||
你站在櫃檯前等待 🕙,以免其他人先拿走你的漢堡,因為這裡沒有號碼牌系統。 |
|||
|
|||
<img src="/img/async/parallel-burgers/parallel-burgers-03.png" class="illustration"> |
|||
|
|||
由於你和戀人都忙著不讓別人搶走你的漢堡,等漢堡準備好時,你根本無法專心和戀人互動。😞 |
|||
|
|||
這是「同步」(synchronous)工作,你和收銀員/廚師 👨🍳 是「同步化」的。你必須等到 🕙 收銀員/廚師 👨🍳 完成漢堡並交給你的那一刻,否則別人可能會拿走你的餐點。 |
|||
|
|||
<img src="/img/async/parallel-burgers/parallel-burgers-04.png" class="illustration"> |
|||
|
|||
最終,經過長時間的等待 🕙,收銀員/廚師 👨🍳 拿著漢堡回來了。 |
|||
|
|||
<img src="/img/async/parallel-burgers/parallel-burgers-05.png" class="illustration"> |
|||
|
|||
你拿著漢堡,和你的戀人回到餐桌。 |
|||
|
|||
你們僅僅是吃完漢堡,然後就結束了。⏹ |
|||
|
|||
<img src="/img/async/parallel-burgers/parallel-burgers-06.png" class="illustration"> |
|||
|
|||
整個過程中沒有太多的談情說愛,因為大部分時間 🕙 都花在櫃檯前等待。😞 |
|||
|
|||
/// info |
|||
|
|||
漂亮的插畫來自 <a href="https://www.instagram.com/ketrinadrawsalot" class="external-link" target="_blank">Ketrina Thompson</a>. 🎨 |
|||
|
|||
/// |
|||
|
|||
--- |
|||
|
|||
在這個平行漢堡的情境下,你是一個程式 🤖 且有兩個處理器(你和戀人),兩者都在等待 🕙 並專注於等待櫃檯上的餐點 🕙,等待的時間非常長。 |
|||
|
|||
這家速食店有 8 個處理器(收銀員/廚師)。而並行漢堡店可能只有 2 個處理器(一位收銀員和一位廚師)。 |
|||
|
|||
儘管如此,最終的體驗並不是最理想的。😞 |
|||
|
|||
--- |
|||
|
|||
這是與漢堡類似的故事。🍔 |
|||
|
|||
一個更「現實」的例子,想像一間銀行。 |
|||
|
|||
直到最近,大多數銀行都有多位出納員 👨💼👨💼👨💼👨💼,以及一條長長的隊伍 🕙🕙🕙🕙🕙🕙🕙🕙。 |
|||
|
|||
所有的出納員都在一個接一個地滿足每位客戶的所有需求 👨💼⏯。 |
|||
|
|||
你必須長時間排隊 🕙,不然就會失去機會。 |
|||
|
|||
所以,你不會想帶你的戀人 😍 一起去銀行辦事 🏦。 |
|||
|
|||
### 漢堡結論 |
|||
|
|||
在「和戀人一起吃速食漢堡」的這個場景中,由於有大量的等待 🕙,使用並行系統 ⏸🔀⏯ 更有意義。 |
|||
|
|||
這也是大多數 Web 應用的情況。 |
|||
|
|||
許多用戶正在使用你的應用程式,而你的伺服器則在等待 🕙 這些用戶不那麼穩定的網路來傳送請求。 |
|||
|
|||
接著,再次等待 🕙 回應。 |
|||
|
|||
這種「等待」 🕙 通常以微秒來衡量,但累加起來,最終還是花費了很多等待時間。 |
|||
|
|||
這就是為什麼對於 Web API 來說,使用非同步程式碼 ⏸🔀⏯ 是非常有意義的。 |
|||
|
|||
這種類型的非同步性正是 NodeJS 成功的原因(儘管 NodeJS 不是平行的),這也是 Go 語言作為程式語言的一個強大優勢。 |
|||
|
|||
這與 **FastAPI** 所能提供的性能水平相同。 |
|||
|
|||
你可以同時利用並行性和平行性,進一步提升效能,這比大多數已測試的 NodeJS 框架都更快,並且與 Go 語言相當,而 Go 是一種更接近 C 的編譯語言(<a href="https://www.techempower.com/benchmarks/#section=data-r17&hw=ph&test=query&l=zijmkf-1" class="external-link" target="_blank">感謝 Starlette</a>)。 |
|||
|
|||
### 並行比平行更好嗎? |
|||
|
|||
不是的!這不是故事的本意。 |
|||
|
|||
並行與平行不同。並行在某些 **特定** 的需要大量等待的情境下表現更好。正因如此,並行在 Web 應用程式開發中通常比平行更有優勢。但並不是所有情境都如此。 |
|||
|
|||
因此,為了平衡報導,想像下面這個短故事 |
|||
|
|||
> 你需要打掃一間又大又髒的房子。 |
|||
|
|||
*是的,這就是全部的故事。* |
|||
|
|||
--- |
|||
|
|||
這裡沒有任何需要等待 🕙 的地方,只需要在房子的多個地方進行大量的工作。 |
|||
|
|||
你可以像漢堡的例子那樣輪流進行,先打掃客廳,再打掃廚房,但由於你不需要等待 🕙 任何事情,只需要持續地打掃,輪流並不會影響任何結果。 |
|||
|
|||
無論輪流執行與否(並行),你都需要相同的工時完成任務,同時需要執行相同工作量。 |
|||
|
|||
但是,在這種情境下,如果你可以邀請8位前收銀員/廚師(現在是清潔工)來幫忙,每個人(加上你)負責房子的某個區域,這樣你就可以 **平行** 地更快完成工作。 |
|||
|
|||
在這個場景中,每個清潔工(包括你)都是一個處理器,完成工作的一部分。 |
|||
|
|||
由於大多數的執行時間都花在實際的工作上(而不是等待),而電腦中的工作由 <abbr title="Central Processing Unit">CPU</abbr> 完成,因此這些問題被稱為「CPU 密集型」。 |
|||
|
|||
--- |
|||
|
|||
常見的 CPU 密集型操作範例包括那些需要進行複雜數學計算的任務。 |
|||
|
|||
例如: |
|||
|
|||
* **音訊**或**圖像處理**; |
|||
* **電腦視覺**:一張圖片由數百萬個像素組成,每個像素有 3 個值/顏色,處理這些像素通常需要同時進行大量計算; |
|||
* **機器學習**: 通常需要大量的「矩陣」和「向量」運算。想像一個包含數字的巨大電子表格,並所有的數字同時相乘; |
|||
* **深度學習**: 這是機器學習的子領域,同樣適用。只不過這不僅僅是一張數字表格,而是大量的數據集合,並且在很多情況下,你會使用特殊的處理器來構建或使用這些模型。 |
|||
|
|||
### 並行 + 平行: Web + 機器學習 |
|||
|
|||
使用 **FastAPI**,你可以利用並行的優勢,這在 Web 開發中非常常見(這也是 NodeJS 的最大吸引力)。 |
|||
|
|||
但你也可以利用平行與多行程 (multiprocessing)(讓多個行程同時運行) 的優勢來處理機器學習系統中的 **CPU 密集型**工作。 |
|||
|
|||
這一點,再加上 Python 是 **資料科學**、機器學習,尤其是深度學習的主要語言,讓 **FastAPI** 成為資料科學/機器學習 Web API 和應用程式(以及許多其他應用程式)的絕佳選擇。 |
|||
|
|||
想了解如何在生產環境中實現這種平行性,請參見 [部屬](deployment/index.md){.internal-link target=_blank}。 |
|||
|
|||
## `async` 和 `await` |
|||
|
|||
現代 Python 版本提供一種非常直觀的方式定義非同步程式碼。這使得它看起來就像正常的「順序」程式碼,並在適當的時機「等待」。 |
|||
|
|||
當某個操作需要等待才能回傳結果,並且支援這些新的 Python 特性時,你可以像這樣編寫程式碼: |
|||
|
|||
```Python |
|||
burgers = await get_burgers(2) |
|||
``` |
|||
|
|||
這裡的關鍵是 `await`。它告訴 Python 必須等待 ⏸ `get_burgers(2)` 完成它的工作 🕙, 然後將結果儲存在 `burgers` 中。如此,Python 就可以在此期間去處理其他事情 🔀 ⏯ (例如接收另一個請求)。 |
|||
|
|||
要讓 `await` 運作,它必須位於支持非同步功能的函式內。為此,只需使用 `async def` 宣告函式: |
|||
|
|||
```Python hl_lines="1" |
|||
async def get_burgers(number: int): |
|||
# Do some asynchronous stuff to create the burgers |
|||
return burgers |
|||
``` |
|||
|
|||
...而不是 `def`: |
|||
|
|||
```Python hl_lines="2" |
|||
# This is not asynchronous |
|||
def get_sequential_burgers(number: int): |
|||
# Do some sequential stuff to create the burgers |
|||
return burgers |
|||
``` |
|||
|
|||
使用 `async def`,Python Python 知道在該函式內需要注意 `await`,並且它可以「暫停」 ⏸ 執行該函式,然後執行其他任務 🔀 後回來。 |
|||
|
|||
當你想要呼叫 `async def` 函式時,必須使用「await」。因此,這樣寫將無法運行: |
|||
|
|||
```Python |
|||
# This won't work, because get_burgers was defined with: async def |
|||
burgers = get_burgers(2) |
|||
``` |
|||
|
|||
--- |
|||
|
|||
如果你正在使用某個函式庫,它告訴你可以使用 `await` 呼叫它,那麼你需要用 `async def` 定義*路徑操作函式*,如: |
|||
|
|||
```Python hl_lines="2-3" |
|||
@app.get('/burgers') |
|||
async def read_burgers(): |
|||
burgers = await get_burgers(2) |
|||
return burgers |
|||
``` |
|||
|
|||
### 更多技術細節 |
|||
|
|||
你可能已經注意到,`await` 只能在 `async def` 定義的函式內使用。 |
|||
|
|||
但同時,使用 `async def` 定義的函式本身也必須被「等待」。所以,帶有 `async def` 函式只能在其他使用 `async def` 定義的函式內呼叫。 |
|||
|
|||
那麼,這就像「先有雞還是先有蛋」的問題,要如何呼叫第一個 `async` 函式呢? |
|||
|
|||
如果你使用 FastAPI,無需擔心這個問題,因為「第一個」函式將是你的*路徑操作函式*,FastAPI 會知道如何正確處理這個問題。 |
|||
|
|||
但如果你想在沒有 FastAPI 的情況下使用 `async` / `await`,你也可以這樣做。 |
|||
|
|||
### 編寫自己的非同步程式碼 |
|||
|
|||
Starlette (和 **FastAPI**) 是基於 <a href="https://anyio.readthedocs.io/en/stable/" class="external-link" target="_blank">AnyIO</a> 實作的,這使得它們與 Python 標準函式庫相容 <a href="https://docs.python.org/3/library/asyncio-task.html" class="external-link" target="_blank">asyncio</a> 和 <a href="https://trio.readthedocs.io/en/stable/" class="external-link" target="_blank">Trio</a>。 |
|||
|
|||
特別是,你可以直接使用 <a href="https://anyio.readthedocs.io/en/stable/" class="external-link" target="_blank">AnyIO</a> 來處理更複雜的並行使用案例,這些案例需要你在自己的程式碼中使用更高階的模式。 |
|||
|
|||
即使你不使用 **FastAPI**,你也可以使用 <a href="https://anyio.readthedocs.io/en/stable/" class="external-link" target="_blank">AnyIO</a> 來撰寫自己的非同步應用程式,並獲得高相容性及一些好處(例如結構化並行)。 |
|||
|
|||
### 其他形式的非同步程式碼 |
|||
|
|||
使用 `async` 和 `await` 的風格在語言中相對較新。 |
|||
|
|||
但它使處理異步程式碼變得更加容易。 |
|||
|
|||
相同的語法(或幾乎相同的語法)最近也被包含在現代 JavaScript(無論是瀏覽器還是 NodeJS)中。 |
|||
|
|||
但在此之前,處理異步程式碼要更加複雜和困難。 |
|||
|
|||
在較舊的 Python 版本中,你可能會使用多執行緒或 <a href="https://www.gevent.org/" class="external-link" target="_blank">Gevent</a>。但這些程式碼要更難以理解、調試和思考。 |
|||
|
|||
在較舊的 NodeJS / 瀏覽器 JavaScript 中,你會使用「回呼」,這可能會導致<a href="http://callbackhell.com/" class="external-link" target="_blank">回呼地獄</a>。 |
|||
|
|||
## 協程 |
|||
|
|||
**協程** 只是 `async def` 函式所回傳的非常特殊的事物名稱。Python 知道它是一個類似函式的東西,可以啟動它,並且在某個時刻它會結束,但它也可能在內部暫停 ⏸,只要遇到 `await`。 |
|||
|
|||
這種使用 `async` 和 `await` 的非同步程式碼功能通常被概括為「協程」。這與 Go 語言的主要特性「Goroutines」相似。 |
|||
|
|||
## 結論 |
|||
|
|||
讓我們再次回顧之前的句子: |
|||
|
|||
> 現代版本的 Python 支持使用 **"協程"** 的 **`async` 和 `await`** 語法來寫 **"非同步程式碼"**。 |
|||
|
|||
現在應該能明白其含意了。✨ |
|||
|
|||
這些就是驅動 FastAPI(通過 Starlette)運作的原理,也讓它擁有如此驚人的效能。 |
|||
|
|||
## 非常技術性的細節 |
|||
|
|||
/// warning |
|||
|
|||
你大概可以跳過這段。 |
|||
|
|||
這裡是有關 FastAPI 內部技術細節。 |
|||
|
|||
如果你有相當多的技術背景(例如協程、執行緒、阻塞等),並且對 FastAPI 如何處理 `async def` 與常規 `def` 感到好奇,請繼續閱讀。 |
|||
|
|||
/// |
|||
|
|||
### 路徑操作函数 |
|||
|
|||
當你使用 `def` 而不是 `async def` 宣告*路徑操作函式*時,該函式會在外部的執行緒池(threadpool)中執行,然後等待結果,而不是直接呼叫(因為這樣會阻塞伺服器)。 |
|||
|
|||
如果你來自於其他不以這種方式運作的非同步框架,而且你習慣於使用普通的 `def` 定義僅進行簡單計算的*路徑操作函式*,目的是獲得微小的性能增益(大約 100 奈秒),請注意,在 FastAPI 中,效果會完全相反。在這些情況下,最好使用 `async def`除非你的*路徑操作函式*執行阻塞的 <abbr title="輸入/輸出:磁碟讀寫或網路通訊">I/O</abbr> 的程式碼。 |
|||
|
|||
不過,在這兩種情況下,**FastAPI** [仍然很快](index.md#_11){.internal-link target=_blank}至少與你之前的框架相當(或者更快)。 |
|||
|
|||
### 依賴項(Dependencies) |
|||
|
|||
同樣適用於[依賴項](tutorial/dependencies/index.md){.internal-link target=_blank}。如果依賴項是一個標準的 `def` 函式,而不是 `async def`,那麼它在外部的執行緒池被運行。 |
|||
|
|||
### 子依賴項 |
|||
|
|||
你可以擁有多個相互依賴的依賴項和[子依賴項](tutorial/dependencies/sub-dependencies.md){.internal-link target=_blank} (作為函式定義的參數),其中一些可能是用 `async def` 宣告,也可能是用 `def` 宣告。它們仍然可以正常運作,用 `def` 定義的那些將會在外部的執行緒中呼叫(來自執行緒池),而不是被「等待」。 |
|||
|
|||
### 其他輔助函式 |
|||
|
|||
你可以直接呼叫任何使用 `def` 或 `async def` 建立的其他輔助函式,FastAPI 不會影響你呼叫它們的方式。 |
|||
|
|||
這與 FastAPI 為你呼叫*路徑操作函式*和依賴項的邏輯有所不同。 |
|||
|
|||
如果你的輔助函式是用 `def` 宣告的,它將會被直接呼叫(按照你在程式碼中撰寫的方式),而不是在執行緒池中。如果該函式是用 `async def` 宣告,那麼你在呼叫時應該使用 `await` 等待其結果。 |
|||
|
|||
--- |
|||
|
|||
再一次強調,這些都是非常技術性的細節,如果你特地在尋找這些資訊,這些內容可能會對你有幫助。 |
|||
|
|||
否則,只需遵循上面提到的指引即可:<a href="#_1">趕時間嗎?</a>. |
@ -0,0 +1,207 @@ |
|||
# 特性 |
|||
|
|||
## FastAPI 特性 |
|||
|
|||
**FastAPI** 提供了以下内容: |
|||
|
|||
### 建立在開放標準的基礎上 |
|||
|
|||
* 使用 <a href="https://github.com/OAI/OpenAPI-Specification" class="external-link" target="_blank"><strong>OpenAPI</strong></a> 來建立 API,包含<abbr title="path,也被叫做: endpoints, routes">路徑</abbr><abbr title="也叫做 HTTP 方法,例如 POST, GET, PUT, DELETE">操作</abbr>、參數、請求內文、安全性等聲明。 |
|||
* 使用 <a href="https://json-schema.org/" class="external-link" target="_blank"><strong>JSON Schema</strong></a>(因為 OpenAPI 本身就是基於 JSON Schema)自動生成資料模型文件。 |
|||
* 經過縝密的研究後圍繞這些標準進行設計,而不是事後在已有系統上附加的一層功能。 |
|||
* 這也讓我們在多種語言中可以使用自動**用戶端程式碼生成**。 |
|||
|
|||
### 能夠自動生成文件 |
|||
|
|||
FastAPI 能生成互動式 API 文件和探索性的 Web 使用者介面。由於該框架基於 OpenAPI,因此有多種選擇,預設提供了兩種。 |
|||
|
|||
* <a href="https://github.com/swagger-api/swagger-ui" class="external-link" target="_blank"><strong>Swagger UI</strong></a> 提供互動式探索,讓你可以直接從瀏覽器呼叫並測試你的 API 。 |
|||
|
|||
 |
|||
|
|||
* <a href="https://github.com/Rebilly/ReDoc" class="external-link" target="_blank"><strong>ReDoc</strong></a> 提供結構性的文件,讓你可以在瀏覽器中查看。 |
|||
|
|||
 |
|||
|
|||
|
|||
### 現代 Python |
|||
|
|||
這一切都基於標準的 **Python 型別**宣告(感謝 Pydantic)。無需學習新的語法,只需使用標準的現代 Python。 |
|||
|
|||
如果你需要 2 分鐘來學習如何使用 Python 型別(即使你不使用 FastAPI),可以看看這個簡短的教學:[Python 型別](python-types.md){.internal-link target=_blank}。 |
|||
|
|||
如果你寫帶有 Python 型別的程式碼: |
|||
|
|||
```python |
|||
from datetime import date |
|||
|
|||
from pydantic import BaseModel |
|||
|
|||
# 宣告一個變數為 string |
|||
# 並在函式中獲得 editor support |
|||
def main(user_id: str): |
|||
return user_id |
|||
|
|||
|
|||
# 宣告一個 Pydantic model |
|||
class User(BaseModel): |
|||
id: int |
|||
name: str |
|||
joined: date |
|||
``` |
|||
|
|||
|
|||
可以像這樣來使用: |
|||
|
|||
```python |
|||
my_user: User = User(id=3, name="John Doe", joined="2018-07-19") |
|||
|
|||
second_user_data = { |
|||
"id": 4, |
|||
"name": "Mary", |
|||
"joined": "2018-11-30", |
|||
} |
|||
|
|||
my_second_user: User = User(**second_user_data) |
|||
``` |
|||
|
|||
|
|||
/// info |
|||
|
|||
`**second_user_data` 意思是: |
|||
|
|||
將 `second_user_data` 字典直接作為 key-value 引數傳遞,等同於:`User(id=4, name="Mary", joined="2018-11-30")` |
|||
|
|||
/// |
|||
|
|||
### 多種編輯器支援 |
|||
|
|||
整個框架的設計是為了讓使用變得簡單且直觀,在開始開發之前,所有決策都在多個編輯器上進行了測試,以確保提供最佳的開發體驗。 |
|||
|
|||
在最近的 Python 開發者調查中,我們能看到<a href="https://www.jetbrains.com/research/python-developers-survey-2017/#tools-and-features" class="external-link" target="_blank"> 被使用最多的功能是 autocompletion</a>,此功能可以預測將要輸入文字,並自動補齊。 |
|||
|
|||
整個 **FastAPI** 框架就是基於這一點,任何地方都可以進行自動補齊。 |
|||
|
|||
你幾乎不需要經常來回看文件。 |
|||
|
|||
在這裡,你的編輯器可能會這樣幫助你: |
|||
|
|||
* <a href="https://code.visualstudio.com/" class="external-link" target="_blank">Visual Studio Code</a> 中: |
|||
|
|||
 |
|||
|
|||
* <a href="https://www.jetbrains.com/pycharm/" class="external-link" target="_blank">PyCharm</a> 中: |
|||
|
|||
 |
|||
|
|||
你將能進行程式碼補齊,這是在之前你可能曾認為不可能的事。例如,請求 JSON body(可能是巢狀的)中的鍵 `price`。 |
|||
|
|||
這樣比較不會輸錯鍵名,不用來回翻看文件,也不用來回滾動尋找你最後使用的 `username` 或者 `user_name`。 |
|||
|
|||
|
|||
|
|||
### 簡潔 |
|||
|
|||
FastAPI 為你提供了**預設值**,讓你不必在初期進行繁瑣的配置,一切都可以自動運作。如果你有更具體的需求,則可以進行調整和自定義, |
|||
|
|||
但在大多數情況下,你只需要直接使用預設值,就能順利完成 API 開發。 |
|||
|
|||
### 驗證 |
|||
|
|||
所有的驗證都由完善且強大的 **Pydantic** 處理。 |
|||
|
|||
* 驗證大部分(甚至所有?)的 Python **資料型別**,包括: |
|||
* JSON 物件 (`dict`)。 |
|||
* JSON 陣列 (`list`) 定義項目型別。 |
|||
* 字串 (`str`) 欄位,定義最小或最大長度。 |
|||
* 數字 (`int`, `float`) 與其最大值和最小值等。 |
|||
|
|||
* 驗證外來的型別,比如: |
|||
* URL |
|||
* Email |
|||
* UUID |
|||
|
|||
|
|||
### 安全性及身份驗證 |
|||
|
|||
FastAPI 已經整合了安全性和身份驗證的功能,但不會強制與特定的資料庫或資料模型進行綁定。 |
|||
|
|||
OpenAPI 中定義的安全模式,包括: |
|||
|
|||
* HTTP 基本認證。 |
|||
* **OAuth2**(也使用 **JWT tokens**)。在 [OAuth2 with JWT](tutorial/security/oauth2-jwt.md){.internal-link target=_blank} 查看教學。 |
|||
* API 密鑰,在: |
|||
* 標頭(Header) |
|||
* 查詢參數 |
|||
* Cookies,等等。 |
|||
|
|||
加上来自 Starlette(包括 **session cookie**)的所有安全特性。 |
|||
|
|||
所有的這些都是可重複使用的工具和套件,可以輕鬆與你的系統、資料儲存(Data Stores)、關聯式資料庫(RDBMS)以及非關聯式資料庫(NoSQL)等等整合。 |
|||
|
|||
|
|||
### 依賴注入(Dependency Injection) |
|||
|
|||
FastAPI 有一個使用簡單,但是非常強大的<abbr title='也叫做 "components", "resources", "services", "providers"'><strong>依賴注入</strong></abbr>系統。 |
|||
|
|||
* 依賴項甚至可以有自己的依賴,從而形成一個層級或**依賴圖**的結構。 |
|||
* 所有**自動化處理**都由框架完成。 |
|||
* 依賴項不僅能從請求中提取資料,還能**對 API 的路徑操作進行強化**,並自動生成文檔。 |
|||
* 即使是依賴項中定義的*路徑操作參數*,也會**自動進行驗證**。 |
|||
* 支持複雜的用戶身份驗證系統、**資料庫連接**等。 |
|||
* 不與資料庫、前端等進行強制綁定,但能輕鬆整合它們。 |
|||
|
|||
|
|||
### 無限制「擴充功能」 |
|||
|
|||
或者說,無需其他額外配置,直接導入並使用你所需要的程式碼。 |
|||
|
|||
任何整合都被設計得非常簡單易用(通過依賴注入),你只需用與*路徑操作*相同的結構和語法,用兩行程式碼就能為你的應用程式建立一個「擴充功能」。 |
|||
|
|||
|
|||
### 測試 |
|||
|
|||
* 100% 的<abbr title="有自動測試的程式碼">測試覆蓋率</abbr>。 |
|||
* 100% 的程式碼有<abbr title="Python 型別註釋,有了這個你的編輯器和外部工具可以給你更好的支援">型別註釋</abbr>。 |
|||
* 已能夠在生產環境應用程式中使用。 |
|||
|
|||
## Starlette 特性 |
|||
|
|||
**FastAPI** 完全相容且基於 <a href="https://www.starlette.io/" class="external-link" target="_blank"><strong>Starlette</strong></a>。所以,你有其他的 Starlette 程式碼也能正常運作。FastAPI 繼承了 Starlette 的所有功能,如果你已經知道或者使用過 Starlette,大部分的功能會以相同的方式運作。 |
|||
|
|||
通過 **FastAPI** 你可以獲得所有 **Starlette** 的特性(FastAPI 就像加強版的 Starlette): |
|||
|
|||
* 性能極其出色。它是 <a href="https://github.com/encode/starlette#performance" class="external-link" target="_blank">Python 可用的最快框架之一,和 **NodeJS** 及 **Go** 相當</a>。 |
|||
* **支援 WebSocket**。 |
|||
* 能在行程內處理背景任務。 |
|||
* 支援啟動和關閉事件。 |
|||
* 有基於 HTTPX 的測試用戶端。 |
|||
* 支援 **CORS**、GZip、靜態檔案、串流回應。 |
|||
* 支援 **Session 和 Cookie** 。 |
|||
* 100% 測試覆蓋率。 |
|||
* 100% 型別註釋的程式碼庫。 |
|||
|
|||
## Pydantic 特性 |
|||
|
|||
**FastAPI** 完全相容且基於 <a href="https://docs.pydantic.dev/" class="external-link" target="_blank"><strong>Pydantic</strong></a>。所以,你有其他 Pydantic 程式碼也能正常工作。 |
|||
|
|||
相容包括基於 Pydantic 的外部函式庫, 例如用於資料庫的 <abbr title="Object-Relational Mapper">ORM</abbr>s, <abbr title="Object-Document Mapper">ODM</abbr>s。 |
|||
|
|||
這也意味著在很多情況下,你可以把從請求中獲得的物件**直接傳到資料庫**,因為所有資料都會自動進行驗證。 |
|||
|
|||
反之亦然,在很多情況下,你也可以把從資料庫中獲取的物件**直接傳給客戶端**。 |
|||
|
|||
通過 **FastAPI** 你可以獲得所有 **Pydantic** 的特性(FastAPI 基於 Pydantic 做了所有的資料處理): |
|||
|
|||
* **更簡單**: |
|||
* 不需要學習新的 micro-language 來定義結構。 |
|||
* 如果你知道 Python 型別,你就知道如何使用 Pydantic。 |
|||
* 和你的 **<abbr title="Integrated Development Environment,和程式碼編輯器類似">IDE</abbr>/<abbr title="一個檢查程式碼錯誤的工具">linter</abbr>/brain** 都能好好配合: |
|||
* 因為 Pydantic 的資料結構其實就是你自己定義的類別實例,所以自動補齊、linting、mypy 以及你的直覺都能很好地在經過驗證的資料上發揮作用。 |
|||
* 驗證**複雜結構**: |
|||
* 使用 Pydantic 模型時,你可以把資料結構分層設計,並且用 Python 的 `List` 和 `Dict` 等型別來定義。 |
|||
* 驗證器讓我們可以輕鬆地定義和檢查複雜的資料結構,並把它們轉換成 JSON Schema 進行記錄。 |
|||
* 你可以擁有深層**巢狀的 JSON** 物件,並對它們進行驗證和註釋。 |
|||
* **可擴展**: |
|||
* Pydantic 讓我們可以定義客製化的資料型別,或者你可以使用帶有 validator 裝飾器的方法來擴展模型中的驗證功能。 |
|||
* 100% 測試覆蓋率。 |
@ -0,0 +1,331 @@ |
|||
# 第一步 |
|||
|
|||
最簡單的 FastAPI 檔案可能看起來像這樣: |
|||
|
|||
{* ../../docs_src/first_steps/tutorial001.py *} |
|||
|
|||
將其複製到一個名為 `main.py` 的文件中。 |
|||
|
|||
執行即時重新載入伺服器(live server): |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ <font color="#4E9A06">fastapi</font> dev <u style="text-decoration-style:single">main.py</u> |
|||
<font color="#3465A4">INFO </font> Using path <font color="#3465A4">main.py</font> |
|||
<font color="#3465A4">INFO </font> Resolved absolute path <font color="#75507B">/home/user/code/awesomeapp/</font><font color="#AD7FA8">main.py</font> |
|||
<font color="#3465A4">INFO </font> Searching for package file structure from directories with <font color="#3465A4">__init__.py</font> files |
|||
<font color="#3465A4">INFO </font> Importing from <font color="#75507B">/home/user/code/</font><font color="#AD7FA8">awesomeapp</font> |
|||
|
|||
╭─ <font color="#8AE234"><b>Python module file</b></font> ─╮ |
|||
│ │ |
|||
│ 🐍 main.py │ |
|||
│ │ |
|||
╰──────────────────────╯ |
|||
|
|||
<font color="#3465A4">INFO </font> Importing module <font color="#4E9A06">main</font> |
|||
<font color="#3465A4">INFO </font> Found importable FastAPI app |
|||
|
|||
╭─ <font color="#8AE234"><b>Importable FastAPI app</b></font> ─╮ |
|||
│ │ |
|||
│ <span style="background-color:#272822"><font color="#FF4689">from</font></span><span style="background-color:#272822"><font color="#F8F8F2"> main </font></span><span style="background-color:#272822"><font color="#FF4689">import</font></span><span style="background-color:#272822"><font color="#F8F8F2"> app</font></span><span style="background-color:#272822"> </span> │ |
|||
│ │ |
|||
╰──────────────────────────╯ |
|||
|
|||
<font color="#3465A4">INFO </font> Using import string <font color="#8AE234"><b>main:app</b></font> |
|||
|
|||
<span style="background-color:#C4A000"><font color="#2E3436">╭────────── FastAPI CLI - Development mode ───────────╮</font></span> |
|||
<span style="background-color:#C4A000"><font color="#2E3436">│ │</font></span> |
|||
<span style="background-color:#C4A000"><font color="#2E3436">│ Serving at: http://127.0.0.1:8000 │</font></span> |
|||
<span style="background-color:#C4A000"><font color="#2E3436">│ │</font></span> |
|||
<span style="background-color:#C4A000"><font color="#2E3436">│ API docs: http://127.0.0.1:8000/docs │</font></span> |
|||
<span style="background-color:#C4A000"><font color="#2E3436">│ │</font></span> |
|||
<span style="background-color:#C4A000"><font color="#2E3436">│ Running in development mode, for production use: │</font></span> |
|||
<span style="background-color:#C4A000"><font color="#2E3436">│ │</font></span> |
|||
<span style="background-color:#C4A000"><font color="#2E3436">│ </font></span><span style="background-color:#C4A000"><font color="#555753"><b>fastapi run</b></font></span><span style="background-color:#C4A000"><font color="#2E3436"> │</font></span> |
|||
<span style="background-color:#C4A000"><font color="#2E3436">│ │</font></span> |
|||
<span style="background-color:#C4A000"><font color="#2E3436">╰─────────────────────────────────────────────────────╯</font></span> |
|||
|
|||
<font color="#4E9A06">INFO</font>: Will watch for changes in these directories: ['/home/user/code/awesomeapp'] |
|||
<font color="#4E9A06">INFO</font>: Uvicorn running on <b>http://127.0.0.1:8000</b> (Press CTRL+C to quit) |
|||
<font color="#4E9A06">INFO</font>: Started reloader process [<font color="#34E2E2"><b>2265862</b></font>] using <font color="#34E2E2"><b>WatchFiles</b></font> |
|||
<font color="#4E9A06">INFO</font>: Started server process [<font color="#06989A">2265873</font>] |
|||
<font color="#4E9A06">INFO</font>: Waiting for application startup. |
|||
<font color="#4E9A06">INFO</font>: Application startup complete. |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
在輸出中,有一列類似於: |
|||
|
|||
```hl_lines="4" |
|||
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) |
|||
``` |
|||
|
|||
那列顯示了你的應用程式正在本地端機器上運行的 URL。 |
|||
|
|||
### 查看它 |
|||
|
|||
在瀏覽器中打開 <a href="http://127.0.0.1:8000" class="external-link" target="_blank">http://127.0.0.1:8000</a>. |
|||
|
|||
你將看到如下的 JSON 回應: |
|||
|
|||
```JSON |
|||
{"message": "Hello World"} |
|||
``` |
|||
|
|||
### 互動式 API 文件 |
|||
|
|||
現在,前往 <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>. |
|||
|
|||
你將看到自動的互動式 API 文件(由 <a href="https://github.com/swagger-api/swagger-ui" class="external-link" target="_blank">Swagger UI</a> 提供): |
|||
|
|||
 |
|||
|
|||
### 替代 API 文件 |
|||
|
|||
現在,前往 <a href="http://127.0.0.1:8000/redoc" class="external-link" target="_blank">http://127.0.0.1:8000/redoc</a>. |
|||
|
|||
你將看到另一種自動文件(由 <a href="https://github.com/Rebilly/ReDoc" class="external-link" target="_blank">ReDoc</a> 提供): |
|||
|
|||
 |
|||
|
|||
### OpenAPI |
|||
|
|||
**FastAPI** 使用定義 API 的 **OpenAPI** 標準來生成一個 「schema」 與你的所有 API。 |
|||
|
|||
#### 「Schema」 |
|||
|
|||
「schema」是對某個事物的定義或描述。它並不是實作它的程式碼,而僅僅是一個抽象的描述。 |
|||
|
|||
#### API 「schema」 |
|||
|
|||
在這種情況下,<a href="https://github.com/OAI/OpenAPI-Specification" class="external-link" target="_blank">OpenAPI</a> 是一個規範,它規定了如何定義 API 的 schema。 |
|||
|
|||
這個 schema 定義包含了你的 API 路徑、可能接收的參數等內容。 |
|||
|
|||
#### 資料 「schema」 |
|||
|
|||
「schema」這個術語也可能指某些資料的結構,比如 JSON 內容的結構。 |
|||
|
|||
在這種情況下,它指的是 JSON 的屬性、資料型別等。 |
|||
|
|||
#### OpenAPI 和 JSON Schema |
|||
|
|||
OpenAPI 定義了 API 的 schema。這個 schema 包含了使用 **JSON Schema** 定義的資料,這是 JSON 資料 schema 的標準。 |
|||
|
|||
#### 檢查 `openapi.json` |
|||
|
|||
如果你好奇原始的 OpenAPI schema 長什麼樣子,FastAPI 會自動生成一個包含所有 API 描述的 JSON (schema)。 |
|||
|
|||
你可以直接在 <a href="http://127.0.0.1:8000/openapi.json" class="external-link" target="_blank">http://127.0.0.1:8000/openapi.json</a> 查看它。 |
|||
|
|||
它會顯示一個 JSON,類似於: |
|||
|
|||
```JSON |
|||
{ |
|||
"openapi": "3.1.0", |
|||
"info": { |
|||
"title": "FastAPI", |
|||
"version": "0.1.0" |
|||
}, |
|||
"paths": { |
|||
"/items/": { |
|||
"get": { |
|||
"responses": { |
|||
"200": { |
|||
"description": "Successful Response", |
|||
"content": { |
|||
"application/json": { |
|||
|
|||
|
|||
|
|||
... |
|||
``` |
|||
|
|||
#### OpenAPI 的用途 |
|||
|
|||
OpenAPI schema 驅動了兩個互動式文件系統。 |
|||
|
|||
而且有許多替代方案,所有這些都是基於 OpenAPI。你可以輕鬆地將任何這些替代方案添加到使用 **FastAPI** 建置的應用程式中。 |
|||
|
|||
你也可以用它自動生成程式碼,讓前端、手機應用程式或物聯網設備等與你的 API 進行通訊。 |
|||
|
|||
## 逐步回顧 |
|||
|
|||
### 第一步:引入 `FastAPI` |
|||
|
|||
{* ../../docs_src/first_steps/tutorial001.py h1[1] *} |
|||
|
|||
`FastAPI` 是一個 Python 類別,提供所有 API 的全部功能。 |
|||
|
|||
/// note | Technical Details |
|||
|
|||
`FastAPI` 是一個直接繼承自 `Starlette` 的類別。 |
|||
|
|||
你同樣可以透過 `FastAPI` 來使用 <a href="https://www.starlette.io/" class="external-link" target="_blank">Starlette</a> 所有的功能。 |
|||
|
|||
/// |
|||
|
|||
### 第二步:建立一個 `FastAPI` 「實例」 |
|||
|
|||
{* ../../docs_src/first_steps/tutorial001.py h1[3] *} |
|||
|
|||
這裡的 `app` 變數將會是 `FastAPI` 類別的「實例」。 |
|||
|
|||
這將是你建立所有 API 的主要互動點。 |
|||
|
|||
### 第三步:建立一個 *路徑操作* |
|||
|
|||
#### 路徑 |
|||
|
|||
這裡的「路徑」指的是 URL 中自第一個 `/` 以後的部分。 |
|||
|
|||
例如,在 URL 中: |
|||
|
|||
``` |
|||
https://example.com/items/foo |
|||
``` |
|||
|
|||
……的路徑將會是: |
|||
|
|||
``` |
|||
/items/foo |
|||
``` |
|||
|
|||
/// info |
|||
|
|||
「路徑」也常被稱為「端點 endpoint」或「路由 route」。 |
|||
|
|||
/// |
|||
|
|||
在建置 API 時,「路徑」是分離「關注點」和「資源」的主要方式。 |
|||
|
|||
#### 操作 |
|||
|
|||
這裡的「操作」指的是 HTTP 的「方法」之一。 |
|||
|
|||
其中包括: |
|||
|
|||
* `POST` |
|||
* `GET` |
|||
* `PUT` |
|||
* `DELETE` |
|||
|
|||
……以及更少見的: |
|||
|
|||
* `OPTIONS` |
|||
* `HEAD` |
|||
* `PATCH` |
|||
* `TRACE` |
|||
|
|||
在 HTTP 協定中,你可以使用這些「方法」之一(或更多)與每個路徑進行通信。 |
|||
|
|||
--- |
|||
|
|||
在建置 API 時,你通常使用這些特定的 HTTP 方法來執行特定的動作。 |
|||
|
|||
通常你使用: |
|||
|
|||
* `POST`:用來建立資料。 |
|||
* `GET`:用來讀取資料。 |
|||
* `PUT`:用來更新資料。 |
|||
* `DELETE`:用來刪除資料。 |
|||
|
|||
所以,在 OpenAPI 中,每個 HTTP 方法都被稱為「操作」。 |
|||
|
|||
我們將會稱它們為「**操作**」。 |
|||
|
|||
#### 定義一個 *路徑操作裝飾器* |
|||
|
|||
{* ../../docs_src/first_steps/tutorial001.py h1[6] *} |
|||
|
|||
`@app.get("/")` 告訴 **FastAPI** 那個函式負責處理請求: |
|||
|
|||
* 路徑 `/` |
|||
* 使用 <abbr title="HTTP GET 方法"><code>get</code>操作</abbr> |
|||
|
|||
/// info | `@decorator` Info |
|||
|
|||
Python 中的 `@something` 語法被稱為「裝飾器」。 |
|||
|
|||
你把它放在一個函式上面。像一個漂亮的裝飾帽子(我猜這是術語的來源)。 |
|||
|
|||
一個「裝飾器」會對下面的函式做一些事情。 |
|||
|
|||
在這種情況下,這個裝飾器告訴 **FastAPI** 那個函式對應於 **路徑** `/` 和 **操作** `get`. |
|||
|
|||
這就是「**路徑操作裝飾器**」。 |
|||
|
|||
/// |
|||
|
|||
你也可以使用其他的操作: |
|||
|
|||
* `@app.post()` |
|||
* `@app.put()` |
|||
* `@app.delete()` |
|||
|
|||
以及更少見的: |
|||
|
|||
* `@app.options()` |
|||
* `@app.head()` |
|||
* `@app.patch()` |
|||
* `@app.trace()` |
|||
|
|||
/// tip |
|||
|
|||
你可以自由地使用每個操作(HTTP 方法)。 |
|||
|
|||
**FastAPI** 不強制任何特定的意義。 |
|||
|
|||
這裡的資訊作為一個指南,而不是要求。 |
|||
|
|||
例如,當使用 GraphQL 時,你通常只使用 `POST` 操作。 |
|||
|
|||
/// |
|||
|
|||
### 第四步:定義 **路徑操作函式** |
|||
|
|||
這是我們的「**路徑操作函式**」: |
|||
|
|||
* **path**: 是 `/`. |
|||
* **operation**: 是 `get`. |
|||
* **function**: 是裝飾器下面的函式(在 `@app.get("/")` 下面)。 |
|||
|
|||
{* ../../docs_src/first_steps/tutorial001.py h1[7] *} |
|||
|
|||
這就是一個 Python 函式。 |
|||
|
|||
它將會在 **FastAPI** 收到一個請求時被呼叫,使用 `GET` 操作。 |
|||
|
|||
在這種情況下,它是一個 `async` 函式。 |
|||
|
|||
--- |
|||
|
|||
你可以將它定義為一個正常的函式,而不是 `async def`: |
|||
|
|||
{* ../../docs_src/first_steps/tutorial003.py h1[7] *} |
|||
|
|||
/// note |
|||
|
|||
如果你不知道差別,請查看 [Async: *"In a hurry?"*](../async.md#in-a-hurry){.internal-link target=_blank}. |
|||
|
|||
/// |
|||
|
|||
### 第五步:回傳內容 |
|||
|
|||
{* ../../docs_src/first_steps/tutorial001.py h1[8] *} |
|||
|
|||
你可以返回一個 `dict`、`list`、單個值作為 `str`、`int` 等。 |
|||
|
|||
你也可以返回 Pydantic 模型(稍後你會看到更多關於這方面的內容)。 |
|||
|
|||
有很多其他物件和模型會自動轉換為 JSON(包括 ORMs,等等)。試用你最喜歡的,很有可能它們已經有支援。 |
|||
|
|||
## 回顧 |
|||
|
|||
* 引入 `FastAPI`. |
|||
* 建立一個 `app` 實例。 |
|||
* 寫一個 **路徑操作裝飾器** 使用裝飾器像 `@app.get("/")`。 |
|||
* 定義一個 **路徑操作函式**;例如,`def root(): ...`。 |
|||
* 使用命令 `fastapi dev` 執行開發伺服器。 |
@ -0,0 +1,68 @@ |
|||
# 查詢參數模型 |
|||
|
|||
如果你有一組具有相關性的**查詢參數**,你可以建立一個 **Pydantic 模型**來聲明它們。 |
|||
|
|||
這將允許你在**多個地方**去**重複使用模型**,並且一次性為所有參數聲明驗證和元資料 (metadata)。😎 |
|||
|
|||
/// note |
|||
|
|||
FastAPI 從 `0.115.0` 版本開始支援這個特性。🤓 |
|||
|
|||
/// |
|||
|
|||
## 使用 Pydantic 模型的查詢參數 |
|||
|
|||
在一個 **Pydantic 模型**中聲明你需要的**查詢參數**,然後將參數聲明為 `Query`: |
|||
|
|||
{* ../../docs_src/query_param_models/tutorial001_an_py310.py hl[9:13,17] *} |
|||
|
|||
**FastAPI** 將會從請求的**查詢參數**中**提取**出**每個欄位**的資料,並將其提供給你定義的 Pydantic 模型。 |
|||
|
|||
## 查看文件 |
|||
|
|||
你可以在 `/docs` 頁面的 UI 中查看查詢參數: |
|||
|
|||
<div class="screenshot"> |
|||
<img src="/img/tutorial/query-param-models/image01.png"> |
|||
</div> |
|||
|
|||
## 禁止額外的查詢參數 |
|||
|
|||
在一些特殊的使用場景中(可能不是很常見),你可能希望**限制**你要收到的查詢參數。 |
|||
|
|||
你可以使用 Pydantic 的模型設定來 `forbid`(禁止)任何 `extra`(額外)欄位: |
|||
|
|||
{* ../../docs_src/query_param_models/tutorial002_an_py310.py hl[10] *} |
|||
|
|||
如果客戶端嘗試在**查詢參數**中發送一些**額外的**資料,他們將會收到一個**錯誤**回應。 |
|||
|
|||
例如,如果客戶端嘗試發送一個值為 `plumbus` 的 `tool` 查詢參數,如: |
|||
|
|||
```http |
|||
https://example.com/items/?limit=10&tool=plumbus |
|||
``` |
|||
|
|||
他們將收到一個**錯誤**回應,告訴他們查詢參數 `tool` 是不允許的: |
|||
|
|||
```json |
|||
{ |
|||
"detail": [ |
|||
{ |
|||
"type": "extra_forbidden", |
|||
"loc": ["query", "tool"], |
|||
"msg": "Extra inputs are not permitted", |
|||
"input": "plumbus" |
|||
} |
|||
] |
|||
} |
|||
``` |
|||
|
|||
## 總結 |
|||
|
|||
你可以使用 **Pydantic 模型**在 **FastAPI** 中聲明**查詢參數**。😎 |
|||
|
|||
/// tip |
|||
|
|||
劇透警告:你也可以使用 Pydantic 模型來聲明 cookie 和 headers,但你將在本教學的後面部分閱讀到這部分內容。🤫 |
|||
|
|||
/// |
@ -0,0 +1,844 @@ |
|||
# 虛擬環境 |
|||
|
|||
當你在 Python 專案中工作時,你可能會需要使用一個**虛擬環境**(或類似的機制)來隔離你為每個專案安裝的套件。 |
|||
|
|||
/// info |
|||
|
|||
如果你已經了解虛擬環境,知道如何建立和使用它們,你可以考慮跳過這一部分。🤓 |
|||
|
|||
/// |
|||
|
|||
/// tip |
|||
|
|||
**虛擬環境**和**環境變數**是不同的。 |
|||
|
|||
**環境變數**是系統中的一個變數,可以被程式使用。 |
|||
|
|||
**虛擬環境**是一個包含一些檔案的目錄。 |
|||
|
|||
/// |
|||
|
|||
/// info |
|||
|
|||
這個頁面將教你如何使用**虛擬環境**以及了解它們的工作原理。 |
|||
|
|||
如果你計畫使用一個**可以為你管理一切的工具**(包括安裝 Python),試試 <a href="https://github.com/astral-sh/uv" class="external-link" target="_blank">uv</a>。 |
|||
|
|||
/// |
|||
|
|||
## 建立一個專案 |
|||
|
|||
首先,為你的專案建立一個目錄。 |
|||
|
|||
我(指原作者 —— 譯者注)通常會在我的主目錄下建立一個名為 `code` 的目錄。 |
|||
|
|||
在這個目錄下,我再為每個專案建立一個目錄。 |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
// 進入主目錄 |
|||
$ cd |
|||
// 建立一個用於存放所有程式碼專案的目錄 |
|||
$ mkdir code |
|||
// 進入 code 目錄 |
|||
$ cd code |
|||
// 建立一個用於存放這個專案的目錄 |
|||
$ mkdir awesome-project |
|||
// 進入這個專案的目錄 |
|||
$ cd awesome-project |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
## 建立一個虛擬環境 |
|||
|
|||
在開始一個 Python 專案的**第一時間**,**<abbr title="還有其他做法,此處僅作為一個簡單的指引">在你的專案內部</abbr>**建立一個虛擬環境。 |
|||
|
|||
/// tip |
|||
|
|||
你只需要**在每個專案中操作一次**,而不是每次工作時都操作。 |
|||
|
|||
/// |
|||
|
|||
//// tab | `venv` |
|||
|
|||
你可以使用 Python 自帶的 `venv` 模組來建立一個虛擬環境。 |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ python -m venv .venv |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
/// details | 上述命令的含義 |
|||
|
|||
* `python`: 使用名為 `python` 的程式 |
|||
* `-m`: 以腳本的方式呼叫一個模組,我們將告訴它接下來使用哪個模組 |
|||
* `venv`: 使用名為 `venv` 的模組,這個模組通常隨 Python 一起安裝 |
|||
* `.venv`: 在新目錄 `.venv` 中建立虛擬環境 |
|||
|
|||
/// |
|||
|
|||
//// |
|||
|
|||
//// tab | `uv` |
|||
|
|||
如果你安裝了 <a href="https://github.com/astral-sh/uv" class="external-link" target="_blank">`uv`</a>,你也可以使用它來建立一個虛擬環境。 |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ uv venv |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
/// tip |
|||
|
|||
預設情況下,`uv` 會在一個名為 `.venv` 的目錄中建立一個虛擬環境。 |
|||
|
|||
但你可以透過傳遞一個額外的引數來自訂它,指定目錄的名稱。 |
|||
|
|||
/// |
|||
|
|||
//// |
|||
|
|||
這個命令會在一個名為 `.venv` 的目錄中建立一個新的虛擬環境。 |
|||
|
|||
/// details | `.venv`,或是其他名稱 |
|||
|
|||
你可以在不同的目錄下建立虛擬環境,但通常我們會把它命名為 `.venv`。 |
|||
|
|||
/// |
|||
|
|||
## 啟動虛擬環境 |
|||
|
|||
啟動新的虛擬環境來確保你運行的任何 Python 指令或安裝的套件都能使用到它。 |
|||
|
|||
/// tip |
|||
|
|||
**每次**開始一個**新的終端會話**來在這個專案工作時,你都需要執行這個操作。 |
|||
|
|||
/// |
|||
|
|||
//// 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 |
|||
|
|||
或者,如果你在 Windows 上使用 Bash(例如 <a href="https://gitforwindows.org/" class="external-link" target="_blank">Git Bash</a>): |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ source .venv/Scripts/activate |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
//// |
|||
|
|||
/// tip |
|||
|
|||
每次你在這個環境中安裝一個**新的套件**時,都需要**重新啟動**這個環境。 |
|||
|
|||
這麼做確保了當你使用一個由這個套件安裝的**終端(<abbr title="命令列介面">CLI</abbr>)程式**時,你使用的是你的虛擬環境中的程式,而不是全域安裝、可能版本不同的程式。 |
|||
|
|||
/// |
|||
|
|||
## 檢查虛擬環境是否啟動 |
|||
|
|||
檢查虛擬環境是否啟動(前面的指令是否生效)。 |
|||
|
|||
/// tip |
|||
|
|||
這是**非必需的**,但這是一個很好的方法,可以**檢查**一切是否按預期工作,以及你是否使用了你打算使用的虛擬環境。 |
|||
|
|||
/// |
|||
|
|||
//// tab | Linux, macOS, Windows Bash |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ which python |
|||
|
|||
/home/user/code/awesome-project/.venv/bin/python |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
如果它顯示了在你專案(在這個例子中是 `awesome-project`)的 `.venv/bin/python` 中的 `python` 二進位檔案,那麼它就生效了。🎉 |
|||
|
|||
//// |
|||
|
|||
//// tab | Windows PowerShell |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ Get-Command python |
|||
|
|||
C:\Users\user\code\awesome-project\.venv\Scripts\python |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
如果它顯示了在你專案(在這個例子中是 `awesome-project`)的 `.venv\Scripts\python` 中的 `python` 二進位檔案,那麼它就生效了。🎉 |
|||
|
|||
//// |
|||
|
|||
## 升級 `pip` |
|||
|
|||
/// tip |
|||
|
|||
如果你使用 <a href="https://github.com/astral-sh/uv" class="external-link" target="_blank">`uv`</a> 來安裝內容,而不是 `pip`,那麼你就不需要升級 `pip`。😎 |
|||
|
|||
/// |
|||
|
|||
如果你使用 `pip` 來安裝套件(它是 Python 的預設元件),你應該將它**升級**到最新版本。 |
|||
|
|||
在安裝套件時出現的許多奇怪的錯誤都可以透過先升級 `pip` 來解決。 |
|||
|
|||
/// tip |
|||
|
|||
通常你只需要在建立虛擬環境後**執行一次**這個操作。 |
|||
|
|||
/// |
|||
|
|||
確保虛擬環境是啟動的(使用上面的指令),然後運行: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ python -m pip install --upgrade pip |
|||
|
|||
---> 100% |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
## 加入 `.gitignore` |
|||
|
|||
如果你使用 **Git**(這是你應該使用的),加入一個 `.gitignore` 檔案來排除你的 `.venv` 中的所有內容。 |
|||
|
|||
/// tip |
|||
|
|||
如果你使用 <a href="https://github.com/astral-sh/uv" class="external-link" target="_blank">`uv`</a> 來建立虛擬環境,它會自動為你完成這個操作,你可以跳過這一步。😎 |
|||
|
|||
/// |
|||
|
|||
/// tip |
|||
|
|||
通常你只需要在建立虛擬環境後**執行一次**這個操作。 |
|||
|
|||
/// |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ echo "*" > .venv/.gitignore |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
/// details | 上述指令的含義 |
|||
|
|||
- `echo "*"`: 將在終端中「顯示」文本 `*`(接下來的部分會對這個操作進行一些修改) |
|||
- `>`: 使左邊的指令顯示到終端的任何內容實際上都不會被顯示,而是會被寫入到右邊的檔案中 |
|||
- `.gitignore`: 被寫入文本的檔案的名稱 |
|||
|
|||
而 `*` 對於 Git 來說意味著「所有內容」。所以,它會忽略 `.venv` 目錄中的所有內容。 |
|||
|
|||
該指令會建立一個名為 .gitignore 的檔案,內容如下: |
|||
|
|||
```gitignore |
|||
* |
|||
``` |
|||
|
|||
/// |
|||
|
|||
## 安裝套件 |
|||
|
|||
在啟用虛擬環境後,你可以在其中安裝套件。 |
|||
|
|||
/// tip |
|||
|
|||
當你需要安裝或升級套件時,執行本操作**一次**; |
|||
|
|||
如果你需要再升級版本或新增套件,你可以**再次執行此操作**。 |
|||
|
|||
/// |
|||
|
|||
### 直接安裝套件 |
|||
|
|||
如果你急於安裝,不想使用檔案來聲明專案的套件依賴,你可以直接安裝它們。 |
|||
|
|||
/// tip |
|||
|
|||
將程式所需的套件及其版本放在檔案中(例如 `requirements.txt` 或 `pyproject.toml`)是個好(而且非常好)的主意。 |
|||
|
|||
/// |
|||
|
|||
//// tab | `pip` |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ pip install "fastapi[standard]" |
|||
|
|||
---> 100% |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
//// |
|||
|
|||
//// tab | `uv` |
|||
|
|||
如果你有 <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> |
|||
|
|||
//// |
|||
|
|||
### 從 `requirements.txt` 安裝 |
|||
|
|||
如果你有一個 `requirements.txt` 檔案,你可以使用它來安裝其中的套件。 |
|||
|
|||
//// tab | `pip` |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ pip install -r requirements.txt |
|||
---> 100% |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
//// |
|||
|
|||
//// tab | `uv` |
|||
|
|||
如果你有 <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` |
|||
|
|||
一個包含一些套件的 `requirements.txt` 檔案看起來應該是這樣的: |
|||
|
|||
```requirements.txt |
|||
fastapi[standard]==0.113.0 |
|||
pydantic==2.8.0 |
|||
``` |
|||
|
|||
/// |
|||
|
|||
## 執行程式 |
|||
|
|||
在啟用虛擬環境後,你可以執行你的程式,它將使用虛擬環境中的 Python 和你在其中安裝的套件。 |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ python main.py |
|||
|
|||
Hello World |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
## 設定編輯器 |
|||
|
|||
你可能會用到編輯器,請確保設定它使用你建立的相同虛擬環境(它可能會自動偵測到),以便你可以獲得自動完成和内嵌錯誤提示。 |
|||
|
|||
例如: |
|||
|
|||
* <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 |
|||
|
|||
通常你只需要在建立虛擬環境時執行此操作**一次**。 |
|||
|
|||
/// |
|||
|
|||
## 退出虛擬環境 |
|||
|
|||
當你完成工作後,你可以**退出**虛擬環境。 |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ deactivate |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
這樣,當你執行 `python` 時它不會嘗試從已安裝套件的虛擬環境中執行。 |
|||
|
|||
## 開始工作 |
|||
|
|||
現在你已經準備好開始你的工作了。 |
|||
|
|||
|
|||
|
|||
/// tip |
|||
|
|||
你想要理解上面的所有內容嗎? |
|||
|
|||
繼續閱讀。👇🤓 |
|||
|
|||
/// |
|||
|
|||
## 為什麼要使用虛擬環境 |
|||
|
|||
你需要安裝 <a href="https://www.python.org/" class="external-link" target="_blank">Python</a> 才能使用 FastAPI。 |
|||
|
|||
接下來,你需要**安裝** FastAPI 以及你想使用的其他**套件**。 |
|||
|
|||
要安裝套件,你通常會使用隨 Python 一起提供的 `pip` 指令(或類似的替代工具)。 |
|||
|
|||
然而,如果你直接使用 `pip`,套件將會安裝在你的**全域 Python 環境**中(即 Python 的全域安裝)。 |
|||
|
|||
### 存在的問題 |
|||
|
|||
那麼,在全域 Python 環境中安裝套件有什麼問題呢? |
|||
|
|||
有時候,你可能會開發許多不同的程式,而這些程式各自依賴於**不同的套件**;有些專案甚至需要依賴於**相同套件的不同版本**。😱 |
|||
|
|||
例如,你可能會建立一個名為 `philosophers-stone` 的專案,這個程式依賴於另一個名為 **`harry` 的套件,並使用版本 `1`**。因此,你需要安裝 `harry`。 |
|||
|
|||
```mermaid |
|||
flowchart LR |
|||
stone(philosophers-stone) -->|需要| harry-1[harry v1] |
|||
``` |
|||
|
|||
然而,在此之後,你又建立了另一個名為 `prisoner-of-azkaban` 的專案,而這個專案也依賴於 `harry`,但需要的是 **`harry` 版本 `3`**。 |
|||
|
|||
```mermaid |
|||
flowchart LR |
|||
azkaban(prisoner-of-azkaban) --> |需要| harry-3[harry v3] |
|||
``` |
|||
|
|||
現在的問題是,如果你在全域環境中安裝套件而不是在本地**虛擬環境**中,你將面臨選擇安裝哪個版本的 `harry` 的困境。 |
|||
|
|||
如果你想運行 `philosophers-stone`,你需要先安裝 `harry` 版本 `1`,例如: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ pip install "harry==1" |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
然後你會在全域 Python 環境中安裝 `harry` 版本 `1`。 |
|||
|
|||
```mermaid |
|||
flowchart LR |
|||
subgraph global[全域環境] |
|||
harry-1[harry v1] |
|||
end |
|||
subgraph stone-project[專案 philosophers-stone] |
|||
stone(philosophers-stone) -->|需要| harry-1 |
|||
end |
|||
``` |
|||
|
|||
但如果你想運行 `prisoner-of-azkaban`,你需要解除安裝 `harry` 版本 `1` 並安裝 `harry` 版本 `3`(或者只要你安裝版本 `3`,版本 `1` 就會自動移除)。 |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ pip install "harry==3" |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
於是,你在全域 Python 環境中安裝了 `harry` 版本 `3`。 |
|||
|
|||
如果你再次嘗試運行 `philosophers-stone`,很可能會**無法正常運作**,因為它需要的是 `harry` 版本 `1`。 |
|||
|
|||
```mermaid |
|||
flowchart LR |
|||
subgraph global[全域環境] |
|||
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] |
|||
stone(philosophers-stone) -.-x|⛔️| harry-1 |
|||
end |
|||
subgraph azkaban-project[專案 prisoner-of-azkaban] |
|||
azkaban(prisoner-of-azkaban) --> |需要| harry-3 |
|||
end |
|||
``` |
|||
|
|||
/// tip |
|||
|
|||
Python 套件在推出**新版本**時通常會儘量**避免破壞性更改**,但最好還是要謹慎,在安裝新版本前進行測試,以確保一切能正常運行。 |
|||
|
|||
/// |
|||
|
|||
現在,想像一下如果有**許多**其他**套件**,它們都是你的**專案所依賴的**。這樣是非常難以管理的。你可能會發現有些專案使用了一些**不相容的套件版本**,而無法得知為什麼某些程式無法正常運作。 |
|||
|
|||
此外,取決於你的操作系統(例如 Linux、Windows、macOS),它可能已經預先安裝了 Python。在這種情況下,它可能已經有一些系統所需的套件和特定版本。如果你在全域 Python 環境中安裝套件,可能會**破壞**某些隨作業系統一起安裝的程式。 |
|||
|
|||
## 套件安裝在哪裡 |
|||
|
|||
當你安裝 Python 時,它會在你的電腦中建立一些目錄並放置一些檔案。 |
|||
|
|||
其中一些目錄專門用來存放你所安裝的所有套件。 |
|||
|
|||
當你運行: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
// 先別去運行這個指令,這只是個示例 🤓 |
|||
$ pip install "fastapi[standard]" |
|||
---> 100% |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
這會從 <a href="https://pypi.org/project/fastapi/" class="external-link" target="_blank">PyPI</a> 下載一個壓縮檔案,其中包含 FastAPI 的程式碼。 |
|||
|
|||
它還會**下載** FastAPI 所依賴的其他套件的檔案。 |
|||
|
|||
接著,它會**解壓**所有這些檔案,並將它們放在你的電腦中的某個目錄中。 |
|||
|
|||
預設情況下,這些下載和解壓的檔案會放置於隨 Python 安裝的目錄中,即**全域環境**。 |
|||
|
|||
## 什麼是虛擬環境 |
|||
|
|||
解決套件都安裝在全域環境中的問題方法是為你所做的每個專案使用一個**虛擬環境**。 |
|||
|
|||
虛擬環境是一個**目錄**,與全域環境非常相似,你可以在其中針對某個專案安裝套件。 |
|||
|
|||
這樣,每個專案都會有自己的虛擬環境(`.venv` 目錄),其中包含自己的套件。 |
|||
|
|||
```mermaid |
|||
flowchart TB |
|||
subgraph stone-project[專案 philosophers-stone] |
|||
stone(philosophers-stone) --->|需要| harry-1 |
|||
subgraph venv1[.venv] |
|||
harry-1[harry v1] |
|||
end |
|||
end |
|||
subgraph azkaban-project[專案 prisoner-of-azkaban] |
|||
azkaban(prisoner-of-azkaban) --->|需要| harry-3 |
|||
subgraph venv2[.venv] |
|||
harry-3[harry v3] |
|||
end |
|||
end |
|||
stone-project ~~~ azkaban-project |
|||
``` |
|||
|
|||
## 啟用虛擬環境意味著什麼 |
|||
|
|||
當你啟用了虛擬環境,例如: |
|||
|
|||
//// 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 |
|||
|
|||
或者如果你在 Windows 上使用 Bash(例如 <a href="https://gitforwindows.org/" class="external-link" target="_blank">Git Bash</a>): |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ source .venv/Scripts/activate |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
//// |
|||
|
|||
這個命令會建立或修改一些[環境變數](environment-variables.md){.internal-link target=_blank},這些環境變數將在接下來的指令中可用。 |
|||
|
|||
其中之一是 `PATH` 變數。 |
|||
|
|||
/// tip |
|||
|
|||
你可以在 [環境變數](environment-variables.md#path-environment-variable){.internal-link target=_blank} 部分了解更多關於 `PATH` 環境變數的內容。 |
|||
|
|||
/// |
|||
|
|||
啟用虛擬環境會將其路徑 `.venv/bin`(在 Linux 和 macOS 上)或 `.venv\Scripts`(在 Windows 上)加入到 `PATH` 環境變數中。 |
|||
|
|||
假設在啟用環境之前,`PATH` 變數看起來像這樣: |
|||
|
|||
//// tab | Linux, macOS |
|||
|
|||
```plaintext |
|||
/usr/bin:/bin:/usr/sbin:/sbin |
|||
``` |
|||
|
|||
這意味著系統會在以下目錄中查找程式: |
|||
|
|||
* `/usr/bin` |
|||
* `/bin` |
|||
* `/usr/sbin` |
|||
* `/sbin` |
|||
|
|||
//// |
|||
|
|||
//// tab | Windows |
|||
|
|||
```plaintext |
|||
C:\Windows\System32 |
|||
``` |
|||
|
|||
這意味著系統會在以下目錄中查找程式: |
|||
|
|||
* `C:\Windows\System32` |
|||
|
|||
//// |
|||
|
|||
啟用虛擬環境後,`PATH` 變數會變成這樣: |
|||
|
|||
//// tab | Linux, macOS |
|||
|
|||
```plaintext |
|||
/home/user/code/awesome-project/.venv/bin:/usr/bin:/bin:/usr/sbin:/sbin |
|||
``` |
|||
|
|||
這意味著系統現在會首先在以下目錄中查找程式: |
|||
|
|||
```plaintext |
|||
/home/user/code/awesome-project/.venv/bin |
|||
``` |
|||
|
|||
然後再在其他目錄中查找。 |
|||
|
|||
因此,當你在終端機中輸入 `python` 時,系統會在以下目錄中找到 Python 程式: |
|||
|
|||
```plaintext |
|||
/home/user/code/awesome-project/.venv/bin/python |
|||
``` |
|||
|
|||
並使用這個。 |
|||
|
|||
//// |
|||
|
|||
//// tab | Windows |
|||
|
|||
```plaintext |
|||
C:\Users\user\code\awesome-project\.venv\Scripts;C:\Windows\System32 |
|||
``` |
|||
|
|||
這意味著系統現在會首先在以下目錄中查找程式: |
|||
|
|||
```plaintext |
|||
C:\Users\user\code\awesome-project\.venv\Scripts |
|||
``` |
|||
|
|||
然後再在其他目錄中查找。 |
|||
|
|||
因此,當你在終端機中輸入 `python` 時,系統會在以下目錄中找到 Python 程式: |
|||
|
|||
```plaintext |
|||
C:\Users\user\code\awesome-project\.venv\Scripts\python |
|||
``` |
|||
|
|||
並使用這個。 |
|||
|
|||
//// |
|||
|
|||
一個重要的細節是,虛擬環境路徑會被放在 `PATH` 變數的**開頭**。系統會在找到任何其他可用的 Python **之前**找到它。這樣,當你運行 `python` 時,它會使用**虛擬環境中的** Python,而不是任何其他 `python`(例如,全域環境中的 `python`)。 |
|||
|
|||
啟用虛擬環境還會改變其他一些內容,但這是它所做的最重要的事情之一。 |
|||
|
|||
## 檢查虛擬環境 |
|||
|
|||
當你檢查虛擬環境是否啟動時,例如: |
|||
|
|||
//// 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> |
|||
|
|||
//// |
|||
|
|||
這表示將使用的 `python` 程式是**在虛擬環境中**的那一個。 |
|||
|
|||
在 Linux 和 macOS 中使用 `which`,在 Windows PowerShell 中使用 `Get-Command`。 |
|||
|
|||
這個指令的運作方式是,它會在 `PATH` 環境變數中搜尋,依序**逐個路徑**查找名為 `python` 的程式。一旦找到,它會**顯示該程式的路徑**。 |
|||
|
|||
最重要的是,當你呼叫 `python` 時,將執行的就是這個確切的 "`python`"。 |
|||
|
|||
因此,你可以確認是否在正確的虛擬環境中。 |
|||
|
|||
/// tip |
|||
|
|||
啟動一個虛擬環境,取得一個 Python,然後**切換到另一個專案**是件很容易的事; |
|||
|
|||
但如果第二個專案**無法正常運作**,那可能是因為你使用了來自其他專案的虛擬環境的、**不正確的 Python**。 |
|||
|
|||
因此,檢查正在使用的 `python` 是非常實用的。🤓 |
|||
|
|||
/// |
|||
|
|||
## 為什麼要停用虛擬環境 |
|||
|
|||
例如,你可能正在一個專案 `philosophers-stone` 上工作,**啟動了該虛擬環境**,安裝了套件並使用了該環境, |
|||
|
|||
然後你想要在**另一個專案** `prisoner-of-azkaban` 上工作, |
|||
|
|||
你進入那個專案: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ cd ~/code/prisoner-of-azkaban |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
如果你不去停用 `philosophers-stone` 的虛擬環境,當你在終端中執行 `python` 時,它會嘗試使用 `philosophers-stone` 中的 Python。 |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ cd ~/code/prisoner-of-azkaban |
|||
|
|||
$ python main.py |
|||
|
|||
// 匯入 sirius 錯誤,未安裝 😱 |
|||
Traceback (most recent call last): |
|||
File "main.py", line 1, in <module> |
|||
import sirius |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
但如果你停用虛擬環境並啟用 `prisoner-of-askaban` 的新虛擬環境,那麼當你執行 `python` 時,它會使用 `prisoner-of-askaban` 中虛擬環境的 Python。 |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ cd ~/code/prisoner-of-azkaban |
|||
|
|||
// 你不需要在舊目錄中操作停用,你可以在任何地方操作停用,甚至在切換到另一個專案之後 😎 |
|||
$ deactivate |
|||
|
|||
// 啟用 prisoner-of-azkaban/.venv 中的虛擬環境 🚀 |
|||
$ source .venv/bin/activate |
|||
|
|||
// 現在當你執行 python 時,它會在這個虛擬環境中找到已安裝的 sirius 套件 ✨ |
|||
$ python main.py |
|||
|
|||
I solemnly swear 🐺 |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
## 替代方案 |
|||
|
|||
這是一個簡單的指南,幫助你入門並教會你如何理解一切**底層**的原理。 |
|||
|
|||
有許多**替代方案**來管理虛擬環境、套件依賴(requirements)、專案。 |
|||
|
|||
當你準備好並想要使用一個工具來**管理整個專案**、套件依賴、虛擬環境等,建議你嘗試 <a href="https://github.com/astral-sh/uv" class="external-link" target="_blank">uv</a>。 |
|||
|
|||
`uv` 可以執行許多操作,它可以: |
|||
|
|||
* 為你**安裝 Python**,包括不同的版本 |
|||
* 為你的專案管理**虛擬環境** |
|||
* 安裝**套件** |
|||
* 為你的專案管理套件的**依賴和版本** |
|||
* 確保你有一個**精確**的套件和版本集合來安裝,包括它們的依賴項,這樣你可以確保專案在生產環境中運行的狀態與開發時在你的電腦上運行的狀態完全相同,這被稱為**鎖定** |
|||
* 還有很多其他功能 |
|||
|
|||
## 結論 |
|||
|
|||
如果你讀過並理解了所有這些,現在**你對虛擬環境的了解已超過許多開發者**。🤓 |
|||
|
|||
未來當你為看起來複雜的問題除錯時,了解這些細節很可能會有所幫助,你會知道**它是如何在底層運作的**。😎 |
@ -0,0 +1,99 @@ |
|||
# 异步测试 |
|||
|
|||
您已经了解了如何使用 `TestClient` 测试 **FastAPI** 应用程序。但是到目前为止,您只了解了如何编写同步测试,而没有使用 `async` 异步函数。 |
|||
|
|||
在测试中能够使用异步函数可能会很有用,比如当您需要异步查询数据库的时候。想象一下,您想要测试向 FastAPI 应用程序发送请求,然后验证您的后端是否成功在数据库中写入了正确的数据,与此同时您使用了异步的数据库的库。 |
|||
|
|||
让我们看看如何才能实现这一点。 |
|||
|
|||
## pytest.mark.anyio |
|||
|
|||
如果我们想在测试中调用异步函数,那么我们的测试函数必须是异步的。 AnyIO 为此提供了一个简洁的插件,它允许我们指定一些测试函数要异步调用。 |
|||
|
|||
## HTTPX |
|||
|
|||
即使您的 **FastAPI** 应用程序使用普通的 `def` 函数而不是 `async def` ,它本质上仍是一个 `async` 异步应用程序。 |
|||
|
|||
`TestClient` 在内部通过一些“魔法”操作,使得您可以在普通的 `def` 测试函数中调用异步的 FastAPI 应用程序,并使用标准的 pytest。但当我们在异步函数中使用它时,这种“魔法”就不再生效了。由于测试以异步方式运行,我们无法在测试函数中继续使用 `TestClient`。 |
|||
|
|||
`TestClient` 是基于 <a href="https://www.python-httpx.org" class="external-link" target="_blank">HTTPX</a> 的。幸运的是,我们可以直接使用它来测试API。 |
|||
|
|||
## 示例 |
|||
|
|||
举个简单的例子,让我们来看一个[更大的应用](../tutorial/bigger-applications.md){.internal-link target=_blank}和[测试](../tutorial/testing.md){.internal-link target=_blank}中描述的类似文件结构: |
|||
|
|||
``` |
|||
. |
|||
├── app |
|||
│ ├── __init__.py |
|||
│ ├── main.py |
|||
│ └── test_main.py |
|||
``` |
|||
|
|||
文件 `main.py` 将包含: |
|||
|
|||
{* ../../docs_src/async_tests/main.py *} |
|||
|
|||
文件 `test_main.py` 将包含针对 `main.py` 的测试,现在它可能看起来如下: |
|||
|
|||
{* ../../docs_src/async_tests/test_main.py *} |
|||
|
|||
## 运行测试 |
|||
|
|||
您可以通过以下方式照常运行测试: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ pytest |
|||
|
|||
---> 100% |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
## 详细说明 |
|||
|
|||
这个标记 `@pytest.mark.anyio` 会告诉 pytest 该测试函数应该被异步调用: |
|||
|
|||
{* ../../docs_src/async_tests/test_main.py hl[7] *} |
|||
|
|||
/// tip |
|||
|
|||
请注意,测试函数现在用的是 `async def`,而不是像以前使用 `TestClient` 时那样只是 `def` 。 |
|||
|
|||
/// |
|||
|
|||
我们现在可以使用应用程序创建一个 `AsyncClient` ,并使用 `await` 向其发送异步请求。 |
|||
|
|||
{* ../../docs_src/async_tests/test_main.py hl[9:12] *} |
|||
|
|||
这相当于: |
|||
|
|||
```Python |
|||
response = client.get('/') |
|||
``` |
|||
|
|||
我们曾经通过它向 `TestClient` 发出请求。 |
|||
|
|||
/// tip |
|||
|
|||
请注意,我们正在将 async/await 与新的 `AsyncClient` 一起使用——请求是异步的。 |
|||
|
|||
/// |
|||
|
|||
/// warning |
|||
|
|||
如果您的应用程序依赖于生命周期事件, `AsyncClient` 将不会触发这些事件。为了确保它们被触发,请使用 <a href="https://github.com/florimondmanca/asgi-lifespan#usage" class="external-link" target="_blank">florimondmanca/asgi-lifespan</a> 中的 `LifespanManager` 。 |
|||
|
|||
/// |
|||
|
|||
## 其他异步函数调用 |
|||
|
|||
由于测试函数现在是异步的,因此除了在测试中向 FastAPI 应用程序发送请求之外,您现在还可以调用(和使用 `await` 等待)其他 `async` 异步函数,就和您在代码中的其他任何地方调用它们的方法一样。 |
|||
|
|||
/// tip |
|||
|
|||
如果您在测试程序中集成异步函数调用的时候遇到一个 `RuntimeError: Task attached to a different loop` 的报错(例如,使用 <a href="https://stackoverflow.com/questions/41584243/runtimeerror-task-attached-to-a-different-loop" class="external-link" target="_blank">MongoDB 的 MotorClient</a> 时),请记住,只能在异步函数中实例化需要事件循环的对象,例如通过 `'@app.on_event("startup")` 回调函数进行初始化。 |
|||
|
|||
/// |
@ -0,0 +1,76 @@ |
|||
# Cookie 参数模型 |
|||
|
|||
如果您有一组相关的 **cookie**,您可以创建一个 **Pydantic 模型**来声明它们。🍪 |
|||
|
|||
这将允许您在**多个地方**能够**重用模型**,并且可以一次性声明所有参数的验证方式和元数据。😎 |
|||
|
|||
/// note |
|||
|
|||
自 FastAPI 版本 `0.115.0` 起支持此功能。🤓 |
|||
|
|||
/// |
|||
|
|||
/// tip |
|||
|
|||
此技术同样适用于 `Query` 、 `Cookie` 和 `Header` 。😎 |
|||
|
|||
/// |
|||
|
|||
## 带有 Pydantic 模型的 Cookie |
|||
|
|||
在 **Pydantic** 模型中声明所需的 **cookie** 参数,然后将参数声明为 `Cookie` : |
|||
|
|||
{* ../../docs_src/cookie_param_models/tutorial001_an_py310.py hl[9:12,16] *} |
|||
|
|||
**FastAPI** 将从请求中接收到的 **cookie** 中**提取**出**每个字段**的数据,并提供您定义的 Pydantic 模型。 |
|||
|
|||
## 查看文档 |
|||
|
|||
您可以在文档 UI 的 `/docs` 中查看定义的 cookie: |
|||
|
|||
<div class="screenshot"> |
|||
<img src="/img/tutorial/cookie-param-models/image01.png"> |
|||
</div> |
|||
|
|||
/// info |
|||
|
|||
请记住,由于**浏览器**以特殊方式**处理 cookie**,并在后台进行操作,因此它们**不会**轻易允许 **JavaScript** 访问这些 cookie。 |
|||
|
|||
如果您访问 `/docs` 的 **API 文档 UI**,您将能够查看您*路径操作*的 cookie **文档**。 |
|||
|
|||
但是即使您**填写数据**并点击“执行”,由于文档界面使用 **JavaScript**,cookie 将不会被发送。而您会看到一条**错误**消息,就好像您没有输入任何值一样。 |
|||
|
|||
/// |
|||
|
|||
## 禁止额外的 Cookie |
|||
|
|||
在某些特殊使用情况下(可能并不常见),您可能希望**限制**您想要接收的 cookie。 |
|||
|
|||
您的 API 现在可以控制自己的 <abbr title="顺带一提,这是一个笑话。它与 cookie 同意无关,但现在连API都能拒绝那些可怜的 cookie,真是太有意思了。来,吃块小饼干(cookie)吧。🍪">cookie 同意</abbr>。🤪🍪 |
|||
|
|||
您可以使用 Pydantic 的模型配置来禁止( `forbid` )任何额外( `extra` )字段: |
|||
|
|||
{* ../../docs_src/cookie_param_models/tutorial002_an_py39.py hl[10] *} |
|||
|
|||
如果客户尝试发送一些**额外的 cookie**,他们将收到**错误**响应。 |
|||
|
|||
可怜的 cookie 通知条,费尽心思为了获得您的同意,却被<abbr title="这又是一个笑话,别管我了,给您的小饼干(cookie)配上点咖啡吧。☕">API 拒绝了</abbr>。🍪 |
|||
|
|||
例如,如果客户端尝试发送一个值为 `good-list-please` 的 `santa_tracker` cookie,客户端将收到一个**错误**响应,告知他们 `santa_tracker` <abbr title="圣诞老人(Santa)不赞成没有小饼干(cookie)。🎅 好吧,不会再开 cookie 的玩笑了。">cookie 是不允许的</abbr>: |
|||
|
|||
```json |
|||
{ |
|||
"detail": [ |
|||
{ |
|||
"type": "extra_forbidden", |
|||
"loc": ["cookie", "santa_tracker"], |
|||
"msg": "Extra inputs are not permitted", |
|||
"input": "good-list-please", |
|||
} |
|||
] |
|||
} |
|||
``` |
|||
|
|||
## 总结 |
|||
|
|||
您可以使用 **Pydantic 模型**在 **FastAPI** 中声明 <abbr title="走之前再来块小饼干吧。 🍪">**cookie**</abbr>。😎 |
@ -0,0 +1,56 @@ |
|||
# Header 参数模型 |
|||
|
|||
如果您有一组相关的 **header 参数**,您可以创建一个 **Pydantic 模型**来声明它们。 |
|||
|
|||
这将允许您在**多个地方**能够**重用模型**,并且可以一次性声明所有参数的验证和元数据。😎 |
|||
|
|||
/// note |
|||
|
|||
自 FastAPI 版本 `0.115.0` 起支持此功能。🤓 |
|||
|
|||
/// |
|||
|
|||
## 使用 Pydantic 模型的 Header 参数 |
|||
|
|||
在 **Pydantic 模型**中声明所需的 **header 参数**,然后将参数声明为 `Header` : |
|||
|
|||
{* ../../docs_src/header_param_models/tutorial001_an_py310.py hl[9:14,18] *} |
|||
|
|||
**FastAPI** 将从请求中接收到的 **headers** 中**提取**出**每个字段**的数据,并提供您定义的 Pydantic 模型。 |
|||
|
|||
## 查看文档 |
|||
|
|||
您可以在文档 UI 的 `/docs` 中查看所需的 headers: |
|||
|
|||
<div class="screenshot"> |
|||
<img src="/img/tutorial/header-param-models/image01.png"> |
|||
</div> |
|||
|
|||
## 禁止额外的 Headers |
|||
|
|||
在某些特殊使用情况下(可能并不常见),您可能希望**限制**您想要接收的 headers。 |
|||
|
|||
您可以使用 Pydantic 的模型配置来禁止( `forbid` )任何额外( `extra` )字段: |
|||
|
|||
{* ../../docs_src/header_param_models/tutorial002_an_py310.py hl[10] *} |
|||
|
|||
如果客户尝试发送一些**额外的 headers**,他们将收到**错误**响应。 |
|||
|
|||
例如,如果客户端尝试发送一个值为 `plumbus` 的 `tool` header,客户端将收到一个**错误**响应,告知他们 header 参数 `tool` 是不允许的: |
|||
|
|||
```json |
|||
{ |
|||
"detail": [ |
|||
{ |
|||
"type": "extra_forbidden", |
|||
"loc": ["header", "tool"], |
|||
"msg": "Extra inputs are not permitted", |
|||
"input": "plumbus", |
|||
} |
|||
] |
|||
} |
|||
``` |
|||
|
|||
## 总结 |
|||
|
|||
您可以使用 **Pydantic 模型**在 **FastAPI** 中声明 **headers**。😎 |
@ -0,0 +1,68 @@ |
|||
# 查询参数模型 |
|||
|
|||
如果你有一组具有相关性的**查询参数**,你可以创建一个 **Pydantic 模型**来声明它们。 |
|||
|
|||
这将允许你在**多个地方**去**复用模型**,并且一次性为所有参数声明验证和元数据。😎 |
|||
|
|||
/// note |
|||
|
|||
FastAPI 从 `0.115.0` 版本开始支持这个特性。🤓 |
|||
|
|||
/// |
|||
|
|||
## 使用 Pydantic 模型的查询参数 |
|||
|
|||
在一个 **Pydantic 模型**中声明你需要的**查询参数**,然后将参数声明为 `Query`: |
|||
|
|||
{* ../../docs_src/query_param_models/tutorial001_an_py310.py hl[9:13,17] *} |
|||
|
|||
**FastAPI** 将会从请求的**查询参数**中**提取**出**每个字段**的数据,并将其提供给你定义的 Pydantic 模型。 |
|||
|
|||
## 查看文档 |
|||
|
|||
你可以在 `/docs` 页面的 UI 中查看查询参数: |
|||
|
|||
<div class="screenshot"> |
|||
<img src="/img/tutorial/query-param-models/image01.png"> |
|||
</div> |
|||
|
|||
## 禁止额外的查询参数 |
|||
|
|||
在一些特殊的使用场景中(可能不是很常见),你可能希望**限制**你要接收的查询参数。 |
|||
|
|||
你可以使用 Pydantic 的模型配置来 `forbid`(意为禁止 —— 译者注)任何 `extra`(意为额外的 —— 译者注)字段: |
|||
|
|||
{* ../../docs_src/query_param_models/tutorial002_an_py310.py hl[10] *} |
|||
|
|||
假设有一个客户端尝试在**查询参数**中发送一些**额外的**数据,它将会收到一个**错误**响应。 |
|||
|
|||
例如,如果客户端尝试发送一个值为 `plumbus` 的 `tool` 查询参数,如: |
|||
|
|||
```http |
|||
https://example.com/items/?limit=10&tool=plumbus |
|||
``` |
|||
|
|||
他们将收到一个**错误**响应,告诉他们查询参数 `tool` 是不允许的: |
|||
|
|||
```json |
|||
{ |
|||
"detail": [ |
|||
{ |
|||
"type": "extra_forbidden", |
|||
"loc": ["query", "tool"], |
|||
"msg": "Extra inputs are not permitted", |
|||
"input": "plumbus" |
|||
} |
|||
] |
|||
} |
|||
``` |
|||
|
|||
## 总结 |
|||
|
|||
你可以使用 **Pydantic 模型**在 **FastAPI** 中声明**查询参数**。😎 |
|||
|
|||
/// tip |
|||
|
|||
剧透警告:你也可以使用 Pydantic 模型来声明 cookie 和 headers,但你将在本教程的后面部分阅读到这部分内容。🤫 |
|||
|
|||
/// |
@ -0,0 +1,78 @@ |
|||
# 表单模型 |
|||
|
|||
您可以使用 **Pydantic 模型**在 FastAPI 中声明**表单字段**。 |
|||
|
|||
/// info |
|||
|
|||
要使用表单,需预先安装 <a href="https://github.com/Kludex/python-multipart" class="external-link" target="_blank">`python-multipart`</a> 。 |
|||
|
|||
确保您创建、激活一个[虚拟环境](../virtual-environments.md){.internal-link target=_blank}后再安装。 |
|||
|
|||
```console |
|||
$ pip install python-multipart |
|||
``` |
|||
|
|||
/// |
|||
|
|||
/// note |
|||
|
|||
自 FastAPI 版本 `0.113.0` 起支持此功能。🤓 |
|||
|
|||
/// |
|||
|
|||
## 表单的 Pydantic 模型 |
|||
|
|||
您只需声明一个 **Pydantic 模型**,其中包含您希望接收的**表单字段**,然后将参数声明为 `Form` : |
|||
|
|||
{* ../../docs_src/request_form_models/tutorial001_an_py39.py hl[9:11,15] *} |
|||
|
|||
**FastAPI** 将从请求中的**表单数据**中**提取**出**每个字段**的数据,并提供您定义的 Pydantic 模型。 |
|||
|
|||
## 检查文档 |
|||
|
|||
您可以在文档 UI 中验证它,地址为 `/docs` : |
|||
|
|||
<div class="screenshot"> |
|||
<img src="/img/tutorial/request-form-models/image01.png"> |
|||
</div> |
|||
|
|||
## 禁止额外的表单字段 |
|||
|
|||
在某些特殊使用情况下(可能并不常见),您可能希望将表单字段**限制**为仅在 Pydantic 模型中声明过的字段,并**禁止**任何**额外**的字段。 |
|||
|
|||
/// note |
|||
|
|||
自 FastAPI 版本 `0.114.0` 起支持此功能。🤓 |
|||
|
|||
/// |
|||
|
|||
您可以使用 Pydantic 的模型配置来禁止( `forbid` )任何额外( `extra` )字段: |
|||
|
|||
{* ../../docs_src/request_form_models/tutorial002_an_py39.py hl[12] *} |
|||
|
|||
如果客户端尝试发送一些额外的数据,他们将收到**错误**响应。 |
|||
|
|||
例如,如果客户端尝试发送这样的表单字段: |
|||
|
|||
* `username`: `Rick` |
|||
* `password`: `Portal Gun` |
|||
* `extra`: `Mr. Poopybutthole` |
|||
|
|||
他们将收到一条错误响应,表明字段 `extra` 是不被允许的: |
|||
|
|||
```json |
|||
{ |
|||
"detail": [ |
|||
{ |
|||
"type": "extra_forbidden", |
|||
"loc": ["body", "extra"], |
|||
"msg": "Extra inputs are not permitted", |
|||
"input": "Mr. Poopybutthole" |
|||
} |
|||
] |
|||
} |
|||
``` |
|||
|
|||
## 总结 |
|||
|
|||
您可以使用 Pydantic 模型在 FastAPI 中声明表单字段。😎 |
@ -0,0 +1,360 @@ |
|||
# SQL(关系型)数据库 |
|||
|
|||
**FastAPI** 并不要求您使用 SQL(关系型)数据库。您可以使用**任何**想用的数据库。 |
|||
|
|||
这里,我们来看一个使用 <a href="https://sqlmodel.tiangolo.com/" class="external-link" target="_blank">SQLModel</a> 的示例。 |
|||
|
|||
**SQLModel** 是基于 <a href="https://www.sqlalchemy.org/" class="external-link" target="_blank">SQLAlchemy</a> 和 Pydantic 构建的。它由 **FastAPI** 的同一作者制作,旨在完美匹配需要使用 **SQL 数据库**的 FastAPI 应用程序。 |
|||
|
|||
/// tip |
|||
|
|||
您可以使用任何其他您想要的 SQL 或 NoSQL 数据库(在某些情况下称为 <abbr title="对象关系映射器(Object Relational Mapper,ORM),一个术语,用来指代一种库,其中某些类对应于 SQL 数据表,这些类的实例则对应于表中的行。">“ORM”</abbr>),FastAPI 不会强迫您使用任何东西。😎 |
|||
|
|||
/// |
|||
|
|||
由于 SQLModel 基于 SQLAlchemy,因此您可以轻松使用任何由 SQLAlchemy **支持的数据库**(这也让它们被 SQLModel 支持),例如: |
|||
|
|||
* PostgreSQL |
|||
* MySQL |
|||
* SQLite |
|||
* Oracle |
|||
* Microsoft SQL Server 等. |
|||
|
|||
在这个例子中,我们将使用 **SQLite**,因为它使用单个文件,并且 Python 对其有集成支持。因此,您可以直接复制这个例子并运行。 |
|||
|
|||
之后,对于您的生产应用程序,您可能会想要使用像 PostgreSQL 这样的数据库服务器。 |
|||
|
|||
/// tip |
|||
|
|||
有一个使用 **FastAPI** 和 **PostgreSQL** 的官方的项目生成器,其中包括了前端和更多工具: <a href="https://github.com/fastapi/full-stack-fastapi-template" class="external-link" target="_blank">https://github.com/fastapi/full-stack-fastapi-template</a> |
|||
|
|||
/// |
|||
|
|||
这是一个非常简单和简短的教程。如果您想了解一般的数据库、SQL 或更高级的功能,请查看 <a href="https://sqlmodel.tiangolo.com/" class="external-link" target="_blank">SQLModel 文档</a>。 |
|||
|
|||
## 安装 `SQLModel` |
|||
|
|||
首先,确保您创建并激活了[虚拟环境](../virtual-environments.md){.internal-link target=_blank},然后安装了 `sqlmodel` : |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ pip install sqlmodel |
|||
---> 100% |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
## 创建含有单一模型的应用程序 |
|||
|
|||
我们首先创建应用程序的最简单的第一个版本,只有一个 **SQLModel** 模型。 |
|||
|
|||
稍后我们将通过下面的**多个模型**提高其安全性和多功能性。🤓 |
|||
|
|||
### 创建模型 |
|||
|
|||
导入 `SQLModel` 并创建一个数据库模型: |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[1:11] hl[7:11] *} |
|||
|
|||
`Hero` 类与 Pydantic 模型非常相似(实际上,从底层来看,它确实*就是一个 Pydantic 模型*)。 |
|||
|
|||
有一些区别: |
|||
|
|||
* `table=True` 会告诉 SQLModel 这是一个*表模型*,它应该表示 SQL 数据库中的一个*表*,而不仅仅是一个*数据模型*(就像其他常规的 Pydantic 类一样)。 |
|||
|
|||
* `Field(primary_key=True)` 会告诉 SQLModel `id` 是 SQL 数据库中的**主键**(您可以在 SQLModel 文档中了解更多关于 SQL 主键的信息)。 |
|||
|
|||
把类型设置为 `int | None` ,SQLModel 就能知道该列在 SQL 数据库中应该是 `INTEGER` 类型,并且应该是 `NULLABLE` 。 |
|||
|
|||
* `Field(index=True)` 会告诉 SQLModel 应该为此列创建一个 **SQL 索引**,这样在读取按此列过滤的数据时,程序能在数据库中进行更快的查找。 |
|||
|
|||
SQLModel 会知道声明为 `str` 的内容将是类型为 `TEXT` (或 `VARCHAR` ,具体取决于数据库)的 SQL 列。 |
|||
|
|||
### 创建引擎(Engine) |
|||
|
|||
SQLModel 的引擎 `engine`(实际上它是一个 SQLAlchemy `engine` )是用来与数据库**保持连接**的。 |
|||
|
|||
您只需构建**一个 `engine`**,来让您的所有代码连接到同一个数据库。 |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[14:18] hl[14:15,17:18] *} |
|||
|
|||
使用 `check_same_thread=False` 可以让 FastAPI 在不同线程中使用同一个 SQLite 数据库。这很有必要,因为**单个请求**可能会使用**多个线程**(例如在依赖项中)。 |
|||
|
|||
不用担心,我们会按照代码结构确保**每个请求使用一个单独的 SQLModel *会话***,这实际上就是 `check_same_thread` 想要实现的。 |
|||
|
|||
### 创建表 |
|||
|
|||
然后,我们来添加一个函数,使用 `SQLModel.metadata.create_all(engine)` 为所有*表模型***创建表**。 |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[21:22] hl[21:22] *} |
|||
|
|||
### 创建会话(Session)依赖项 |
|||
|
|||
**`Session`** 会存储**内存中的对象**并跟踪数据中所需更改的内容,然后它**使用 `engine`** 与数据库进行通信。 |
|||
|
|||
我们会使用 `yield` 创建一个 FastAPI **依赖项**,为每个请求提供一个新的 `Session` 。这确保我们每个请求使用一个单独的会话。🤓 |
|||
|
|||
然后我们创建一个 `Annotated` 的依赖项 `SessionDep` 来简化其他也会用到此依赖的代码。 |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[25:30] hl[25:27,30] *} |
|||
|
|||
### 在启动时创建数据库表 |
|||
|
|||
我们会在应用程序启动时创建数据库表。 |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[32:37] hl[35:37] *} |
|||
|
|||
此处,在应用程序启动事件中,我们创建了表。 |
|||
|
|||
而对于生产环境,您可能会用一个能够在启动应用程序之前运行的迁移脚本。🤓 |
|||
|
|||
/// tip |
|||
|
|||
SQLModel 将会拥有封装 Alembic 的迁移工具,但目前您可以直接使用 <a href="https://alembic.sqlalchemy.org/en/latest/" class="external-link" target="_blank">Alembic</a>。 |
|||
|
|||
/// |
|||
|
|||
### 创建 Hero 类 |
|||
|
|||
因为每个 SQLModel 模型同时也是一个 Pydantic 模型,所以您可以在与 Pydantic 模型相同的**类型注释**中使用它。 |
|||
|
|||
例如,如果您声明一个类型为 `Hero` 的参数,它将从 **JSON 主体**中读取数据。 |
|||
|
|||
同样,您可以将其声明为函数的**返回类型**,然后数据的结构就会显示在自动生成的 API 文档界面中。 |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[40:45] hl[40:45] *} |
|||
|
|||
</details> |
|||
|
|||
这里,我们使用 `SessionDep` 依赖项(一个 `Session` )将新的 `Hero` 添加到 `Session` 实例中,提交更改到数据库,刷新 hero 中的数据,并返回它。 |
|||
|
|||
### 读取 Hero 类 |
|||
|
|||
我们可以使用 `select()` 从数据库中**读取** `Hero` 类,并利用 `limit` 和 `offset` 来对结果进行分页。 |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[48:55] hl[51:52,54] *} |
|||
|
|||
### 读取单个 Hero |
|||
|
|||
我们可以**读取**单个 `Hero` 。 |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[58:63] hl[60] *} |
|||
|
|||
### 删除单个 Hero |
|||
|
|||
我们也可以**删除**单个 `Hero` 。 |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial001_an_py310.py ln[66:73] hl[71] *} |
|||
|
|||
### 运行应用程序 |
|||
|
|||
您可以运行这个应用程序: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ fastapi dev main.py |
|||
|
|||
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
然后在 `/docs` UI 中,您能够看到 **FastAPI** 会用这些**模型**来**记录** API,并且还会用它们来**序列化**和**验证**数据。 |
|||
|
|||
<div class="screenshot"> |
|||
<img src="/img/tutorial/sql-databases/image01.png"> |
|||
</div> |
|||
|
|||
## 更新应用程序以支持多个模型 |
|||
|
|||
现在让我们稍微**重构**一下这个应用,以提高**安全性**和**多功能性**。 |
|||
|
|||
如果您查看之前的应用程序,您可以在 UI 界面中看到,到目前为止,由客户端决定要创建的 `Hero` 的 `id` 值。😱 |
|||
|
|||
我们不应该允许这样做,因为他们可能会覆盖我们在数据库中已经分配的 `id` 。决定 `id` 的行为应该由**后端**或**数据库**来完成,**而非客户端**。 |
|||
|
|||
此外,我们为 hero 创建了一个 `secret_name` ,但到目前为止,我们在各处都返回了它,这就不太**秘密**了……😅 |
|||
|
|||
我们将通过添加一些**额外的模型**来解决这些问题,而 SQLModel 将在这里大放异彩。✨ |
|||
|
|||
### 创建多个模型 |
|||
|
|||
在 **SQLModel** 中,任何含有 `table=True` 属性的模型类都是一个**表模型**。 |
|||
|
|||
任何不含有 `table=True` 属性的模型类都是**数据模型**,这些实际上只是 Pydantic 模型(附带一些小的额外功能)。🤓 |
|||
|
|||
有了 SQLModel,我们就可以利用**继承**来在所有情况下**避免重复**所有字段。 |
|||
|
|||
#### `HeroBase` - 基类 |
|||
|
|||
我们从一个 `HeroBase` 模型开始,该模型具有所有模型**共享的字段**: |
|||
|
|||
* `name` |
|||
* `age` |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:9] hl[7:9] *} |
|||
|
|||
#### `Hero` - *表模型* |
|||
|
|||
接下来,我们创建 `Hero` ,实际的*表模型*,并添加那些不总是在其他模型中的**额外字段**: |
|||
|
|||
* `id` |
|||
* `secret_name` |
|||
|
|||
因为 `Hero` 继承自 HeroBase ,所以它**也**包含了在 `HeroBase` 中声明过的**字段**。因此 `Hero` 的所有字段为: |
|||
|
|||
* `id` |
|||
* `name` |
|||
* `age` |
|||
* `secret_name` |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:14] hl[12:14] *} |
|||
|
|||
#### `HeroPublic` - 公共*数据模型* |
|||
|
|||
接下来,我们创建一个 `HeroPublic` 模型,这是将**返回**给 API 客户端的模型。 |
|||
|
|||
它包含与 `HeroBase` 相同的字段,因此不会包括 `secret_name` 。 |
|||
|
|||
终于,我们英雄(hero)的身份得到了保护! 🥷 |
|||
|
|||
它还重新声明了 `id: int` 。这样我们便与 API 客户端建立了一种**约定**,使他们始终可以期待 `id` 存在并且是一个整数 `int`(永远不会是 `None` )。 |
|||
|
|||
/// tip |
|||
|
|||
确保返回模型始终提供一个值并且始终是 `int` (而不是 `None` )对 API 客户端非常有用,他们可以在这种确定性下编写更简单的代码。 |
|||
|
|||
此外,**自动生成的客户端**将拥有更简洁的接口,这样与您的 API 交互的开发者就能更轻松地使用您的 API。😎 |
|||
|
|||
/// |
|||
|
|||
`HeroPublic` 中的所有字段都与 `HeroBase` 中的相同,其中 `id` 声明为 `int` (不是 `None` ): |
|||
|
|||
* `id` |
|||
* `name` |
|||
* `age` |
|||
* `secret_name` |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:18] hl[17:18] *} |
|||
|
|||
#### `HeroCreate` - 用于创建 hero 的*数据模型* |
|||
|
|||
现在我们创建一个 `HeroCreate` 模型,这是用于**验证**客户数据的模型。 |
|||
|
|||
它不仅拥有与 `HeroBase` 相同的字段,还有 `secret_name` 。 |
|||
|
|||
现在,当客户端**创建一个新的 hero** 时,他们会发送 `secret_name` ,它会被存储到数据库中,但这些 `secret_name` 不会通过 API 返回给客户端。 |
|||
|
|||
/// tip |
|||
|
|||
这应当是**密码**被处理的方式:接收密码,但不要通过 API 返回它们。 |
|||
|
|||
在存储密码之前,您还应该对密码的值进行**哈希**处理,**绝不要以明文形式存储它们**。 |
|||
|
|||
/// |
|||
|
|||
`HeroCreate` 的字段包括: |
|||
|
|||
* `name` |
|||
* `age` |
|||
* `secret_name` |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:22] hl[21:22] *} |
|||
|
|||
#### `HeroUpdate` - 用于更新 hero 的*数据模型* |
|||
|
|||
在之前的应用程序中,我们没有办法**更新 hero**,但现在有了**多个模型**,我们便能做到这一点了。🎉 |
|||
|
|||
`HeroUpdate` *数据模型*有些特殊,它包含创建新 hero 所需的**所有相同字段**,但所有字段都是**可选的**(它们都有默认值)。这样,当您更新一个 hero 时,您可以只发送您想要更新的字段。 |
|||
|
|||
因为所有**字段实际上**都发生了**变化**(类型现在包括 `None` ,并且它们现在有一个默认值 `None` ),我们需要**重新声明**它们。 |
|||
|
|||
我们会重新声明所有字段,因此我们并不是真的需要从 `HeroBase` 继承。我会让它继承只是为了保持一致,但这并不必要。这更多是个人喜好的问题。🤷 |
|||
|
|||
`HeroUpdate` 的字段包括: |
|||
|
|||
* `name` |
|||
* `age` |
|||
* `secret_name` |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[7:28] hl[25:28] *} |
|||
|
|||
### 使用 `HeroCreate` 创建并返回 `HeroPublic` |
|||
|
|||
既然我们有了**多个模型**,我们就可以对使用它们的应用程序部分进行更新。 |
|||
|
|||
我们在请求中接收到一个 `HeroCreate` *数据模型*,然后从中创建一个 `Hero` *表模型*。 |
|||
|
|||
这个新的*表模型* `Hero` 会包含客户端发送的字段,以及一个由数据库生成的 `id` 。 |
|||
|
|||
然后我们将与函数中相同的*表模型* `Hero` 原样返回。但是由于我们使用 `HeroPublic` *数据模型*声明了 `response_model` ,**FastAPI** 会使用 `HeroPublic` 来验证和序列化数据。 |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[56:62] hl[56:58] *} |
|||
|
|||
/// tip |
|||
|
|||
现在我们使用 `response_model=HeroPublic` 来代替**返回类型注释** `-> HeroPublic` ,因为我们返回的值实际上**并不是** `HeroPublic` 类型。 |
|||
|
|||
如果我们声明了 `-> HeroPublic` ,您的编辑器和代码检查工具会抱怨(但也确实理所应当)您返回了一个 `Hero` 而不是一个 `HeroPublic` 。 |
|||
|
|||
通过 `response_model` 的声明,我们让 **FastAPI** 按照它自己的方式处理,而不会干扰类型注解以及编辑器和其他工具提供的帮助。 |
|||
|
|||
/// |
|||
|
|||
### 用 `HeroPublic` 读取 Hero |
|||
|
|||
我们可以像之前一样**读取** `Hero` 。同样,使用 `response_model=list[HeroPublic]` 确保正确地验证和序列化数据。 |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[65:72] hl[65] *} |
|||
|
|||
### 用 `HeroPublic` 读取单个 Hero |
|||
|
|||
我们可以**读取**单个 `hero` 。 |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[75:80] hl[77] *} |
|||
|
|||
### 用 `HeroUpdate` 更新单个 Hero |
|||
|
|||
我们可以**更新**单个 `hero` 。为此,我们会使用 HTTP 的 `PATCH` 操作。 |
|||
|
|||
在代码中,我们会得到一个 `dict` ,其中包含客户端发送的所有数据,**只有客户端发送的数据**,并排除了任何一个仅仅作为默认值存在的值。为此,我们使用 `exclude_unset=True` 。这是最主要的技巧。🪄 |
|||
|
|||
然后我们会使用 `hero_db.sqlmodel_update(hero_data)` ,来利用 `hero_data` 的数据更新 `hero_db` 。 |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[83:93] hl[83:84,88:89] *} |
|||
|
|||
### (又一次)删除单个 Hero |
|||
|
|||
**删除**一个 hero 基本保持不变。 |
|||
|
|||
我们不会满足在这一部分中重构一切的愿望。😅 |
|||
|
|||
{* ../../docs_src/sql_databases/tutorial002_an_py310.py ln[96:103] hl[101] *} |
|||
|
|||
### (又一次)运行应用程序 |
|||
|
|||
您可以再运行一次应用程序: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ fastapi dev main.py |
|||
|
|||
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
您会在 `/docs` API UI 看到它现在已经更新,并且在进行创建 hero 等操作时,它不会再期望从客户端接收 `id` 数据。 |
|||
|
|||
<div class="screenshot"> |
|||
<img src="/img/tutorial/sql-databases/image02.png"> |
|||
</div> |
|||
|
|||
## 总结 |
|||
|
|||
您可以使用 <a href="https://sqlmodel.tiangolo.com/" class="external-link" target="_blank">**SQLModel**</a> 与 SQL 数据库进行交互,并通过*数据模型*和*表模型*简化代码。 |
|||
|
|||
您可以在 SQLModel 的文档中学习到更多内容,其中有一个更详细的关于<a href="https://sqlmodel.tiangolo.com/tutorial/fastapi/" class="external-link" target="_blank">如何将 SQLModel 与 FastAPI 一起使用的教程</a>。🚀 |
Loading…
Reference in new issue