committed by
GitHub
49 changed files with 1399 additions and 262 deletions
@ -1,7 +0,0 @@ |
|||
FROM python:3.7 |
|||
|
|||
RUN pip install httpx PyGithub "pydantic==1.5.1" |
|||
|
|||
COPY ./app /app |
|||
|
|||
CMD ["python", "/app/main.py"] |
@ -1,10 +0,0 @@ |
|||
name: "Watch docs previews in PRs" |
|||
description: "Check PRs and trigger new docs deploys" |
|||
author: "Sebastián Ramírez <[email protected]>" |
|||
inputs: |
|||
token: |
|||
description: 'Token for the repo. Can be passed in using {{ secrets.GITHUB_TOKEN }}' |
|||
required: true |
|||
runs: |
|||
using: 'docker' |
|||
image: 'Dockerfile' |
@ -1,101 +0,0 @@ |
|||
import logging |
|||
from datetime import datetime |
|||
from pathlib import Path |
|||
from typing import List, Union |
|||
|
|||
import httpx |
|||
from github import Github |
|||
from github.NamedUser import NamedUser |
|||
from pydantic import BaseModel, BaseSettings, SecretStr |
|||
|
|||
github_api = "https://api.github.com" |
|||
netlify_api = "https://api.netlify.com" |
|||
|
|||
|
|||
class Settings(BaseSettings): |
|||
input_token: SecretStr |
|||
github_repository: str |
|||
github_event_path: Path |
|||
github_event_name: Union[str, None] = None |
|||
|
|||
|
|||
class Artifact(BaseModel): |
|||
id: int |
|||
node_id: str |
|||
name: str |
|||
size_in_bytes: int |
|||
url: str |
|||
archive_download_url: str |
|||
expired: bool |
|||
created_at: datetime |
|||
updated_at: datetime |
|||
|
|||
|
|||
class ArtifactResponse(BaseModel): |
|||
total_count: int |
|||
artifacts: List[Artifact] |
|||
|
|||
|
|||
def get_message(commit: str) -> str: |
|||
return f"Docs preview for commit {commit} at" |
|||
|
|||
|
|||
if __name__ == "__main__": |
|||
logging.basicConfig(level=logging.INFO) |
|||
settings = Settings() |
|||
logging.info(f"Using config: {settings.json()}") |
|||
g = Github(settings.input_token.get_secret_value()) |
|||
repo = g.get_repo(settings.github_repository) |
|||
owner: NamedUser = repo.owner |
|||
headers = {"Authorization": f"token {settings.input_token.get_secret_value()}"} |
|||
prs = list(repo.get_pulls(state="open")) |
|||
response = httpx.get( |
|||
f"{github_api}/repos/{settings.github_repository}/actions/artifacts", |
|||
headers=headers, |
|||
) |
|||
data = response.json() |
|||
artifacts_response = ArtifactResponse.parse_obj(data) |
|||
for pr in prs: |
|||
logging.info("-----") |
|||
logging.info(f"Processing PR #{pr.number}: {pr.title}") |
|||
pr_comments = list(pr.get_issue_comments()) |
|||
pr_commits = list(pr.get_commits()) |
|||
last_commit = pr_commits[0] |
|||
for pr_commit in pr_commits: |
|||
if pr_commit.commit.author.date > last_commit.commit.author.date: |
|||
last_commit = pr_commit |
|||
commit = last_commit.commit.sha |
|||
logging.info(f"Last commit: {commit}") |
|||
message = get_message(commit) |
|||
notified = False |
|||
for pr_comment in pr_comments: |
|||
if message in pr_comment.body: |
|||
notified = True |
|||
logging.info(f"Docs preview was notified: {notified}") |
|||
if not notified: |
|||
artifact_name = f"docs-zip-{commit}" |
|||
use_artifact: Union[Artifact, None] = None |
|||
for artifact in artifacts_response.artifacts: |
|||
if artifact.name == artifact_name: |
|||
use_artifact = artifact |
|||
break |
|||
if not use_artifact: |
|||
logging.info("Artifact not available") |
|||
else: |
|||
logging.info(f"Existing artifact: {use_artifact.name}") |
|||
response = httpx.post( |
|||
"https://api.github.com/repos/tiangolo/fastapi/actions/workflows/preview-docs.yml/dispatches", |
|||
headers=headers, |
|||
json={ |
|||
"ref": "master", |
|||
"inputs": { |
|||
"pr": f"{pr.number}", |
|||
"name": artifact_name, |
|||
"commit": commit, |
|||
}, |
|||
}, |
|||
) |
|||
logging.info( |
|||
f"Trigger sent, response status: {response.status_code} - content: {response.content}" |
|||
) |
|||
logging.info("Finished") |
@ -4,10 +4,64 @@ on: |
|||
branches: |
|||
- master |
|||
pull_request: |
|||
types: [opened, synchronize] |
|||
types: |
|||
- opened |
|||
- synchronize |
|||
jobs: |
|||
changes: |
|||
runs-on: ubuntu-latest |
|||
# Required permissions |
|||
permissions: |
|||
pull-requests: read |
|||
# Set job outputs to values from filter step |
|||
outputs: |
|||
docs: ${{ steps.filter.outputs.docs }} |
|||
steps: |
|||
- uses: actions/checkout@v3 |
|||
# For pull requests it's not necessary to checkout the code but for master it is |
|||
- uses: dorny/paths-filter@v2 |
|||
id: filter |
|||
with: |
|||
filters: | |
|||
docs: |
|||
- README.md |
|||
- docs/** |
|||
- docs_src/** |
|||
- requirements-docs.txt |
|||
langs: |
|||
needs: |
|||
- changes |
|||
runs-on: ubuntu-latest |
|||
outputs: |
|||
langs: ${{ steps.show-langs.outputs.langs }} |
|||
steps: |
|||
- uses: actions/checkout@v3 |
|||
- name: Set up Python |
|||
uses: actions/setup-python@v4 |
|||
with: |
|||
python-version: "3.11" |
|||
- uses: actions/cache@v3 |
|||
id: cache |
|||
with: |
|||
path: ${{ env.pythonLocation }} |
|||
key: ${{ runner.os }}-python-docs-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml', 'requirements-docs.txt') }}-v03 |
|||
- name: Install docs extras |
|||
if: steps.cache.outputs.cache-hit != 'true' |
|||
run: pip install -r requirements-docs.txt |
|||
- name: Export Language Codes |
|||
id: show-langs |
|||
run: | |
|||
echo "langs=$(python ./scripts/docs.py langs-json)" >> $GITHUB_OUTPUT |
|||
|
|||
build-docs: |
|||
needs: |
|||
- changes |
|||
- langs |
|||
if: ${{ needs.changes.outputs.docs == 'true' }} |
|||
runs-on: ubuntu-latest |
|||
strategy: |
|||
matrix: |
|||
lang: ${{ fromJson(needs.langs.outputs.langs) }} |
|||
steps: |
|||
- name: Dump GitHub context |
|||
env: |
|||
@ -29,21 +83,24 @@ jobs: |
|||
- name: Install Material for MkDocs Insiders |
|||
if: ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false ) && steps.cache.outputs.cache-hit != 'true' |
|||
run: pip install git+https://${{ secrets.ACTIONS_TOKEN }}@github.com/squidfunk/mkdocs-material-insiders.git |
|||
- name: Update Languages |
|||
run: python ./scripts/docs.py update-languages |
|||
- name: Build Docs |
|||
run: python ./scripts/docs.py build-all |
|||
- name: Zip docs |
|||
run: bash ./scripts/zip-docs.sh |
|||
run: python ./scripts/docs.py build-lang ${{ matrix.lang }} |
|||
- uses: actions/upload-artifact@v3 |
|||
with: |
|||
name: docs-zip |
|||
path: ./site/docs.zip |
|||
- name: Deploy to Netlify |
|||
uses: nwtgck/[email protected] |
|||
name: docs-site |
|||
path: ./site/** |
|||
|
|||
# https://github.com/marketplace/actions/alls-green#why |
|||
docs-all-green: # This job does nothing and is only used for the branch protection |
|||
if: always() |
|||
needs: |
|||
- build-docs |
|||
runs-on: ubuntu-latest |
|||
steps: |
|||
- name: Decide whether the needed jobs succeeded or failed |
|||
uses: re-actors/alls-green@release/v1 |
|||
with: |
|||
publish-dir: './site' |
|||
production-branch: master |
|||
github-token: ${{ secrets.FASTAPI_BUILD_DOCS_NETLIFY }} |
|||
enable-commit-comment: false |
|||
env: |
|||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} |
|||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} |
|||
jobs: ${{ toJSON(needs) }} |
|||
allowed-skips: build-docs |
|||
|
@ -1,4 +1,4 @@ |
|||
name: Preview Docs |
|||
name: Deploy Docs |
|||
on: |
|||
workflow_run: |
|||
workflows: |
|||
@ -7,39 +7,42 @@ on: |
|||
- completed |
|||
|
|||
jobs: |
|||
preview-docs: |
|||
deploy-docs: |
|||
runs-on: ubuntu-latest |
|||
steps: |
|||
- name: Dump GitHub context |
|||
env: |
|||
GITHUB_CONTEXT: ${{ toJson(github) }} |
|||
run: echo "$GITHUB_CONTEXT" |
|||
- uses: actions/checkout@v3 |
|||
- name: Clean site |
|||
run: | |
|||
rm -rf ./site |
|||
mkdir ./site |
|||
- name: Download Artifact Docs |
|||
id: download |
|||
uses: dawidd6/[email protected] |
|||
with: |
|||
if_no_artifact_found: ignore |
|||
github_token: ${{ secrets.FASTAPI_PREVIEW_DOCS_DOWNLOAD_ARTIFACTS }} |
|||
workflow: build-docs.yml |
|||
run_id: ${{ github.event.workflow_run.id }} |
|||
name: docs-zip |
|||
name: docs-site |
|||
path: ./site/ |
|||
- name: Unzip docs |
|||
run: | |
|||
cd ./site |
|||
unzip docs.zip |
|||
rm -f docs.zip |
|||
- name: Deploy to Netlify |
|||
if: steps.download.outputs.found_artifact == 'true' |
|||
id: netlify |
|||
uses: nwtgck/[email protected] |
|||
with: |
|||
publish-dir: './site' |
|||
production-deploy: false |
|||
production-deploy: ${{ github.event.workflow_run.head_repository.full_name == github.repository && github.event.workflow_run.head_branch == 'master' }} |
|||
github-token: ${{ secrets.FASTAPI_PREVIEW_DOCS_NETLIFY }} |
|||
enable-commit-comment: false |
|||
env: |
|||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} |
|||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} |
|||
- name: Comment Deploy |
|||
if: steps.netlify.outputs.deploy-url != '' |
|||
uses: ./.github/actions/comment-docs-preview-in-pr |
|||
with: |
|||
token: ${{ secrets.FASTAPI_PREVIEW_DOCS_COMMENT_DEPLOY }} |
@ -32,7 +32,7 @@ jobs: |
|||
- name: Build distribution |
|||
run: python -m build |
|||
- name: Publish |
|||
uses: pypa/[email protected].5 |
|||
uses: pypa/[email protected].6 |
|||
with: |
|||
password: ${{ secrets.PYPI_API_TOKEN }} |
|||
- name: Dump GitHub context |
|||
|
@ -0,0 +1,111 @@ |
|||
# URL-адреса метаданных и документации |
|||
|
|||
Вы можете настроить несколько конфигураций метаданных в вашем **FastAPI** приложении. |
|||
|
|||
## Метаданные для API |
|||
|
|||
Вы можете задать следующие поля, которые используются в спецификации OpenAPI и в UI автоматической документации API: |
|||
|
|||
| Параметр | Тип | Описание | |
|||
|------------|--|-------------| |
|||
| `title` | `str` | Заголовок API. | |
|||
| `description` | `str` | Краткое описание API. Может быть использован Markdown. | |
|||
| `version` | `string` | Версия API. Версия вашего собственного приложения, а не OpenAPI. К примеру `2.5.0`. | |
|||
| `terms_of_service` | `str` | Ссылка к условиям пользования 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 адрес контактного лица/организации. ДОЛЖЕН быть в формате 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>url</code></td><td><code>str</code></td><td>URL, указывающий на лицензию, используемую для API. ДОЛЖЕН быть в формате URL.</td></tr></tbody></table></details> | |
|||
|
|||
Вы можете задать их следующим образом: |
|||
|
|||
```Python hl_lines="3-16 19-31" |
|||
{!../../../docs_src/metadata/tutorial001.py!} |
|||
``` |
|||
|
|||
!!! tip "Подсказка" |
|||
Вы можете использовать Markdown в поле `description`, и оно будет отображено в выводе. |
|||
|
|||
С этой конфигурацией автоматическая документация API будут выглядеть так: |
|||
|
|||
<img src="/img/tutorial/metadata/image01.png"> |
|||
|
|||
## Метаданные для тегов |
|||
|
|||
Вы также можете добавить дополнительные метаданные для различных тегов, используемых для группировки ваших операций пути с помощью параметра `openapi_tags`. |
|||
|
|||
Он принимает список, содержащий один словарь для каждого тега. |
|||
|
|||
Каждый словарь может содержать в себе: |
|||
|
|||
* `name` (**обязательно**): `str`-значение с тем же именем тега, которое вы используете в параметре `tags` в ваших *операциях пути* и `APIRouter`ах. |
|||
* `description`: `str`-значение с кратким описанием для тега. Может содержать Markdown и будет отображаться в UI документации. |
|||
* `externalDocs`: `dict`-значение описывающее внешнюю документацию. Включает в себя: |
|||
* `description`: `str`-значение с кратким описанием для внешней документации. |
|||
* `url` (**обязательно**): `str`-значение с URL-адресом для внешней документации. |
|||
|
|||
### Создание метаданных для тегов |
|||
|
|||
Давайте попробуем сделать это на примере с тегами для `users` и `items`. |
|||
|
|||
Создайте метаданные для ваших тегов и передайте их в параметре `openapi_tags`: |
|||
|
|||
```Python hl_lines="3-16 18" |
|||
{!../../../docs_src/metadata/tutorial004.py!} |
|||
``` |
|||
|
|||
Помните, что вы можете использовать Markdown внутри описания, к примеру "login" будет отображен жирным шрифтом (**login**) и "fancy" будет отображаться курсивом (_fancy_). |
|||
|
|||
!!! tip "Подсказка" |
|||
Вам необязательно добавлять метаданные для всех используемых тегов |
|||
|
|||
### Используйте собственные теги |
|||
Используйте параметр `tags` с вашими *операциями пути* (и `APIRouter`ами), чтобы присвоить им различные теги: |
|||
|
|||
```Python hl_lines="21 26" |
|||
{!../../../docs_src/metadata/tutorial004.py!} |
|||
``` |
|||
|
|||
!!! info "Дополнительная информация" |
|||
Узнайте больше о тегах в [Конфигурации операции пути](../path-operation-configuration/#tags){.internal-link target=_blank}. |
|||
|
|||
### Проверьте документацию |
|||
|
|||
Теперь, если вы проверите документацию, вы увидите всю дополнительную информацию: |
|||
|
|||
<img src="/img/tutorial/metadata/image02.png"> |
|||
|
|||
### Порядок расположения тегов |
|||
|
|||
Порядок расположения словарей метаданных для каждого тега определяет также порядок, отображаемый в документах UI |
|||
|
|||
К примеру, несмотря на то, что `users` будут идти после `items` в алфавитном порядке, они отображаются раньше, потому что мы добавляем свои метаданные в качестве первого словаря в списке. |
|||
|
|||
## URL-адреса OpenAPI |
|||
|
|||
По умолчанию схема OpenAPI отображена по адресу `/openapi.json`. |
|||
|
|||
Но вы можете изменить это с помощью параметра `openapi_url`. |
|||
|
|||
К примеру, чтобы задать её отображение по адресу `/api/v1/openapi.json`: |
|||
|
|||
```Python hl_lines="3" |
|||
{!../../../docs_src/metadata/tutorial002.py!} |
|||
``` |
|||
|
|||
Если вы хотите отключить схему 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: |
|||
|
|||
```Python hl_lines="3" |
|||
{!../../../docs_src/metadata/tutorial003.py!} |
|||
``` |
@ -0,0 +1,179 @@ |
|||
# Конфигурация операций пути |
|||
|
|||
Существует несколько параметров, которые вы можете передать вашему *декоратору операций пути* для его настройки. |
|||
|
|||
!!! warning "Внимание" |
|||
Помните, что эти параметры передаются непосредственно *декоратору операций пути*, а не вашей *функции-обработчику операций пути*. |
|||
|
|||
## Коды состояния |
|||
|
|||
Вы можете определить (HTTP) `status_code`, который будет использован в ответах вашей *операции пути*. |
|||
|
|||
Вы можете передать только `int`-значение кода, например `404`. |
|||
|
|||
Но если вы не помните, для чего нужен каждый числовой код, вы можете использовать сокращенные константы в параметре `status`: |
|||
|
|||
=== "Python 3.10+" |
|||
|
|||
```Python hl_lines="1 15" |
|||
{!> ../../../docs_src/path_operation_configuration/tutorial001_py310.py!} |
|||
``` |
|||
|
|||
=== "Python 3.9+" |
|||
|
|||
```Python hl_lines="3 17" |
|||
{!> ../../../docs_src/path_operation_configuration/tutorial001_py39.py!} |
|||
``` |
|||
|
|||
=== "Python 3.6+" |
|||
|
|||
```Python hl_lines="3 17" |
|||
{!> ../../../docs_src/path_operation_configuration/tutorial001.py!} |
|||
``` |
|||
|
|||
Этот код состояния будет использован в ответе и будет добавлен в схему OpenAPI. |
|||
|
|||
!!! note "Технические детали" |
|||
Вы также можете использовать `from starlette import status`. |
|||
|
|||
**FastAPI** предоставляет тот же `starlette.status` под псевдонимом `fastapi.status` для удобства разработчика. Но его источник - это непосредственно Starlette. |
|||
|
|||
## Теги |
|||
|
|||
Вы можете добавлять теги к вашим *операциям пути*, добавив параметр `tags` с `list` заполненным `str`-значениями (обычно в нём только одна строка): |
|||
|
|||
=== "Python 3.10+" |
|||
|
|||
```Python hl_lines="15 20 25" |
|||
{!> ../../../docs_src/path_operation_configuration/tutorial002_py310.py!} |
|||
``` |
|||
|
|||
=== "Python 3.9+" |
|||
|
|||
```Python hl_lines="17 22 27" |
|||
{!> ../../../docs_src/path_operation_configuration/tutorial002_py39.py!} |
|||
``` |
|||
|
|||
=== "Python 3.6+" |
|||
|
|||
```Python hl_lines="17 22 27" |
|||
{!> ../../../docs_src/path_operation_configuration/tutorial002.py!} |
|||
``` |
|||
|
|||
Они будут добавлены в схему OpenAPI и будут использованы в автоматической документации интерфейса: |
|||
|
|||
<img src="/img/tutorial/path-operation-configuration/image01.png"> |
|||
|
|||
### Теги с перечислениями |
|||
|
|||
Если у вас большое приложение, вы можете прийти к необходимости добавить **несколько тегов**, и возможно, вы захотите убедиться в том, что всегда используете **один и тот же тег** для связанных *операций пути*. |
|||
|
|||
В этих случаях, имеет смысл хранить теги в классе `Enum`. |
|||
|
|||
**FastAPI** поддерживает это так же, как и в случае с обычными строками: |
|||
|
|||
```Python hl_lines="1 8-10 13 18" |
|||
{!../../../docs_src/path_operation_configuration/tutorial002b.py!} |
|||
``` |
|||
|
|||
## Краткое и развёрнутое содержание |
|||
|
|||
Вы можете добавить параметры `summary` и `description`: |
|||
|
|||
=== "Python 3.10+" |
|||
|
|||
```Python hl_lines="18-19" |
|||
{!> ../../../docs_src/path_operation_configuration/tutorial003_py310.py!} |
|||
``` |
|||
|
|||
=== "Python 3.9+" |
|||
|
|||
```Python hl_lines="20-21" |
|||
{!> ../../../docs_src/path_operation_configuration/tutorial003_py39.py!} |
|||
``` |
|||
|
|||
=== "Python 3.6+" |
|||
|
|||
```Python hl_lines="20-21" |
|||
{!> ../../../docs_src/path_operation_configuration/tutorial003.py!} |
|||
``` |
|||
|
|||
## Описание из строк документации |
|||
|
|||
Так как описания обычно длинные и содержат много строк, вы можете объявить описание *операции пути* в функции <abbr title="многострочный текст, первое выражение внутри функции (не присвоенный какой-либо переменной), используемый для документации">строки документации</abbr> и **FastAPI** прочитает её отсюда. |
|||
|
|||
Вы можете использовать <a href="https://en.wikipedia.org/wiki/Markdown" class="external-link" target="_blank">Markdown</a> в строке документации, и он будет интерпретирован и отображён корректно (с учетом отступа в строке документации). |
|||
|
|||
=== "Python 3.10+" |
|||
|
|||
```Python hl_lines="17-25" |
|||
{!> ../../../docs_src/path_operation_configuration/tutorial004_py310.py!} |
|||
``` |
|||
|
|||
=== "Python 3.9+" |
|||
|
|||
```Python hl_lines="19-27" |
|||
{!> ../../../docs_src/path_operation_configuration/tutorial004_py39.py!} |
|||
``` |
|||
|
|||
=== "Python 3.6+" |
|||
|
|||
```Python hl_lines="19-27" |
|||
{!> ../../../docs_src/path_operation_configuration/tutorial004.py!} |
|||
``` |
|||
|
|||
Он будет использован в интерактивной документации: |
|||
|
|||
<img src="/img/tutorial/path-operation-configuration/image02.png"> |
|||
|
|||
## Описание ответа |
|||
|
|||
Вы можете указать описание ответа с помощью параметра `response_description`: |
|||
|
|||
=== "Python 3.10+" |
|||
|
|||
```Python hl_lines="19" |
|||
{!> ../../../docs_src/path_operation_configuration/tutorial005_py310.py!} |
|||
``` |
|||
|
|||
=== "Python 3.9+" |
|||
|
|||
```Python hl_lines="21" |
|||
{!> ../../../docs_src/path_operation_configuration/tutorial005_py39.py!} |
|||
``` |
|||
|
|||
=== "Python 3.6+" |
|||
|
|||
```Python hl_lines="21" |
|||
{!> ../../../docs_src/path_operation_configuration/tutorial005.py!} |
|||
``` |
|||
|
|||
!!! info "Дополнительная информация" |
|||
Помните, что `response_description` относится конкретно к ответу, а `description` относится к *операции пути* в целом. |
|||
|
|||
!!! check "Технические детали" |
|||
OpenAPI указывает, что каждой *операции пути* необходимо описание ответа. |
|||
|
|||
Если вдруг вы не укажете его, то **FastAPI** автоматически сгенерирует это описание с текстом "Successful response". |
|||
|
|||
<img src="/img/tutorial/path-operation-configuration/image03.png"> |
|||
|
|||
## Обозначение *операции пути* как устаревшей |
|||
|
|||
Если вам необходимо пометить *операцию пути* как <abbr title="устаревшее, не рекомендовано к использованию">устаревшую</abbr>, при этом не удаляя её, передайте параметр `deprecated`: |
|||
|
|||
```Python hl_lines="16" |
|||
{!../../../docs_src/path_operation_configuration/tutorial006.py!} |
|||
``` |
|||
|
|||
Он будет четко помечен как устаревший в интерактивной документации: |
|||
|
|||
<img src="/img/tutorial/path-operation-configuration/image04.png"> |
|||
|
|||
Проверьте, как будут выглядеть устаревшие и не устаревшие *операции пути*: |
|||
|
|||
<img src="/img/tutorial/path-operation-configuration/image05.png"> |
|||
|
|||
## Резюме |
|||
|
|||
Вы можете легко конфигурировать и добавлять метаданные в ваши *операции пути*, передавая параметры *декораторам операций пути*. |
@ -0,0 +1,16 @@ |
|||
# 高级安全 - 介绍 |
|||
|
|||
## 附加特性 |
|||
|
|||
除 [教程 - 用户指南: 安全性](../../tutorial/security/){.internal-link target=_blank} 中涵盖的功能之外,还有一些额外的功能来处理安全性. |
|||
|
|||
!!! tip "小贴士" |
|||
接下来的章节 **并不一定是 "高级的"**. |
|||
|
|||
而且对于你的使用场景来说,解决方案很可能就在其中。 |
|||
|
|||
## 先阅读教程 |
|||
|
|||
接下来的部分假设你已经阅读了主要的 [教程 - 用户指南: 安全性](../../tutorial/security/){.internal-link target=_blank}. |
|||
|
|||
它们都基于相同的概念,但支持一些额外的功能. |
@ -0,0 +1,433 @@ |
|||
# 设置和环境变量 |
|||
|
|||
在许多情况下,您的应用程序可能需要一些外部设置或配置,例如密钥、数据库凭据、电子邮件服务的凭据等等。 |
|||
|
|||
这些设置中的大多数是可变的(可以更改的),比如数据库的 URL。而且许多设置可能是敏感的,比如密钥。 |
|||
|
|||
因此,通常会将它们提供为由应用程序读取的环境变量。 |
|||
|
|||
## 环境变量 |
|||
|
|||
!!! tip |
|||
如果您已经知道什么是"环境变量"以及如何使用它们,请随意跳到下面的下一节。 |
|||
|
|||
环境变量(也称为"env var")是一种存在于 Python 代码之外、存在于操作系统中的变量,可以被您的 Python 代码(或其他程序)读取。 |
|||
|
|||
您可以在 shell 中创建和使用环境变量,而无需使用 Python: |
|||
|
|||
=== "Linux、macOS、Windows Bash" |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
// 您可以创建一个名为 MY_NAME 的环境变量 |
|||
$ export MY_NAME="Wade Wilson" |
|||
|
|||
// 然后您可以与其他程序一起使用它,例如 |
|||
$ echo "Hello $MY_NAME" |
|||
|
|||
Hello Wade Wilson |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
=== "Windows PowerShell" |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
// 创建一个名为 MY_NAME 的环境变量 |
|||
$ $Env:MY_NAME = "Wade Wilson" |
|||
|
|||
// 与其他程序一起使用它,例如 |
|||
$ echo "Hello $Env:MY_NAME" |
|||
|
|||
Hello Wade Wilson |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
### 在 Python 中读取环境变量 |
|||
|
|||
您还可以在 Python 之外的地方(例如终端中或使用任何其他方法)创建环境变量,然后在 Python 中读取它们。 |
|||
|
|||
例如,您可以有一个名为 `main.py` 的文件,其中包含以下内容: |
|||
|
|||
```Python hl_lines="3" |
|||
import os |
|||
|
|||
name = os.getenv("MY_NAME", "World") |
|||
print(f"Hello {name} from Python") |
|||
``` |
|||
|
|||
!!! tip |
|||
<a href="https://docs.python.org/3.8/library/os.html#os.getenv" class="external-link" target="_blank">`os.getenv()`</a> 的第二个参数是要返回的默认值。 |
|||
|
|||
如果没有提供默认值,默认为 `None`,此处我们提供了 `"World"` 作为要使用的默认值。 |
|||
|
|||
然后,您可以调用该 Python 程序: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
// 这里我们还没有设置环境变量 |
|||
$ python main.py |
|||
|
|||
// 因为我们没有设置环境变量,所以我们得到默认值 |
|||
|
|||
Hello World from Python |
|||
|
|||
// 但是如果我们先创建一个环境变量 |
|||
$ export MY_NAME="Wade Wilson" |
|||
|
|||
// 然后再次调用程序 |
|||
$ python main.py |
|||
|
|||
// 现在它可以读取环境变量 |
|||
|
|||
Hello Wade Wilson from Python |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
由于环境变量可以在代码之外设置,但可以由代码读取,并且不需要与其他文件一起存储(提交到 `git`),因此通常将它们用于配置或设置。 |
|||
|
|||
|
|||
|
|||
您还可以仅为特定程序调用创建一个环境变量,该环境变量仅对该程序可用,并且仅在其运行期间有效。 |
|||
|
|||
要做到这一点,在程序本身之前的同一行创建它: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
// 在此程序调用行中创建一个名为 MY_NAME 的环境变量 |
|||
$ MY_NAME="Wade Wilson" python main.py |
|||
|
|||
// 现在它可以读取环境变量 |
|||
|
|||
Hello Wade Wilson from Python |
|||
|
|||
// 之后环境变量不再存在 |
|||
$ python main.py |
|||
|
|||
Hello World from Python |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
!!! tip |
|||
您可以在 <a href="https://12factor.net/config" class="external-link" target="_blank">Twelve-Factor App: Config</a> 中阅读更多相关信息。 |
|||
|
|||
### 类型和验证 |
|||
|
|||
这些环境变量只能处理文本字符串,因为它们是外部于 Python 的,并且必须与其他程序和整个系统兼容(甚至与不同的操作系统,如 Linux、Windows、macOS)。 |
|||
|
|||
这意味着从环境变量中在 Python 中读取的任何值都将是 `str` 类型,任何类型的转换或验证都必须在代码中完成。 |
|||
|
|||
## Pydantic 的 `Settings` |
|||
|
|||
幸运的是,Pydantic 提供了一个很好的工具来处理来自环境变量的设置,即<a href="https://pydantic-docs.helpmanual.io/usage/settings/" class="external-link" target="_blank">Pydantic: Settings management</a>。 |
|||
|
|||
### 创建 `Settings` 对象 |
|||
|
|||
从 Pydantic 导入 `BaseSettings` 并创建一个子类,与 Pydantic 模型非常相似。 |
|||
|
|||
与 Pydantic 模型一样,您使用类型注释声明类属性,还可以指定默认值。 |
|||
|
|||
您可以使用与 Pydantic 模型相同的验证功能和工具,比如不同的数据类型和使用 `Field()` 进行附加验证。 |
|||
|
|||
```Python hl_lines="2 5-8 11" |
|||
{!../../../docs_src/settings/tutorial001.py!} |
|||
``` |
|||
|
|||
!!! tip |
|||
如果您需要一个快速的复制粘贴示例,请不要使用此示例,而应使用下面的最后一个示例。 |
|||
|
|||
然后,当您创建该 `Settings` 类的实例(在此示例中是 `settings` 对象)时,Pydantic 将以不区分大小写的方式读取环境变量,因此,大写的变量 `APP_NAME` 仍将为属性 `app_name` 读取。 |
|||
|
|||
然后,它将转换和验证数据。因此,当您使用该 `settings` 对象时,您将获得您声明的类型的数据(例如 `items_per_user` 将为 `int` 类型)。 |
|||
|
|||
### 使用 `settings` |
|||
|
|||
然后,您可以在应用程序中使用新的 `settings` 对象: |
|||
|
|||
```Python hl_lines="18-20" |
|||
{!../../../docs_src/settings/tutorial001.py!} |
|||
``` |
|||
|
|||
### 运行服务器 |
|||
|
|||
接下来,您将运行服务器,并将配置作为环境变量传递。例如,您可以设置一个 `ADMIN_EMAIL` 和 `APP_NAME`,如下所示: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ ADMIN_EMAIL="[email protected]" APP_NAME="ChimichangApp"uvicorn main:app |
|||
|
|||
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
!!! tip |
|||
要为单个命令设置多个环境变量,只需用空格分隔它们,并将它们全部放在命令之前。 |
|||
|
|||
然后,`admin_email` 设置将为 `"[email protected]"`。 |
|||
|
|||
`app_name` 将为 `"ChimichangApp"`。 |
|||
|
|||
而 `items_per_user` 将保持其默认值为 `50`。 |
|||
|
|||
## 在另一个模块中设置 |
|||
|
|||
您可以将这些设置放在另一个模块文件中,就像您在[Bigger Applications - Multiple Files](../tutorial/bigger-applications.md){.internal-link target=_blank}中所见的那样。 |
|||
|
|||
例如,您可以创建一个名为 `config.py` 的文件,其中包含以下内容: |
|||
|
|||
```Python |
|||
{!../../../docs_src/settings/app01/config.py!} |
|||
``` |
|||
|
|||
然后在一个名为 `main.py` 的文件中使用它: |
|||
|
|||
```Python hl_lines="3 11-13" |
|||
{!../../../docs_src/settings/app01/main.py!} |
|||
``` |
|||
!!! tip |
|||
您还需要一个名为 `__init__.py` 的文件,就像您在[Bigger Applications - Multiple Files](../tutorial/bigger-applications.md){.internal-link target=_blank}中看到的那样。 |
|||
|
|||
## 在依赖项中使用设置 |
|||
|
|||
在某些情况下,从依赖项中提供设置可能比在所有地方都使用全局对象 `settings` 更有用。 |
|||
|
|||
这在测试期间尤其有用,因为很容易用自定义设置覆盖依赖项。 |
|||
|
|||
### 配置文件 |
|||
|
|||
根据前面的示例,您的 `config.py` 文件可能如下所示: |
|||
|
|||
```Python hl_lines="10" |
|||
{!../../../docs_src/settings/app02/config.py!} |
|||
``` |
|||
|
|||
请注意,现在我们不创建默认实例 `settings = Settings()`。 |
|||
|
|||
### 主应用程序文件 |
|||
|
|||
现在我们创建一个依赖项,返回一个新的 `config.Settings()`。 |
|||
|
|||
=== "Python 3.9+" |
|||
|
|||
```Python hl_lines="6 12-13" |
|||
{!> ../../../docs_src/settings/app02_an_py39/main.py!} |
|||
``` |
|||
|
|||
=== "Python 3.6+" |
|||
|
|||
```Python hl_lines="6 12-13" |
|||
{!> ../../../docs_src/settings/app02_an/main.py!} |
|||
``` |
|||
|
|||
=== "Python 3.6+ 非注解版本" |
|||
|
|||
!!! tip |
|||
如果可能,请尽量使用 `Annotated` 版本。 |
|||
|
|||
```Python hl_lines="5 11-12" |
|||
{!> ../../../docs_src/settings/app02/main.py!} |
|||
``` |
|||
|
|||
!!! tip |
|||
我们稍后会讨论 `@lru_cache()`。 |
|||
|
|||
目前,您可以将 `get_settings()` 视为普通函数。 |
|||
|
|||
然后,我们可以将其作为依赖项从“路径操作函数”中引入,并在需要时使用它。 |
|||
|
|||
=== "Python 3.9+" |
|||
|
|||
```Python hl_lines="17 19-21" |
|||
{!> ../../../docs_src/settings/app02_an_py39/main.py!} |
|||
``` |
|||
|
|||
=== "Python 3.6+" |
|||
|
|||
```Python hl_lines="17 19-21" |
|||
{!> ../../../docs_src/settings/app02_an/main.py!} |
|||
``` |
|||
|
|||
=== "Python 3.6+ 非注解版本" |
|||
|
|||
!!! tip |
|||
如果可能,请尽量使用 `Annotated` 版本。 |
|||
|
|||
```Python hl_lines="16 18-20" |
|||
{!> ../../../docs_src/settings/app02/main.py!} |
|||
``` |
|||
|
|||
### 设置和测试 |
|||
|
|||
然后,在测试期间,通过创建 `get_settings` 的依赖项覆盖,很容易提供一个不同的设置对象: |
|||
|
|||
```Python hl_lines="9-10 13 21" |
|||
{!../../../docs_src/settings/app02/test_main.py!} |
|||
``` |
|||
|
|||
在依赖项覆盖中,我们在创建新的 `Settings` 对象时为 `admin_email` 设置了一个新值,然后返回该新对象。 |
|||
|
|||
然后,我们可以测试它是否被使用。 |
|||
|
|||
## 从 `.env` 文件中读取设置 |
|||
|
|||
如果您有许多可能经常更改的设置,可能在不同的环境中,将它们放在一个文件中,然后从该文件中读取它们,就像它们是环境变量一样,可能非常有用。 |
|||
|
|||
这种做法相当常见,有一个名称,这些环境变量通常放在一个名为 `.env` 的文件中,该文件被称为“dotenv”。 |
|||
|
|||
!!! tip |
|||
以点 (`.`) 开头的文件是 Unix-like 系统(如 Linux 和 macOS)中的隐藏文件。 |
|||
|
|||
但是,dotenv 文件实际上不一定要具有确切的文件名。 |
|||
|
|||
Pydantic 支持使用外部库从这些类型的文件中读取。您可以在<a href="https://pydantic-docs.helpmanual.io/usage/settings/#dotenv-env-support" class="external-link" target="_blank">Pydantic 设置: Dotenv (.env) 支持</a>中阅读更多相关信息。 |
|||
|
|||
!!! tip |
|||
要使其工作,您需要执行 `pip install python-dotenv`。 |
|||
|
|||
### `.env` 文件 |
|||
|
|||
您可以使用以下内容创建一个名为 `.env` 的文件: |
|||
|
|||
```bash |
|||
ADMIN_EMAIL="[email protected]" |
|||
APP_NAME="ChimichangApp" |
|||
``` |
|||
|
|||
### 从 `.env` 文件中读取设置 |
|||
|
|||
然后,您可以使用以下方式更新您的 `config.py`: |
|||
|
|||
```Python hl_lines="9-10" |
|||
{!../../../docs_src/settings/app03/config.py!} |
|||
``` |
|||
|
|||
在这里,我们在 Pydantic 的 `Settings` 类中创建了一个名为 `Config` 的类,并将 `env_file` 设置为我们想要使用的 dotenv 文件的文件名。 |
|||
|
|||
!!! tip |
|||
`Config` 类仅用于 Pydantic 配置。您可以在<a href="https://pydantic-docs.helpmanual.io/usage/model_config/" class="external-link" target="_blank">Pydantic Model Config</a>中阅读更多相关信息。 |
|||
|
|||
### 使用 `lru_cache` 仅创建一次 `Settings` |
|||
|
|||
从磁盘中读取文件通常是一项耗时的(慢)操作,因此您可能希望仅在首次读取后并重复使用相同的设置对象,而不是为每个请求都读取它。 |
|||
|
|||
但是,每次执行以下操作: |
|||
|
|||
```Python |
|||
Settings() |
|||
``` |
|||
|
|||
都会创建一个新的 `Settings` 对象,并且在创建时会再次读取 `.env` 文件。 |
|||
|
|||
如果依赖项函数只是这样的: |
|||
|
|||
```Python |
|||
def get_settings(): |
|||
return Settings() |
|||
``` |
|||
|
|||
我们将为每个请求创建该对象,并且将在每个请求中读取 `.env` 文件。 ⚠️ |
|||
|
|||
但是,由于我们在顶部使用了 `@lru_cache()` 装饰器,因此只有在第一次调用它时,才会创建 `Settings` 对象一次。 ✔️ |
|||
|
|||
=== "Python 3.9+" |
|||
|
|||
```Python hl_lines="1 11" |
|||
{!> ../../../docs_src/settings/app03_an_py39/main.py!} |
|||
``` |
|||
|
|||
=== "Python 3.6+" |
|||
|
|||
```Python hl_lines="1 11" |
|||
{!> ../../../docs_src/settings/app03_an/main.py!} |
|||
``` |
|||
|
|||
=== "Python 3.6+ 非注解版本" |
|||
|
|||
!!! tip |
|||
如果可能,请尽量使用 `Annotated` 版本。 |
|||
|
|||
```Python hl_lines="1 10" |
|||
{!> ../../../docs_src/settings/app03/main.py!} |
|||
``` |
|||
|
|||
然后,在下一次请求的依赖项中对 `get_settings()` 进行任何后续调用时,它不会执行 `get_settings()` 的内部代码并创建新的 `Settings` 对象,而是返回在第一次调用时返回的相同对象,一次又一次。 |
|||
|
|||
#### `lru_cache` 技术细节 |
|||
|
|||
`@lru_cache()` 修改了它所装饰的函数,以返回第一次返回的相同值,而不是再次计算它,每次都执行函数的代码。 |
|||
|
|||
因此,下面的函数将对每个参数组合执行一次。然后,每个参数组合返回的值将在使用完全相同的参数组合调用函数时再次使用。 |
|||
|
|||
例如,如果您有一个函数: |
|||
```Python |
|||
@lru_cache() |
|||
def say_hi(name: str, salutation: str = "Ms."): |
|||
return f"Hello {salutation} {name}" |
|||
``` |
|||
|
|||
您的程序可以像这样执行: |
|||
|
|||
```mermaid |
|||
sequenceDiagram |
|||
|
|||
participant code as Code |
|||
participant function as say_hi() |
|||
participant execute as Execute function |
|||
|
|||
rect rgba(0, 255, 0, .1) |
|||
code ->> function: say_hi(name="Camila") |
|||
function ->> execute: 执行函数代码 |
|||
execute ->> code: 返回结果 |
|||
end |
|||
|
|||
rect rgba(0, 255, 255, .1) |
|||
code ->> function: say_hi(name="Camila") |
|||
function ->> code: 返回存储的结果 |
|||
end |
|||
|
|||
rect rgba(0, 255, 0, .1) |
|||
code ->> function: say_hi(name="Rick") |
|||
function ->> execute: 执行函数代码 |
|||
execute ->> code: 返回结果 |
|||
end |
|||
|
|||
rect rgba(0, 255, 0, .1) |
|||
code ->> function: say_hi(name="Rick", salutation="Mr.") |
|||
function ->> execute: 执行函数代码 |
|||
execute ->> code: 返回结果 |
|||
end |
|||
|
|||
rect rgba(0, 255, 255, .1) |
|||
code ->> function: say_hi(name="Rick") |
|||
function ->> code: 返回存储的结果 |
|||
end |
|||
|
|||
rect rgba(0, 255, 255, .1) |
|||
code ->> function: say_hi(name="Camila") |
|||
function ->> code: 返回存储的结果 |
|||
end |
|||
``` |
|||
|
|||
对于我们的依赖项 `get_settings()`,该函数甚至不接受任何参数,因此它始终返回相同的值。 |
|||
|
|||
这样,它的行为几乎就像是一个全局变量。但是由于它使用了依赖项函数,因此我们可以轻松地进行测试时的覆盖。 |
|||
|
|||
`@lru_cache()` 是 `functools` 的一部分,它是 Python 标准库的一部分,您可以在<a href="https://docs.python.org/3/library/functools.html#functools.lru_cache" class="external-link" target="_blank">Python 文档中了解有关 `@lru_cache()` 的更多信息</a>。 |
|||
|
|||
## 小结 |
|||
|
|||
您可以使用 Pydantic 设置处理应用程序的设置或配置,利用 Pydantic 模型的所有功能。 |
|||
|
|||
* 通过使用依赖项,您可以简化测试。 |
|||
* 您可以使用 `.env` 文件。 |
|||
* 使用 `@lru_cache()` 可以避免为每个请求重复读取 dotenv 文件,同时允许您在测试时进行覆盖。 |
@ -0,0 +1,214 @@ |
|||
# WebSockets |
|||
|
|||
您可以在 **FastAPI** 中使用 [WebSockets](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API)。 |
|||
|
|||
## 安装 `WebSockets` |
|||
|
|||
首先,您需要安装 `WebSockets`: |
|||
|
|||
```console |
|||
$ pip install websockets |
|||
|
|||
---> 100% |
|||
``` |
|||
|
|||
## WebSockets 客户端 |
|||
|
|||
### 在生产环境中 |
|||
|
|||
在您的生产系统中,您可能使用现代框架(如React、Vue.js或Angular)创建了一个前端。 |
|||
|
|||
要使用 WebSockets 与后端进行通信,您可能会使用前端的工具。 |
|||
|
|||
或者,您可能有一个原生移动应用程序,直接使用原生代码与 WebSocket 后端通信。 |
|||
|
|||
或者,您可能有其他与 WebSocket 终端通信的方式。 |
|||
|
|||
--- |
|||
|
|||
但是,在本示例中,我们将使用一个非常简单的HTML文档,其中包含一些JavaScript,全部放在一个长字符串中。 |
|||
|
|||
当然,这并不是最优的做法,您不应该在生产环境中使用它。 |
|||
|
|||
在生产环境中,您应该选择上述任一选项。 |
|||
|
|||
但这是一种专注于 WebSockets 的服务器端并提供一个工作示例的最简单方式: |
|||
|
|||
```Python hl_lines="2 6-38 41-43" |
|||
{!../../../docs_src/websockets/tutorial001.py!} |
|||
``` |
|||
|
|||
## 创建 `websocket` |
|||
|
|||
在您的 **FastAPI** 应用程序中,创建一个 `websocket`: |
|||
|
|||
```Python hl_lines="1 46-47" |
|||
{!../../../docs_src/websockets/tutorial001.py!} |
|||
``` |
|||
|
|||
!!! note "技术细节" |
|||
您也可以使用 `from starlette.websockets import WebSocket`。 |
|||
|
|||
**FastAPI** 直接提供了相同的 `WebSocket`,只是为了方便开发人员。但它直接来自 Starlette。 |
|||
|
|||
## 等待消息并发送消息 |
|||
|
|||
在您的 WebSocket 路由中,您可以使用 `await` 等待消息并发送消息。 |
|||
|
|||
```Python hl_lines="48-52" |
|||
{!../../../docs_src/websockets/tutorial001.py!} |
|||
``` |
|||
|
|||
您可以接收和发送二进制、文本和 JSON 数据。 |
|||
|
|||
## 尝试一下 |
|||
|
|||
如果您的文件名为 `main.py`,请使用以下命令运行应用程序: |
|||
|
|||
```console |
|||
$ uvicorn main:app --reload |
|||
|
|||
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) |
|||
``` |
|||
|
|||
在浏览器中打开 <a href="http://127.0.0.1:8000" class="external-link" target="_blank">http://127.0.0.1:8000</a>。 |
|||
|
|||
您将看到一个简单的页面,如下所示: |
|||
|
|||
<img src="/img/tutorial/websockets/image01.png"> |
|||
|
|||
您可以在输入框中输入消息并发送: |
|||
|
|||
<img src="/img/tutorial/websockets/image02.png"> |
|||
|
|||
您的 **FastAPI** 应用程序将回复: |
|||
|
|||
<img src="/img/tutorial/websockets/image03.png"> |
|||
|
|||
您可以发送(和接收)多条消息: |
|||
|
|||
<img src="/img/tutorial/websockets/image04.png"> |
|||
|
|||
所有这些消息都将使用同一个 WebSocket 连 |
|||
|
|||
接。 |
|||
|
|||
## 使用 `Depends` 和其他依赖项 |
|||
|
|||
在 WebSocket 端点中,您可以从 `fastapi` 导入并使用以下内容: |
|||
|
|||
* `Depends` |
|||
* `Security` |
|||
* `Cookie` |
|||
* `Header` |
|||
* `Path` |
|||
* `Query` |
|||
|
|||
它们的工作方式与其他 FastAPI 端点/ *路径操作* 相同: |
|||
|
|||
=== "Python 3.10+" |
|||
|
|||
```Python hl_lines="68-69 82" |
|||
{!> ../../../docs_src/websockets/tutorial002_an_py310.py!} |
|||
``` |
|||
|
|||
=== "Python 3.9+" |
|||
|
|||
```Python hl_lines="68-69 82" |
|||
{!> ../../../docs_src/websockets/tutorial002_an_py39.py!} |
|||
``` |
|||
|
|||
=== "Python 3.6+" |
|||
|
|||
```Python hl_lines="69-70 83" |
|||
{!> ../../../docs_src/websockets/tutorial002_an.py!} |
|||
``` |
|||
|
|||
=== "Python 3.10+ 非带注解版本" |
|||
|
|||
!!! tip |
|||
如果可能,请尽量使用 `Annotated` 版本。 |
|||
|
|||
```Python hl_lines="66-67 79" |
|||
{!> ../../../docs_src/websockets/tutorial002_py310.py!} |
|||
``` |
|||
|
|||
=== "Python 3.6+ 非带注解版本" |
|||
|
|||
!!! tip |
|||
如果可能,请尽量使用 `Annotated` 版本。 |
|||
|
|||
```Python hl_lines="68-69 81" |
|||
{!> ../../../docs_src/websockets/tutorial002.py!} |
|||
``` |
|||
|
|||
!!! info |
|||
由于这是一个 WebSocket,抛出 `HTTPException` 并不是很合理,而是抛出 `WebSocketException`。 |
|||
|
|||
您可以使用<a href="https://tools.ietf.org/html/rfc6455#section-7.4.1" class="external-link" target="_blank">规范中定义的有效代码</a>。 |
|||
|
|||
### 尝试带有依赖项的 WebSockets |
|||
|
|||
如果您的文件名为 `main.py`,请使用以下命令运行应用程序: |
|||
|
|||
```console |
|||
$ uvicorn main:app --reload |
|||
|
|||
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) |
|||
``` |
|||
|
|||
在浏览器中打开 <a href="http://127.0.0.1:8000" class="external-link" target="_blank">http://127.0.0.1:8000</a>。 |
|||
|
|||
在页面中,您可以设置: |
|||
|
|||
* "Item ID",用于路径。 |
|||
* "Token",作为查询参数。 |
|||
|
|||
!!! tip |
|||
注意,查询参数 `token` 将由依赖项处理。 |
|||
|
|||
通过这样,您可以连接 WebSocket,然后发送和接收消息: |
|||
|
|||
<img src="/img/tutorial/websockets/image05.png"> |
|||
|
|||
## 处理断开连接和多个客户端 |
|||
|
|||
当 WebSocket 连接关闭时,`await websocket.receive_text()` 将引发 `WebSocketDisconnect` 异常,您可以捕获并处理该异常,就像本示例中的示例一样。 |
|||
|
|||
=== "Python 3.9+" |
|||
|
|||
```Python hl_lines="79-81" |
|||
{!> ../../../docs_src/websockets/tutorial003_py39.py!} |
|||
``` |
|||
|
|||
=== "Python 3.6+" |
|||
|
|||
```Python hl_lines="81-83" |
|||
{!> ../../../docs_src/websockets/tutorial003.py!} |
|||
``` |
|||
|
|||
尝试以下操作: |
|||
|
|||
* 使用多个浏览器选项卡打开应用程序。 |
|||
* 从这些选项卡中发送消息。 |
|||
* 然后关闭其中一个选项卡。 |
|||
|
|||
这将引发 `WebSocketDisconnect` 异常,并且所有其他客户端都会收到类似以下的消息: |
|||
|
|||
``` |
|||
Client #1596980209979 left the chat |
|||
``` |
|||
|
|||
!!! tip |
|||
上面的应用程序是一个最小和简单的示例,用于演示如何处理和向多个 WebSocket 连接广播消息。 |
|||
|
|||
但请记住,由于所有内容都在内存中以单个列表的形式处理,因此它只能在进程运行时工作,并且只能使用单个进程。 |
|||
|
|||
如果您需要与 FastAPI 集成更简单但更强大的功能,支持 Redis、PostgreSQL 或其他功能,请查看 [encode/broadcaster](https://github.com/encode/broadcaster)。 |
|||
|
|||
## 更多信息 |
|||
|
|||
要了解更多选项,请查看 Starlette 的文档: |
|||
|
|||
* [WebSocket 类](https://www.starlette.io/websockets/) |
|||
* [基于类的 WebSocket 处理](https://www.starlette.io/endpoints/#websocketendpoint)。 |
@ -0,0 +1,212 @@ |
|||
# 测试 |
|||
|
|||
感谢 <a href="https://www.starlette.io/testclient/" class="external-link" target="_blank">Starlette</a>,测试**FastAPI** 应用轻松又愉快。 |
|||
|
|||
它基于 <a href="https://www.python-httpx.org" class="external-link" target="_blank">HTTPX</a>, 而HTTPX又是基于Requests设计的,所以很相似且易懂。 |
|||
|
|||
有了它,你可以直接与**FastAPI**一起使用 <a href="https://docs.pytest.org/" class="external-link" target="_blank">pytest</a>。 |
|||
|
|||
## 使用 `TestClient` |
|||
|
|||
!!! 信息 |
|||
要使用 `TestClient`,先要安装 <a href="https://www.python-httpx.org" class="external-link" target="_blank">`httpx`</a>. |
|||
|
|||
例:`pip install httpx`. |
|||
|
|||
导入 `TestClient`. |
|||
|
|||
通过传入你的**FastAPI**应用创建一个 `TestClient` 。 |
|||
|
|||
创建名字以 `test_` 开头的函数(这是标准的 `pytest` 约定)。 |
|||
|
|||
像使用 `httpx` 那样使用 `TestClient` 对象。 |
|||
|
|||
为你需要检查的地方用标准的Python表达式写个简单的 `assert` 语句(重申,标准的`pytest`)。 |
|||
|
|||
```Python hl_lines="2 12 15-18" |
|||
{!../../../docs_src/app_testing/tutorial001.py!} |
|||
``` |
|||
|
|||
!!! 提示 |
|||
注意测试函数是普通的 `def`,不是 `async def`。 |
|||
|
|||
还有client的调用也是普通的调用,不是用 `await`。 |
|||
|
|||
这让你可以直接使用 `pytest` 而不会遇到麻烦。 |
|||
|
|||
!!! note "技术细节" |
|||
你也可以用 `from starlette.testclient import TestClient`。 |
|||
|
|||
**FastAPI** 提供了和 `starlette.testclient` 一样的 `fastapi.testclient`,只是为了方便开发者。但它直接来自Starlette。 |
|||
|
|||
!!! 提示 |
|||
除了发送请求之外,如果你还想测试时在FastAPI应用中调用 `async` 函数(例如异步数据库函数), 可以在高级教程中看下 [Async Tests](../advanced/async-tests.md){.internal-link target=_blank} 。 |
|||
|
|||
## 分离测试 |
|||
|
|||
在实际应用中,你可能会把你的测试放在另一个文件里。 |
|||
|
|||
您的**FastAPI**应用程序也可能由一些文件/模块组成等等。 |
|||
|
|||
### **FastAPI** app 文件 |
|||
|
|||
假设你有一个像 [更大的应用](./bigger-applications.md){.internal-link target=_blank} 中所描述的文件结构: |
|||
|
|||
``` |
|||
. |
|||
├── app |
|||
│ ├── __init__.py |
|||
│ └── main.py |
|||
``` |
|||
|
|||
在 `main.py` 文件中你有一个 **FastAPI** app: |
|||
|
|||
|
|||
```Python |
|||
{!../../../docs_src/app_testing/main.py!} |
|||
``` |
|||
|
|||
### 测试文件 |
|||
|
|||
然后你会有一个包含测试的文件 `test_main.py` 。app可以像Python包那样存在(一样是目录,但有个 `__init__.py` 文件): |
|||
|
|||
``` hl_lines="5" |
|||
. |
|||
├── app |
|||
│ ├── __init__.py |
|||
│ ├── main.py |
|||
│ └── test_main.py |
|||
``` |
|||
|
|||
因为这文件在同一个包中,所以你可以通过相对导入从 `main` 模块(`main.py`)导入`app`对象: |
|||
|
|||
```Python hl_lines="3" |
|||
{!../../../docs_src/app_testing/test_main.py!} |
|||
``` |
|||
|
|||
...然后测试代码和之前一样的。 |
|||
|
|||
## 测试:扩展示例 |
|||
|
|||
现在让我们扩展这个例子,并添加更多细节,看下如何测试不同部分。 |
|||
|
|||
### 扩展后的 **FastAPI** app 文件 |
|||
|
|||
让我们继续之前的文件结构: |
|||
|
|||
``` |
|||
. |
|||
├── app |
|||
│ ├── __init__.py |
|||
│ ├── main.py |
|||
│ └── test_main.py |
|||
``` |
|||
|
|||
假设现在包含**FastAPI** app的文件 `main.py` 有些其他**路径操作**。 |
|||
|
|||
有个 `GET` 操作会返回错误。 |
|||
|
|||
有个 `POST` 操作会返回一些错误。 |
|||
|
|||
所有*路径操作* 都需要一个`X-Token` 头。 |
|||
|
|||
=== "Python 3.10+" |
|||
|
|||
```Python |
|||
{!> ../../../docs_src/app_testing/app_b_an_py310/main.py!} |
|||
``` |
|||
|
|||
=== "Python 3.9+" |
|||
|
|||
```Python |
|||
{!> ../../../docs_src/app_testing/app_b_an_py39/main.py!} |
|||
``` |
|||
|
|||
=== "Python 3.6+" |
|||
|
|||
```Python |
|||
{!> ../../../docs_src/app_testing/app_b_an/main.py!} |
|||
``` |
|||
|
|||
=== "Python 3.10+ non-Annotated" |
|||
|
|||
!!! tip |
|||
Prefer to use the `Annotated` version if possible. |
|||
|
|||
```Python |
|||
{!> ../../../docs_src/app_testing/app_b_py310/main.py!} |
|||
``` |
|||
|
|||
=== "Python 3.6+ non-Annotated" |
|||
|
|||
!!! tip |
|||
Prefer to use the `Annotated` version if possible. |
|||
|
|||
```Python |
|||
{!> ../../../docs_src/app_testing/app_b/main.py!} |
|||
``` |
|||
|
|||
### 扩展后的测试文件 |
|||
|
|||
然后您可以使用扩展后的测试更新`test_main.py`: |
|||
|
|||
```Python |
|||
{!> ../../../docs_src/app_testing/app_b/test_main.py!} |
|||
``` |
|||
|
|||
每当你需要客户端在请求中传递信息,但你不知道如何传递时,你可以通过搜索(谷歌)如何用 `httpx`做,或者是用 `requests` 做,毕竟HTTPX的设计是基于Requests的设计的。 |
|||
|
|||
接着只需在测试中同样操作。 |
|||
|
|||
示例: |
|||
|
|||
* 传一个*路径* 或*查询* 参数,添加到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>. |
|||
|
|||
!!! 信息 |
|||
注意 `TestClient` 接收可以被转化为JSON的数据,而不是Pydantic模型。 |
|||
|
|||
如果你在测试中有一个Pydantic模型,并且你想在测试时发送它的数据给应用,你可以使用在[JSON Compatible Encoder](encoder.md){.internal-link target=_blank}介绍的`jsonable_encoder` 。 |
|||
|
|||
## 运行起来 |
|||
|
|||
之后,你只需要安装 `pytest`: |
|||
|
|||
<div class="termy"> |
|||
|
|||
```console |
|||
$ pip install pytest |
|||
|
|||
---> 100% |
|||
``` |
|||
|
|||
</div> |
|||
|
|||
他会自动检测文件和测试,执行测试,然后向你报告结果。 |
|||
|
|||
执行测试: |
|||
|
|||
<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> |
@ -1,5 +1,5 @@ |
|||
-e .[all] |
|||
-r requirements-tests.txt |
|||
-r requirements-docs.txt |
|||
uvicorn[standard] >=0.12.0,<0.21.0 |
|||
pre-commit >=2.17.0,<3.0.0 |
|||
uvicorn[standard] >=0.12.0,<0.23.0 |
|||
pre-commit >=2.17.0,<4.0.0 |
|||
|
@ -1,11 +0,0 @@ |
|||
#!/usr/bin/env bash |
|||
|
|||
set -x |
|||
set -e |
|||
|
|||
cd ./site |
|||
|
|||
if [ -f docs.zip ]; then |
|||
rm -rf docs.zip |
|||
fi |
|||
zip -r docs.zip ./ |
Loading…
Reference in new issue