committed by
GitHub
101 changed files with 5341 additions and 1048 deletions
@ -62,10 +62,7 @@ jobs: |
|||
env: |
|||
PROJECT_NAME: fastapitiangolo |
|||
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] |
|||
# uses: cloudflare/wrangler-action@v3 |
|||
uses: cloudflare/wrangler-action@v3 |
|||
with: |
|||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} |
|||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} |
|||
|
After Width: | Height: | Size: 9.3 KiB |
After Width: | Height: | Size: 21 KiB |
@ -0,0 +1,77 @@ |
|||
# クッキーパラメータモデル |
|||
|
|||
もし関連する**複数のクッキー**から成るグループがあるなら、それらを宣言するために、**Pydanticモデル**を作成できます。🍪 |
|||
|
|||
こうすることで、**複数の場所**で**その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モデル**を提供します。 |
|||
|
|||
## ドキュメントの確認 |
|||
|
|||
対話的APIドキュメントUI `/docs` で、定義されているクッキーを確認できます: |
|||
|
|||
<div class="screenshot"> |
|||
<img src="/img/tutorial/cookie-param-models/image01.png"> |
|||
</div> |
|||
|
|||
/// info | 備考 |
|||
|
|||
|
|||
**ブラウザがクッキーを処理し**ていますが、特別な方法で内部的に処理を行っているために、**JavaScript**からは簡単に操作**できない**ことに留意してください。 |
|||
|
|||
**対話的APIドキュメントUI** `/docs` にアクセスすれば、*パスオペレーション*に関するクッキーの**ドキュメンテーション**を確認できます。 |
|||
|
|||
しかし、たとえ**クッキーデータを入力して**「Execute」をクリックしても、対話的APIドキュメントUIは**JavaScript**で動作しているためクッキーは送信されず、まるで値を入力しなかったかのような**エラー**メッセージが表示されます。 |
|||
|
|||
/// |
|||
|
|||
## 余分なクッキーを禁止する |
|||
|
|||
特定の(あまり一般的ではないかもしれない)ケースで、受け付けるクッキーを**制限**する必要があるかもしれません。 |
|||
|
|||
あなたのAPIは独自の <abbr title="念のためですが、これはジョークです。クッキー同意とは関係ありませんが、APIでさえ不適切なクッキーを拒否できるとは愉快ですね。クッキーでも食べてください。🍪 (原文: This is a joke, just in case. It has nothing to do with cookie consents, but it's funny that even the API can now reject the poor cookies. Have a cookie. 🍪)">クッキー同意</abbr> を管理する能力を持っています。 🤪🍪 |
|||
|
|||
Pydanticのモデルの Configuration を利用して、 `extra` フィールドを `forbid` とすることができます。 |
|||
|
|||
{* ../../docs_src/cookie_param_models/tutorial002_an_py39.py hl[10] *} |
|||
|
|||
もしクライアントが**余分なクッキー**を送ろうとすると、**エラー**レスポンスが返されます。 |
|||
|
|||
<abbr title="これもジョークです。気にしないでください。クッキーのお供にコーヒーでも飲んでください。☕ (原文: This is another joke. Don't pay attention to me. Have some coffee for your cookie. ☕)">どうせAPIに拒否されるのに</abbr>あなたの同意を得ようと精一杯努力する可哀想なクッキーバナーたち... 🍪 |
|||
|
|||
例えば、クライアントがクッキー `santa_tracker` を `good-list-please` という値で送ろうとすると、`santa_tracker` という <abbr title="サンタはクッキー不足を良しとはしないでしょう。🎅 はい、クッキージョークはもう止めておきます。(原文: Santa disapproves the lack of cookies. 🎅 Okay, no more cookie jokes.)">クッキーが許可されていない</abbr> ことを通知する**エラー**レスポンスが返されます: |
|||
|
|||
```json |
|||
{ |
|||
"detail": [ |
|||
{ |
|||
"type": "extra_forbidden", |
|||
"loc": ["cookie", "santa_tracker"], |
|||
"msg": "Extra inputs are not permitted", |
|||
"input": "good-list-please", |
|||
} |
|||
] |
|||
} |
|||
``` |
|||
|
|||
## まとめ |
|||
|
|||
**FastAPI**では、<abbr title="帰ってしまう前に最後のクッキーをどうぞ。🍪 (原文: Have a last cookie before you go. 🍪)">**クッキー**</abbr>を宣言するために、**Pydanticモデル**を使用できます。😎 |
@ -0,0 +1,68 @@ |
|||
# クエリパラメータモデル |
|||
|
|||
もし関連する**複数のクエリパラメータ**から成るグループがあるなら、それらを宣言するために、**Pydanticモデル**を作成できます。 |
|||
|
|||
こうすることで、**複数の場所**で**そのPydanticモデルを再利用**でき、バリデーションやメタデータを、すべてのクエリパラメータに対して一度に宣言できます。😎 |
|||
|
|||
/// note | 備考 |
|||
|
|||
この機能は、FastAPIのバージョン `0.115.0` からサポートされています。🤓 |
|||
|
|||
/// |
|||
|
|||
## クエリパラメータにPydanticモデルを使用する |
|||
|
|||
必要な**複数のクエリパラメータ**を**Pydanticモデル**で宣言し、さらに、それを `Query` として宣言しましょう: |
|||
|
|||
{* ../../docs_src/query_param_models/tutorial001_an_py310.py hl[9:13,17] *} |
|||
|
|||
**FastAPI**は、リクエストの**クエリパラメータ**からそれぞれの**フィールド**のデータを**抽出**し、定義された**Pydanticモデル**を提供します。 |
|||
|
|||
## ドキュメントの確認 |
|||
|
|||
対話的APIドキュメント `/docs` でクエリパラメータを確認できます: |
|||
|
|||
<div class="screenshot"> |
|||
<img src="/img/tutorial/query-param-models/image01.png"> |
|||
</div> |
|||
|
|||
## 余分なクエリパラメータを禁止する |
|||
|
|||
特定の(あまり一般的ではないかもしれない)ケースで、受け付けるクエリパラメータを**制限**する必要があるかもしれません。 |
|||
|
|||
Pydanticのモデルの Configuration を利用して、 `extra` フィールドを `forbid` とすることができます。 |
|||
|
|||
{* ../../docs_src/query_param_models/tutorial002_an_py310.py hl[10] *} |
|||
|
|||
もしクライアントが**クエリパラメータ**として**余分な**データを送ろうとすると、**エラー**レスポンスが返されます。 |
|||
|
|||
例えば、クライアントがクエリパラメータ `tool` に、値 `plumbus` を設定して送ろうとすると: |
|||
|
|||
```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" |
|||
} |
|||
] |
|||
} |
|||
``` |
|||
|
|||
## まとめ |
|||
|
|||
**FastAPI**では、**クエリパラメータ**を宣言するために、**Pydanticモデル**を使用できます。😎 |
|||
|
|||
/// tip | 豆知識 |
|||
|
|||
ネタバレ注意: Pydanticモデルはクッキーやヘッダーの宣言にも使用できますが、その内容については後のチュートリアルで学びます。🤫 |
|||
|
|||
/// |
@ -0,0 +1,831 @@ |
|||
# 仮想環境 |
|||
|
|||
Pythonプロジェクトの作業では、**仮想環境**(または類似の仕組み)を使用し、プロジェクトごとにインストールするパッケージを分離するべきでしょう。 |
|||
|
|||
/// info | 情報 |
|||
|
|||
もし、仮想環境の概要や作成方法、使用方法について既にご存知なら、このセクションをスキップすることができます。🤓 |
|||
|
|||
/// |
|||
|
|||
/// tip | 豆知識 |
|||
|
|||
**仮想環境**は、**環境変数**とは異なります。 |
|||
|
|||
**環境変数**は、プログラムが使用できるシステム内の変数です。 |
|||
|
|||
**仮想環境**は、ファイルをまとめたディレクトリのことです。 |
|||
|
|||
/// |
|||
|
|||
/// info | 情報 |
|||
このページでは、**仮想環境**の使用方法と、そのはたらきについて説明します。 |
|||
|
|||
もし**すべてを管理するツール**(Pythonのインストールも含む)を導入する準備ができているなら、<a href="https://github.com/astral-sh/uv" class="external-link" target="_blank">uv</a> をお試しください。 |
|||
|
|||
/// |
|||
|
|||
## プロジェクトの作成 |
|||
|
|||
まず、プロジェクト用のディレクトリを作成します。 |
|||
|
|||
私は通常 home/user ディレクトリの中に `code` というディレクトリを用意していて、プロジェクトごとに1つのディレクトリをその中に作成しています。 |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
// Go to the home directory |
|||
$ cd |
|||
// Create a directory for all your code projects |
|||
$ mkdir code |
|||
// Enter into that code directory |
|||
$ cd code |
|||
// Create a directory for this project |
|||
$ mkdir awesome-project |
|||
// Enter into that project directory |
|||
$ cd awesome-project |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
## 仮想環境の作成 |
|||
|
|||
Pythonプロジェクトでの**初めての**作業を開始する際には、**<abbr title="他の選択肢もありますが、これはシンプルなガイドラインです">プロジェクト内</abbr>**に仮想環境を作成してください。 |
|||
|
|||
/// tip | 豆知識 |
|||
|
|||
これを行うのは、**プロジェクトごとに1回だけ**です。作業のたびに行う必要はありません。 |
|||
|
|||
/// |
|||
|
|||
//// tab | `venv` |
|||
|
|||
仮想環境を作成するには、Pythonに付属している `venv` モジュールを使用できます。 |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ python -m venv .venv |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
/// details | このコマンドの意味 |
|||
|
|||
- `python` : `python` というプログラムを呼び出します |
|||
- `-m` : モジュールをスクリプトとして呼び出します。どのモジュールを呼び出すのか、この次に指定します |
|||
- `venv` : 通常Pythonに付随してインストールされる `venv`モジュールを使用します |
|||
- `.venv` : 仮想環境を`.venv`という新しいディレクトリに作成します |
|||
|
|||
/// |
|||
|
|||
//// |
|||
|
|||
//// tab | `uv` |
|||
|
|||
もし <a href="https://github.com/astral-sh/uv" class="external-link" target="_blank">`uv`</a> をインストール済みなら、仮想環境を作成するために `uv` を使うこともできます。 |
|||
|
|||
<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="command line interface">CLI</abbr>)プログラム**を使用する場合に、仮想環境内のものが確実に使われ、グローバル環境にインストールされている別のもの(おそらく必要なものとは異なるバージョン)を誤って使用することを防ぎます。 |
|||
|
|||
/// |
|||
|
|||
## 仮想環境が有効であることを確認する |
|||
|
|||
仮想環境が有効である(前のコマンドが正常に機能した)ことを確認します。 |
|||
|
|||
/// tip | 豆知識 |
|||
|
|||
これは**任意**ですが、すべてが期待通りに機能し、意図した仮想環境を使用していることを**確認する**良い方法です。 |
|||
|
|||
/// |
|||
|
|||
//// tab | Linux, macOS, Windows Bash |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ which python |
|||
|
|||
/home/user/code/awesome-project/.venv/bin/python |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
`.venv/bin/python` にある `python` バイナリが、プロジェクト(この場合は `awesome-project` )内に表示されていれば、正常に動作しています 🎉。 |
|||
|
|||
//// |
|||
|
|||
//// tab | Windows PowerShell |
|||
|
|||
<div class="termy"> |
|||
|
|||
``` console |
|||
$ Get-Command python |
|||
|
|||
C:\Users\user\code\awesome-project\.venv\Scripts\python |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
`.venv\Scripts\python` にある `python` バイナリが、プロジェクト(この場合は `awesome-project` )内に表示されていれば、正常に動作しています 🎉。 |
|||
|
|||
//// |
|||
|
|||
## `pip` をアップグレードする |
|||
|
|||
/// tip | 豆知識 |
|||
|
|||
もし <a href="https://github.com/astral-sh/uv" class="external-link" target="_blank">`uv`</a> を使用している場合は、 `pip` の代わりに `uv` を使ってインストールを行うため、 `pip` をアップグレードする必要はありません 😎。 |
|||
|
|||
/// |
|||
|
|||
もしパッケージのインストールに `pip`(Pythonに標準で付属しています)を使用しているなら、 `pip` を最新バージョンに**アップグレード**しましょう。 |
|||
|
|||
パッケージのインストール中に発生する想定外のエラーの多くは、最初に `pip` をアップグレードしておくだけで解決されます。 |
|||
|
|||
/// tip | 豆知識 |
|||
|
|||
通常、これは仮想環境を作成した直後に**一度だけ**実行します。 |
|||
|
|||
/// |
|||
|
|||
仮想環境が有効であることを(上で説明したコマンドで)確認し、アップグレードを実行しましょう: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ python -m pip install --upgrade pip |
|||
|
|||
---> 100% |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
## `.gitignore` を追加する |
|||
|
|||
**Git**を使用している場合(使用するべきでしょう)、 `.gitignore` ファイルを追加して、 `.venv` 内のあらゆるファイルをGitの管理対象から除外します。 |
|||
|
|||
/// 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` ディレクトリ内のすべてがGitに無視されるようになります。 |
|||
|
|||
このコマンドは以下のテキストを持つ `.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を使った作業をするには、 [Python](https://www.python.org/) のインストールが必要です。 |
|||
|
|||
それから、FastAPIや、使用したいその他の**パッケージ**を**インストール**する必要があります。 |
|||
|
|||
パッケージをインストールするには、通常、Python に付属する `pip` コマンド (または同様の代替コマンド) を使用します。 |
|||
|
|||
ただし、`pip` を直接使用すると、パッケージは**グローバルなPython環境**(OS全体にインストールされた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> |
|||
|
|||
これにより、`harry` (ハリー)バージョン1がグローバルなPython環境にインストールされます。 |
|||
|
|||
```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` をインストールし直す必要があります。(あるいは、単に`harry` (ハリー)のバージョン `3` をインストールすることで、自動的にバージョン `1` がアンインストールされます) |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ pip install "harry==3" |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
このようにして、グローバル環境への `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のパッケージでは、**新しいバージョン**で**互換性を損なう変更を避ける**よう努めるのが一般的ですが、それでも注意が必要です。すべてが正常に動作することをテストで確認してから、意図的に指定して新しいバージョンをインストールするのが良いでしょう。 |
|||
|
|||
/// |
|||
|
|||
あなたのすべての**プロジェクトが依存している**、**多数の**他の**パッケージ**が上記の問題を抱えていると想像してください。これは管理が非常に困難です。そして、**互換性のないバージョン**のパッケージを使ってプロジェクトを実行し、なぜ動作しないのか分からなくなるでしょう。 |
|||
|
|||
また、使用しているOS(Linux、Windows、macOS など)によっては、Pythonがすでにインストールされていることがあります。この場合、特定のバージョンのパッケージが**OSの動作に必要である**ことがあります。グローバル環境にパッケージをインストールすると、OSに付属するプログラムを**壊してしまう**可能性があります。 |
|||
|
|||
## パッケージのインストール先 |
|||
|
|||
Pythonをインストールしたとき、ファイルを含んだいくつかのディレクトリが作成されます。 |
|||
|
|||
これらの中には、インストールされたパッケージを保存するためのものもあります。 |
|||
|
|||
以下のコマンドを実行したとき: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
// Don't run this now, it's just an example 🤓 |
|||
$ pip install "fastapi[standard]" |
|||
---> 100% |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
FastAPIのコードを含む圧縮ファイルが、通常は [PyPI](https://pypi.org/project/fastapi/) からダウンロードされます。 |
|||
|
|||
また、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 |
|||
|
|||
あるいは、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 | 豆知識 |
|||
|
|||
`PATH` 変数についての詳細は [環境変数](environment-variables.md#path環境変数){.internal-link target=_blank} を参照してください。 |
|||
|
|||
/// |
|||
|
|||
仮想環境を有効にすると、その仮想環境のパス `.venv/bin` (LinuxとmacOS)、あるいは `.venv\Scripts` (Windows)が `PATH` 変数に追加されます。 |
|||
|
|||
その環境を有効にする前の `PATH` 変数が次のようになっているとします。 |
|||
|
|||
//// tab | Linux, macOS |
|||
|
|||
```plaintext |
|||
/usr/bin:/bin:/usr/sbin:/sbin |
|||
``` |
|||
|
|||
これは、OSが以下のディレクトリ中でプログラムを探すことを意味します: |
|||
|
|||
* `/usr/bin` |
|||
* `/bin` |
|||
* `/usr/sbin` |
|||
* `/sbin` |
|||
|
|||
//// |
|||
|
|||
//// tab | Windows |
|||
|
|||
```plaintext |
|||
C:\Windows\System32 |
|||
``` |
|||
|
|||
これは、OSが以下のディレクトリ中でプログラムを探すことを意味します: |
|||
|
|||
* `C:\Windows\System32` |
|||
|
|||
//// |
|||
|
|||
仮想環境を有効にすると、 `PATH` 変数は次のようになります。 |
|||
|
|||
//// tab | Linux, macOS |
|||
|
|||
```plaintext |
|||
/home/user/code/awesome-project/.venv/bin:/usr/bin:/bin:/usr/sbin:/sbin |
|||
``` |
|||
|
|||
これは、OSが他のディレクトリを探すより前に、最初に以下のディレクトリ中でプログラムを探し始めることを意味します: |
|||
|
|||
```plaintext |
|||
/home/user/code/awesome-project/.venv/bin |
|||
``` |
|||
|
|||
そのため、ターミナルで `python` と入力した際に、OSはPythonプログラムを以下のパスで発見し、使用します。 |
|||
|
|||
```plaintext |
|||
/home/user/code/awesome-project/.venv/bin/python |
|||
``` |
|||
|
|||
//// |
|||
|
|||
//// tab | Windows |
|||
|
|||
```plaintext |
|||
C:\Users\user\code\awesome-project\.venv\Scripts;C:\Windows\System32 |
|||
``` |
|||
|
|||
これは、OSが他のディレクトリを探すより前に、最初に以下のディレクトリ中でプログラムを探し始めることを意味します: |
|||
|
|||
```plaintext |
|||
C:\Users\user\code\awesome-project\.venv\Scripts |
|||
``` |
|||
|
|||
そのため、ターミナルで `python` と入力した際に、OSはPythonプログラムを以下のパスで発見し、使用します。 |
|||
|
|||
```plaintext |
|||
C:\Users\user\code\awesome-project\.venv\Scripts\python |
|||
``` |
|||
|
|||
//// |
|||
|
|||
重要な点は、仮想環境のパスを `PATH` 変数の**先頭**に配置することです。OSは利用可能な他のPythonを見つけるより**前に**、この仮想環境の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 |
|||
|
|||
// Error importing sirius, it's not installed 😱 |
|||
Traceback (most recent call last): |
|||
File "main.py", line 1, in <module> |
|||
import sirius |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
しかし、その仮想環境を無効化し、 `prisoner-of-azkaban` (アズカバンの囚人)のための新しい仮想環境を有効にすれば、 `python` を実行したときに `prisoner-of-azkaban` (アズカバンの囚人)の仮想環境の Python が使用されるようになります。 |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ cd ~/code/prisoner-of-azkaban |
|||
|
|||
// You don't need to be in the old directory to deactivate, you can do it wherever you are, even after going to the other project 😎 |
|||
$ deactivate |
|||
|
|||
// Activate the virtual environment in prisoner-of-azkaban/.venv 🚀 |
|||
$ source .venv/bin/activate |
|||
|
|||
// Now when you run python, it will find the package sirius installed in this virtual environment ✨ |
|||
$ 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 のインストール** |
|||
* プロジェクトごとの**仮想環境**の管理 |
|||
* **パッケージ**のインストール |
|||
* プロジェクトのパッケージの**依存関係やバージョン**の管理 |
|||
* パッケージとそのバージョンの、依存関係を含めた**厳密な**組み合わせを保持し、これによって、本番環境で、開発環境と全く同じようにプロジェクトを実行できる(これは**locking**と呼ばれます) |
|||
* その他のさまざまな機能 |
|||
|
|||
## まとめ |
|||
|
|||
ここまで読みすべて理解したなら、世間の多くの開発者と比べて、仮想環境について**あなたはより多くのことを知っています**。🤓 |
|||
|
|||
これらの詳細を知ることは、将来、複雑に見える何かのデバッグにきっと役立つでしょう。しかし、その頃には、あなたは**そのすべての動作を根本から**理解しているでしょう。😎 |
@ -0,0 +1,273 @@ |
|||
# 패스워드 해싱을 이용한 OAuth2, JWT 토큰을 사용하는 Bearer 인증 |
|||
|
|||
모든 보안 흐름을 구성했으므로, 이제 <abbr title="JSON Web Tokens">JWT</abbr> 토큰과 패스워드 해싱을 사용해 애플리케이션을 안전하게 만들 것입니다. |
|||
|
|||
이 코드는 실제로 애플리케이션에서 패스워드를 해싱하여 DB에 저장하는 등의 작업에 활용할 수 있습니다. |
|||
|
|||
이전 장에 이어서 시작해 봅시다. |
|||
|
|||
## JWT |
|||
|
|||
JWT 는 "JSON Web Tokens" 을 의미합니다. |
|||
|
|||
JSON 객체를 공백이 없는 긴 문자열로 인코딩하는 표준이며, 다음과 같은 형태입니다: |
|||
|
|||
``` |
|||
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c |
|||
``` |
|||
|
|||
JWT는 암호화되지 않아 누구든지 토큰에서 정보를 복원할 수 있습니다. |
|||
|
|||
하지만 JWT는 서명되어 있습니다. 그래서 자신이 발급한 토큰을 받았을 때, 실제로 자신이 발급한게 맞는지 검증할 수 있습니다. |
|||
|
|||
만료 기간이 일주일인 토큰을 발행했다고 가정해 봅시다. 다음 날 사용자가 토큰을 가져왔을 때, 그 사용자가 시스템에 여전히 로그인되어 있다는 것을 알 수 있습니다. |
|||
|
|||
일주일 뒤에는 토큰이 만료될 것이고, 사용자는 인가되지 않아 새 토큰을 받기 위해 다시 로그인해야 할 것입니다. 만약 사용자(또는 제3자)가 토큰을 수정하거나 만료일을 변경하면, 서명이 일치하지 않기 때문에 알아챌 수 있을 것입니다. |
|||
|
|||
만약 JWT 토큰을 다뤄보고, 작동 방식도 알아보고 싶다면 <a href="https://jwt.io/" class="external-link" target="_blank">https://jwt.io</a> 을 확인하십시오. |
|||
|
|||
## `PyJWT` 설치 |
|||
|
|||
파이썬으로 JWT 토큰을 생성하고 검증하려면 `PyJWT` 를 설치해야 합니다. |
|||
|
|||
[가상환경](../../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> 에서 확인할 수 있습니다. |
|||
|
|||
/// |
|||
|
|||
## 패스워드 해싱 |
|||
|
|||
"해싱(Hashing)"은 어떤 내용(여기서는 패스워드)을 해석할 수 없는 일련의 바이트 집합(단순 문자열)으로 변환하는 것을 의미합니다. |
|||
|
|||
동일한 내용(똑같은 패스워드)을 해싱하면 동일한 문자열을 얻습니다. |
|||
|
|||
하지만 그 문자열을 다시 패스워드로 되돌릴 수는 없습니다. |
|||
|
|||
### 패스워드를 해싱하는 이유 |
|||
|
|||
데이터베이스를 탈취당하더라도, 침입자는 사용자의 평문 패스워드 대신 해시 값만 얻을 수 있습니다. |
|||
|
|||
따라서 침입자는 훔친 사용자 패스워드를 다른 시스템에서 활용할 수 없습니다. (대다수 사용자가 여러 시스템에서 동일한 패스워드를 사용하기 때문에 평문 패스워드가 유출되면 위험합니다.) |
|||
|
|||
## `passlib` 설치 |
|||
|
|||
PassLib는 패스워드 해시를 다루는 훌륭한 파이썬 패키지입니다. |
|||
|
|||
많은 안전한 해시 알고리즘과 도구들을 지원합니다. |
|||
|
|||
추천하는 알고리즘은 "Bcrypt"입니다. |
|||
|
|||
[가상환경](../../virtual-environments.md){.internal-link target=_blank} 을 만들고 활성화한 다음 PassLib와 Bcrypt를 설치하십시오: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ pip install "passlib[bcrypt]" |
|||
|
|||
---> 100% |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
/// tip | 팁 |
|||
|
|||
`passlib`를 사용하여, **Django**, **Flask** 의 보안 플러그인이나 다른 도구로 생성한 패스워드를 읽을 수 있도록 설정할 수도 있습니다. |
|||
|
|||
예를 들자면, FastAPI 애플리케이션과 Django 애플리케이션이 같은 데이터베이스에서 데이터를 공유할 수 있습니다. 또는 같은 데이터베이스를 사용하여 Django 애플리케이션을 점진적으로 마이그레이션 할 수도 있습니다. |
|||
|
|||
그리고 사용자는 FastAPI 애플리케이션과 Django 애플리케이션에 동시에 로그인할 수 있습니다. |
|||
|
|||
/// |
|||
|
|||
## 패스워드의 해시와 검증 |
|||
|
|||
필요한 도구를 `passlib`에서 임포트합니다. |
|||
|
|||
PassLib "컨텍스트(context)"를 생성합니다. 이것은 패스워드를 해싱하고 검증하는데 사용합니다. |
|||
|
|||
/// 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`에 대입합니다. (이 예제의 변수 값을 그대로 사용하지 마십시오.) |
|||
|
|||
JWT 토큰을 서명하는 데 사용될 알고리즘을 위한 변수 `ALGORITHM` 을 생성하고 `"HS256"` 으로 설정합니다. |
|||
|
|||
토큰 만료 기간을 위한 변수를 생성합니다. |
|||
|
|||
응답을 위한 토큰 엔드포인트에 사용될 Pydantic 모델을 정의합니다. |
|||
|
|||
새 액세스 토큰을 생성하기 위한 유틸리티 함수를 생성합니다. |
|||
|
|||
{* ../../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 "주체(subject)" `sub`에 대한 기술 세부 사항 |
|||
|
|||
JWT 명세에 따르면 토큰의 주체를 포함하는 `sub`라는 키가 있습니다. |
|||
|
|||
사용 여부는 선택사항이지만, 사용자의 식별 정보를 저장할 수 있으므로 여기서는 이를 사용합니다. |
|||
|
|||
JWT는 사용자를 식별하고 사용자가 API를 직접 사용할 수 있도록 허용하는 것 외에도 다른 용도로 사용될 수도 있습니다. |
|||
|
|||
예를 들어 "자동차"나 "블로그 게시물"을 식별하는 데 사용할 수 있습니다. |
|||
|
|||
그리고 "자동차를 운전하다"나 "블로그 게시물을 수정하다"처럼 해당 엔터티에 대한 권한을 추가할 수 있습니다. |
|||
|
|||
그 후 이 JWT 토큰을 사용자(또는 봇)에게 제공하면, 그들은 계정을 따로 만들 필요 없이 API가 생성한 JWT 토큰만으로 작업(자동차 운전 또는 블로그 게시물 편집)을 수행할 수 있습니다. |
|||
|
|||
이러한 개념을 활용하면 JWT는 훨씬 더 복잡한 시나리오에도 사용할 수 있습니다. |
|||
|
|||
이 경우 여러 엔터티가 동일한 ID를 가질 수 있습니다. 예를 들어 foo라는 ID를 가진 사용자, 자동차, 블로그 게시물이 있을 수 있습니다. |
|||
|
|||
그래서 ID 충돌을 방지하기 위해, 사용자의 JWT 토큰을 생성할 때 접두사로 `sub` 키를 추가할 수 있습니다. 예를 들어 `username:` 을 붙이는 방식입니다. 이 예제에서는 `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 |
|||
|
|||
`Bearer `로 시작하는 `Authorization` 헤더에 주목하십시오. |
|||
|
|||
/// |
|||
|
|||
## `scopes` 의 고급 사용법 |
|||
|
|||
OAuth2는 "스코프(scopes)" 라는 개념을 갖고 있습니다. |
|||
|
|||
이를 사용하여 JWT 토큰에 특정 권한 집합을 추가할 수 있습니다. |
|||
|
|||
그 후 이 토큰을 사용자에게 직접 제공하거나 제3자에게 제공하여, 특정 제한사항 하에있는 API와 통신하도록 할 수 있습니다. |
|||
|
|||
**FastAPI** 에서의 사용 방법과 통합 방식은 **심화 사용자 안내서** 에서 자세히 배울 수 있습니다. |
|||
|
|||
## 요약 |
|||
|
|||
지금까지 살펴본 내용을 바탕으로, OAuth2와 JWT 같은 표준을 사용하여 안전한 **FastAPI** 애플리케이션을 만들 수 있습니다. |
|||
|
|||
거의 모든 프레임워크에서 보안 처리는 상당히 복잡한 주제입니다. |
|||
|
|||
이를 단순화하는 많은 패키지는 데이터 모델, 데이터베이스, 사용 가능한 기능들에 대해 여러 제약이 있습니다. 그리고 지나치게 단순화하는 일부 패키지들은 심각한 보안 결함을 가질 수도 있습니다. |
|||
|
|||
--- |
|||
|
|||
**FastAPI** 는 어떤 데이터베이스, 데이터 모델, 도구도 강요하지 않습니다. |
|||
|
|||
프로젝트에 가장 적합한 것을 선택할 수 있는 유연성을 제공합니다. |
|||
|
|||
그리고 `passlib` 와 `PyJWT` 처럼 잘 관리되고 널리 사용되는 패키지들을 바로 사용할 수 있습니다. **FastAPI** 는 외부 패키지 통합을 위해 복잡한 메커니즘이 필요하지 않기 때문입니다. |
|||
|
|||
그러나 유연성, 견고성, 보안성을 해치지 않으면서 과정을 단순화할 수 있는 도구들을 제공합니다. |
|||
|
|||
그리고 OAuth2와 같은 표준 프로토콜을 비교적 간단한 방법으로 구현하고 사용할 수 있습니다. |
|||
|
|||
더 세분화된 권한 체계를 위해 OAuth2의 "스코프"를 사용하는 방법은 **심화 사용자 안내서**에서 더 자세히 배울 수 있습니다. OAuth2의 스코프는 제3자 애플리케이션이 사용자를 대신해 그들의 API와 상호작용하도록 권한을 부여하기 위해, Facebook, Google, GitHub, Microsoft, Twitter 등의 많은 대형 인증 제공업체들이 사용하는 메커니즘입니다. |
@ -0,0 +1,74 @@ |
|||
# Middleware (Промежуточный слой) |
|||
|
|||
Вы можете добавить промежуточный слой (middleware) в **FastAPI** приложение. |
|||
|
|||
"Middleware" это функция, которая выполняется с каждым запросом до его обработки какой-либо конкретной *операцией пути*. |
|||
А также с каждым ответом перед его возвращением. |
|||
|
|||
|
|||
* Она принимает каждый поступающий **запрос**. |
|||
* Может что-то сделать с этим **запросом** или выполнить любой нужный код. |
|||
* Затем передает **запрос** для последующей обработки (какой-либо *операцией пути*). |
|||
* Получает **ответ** (от *операции пути*). |
|||
* Может что-то сделать с этим **ответом** или выполнить любой нужный код. |
|||
* И возвращает **ответ**. |
|||
|
|||
/// note | Технические детали |
|||
|
|||
Если у вас есть зависимости с `yield`, то код выхода (код после `yield`) будет выполняться *после* middleware. |
|||
|
|||
Если у вас имеются некие фоновые задачи (см. документацию), то они будут запущены после middleware. |
|||
|
|||
/// |
|||
|
|||
## Создание middleware |
|||
|
|||
Для создания middleware используйте декоратор `@app.middleware("http")`. |
|||
|
|||
Функция middleware получает: |
|||
|
|||
* `request` (объект запроса). |
|||
* Функцию `call_next`, которая получает `request` в качестве параметра. |
|||
* Эта функция передаёт `request` соответствующей *операции пути*. |
|||
* Затем она возвращает ответ `response`, сгенерированный *операцией пути*. |
|||
* Также имеется возможность видоизменить `response`, перед тем как его вернуть. |
|||
|
|||
{* ../../docs_src/middleware/tutorial001.py hl[8:9,11,14] *} |
|||
|
|||
/// tip | Примечание |
|||
|
|||
Имейте в виду, что можно добавлять свои собственные заголовки <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers" class="external-link" target="_blank">при помощи префикса 'X-'</a>. |
|||
|
|||
Если же вы хотите добавить собственные заголовки, которые клиент сможет увидеть в браузере, то вам потребуется добавить их в настройки CORS ([CORS (Cross-Origin Resource Sharing)](cors.md){.internal-link target=_blank}), используя параметр `expose_headers`, см. документацию <a href="https://www.starlette.io/middleware/#corsmiddleware" class="external-link" target="_blank">Starlette's CORS docs</a>. |
|||
|
|||
/// |
|||
|
|||
/// note | Технические детали |
|||
|
|||
Вы также можете использовать `from starlette.requests import Request`. |
|||
|
|||
**FastAPI** предоставляет такой доступ для удобства разработчиков. Но, на самом деле, это `Request` из Starlette. |
|||
|
|||
/// |
|||
|
|||
### До и после `response` |
|||
|
|||
Вы можете добавить код, использующий `request` до передачи его какой-либо *операции пути*. |
|||
|
|||
А также после формирования `response`, до того, как вы его вернёте. |
|||
|
|||
Например, вы можете добавить собственный заголовок `X-Process-Time`, содержащий время в секундах, необходимое для обработки запроса и генерации ответа: |
|||
|
|||
{* ../../docs_src/middleware/tutorial001.py hl[10,12:13] *} |
|||
|
|||
/// tip | Примечание |
|||
|
|||
Мы используем <a href="https://docs.python.org/3/library/time.html#time.perf_counter" class="external-link" target="_blank">`time.perf_counter()`</a> вместо `time.time()` для обеспечения большей точности наших примеров. 🤓 |
|||
|
|||
/// |
|||
|
|||
## Другие middleware |
|||
|
|||
О других middleware вы можете узнать больше в разделе [Advanced User Guide: Advanced Middleware](../advanced/middleware.md){.internal-link target=_blank}. |
|||
|
|||
В следующем разделе вы можете прочитать, как настроить <abbr title="Cross-Origin Resource Sharing">CORS</abbr> с помощью middleware. |
@ -0,0 +1,170 @@ |
|||
# Тіло запиту - Декілька параметрів |
|||
|
|||
Тепер, коли ми розглянули використання `Path` та `Query`, розгляньмо більш просунуті способи оголошення тіла запиту в **FastAPI**. |
|||
|
|||
## Змішування `Path`, `Query` та параметрів тіла запиту |
|||
|
|||
По-перше, звісно, Ви можете вільно змішувати оголошення параметрів `Path`, `Query` та тіла запиту, і **FastAPI** правильно їх обробить. |
|||
|
|||
Також Ви можете оголосити параметри тіла як необов’язкові, встановивши для них значення за замовчуванням `None`: |
|||
|
|||
{* ../../docs_src/body_multiple_params/tutorial001_an_py310.py hl[18:20] *} |
|||
|
|||
/// note | Примітка |
|||
|
|||
Зверніть увагу, що в цьому випадку параметр `item`, який береться з тіла запиту, є необов'язковим, оскільки має значення за замовчуванням `None`. |
|||
|
|||
/// |
|||
|
|||
## Декілька параметрів тіла запиту |
|||
|
|||
У попередньому прикладі *операція шляху* очікувала JSON з атрибутами `Item`, наприклад: |
|||
|
|||
```JSON |
|||
{ |
|||
"name": "Foo", |
|||
"description": "The pretender", |
|||
"price": 42.0, |
|||
"tax": 3.2 |
|||
} |
|||
``` |
|||
Але Ви також можете оголосити декілька параметрів тіла, наприклад `item` та `user`: |
|||
|
|||
{* ../../docs_src/body_multiple_params/tutorial002_py310.py hl[20] *} |
|||
|
|||
У цьому випадку **FastAPI** розпізнає, що є кілька параметрів тіла (два параметри є моделями Pydantic). |
|||
|
|||
Тому він використає назви параметрів як ключі (назви полів) у тілі запиту, очікуючи: |
|||
|
|||
```JSON |
|||
{ |
|||
"item": { |
|||
"name": "Foo", |
|||
"description": "The pretender", |
|||
"price": 42.0, |
|||
"tax": 3.2 |
|||
}, |
|||
"user": { |
|||
"username": "dave", |
|||
"full_name": "Dave Grohl" |
|||
} |
|||
} |
|||
``` |
|||
|
|||
/// note | Примітка |
|||
|
|||
Зверніть увагу, що хоча `item` оголошено, так само як і раніше, тепер він очікується в тілі під ключем `item`. |
|||
|
|||
/// |
|||
|
|||
**FastAPI** автоматично конвертує дані із запиту таким чином, щоб параметр `item` отримав свій вміст, і те ж саме стосується `user`. |
|||
|
|||
Він виконає валідацію складених даних і задокументує їх відповідним чином у схемі OpenAPI та в автоматичній документації. |
|||
|
|||
## Одиничні значення в тілі запиту |
|||
|
|||
Так само як є `Query` і `Path` для визначення додаткових даних для параметрів запиту та шляху, **FastAPI** надає еквівалентний `Body`. |
|||
|
|||
Наприклад, розширюючи попередню модель, Ви можете вирішити додати ще один ключ `importance` в те ж саме тіло запиту разом із `item` і `user`. |
|||
|
|||
Якщо Ви оголосите його як є, то, оскільки це одиничне значення, **FastAPI** припускатиме, що це параметр запиту (query parameter). |
|||
|
|||
Але Ви можете вказати **FastAPI** обробляти його як інший ключ тіла (body key), використовуючи `Body`: |
|||
|
|||
{* ../../docs_src/body_multiple_params/tutorial003_an_py310.py hl[23] *} |
|||
|
|||
У цьому випадку **FastAPI** очікуватиме тіло запиту у такому вигляді: |
|||
|
|||
```JSON |
|||
{ |
|||
"item": { |
|||
"name": "Foo", |
|||
"description": "The pretender", |
|||
"price": 42.0, |
|||
"tax": 3.2 |
|||
}, |
|||
"user": { |
|||
"username": "dave", |
|||
"full_name": "Dave Grohl" |
|||
}, |
|||
"importance": 5 |
|||
} |
|||
``` |
|||
Знову ж таки, **FastAPI** конвертуватиме типи даних, перевірятиме їх, створюватиме документацію тощо. |
|||
|
|||
## Декілька body та query параметрів |
|||
|
|||
Звісно, Ви можете оголошувати додаткові query параметри запиту, коли це необхідно, на додаток до будь-яких параметрів тіла запиту. |
|||
|
|||
Оскільки за замовчуванням окремі значення інтерпретуються як параметри запиту, Вам не потрібно явно додавати `Query`, можна просто використати: |
|||
|
|||
```Python |
|||
q: Union[str, None] = None |
|||
``` |
|||
|
|||
Або в Python 3.10 та вище: |
|||
|
|||
```Python |
|||
q: str | None = None |
|||
``` |
|||
|
|||
Наприклад: |
|||
|
|||
{* ../../docs_src/body_multiple_params/tutorial004_an_py310.py hl[28] *} |
|||
|
|||
|
|||
/// info | Інформація |
|||
|
|||
`Body` також має ті самі додаткові параметри валідації та метаданих, що й `Query`, `Path` та інші, які Ви побачите пізніше. |
|||
|
|||
/// |
|||
|
|||
## Вкладений поодинокий параметр тіла запиту |
|||
|
|||
Припустимо, у вас є лише один параметр тіла запиту `item` з моделі Pydantic `Item`. |
|||
|
|||
За замовчуванням **FastAPI** очікуватиме, що тіло запиту міститиме вміст безпосередньо. |
|||
|
|||
Але якщо Ви хочете, щоб він очікував JSON з ключем `item`, а всередині — вміст моделі (так, як це відбувається при оголошенні додаткових параметрів тіла), Ви можете використати спеціальний параметр `Body` — `embed`: |
|||
|
|||
```Python |
|||
item: Item = Body(embed=True) |
|||
``` |
|||
|
|||
як у прикладі: |
|||
|
|||
{* ../../docs_src/body_multiple_params/tutorial005_an_py310.py hl[17] *} |
|||
|
|||
У цьому випадку **FastAPI** очікуватиме тіло запиту такого вигляду: |
|||
|
|||
```JSON hl_lines="2" |
|||
{ |
|||
"item": { |
|||
"name": "Foo", |
|||
"description": "The pretender", |
|||
"price": 42.0, |
|||
"tax": 3.2 |
|||
} |
|||
} |
|||
``` |
|||
|
|||
замість: |
|||
|
|||
```JSON |
|||
{ |
|||
"name": "Foo", |
|||
"description": "The pretender", |
|||
"price": 42.0, |
|||
"tax": 3.2 |
|||
} |
|||
``` |
|||
|
|||
## Підсумок |
|||
|
|||
Ви можете додавати кілька параметрів тіла до Вашої *функції операції шляху* (*path operation function*), навіть якщо запит може мати лише одне тіло. |
|||
|
|||
Але **FastAPI** обробить це, надасть Вам потрібні дані у функції, перевірить їх та задокументує коректну схему в *операції шляху*. |
|||
|
|||
Також Ви можете оголошувати окремі значення, які будуть отримані як частина тіла запиту. |
|||
|
|||
Крім того, Ви можете вказати **FastAPI** вбудовувати тіло в ключ, навіть якщо оголошено лише один параметр. |
@ -0,0 +1,245 @@ |
|||
# Тіло запиту - Вкладені моделі |
|||
|
|||
З **FastAPI** Ви можете визначати, перевіряти, документувати та використовувати моделі, які можуть бути вкладені на будь-яку глибину (завдяки Pydantic). |
|||
|
|||
## Поля списку |
|||
|
|||
Ви можете визначити атрибут як підтип. Наприклад, Python-список (`list`): |
|||
|
|||
{* ../../docs_src/body_nested_models/tutorial001_py310.py hl[12] *} |
|||
|
|||
Це зробить `tags` списком, хоча не визначається тип елементів списку. |
|||
|
|||
## Поля списку з параметром типу |
|||
|
|||
Але Python має специфічний спосіб оголошення списків з внутрішніми типами або "параметрами типу": |
|||
### Імпортуємо `List` з модуля typing |
|||
|
|||
У Python 3.9 і вище можна використовувати стандартний `list` для оголошення таких типів, як ми побачимо нижче. 💡 |
|||
|
|||
Але в Python версії до 3.9 (від 3.6 і вище) спочатку потрібно імпортувати `List` з модуля стандартної бібліотеки Python `typing`: |
|||
|
|||
{* ../../docs_src/body_nested_models/tutorial002.py hl[1] *} |
|||
|
|||
### Оголошення `list` з параметром типу |
|||
|
|||
Щоб оголосити типи з параметрами типу (внутрішніми типами), такими як `list`, `dict`, `tuple`: |
|||
|
|||
* Якщо Ви використовуєте версію Python до 3.9, імпортуйте їх відповідну версію з модуля `typing`. |
|||
* Передайте внутрішні типи як "параметри типу", використовуючи квадратні дужки: `[` and `]`. |
|||
|
|||
У Python 3.9 це буде виглядати так: |
|||
|
|||
```Python |
|||
my_list: list[str] |
|||
``` |
|||
|
|||
У версіях Python до 3.9 це виглядає так: |
|||
|
|||
```Python |
|||
from typing import List |
|||
|
|||
my_list: List[str] |
|||
``` |
|||
|
|||
Це стандартний синтаксис Python для оголошення типів. |
|||
|
|||
Використовуйте той самий стандартний синтаксис для атрибутів моделей з внутрішніми типами. |
|||
|
|||
Отже, у нашому прикладі, ми можемо зробити `tags` саме "списком рядків": |
|||
|
|||
{* ../../docs_src/body_nested_models/tutorial002_py310.py hl[12] *} |
|||
|
|||
## Типи множин |
|||
|
|||
Але потім ми подумали, що теги не повинні повторюватися, вони, ймовірно, повинні бути унікальними рядками. |
|||
|
|||
І Python має спеціальний тип даних для множин унікальних елементів — це `set`. |
|||
|
|||
Тому ми можемо оголосити `tags` як множину рядків: |
|||
|
|||
{* ../../docs_src/body_nested_models/tutorial003_py310.py hl[12] *} |
|||
|
|||
Навіть якщо Ви отримаєте запит з дубльованими даними, він буде перетворений у множину унікальних елементів. |
|||
|
|||
І коли Ви будете виводити ці дані, навіть якщо джерело містить дублікати, вони будуть виведені як множина унікальних елементів. |
|||
|
|||
І це буде анотовано/документовано відповідно. |
|||
|
|||
## Вкладені моделі |
|||
|
|||
Кожен атрибут моделі Pydantic має тип. |
|||
|
|||
Але цей тип сам може бути іншою моделлю Pydantic. |
|||
|
|||
Отже, Ви можете оголосити глибоко вкладені JSON "об'єкти" з конкретними іменами атрибутів, типами та перевірками. |
|||
|
|||
Усе це, вкладене без обмежень. |
|||
|
|||
### Визначення підмоделі |
|||
|
|||
Наприклад, ми можемо визначити модель `Image`: |
|||
|
|||
{* ../../docs_src/body_nested_models/tutorial004_py310.py hl[7:9] *} |
|||
|
|||
### Використання підмоделі як типу |
|||
|
|||
А потім ми можемо використовувати її як тип атрибута: |
|||
|
|||
{* ../../docs_src/body_nested_models/tutorial004_py310.py hl[18] *} |
|||
|
|||
Це означатиме, що **FastAPI** очікуватиме тіло запиту такого вигляду: |
|||
|
|||
```JSON |
|||
{ |
|||
"name": "Foo", |
|||
"description": "The pretender", |
|||
"price": 42.0, |
|||
"tax": 3.2, |
|||
"tags": ["rock", "metal", "bar"], |
|||
"image": { |
|||
"url": "http://example.com/baz.jpg", |
|||
"name": "The Foo live" |
|||
} |
|||
} |
|||
``` |
|||
|
|||
Завдяки такій декларації у **FastAPI** Ви отримуєте: |
|||
|
|||
* Підтримку в редакторі (автозавершення тощо), навіть для вкладених моделей |
|||
* Конвертацію даних |
|||
* Валідацію даних |
|||
* Автоматичну документацію |
|||
|
|||
## Спеціальні типи та валідація |
|||
|
|||
Окрім звичайних типів, таких як `str`, `int`, `float`, та ін. Ви можете використовувати складніші типи, які наслідують `str`. |
|||
|
|||
Щоб побачити всі доступні варіанти, ознайомтеся з оглядом <a href="https://docs.pydantic.dev/latest/concepts/types/" class="external-link" target="_blank">типів у Pydantic</a>. Деякі приклади будуть у наступних розділах. |
|||
|
|||
Наприклад, у моделі `Image` є поле `url`, тому ми можемо оголосити його як `HttpUrl` від Pydantic замість `str`: |
|||
|
|||
{* ../../docs_src/body_nested_models/tutorial005_py310.py hl[2,8] *} |
|||
|
|||
Рядок буде перевірено як дійсну URL-адресу і задокументовано в JSON Schema / OpenAPI як URL. |
|||
|
|||
## Атрибути зі списками підмоделей |
|||
|
|||
У Pydantic Ви можете використовувати моделі як підтипи для `list`, `set` тощо: |
|||
|
|||
{* ../../docs_src/body_nested_models/tutorial006_py310.py hl[18] *} |
|||
|
|||
Це означає, що **FastAPI** буде очікувати (конвертувати, валідувати, документувати тощо) JSON тіло запиту у вигляді: |
|||
|
|||
```JSON hl_lines="11" |
|||
{ |
|||
"name": "Foo", |
|||
"description": "The pretender", |
|||
"price": 42.0, |
|||
"tax": 3.2, |
|||
"tags": [ |
|||
"rock", |
|||
"metal", |
|||
"bar" |
|||
], |
|||
"images": [ |
|||
{ |
|||
"url": "http://example.com/baz.jpg", |
|||
"name": "The Foo live" |
|||
}, |
|||
{ |
|||
"url": "http://example.com/dave.jpg", |
|||
"name": "The Baz" |
|||
} |
|||
] |
|||
} |
|||
``` |
|||
|
|||
/// info | Інформація |
|||
|
|||
Зверніть увагу, що тепер ключ `images` містить список об'єктів зображень. |
|||
|
|||
/// |
|||
|
|||
## Глибоко вкладені моделі |
|||
|
|||
Ви можете визначати вкладені моделі довільної глибини: |
|||
|
|||
{* ../../docs_src/body_nested_models/tutorial007_py310.py hl[7,12,18,21,25] *} |
|||
|
|||
/// info | Інформація |
|||
|
|||
Зверніть увагу, що в моделі `Offer` є список `Item`ів, які, своєю чергою, можуть мати необов'язковий список `Image`ів. |
|||
|
|||
/// |
|||
|
|||
## Тіла запитів, що складаються зі списків |
|||
|
|||
Якщо верхній рівень JSON тіла, яке Ви очікуєте, є JSON `масивом` (у Python — `list`), Ви можете оголосити тип у параметрі функції, як і в моделях Pydantic: |
|||
|
|||
```Python |
|||
images: List[Image] |
|||
``` |
|||
або в Python 3.9 і вище: |
|||
|
|||
```Python |
|||
images: list[Image] |
|||
``` |
|||
|
|||
наприклад: |
|||
|
|||
{* ../../docs_src/body_nested_models/tutorial008_py39.py hl[13] *} |
|||
|
|||
## Підтримка в редакторі всюди |
|||
|
|||
Ви отримаєте підтримку в редакторі всюди. |
|||
|
|||
Навіть для елементів у списках: |
|||
|
|||
<img src="/img/tutorial/body-nested-models/image01.png"> |
|||
|
|||
Ви не змогли б отримати таку підтримку в редакторі, якби працювали напряму зі `dict`, а не з моделями Pydantic. |
|||
|
|||
Але Вам не потрібно турбуватися про це: вхідні dict'и автоматично конвертуються, а вихідні дані автоматично перетворюються в JSON. |
|||
|
|||
## Тіла з довільними `dict` |
|||
|
|||
Ви також можете оголосити тіло як `dict` з ключами одного типу та значеннями іншого типу. |
|||
|
|||
Це корисно, якщо Ви не знаєте наперед, які імена полів будуть дійсними (як у випадку з моделями Pydantic). |
|||
|
|||
Це буде корисно, якщо Ви хочете приймати ключі, які заздалегідь невідомі. |
|||
|
|||
--- |
|||
|
|||
Це також зручно, якщо Ви хочете мати ключі іншого типу (наприклад, `int`). |
|||
|
|||
Ось що ми розглянемо далі. |
|||
|
|||
У цьому випадку Ви можете приймати будь-який `dict`, якщо його ключі — це `int`, а значення — `float`: |
|||
|
|||
{* ../../docs_src/body_nested_models/tutorial009_py39.py hl[7] *} |
|||
|
|||
/// tip | Порада |
|||
|
|||
Майте на увазі, що в JSON тілі ключі можуть бути лише рядками (`str`). |
|||
|
|||
Але Pydantic автоматично конвертує дані. |
|||
|
|||
Це означає, що навіть якщо клієнти вашого API надсилатимуть ключі у вигляді рядків, якщо вони містять цілі числа, Pydantic конвертує їх і проведе валідацію. |
|||
|
|||
Тобто `dict`, який Ви отримаєте як `weights`, матиме ключі типу `int` та значення типу `float`. |
|||
|
|||
/// |
|||
|
|||
## Підсумок |
|||
|
|||
З **FastAPI** Ви маєте максимальну гнучкість завдяки моделям Pydantic, зберігаючи при цьому код простим, коротким та елегантним. |
|||
|
|||
А також отримуєте всі переваги: |
|||
|
|||
* Підтримка в редакторі (автодоповнення всюди!) |
|||
* Конвертація даних (парсинг/сериалізація) |
|||
* Валідація даних |
|||
* Документація схем |
|||
* Автоматичне створення документації |
@ -0,0 +1,76 @@ |
|||
# Моделі для Cookie-параметрів |
|||
|
|||
Якщо у Вас є група **cookies** параметрів, які пов'язані між собою, Ви можете створити **Pydantic-модель**, щоб оголосити їх. 🍪 |
|||
|
|||
Це дозволить Вам повторно **використовувати модель** у **різних місцях**, а також оголосити валідацію та метадані для всіх параметрів одночасно. 😎 |
|||
|
|||
/// note | Нотатки |
|||
|
|||
Це підтримується з версії FastAPI `0.115.0`. 🤓 |
|||
|
|||
/// |
|||
|
|||
/// tip | Порада |
|||
|
|||
Ця ж техніка застосовується до `Query`, `Cookie`, та `Header`. 😎 |
|||
|
|||
/// |
|||
|
|||
## Cookie з Pydantic-моделлю |
|||
|
|||
Оголосіть **cookie-параметри**, які Вам потрібні, у **Pydantic-моделі**, а потім оголосіть параметр як `Cookie`: |
|||
|
|||
{* ../../docs_src/cookie_param_models/tutorial001_an_py310.py hl[9:12,16] *} |
|||
|
|||
**FastAPI** буде **витягувати** дані для **кожного поля** з **cookie** параметрів, отриманих у запиті, і передавати Вам Pydantic-модель, яку Ви визначили. |
|||
|
|||
## Перевірка у документації |
|||
|
|||
Ви можете побачити визначені cookie в інтерфейсі документації за адресою `/docs`: |
|||
|
|||
<div class="screenshot"> |
|||
<img src="/img/tutorial/cookie-param-models/image01.png"> |
|||
</div> |
|||
|
|||
/// info | Інформація |
|||
|
|||
Майте на увазі, що оскільки **браузери обробляють cookie** особливим чином і "за лаштунками", вони **не** дозволяють **JavaScript** легко з ними працювати. |
|||
|
|||
Якщо Ви зайдете до **інтерфейсу документації API** за адресою `/docs`, Ви зможете побачити **документацію** для cookie у Ваших **операціях шляху**. |
|||
|
|||
Але навіть якщо Ви заповните дані й натиснете "Execute", оскільки інтерфейс документації працює з **JavaScript**, cookie не будуть відправлені, і Ви побачите **помилку**, ніби Ви не ввели жодних значень. |
|||
|
|||
/// |
|||
|
|||
## Заборона додаткових cookie |
|||
|
|||
У деяких спеціальних випадках (ймовірно, не дуже поширених) Ви можете захотіти **обмежити** список cookie, які хочете отримувати. |
|||
|
|||
Ваша API тепер має можливість контролювати власну <abbr title="Це жарт, якщо що. Це не має нічого спільного зі згодою на використання cookie, але це кумедно, що навіть API тепер може відхиляти бідні cookie. Ловіть печиво. 🍪">згоду на cookie</abbr>. 🤪🍪 |
|||
|
|||
Ви можете використовувати налаштування моделі Pydantic, щоб `заборонити` будь-які `додаткові` поля: |
|||
|
|||
{* ../../docs_src/cookie_param_models/tutorial002_an_py39.py hl[10] *} |
|||
|
|||
Якщо клієнт спробує надіслати якісь **додаткові cookie**, він отримає відповідь з **помилкою**. |
|||
|
|||
Бідні банери cookie, які так старанно намагаються отримати Вашу згоду, щоб <abbr title="Це ще один жарт. Не звертайте уваги. Візьміть каву для свого печива. ☕">API її відхилила</abbr>. 🍪 |
|||
|
|||
Наприклад, якщо клієнт спробує надіслати cookie `santa_tracker` зі значенням `good-list-please`, він отримає відповідь з помилкою, яка повідомить, що <abbr title="Санта не схвалює відсутність cookie. 🎅 Гаразд, більше жартів не буде.">cookie `santa_tracker` не дозволено</abbr>: |
|||
|
|||
```json |
|||
{ |
|||
"detail": [ |
|||
{ |
|||
"type": "extra_forbidden", |
|||
"loc": ["cookie", "santa_tracker"], |
|||
"msg": "Extra inputs are not permitted", |
|||
"input": "good-list-please", |
|||
} |
|||
] |
|||
} |
|||
``` |
|||
|
|||
## Підсумок |
|||
|
|||
Ви можете використовувати **Pydantic-моделі** для оголошення <abbr title="Отримайте останнє печиво перед тим, як піти. 🍪">cookie</abbr> у FastAPI. 😎 |
@ -0,0 +1,112 @@ |
|||
# Налагодження (Debugging) |
|||
|
|||
Ви можете під'єднати дебагер у Вашому редакторі коду, наприклад, у Visual Studio Code або PyCharm. |
|||
|
|||
## Виклик `uvicorn` |
|||
|
|||
У Вашому FastAPI-додатку імпортуйте та запустіть `uvicorn` безпосередньо: |
|||
|
|||
{* ../../docs_src/debugging/tutorial001.py hl[1,15] *} |
|||
|
|||
### Про `__name__ == "__main__"` |
|||
|
|||
Головна мета використання `__name__ == "__main__"` — це забезпечення виконання певного коду тільки тоді, коли файл запускається безпосередньо: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ python myapp.py |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
але не виконується при його імпорті в інший файл, наприклад: |
|||
|
|||
```Python |
|||
from myapp import app |
|||
``` |
|||
|
|||
#### Детальніше |
|||
|
|||
Припустимо, Ваш файл називається `myapp.py`. |
|||
|
|||
Якщо Ви запустите його так: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ python myapp.py |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
тоді внутрішня змінна `__name__`, яка створюється автоматично Python, матиме значення `"__main__"`. |
|||
|
|||
Отже, цей блок коду: |
|||
|
|||
```Python |
|||
uvicorn.run(app, host="0.0.0.0", port=8000) |
|||
``` |
|||
|
|||
буде виконаний. |
|||
|
|||
--- |
|||
|
|||
Це не станеться, якщо Ви імпортуєте цей модуль (файл). |
|||
|
|||
Якщо у Вас є інший файл, наприклад `importer.py`, з наступним кодом: |
|||
|
|||
```Python |
|||
from myapp import app |
|||
|
|||
# Додатковий код |
|||
``` |
|||
|
|||
У цьому випадку автоматично створена змінна у файлі `myapp.py` не матиме значення змінної `__name__` як `"__main__"`. |
|||
|
|||
Отже, рядок: |
|||
|
|||
```Python |
|||
uvicorn.run(app, host="0.0.0.0", port=8000) |
|||
``` |
|||
|
|||
не буде виконано. |
|||
|
|||
/// info | Інформація |
|||
|
|||
Більш детальну інформацію можна знайти в <a href="https://docs.python.org/3/library/__main__.html" class="external-link" target="_blank">офіційній документації Python</a>. |
|||
|
|||
/// |
|||
|
|||
## Запуск коду з вашим дебагером |
|||
|
|||
Оскільки Ви запускаєте сервер Uvicorn безпосередньо з Вашого коду, Ви можете запустити вашу Python програму (ваш FastAPI додаток) безпосередньо з дебагера. |
|||
|
|||
--- |
|||
|
|||
Наприклад, у Visual Studio Code Ви можете: |
|||
|
|||
* Перейдіть на вкладку "Debug". |
|||
* Натисніть "Add configuration...". |
|||
* Виберіть "Python" |
|||
* Запустіть дебагер з опцією "`Python: Current File (Integrated Terminal)`". |
|||
|
|||
Це запустить сервер з Вашим **FastAPI** кодом, зупиниться на точках зупину тощо. |
|||
|
|||
Ось як це може виглядати: |
|||
|
|||
<img src="/img/tutorial/debugging/image01.png"> |
|||
|
|||
--- |
|||
Якщо Ви використовуєте PyCharm, ви можете: |
|||
|
|||
* Відкрити меню "Run". |
|||
* Вибрати опцію "Debug...". |
|||
* Потім з'явиться контекстне меню. |
|||
* Вибрати файл для налагодження (у цьому випадку, `main.py`). |
|||
|
|||
Це запустить сервер з Вашим **FastAPI** кодом, зупиниться на точках зупину тощо. |
|||
|
|||
Ось як це може виглядати: |
|||
|
|||
<img src="/img/tutorial/debugging/image02.png"> |
@ -0,0 +1,58 @@ |
|||
# Моделі Параметрів Заголовків |
|||
|
|||
Якщо у Вас є група пов’язаних параметрів заголовків, Ви можете створити **Pydantic модель** для їх оголошення. |
|||
|
|||
Це дозволить Вам повторно **використовувати модель** в **різних місцях**, а також оголосити валідації та метадані для всіх параметрів одночасно. 😎 |
|||
|
|||
/// note | Нотатки |
|||
|
|||
Ця можливість підтримується починаючи з версії FastAPI `0.115.0`. 🤓 |
|||
|
|||
/// |
|||
|
|||
## Параметри Заголовків з Використанням Pydantic Model |
|||
|
|||
Оголосіть потрібні **параметри заголовків** у **Pydantic моделі**, а потім оголосіть параметр як `Header`: |
|||
|
|||
{* ../../docs_src/header_param_models/tutorial001_an_py310.py hl[9:14,18] *} |
|||
|
|||
FastAPI буде витягувати дані для кожного поля з заголовків у запиті та передавати їх у створену Вами Pydantic модель. |
|||
|
|||
**FastAPI** буде **витягувати** дані для **кожного поля** з **заголовків** у запиті та передавати їх у створену Вами Pydantic модель. |
|||
|
|||
## Перевірка в Документації |
|||
|
|||
Ви можете побачити необхідні заголовки в інтерфейсі документації за адресою `/docs`: |
|||
|
|||
<div class="screenshot"> |
|||
<img src="/img/tutorial/header-param-models/image01.png"> |
|||
</div> |
|||
|
|||
## Заборона Додаткових Заголовків |
|||
|
|||
У деяких особливих випадках (ймовірно, не дуже поширених) Ви можете захотіти **обмежити** заголовки, які хочете отримати. |
|||
|
|||
Ви можете використати конфігурацію моделі Pydantic, щоб `заборонити` будь-які `додаткові` поля: |
|||
|
|||
{* ../../docs_src/header_param_models/tutorial002_an_py310.py hl[10] *} |
|||
|
|||
Якщо клієнт спробує надіслати **додаткові заголовки**, він отримає **помилку** у відповіді. |
|||
|
|||
Наприклад, якщо клієнт спробує надіслати заголовок `tool` зі значенням `plumbus`, він отримає **помилку** з повідомленням про те, що параметр заголовка `tool` не дозволений: |
|||
|
|||
```json |
|||
{ |
|||
"detail": [ |
|||
{ |
|||
"type": "extra_forbidden", |
|||
"loc": ["header", "tool"], |
|||
"msg": "Extra inputs are not permitted", |
|||
"input": "plumbus", |
|||
} |
|||
] |
|||
} |
|||
``` |
|||
|
|||
## Підсумок |
|||
|
|||
Ви можете використовувати **Pydantic моделі** для оголошення **заголовків** у **FastAPI**. 😎 |
@ -0,0 +1,91 @@ |
|||
# Header-параметри |
|||
|
|||
Ви можете визначати параметри заголовків, так само як визначаєте `Query`, `Path` і `Cookie` параметри. |
|||
|
|||
## Імпорт `Header` |
|||
|
|||
Спочатку імпортуйте `Header`: |
|||
|
|||
{* ../../docs_src/header_params/tutorial001_an_py310.py hl[3] *} |
|||
|
|||
## Оголошення параметрів `Header` |
|||
|
|||
Потім оголосіть параметри заголовків, використовуючи ту ж структуру, що й для `Path`, `Query` та `Cookie`. |
|||
|
|||
Ви можете визначити значення за замовчуванням, а також усі додаткові параметри валідації або анотації: |
|||
|
|||
{* ../../docs_src/header_params/tutorial001_an_py310.py hl[9] *} |
|||
|
|||
/// note | Технічні деталі |
|||
|
|||
`Header`є "сестринським" класом для `Path`, `Query` і `Cookie`. Він також успадковується від загального класу `Param`. |
|||
|
|||
Але пам’ятайте, що при імпорті `Query`, `Path`, `Header` та інших із `fastapi`, то насправді вони є функціями, які повертають спеціальні класи. |
|||
|
|||
/// |
|||
|
|||
/// info | Інформація |
|||
|
|||
Щоб оголосити заголовки, потрібно використовувати `Header`, інакше параметри будуть інтерпретуватися як параметри запиту. |
|||
|
|||
/// |
|||
|
|||
## Автоматичне перетворення |
|||
|
|||
`Header` має додатковий функціонал порівняно з `Path`, `Query` та `Cookie`. |
|||
|
|||
Більшість стандартних заголовків розділяються символом «дефіс», також відомим як «мінус» (`-`). |
|||
|
|||
Але змінна, така як `user-agent`, є недійсною в Python. |
|||
|
|||
Тому, за замовчуванням, `Header` автоматично перетворює символи підкреслення (`_`) на дефіси (`-`) для отримання та документування заголовків. |
|||
|
|||
Оскільки заголовки HTTP не чутливі до регістру, Ви можете використовувати стандартний стиль Python ("snake_case"). |
|||
|
|||
Тому Ви можете використовувати `user_agent`, як зазвичай у коді Python, замість того щоб писати з великої літери, як `User_Agent` або щось подібне. |
|||
|
|||
Якщо Вам потрібно вимкнути автоматичне перетворення підкреслень у дефіси, встановіть `convert_underscores` в `Header` значення `False`: |
|||
|
|||
{* ../../docs_src/header_params/tutorial002_an_py310.py hl[10] *} |
|||
|
|||
/// warning | Увага |
|||
|
|||
Перед тим як встановити значення `False` для `convert_underscores` пам’ятайте, що деякі HTTP-проксі та сервери не підтримують заголовки з підкресленнями. |
|||
|
|||
/// |
|||
|
|||
## Дубльовані заголовки |
|||
|
|||
Можливо отримати дубльовані заголовки, тобто той самий заголовок із кількома значеннями. |
|||
|
|||
Це можна визначити, використовуючи список у типізації параметра. |
|||
|
|||
Ви отримаєте всі значення дубльованого заголовка у вигляді `list` у Python. |
|||
|
|||
Наприклад, щоб оголосити заголовок `X-Token`, який може з’являтися більше ніж один раз: |
|||
|
|||
{* ../../docs_src/header_params/tutorial003_an_py310.py hl[9] *} |
|||
|
|||
Якщо Ви взаємодієте з цією операцією шляху, надсилаючи два HTTP-заголовки, наприклад: |
|||
|
|||
``` |
|||
X-Token: foo |
|||
X-Token: bar |
|||
``` |
|||
|
|||
Відповідь буде така: |
|||
|
|||
```JSON |
|||
{ |
|||
"X-Token values": [ |
|||
"bar", |
|||
"foo" |
|||
] |
|||
} |
|||
``` |
|||
|
|||
## Підсумок |
|||
|
|||
Оголошуйте заголовки за допомогою `Header`, використовуючи той самий підхід, що й для `Query`, `Path` та `Cookie`. |
|||
|
|||
Не хвилюйтеся про підкреслення у змінних — **FastAPI** автоматично конвертує їх. |
@ -0,0 +1,120 @@ |
|||
# Метадані та URL-адреси документації |
|||
|
|||
Ви можете налаштувати кілька конфігурацій метаданих у Вашому додатку **FastAPI**. |
|||
|
|||
## Метадані для API |
|||
|
|||
Ви можете встановити такі поля, які використовуються в специфікації OpenAPI та в автоматично згенерованих інтерфейсах документації API: |
|||
|
|||
| Параметр | Тип | Опис | |
|||
|------------|------|-------------| |
|||
| `title` | `str` | Назва API. | |
|||
| `summary` | `str` | Короткий опис API. <small>Доступно з OpenAPI 3.1.0, FastAPI 0.99.0.</small> | |
|||
| `description` | `str` | Більш детальний опис API. Може використовувати Markdown. | |
|||
| `version` | `string` | Версія API. Це версія Вашого додатка, а не OpenAPI. Наприклад, `2.5.0`. | |
|||
| `terms_of_service` | `str` | URL до умов використання API. Якщо вказано, має бути у форматі URL. | |
|||
| `contact` | `dict` | Інформація для контакту з API. Може містити кілька полів. <details><summary><code>contact</code> поля</summary><table><thead><tr><th>Параметр</th><th>Тип</th><th>Опис</th></tr></thead><tbody><tr><td><code>name</code></td><td><code>str</code></td><td>Ім'я контактної особи або організації.</td></tr><tr><td><code>url</code></td><td><code>str</code></td><td>URL з інформацією для контакту. Повинен бути у форматі URL.</td></tr><tr><td><code>email</code></td><td><code>str</code></td><td>Email контактної особи або організації. Повинен бути у форматі електронної пошти.</td></tr></tbody></table></details> | |
|||
| `license_info` | `dict` | Інформація про ліцензію для API. Може містити кілька полів. <details><summary><code>license_info</code> поля</summary><table><thead><tr><th>Параметр</th><th>Тип</th><th>Опис</th></tr></thead><tbody><tr><td><code>name</code></td><td><code>str</code></td><td><strong>ОБОВ'ЯЗКОВО</strong> (якщо встановлено <code>license_info</code>). Назва ліцензії для API.</td></tr><tr><td><code>identifier</code></td><td><code>str</code></td><td>Ліцензійний вираз за <a href="https://spdx.org/licenses/" class="external-link" target="_blank">SPDX</a> для API. Поле <code>identifier</code> взаємовиключне з полем <code>url</code>. <small>Доступно з OpenAPI 3.1.0, FastAPI 0.99.0.</small></td></tr><tr><td><code>url</code></td><td><code>str</code></td><td>URL до ліцензії, яка використовується для API. Повинен бути у форматі URL.</td></tr></tbody></table></details> | |
|||
|
|||
Ви можете налаштувати їх наступним чином: |
|||
|
|||
{* ../../docs_src/metadata/tutorial001.py hl[3:16, 19:32] *} |
|||
|
|||
/// tip | Підказка |
|||
|
|||
У полі `description` можна використовувати Markdown, і він буде відображатися у результаті. |
|||
|
|||
/// |
|||
|
|||
З цією конфігурацією автоматична документація API виглядатиме так: |
|||
|
|||
<img src="/img/tutorial/metadata/image01.png"> |
|||
|
|||
## Ідентифікатор ліцензії |
|||
|
|||
З початку використання OpenAPI 3.1.0 та FastAPI 0.99.0 Ви також можете налаштувати `license_info` за допомогою `identifier` замість `url`. |
|||
|
|||
Наприклад: |
|||
|
|||
{* ../../docs_src/metadata/tutorial001_1.py hl[31] *} |
|||
|
|||
## Метадані для тегів |
|||
|
|||
Ви також можете додати додаткові метадані для різних тегів, які використовуються для групування операцій шляхів, за допомогою параметра `openapi_tags`. |
|||
|
|||
Він приймає список, який містить один словник для кожного тега. |
|||
|
|||
Кожен словник може містити: |
|||
|
|||
* `name` (**обов'язково**): `str` з тією ж назвою тегу, яку Ви використовуєте у параметрі `tags` у Ваших *операціях шляху* та `APIRouter`s. |
|||
* `description`: `str` з коротким описом тегу. Може містити Markdown і буде відображено в інтерфейсі документації. |
|||
* `externalDocs`: `dict` який описує зовнішню документацію з такими полями: |
|||
* `description`: `str` з коротким описом зовнішньої документації. |
|||
* `url` (**обов'язково**): `str`з URL-адресою зовнішньої документації. |
|||
|
|||
### Створення метаданих для тегів |
|||
|
|||
Спробуймо це на прикладі з тегами для `users` та `items`. |
|||
|
|||
Створіть метадані для своїх тегів і передайте їх у параметр `openapi_tags`: |
|||
|
|||
{* ../../docs_src/metadata/tutorial004.py hl[3:16,18] *} |
|||
|
|||
Зверніть увагу, що в описах можна використовувати Markdown, наприклад, "login" буде показано жирним шрифтом (**login**), а "fancy" буде показано курсивом (_fancy_). |
|||
|
|||
/// tip | Порада |
|||
|
|||
Не обов'язково додавати метадані для всіх тегів, які Ви використовуєте. |
|||
|
|||
/// |
|||
|
|||
### Використання тегів |
|||
|
|||
Використовуйте параметр `tags` зі своїми *операціями шляху* (і `APIRouter`) для призначення їх до різних тегів: |
|||
|
|||
{* ../../docs_src/metadata/tutorial004.py hl[21,26] *} |
|||
|
|||
/// info | Інформація |
|||
|
|||
Детальніше про теги читайте в розділі [Конфігурація шляхів операцій](path-operation-configuration.md#tags){.internal-link target=_blank}. |
|||
|
|||
/// |
|||
|
|||
### Перевірка документації |
|||
|
|||
Якщо Ви зараз перевірите документацію, вона покаже всі додаткові метадані: |
|||
|
|||
<img src="/img/tutorial/metadata/image02.png"> |
|||
|
|||
### Порядок тегів |
|||
|
|||
Порядок кожного словника метаданих тегу також визначає порядок відображення в інтерфейсі документації. |
|||
|
|||
Наприклад, хоча `users` мав би йти після `items` в алфавітному порядку, він відображається перед ними, оскільки ми додали його метадані як перший словник у списку. |
|||
|
|||
## URL для OpenAPI |
|||
|
|||
За замовчуванням схема OpenAPI надається за адресою `/openapi.json`. |
|||
|
|||
Але Ви можете налаштувати це за допомогою параметра `openapi_url`. |
|||
|
|||
Наприклад, щоб налаштувати його на `/api/v1/openapi.json`: |
|||
|
|||
{* ../../docs_src/metadata/tutorial002.py hl[3] *} |
|||
|
|||
Якщо Ви хочете повністю вимкнути схему OpenAPI, Ви можете встановити `openapi_url=None`, це також вимкне інтерфейси документації, які її використовують. |
|||
|
|||
## URL-адреси документації |
|||
|
|||
Ви можете налаштувати два інтерфейси користувача для документації, які включені: |
|||
|
|||
* **Swagger UI**: доступний за адресою `/docs`. |
|||
* Ви можете змінити його URL за допомогою параметра `docs_url`. |
|||
* Ви можете вимкнути його, встановивши `docs_url=None`. |
|||
* **ReDoc**: доступний за адресою `/redoc`. |
|||
* Ви можете змінити його URL за допомогою параметра `redoc_url`. |
|||
* Ви можете вимкнути його, встановивши `redoc_url=None`. |
|||
|
|||
Наприклад, щоб налаштувати Swagger UI на `/documentation` і вимкнути ReDoc: |
|||
|
|||
{* ../../docs_src/metadata/tutorial003.py hl[3] *} |
@ -0,0 +1,261 @@ |
|||
# Path Параметри |
|||
|
|||
Ви можете визначити "параметри" або "змінні" шляху, використовуючи синтаксис форматованих рядків: |
|||
|
|||
{* ../../docs_src/path_params/tutorial001.py hl[6:7] *} |
|||
|
|||
Значення параметра шляху `item_id` передається у функцію як аргумент `item_id`. |
|||
|
|||
Якщо запустити цей приклад та перейти за посиланням <a href="http://127.0.0.1:8000/items/foo" class="external-link" target="_blank">http://127.0.0.1:8000/items/foo</a>, то отримаємо таку відповідь: |
|||
|
|||
```JSON |
|||
{"item_id":"foo"} |
|||
``` |
|||
|
|||
## Path параметри з типами |
|||
|
|||
Ви можете визначити тип параметра шляху у функції, використовуючи стандартні анотації типів Python: |
|||
|
|||
{* ../../docs_src/path_params/tutorial002.py hl[7] *} |
|||
|
|||
У такому випадку `item_id` визначається як `int`. |
|||
|
|||
/// check | Примітка |
|||
|
|||
Це дасть можливість підтримки редактора всередині функції з перевірками помилок, автодоповнення тощо. |
|||
|
|||
/// |
|||
|
|||
## <abbr title="або: серіалізація, парсинг, маршалізація">Перетворення</abbr> даних |
|||
|
|||
Якщо запустити цей приклад і перейти за посиланням <a href="http://127.0.0.1:8000/items/3" class="external-link" target="_blank">http://127.0.0.1:8000/items/3</a>, то отримаєте таку відповідь: |
|||
|
|||
```JSON |
|||
{"item_id":3} |
|||
``` |
|||
|
|||
/// check | Примітка |
|||
|
|||
Зверніть увагу, що значення, яке отримала (і повернула) ваша функція, — це `3`. Це Python `int`, а не рядок `"3"`. |
|||
|
|||
Отже, з таким оголошенням типу **FastAPI** автоматично виконує <abbr title="перетворення рядка, що надходить із HTTP-запиту, у типи даних Python">"парсинг"</abbr> запитів. |
|||
|
|||
/// |
|||
|
|||
## <abbr title="Або валідація">Перевірка</abbr> даних |
|||
|
|||
Якщо ж відкрити у браузері посилання <a href="http://127.0.0.1:8000/items/foo" class="external-link" target="_blank">http://127.0.0.1:8000/items/foo</a>, то побачимо цікаву HTTP-помилку: |
|||
|
|||
```JSON |
|||
{ |
|||
"detail": [ |
|||
{ |
|||
"type": "int_parsing", |
|||
"loc": [ |
|||
"path", |
|||
"item_id" |
|||
], |
|||
"msg": "Input should be a valid integer, unable to parse string as an integer", |
|||
"input": "foo", |
|||
"url": "https://errors.pydantic.dev/2.1/v/int_parsing" |
|||
} |
|||
] |
|||
} |
|||
``` |
|||
тому що параметр шляху має значення `"foo"`, яке не є типом `int`. |
|||
|
|||
Таку саму помилку отримаємо, якщо передати `float` замість `int`, як бачимо, у цьому прикладі: <a href="http://127.0.0.1:8000/items/4.2" class="external-link" target="_blank">http://127.0.0.1:8000/items/4.2</a> |
|||
|
|||
/// check | Примітка |
|||
|
|||
Отже, **FastAPI** надає перевірку типів з таким самим оголошенням типу в Python. |
|||
|
|||
Зверніть увагу, що помилка також чітко вказує саме на те місце, де валідація не пройшла. |
|||
|
|||
Це неймовірно корисно під час розробки та дебагінгу коду, що взаємодіє з вашим API. |
|||
|
|||
/// |
|||
|
|||
## Документація |
|||
|
|||
Тепер коли відкриєте свій браузер за посиланням <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>, то побачите автоматично згенеровану, інтерактивну API-документацію: |
|||
|
|||
<img src="/img/tutorial/path-params/image01.png"> |
|||
|
|||
/// check | Примітка |
|||
|
|||
Знову ж таки, лише з цим самим оголошенням типу в Python, FastAPI надає вам автоматичну, інтерактивну документацію (з інтеграцією Swagger UI). |
|||
|
|||
Зверніть увагу, що параметр шляху оголошений як ціле число. |
|||
|
|||
|
|||
/// |
|||
|
|||
## Переваги стандартизації, альтернативна документація |
|||
|
|||
І оскільки згенерована схема відповідає стандарту <a href="https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md" class="external-link" target="_blank">OpenAPI</a>, існує багато сумісних інструментів. |
|||
|
|||
З цієї причини FastAPI також надає альтернативну документацію API (використовуючи ReDoc), до якої можна отримати доступ за посиланням <a href="http://127.0.0.1:8000/redoc" class="external-link" target="_blank">http://127.0.0.1:8000/redoc</a>: |
|||
|
|||
<img src="/img/tutorial/path-params/image02.png"> |
|||
|
|||
Таким чином, існує багато сумісних інструментів, включаючи інструменти для генерації коду для багатьох мов. |
|||
|
|||
|
|||
## Pydantic |
|||
|
|||
Вся валідація даних виконується за лаштунками за допомогою <a href="https://docs.pydantic.dev/" class="external-link" target="_blank">Pydantic</a>, тому Ви отримуєте всі переваги від його використання. І можете бути впевнені, що все в надійних руках. |
|||
|
|||
Ви можете використовувати ті самі оголошення типів з `str`, `float`, `bool` та багатьма іншими складними типами даних. |
|||
|
|||
Декілька з них будуть розглянуті в наступних розділах посібника. |
|||
|
|||
## Порядок має значення |
|||
|
|||
При створенні *операцій шляху* можуть виникати ситуації, коли шлях фіксований. |
|||
|
|||
Наприклад, `/users/me`. Припустимо, що це шлях для отримання даних про поточного користувача. |
|||
|
|||
А також у вас може бути шлях `/users/{user_id}`, щоб отримати дані про конкретного користувача за його ID. |
|||
|
|||
Оскільки *операції шляху* оцінюються по черзі, Ви повинні переконатися, що шлях для `/users/me` оголошений перед шляхом для `/users/{user_id}`: |
|||
|
|||
{* ../../docs_src/path_params/tutorial003.py hl[6,11] *} |
|||
|
|||
Інакше шлях для `/users/{user_id}` також буде відповідати для `/users/me`, "вважаючи", що він отримує параметр `user_id` зі значенням `"me"`. |
|||
|
|||
Аналогічно, Ви не можете оголосити операцію шляху: |
|||
|
|||
{* ../../docs_src/path_params/tutorial003b.py hl[6,11] *} |
|||
|
|||
Перша операція буде завжди використовуватися, оскільки шлях збігається першим. |
|||
## Попередньо визначені значення |
|||
|
|||
Якщо у вас є *операція шляху*, яка приймає *параметр шляху*, але Ви хочете, щоб можливі допустимі значення *параметра шляху* були попередньо визначені, Ви можете використати стандартний Python <abbr title="перелічення">Enum</abbr>. |
|||
|
|||
### Створення класу `Enum` |
|||
|
|||
Імпортуйте `Enum` і створіть підклас, що наслідується від `str` та `Enum`. |
|||
|
|||
Наслідуючи від `str`, документація API зможе визначити, що значення повинні бути типу `string`, і правильно їх відобразить. |
|||
|
|||
Після цього створіть атрибути класу з фіксованими значеннями, які будуть доступними допустимими значеннями: |
|||
|
|||
{* ../../docs_src/path_params/tutorial005.py hl[1,6:9] *} |
|||
|
|||
/// info | Додаткова інформація |
|||
|
|||
<a href="https://docs.python.org/3/library/enum.html" class="external-link" target="_blank">Перелічення (або enums) доступні в Python</a> починаючи з версії 3.4. |
|||
|
|||
/// |
|||
|
|||
/// tip | Порада |
|||
|
|||
Якщо вам цікаво, "AlexNet", "ResNet" та "LeNet" — це просто назви ML моделей <abbr title="Технічно, архітектури Deep Learning моделей">Machine Learning</abbr>. |
|||
|
|||
/// |
|||
|
|||
|
|||
### Оголосіть *параметр шляху* |
|||
|
|||
Потім створіть *параметр шляху* з анотацією типу, використовуючи створений вами клас enum (`ModelName`): |
|||
|
|||
{* ../../docs_src/path_params/tutorial005.py hl[16] *} |
|||
|
|||
### Перевірка документації |
|||
|
|||
Оскільки доступні значення для *параметра шляху* визначені заздалегідь, інтерактивна документація зможе красиво їх відобразити: |
|||
|
|||
<img src="/img/tutorial/path-params/image03.png"> |
|||
|
|||
### Робота з *перелічуваннями* у Python |
|||
|
|||
Значення *параметра шляху* буде елементом *перелічування*. |
|||
|
|||
#### Порівняння *елементів перелічування* |
|||
|
|||
Ви можете порівнювати його з *елементами перелічування* у створеному вами enum `ModelName`: |
|||
|
|||
{* ../../docs_src/path_params/tutorial005.py hl[17] *} |
|||
|
|||
#### Отримання *значення перелічування* |
|||
|
|||
Ви можете отримати фактичне значення (у цьому випадку це `str`), використовуючи `model_name.value`, або загалом `your_enum_member.value`: |
|||
|
|||
{* ../../docs_src/path_params/tutorial005.py hl[20] *} |
|||
|
|||
/// tip | Порада |
|||
|
|||
Ви також можете отримати доступ до значення `"lenet"`, використовуючи `ModelName.lenet.value`. |
|||
|
|||
/// |
|||
|
|||
|
|||
#### Повернення *елементів перелічування* |
|||
|
|||
Ви можете повертати *елементи перелічування* з вашої *операції шляху*, навіть вкладені у JSON-тіло (наприклад, `dict`). |
|||
|
|||
Вони будуть перетворені на відповідні значення (у цьому випадку рядки) перед поверненням клієнту: |
|||
|
|||
{* ../../docs_src/path_params/tutorial005.py hl[18,21,23] *} |
|||
|
|||
На стороні клієнта Ви отримаєте відповідь у форматі JSON, наприклад: |
|||
|
|||
```JSON |
|||
{ |
|||
"model_name": "alexnet", |
|||
"message": "Deep Learning FTW!" |
|||
} |
|||
``` |
|||
|
|||
## Path-параметри, що містять шляхи |
|||
|
|||
Припустимо, у вас є *операція шляху* з маршрутом `/files/{file_path}`. |
|||
|
|||
Але вам потрібно, щоб `file_path` містив *шлях*, наприклад `home/johndoe/myfile.txt`. |
|||
|
|||
Отже, URL для цього файлу виглядатиме так: `/files/home/johndoe/myfile.txt`. |
|||
|
|||
|
|||
|
|||
### Підтримка OpenAPI |
|||
|
|||
OpenAPI не підтримує спосіб оголошення *параметра шляху*, що містить *шлях* всередині, оскільки це може призвести до сценаріїв, які складно тестувати та визначати. |
|||
|
|||
Однак (одначе), Ви все одно можете зробити це в **FastAPI**, використовуючи один із внутрішніх інструментів Starlette. |
|||
|
|||
Документація все ще працюватиме, хоча й не додаватиме опису про те, що параметр повинен містити шлях. |
|||
|
|||
### Конвертер шляху |
|||
|
|||
Використовуючи опцію безпосередньо зі Starlette, Ви можете оголосити *параметр шляху*, що містить *шлях*, використовуючи URL на кшталт: |
|||
|
|||
``` |
|||
/files/{file_path:path} |
|||
``` |
|||
У цьому випадку ім'я параметра — `file_path`, а остання частина `:path` вказує на те, що параметр повинен відповідати будь-якому *шляху*. |
|||
|
|||
Отже, Ви можете використати його так: |
|||
|
|||
{* ../../docs_src/path_params/tutorial004.py hl[6] *} |
|||
|
|||
/// tip | Порада |
|||
|
|||
Вам може знадобитися, щоб параметр містив `/home/johndoe/myfile.txt` із початковою косою рискою (`/`). |
|||
|
|||
У такому випадку URL виглядатиме так: `/files//home/johndoe/myfile.txt`, із подвійною косою рискою (`//`) між `files` і `home`. |
|||
|
|||
/// |
|||
|
|||
## Підсумок |
|||
|
|||
З **FastAPI**, використовуючи короткі, інтуїтивно зрозумілі та стандартні оголошення типів Python, Ви отримуєте: |
|||
|
|||
* Підтримку в редакторі: перевірка помилок, автодоповнення тощо. |
|||
* "<abbr title="перетворення рядка, що надходить з HTTP-запиту, у типи даних Python">Парсинг</abbr>" даних |
|||
* Валідацію даних |
|||
* Анотацію API та автоматичну документацію |
|||
|
|||
І вам потрібно оголосити їх лише один раз. |
|||
|
|||
Це, ймовірно, основна видима перевага **FastAPI** порівняно з альтернативними фреймворками (окрім високої продуктивності). |
@ -0,0 +1,192 @@ |
|||
# Query Параметри |
|||
|
|||
Коли Ви оголошуєте інші параметри функції, які не є частиною параметрів шляху, вони автоматично інтерпретуються як "query" параметри. |
|||
|
|||
{* ../../docs_src/query_params/tutorial001.py hl[9] *} |
|||
|
|||
Query параметри — це набір пар ключ-значення, що йдуть після символу `?` в URL, розділені символами `&`. |
|||
|
|||
Наприклад, в URL: |
|||
|
|||
``` |
|||
http://127.0.0.1:8000/items/?skip=0&limit=10 |
|||
``` |
|||
|
|||
...query параметрами є: |
|||
|
|||
* `skip`: зі значенням `0` |
|||
* `limit`: зі значенням `10` |
|||
|
|||
Оскільки вони є частиною URL, вони "за замовчуванням" є рядками. |
|||
|
|||
Але коли Ви оголошуєте їх із типами Python (у наведеному прикладі як `int`), вони перетворюються на цей тип і проходять перевірку відповідності. |
|||
|
|||
Увесь той самий процес, який застосовується до параметрів шляху, також застосовується до query параметрів: |
|||
|
|||
* Підтримка в редакторі (автодоповнення, перевірка помилок) |
|||
* <abbr title="перетворення рядка, що надходить з HTTP-запиту, у типи даних Python">"Парсинг"</abbr> даних |
|||
* Валідація даних |
|||
* Автоматична документація |
|||
|
|||
|
|||
## Значення за замовчуванням |
|||
|
|||
Оскільки query параметри не є фіксованою частиною шляху, вони можуть бути необов’язковими та мати значення за замовчуванням. |
|||
|
|||
У наведеному вище прикладі вони мають значення за замовчуванням: `skip=0` і `limit=10`. |
|||
|
|||
Отже, результат переходу за URL: |
|||
|
|||
``` |
|||
http://127.0.0.1:8000/items/ |
|||
``` |
|||
буде таким самим, як і перехід за посиланням: |
|||
|
|||
``` |
|||
http://127.0.0.1:8000/items/?skip=0&limit=10 |
|||
``` |
|||
|
|||
Але якщо Ви перейдете, наприклад, за посиланням: |
|||
|
|||
``` |
|||
http://127.0.0.1:8000/items/?skip=20 |
|||
``` |
|||
|
|||
Значення параметрів у вашій функції будуть такими: |
|||
|
|||
* `skip=20`: оскільки Ви вказали його в URL |
|||
* `limit=10`: оскільки це значення за замовчуванням |
|||
|
|||
## Необов'язкові параметри |
|||
|
|||
Аналогічно, Ви можете оголосити необов’язкові query параметри, встановивши для них значення за замовчуванням `None`: |
|||
|
|||
{* ../../docs_src/query_params/tutorial002_py310.py hl[7] *} |
|||
|
|||
У цьому випадку параметр функції `q` буде необов’язковим і за замовчуванням матиме значення `None`. |
|||
|
|||
/// check | Примітка |
|||
|
|||
Також зверніть увагу, що **FastAPI** достатньо розумний, щоб визначити, що параметр шляху `item_id` є параметром шляху, а `q` — ні, отже, це query параметр. |
|||
|
|||
/// |
|||
|
|||
## Перетворення типу Query параметра |
|||
|
|||
Ви також можете оголошувати параметри типу `bool`, і вони будуть автоматично конвертовані: |
|||
|
|||
{* ../../docs_src/query_params/tutorial003_py310.py hl[7] *} |
|||
|
|||
У цьому випадку, якщо Ви звернетесь до: |
|||
|
|||
|
|||
``` |
|||
http://127.0.0.1:8000/items/foo?short=1 |
|||
``` |
|||
|
|||
або |
|||
|
|||
``` |
|||
http://127.0.0.1:8000/items/foo?short=True |
|||
``` |
|||
|
|||
або |
|||
|
|||
``` |
|||
http://127.0.0.1:8000/items/foo?short=true |
|||
``` |
|||
|
|||
або |
|||
|
|||
``` |
|||
http://127.0.0.1:8000/items/foo?short=on |
|||
``` |
|||
|
|||
або |
|||
|
|||
``` |
|||
http://127.0.0.1:8000/items/foo?short=yes |
|||
``` |
|||
|
|||
або будь-який інший варіант написання (великі літери, перша літера велика тощо), ваша функція побачить параметр `short` зі значенням `True` з типом даних `bool`. В іншому випадку – `False`. |
|||
|
|||
## Кілька path і query параметрів |
|||
|
|||
Ви можете одночасно оголошувати кілька path і query параметрів, і **FastAPI** автоматично визначить, який з них до чого належить. |
|||
|
|||
|
|||
Не потрібно дотримуватись певного порядку їх оголошення. |
|||
|
|||
Вони визначаються за назвою: |
|||
|
|||
{* ../../docs_src/query_params/tutorial004_py310.py hl[6,8] *} |
|||
|
|||
## Обов’язкові Query параметри |
|||
|
|||
Якщо Ви оголошуєте значення за замовчуванням для параметрів, які не є path-параметрами (у цьому розділі ми бачили поки що лише path параметри), тоді вони стають необов’язковими. |
|||
|
|||
Якщо Ви не хочете вказувати конкретні значення, але хочете зробити параметр опціональним, задайте `None` як значення за замовчуванням. |
|||
|
|||
Але якщо Ви хочете зробити query параметр обов’язковим, просто не вказуйте для нього значення за замовчуванням: |
|||
|
|||
{* ../../docs_src/query_params/tutorial005.py hl[6:7] *} |
|||
|
|||
Тут `needy` – обов’язковий query параметр типу `str`. |
|||
|
|||
Якщо Ви відкриєте у браузері URL-адресу: |
|||
|
|||
``` |
|||
http://127.0.0.1:8000/items/foo-item |
|||
``` |
|||
|
|||
...без додавання обов’язкового параметра `needy`, Ви побачите помилку: |
|||
|
|||
```JSON |
|||
{ |
|||
"detail": [ |
|||
{ |
|||
"type": "missing", |
|||
"loc": [ |
|||
"query", |
|||
"needy" |
|||
], |
|||
"msg": "Field required", |
|||
"input": null, |
|||
"url": "https://errors.pydantic.dev/2.1/v/missing" |
|||
} |
|||
] |
|||
} |
|||
``` |
|||
|
|||
Оскільки `needy` є обов’язковим параметром, вам потрібно вказати його в URL: |
|||
|
|||
``` |
|||
http://127.0.0.1:8000/items/foo-item?needy=sooooneedy |
|||
``` |
|||
|
|||
...цей запит поверне: |
|||
|
|||
```JSON |
|||
{ |
|||
"item_id": "foo-item", |
|||
"needy": "sooooneedy" |
|||
} |
|||
``` |
|||
|
|||
|
|||
Звичайно, Ви можете визначити деякі параметри як обов’язкові, інші зі значенням за замовчуванням, а ще деякі — повністю опціональні: |
|||
|
|||
{* ../../docs_src/query_params/tutorial006_py310.py hl[8] *} |
|||
|
|||
У цьому випадку є 3 query параметри: |
|||
|
|||
* `needy`, обов’язковий `str`. |
|||
* `skip`, `int` зі значенням за замовчуванням `0`. |
|||
* `limit`, опціональний `int`. |
|||
|
|||
|
|||
/// tip | Підказка |
|||
|
|||
Ви також можете використовувати `Enum`-и, так само як і з [Path Parameters](path-params.md#predefined-values){.internal-link target=_blank}. |
|||
|
|||
/// |
@ -0,0 +1,175 @@ |
|||
# Запит файлів |
|||
|
|||
Ви можете визначити файли, які будуть завантажуватися клієнтом, використовуючи `File`. |
|||
|
|||
/// 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 |
|||
``` |
|||
|
|||
Це необхідно, оскільки завантажені файли передаються у вигляді "форматованих даних форми". |
|||
|
|||
/// |
|||
|
|||
## Імпорт `File` |
|||
|
|||
Імпортуйте `File` та `UploadFile` з `fastapi`: |
|||
|
|||
{* ../../docs_src/request_files/tutorial001_an_py39.py hl[3] *} |
|||
|
|||
## Визначення параметрів `File` |
|||
|
|||
Створіть параметри файлів так само як Ви б створювали `Body` або `Form`: |
|||
|
|||
{* ../../docs_src/request_files/tutorial001_an_py39.py hl[9] *} |
|||
|
|||
/// info | Інформація |
|||
|
|||
`File` — це клас, який безпосередньо успадковує `Form`. |
|||
|
|||
Але пам’ятайте, що коли Ви імпортуєте `Query`, `Path`, `File` та інші з `fastapi`, це насправді функції, які повертають спеціальні класи. |
|||
|
|||
/// |
|||
|
|||
/// tip | Підказка |
|||
|
|||
Щоб оголосити тіла файлів, Вам потрібно використовувати `File`, тому що інакше параметри будуть інтерпретовані як параметри запиту або параметри тіла (JSON). |
|||
|
|||
/// |
|||
|
|||
Файли будуть завантажені у вигляді "форматованих даних форми". |
|||
|
|||
Якщо Ви оголосите тип параметра функції обробника маршруту як `bytes`, **FastAPI** прочитає файл за Вас, і Ви отримаєте його вміст у вигляді `bytes`. |
|||
|
|||
Однак майте на увазі, що весь вміст буде збережено в пам'яті. Це працюватиме добре для малих файлів. |
|||
|
|||
Але в деяких випадках Вам може знадобитися `UploadFile`. |
|||
|
|||
## Параметри файлу з `UploadFile` |
|||
|
|||
Визначте параметр файлу з типом `UploadFile`: |
|||
|
|||
{* ../../docs_src/request_files/tutorial001_an_py39.py hl[14] *} |
|||
|
|||
Використання `UploadFile` має кілька переваг перед `bytes`: |
|||
|
|||
* Вам не потрібно використовувати `File()` у значенні за замовчуванням параметра. |
|||
* Використовується "буферизований" файл: |
|||
* Файл зберігається в пам'яті до досягнення певного обмеження, після чого він записується на диск. |
|||
* Це означає, що він добре працює для великих файлів, таких як зображення, відео, великі двійкові файли тощо, не споживаючи всю пам'ять. |
|||
Ви можете отримати метадані про завантажений файл. |
|||
* Він має <a href="https://docs.python.org/3/glossary.html#term-file-like-object" class="external-link" target="_blank">file-like</a> `асинхронний файловий інтерфейс` interface. |
|||
* Він надає фактичний об'єкт Python <a href="https://docs.python.org/3/library/tempfile.html#tempfile.SpooledTemporaryFile" class="external-link" target="_blank">`SpooledTemporaryFile`</a>, який можна передавати безпосередньо іншим бібліотекам. |
|||
|
|||
### `UploadFile` |
|||
|
|||
`UploadFile` має такі атрибути: |
|||
|
|||
* `filename`: Рядок `str` з оригінальною назвою файлу, який був завантажений (наприклад, `myimage.jpg`). |
|||
* `content_type`: Рядок `str` з MIME-типом (наприклад, `image/jpeg`). |
|||
* `file`: Об'єкт <a href="https://docs.python.org/3/library/tempfile.html#tempfile.SpooledTemporaryFile" class="external-link" target="_blank">SpooledTemporaryFile</a> (<a href="https://docs.python.org/3/glossary.html#term-file-like-object" class="external-link" target="_blank">файлоподібний</a> об'єкт). Це фактичний файловий об'єкт Python, який можна безпосередньо передавати іншим функціям або бібліотекам, що очікують "файлоподібний" об'єкт. |
|||
|
|||
`UploadFile` має такі асинхронні `async` методи. Вони викликають відповідні методи файлу під капотом (використовуючи внутрішній `SpooledTemporaryFile`). |
|||
|
|||
* `write(data)`: Записує `data` (`str` або `bytes`) у файл. |
|||
* `read(size)`: Читає `size` (`int`) байтів/символів з файлу. |
|||
* `seek(offset)`: Переміщується до позиції `offset` (`int`) у файлі. |
|||
* Наприклад, `await myfile.seek(0)` поверне курсор на початок файлу. |
|||
* This is especially useful if you run `await myfile.read()` once and then need to read the contents again. Це особливо корисно, якщо Ви виконуєте await `await myfile.read()` один раз, а потім потрібно знову прочитати вміст. |
|||
* `close()`: Закриває файл. |
|||
|
|||
Оскільки всі ці методи є асинхронними `async`, Вам потрібно використовувати "await": |
|||
|
|||
Наприклад, всередині `async` *функції обробки шляху* Ви можете отримати вміст за допомогою: |
|||
|
|||
```Python |
|||
contents = await myfile.read() |
|||
``` |
|||
Якщо Ви знаходитесь у звичайній `def` *функції обробки шляху*, Ви можете отримати доступ до `UploadFile.file` безпосередньо, наприклад: |
|||
|
|||
```Python |
|||
contents = myfile.file.read() |
|||
``` |
|||
|
|||
/// note | Технічні деталі `async` |
|||
|
|||
Коли Ви використовуєте `async` методи, **FastAPI** виконує файлові операції у пулі потоків та очікує їх завершення. |
|||
|
|||
/// |
|||
|
|||
/// note | Технічні деталі Starlette |
|||
|
|||
`UploadFile` у **FastAPI** успадковується безпосередньо від `UploadFile` у **Starlette**, але додає деякі необхідні частини, щоб зробити його сумісним із **Pydantic** та іншими компонентами FastAPI. |
|||
|
|||
/// |
|||
|
|||
## Що таке "Form Data" |
|||
|
|||
Спосіб, у який HTML-форми (`<form></form>`) надсилають дані на сервер, зазвичай використовує "спеціальне" кодування, відмінне від JSON. |
|||
|
|||
**FastAPI** забезпечує правильне зчитування цих даних з відповідної частини запиту, а не з JSON. |
|||
|
|||
/// note | Технічні деталі |
|||
|
|||
Дані з форм зазвичай кодуються за допомогою "media type" `application/x-www-form-urlencoded`, якщо вони не містять файлів. |
|||
|
|||
Але якщо форма містить файли, вона кодується у форматі `multipart/form-data`. Якщо Ви використовуєте `File`, **FastAPI** визначить, що потрібно отримати файли з відповідної частини тіла запиту. |
|||
|
|||
Щоб дізнатися більше про ці типи кодування та формові поля, ознайомтеся з <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST" class="external-link" target="_blank"><abbr title="Mozilla Developer Network">документацією MDN</abbr> щодо <code>POST</code></a>. |
|||
|
|||
/// |
|||
|
|||
/// warning | Увага |
|||
|
|||
Ви можете оголосити кілька параметрів `File` і `Form` в *операції шляху*, але Ви не можете одночасно оголошувати поля `Body`, які мають надходити у форматі JSON, оскільки тіло запиту буде закодоване у форматі `multipart/form-data`, а не `application/json`. |
|||
|
|||
Це не обмеження **FastAPI**, а особливість протоколу HTTP. |
|||
|
|||
/// |
|||
|
|||
## Опціональне Завантаження Файлів |
|||
|
|||
Файл можна зробити необов’язковим, використовуючи стандартні анотації типів і встановлюючи значення за замовчуванням `None`: |
|||
|
|||
{* ../../docs_src/request_files/tutorial001_02_an_py310.py hl[9,17] *} |
|||
|
|||
## `UploadFile` із Додатковими Мета Даними |
|||
|
|||
Ви також можете використовувати `File()` разом із `UploadFile`, наприклад, для встановлення додаткових метаданих: |
|||
|
|||
{* ../../docs_src/request_files/tutorial001_03_an_py39.py hl[9,15] *} |
|||
|
|||
## Завантаження Кількох Файлів |
|||
|
|||
Можна завантажувати кілька файлів одночасно. |
|||
|
|||
Вони будуть пов’язані з одним і тим самим "form field", який передається у вигляді "form data". |
|||
|
|||
Щоб це реалізувати, потрібно оголосити список `bytes` або `UploadFile`: |
|||
|
|||
{* ../../docs_src/request_files/tutorial002_an_py39.py hl[10,15] *} |
|||
|
|||
Ви отримаєте, як і було оголошено, `list` із `bytes` або `UploadFile`. |
|||
|
|||
/// note | Технічні деталі |
|||
|
|||
Ви також можете використати `from starlette.responses import HTMLResponse`. |
|||
|
|||
**FastAPI** надає ті ж самі `starlette.responses`, що й `fastapi.responses`, для зручності розробників. Однак більшість доступних відповідей надходять безпосередньо від Starlette. |
|||
|
|||
/// |
|||
|
|||
### Завантаження декількох файлів із додатковими метаданими |
|||
|
|||
Так само як і раніше, Ви можете використовувати `File()`, щоб встановити додаткові параметри навіть для `UploadFile`: |
|||
|
|||
{* ../../docs_src/request_files/tutorial003_an_py39.py hl[11,18:20] *} |
|||
|
|||
## Підсумок |
|||
|
|||
Використовуйте `File`, `bytes`та `UploadFile`, щоб оголошувати файли для завантаження у запитах, які надсилаються у вигляді form data. |
@ -0,0 +1,78 @@ |
|||
# Моделі форм (Form Models) |
|||
|
|||
У FastAPI Ви можете використовувати **Pydantic-моделі** для оголошення **полів форми**. |
|||
|
|||
/// 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,41 @@ |
|||
# Запити з формами та файлами |
|||
|
|||
У FastAPI Ви можете одночасно отримувати файли та поля форми, використовуючи `File` і `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 |
|||
``` |
|||
|
|||
/// |
|||
|
|||
## Імпорт `File` та `Form` |
|||
|
|||
{* ../../docs_src/request_forms_and_files/tutorial001_an_py39.py hl[3] *} |
|||
|
|||
## Оголошення параметрів `File` та `Form` |
|||
|
|||
Створіть параметри файлів та форми так само як і для `Body` або `Query`: |
|||
|
|||
{* ../../docs_src/request_forms_and_files/tutorial001_an_py39.py hl[10:12] *} |
|||
|
|||
Файли та поля форми будуть завантажені як формові дані, і Ви отримаєте як файли, так і введені користувачем поля. |
|||
|
|||
Ви також можете оголосити деякі файли як `bytes`, а деякі як `UploadFile`. |
|||
|
|||
/// warning | Увага |
|||
|
|||
Ви можете оголосити кілька параметрів `File` і `Form` в операції *шляху*, але не можете одночасно оголошувати `Body`-поля, які очікуєте отримати у форматі JSON, оскільки запит матиме тіло, закодоване за допомогою `multipart/form-data`, а не `application/json`. |
|||
|
|||
Це не обмеження **FastAPI**, а частина протоколу HTTP. |
|||
|
|||
/// |
|||
|
|||
## Підсумок |
|||
|
|||
Використовуйте `File` та `Form` разом, коли вам потрібно отримувати дані форми та файли в одному запиті. |
@ -0,0 +1,73 @@ |
|||
# Дані форми |
|||
|
|||
Якщо Вам потрібно отримувати поля форми замість 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` |
|||
|
|||
Імпортуйте `Form` з `fastapi`: |
|||
|
|||
{* ../../docs_src/request_forms/tutorial001_an_py39.py hl[3] *} |
|||
|
|||
## Оголошення параметрів `Form` |
|||
|
|||
Створюйте параметри форми так само як Ви б створювали `Body` або `Query`: |
|||
|
|||
{* ../../docs_src/request_forms/tutorial001_an_py39.py hl[9] *} |
|||
|
|||
Наприклад, один зі способів використання специфікації OAuth2 (так званий "password flow") вимагає надсилати `username` та `password` як поля форми. |
|||
|
|||
<abbr title="Специфікація">spec</abbr> вимагає, щоб ці поля мали точні назви `username` і `password` та надсилалися у вигляді полів форми, а не JSON. |
|||
|
|||
З `Form` Ви можете оголошувати ті ж конфігурації, що і з `Body` (та `Query`, `Path`, `Cookie`), включаючи валідацію, приклади, псевдоніми (наприклад, `user-name` замість `username`) тощо. |
|||
|
|||
/// info | Інформація |
|||
|
|||
`Form` — це клас, який безпосередньо наслідується від `Body`. |
|||
|
|||
/// |
|||
|
|||
/// tip | Порада |
|||
|
|||
Щоб оголосити тіло форми, потрібно явно використовувати `Form`, оскільки без нього параметри будуть інтерпретуватися як параметри запиту або тіла (JSON). |
|||
|
|||
/// |
|||
|
|||
## Про "поля форми" |
|||
|
|||
HTML-форми (`<form></form>`) надсилають дані на сервер у "спеціальному" кодуванні, яке відрізняється від JSON. |
|||
|
|||
**FastAPI** подбає про те, щоб зчитати ці дані з правильного місця, а не з JSON. |
|||
|
|||
/// note | Технічні деталі |
|||
|
|||
Дані з форм зазвичай кодуються за допомогою "типу медіа" `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"><abbr title="Mozilla Developer Network">MDN</abbr> вебдокументації для <code>POST</code></a>. |
|||
|
|||
/// |
|||
|
|||
/// warning | Попередження |
|||
|
|||
Ви можете оголосити кілька параметрів `Form` в *операції шляху*, але не можете одночасно оголосити поля `Body`, які Ви очікуєте отримати у форматі JSON, оскільки тіло запиту буде закодовано у форматі `application/x-www-form-urlencoded`, а не `application/json`. |
|||
|
|||
Це не обмеження **FastAPI**, а частина HTTP-протоколу. |
|||
|
|||
/// |
|||
|
|||
## Підсумок |
|||
|
|||
Використовуйте `Form` для оголошення вхідних параметрів у вигляді даних форми. |
@ -0,0 +1,100 @@ |
|||
# Статус коди Відповідей |
|||
|
|||
Так само як Ви можете вказати модель відповіді, Ви також можете оголосити HTTP код статусу для відповіді за допомогою параметра `status_code` в будь-якій з *операцій шляху*: |
|||
|
|||
* `@app.get()` |
|||
* `@app.post()` |
|||
* `@app.put()` |
|||
* `@app.delete()` |
|||
* тощо. |
|||
|
|||
{* ../../docs_src/response_status_code/tutorial001.py hl[6] *} |
|||
|
|||
/// note | Нотатка |
|||
|
|||
Зверніть увагу, що `status_code` є параметром методу "декоратора" (`get`, `post` і т.д.), а не Вашої *функції операції шляху*, як усі інші параметри та тіло запиту. |
|||
|
|||
/// |
|||
|
|||
Параметр `status_code` приймає число, яке відповідає HTTP коду статусу. |
|||
|
|||
/// info | Інформація |
|||
`status_code` також може отримувати значення з `IntEnum`, наприклад, з Python <a href="https://docs.python.org/3/library/http.html#http.HTTPStatus" class="external-link" target="_blank">`http.HTTPStatus`</a>. |
|||
|
|||
/// |
|||
|
|||
Він буде: |
|||
|
|||
* Повертати вказаний код статусу у відповіді. |
|||
* Документувати його як такий у схемі OpenAPI (і, таким чином, в інтерфейсі користувача): |
|||
|
|||
<img src="/img/tutorial/response-status-code/image01.png"> |
|||
|
|||
/// note | Нотатка |
|||
|
|||
Деякі коди відповіді (див. наступний розділ) вказують, що відповідь не має тіла. |
|||
|
|||
FastAPI знає про це і створить OpenAPI документацію, яка вказує, що тіла відповіді немає. |
|||
|
|||
/// |
|||
|
|||
## Про HTTP статус коди |
|||
|
|||
/// note | Нотатка |
|||
|
|||
Якщо Ви вже знаєте, що таке HTTP коди статусу, переходьте до наступного розділу. |
|||
|
|||
/// |
|||
|
|||
В HTTP Ви надсилаєте числовий код статусу з 3 цифр як частину відповіді. |
|||
|
|||
Ці коди статусу мають пов’язану назву для їх розпізнавання, але найважливішою частиною є саме число. |
|||
|
|||
Коротко: |
|||
|
|||
* **`100 - 199`** "Інформаційні" відповіді. Ви рідко використовуєте їх напряму. Відповіді з такими кодами не можуть мати тіла. |
|||
* **`200 - 299`** "Успішні" відповіді. Це ті, які Ви використовуватимете найчастіше. |
|||
* `200` - код за замовчуванням, який означає, що все пройшло "OK". |
|||
* Інший приклад – `201`, "Created" (створено). Його зазвичай використовують після створення нового запису в базі даних. |
|||
* Особливий випадок – `204`, "No Content" (немає вмісту). Ця відповідь використовується, коли немає даних для повернення клієнту, тому відповідь не повинна мати тіла. |
|||
* **`300 - 399`** "Перенаправлення". Відповіді з цими кодами можуть мати або не мати тіла, за винятком `304`, "Not Modified" (не змінено), яка не повинна мати тіла. |
|||
* **`400 - 499`** "Помилка клієнта". Це другий тип, який Ви, ймовірно, будете використовувати найчастіше. |
|||
* Приклад `404`, "Not Found" (не знайдено). |
|||
* Для загальних помилок клієнта можна використовувати `400`. |
|||
* `500 - 599` "Помилки сервера". Ви майже ніколи не використовуєте їх напряму. Якщо в коді Вашого застосунку або на сервері щось пішло не так, автоматично буде повернено один із цих кодів статусу. |
|||
|
|||
/// tip | Порада |
|||
|
|||
Щоб дізнатися більше про кожен код статусу і призначення кожного з них, перегляньте документацію <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status" class="external-link" target="_blank"><abbr title="Mozilla Developer Network">MDN</abbr> про HTTP коди статусу</a>. |
|||
|
|||
/// |
|||
|
|||
## Легкий спосіб запам'ятати назви |
|||
|
|||
Розглянемо ще раз попередній приклад: |
|||
|
|||
{* ../../docs_src/response_status_code/tutorial001.py hl[6] *} |
|||
|
|||
`201` - це код статусу для "Created" (створено). |
|||
|
|||
Але Вам не потрібно запам'ятовувати, що означає кожен із цих кодів. |
|||
|
|||
Ви можете використовувати зручні змінні з `fastapi.status` |
|||
|
|||
{* ../../docs_src/response_status_code/tutorial002.py hl[1,6] *} |
|||
|
|||
Ці змінні просто для зручності. Вони містять ті ж самі числа, але Ви можете скористатися автозаповненням в редакторі: |
|||
|
|||
<img src="/img/tutorial/response-status-code/image02.png"> |
|||
|
|||
/// note | Технічні деталі |
|||
|
|||
Ви також можете використати `from starlette import status`. |
|||
|
|||
**FastAPI** надає ті ж самі змінні `starlette.status` як `fastapi.status`, просто для зручності розробника. Однак вони походять безпосередньо зі Starlette. |
|||
|
|||
/// |
|||
|
|||
## Зміна значення за замовчуванням |
|||
|
|||
Далі, у Посібнику для досвідчених користувачів{.internal-link target=_blank}, Ви дізнаєтесь, як повернути інший код статусу, ніж той, який Ви оголосили тут. |
@ -0,0 +1,240 @@ |
|||
# Тестування |
|||
|
|||
Тестування **FastAPI** додатків є простим та ефективним завдяки бібліотеці <a href="https://www.starlette.io/testclient/" class="external-link" target="_blank">Starlette</a>, яка базується на <a href="https://www.python-httpx.org" class="external-link" target="_blank">HTTPX</a>. |
|||
Оскільки HTTPX розроблений на основі Requests, його API є інтуїтивно зрозумілим для тих, хто вже знайомий з Requests. |
|||
|
|||
З його допомогою Ви можете використовувати <a href="https://docs.pytest.org/" class="external-link" target="_blank">pytest</a> безпосередньо з **FastAPI**. |
|||
|
|||
## Використання `TestClient` |
|||
|
|||
/// info | Інформація |
|||
|
|||
Щоб використовувати `TestClient`, спочатку встановіть <a href="https://www.python-httpx.org" class="external-link" target="_blank">`httpx`</a>. |
|||
|
|||
Переконайтеся, що Ви створили [віртуальне середовище](../virtual-environments.md){.internal-link target=_blank}, активували його, а потім встановили саму бібліотеку, наприклад: |
|||
|
|||
```console |
|||
$ pip install httpx |
|||
``` |
|||
|
|||
/// |
|||
|
|||
Імпортуйте `TestClient`. |
|||
|
|||
Створіть `TestClient`, передавши йому Ваш застосунок **FastAPI**. |
|||
|
|||
Створюйте функції з іменами, що починаються з `test_` (це стандартна угода для `pytest`). |
|||
|
|||
Використовуйте об'єкт `TestClient` так само як і `httpx`. |
|||
|
|||
Записуйте прості `assert`-вирази зі стандартними виразами Python, які потрібно перевірити (це також стандарт для `pytest`). |
|||
|
|||
{* ../../docs_src/app_testing/tutorial001.py hl[2,12,15:18] *} |
|||
|
|||
|
|||
/// tip | Порада |
|||
|
|||
Зверніть увагу, що тестові функції — це звичайні `def`, а не `async def`. |
|||
|
|||
Виклики клієнта також звичайні, без використання `await`. |
|||
|
|||
Це дозволяє використовувати `pytest` без зайвих ускладнень. |
|||
|
|||
/// |
|||
|
|||
/// note | Технічні деталі |
|||
|
|||
Ви також можете використовувати `from starlette.testclient import TestClient`. |
|||
|
|||
**FastAPI** надає той самий `starlette.testclient` під назвою `fastapi.testclient` для зручності розробників, але він безпосередньо походить із Starlette. |
|||
|
|||
/// |
|||
|
|||
/// tip | Порада |
|||
|
|||
Якщо Вам потрібно викликати `async`-функції у ваших тестах, окрім відправлення запитів до FastAPI-застосунку (наприклад, асинхронні функції роботи з базою даних), перегляньте [Асинхронні тести](../advanced/async-tests.md){.internal-link target=_blank} у розширеному керівництві. |
|||
|
|||
/// |
|||
|
|||
## Розділення тестів |
|||
|
|||
У реальному застосунку Ваші тести, ймовірно, будуть в окремому файлі. |
|||
|
|||
Також Ваш **FastAPI**-застосунок може складатися з кількох файлів або модулів тощо. |
|||
|
|||
### Файл застосунку **FastAPI** |
|||
|
|||
Припустимо, у Вас є структура файлів, описана в розділі [Більші застосунки](bigger-applications.md){.internal-link target=_blank}: |
|||
|
|||
``` |
|||
. |
|||
├── app |
|||
│ ├── __init__.py |
|||
│ └── main.py |
|||
``` |
|||
У файлі `main.py` знаходиться Ваш застосунок **FastAPI** : |
|||
|
|||
{* ../../docs_src/app_testing/main.py *} |
|||
|
|||
### Файл тестування |
|||
|
|||
Ви можете створити файл `test_main.py` з Вашими тестами. Він може знаходитися в тому ж пакеті Python (у тій самій директорії з файлом `__init__.py`): |
|||
|
|||
|
|||
``` hl_lines="5" |
|||
. |
|||
├── app |
|||
│ ├── __init__.py |
|||
│ ├── main.py |
|||
│ └── test_main.py |
|||
``` |
|||
|
|||
Оскільки цей файл знаходиться в тому ж пакеті, Ви можете використовувати відносний імпорт, щоб імпортувати об'єкт `app` із модуля `main` (`main.py`): |
|||
|
|||
{* ../../docs_src/app_testing/test_main.py hl[3] *} |
|||
|
|||
|
|||
...і написати код для тестів так само як і раніше. |
|||
|
|||
## Тестування: розширений приклад |
|||
|
|||
Тепер розширимо цей приклад і додамо більше деталей, щоб побачити, як тестувати різні частини. |
|||
|
|||
### Розширений файл застосунку **FastAPI** |
|||
|
|||
Залишимо ту саму структуру файлів: |
|||
|
|||
``` |
|||
. |
|||
├── app |
|||
│ ├── __init__.py |
|||
│ ├── main.py |
|||
│ └── test_main.py |
|||
``` |
|||
|
|||
Припустимо, що тепер файл `main.py` із Вашим **FastAPI**-застосунком містить додаткові операції шляху (**path operations**). |
|||
|
|||
Він має `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 *} |
|||
|
|||
Коли Вам потрібно передати клієнту інформацію в запиті, але Ви не знаєте, як це зробити, Ви можете пошукати (наприклад, у Google) спосіб реалізації в `httpx`, або навіть у `requests`, оскільки HTTPX розроблений на основі дизайну Requests. |
|||
|
|||
Далі Ви просто повторюєте ці ж дії у ваших тестах. |
|||
|
|||
Наприклад: |
|||
|
|||
* Щоб передати *path* або *query* параметр, додайте його безпосередньо до URL. |
|||
* Щоб передати тіло JSON, передайте Python-об'єкт (наприклад, `dict`) у параметр `json`. |
|||
* Якщо потрібно надіслати *Form Data* замість JSON, використовуйте параметр `data`. |
|||
* Щоб передати заголовки *headers*, використовуйте `dict` у параметрі `headers`. |
|||
* Для *cookies* використовуйте `dict` у параметрі `cookies`. |
|||
|
|||
Докладніше про передачу даних у бекенд (за допомогою `httpx` або `TestClient`) можна знайти в <a href="https://www.python-httpx.org" class="external-link" target="_blank">документації HTTPX</a>. |
|||
|
|||
/// info | Інформація |
|||
|
|||
Зверніть увагу, що `TestClient` отримує дані, які можна конвертувати в JSON, а не Pydantic-моделі. |
|||
Якщо у Вас є Pydantic-модель у тесті, і Ви хочете передати її дані в додаток під час тестування, Ви можете використати `jsonable_encoder`, описаний у розділі [JSON Compatible Encoder](encoder.md){.internal-link target=_blank}. |
|||
|
|||
/// |
|||
|
|||
## Запуск тестів |
|||
|
|||
Після цього вам потрібно встановити `pytest`. |
|||
|
|||
Переконайтеся, що Ви створили [віртуальне середовище]{.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,17 @@ |
|||
# Triển khai FastAPI trên các Dịch vụ Cloud |
|||
|
|||
Bạn có thể sử dụng **bất kỳ nhà cung cấp dịch vụ cloud** nào để triển khai ứng dụng FastAPI của mình. |
|||
|
|||
Trong hầu hết các trường hợp, các nhà cung cấp dịch vụ cloud lớn đều có hướng dẫn triển khai FastAPI với họ. |
|||
|
|||
## Nhà cung cấp dịch vụ Cloud - Nhà tài trợ |
|||
Một vài nhà cung cấp dịch vụ cloud ✨ [**tài trợ cho FastAPI**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨, điều này giúp đảm bảo sự phát triển liên tục và khỏe mạnh của FastAPI và hệ sinh thái của nó. |
|||
|
|||
Thêm nữa, điều này cũng thể hiện cam kết thực sự của họ đối với FastAPI và **cộng đồng người dùng** (bạn), vì họ không chỉ muốn cung cấp cho bạn một **dịch vụ tốt** mà còn muốn đảm bảo rằng bạn có một **framework tốt và bền vững**, đó chính là FastAPI. 🙇 |
|||
|
|||
Bạn có thể thử các dịch vụ của họ và làm theo hướng dẫn của họ: |
|||
|
|||
* <a href="https://docs.platform.sh/languages/python.html?utm_source=fastapi-signup&utm_medium=banner&utm_campaign=FastAPI-signup-June-2023" class="external-link" target="_blank">Platform.sh</a> |
|||
* <a href="https://docs.porter.run/language-specific-guides/fastapi" class="external-link" target="_blank">Porter</a> |
|||
* <a href="https://www.withcoherence.com/?utm_medium=advertising&utm_source=fastapi&utm_campaign=website" class="external-link" target="_blank">Coherence</a> |
|||
* <a href="https://docs.render.com/deploy-fastapi?utm_source=deploydoc&utm_medium=referral&utm_campaign=fastapi" class="external-link" target="_blank">Render</a> |
@ -0,0 +1,21 @@ |
|||
# Triển khai |
|||
|
|||
Triển khai một ứng dụng **FastAPI** khá dễ dàng. |
|||
|
|||
## Triển khai là gì |
|||
|
|||
Triển khai một ứng dụng có nghĩa là thực hiện các bước cần thiết để làm cho nó **sẵn sàng phục vụ người dùng**. |
|||
|
|||
Đối với một **API web**, điều này có nghĩa là đặt nó trong một **máy chủ từ xa**, với một **chương trình máy chủ** cung cấp hiệu suất tốt, ổn định, v.v., để người dùng của bạn có thể truy cập ứng dụng của bạn một cách hiệu quả và không bị gián đoạn hoặc gặp vấn đề. |
|||
|
|||
Điều này trái ngược với các **giai đoạn phát triển**, trong đó bạn liên tục thay đổi mã, phá vỡ nó và sửa nó, ngừng và khởi động lại máy chủ phát triển, v.v. |
|||
|
|||
## Các Chiến lược Triển khai |
|||
|
|||
Có nhiều cách để triển khai ứng dụng của bạn tùy thuộc vào trường hợp sử dụng của bạn và các công cụ mà bạn sử dụng. |
|||
|
|||
Bạn có thể **triển khai một máy chủ** của riêng bạn bằng cách sử dụng một sự kết hợp các công cụ, hoặc bạn có thể sử dụng một **dịch vụ cloud** để làm một số công việc cho bạn, hoặc các tùy chọn khác. |
|||
|
|||
Tôi sẽ chỉ ra một số khái niệm chính cần thiết khi triển khai một ứng dụng **FastAPI** (mặc dù hầu hết nó áp dụng cho bất kỳ loại ứng dụng web nào). |
|||
|
|||
Bạn sẽ thấy nhiều chi tiết cần thiết và một số kỹ thuật để triển khai trong các phần tiếp theo. ✨ |
@ -0,0 +1,93 @@ |
|||
# Về các phiên bản của FastAPI |
|||
|
|||
**FastAPI** đã được sử dụng ở quy mô thực tế (production) trong nhiều ứng dụng và hệ thống. Và phạm vi kiểm thử được giữ ở mức 100%. Nhưng việc phát triển của nó vẫn đang diễn ra nhanh chóng. |
|||
|
|||
Các tính năng mới được bổ sung thường xuyên, lỗi được sửa định kỳ, và mã nguồn vẫn đang được cải thiện liên tục |
|||
|
|||
Đó là lí do các phiên bản hiện tại vẫn còn là 0.x.x, điều này phản ánh rằng mỗi phiên bản có thể có các thay đổi gây mất tương thích. Điều này tuân theo các quy ước về <a href="https://semver.org/" class="external-link" target="blank">Semantic Versioning</a>. |
|||
|
|||
Bạn có thể tạo ra sản phẩm thực tế với **FastAPI** ngay bây giờ (và bạn có thể đã làm điều này trong một thời gian dài), bạn chỉ cần đảm bảo rằng bạn sử dụng một phiên bản hoạt động đúng với các đoạn mã còn lại của bạn. |
|||
|
|||
## Cố định phiên bản của `fastapi` |
|||
|
|||
Điều đầu tiên bạn nên làm là "cố định" phiên bản của **FastAPI** bạn đang sử dụng để phiên bản mới nhất mà bạn biết hoạt động đúng với ứng dụng của bạn. |
|||
|
|||
Ví dụ, giả sử bạn đang sử dụng phiên bản `0.112.0` trong ứng dụng của bạn. |
|||
|
|||
Nếu bạn sử dụng một tệp `requirements.txt` bạn có thể chỉ định phiên bản với: |
|||
|
|||
```txt |
|||
fastapi[standard]==0.112.0 |
|||
``` |
|||
|
|||
Như vậy, bạn sẽ sử dụng chính xác phiên bản `0.112.0`. |
|||
|
|||
Hoặc bạn cũng có thể cố định nó với: |
|||
|
|||
```txt |
|||
fastapi[standard]>=0.112.0,<0.113.0 |
|||
``` |
|||
|
|||
Như vậy, bạn sẽ sử dụng các phiên bản `0.112.0` trở lên, nhưng nhỏ hơn `0.113.0`, ví dụ, một phiên bản `0.112.2` vẫn được chấp nhận. |
|||
|
|||
Nếu bạn sử dụng bất kỳ công cụ nào để quản lý cài đặt của bạn, như `uv`, Poetry, Pipenv, hoặc bất kỳ công cụ nào khác, chúng đều có một cách để bạn có thể định nghĩa các phiên bản cụ thể cho các gói của bạn. |
|||
|
|||
## Các phiên bản có sẵn |
|||
|
|||
Bạn có thể xem các phiên bản có sẵn (ví dụ để kiểm tra phiên bản mới nhất) trong [Release Notes](../release-notes.md){.internal-link target=_blank}. |
|||
|
|||
## Về các phiên bản |
|||
|
|||
Theo quy ước về Semantic Versioning, bất kỳ phiên bản nào bên dưới `1.0.0` có thể thêm các thay đổi gây mất tương thích. |
|||
|
|||
**FastAPI** cũng theo quy ước rằng bất kỳ thay đổi phiên bản "PATCH" nào là cho các lỗi và các thay đổi không gây mất tương thích. |
|||
|
|||
/// tip |
|||
|
|||
"PATCH" là số cuối cùng, ví dụ, trong `0.2.3`, phiên bản PATCH là `3`. |
|||
|
|||
/// |
|||
|
|||
Vì vậy, bạn có thể cố định đến một phiên bản như: |
|||
|
|||
```txt |
|||
fastapi>=0.45.0,<0.46.0 |
|||
``` |
|||
|
|||
Các thay đổi gây mất tương thích và các tính năng mới được thêm vào trong các phiên bản "MINOR". |
|||
|
|||
/// tip |
|||
|
|||
"MINOR" là số ở giữa, ví dụ, trong `0.2.3`, phiên bản MINOR là `2`. |
|||
|
|||
/// |
|||
|
|||
## Nâng cấp các phiên bản của FastAPI |
|||
|
|||
Bạn nên thêm các bài kiểm tra (tests) cho ứng dụng của bạn. |
|||
|
|||
Với **FastAPI** điều này rất dễ dàng (nhờ vào Starlette), kiểm tra tài liệu: [Testing](../tutorial/testing.md){.internal-link target=_blank} |
|||
|
|||
Sau khi bạn có các bài kiểm tra, bạn có thể nâng cấp phiên bản **FastAPI** lên một phiên bản mới hơn, và đảm bảo rằng tất cả mã của bạn hoạt động đúng bằng cách chạy các bài kiểm tra của bạn. |
|||
|
|||
Nếu mọi thứ đang hoạt động, hoặc sau khi bạn thực hiện các thay đổi cần thiết, và tất cả các bài kiểm tra của bạn đều đi qua, thì bạn có thể cố định phiên bản của `fastapi` đến phiên bản mới hơn. |
|||
|
|||
## Về Starlette |
|||
|
|||
Bạn không nên cố định phiên bản của `starlette`. |
|||
|
|||
Các phiên bản khác nhau của **FastAPI** sẽ sử dụng một phiên bản Starlette mới hơn. |
|||
|
|||
Vì vậy, bạn có thể để **FastAPI** sử dụng phiên bản Starlette phù hợp. |
|||
|
|||
## Về Pydantic |
|||
|
|||
Pydantic bao gồm các bài kiểm tra của riêng nó cho **FastAPI**, vì vậy các phiên bản mới hơn của Pydantic (trên `1.0.0`) luôn tương thích với **FastAPI**. |
|||
|
|||
Bạn có thể cố định Pydantic đến bất kỳ phiên bản nào trên `1.0.0` mà bạn muốn. |
|||
|
|||
Ví dụ: |
|||
|
|||
```txt |
|||
pydantic>=2.7.0,<3.0.0 |
|||
``` |
@ -0,0 +1,19 @@ |
|||
from typing import List, Union |
|||
|
|||
from fastapi import FastAPI, Header |
|||
from pydantic import BaseModel |
|||
|
|||
app = FastAPI() |
|||
|
|||
|
|||
class CommonHeaders(BaseModel): |
|||
host: str |
|||
save_data: bool |
|||
if_modified_since: Union[str, None] = None |
|||
traceparent: Union[str, None] = None |
|||
x_tag: List[str] = [] |
|||
|
|||
|
|||
@app.get("/items/") |
|||
async def read_items(headers: CommonHeaders = Header(convert_underscores=False)): |
|||
return headers |
@ -0,0 +1,22 @@ |
|||
from typing import List, Union |
|||
|
|||
from fastapi import FastAPI, Header |
|||
from pydantic import BaseModel |
|||
from typing_extensions import Annotated |
|||
|
|||
app = FastAPI() |
|||
|
|||
|
|||
class CommonHeaders(BaseModel): |
|||
host: str |
|||
save_data: bool |
|||
if_modified_since: Union[str, None] = None |
|||
traceparent: Union[str, None] = None |
|||
x_tag: List[str] = [] |
|||
|
|||
|
|||
@app.get("/items/") |
|||
async def read_items( |
|||
headers: Annotated[CommonHeaders, Header(convert_underscores=False)], |
|||
): |
|||
return headers |
@ -0,0 +1,21 @@ |
|||
from typing import Annotated |
|||
|
|||
from fastapi import FastAPI, Header |
|||
from pydantic import BaseModel |
|||
|
|||
app = FastAPI() |
|||
|
|||
|
|||
class CommonHeaders(BaseModel): |
|||
host: str |
|||
save_data: bool |
|||
if_modified_since: str | None = None |
|||
traceparent: str | None = None |
|||
x_tag: list[str] = [] |
|||
|
|||
|
|||
@app.get("/items/") |
|||
async def read_items( |
|||
headers: Annotated[CommonHeaders, Header(convert_underscores=False)], |
|||
): |
|||
return headers |
@ -0,0 +1,21 @@ |
|||
from typing import Annotated, Union |
|||
|
|||
from fastapi import FastAPI, Header |
|||
from pydantic import BaseModel |
|||
|
|||
app = FastAPI() |
|||
|
|||
|
|||
class CommonHeaders(BaseModel): |
|||
host: str |
|||
save_data: bool |
|||
if_modified_since: Union[str, None] = None |
|||
traceparent: Union[str, None] = None |
|||
x_tag: list[str] = [] |
|||
|
|||
|
|||
@app.get("/items/") |
|||
async def read_items( |
|||
headers: Annotated[CommonHeaders, Header(convert_underscores=False)], |
|||
): |
|||
return headers |
@ -0,0 +1,17 @@ |
|||
from fastapi import FastAPI, Header |
|||
from pydantic import BaseModel |
|||
|
|||
app = FastAPI() |
|||
|
|||
|
|||
class CommonHeaders(BaseModel): |
|||
host: str |
|||
save_data: bool |
|||
if_modified_since: str | None = None |
|||
traceparent: str | None = None |
|||
x_tag: list[str] = [] |
|||
|
|||
|
|||
@app.get("/items/") |
|||
async def read_items(headers: CommonHeaders = Header(convert_underscores=False)): |
|||
return headers |
@ -0,0 +1,19 @@ |
|||
from typing import Union |
|||
|
|||
from fastapi import FastAPI, Header |
|||
from pydantic import BaseModel |
|||
|
|||
app = FastAPI() |
|||
|
|||
|
|||
class CommonHeaders(BaseModel): |
|||
host: str |
|||
save_data: bool |
|||
if_modified_since: Union[str, None] = None |
|||
traceparent: Union[str, None] = None |
|||
x_tag: list[str] = [] |
|||
|
|||
|
|||
@app.get("/items/") |
|||
async def read_items(headers: CommonHeaders = Header(convert_underscores=False)): |
|||
return headers |
@ -0,0 +1,31 @@ |
|||
import random |
|||
from typing import Union |
|||
|
|||
from fastapi import FastAPI |
|||
from pydantic import AfterValidator |
|||
from typing_extensions import Annotated |
|||
|
|||
app = FastAPI() |
|||
|
|||
data = { |
|||
"isbn-9781529046137": "The Hitchhiker's Guide to the Galaxy", |
|||
"imdb-tt0371724": "The Hitchhiker's Guide to the Galaxy", |
|||
"isbn-9781439512982": "Isaac Asimov: The Complete Stories, Vol. 2", |
|||
} |
|||
|
|||
|
|||
def check_valid_id(id: str): |
|||
if not id.startswith(("isbn-", "imdb-")): |
|||
raise ValueError('Invalid ID format, it must start with "isbn-" or "imdb-"') |
|||
return id |
|||
|
|||
|
|||
@app.get("/items/") |
|||
async def read_items( |
|||
id: Annotated[Union[str, None], AfterValidator(check_valid_id)] = None, |
|||
): |
|||
if id: |
|||
item = data.get(id) |
|||
else: |
|||
id, item = random.choice(list(data.items())) |
|||
return {"id": id, "name": item} |
@ -0,0 +1,30 @@ |
|||
import random |
|||
from typing import Annotated |
|||
|
|||
from fastapi import FastAPI |
|||
from pydantic import AfterValidator |
|||
|
|||
app = FastAPI() |
|||
|
|||
data = { |
|||
"isbn-9781529046137": "The Hitchhiker's Guide to the Galaxy", |
|||
"imdb-tt0371724": "The Hitchhiker's Guide to the Galaxy", |
|||
"isbn-9781439512982": "Isaac Asimov: The Complete Stories, Vol. 2", |
|||
} |
|||
|
|||
|
|||
def check_valid_id(id: str): |
|||
if not id.startswith(("isbn-", "imdb-")): |
|||
raise ValueError('Invalid ID format, it must start with "isbn-" or "imdb-"') |
|||
return id |
|||
|
|||
|
|||
@app.get("/items/") |
|||
async def read_items( |
|||
id: Annotated[str | None, AfterValidator(check_valid_id)] = None, |
|||
): |
|||
if id: |
|||
item = data.get(id) |
|||
else: |
|||
id, item = random.choice(list(data.items())) |
|||
return {"id": id, "name": item} |
@ -0,0 +1,30 @@ |
|||
import random |
|||
from typing import Annotated, Union |
|||
|
|||
from fastapi import FastAPI |
|||
from pydantic import AfterValidator |
|||
|
|||
app = FastAPI() |
|||
|
|||
data = { |
|||
"isbn-9781529046137": "The Hitchhiker's Guide to the Galaxy", |
|||
"imdb-tt0371724": "The Hitchhiker's Guide to the Galaxy", |
|||
"isbn-9781439512982": "Isaac Asimov: The Complete Stories, Vol. 2", |
|||
} |
|||
|
|||
|
|||
def check_valid_id(id: str): |
|||
if not id.startswith(("isbn-", "imdb-")): |
|||
raise ValueError('Invalid ID format, it must start with "isbn-" or "imdb-"') |
|||
return id |
|||
|
|||
|
|||
@app.get("/items/") |
|||
async def read_items( |
|||
id: Annotated[Union[str, None], AfterValidator(check_valid_id)] = None, |
|||
): |
|||
if id: |
|||
item = data.get(id) |
|||
else: |
|||
id, item = random.choice(list(data.items())) |
|||
return {"id": id, "name": item} |
@ -1,4 +1,4 @@ |
|||
# For mkdocstrings and tests |
|||
httpx >=0.23.0,<0.28.0 |
|||
# For linting and generating docs versions |
|||
ruff ==0.6.4 |
|||
ruff ==0.11.2 |
|||
|
@ -1 +1 @@ |
|||
pydantic-ai==0.0.15 |
|||
pydantic-ai==0.0.30 |
|||
|
@ -0,0 +1,285 @@ |
|||
import importlib |
|||
|
|||
import pytest |
|||
from dirty_equals import IsDict |
|||
from fastapi.testclient import TestClient |
|||
from inline_snapshot import snapshot |
|||
|
|||
from tests.utils import needs_py39, needs_py310 |
|||
|
|||
|
|||
@pytest.fixture( |
|||
name="client", |
|||
params=[ |
|||
"tutorial003", |
|||
pytest.param("tutorial003_py39", marks=needs_py39), |
|||
pytest.param("tutorial003_py310", marks=needs_py310), |
|||
"tutorial003_an", |
|||
pytest.param("tutorial003_an_py39", marks=needs_py39), |
|||
pytest.param("tutorial003_an_py310", marks=needs_py310), |
|||
], |
|||
) |
|||
def get_client(request: pytest.FixtureRequest): |
|||
mod = importlib.import_module(f"docs_src.header_param_models.{request.param}") |
|||
|
|||
client = TestClient(mod.app) |
|||
return client |
|||
|
|||
|
|||
def test_header_param_model(client: TestClient): |
|||
response = client.get( |
|||
"/items/", |
|||
headers=[ |
|||
("save_data", "true"), |
|||
("if_modified_since", "yesterday"), |
|||
("traceparent", "123"), |
|||
("x_tag", "one"), |
|||
("x_tag", "two"), |
|||
], |
|||
) |
|||
assert response.status_code == 200 |
|||
assert response.json() == { |
|||
"host": "testserver", |
|||
"save_data": True, |
|||
"if_modified_since": "yesterday", |
|||
"traceparent": "123", |
|||
"x_tag": ["one", "two"], |
|||
} |
|||
|
|||
|
|||
def test_header_param_model_no_underscore(client: TestClient): |
|||
response = client.get( |
|||
"/items/", |
|||
headers=[ |
|||
("save-data", "true"), |
|||
("if-modified-since", "yesterday"), |
|||
("traceparent", "123"), |
|||
("x-tag", "one"), |
|||
("x-tag", "two"), |
|||
], |
|||
) |
|||
assert response.status_code == 422 |
|||
assert response.json() == snapshot( |
|||
{ |
|||
"detail": [ |
|||
IsDict( |
|||
{ |
|||
"type": "missing", |
|||
"loc": ["header", "save_data"], |
|||
"msg": "Field required", |
|||
"input": { |
|||
"host": "testserver", |
|||
"traceparent": "123", |
|||
"x_tag": [], |
|||
"accept": "*/*", |
|||
"accept-encoding": "gzip, deflate", |
|||
"connection": "keep-alive", |
|||
"user-agent": "testclient", |
|||
"save-data": "true", |
|||
"if-modified-since": "yesterday", |
|||
"x-tag": "two", |
|||
}, |
|||
} |
|||
) |
|||
| IsDict( |
|||
# TODO: remove when deprecating Pydantic v1 |
|||
{ |
|||
"type": "value_error.missing", |
|||
"loc": ["header", "save_data"], |
|||
"msg": "field required", |
|||
} |
|||
) |
|||
] |
|||
} |
|||
) |
|||
|
|||
|
|||
def test_header_param_model_defaults(client: TestClient): |
|||
response = client.get("/items/", headers=[("save_data", "true")]) |
|||
assert response.status_code == 200 |
|||
assert response.json() == { |
|||
"host": "testserver", |
|||
"save_data": True, |
|||
"if_modified_since": None, |
|||
"traceparent": None, |
|||
"x_tag": [], |
|||
} |
|||
|
|||
|
|||
def test_header_param_model_invalid(client: TestClient): |
|||
response = client.get("/items/") |
|||
assert response.status_code == 422 |
|||
assert response.json() == snapshot( |
|||
{ |
|||
"detail": [ |
|||
IsDict( |
|||
{ |
|||
"type": "missing", |
|||
"loc": ["header", "save_data"], |
|||
"msg": "Field required", |
|||
"input": { |
|||
"x_tag": [], |
|||
"host": "testserver", |
|||
"accept": "*/*", |
|||
"accept-encoding": "gzip, deflate", |
|||
"connection": "keep-alive", |
|||
"user-agent": "testclient", |
|||
}, |
|||
} |
|||
) |
|||
| IsDict( |
|||
# TODO: remove when deprecating Pydantic v1 |
|||
{ |
|||
"type": "value_error.missing", |
|||
"loc": ["header", "save_data"], |
|||
"msg": "field required", |
|||
} |
|||
) |
|||
] |
|||
} |
|||
) |
|||
|
|||
|
|||
def test_header_param_model_extra(client: TestClient): |
|||
response = client.get( |
|||
"/items/", headers=[("save_data", "true"), ("tool", "plumbus")] |
|||
) |
|||
assert response.status_code == 200, response.text |
|||
assert response.json() == snapshot( |
|||
{ |
|||
"host": "testserver", |
|||
"save_data": True, |
|||
"if_modified_since": None, |
|||
"traceparent": None, |
|||
"x_tag": [], |
|||
} |
|||
) |
|||
|
|||
|
|||
def test_openapi_schema(client: TestClient): |
|||
response = client.get("/openapi.json") |
|||
assert response.status_code == 200, response.text |
|||
assert response.json() == snapshot( |
|||
{ |
|||
"openapi": "3.1.0", |
|||
"info": {"title": "FastAPI", "version": "0.1.0"}, |
|||
"paths": { |
|||
"/items/": { |
|||
"get": { |
|||
"summary": "Read Items", |
|||
"operationId": "read_items_items__get", |
|||
"parameters": [ |
|||
{ |
|||
"name": "host", |
|||
"in": "header", |
|||
"required": True, |
|||
"schema": {"type": "string", "title": "Host"}, |
|||
}, |
|||
{ |
|||
"name": "save_data", |
|||
"in": "header", |
|||
"required": True, |
|||
"schema": {"type": "boolean", "title": "Save Data"}, |
|||
}, |
|||
{ |
|||
"name": "if_modified_since", |
|||
"in": "header", |
|||
"required": False, |
|||
"schema": IsDict( |
|||
{ |
|||
"anyOf": [{"type": "string"}, {"type": "null"}], |
|||
"title": "If Modified Since", |
|||
} |
|||
) |
|||
| IsDict( |
|||
# TODO: remove when deprecating Pydantic v1 |
|||
{ |
|||
"type": "string", |
|||
"title": "If Modified Since", |
|||
} |
|||
), |
|||
}, |
|||
{ |
|||
"name": "traceparent", |
|||
"in": "header", |
|||
"required": False, |
|||
"schema": IsDict( |
|||
{ |
|||
"anyOf": [{"type": "string"}, {"type": "null"}], |
|||
"title": "Traceparent", |
|||
} |
|||
) |
|||
| IsDict( |
|||
# TODO: remove when deprecating Pydantic v1 |
|||
{ |
|||
"type": "string", |
|||
"title": "Traceparent", |
|||
} |
|||
), |
|||
}, |
|||
{ |
|||
"name": "x_tag", |
|||
"in": "header", |
|||
"required": False, |
|||
"schema": { |
|||
"type": "array", |
|||
"items": {"type": "string"}, |
|||
"default": [], |
|||
"title": "X Tag", |
|||
}, |
|||
}, |
|||
], |
|||
"responses": { |
|||
"200": { |
|||
"description": "Successful Response", |
|||
"content": {"application/json": {"schema": {}}}, |
|||
}, |
|||
"422": { |
|||
"description": "Validation Error", |
|||
"content": { |
|||
"application/json": { |
|||
"schema": { |
|||
"$ref": "#/components/schemas/HTTPValidationError" |
|||
} |
|||
} |
|||
}, |
|||
}, |
|||
}, |
|||
} |
|||
} |
|||
}, |
|||
"components": { |
|||
"schemas": { |
|||
"HTTPValidationError": { |
|||
"properties": { |
|||
"detail": { |
|||
"items": { |
|||
"$ref": "#/components/schemas/ValidationError" |
|||
}, |
|||
"type": "array", |
|||
"title": "Detail", |
|||
} |
|||
}, |
|||
"type": "object", |
|||
"title": "HTTPValidationError", |
|||
}, |
|||
"ValidationError": { |
|||
"properties": { |
|||
"loc": { |
|||
"items": { |
|||
"anyOf": [{"type": "string"}, {"type": "integer"}] |
|||
}, |
|||
"type": "array", |
|||
"title": "Location", |
|||
}, |
|||
"msg": {"type": "string", "title": "Message"}, |
|||
"type": {"type": "string", "title": "Error Type"}, |
|||
}, |
|||
"type": "object", |
|||
"required": ["loc", "msg", "type"], |
|||
"title": "ValidationError", |
|||
}, |
|||
} |
|||
}, |
|||
} |
|||
) |
@ -0,0 +1,143 @@ |
|||
import importlib |
|||
|
|||
import pytest |
|||
from dirty_equals import IsStr |
|||
from fastapi.testclient import TestClient |
|||
from inline_snapshot import snapshot |
|||
|
|||
from ...utils import needs_py39, needs_py310, needs_pydanticv2 |
|||
|
|||
|
|||
@pytest.fixture( |
|||
name="client", |
|||
params=[ |
|||
pytest.param("tutorial015_an", marks=needs_pydanticv2), |
|||
pytest.param("tutorial015_an_py310", marks=(needs_py310, needs_pydanticv2)), |
|||
pytest.param("tutorial015_an_py39", marks=(needs_py39, needs_pydanticv2)), |
|||
], |
|||
) |
|||
def get_client(request: pytest.FixtureRequest): |
|||
mod = importlib.import_module( |
|||
f"docs_src.query_params_str_validations.{request.param}" |
|||
) |
|||
|
|||
client = TestClient(mod.app) |
|||
return client |
|||
|
|||
|
|||
def test_get_random_item(client: TestClient): |
|||
response = client.get("/items") |
|||
assert response.status_code == 200, response.text |
|||
assert response.json() == {"id": IsStr(), "name": IsStr()} |
|||
|
|||
|
|||
def test_get_item(client: TestClient): |
|||
response = client.get("/items?id=isbn-9781529046137") |
|||
assert response.status_code == 200, response.text |
|||
assert response.json() == { |
|||
"id": "isbn-9781529046137", |
|||
"name": "The Hitchhiker's Guide to the Galaxy", |
|||
} |
|||
|
|||
|
|||
def test_get_item_does_not_exist(client: TestClient): |
|||
response = client.get("/items?id=isbn-nope") |
|||
assert response.status_code == 200, response.text |
|||
assert response.json() == {"id": "isbn-nope", "name": None} |
|||
|
|||
|
|||
def test_get_invalid_item(client: TestClient): |
|||
response = client.get("/items?id=wtf-yes") |
|||
assert response.status_code == 422, response.text |
|||
assert response.json() == snapshot( |
|||
{ |
|||
"detail": [ |
|||
{ |
|||
"type": "value_error", |
|||
"loc": ["query", "id"], |
|||
"msg": 'Value error, Invalid ID format, it must start with "isbn-" or "imdb-"', |
|||
"input": "wtf-yes", |
|||
"ctx": {"error": {}}, |
|||
} |
|||
] |
|||
} |
|||
) |
|||
|
|||
|
|||
def test_openapi_schema(client: TestClient): |
|||
response = client.get("/openapi.json") |
|||
assert response.status_code == 200, response.text |
|||
assert response.json() == snapshot( |
|||
{ |
|||
"openapi": "3.1.0", |
|||
"info": {"title": "FastAPI", "version": "0.1.0"}, |
|||
"paths": { |
|||
"/items/": { |
|||
"get": { |
|||
"summary": "Read Items", |
|||
"operationId": "read_items_items__get", |
|||
"parameters": [ |
|||
{ |
|||
"name": "id", |
|||
"in": "query", |
|||
"required": False, |
|||
"schema": { |
|||
"anyOf": [{"type": "string"}, {"type": "null"}], |
|||
"title": "Id", |
|||
}, |
|||
} |
|||
], |
|||
"responses": { |
|||
"200": { |
|||
"description": "Successful Response", |
|||
"content": {"application/json": {"schema": {}}}, |
|||
}, |
|||
"422": { |
|||
"description": "Validation Error", |
|||
"content": { |
|||
"application/json": { |
|||
"schema": { |
|||
"$ref": "#/components/schemas/HTTPValidationError" |
|||
} |
|||
} |
|||
}, |
|||
}, |
|||
}, |
|||
} |
|||
} |
|||
}, |
|||
"components": { |
|||
"schemas": { |
|||
"HTTPValidationError": { |
|||
"properties": { |
|||
"detail": { |
|||
"items": { |
|||
"$ref": "#/components/schemas/ValidationError" |
|||
}, |
|||
"type": "array", |
|||
"title": "Detail", |
|||
} |
|||
}, |
|||
"type": "object", |
|||
"title": "HTTPValidationError", |
|||
}, |
|||
"ValidationError": { |
|||
"properties": { |
|||
"loc": { |
|||
"items": { |
|||
"anyOf": [{"type": "string"}, {"type": "integer"}] |
|||
}, |
|||
"type": "array", |
|||
"title": "Location", |
|||
}, |
|||
"msg": {"type": "string", "title": "Message"}, |
|||
"type": {"type": "string", "title": "Error Type"}, |
|||
}, |
|||
"type": "object", |
|||
"required": ["loc", "msg", "type"], |
|||
"title": "ValidationError", |
|||
}, |
|||
} |
|||
}, |
|||
} |
|||
) |
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue