From f61217a18a011c597f109be4e6033014c7ff92e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 24 Jun 2023 01:51:56 +0200 Subject: [PATCH 01/58] =?UTF-8?q?=F0=9F=94=A5=20Remove=20old=20internal=20?= =?UTF-8?q?GitHub=20Action=20watch-previews=20that=20is=20no=20longer=20ne?= =?UTF-8?q?eded=20(#9730)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/actions/watch-previews/Dockerfile | 7 -- .github/actions/watch-previews/action.yml | 10 -- .github/actions/watch-previews/app/main.py | 101 --------------------- 3 files changed, 118 deletions(-) delete mode 100644 .github/actions/watch-previews/Dockerfile delete mode 100644 .github/actions/watch-previews/action.yml delete mode 100644 .github/actions/watch-previews/app/main.py diff --git a/.github/actions/watch-previews/Dockerfile b/.github/actions/watch-previews/Dockerfile deleted file mode 100644 index b8cc64d94..000000000 --- a/.github/actions/watch-previews/Dockerfile +++ /dev/null @@ -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"] diff --git a/.github/actions/watch-previews/action.yml b/.github/actions/watch-previews/action.yml deleted file mode 100644 index 5c09ad487..000000000 --- a/.github/actions/watch-previews/action.yml +++ /dev/null @@ -1,10 +0,0 @@ -name: "Watch docs previews in PRs" -description: "Check PRs and trigger new docs deploys" -author: "Sebastián Ramírez " -inputs: - token: - description: 'Token for the repo. Can be passed in using {{ secrets.GITHUB_TOKEN }}' - required: true -runs: - using: 'docker' - image: 'Dockerfile' diff --git a/.github/actions/watch-previews/app/main.py b/.github/actions/watch-previews/app/main.py deleted file mode 100644 index 51285d02b..000000000 --- a/.github/actions/watch-previews/app/main.py +++ /dev/null @@ -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") From 2848951082cf3301818b3a120b276dff91646458 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 23 Jun 2023 23:52:34 +0000 Subject: [PATCH 02/58] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 74b7f25ca..6f43126b0 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔥 Remove old internal GitHub Action watch-previews that is no longer needed. PR [#9730](https://github.com/tiangolo/fastapi/pull/9730) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Upgrade MkDocs and MkDocs Material. PR [#9729](https://github.com/tiangolo/fastapi/pull/9729) by [@tiangolo](https://github.com/tiangolo). * 👷 Build and deploy docs only on docs changes. PR [#9728](https://github.com/tiangolo/fastapi/pull/9728) by [@tiangolo](https://github.com/tiangolo). From c09e5cdfa70a6c0226e10d93595717bf3c0feedb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 24 Jun 2023 02:00:12 +0200 Subject: [PATCH 03/58] =?UTF-8?q?=F0=9F=91=B7=20Refactor=20Docs=20CI,=20ru?= =?UTF-8?q?n=20in=20multiple=20workers=20with=20a=20dynamic=20matrix=20to?= =?UTF-8?q?=20optimize=20speed=20(#9732)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build-docs.yml | 73 ++++++--- .../{preview-docs.yml => deploy-docs.yml} | 18 +-- .gitignore | 1 + scripts/docs.py | 152 +++++++++--------- scripts/zip-docs.sh | 11 -- 5 files changed, 142 insertions(+), 113 deletions(-) rename .github/workflows/{preview-docs.yml => deploy-docs.yml} (79%) delete mode 100644 scripts/zip-docs.sh diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index fb1fa6f09..c2880ef71 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -23,15 +23,45 @@ jobs: id: filter with: filters: | - docs: - - README.md - - docs/** - - docs_src/** - - requirements-docs.txt + 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 + 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: @@ -53,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/actions-netlify@v2.0.0 + 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 diff --git a/.github/workflows/preview-docs.yml b/.github/workflows/deploy-docs.yml similarity index 79% rename from .github/workflows/preview-docs.yml rename to .github/workflows/deploy-docs.yml index da98f5d2b..312d835af 100644 --- a/.github/workflows/preview-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -1,4 +1,4 @@ -name: Preview Docs +name: Deploy Docs on: workflow_run: workflows: @@ -7,9 +7,13 @@ 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: | @@ -23,21 +27,15 @@ jobs: 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 - if: steps.download.outputs.found_artifact == 'true' - 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/actions-netlify@v2.0.0 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: diff --git a/.gitignore b/.gitignore index a26bb5cd6..3cb64c047 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ Pipfile.lock env3.* env docs_build +site_build venv docs.zip archive.zip diff --git a/scripts/docs.py b/scripts/docs.py index e0953b8ed..c464f8dbe 100644 --- a/scripts/docs.py +++ b/scripts/docs.py @@ -1,3 +1,4 @@ +import json import os import re import shutil @@ -133,75 +134,83 @@ def build_lang( build_lang_path = build_dir_path / lang en_lang_path = Path("docs/en") site_path = Path("site").absolute() + build_site_path = Path("site_build").absolute() + build_site_dist_path = build_site_path / lang if lang == "en": dist_path = site_path else: dist_path: Path = site_path / lang shutil.rmtree(build_lang_path, ignore_errors=True) shutil.copytree(lang_path, build_lang_path) - shutil.copytree(en_docs_path / "data", build_lang_path / "data") - overrides_src = en_docs_path / "overrides" - overrides_dest = build_lang_path / "overrides" - for path in overrides_src.iterdir(): - dest_path = overrides_dest / path.name - if not dest_path.exists(): - shutil.copy(path, dest_path) - en_config_path: Path = en_lang_path / mkdocs_name - en_config: dict = mkdocs.utils.yaml_load(en_config_path.read_text(encoding="utf-8")) - nav = en_config["nav"] - lang_config_path: Path = lang_path / mkdocs_name - lang_config: dict = mkdocs.utils.yaml_load( - lang_config_path.read_text(encoding="utf-8") - ) - lang_nav = lang_config["nav"] - # Exclude first 2 entries FastAPI and Languages, for custom handling - use_nav = nav[2:] - lang_use_nav = lang_nav[2:] - file_to_nav = get_file_to_nav_map(use_nav) - sections = get_sections(use_nav) - lang_file_to_nav = get_file_to_nav_map(lang_use_nav) - use_lang_file_to_nav = get_file_to_nav_map(lang_use_nav) - for file in file_to_nav: - file_path = Path(file) - lang_file_path: Path = build_lang_path / "docs" / file_path - en_file_path: Path = en_lang_path / "docs" / file_path - lang_file_path.parent.mkdir(parents=True, exist_ok=True) - if not lang_file_path.is_file(): - en_text = en_file_path.read_text(encoding="utf-8") - lang_text = get_text_with_translate_missing(en_text) - lang_file_path.write_text(lang_text, encoding="utf-8") - file_key = file_to_nav[file] - use_lang_file_to_nav[file] = file_key - if file_key: - composite_key = () - new_key = () - for key_part in file_key: - composite_key += (key_part,) - key_first_file = sections[composite_key] - if key_first_file in lang_file_to_nav: - new_key = lang_file_to_nav[key_first_file] - else: - new_key += (key_part,) - use_lang_file_to_nav[file] = new_key - key_to_section = {(): []} - for file, orig_file_key in file_to_nav.items(): - if file in use_lang_file_to_nav: - file_key = use_lang_file_to_nav[file] - else: - file_key = orig_file_key - section = get_key_section(key_to_section=key_to_section, key=file_key) - section.append(file) - new_nav = key_to_section[()] - export_lang_nav = [lang_nav[0], nav[1]] + new_nav - lang_config["nav"] = export_lang_nav - build_lang_config_path: Path = build_lang_path / mkdocs_name - build_lang_config_path.write_text( - yaml.dump(lang_config, sort_keys=False, width=200, allow_unicode=True), - encoding="utf-8", - ) + if not lang == "en": + shutil.copytree(en_docs_path / "data", build_lang_path / "data") + overrides_src = en_docs_path / "overrides" + overrides_dest = build_lang_path / "overrides" + for path in overrides_src.iterdir(): + dest_path = overrides_dest / path.name + if not dest_path.exists(): + shutil.copy(path, dest_path) + en_config_path: Path = en_lang_path / mkdocs_name + en_config: dict = mkdocs.utils.yaml_load( + en_config_path.read_text(encoding="utf-8") + ) + nav = en_config["nav"] + lang_config_path: Path = lang_path / mkdocs_name + lang_config: dict = mkdocs.utils.yaml_load( + lang_config_path.read_text(encoding="utf-8") + ) + lang_nav = lang_config["nav"] + # Exclude first 2 entries FastAPI and Languages, for custom handling + use_nav = nav[2:] + lang_use_nav = lang_nav[2:] + file_to_nav = get_file_to_nav_map(use_nav) + sections = get_sections(use_nav) + lang_file_to_nav = get_file_to_nav_map(lang_use_nav) + use_lang_file_to_nav = get_file_to_nav_map(lang_use_nav) + for file in file_to_nav: + file_path = Path(file) + lang_file_path: Path = build_lang_path / "docs" / file_path + en_file_path: Path = en_lang_path / "docs" / file_path + lang_file_path.parent.mkdir(parents=True, exist_ok=True) + if not lang_file_path.is_file(): + en_text = en_file_path.read_text(encoding="utf-8") + lang_text = get_text_with_translate_missing(en_text) + lang_file_path.write_text(lang_text, encoding="utf-8") + file_key = file_to_nav[file] + use_lang_file_to_nav[file] = file_key + if file_key: + composite_key = () + new_key = () + for key_part in file_key: + composite_key += (key_part,) + key_first_file = sections[composite_key] + if key_first_file in lang_file_to_nav: + new_key = lang_file_to_nav[key_first_file] + else: + new_key += (key_part,) + use_lang_file_to_nav[file] = new_key + key_to_section = {(): []} + for file, orig_file_key in file_to_nav.items(): + if file in use_lang_file_to_nav: + file_key = use_lang_file_to_nav[file] + else: + file_key = orig_file_key + section = get_key_section(key_to_section=key_to_section, key=file_key) + section.append(file) + new_nav = key_to_section[()] + export_lang_nav = [lang_nav[0], nav[1]] + new_nav + lang_config["nav"] = export_lang_nav + build_lang_config_path: Path = build_lang_path / mkdocs_name + build_lang_config_path.write_text( + yaml.dump(lang_config, sort_keys=False, width=200, allow_unicode=True), + encoding="utf-8", + ) current_dir = os.getcwd() os.chdir(build_lang_path) - subprocess.run(["mkdocs", "build", "--site-dir", dist_path], check=True) + shutil.rmtree(build_site_dist_path, ignore_errors=True) + shutil.rmtree(dist_path, ignore_errors=True) + subprocess.run(["mkdocs", "build", "--site-dir", build_site_dist_path], check=True) + shutil.copytree(build_site_dist_path, dist_path, dirs_exist_ok=True) os.chdir(current_dir) typer.secho(f"Successfully built docs for: {lang}", color=typer.colors.GREEN) @@ -271,18 +280,8 @@ def build_all(): Build mkdocs site for en, and then build each language inside, end result is located at directory ./site/ with each language inside. """ - site_path = Path("site").absolute() update_languages(lang=None) - current_dir = os.getcwd() - os.chdir(en_docs_path) - typer.echo("Building docs for: en") - subprocess.run(["mkdocs", "build", "--site-dir", site_path], check=True) - os.chdir(current_dir) - langs = [] - for lang in get_lang_paths(): - if lang == en_docs_path or not lang.is_dir(): - continue - langs.append(lang.name) + langs = [lang.name for lang in get_lang_paths() if lang.is_dir()] cpu_count = os.cpu_count() or 1 process_pool_size = cpu_count * 4 typer.echo(f"Using process pool size: {process_pool_size}") @@ -397,6 +396,15 @@ def update_config(lang: str): ) +@app.command() +def langs_json(): + langs = [] + for lang_path in get_lang_paths(): + if lang_path.is_dir(): + langs.append(lang_path.name) + print(json.dumps(langs)) + + def get_key_section( *, key_to_section: Dict[Tuple[str, ...], list], key: Tuple[str, ...] ) -> list: diff --git a/scripts/zip-docs.sh b/scripts/zip-docs.sh deleted file mode 100644 index 47c3b0977..000000000 --- a/scripts/zip-docs.sh +++ /dev/null @@ -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 ./ From 7d865c9487eb8d7e6bcb508b933b17a5a6e8586b Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 24 Jun 2023 00:00:47 +0000 Subject: [PATCH 04/58] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 6f43126b0..0a5f51e98 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 👷 Refactor Docs CI, run in multiple workers with a dynamic matrix to optimize speed. PR [#9732](https://github.com/tiangolo/fastapi/pull/9732) by [@tiangolo](https://github.com/tiangolo). * 🔥 Remove old internal GitHub Action watch-previews that is no longer needed. PR [#9730](https://github.com/tiangolo/fastapi/pull/9730) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Upgrade MkDocs and MkDocs Material. PR [#9729](https://github.com/tiangolo/fastapi/pull/9729) by [@tiangolo](https://github.com/tiangolo). * 👷 Build and deploy docs only on docs changes. PR [#9728](https://github.com/tiangolo/fastapi/pull/9728) by [@tiangolo](https://github.com/tiangolo). From dd590f46ad932533bd6f28ea388a6e687683fb1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 24 Jun 2023 14:28:43 +0200 Subject: [PATCH 05/58] =?UTF-8?q?=F0=9F=94=A7=20Update=20MkDocs=20for=20ot?= =?UTF-8?q?her=20languages=20(#9734)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/az/mkdocs.yml | 4 ++-- docs/cs/mkdocs.yml | 10 ++++++++-- docs/de/mkdocs.yml | 4 ++-- docs/em/mkdocs.yml | 7 +++++-- docs/es/mkdocs.yml | 4 ++-- docs/fa/mkdocs.yml | 4 ++-- docs/fr/mkdocs.yml | 4 ++-- docs/he/mkdocs.yml | 4 ++-- docs/hy/mkdocs.yml | 4 ++-- docs/id/mkdocs.yml | 4 ++-- docs/it/mkdocs.yml | 4 ++-- docs/ja/mkdocs.yml | 4 ++-- docs/ko/mkdocs.yml | 4 ++-- docs/lo/mkdocs.yml | 7 +++++-- docs/nl/mkdocs.yml | 4 ++-- docs/pl/mkdocs.yml | 4 ++-- docs/pt/mkdocs.yml | 4 ++-- docs/ru/mkdocs.yml | 4 ++-- docs/sq/mkdocs.yml | 4 ++-- docs/sv/mkdocs.yml | 4 ++-- docs/ta/mkdocs.yml | 4 ++-- docs/tr/mkdocs.yml | 4 ++-- docs/uk/mkdocs.yml | 4 ++-- docs/zh/mkdocs.yml | 4 ++-- 24 files changed, 60 insertions(+), 48 deletions(-) diff --git a/docs/az/mkdocs.yml b/docs/az/mkdocs.yml index 1d2930494..b846b91f8 100644 --- a/docs/az/mkdocs.yml +++ b/docs/az/mkdocs.yml @@ -11,14 +11,14 @@ theme: accent: amber toggle: icon: material/lightbulb - name: Switch to light mode + name: Switch to dark mode - media: '(prefers-color-scheme: dark)' scheme: slate primary: teal accent: amber toggle: icon: material/lightbulb-outline - name: Switch to dark mode + name: Switch to light mode features: - search.suggest - search.highlight diff --git a/docs/cs/mkdocs.yml b/docs/cs/mkdocs.yml index 539d7d65d..c303d8f6a 100644 --- a/docs/cs/mkdocs.yml +++ b/docs/cs/mkdocs.yml @@ -11,14 +11,14 @@ theme: accent: amber toggle: icon: material/lightbulb - name: Switch to light mode + name: Switch to dark mode - media: '(prefers-color-scheme: dark)' scheme: slate primary: teal accent: amber toggle: icon: material/lightbulb-outline - name: Switch to dark mode + name: Switch to light mode features: - search.suggest - search.highlight @@ -42,6 +42,7 @@ nav: - az: /az/ - cs: /cs/ - de: /de/ + - em: /em/ - es: /es/ - fa: /fa/ - fr: /fr/ @@ -51,6 +52,7 @@ nav: - it: /it/ - ja: /ja/ - ko: /ko/ + - lo: /lo/ - nl: /nl/ - pl: /pl/ - pt: /pt/ @@ -108,6 +110,8 @@ extra: name: cs - link: /de/ name: de + - link: /em/ + name: 😉 - link: /es/ name: es - español - link: /fa/ @@ -126,6 +130,8 @@ extra: name: ja - 日本語 - link: /ko/ name: ko - 한국어 + - link: /lo/ + name: lo - ພາສາລາວ - link: /nl/ name: nl - link: /pl/ diff --git a/docs/de/mkdocs.yml b/docs/de/mkdocs.yml index e475759a8..4be982509 100644 --- a/docs/de/mkdocs.yml +++ b/docs/de/mkdocs.yml @@ -11,14 +11,14 @@ theme: accent: amber toggle: icon: material/lightbulb - name: Switch to light mode + name: Switch to dark mode - media: '(prefers-color-scheme: dark)' scheme: slate primary: teal accent: amber toggle: icon: material/lightbulb-outline - name: Switch to dark mode + name: Switch to light mode features: - search.suggest - search.highlight diff --git a/docs/em/mkdocs.yml b/docs/em/mkdocs.yml index 2c48de93a..bceef0d65 100644 --- a/docs/em/mkdocs.yml +++ b/docs/em/mkdocs.yml @@ -11,14 +11,14 @@ theme: accent: amber toggle: icon: material/lightbulb - name: Switch to light mode + name: Switch to dark mode - media: '(prefers-color-scheme: dark)' scheme: slate primary: teal accent: amber toggle: icon: material/lightbulb-outline - name: Switch to dark mode + name: Switch to light mode features: - search.suggest - search.highlight @@ -40,6 +40,7 @@ nav: - Languages: - en: / - az: /az/ + - cs: /cs/ - de: /de/ - em: /em/ - es: /es/ @@ -212,6 +213,8 @@ extra: name: en - English - link: /az/ name: az + - link: /cs/ + name: cs - link: /de/ name: de - link: /em/ diff --git a/docs/es/mkdocs.yml b/docs/es/mkdocs.yml index 8152c91e3..e01f55b3a 100644 --- a/docs/es/mkdocs.yml +++ b/docs/es/mkdocs.yml @@ -11,14 +11,14 @@ theme: accent: amber toggle: icon: material/lightbulb - name: Switch to light mode + name: Switch to dark mode - media: '(prefers-color-scheme: dark)' scheme: slate primary: teal accent: amber toggle: icon: material/lightbulb-outline - name: Switch to dark mode + name: Switch to light mode features: - search.suggest - search.highlight diff --git a/docs/fa/mkdocs.yml b/docs/fa/mkdocs.yml index 2a966f664..5c5b5e3e1 100644 --- a/docs/fa/mkdocs.yml +++ b/docs/fa/mkdocs.yml @@ -11,14 +11,14 @@ theme: accent: amber toggle: icon: material/lightbulb - name: Switch to light mode + name: Switch to dark mode - media: '(prefers-color-scheme: dark)' scheme: slate primary: teal accent: amber toggle: icon: material/lightbulb-outline - name: Switch to dark mode + name: Switch to light mode features: - search.suggest - search.highlight diff --git a/docs/fr/mkdocs.yml b/docs/fr/mkdocs.yml index 0b73d3cae..5714a74cb 100644 --- a/docs/fr/mkdocs.yml +++ b/docs/fr/mkdocs.yml @@ -11,14 +11,14 @@ theme: accent: amber toggle: icon: material/lightbulb - name: Switch to light mode + name: Switch to dark mode - media: '(prefers-color-scheme: dark)' scheme: slate primary: teal accent: amber toggle: icon: material/lightbulb-outline - name: Switch to dark mode + name: Switch to light mode features: - search.suggest - search.highlight diff --git a/docs/he/mkdocs.yml b/docs/he/mkdocs.yml index b8a674812..39e533342 100644 --- a/docs/he/mkdocs.yml +++ b/docs/he/mkdocs.yml @@ -11,14 +11,14 @@ theme: accent: amber toggle: icon: material/lightbulb - name: Switch to light mode + name: Switch to dark mode - media: '(prefers-color-scheme: dark)' scheme: slate primary: teal accent: amber toggle: icon: material/lightbulb-outline - name: Switch to dark mode + name: Switch to light mode features: - search.suggest - search.highlight diff --git a/docs/hy/mkdocs.yml b/docs/hy/mkdocs.yml index 19d747c31..64e5ab876 100644 --- a/docs/hy/mkdocs.yml +++ b/docs/hy/mkdocs.yml @@ -11,14 +11,14 @@ theme: accent: amber toggle: icon: material/lightbulb - name: Switch to light mode + name: Switch to dark mode - media: '(prefers-color-scheme: dark)' scheme: slate primary: teal accent: amber toggle: icon: material/lightbulb-outline - name: Switch to dark mode + name: Switch to light mode features: - search.suggest - search.highlight diff --git a/docs/id/mkdocs.yml b/docs/id/mkdocs.yml index 460cb6914..acd93df48 100644 --- a/docs/id/mkdocs.yml +++ b/docs/id/mkdocs.yml @@ -11,14 +11,14 @@ theme: accent: amber toggle: icon: material/lightbulb - name: Switch to light mode + name: Switch to dark mode - media: '(prefers-color-scheme: dark)' scheme: slate primary: teal accent: amber toggle: icon: material/lightbulb-outline - name: Switch to dark mode + name: Switch to light mode features: - search.suggest - search.highlight diff --git a/docs/it/mkdocs.yml b/docs/it/mkdocs.yml index b3a48482b..4074dff5a 100644 --- a/docs/it/mkdocs.yml +++ b/docs/it/mkdocs.yml @@ -11,14 +11,14 @@ theme: accent: amber toggle: icon: material/lightbulb - name: Switch to light mode + name: Switch to dark mode - media: '(prefers-color-scheme: dark)' scheme: slate primary: teal accent: amber toggle: icon: material/lightbulb-outline - name: Switch to dark mode + name: Switch to light mode features: - search.suggest - search.highlight diff --git a/docs/ja/mkdocs.yml b/docs/ja/mkdocs.yml index 91b9a6658..56dc4ff4b 100644 --- a/docs/ja/mkdocs.yml +++ b/docs/ja/mkdocs.yml @@ -11,14 +11,14 @@ theme: accent: amber toggle: icon: material/lightbulb - name: Switch to light mode + name: Switch to dark mode - media: '(prefers-color-scheme: dark)' scheme: slate primary: teal accent: amber toggle: icon: material/lightbulb-outline - name: Switch to dark mode + name: Switch to light mode features: - search.suggest - search.highlight diff --git a/docs/ko/mkdocs.yml b/docs/ko/mkdocs.yml index aec1c7569..d91f0dd12 100644 --- a/docs/ko/mkdocs.yml +++ b/docs/ko/mkdocs.yml @@ -11,14 +11,14 @@ theme: accent: amber toggle: icon: material/lightbulb - name: Switch to light mode + name: Switch to dark mode - media: '(prefers-color-scheme: dark)' scheme: slate primary: teal accent: amber toggle: icon: material/lightbulb-outline - name: Switch to dark mode + name: Switch to light mode features: - search.suggest - search.highlight diff --git a/docs/lo/mkdocs.yml b/docs/lo/mkdocs.yml index 450ebcd2b..2ec3d6a2f 100644 --- a/docs/lo/mkdocs.yml +++ b/docs/lo/mkdocs.yml @@ -11,14 +11,14 @@ theme: accent: amber toggle: icon: material/lightbulb - name: Switch to light mode + name: Switch to dark mode - media: '(prefers-color-scheme: dark)' scheme: slate primary: teal accent: amber toggle: icon: material/lightbulb-outline - name: Switch to dark mode + name: Switch to light mode features: - search.suggest - search.highlight @@ -40,6 +40,7 @@ nav: - Languages: - en: / - az: /az/ + - cs: /cs/ - de: /de/ - em: /em/ - es: /es/ @@ -105,6 +106,8 @@ extra: name: en - English - link: /az/ name: az + - link: /cs/ + name: cs - link: /de/ name: de - link: /em/ diff --git a/docs/nl/mkdocs.yml b/docs/nl/mkdocs.yml index 96c93abff..52039bbb5 100644 --- a/docs/nl/mkdocs.yml +++ b/docs/nl/mkdocs.yml @@ -11,14 +11,14 @@ theme: accent: amber toggle: icon: material/lightbulb - name: Switch to light mode + name: Switch to dark mode - media: '(prefers-color-scheme: dark)' scheme: slate primary: teal accent: amber toggle: icon: material/lightbulb-outline - name: Switch to dark mode + name: Switch to light mode features: - search.suggest - search.highlight diff --git a/docs/pl/mkdocs.yml b/docs/pl/mkdocs.yml index 5ca1bbfef..3b1e82c66 100644 --- a/docs/pl/mkdocs.yml +++ b/docs/pl/mkdocs.yml @@ -11,14 +11,14 @@ theme: accent: amber toggle: icon: material/lightbulb - name: Switch to light mode + name: Switch to dark mode - media: '(prefers-color-scheme: dark)' scheme: slate primary: teal accent: amber toggle: icon: material/lightbulb-outline - name: Switch to dark mode + name: Switch to light mode features: - search.suggest - search.highlight diff --git a/docs/pt/mkdocs.yml b/docs/pt/mkdocs.yml index 023944618..fc933db94 100644 --- a/docs/pt/mkdocs.yml +++ b/docs/pt/mkdocs.yml @@ -11,14 +11,14 @@ theme: accent: amber toggle: icon: material/lightbulb - name: Switch to light mode + name: Switch to dark mode - media: '(prefers-color-scheme: dark)' scheme: slate primary: teal accent: amber toggle: icon: material/lightbulb-outline - name: Switch to dark mode + name: Switch to light mode features: - search.suggest - search.highlight diff --git a/docs/ru/mkdocs.yml b/docs/ru/mkdocs.yml index 4a7512ac0..dbae5ac95 100644 --- a/docs/ru/mkdocs.yml +++ b/docs/ru/mkdocs.yml @@ -11,14 +11,14 @@ theme: accent: amber toggle: icon: material/lightbulb - name: Switch to light mode + name: Switch to dark mode - media: '(prefers-color-scheme: dark)' scheme: slate primary: teal accent: amber toggle: icon: material/lightbulb-outline - name: Switch to dark mode + name: Switch to light mode features: - search.suggest - search.highlight diff --git a/docs/sq/mkdocs.yml b/docs/sq/mkdocs.yml index 092c50816..d3038644f 100644 --- a/docs/sq/mkdocs.yml +++ b/docs/sq/mkdocs.yml @@ -11,14 +11,14 @@ theme: accent: amber toggle: icon: material/lightbulb - name: Switch to light mode + name: Switch to dark mode - media: '(prefers-color-scheme: dark)' scheme: slate primary: teal accent: amber toggle: icon: material/lightbulb-outline - name: Switch to dark mode + name: Switch to light mode features: - search.suggest - search.highlight diff --git a/docs/sv/mkdocs.yml b/docs/sv/mkdocs.yml index 215b32f18..1409b49dc 100644 --- a/docs/sv/mkdocs.yml +++ b/docs/sv/mkdocs.yml @@ -11,14 +11,14 @@ theme: accent: amber toggle: icon: material/lightbulb - name: Switch to light mode + name: Switch to dark mode - media: '(prefers-color-scheme: dark)' scheme: slate primary: teal accent: amber toggle: icon: material/lightbulb-outline - name: Switch to dark mode + name: Switch to light mode features: - search.suggest - search.highlight diff --git a/docs/ta/mkdocs.yml b/docs/ta/mkdocs.yml index 4b96d2cad..5c63d659f 100644 --- a/docs/ta/mkdocs.yml +++ b/docs/ta/mkdocs.yml @@ -11,14 +11,14 @@ theme: accent: amber toggle: icon: material/lightbulb - name: Switch to light mode + name: Switch to dark mode - media: '(prefers-color-scheme: dark)' scheme: slate primary: teal accent: amber toggle: icon: material/lightbulb-outline - name: Switch to dark mode + name: Switch to light mode features: - search.suggest - search.highlight diff --git a/docs/tr/mkdocs.yml b/docs/tr/mkdocs.yml index 5811f793e..125341fc6 100644 --- a/docs/tr/mkdocs.yml +++ b/docs/tr/mkdocs.yml @@ -11,14 +11,14 @@ theme: accent: amber toggle: icon: material/lightbulb - name: Switch to light mode + name: Switch to dark mode - media: '(prefers-color-scheme: dark)' scheme: slate primary: teal accent: amber toggle: icon: material/lightbulb-outline - name: Switch to dark mode + name: Switch to light mode features: - search.suggest - search.highlight diff --git a/docs/uk/mkdocs.yml b/docs/uk/mkdocs.yml index 5e22570b1..33e6fff40 100644 --- a/docs/uk/mkdocs.yml +++ b/docs/uk/mkdocs.yml @@ -11,14 +11,14 @@ theme: accent: amber toggle: icon: material/lightbulb - name: Switch to light mode + name: Switch to dark mode - media: '(prefers-color-scheme: dark)' scheme: slate primary: teal accent: amber toggle: icon: material/lightbulb-outline - name: Switch to dark mode + name: Switch to light mode features: - search.suggest - search.highlight diff --git a/docs/zh/mkdocs.yml b/docs/zh/mkdocs.yml index a6afb3039..b64228d2c 100644 --- a/docs/zh/mkdocs.yml +++ b/docs/zh/mkdocs.yml @@ -11,14 +11,14 @@ theme: accent: amber toggle: icon: material/lightbulb - name: Switch to light mode + name: Switch to dark mode - media: '(prefers-color-scheme: dark)' scheme: slate primary: teal accent: amber toggle: icon: material/lightbulb-outline - name: Switch to dark mode + name: Switch to light mode features: - search.suggest - search.highlight From 8cee653ad820761efaf1eca1f31971335fd15c94 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 24 Jun 2023 12:29:17 +0000 Subject: [PATCH 06/58] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 0a5f51e98..816765a8c 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔧 Update MkDocs for other languages. PR [#9734](https://github.com/tiangolo/fastapi/pull/9734) by [@tiangolo](https://github.com/tiangolo). * 👷 Refactor Docs CI, run in multiple workers with a dynamic matrix to optimize speed. PR [#9732](https://github.com/tiangolo/fastapi/pull/9732) by [@tiangolo](https://github.com/tiangolo). * 🔥 Remove old internal GitHub Action watch-previews that is no longer needed. PR [#9730](https://github.com/tiangolo/fastapi/pull/9730) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Upgrade MkDocs and MkDocs Material. PR [#9729](https://github.com/tiangolo/fastapi/pull/9729) by [@tiangolo](https://github.com/tiangolo). From dfa56f743ac0443c3f252b9e98ce925c4d630620 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 24 Jun 2023 14:30:57 +0200 Subject: [PATCH 07/58] =?UTF-8?q?=F0=9F=91=B7=20Make=20cron=20jobs=20run?= =?UTF-8?q?=20only=20on=20main=20repo,=20not=20on=20forks,=20to=20avoid=20?= =?UTF-8?q?error=20notifications=20from=20missing=20tokens=20(#9735)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/issue-manager.yml | 3 ++- .github/workflows/label-approved.yml | 1 + .github/workflows/people.yml | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/issue-manager.yml b/.github/workflows/issue-manager.yml index 617105b6e..324623103 100644 --- a/.github/workflows/issue-manager.yml +++ b/.github/workflows/issue-manager.yml @@ -2,7 +2,7 @@ name: Issue Manager on: schedule: - - cron: "0 0 * * *" + - cron: "10 3 * * *" issue_comment: types: - created @@ -16,6 +16,7 @@ on: jobs: issue-manager: + if: github.repository_owner == 'tiangolo' runs-on: ubuntu-latest steps: - uses: tiangolo/issue-manager@0.4.0 diff --git a/.github/workflows/label-approved.yml b/.github/workflows/label-approved.yml index 4a73b02aa..976d29f74 100644 --- a/.github/workflows/label-approved.yml +++ b/.github/workflows/label-approved.yml @@ -6,6 +6,7 @@ on: jobs: label-approved: + if: github.repository_owner == 'tiangolo' runs-on: ubuntu-latest steps: - uses: docker://tiangolo/label-approved:0.0.2 diff --git a/.github/workflows/people.yml b/.github/workflows/people.yml index b167c268f..15ea464a1 100644 --- a/.github/workflows/people.yml +++ b/.github/workflows/people.yml @@ -12,6 +12,7 @@ on: jobs: fastapi-people: + if: github.repository_owner == 'tiangolo' runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 From 3aea9acc6879be81d25209937ec15502abb49958 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 24 Jun 2023 12:31:54 +0000 Subject: [PATCH 08/58] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 816765a8c..ccf12b334 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 👷 Make cron jobs run only on main repo, not on forks, to avoid error notifications from missing tokens. PR [#9735](https://github.com/tiangolo/fastapi/pull/9735) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update MkDocs for other languages. PR [#9734](https://github.com/tiangolo/fastapi/pull/9734) by [@tiangolo](https://github.com/tiangolo). * 👷 Refactor Docs CI, run in multiple workers with a dynamic matrix to optimize speed. PR [#9732](https://github.com/tiangolo/fastapi/pull/9732) by [@tiangolo](https://github.com/tiangolo). * 🔥 Remove old internal GitHub Action watch-previews that is no longer needed. PR [#9730](https://github.com/tiangolo/fastapi/pull/9730) by [@tiangolo](https://github.com/tiangolo). From 51d3a8ff127fd1ba6c34039961debd38597e403d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 24 Jun 2023 16:47:15 +0200 Subject: [PATCH 09/58] =?UTF-8?q?=F0=9F=94=A8=20Add=20MkDocs=20hook=20that?= =?UTF-8?q?=20renames=20sections=20based=20on=20the=20first=20index=20file?= =?UTF-8?q?=20(#9737)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/az/mkdocs.yml | 3 ++ docs/cs/mkdocs.yml | 3 ++ docs/de/mkdocs.yml | 3 ++ docs/em/docs/advanced/index.md | 2 +- docs/em/docs/advanced/security/index.md | 2 +- docs/em/docs/deployment/index.md | 2 +- docs/em/docs/tutorial/dependencies/index.md | 2 +- docs/em/docs/tutorial/index.md | 2 +- docs/em/docs/tutorial/security/index.md | 2 +- docs/em/mkdocs.yml | 3 ++ docs/en/docs/advanced/index.md | 2 +- docs/en/docs/advanced/security/index.md | 2 +- docs/en/docs/deployment/index.md | 2 +- docs/en/docs/tutorial/dependencies/index.md | 2 +- docs/en/docs/tutorial/index.md | 2 +- docs/en/docs/tutorial/security/index.md | 2 +- docs/en/mkdocs.yml | 3 ++ docs/es/docs/advanced/index.md | 2 +- docs/es/docs/tutorial/index.md | 2 +- docs/es/mkdocs.yml | 3 ++ docs/fa/mkdocs.yml | 3 ++ docs/fr/docs/advanced/index.md | 2 +- docs/fr/docs/deployment/index.md | 2 +- docs/fr/mkdocs.yml | 3 ++ docs/he/mkdocs.yml | 3 ++ docs/hy/mkdocs.yml | 3 ++ docs/id/mkdocs.yml | 3 ++ docs/it/mkdocs.yml | 3 ++ docs/ja/docs/advanced/index.md | 2 +- docs/ja/docs/deployment/index.md | 2 +- docs/ja/docs/tutorial/index.md | 2 +- docs/ja/mkdocs.yml | 3 ++ docs/ko/docs/tutorial/index.md | 2 +- docs/ko/mkdocs.yml | 3 ++ docs/lo/mkdocs.yml | 3 ++ docs/nl/mkdocs.yml | 3 ++ docs/pl/docs/tutorial/index.md | 2 +- docs/pl/mkdocs.yml | 3 ++ docs/pt/docs/advanced/index.md | 2 +- docs/pt/docs/deployment/index.md | 2 +- docs/pt/docs/tutorial/index.md | 2 +- docs/pt/docs/tutorial/security/index.md | 2 +- docs/pt/mkdocs.yml | 3 ++ docs/ru/docs/deployment/index.md | 2 +- docs/ru/docs/tutorial/index.md | 2 +- docs/ru/mkdocs.yml | 3 ++ docs/sq/mkdocs.yml | 3 ++ docs/sv/mkdocs.yml | 3 ++ docs/ta/mkdocs.yml | 3 ++ docs/tr/mkdocs.yml | 3 ++ docs/uk/mkdocs.yml | 3 ++ docs/zh/docs/advanced/index.md | 2 +- docs/zh/docs/advanced/security/index.md | 2 +- docs/zh/docs/tutorial/dependencies/index.md | 2 +- docs/zh/docs/tutorial/index.md | 2 +- docs/zh/docs/tutorial/security/index.md | 2 +- docs/zh/mkdocs.yml | 3 ++ scripts/mkdocs_hooks.py | 38 +++++++++++++++++++++ 58 files changed, 145 insertions(+), 32 deletions(-) create mode 100644 scripts/mkdocs_hooks.py diff --git a/docs/az/mkdocs.yml b/docs/az/mkdocs.yml index b846b91f8..c9f467768 100644 --- a/docs/az/mkdocs.yml +++ b/docs/az/mkdocs.yml @@ -23,6 +23,7 @@ theme: - search.suggest - search.highlight - content.tabs.link + - navigation.indexes icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg @@ -158,3 +159,5 @@ extra_css: extra_javascript: - https://fastapi.tiangolo.com/js/termynal.js - https://fastapi.tiangolo.com/js/custom.js +hooks: +- ../../scripts/mkdocs_hooks.py diff --git a/docs/cs/mkdocs.yml b/docs/cs/mkdocs.yml index c303d8f6a..358f0ccf2 100644 --- a/docs/cs/mkdocs.yml +++ b/docs/cs/mkdocs.yml @@ -23,6 +23,7 @@ theme: - search.suggest - search.highlight - content.tabs.link + - navigation.indexes icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg @@ -158,3 +159,5 @@ extra_css: extra_javascript: - https://fastapi.tiangolo.com/js/termynal.js - https://fastapi.tiangolo.com/js/custom.js +hooks: +- ../../scripts/mkdocs_hooks.py diff --git a/docs/de/mkdocs.yml b/docs/de/mkdocs.yml index 4be982509..bdbaa36e3 100644 --- a/docs/de/mkdocs.yml +++ b/docs/de/mkdocs.yml @@ -23,6 +23,7 @@ theme: - search.suggest - search.highlight - content.tabs.link + - navigation.indexes icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg @@ -159,3 +160,5 @@ extra_css: extra_javascript: - https://fastapi.tiangolo.com/js/termynal.js - https://fastapi.tiangolo.com/js/custom.js +hooks: +- ../../scripts/mkdocs_hooks.py diff --git a/docs/em/docs/advanced/index.md b/docs/em/docs/advanced/index.md index 6a43a09e7..abe8d357c 100644 --- a/docs/em/docs/advanced/index.md +++ b/docs/em/docs/advanced/index.md @@ -1,4 +1,4 @@ -# 🏧 👩‍💻 🦮 - 🎶 +# 🏧 👩‍💻 🦮 ## 🌖 ⚒ diff --git a/docs/em/docs/advanced/security/index.md b/docs/em/docs/advanced/security/index.md index 20ee85553..f2bb66df4 100644 --- a/docs/em/docs/advanced/security/index.md +++ b/docs/em/docs/advanced/security/index.md @@ -1,4 +1,4 @@ -# 🏧 💂‍♂ - 🎶 +# 🏧 💂‍♂ ## 🌖 ⚒ diff --git a/docs/em/docs/deployment/index.md b/docs/em/docs/deployment/index.md index 1010c589f..9bcf427b6 100644 --- a/docs/em/docs/deployment/index.md +++ b/docs/em/docs/deployment/index.md @@ -1,4 +1,4 @@ -# 🛠️ - 🎶 +# 🛠️ 🛠️ **FastAPI** 🈸 📶 ⏩. diff --git a/docs/em/docs/tutorial/dependencies/index.md b/docs/em/docs/tutorial/dependencies/index.md index f1c28c573..ffd38d716 100644 --- a/docs/em/docs/tutorial/dependencies/index.md +++ b/docs/em/docs/tutorial/dependencies/index.md @@ -1,4 +1,4 @@ -# 🔗 - 🥇 🔁 +# 🔗 **FastAPI** ✔️ 📶 🏋️ ✋️ 🏋️ **🔗 💉** ⚙️. diff --git a/docs/em/docs/tutorial/index.md b/docs/em/docs/tutorial/index.md index 8536dc3ee..26b4c1913 100644 --- a/docs/em/docs/tutorial/index.md +++ b/docs/em/docs/tutorial/index.md @@ -1,4 +1,4 @@ -# 🔰 - 👩‍💻 🦮 - 🎶 +# 🔰 - 👩‍💻 🦮 👉 🔰 🎦 👆 ❔ ⚙️ **FastAPI** ⏮️ 🌅 🚮 ⚒, 🔁 🔁. diff --git a/docs/em/docs/tutorial/security/index.md b/docs/em/docs/tutorial/security/index.md index 5b507af3e..d76f7203f 100644 --- a/docs/em/docs/tutorial/security/index.md +++ b/docs/em/docs/tutorial/security/index.md @@ -1,4 +1,4 @@ -# 💂‍♂ 🎶 +# 💂‍♂ 📤 📚 🌌 🍵 💂‍♂, 🤝 & ✔. diff --git a/docs/em/mkdocs.yml b/docs/em/mkdocs.yml index bceef0d65..8b6b3997c 100644 --- a/docs/em/mkdocs.yml +++ b/docs/em/mkdocs.yml @@ -23,6 +23,7 @@ theme: - search.suggest - search.highlight - content.tabs.link + - navigation.indexes icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg @@ -265,3 +266,5 @@ extra_css: extra_javascript: - https://fastapi.tiangolo.com/js/termynal.js - https://fastapi.tiangolo.com/js/custom.js +hooks: +- ../../scripts/mkdocs_hooks.py diff --git a/docs/en/docs/advanced/index.md b/docs/en/docs/advanced/index.md index 917f4a62e..467f0833e 100644 --- a/docs/en/docs/advanced/index.md +++ b/docs/en/docs/advanced/index.md @@ -1,4 +1,4 @@ -# Advanced User Guide - Intro +# Advanced User Guide ## Additional Features diff --git a/docs/en/docs/advanced/security/index.md b/docs/en/docs/advanced/security/index.md index 0c94986b5..c18baf64b 100644 --- a/docs/en/docs/advanced/security/index.md +++ b/docs/en/docs/advanced/security/index.md @@ -1,4 +1,4 @@ -# Advanced Security - Intro +# Advanced Security ## Additional Features diff --git a/docs/en/docs/deployment/index.md b/docs/en/docs/deployment/index.md index f0fd001cd..6c43d8abb 100644 --- a/docs/en/docs/deployment/index.md +++ b/docs/en/docs/deployment/index.md @@ -1,4 +1,4 @@ -# Deployment - Intro +# Deployment Deploying a **FastAPI** application is relatively easy. diff --git a/docs/en/docs/tutorial/dependencies/index.md b/docs/en/docs/tutorial/dependencies/index.md index 4f5ecea66..f6f4bced0 100644 --- a/docs/en/docs/tutorial/dependencies/index.md +++ b/docs/en/docs/tutorial/dependencies/index.md @@ -1,4 +1,4 @@ -# Dependencies - First Steps +# Dependencies **FastAPI** has a very powerful but intuitive **Dependency Injection** system. diff --git a/docs/en/docs/tutorial/index.md b/docs/en/docs/tutorial/index.md index 8b4a9df9b..75665324d 100644 --- a/docs/en/docs/tutorial/index.md +++ b/docs/en/docs/tutorial/index.md @@ -1,4 +1,4 @@ -# Tutorial - User Guide - Intro +# Tutorial - User Guide This tutorial shows you how to use **FastAPI** with most of its features, step by step. diff --git a/docs/en/docs/tutorial/security/index.md b/docs/en/docs/tutorial/security/index.md index 035b31736..659a94dc3 100644 --- a/docs/en/docs/tutorial/security/index.md +++ b/docs/en/docs/tutorial/security/index.md @@ -1,4 +1,4 @@ -# Security Intro +# Security There are many ways to handle security, authentication and authorization. diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index 73df174d1..40dfb1661 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -23,6 +23,7 @@ theme: - search.suggest - search.highlight - content.tabs.link + - navigation.indexes icon: repo: fontawesome/brands/github-alt logo: img/icon-white.svg @@ -265,3 +266,5 @@ extra_css: extra_javascript: - js/termynal.js - js/custom.js +hooks: +- ../../scripts/mkdocs_hooks.py diff --git a/docs/es/docs/advanced/index.md b/docs/es/docs/advanced/index.md index 1bee540f2..ba1d20b0d 100644 --- a/docs/es/docs/advanced/index.md +++ b/docs/es/docs/advanced/index.md @@ -1,4 +1,4 @@ -# Guía de Usuario Avanzada - Introducción +# Guía de Usuario Avanzada ## Características Adicionales diff --git a/docs/es/docs/tutorial/index.md b/docs/es/docs/tutorial/index.md index e3671f381..1cff8b4e3 100644 --- a/docs/es/docs/tutorial/index.md +++ b/docs/es/docs/tutorial/index.md @@ -1,4 +1,4 @@ -# Tutorial - Guía de Usuario - Introducción +# Tutorial - Guía de Usuario Este tutorial te muestra cómo usar **FastAPI** con la mayoría de sus características paso a paso. diff --git a/docs/es/mkdocs.yml b/docs/es/mkdocs.yml index e01f55b3a..d8aa9c494 100644 --- a/docs/es/mkdocs.yml +++ b/docs/es/mkdocs.yml @@ -23,6 +23,7 @@ theme: - search.suggest - search.highlight - content.tabs.link + - navigation.indexes icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg @@ -168,3 +169,5 @@ extra_css: extra_javascript: - https://fastapi.tiangolo.com/js/termynal.js - https://fastapi.tiangolo.com/js/custom.js +hooks: +- ../../scripts/mkdocs_hooks.py diff --git a/docs/fa/mkdocs.yml b/docs/fa/mkdocs.yml index 5c5b5e3e1..287521ab3 100644 --- a/docs/fa/mkdocs.yml +++ b/docs/fa/mkdocs.yml @@ -23,6 +23,7 @@ theme: - search.suggest - search.highlight - content.tabs.link + - navigation.indexes icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg @@ -158,3 +159,5 @@ extra_css: extra_javascript: - https://fastapi.tiangolo.com/js/termynal.js - https://fastapi.tiangolo.com/js/custom.js +hooks: +- ../../scripts/mkdocs_hooks.py diff --git a/docs/fr/docs/advanced/index.md b/docs/fr/docs/advanced/index.md index 41737889a..f4fa5ecf6 100644 --- a/docs/fr/docs/advanced/index.md +++ b/docs/fr/docs/advanced/index.md @@ -1,4 +1,4 @@ -# Guide de l'utilisateur avancé - Introduction +# Guide de l'utilisateur avancé ## Caractéristiques supplémentaires diff --git a/docs/fr/docs/deployment/index.md b/docs/fr/docs/deployment/index.md index e855adfa3..e2014afe9 100644 --- a/docs/fr/docs/deployment/index.md +++ b/docs/fr/docs/deployment/index.md @@ -1,4 +1,4 @@ -# Déploiement - Intro +# Déploiement Le déploiement d'une application **FastAPI** est relativement simple. diff --git a/docs/fr/mkdocs.yml b/docs/fr/mkdocs.yml index 5714a74cb..67e5383ed 100644 --- a/docs/fr/mkdocs.yml +++ b/docs/fr/mkdocs.yml @@ -23,6 +23,7 @@ theme: - search.suggest - search.highlight - content.tabs.link + - navigation.indexes icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg @@ -187,3 +188,5 @@ extra_css: extra_javascript: - https://fastapi.tiangolo.com/js/termynal.js - https://fastapi.tiangolo.com/js/custom.js +hooks: +- ../../scripts/mkdocs_hooks.py diff --git a/docs/he/mkdocs.yml b/docs/he/mkdocs.yml index 39e533342..b390875ea 100644 --- a/docs/he/mkdocs.yml +++ b/docs/he/mkdocs.yml @@ -23,6 +23,7 @@ theme: - search.suggest - search.highlight - content.tabs.link + - navigation.indexes icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg @@ -158,3 +159,5 @@ extra_css: extra_javascript: - https://fastapi.tiangolo.com/js/termynal.js - https://fastapi.tiangolo.com/js/custom.js +hooks: +- ../../scripts/mkdocs_hooks.py diff --git a/docs/hy/mkdocs.yml b/docs/hy/mkdocs.yml index 64e5ab876..e5af7dd30 100644 --- a/docs/hy/mkdocs.yml +++ b/docs/hy/mkdocs.yml @@ -23,6 +23,7 @@ theme: - search.suggest - search.highlight - content.tabs.link + - navigation.indexes icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg @@ -158,3 +159,5 @@ extra_css: extra_javascript: - https://fastapi.tiangolo.com/js/termynal.js - https://fastapi.tiangolo.com/js/custom.js +hooks: +- ../../scripts/mkdocs_hooks.py diff --git a/docs/id/mkdocs.yml b/docs/id/mkdocs.yml index acd93df48..6cc2cf045 100644 --- a/docs/id/mkdocs.yml +++ b/docs/id/mkdocs.yml @@ -23,6 +23,7 @@ theme: - search.suggest - search.highlight - content.tabs.link + - navigation.indexes icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg @@ -158,3 +159,5 @@ extra_css: extra_javascript: - https://fastapi.tiangolo.com/js/termynal.js - https://fastapi.tiangolo.com/js/custom.js +hooks: +- ../../scripts/mkdocs_hooks.py diff --git a/docs/it/mkdocs.yml b/docs/it/mkdocs.yml index 4074dff5a..f7de769ee 100644 --- a/docs/it/mkdocs.yml +++ b/docs/it/mkdocs.yml @@ -23,6 +23,7 @@ theme: - search.suggest - search.highlight - content.tabs.link + - navigation.indexes icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg @@ -158,3 +159,5 @@ extra_css: extra_javascript: - https://fastapi.tiangolo.com/js/termynal.js - https://fastapi.tiangolo.com/js/custom.js +hooks: +- ../../scripts/mkdocs_hooks.py diff --git a/docs/ja/docs/advanced/index.md b/docs/ja/docs/advanced/index.md index 676f60359..0732fc405 100644 --- a/docs/ja/docs/advanced/index.md +++ b/docs/ja/docs/advanced/index.md @@ -1,4 +1,4 @@ -# ユーザーガイド 応用編 +# 高度なユーザーガイド ## さらなる機能 diff --git a/docs/ja/docs/deployment/index.md b/docs/ja/docs/deployment/index.md index 40710a93a..897956e38 100644 --- a/docs/ja/docs/deployment/index.md +++ b/docs/ja/docs/deployment/index.md @@ -1,4 +1,4 @@ -# デプロイ - イントロ +# デプロイ **FastAPI** 製のアプリケーションは比較的容易にデプロイできます。 diff --git a/docs/ja/docs/tutorial/index.md b/docs/ja/docs/tutorial/index.md index a2dd59c9b..856cde44b 100644 --- a/docs/ja/docs/tutorial/index.md +++ b/docs/ja/docs/tutorial/index.md @@ -1,4 +1,4 @@ -# チュートリアル - ユーザーガイド - はじめに +# チュートリアル - ユーザーガイド このチュートリアルは**FastAPI**のほぼすべての機能の使い方を段階的に紹介します。 diff --git a/docs/ja/mkdocs.yml b/docs/ja/mkdocs.yml index 56dc4ff4b..f21d731f9 100644 --- a/docs/ja/mkdocs.yml +++ b/docs/ja/mkdocs.yml @@ -23,6 +23,7 @@ theme: - search.suggest - search.highlight - content.tabs.link + - navigation.indexes icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg @@ -202,3 +203,5 @@ extra_css: extra_javascript: - https://fastapi.tiangolo.com/js/termynal.js - https://fastapi.tiangolo.com/js/custom.js +hooks: +- ../../scripts/mkdocs_hooks.py diff --git a/docs/ko/docs/tutorial/index.md b/docs/ko/docs/tutorial/index.md index d6db525e8..deb5ca8f2 100644 --- a/docs/ko/docs/tutorial/index.md +++ b/docs/ko/docs/tutorial/index.md @@ -1,4 +1,4 @@ -# 자습서 - 사용자 안내서 - 도입부 +# 자습서 - 사용자 안내서 이 자습서는 **FastAPI**의 대부분의 기능을 단계별로 사용하는 방법을 보여줍니다. diff --git a/docs/ko/mkdocs.yml b/docs/ko/mkdocs.yml index d91f0dd12..0a1e6b639 100644 --- a/docs/ko/mkdocs.yml +++ b/docs/ko/mkdocs.yml @@ -23,6 +23,7 @@ theme: - search.suggest - search.highlight - content.tabs.link + - navigation.indexes icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg @@ -172,3 +173,5 @@ extra_css: extra_javascript: - https://fastapi.tiangolo.com/js/termynal.js - https://fastapi.tiangolo.com/js/custom.js +hooks: +- ../../scripts/mkdocs_hooks.py diff --git a/docs/lo/mkdocs.yml b/docs/lo/mkdocs.yml index 2ec3d6a2f..7f9253d6c 100644 --- a/docs/lo/mkdocs.yml +++ b/docs/lo/mkdocs.yml @@ -23,6 +23,7 @@ theme: - search.suggest - search.highlight - content.tabs.link + - navigation.indexes icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg @@ -158,3 +159,5 @@ extra_css: extra_javascript: - https://fastapi.tiangolo.com/js/termynal.js - https://fastapi.tiangolo.com/js/custom.js +hooks: +- ../../scripts/mkdocs_hooks.py diff --git a/docs/nl/mkdocs.yml b/docs/nl/mkdocs.yml index 52039bbb5..e74e1a6e3 100644 --- a/docs/nl/mkdocs.yml +++ b/docs/nl/mkdocs.yml @@ -23,6 +23,7 @@ theme: - search.suggest - search.highlight - content.tabs.link + - navigation.indexes icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg @@ -158,3 +159,5 @@ extra_css: extra_javascript: - https://fastapi.tiangolo.com/js/termynal.js - https://fastapi.tiangolo.com/js/custom.js +hooks: +- ../../scripts/mkdocs_hooks.py diff --git a/docs/pl/docs/tutorial/index.md b/docs/pl/docs/tutorial/index.md index ed8752a95..f8c5c6022 100644 --- a/docs/pl/docs/tutorial/index.md +++ b/docs/pl/docs/tutorial/index.md @@ -1,4 +1,4 @@ -# Samouczek - Wprowadzenie +# Samouczek Ten samouczek pokaże Ci, krok po kroku, jak używać większości funkcji **FastAPI**. diff --git a/docs/pl/mkdocs.yml b/docs/pl/mkdocs.yml index 3b1e82c66..588eddf97 100644 --- a/docs/pl/mkdocs.yml +++ b/docs/pl/mkdocs.yml @@ -23,6 +23,7 @@ theme: - search.suggest - search.highlight - content.tabs.link + - navigation.indexes icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg @@ -162,3 +163,5 @@ extra_css: extra_javascript: - https://fastapi.tiangolo.com/js/termynal.js - https://fastapi.tiangolo.com/js/custom.js +hooks: +- ../../scripts/mkdocs_hooks.py diff --git a/docs/pt/docs/advanced/index.md b/docs/pt/docs/advanced/index.md index d1a57c6d1..7e276f732 100644 --- a/docs/pt/docs/advanced/index.md +++ b/docs/pt/docs/advanced/index.md @@ -1,4 +1,4 @@ -# Guia de Usuário Avançado - Introdução +# Guia de Usuário Avançado ## Recursos Adicionais diff --git a/docs/pt/docs/deployment/index.md b/docs/pt/docs/deployment/index.md index 1ff0e44a0..6b4290d1d 100644 --- a/docs/pt/docs/deployment/index.md +++ b/docs/pt/docs/deployment/index.md @@ -1,4 +1,4 @@ -# Implantação - Introdução +# Implantação A implantação de uma aplicação **FastAPI** é relativamente simples. diff --git a/docs/pt/docs/tutorial/index.md b/docs/pt/docs/tutorial/index.md index b1abd32bc..5fc0485a0 100644 --- a/docs/pt/docs/tutorial/index.md +++ b/docs/pt/docs/tutorial/index.md @@ -1,4 +1,4 @@ -# Tutorial - Guia de Usuário - Introdução +# Tutorial - Guia de Usuário Esse tutorial mostra como usar o **FastAPI** com a maior parte de seus recursos, passo a passo. diff --git a/docs/pt/docs/tutorial/security/index.md b/docs/pt/docs/tutorial/security/index.md index 70f864040..f94a8ab62 100644 --- a/docs/pt/docs/tutorial/security/index.md +++ b/docs/pt/docs/tutorial/security/index.md @@ -1,4 +1,4 @@ -# Introdução à segurança +# Segurança Há várias formas de lidar segurança, autenticação e autorização. diff --git a/docs/pt/mkdocs.yml b/docs/pt/mkdocs.yml index fc933db94..54520642e 100644 --- a/docs/pt/mkdocs.yml +++ b/docs/pt/mkdocs.yml @@ -23,6 +23,7 @@ theme: - search.suggest - search.highlight - content.tabs.link + - navigation.indexes icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg @@ -199,3 +200,5 @@ extra_css: extra_javascript: - https://fastapi.tiangolo.com/js/termynal.js - https://fastapi.tiangolo.com/js/custom.js +hooks: +- ../../scripts/mkdocs_hooks.py diff --git a/docs/ru/docs/deployment/index.md b/docs/ru/docs/deployment/index.md index 4dc4e482e..d214a9d62 100644 --- a/docs/ru/docs/deployment/index.md +++ b/docs/ru/docs/deployment/index.md @@ -1,4 +1,4 @@ -# Развёртывание - Введение +# Развёртывание Развернуть приложение **FastAPI** довольно просто. diff --git a/docs/ru/docs/tutorial/index.md b/docs/ru/docs/tutorial/index.md index 4277a6c4f..ea3a1c37a 100644 --- a/docs/ru/docs/tutorial/index.md +++ b/docs/ru/docs/tutorial/index.md @@ -1,4 +1,4 @@ -# Учебник - Руководство пользователя - Введение +# Учебник - Руководство пользователя В этом руководстве шаг за шагом показано, как использовать **FastApi** с большинством его функций. diff --git a/docs/ru/mkdocs.yml b/docs/ru/mkdocs.yml index dbae5ac95..66c7687b0 100644 --- a/docs/ru/mkdocs.yml +++ b/docs/ru/mkdocs.yml @@ -23,6 +23,7 @@ theme: - search.suggest - search.highlight - content.tabs.link + - navigation.indexes icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg @@ -197,3 +198,5 @@ extra_css: extra_javascript: - https://fastapi.tiangolo.com/js/termynal.js - https://fastapi.tiangolo.com/js/custom.js +hooks: +- ../../scripts/mkdocs_hooks.py diff --git a/docs/sq/mkdocs.yml b/docs/sq/mkdocs.yml index d3038644f..64f3dec2e 100644 --- a/docs/sq/mkdocs.yml +++ b/docs/sq/mkdocs.yml @@ -23,6 +23,7 @@ theme: - search.suggest - search.highlight - content.tabs.link + - navigation.indexes icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg @@ -158,3 +159,5 @@ extra_css: extra_javascript: - https://fastapi.tiangolo.com/js/termynal.js - https://fastapi.tiangolo.com/js/custom.js +hooks: +- ../../scripts/mkdocs_hooks.py diff --git a/docs/sv/mkdocs.yml b/docs/sv/mkdocs.yml index 1409b49dc..8604a06f6 100644 --- a/docs/sv/mkdocs.yml +++ b/docs/sv/mkdocs.yml @@ -23,6 +23,7 @@ theme: - search.suggest - search.highlight - content.tabs.link + - navigation.indexes icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg @@ -158,3 +159,5 @@ extra_css: extra_javascript: - https://fastapi.tiangolo.com/js/termynal.js - https://fastapi.tiangolo.com/js/custom.js +hooks: +- ../../scripts/mkdocs_hooks.py diff --git a/docs/ta/mkdocs.yml b/docs/ta/mkdocs.yml index 5c63d659f..4000d9a41 100644 --- a/docs/ta/mkdocs.yml +++ b/docs/ta/mkdocs.yml @@ -23,6 +23,7 @@ theme: - search.suggest - search.highlight - content.tabs.link + - navigation.indexes icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg @@ -158,3 +159,5 @@ extra_css: extra_javascript: - https://fastapi.tiangolo.com/js/termynal.js - https://fastapi.tiangolo.com/js/custom.js +hooks: +- ../../scripts/mkdocs_hooks.py diff --git a/docs/tr/mkdocs.yml b/docs/tr/mkdocs.yml index 125341fc6..408b3ec29 100644 --- a/docs/tr/mkdocs.yml +++ b/docs/tr/mkdocs.yml @@ -23,6 +23,7 @@ theme: - search.suggest - search.highlight - content.tabs.link + - navigation.indexes icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg @@ -163,3 +164,5 @@ extra_css: extra_javascript: - https://fastapi.tiangolo.com/js/termynal.js - https://fastapi.tiangolo.com/js/custom.js +hooks: +- ../../scripts/mkdocs_hooks.py diff --git a/docs/uk/mkdocs.yml b/docs/uk/mkdocs.yml index 33e6fff40..49516cebf 100644 --- a/docs/uk/mkdocs.yml +++ b/docs/uk/mkdocs.yml @@ -23,6 +23,7 @@ theme: - search.suggest - search.highlight - content.tabs.link + - navigation.indexes icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg @@ -158,3 +159,5 @@ extra_css: extra_javascript: - https://fastapi.tiangolo.com/js/termynal.js - https://fastapi.tiangolo.com/js/custom.js +hooks: +- ../../scripts/mkdocs_hooks.py diff --git a/docs/zh/docs/advanced/index.md b/docs/zh/docs/advanced/index.md index d71838cd7..824f91f47 100644 --- a/docs/zh/docs/advanced/index.md +++ b/docs/zh/docs/advanced/index.md @@ -1,4 +1,4 @@ -# 高级用户指南 - 简介 +# 高级用户指南 ## 额外特性 diff --git a/docs/zh/docs/advanced/security/index.md b/docs/zh/docs/advanced/security/index.md index 962523c09..fdc8075c7 100644 --- a/docs/zh/docs/advanced/security/index.md +++ b/docs/zh/docs/advanced/security/index.md @@ -1,4 +1,4 @@ -# 高级安全 - 介绍 +# 高级安全 ## 附加特性 diff --git a/docs/zh/docs/tutorial/dependencies/index.md b/docs/zh/docs/tutorial/dependencies/index.md index c717da0f6..7a133061d 100644 --- a/docs/zh/docs/tutorial/dependencies/index.md +++ b/docs/zh/docs/tutorial/dependencies/index.md @@ -1,4 +1,4 @@ -# 依赖项 - 第一步 +# 依赖项 FastAPI 提供了简单易用,但功能强大的**依赖注入**系统。 diff --git a/docs/zh/docs/tutorial/index.md b/docs/zh/docs/tutorial/index.md index 6093caeb6..6180d3de3 100644 --- a/docs/zh/docs/tutorial/index.md +++ b/docs/zh/docs/tutorial/index.md @@ -1,4 +1,4 @@ -# 教程 - 用户指南 - 简介 +# 教程 - 用户指南 本教程将一步步向你展示如何使用 **FastAPI** 的绝大部分特性。 diff --git a/docs/zh/docs/tutorial/security/index.md b/docs/zh/docs/tutorial/security/index.md index 8f302a16c..0595f5f63 100644 --- a/docs/zh/docs/tutorial/security/index.md +++ b/docs/zh/docs/tutorial/security/index.md @@ -1,4 +1,4 @@ -# 安全性简介 +# 安全性 有许多方法可以处理安全性、身份认证和授权等问题。 diff --git a/docs/zh/mkdocs.yml b/docs/zh/mkdocs.yml index b64228d2c..39f989790 100644 --- a/docs/zh/mkdocs.yml +++ b/docs/zh/mkdocs.yml @@ -23,6 +23,7 @@ theme: - search.suggest - search.highlight - content.tabs.link + - navigation.indexes icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg @@ -223,3 +224,5 @@ extra_css: extra_javascript: - https://fastapi.tiangolo.com/js/termynal.js - https://fastapi.tiangolo.com/js/custom.js +hooks: +- ../../scripts/mkdocs_hooks.py diff --git a/scripts/mkdocs_hooks.py b/scripts/mkdocs_hooks.py new file mode 100644 index 000000000..f09e9a99d --- /dev/null +++ b/scripts/mkdocs_hooks.py @@ -0,0 +1,38 @@ +from typing import Any, List, Union + +from mkdocs.config.defaults import MkDocsConfig +from mkdocs.structure.files import Files +from mkdocs.structure.nav import Link, Navigation, Section +from mkdocs.structure.pages import Page + + +def generate_renamed_section_items( + items: List[Union[Page, Section, Link]], *, config: MkDocsConfig +) -> List[Union[Page, Section, Link]]: + new_items: List[Union[Page, Section, Link]] = [] + for item in items: + if isinstance(item, Section): + new_title = item.title + new_children = generate_renamed_section_items(item.children, config=config) + first_child = new_children[0] + if isinstance(first_child, Page): + if first_child.file.src_path.endswith("index.md"): + # Read the source so that the title is parsed and available + first_child.read_source(config=config) + new_title = first_child.title or new_title + # Creating a new section makes it render it collapsed by default + # no idea why, so, let's just modify the existing one + # new_section = Section(title=new_title, children=new_children) + item.title = new_title + item.children = new_children + new_items.append(item) + else: + new_items.append(item) + return new_items + + +def on_nav( + nav: Navigation, *, config: MkDocsConfig, files: Files, **kwargs: Any +) -> Navigation: + new_items = generate_renamed_section_items(nav.items, config=config) + return Navigation(items=new_items, pages=nav.pages) From c563b5bcf11a35f36a5f9345facf52b39ca7e9dd Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 24 Jun 2023 14:47:59 +0000 Subject: [PATCH 10/58] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index ccf12b334..88f2ddf2d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔨 Add MkDocs hook that renames sections based on the first index file. PR [#9737](https://github.com/tiangolo/fastapi/pull/9737) by [@tiangolo](https://github.com/tiangolo). * 👷 Make cron jobs run only on main repo, not on forks, to avoid error notifications from missing tokens. PR [#9735](https://github.com/tiangolo/fastapi/pull/9735) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update MkDocs for other languages. PR [#9734](https://github.com/tiangolo/fastapi/pull/9734) by [@tiangolo](https://github.com/tiangolo). * 👷 Refactor Docs CI, run in multiple workers with a dynamic matrix to optimize speed. PR [#9732](https://github.com/tiangolo/fastapi/pull/9732) by [@tiangolo](https://github.com/tiangolo). From 5656ed09efea3451145087f63e402a0d024622b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 25 Jun 2023 14:33:58 +0200 Subject: [PATCH 11/58] =?UTF-8?q?=E2=9C=A8=20Refactor=20docs=20for=20build?= =?UTF-8?q?ing=20scripts,=20use=20MkDocs=20hooks,=20simplify=20(remove)=20?= =?UTF-8?q?configs=20for=20languages=20(#9742)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ Add MkDocs hooks to re-use all config from en, and auto-generate missing docs files form en * 🔧 Update MkDocs config for es * 🔧 Simplify configs for all languages * ✨ Compute available languages from MkDocs Material for config overrides in hooks * 🔧 Update config for MkDocs for en, to make paths compatible for other languages * ♻️ Refactor scripts/docs.py to remove all custom logic that is now handled by the MkDocs hooks * 🔧 Remove ta language as it's incomplete (no translations and causing errors) * 🔥 Remove ta lang, no translations available * 🔥 Remove dummy overrides directories, no longer needed * ✨ Use the same missing-translation.md file contents for hooks * ⏪️ Restore and refactor new-lang command * 📝 Update docs for contributing with new simplified workflow for translations * 🔊 Enable logs so that MkDocs can show its standard output on the docs.py script --- docs/az/mkdocs.yml | 164 +-------------------- docs/az/overrides/.gitignore | 0 docs/cs/mkdocs.yml | 164 +-------------------- docs/cs/overrides/.gitignore | 0 docs/de/mkdocs.yml | 165 +-------------------- docs/de/overrides/.gitignore | 0 docs/em/mkdocs.yml | 271 +---------------------------------- docs/em/overrides/.gitignore | 0 docs/en/docs/contributing.md | 128 ++++++----------- docs/en/mkdocs.yml | 7 +- docs/es/mkdocs.yml | 174 +--------------------- docs/es/overrides/.gitignore | 0 docs/fa/mkdocs.yml | 164 +-------------------- docs/fa/overrides/.gitignore | 0 docs/fr/mkdocs.yml | 193 +------------------------ docs/fr/overrides/.gitignore | 0 docs/he/mkdocs.yml | 164 +-------------------- docs/he/overrides/.gitignore | 0 docs/hy/mkdocs.yml | 164 +-------------------- docs/hy/overrides/.gitignore | 0 docs/id/mkdocs.yml | 164 +-------------------- docs/id/overrides/.gitignore | 0 docs/it/mkdocs.yml | 164 +-------------------- docs/it/overrides/.gitignore | 0 docs/ja/mkdocs.yml | 208 +-------------------------- docs/ja/overrides/.gitignore | 0 docs/ko/mkdocs.yml | 178 +---------------------- docs/ko/overrides/.gitignore | 0 docs/lo/mkdocs.yml | 164 +-------------------- docs/lo/overrides/.gitignore | 0 docs/nl/mkdocs.yml | 164 +-------------------- docs/nl/overrides/.gitignore | 0 docs/pl/mkdocs.yml | 168 +--------------------- docs/pl/overrides/.gitignore | 0 docs/pt/mkdocs.yml | 205 +------------------------- docs/pt/overrides/.gitignore | 0 docs/ru/mkdocs.yml | 203 +------------------------- docs/ru/overrides/.gitignore | 0 docs/sq/mkdocs.yml | 164 +-------------------- docs/sq/overrides/.gitignore | 0 docs/sv/mkdocs.yml | 164 +-------------------- docs/sv/overrides/.gitignore | 0 docs/ta/mkdocs.yml | 163 --------------------- docs/ta/overrides/.gitignore | 0 docs/tr/mkdocs.yml | 169 +--------------------- docs/tr/overrides/.gitignore | 0 docs/uk/mkdocs.yml | 164 +-------------------- docs/uk/overrides/.gitignore | 0 docs/zh/mkdocs.yml | 229 +---------------------------- docs/zh/overrides/.gitignore | 0 scripts/docs.py | 245 +++++-------------------------- scripts/mkdocs_hooks.py | 96 ++++++++++++- 52 files changed, 200 insertions(+), 4570 deletions(-) delete mode 100644 docs/az/overrides/.gitignore delete mode 100644 docs/cs/overrides/.gitignore delete mode 100644 docs/de/overrides/.gitignore delete mode 100644 docs/em/overrides/.gitignore delete mode 100644 docs/es/overrides/.gitignore delete mode 100644 docs/fa/overrides/.gitignore delete mode 100644 docs/fr/overrides/.gitignore delete mode 100644 docs/he/overrides/.gitignore delete mode 100644 docs/hy/overrides/.gitignore delete mode 100644 docs/id/overrides/.gitignore delete mode 100644 docs/it/overrides/.gitignore delete mode 100644 docs/ja/overrides/.gitignore delete mode 100644 docs/ko/overrides/.gitignore delete mode 100644 docs/lo/overrides/.gitignore delete mode 100644 docs/nl/overrides/.gitignore delete mode 100644 docs/pl/overrides/.gitignore delete mode 100644 docs/pt/overrides/.gitignore delete mode 100644 docs/ru/overrides/.gitignore delete mode 100644 docs/sq/overrides/.gitignore delete mode 100644 docs/sv/overrides/.gitignore delete mode 100644 docs/ta/mkdocs.yml delete mode 100644 docs/ta/overrides/.gitignore delete mode 100644 docs/tr/overrides/.gitignore delete mode 100644 docs/uk/overrides/.gitignore delete mode 100644 docs/zh/overrides/.gitignore diff --git a/docs/az/mkdocs.yml b/docs/az/mkdocs.yml index c9f467768..de18856f4 100644 --- a/docs/az/mkdocs.yml +++ b/docs/az/mkdocs.yml @@ -1,163 +1 @@ -site_name: FastAPI -site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production -site_url: https://fastapi.tiangolo.com/az/ -theme: - name: material - custom_dir: overrides - palette: - - media: '(prefers-color-scheme: light)' - scheme: default - primary: teal - accent: amber - toggle: - icon: material/lightbulb - name: Switch to dark mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - primary: teal - accent: amber - toggle: - icon: material/lightbulb-outline - name: Switch to light mode - features: - - search.suggest - - search.highlight - - content.tabs.link - - navigation.indexes - icon: - repo: fontawesome/brands/github-alt - logo: https://fastapi.tiangolo.com/img/icon-white.svg - favicon: https://fastapi.tiangolo.com/img/favicon.png - language: en -repo_name: tiangolo/fastapi -repo_url: https://github.com/tiangolo/fastapi -edit_uri: '' -plugins: -- search -- markdownextradata: - data: data -nav: -- FastAPI: index.md -- Languages: - - en: / - - az: /az/ - - cs: /cs/ - - de: /de/ - - em: /em/ - - es: /es/ - - fa: /fa/ - - fr: /fr/ - - he: /he/ - - hy: /hy/ - - id: /id/ - - it: /it/ - - ja: /ja/ - - ko: /ko/ - - lo: /lo/ - - nl: /nl/ - - pl: /pl/ - - pt: /pt/ - - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - - ta: /ta/ - - tr: /tr/ - - uk: /uk/ - - zh: /zh/ -markdown_extensions: -- toc: - permalink: true -- markdown.extensions.codehilite: - guess_lang: false -- mdx_include: - base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: - alternate_style: true -- attr_list -- md_in_html -extra: - analytics: - provider: google - property: G-YNEVN69SC3 - social: - - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/fastapi - - icon: fontawesome/brands/discord - link: https://discord.gg/VQjSZaeJmf - - icon: fontawesome/brands/twitter - link: https://twitter.com/fastapi - - icon: fontawesome/brands/linkedin - link: https://www.linkedin.com/in/tiangolo - - icon: fontawesome/brands/dev - link: https://dev.to/tiangolo - - icon: fontawesome/brands/medium - link: https://medium.com/@tiangolo - - icon: fontawesome/solid/globe - link: https://tiangolo.com - alternate: - - link: / - name: en - English - - link: /az/ - name: az - - link: /cs/ - name: cs - - link: /de/ - name: de - - link: /em/ - name: 😉 - - link: /es/ - name: es - español - - link: /fa/ - name: fa - - link: /fr/ - name: fr - français - - link: /he/ - name: he - - link: /hy/ - name: hy - - link: /id/ - name: id - - link: /it/ - name: it - italiano - - link: /ja/ - name: ja - 日本語 - - link: /ko/ - name: ko - 한국어 - - link: /lo/ - name: lo - ພາສາລາວ - - link: /nl/ - name: nl - - link: /pl/ - name: pl - - link: /pt/ - name: pt - português - - link: /ru/ - name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - - link: /ta/ - name: ta - தமிழ் - - link: /tr/ - name: tr - Türkçe - - link: /uk/ - name: uk - українська мова - - link: /zh/ - name: zh - 汉语 -extra_css: -- https://fastapi.tiangolo.com/css/termynal.css -- https://fastapi.tiangolo.com/css/custom.css -extra_javascript: -- https://fastapi.tiangolo.com/js/termynal.js -- https://fastapi.tiangolo.com/js/custom.js -hooks: -- ../../scripts/mkdocs_hooks.py +INHERIT: ../en/mkdocs.yml diff --git a/docs/az/overrides/.gitignore b/docs/az/overrides/.gitignore deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/cs/mkdocs.yml b/docs/cs/mkdocs.yml index 358f0ccf2..de18856f4 100644 --- a/docs/cs/mkdocs.yml +++ b/docs/cs/mkdocs.yml @@ -1,163 +1 @@ -site_name: FastAPI -site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production -site_url: https://fastapi.tiangolo.com/cs/ -theme: - name: material - custom_dir: overrides - palette: - - media: '(prefers-color-scheme: light)' - scheme: default - primary: teal - accent: amber - toggle: - icon: material/lightbulb - name: Switch to dark mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - primary: teal - accent: amber - toggle: - icon: material/lightbulb-outline - name: Switch to light mode - features: - - search.suggest - - search.highlight - - content.tabs.link - - navigation.indexes - icon: - repo: fontawesome/brands/github-alt - logo: https://fastapi.tiangolo.com/img/icon-white.svg - favicon: https://fastapi.tiangolo.com/img/favicon.png - language: cs -repo_name: tiangolo/fastapi -repo_url: https://github.com/tiangolo/fastapi -edit_uri: '' -plugins: -- search -- markdownextradata: - data: data -nav: -- FastAPI: index.md -- Languages: - - en: / - - az: /az/ - - cs: /cs/ - - de: /de/ - - em: /em/ - - es: /es/ - - fa: /fa/ - - fr: /fr/ - - he: /he/ - - hy: /hy/ - - id: /id/ - - it: /it/ - - ja: /ja/ - - ko: /ko/ - - lo: /lo/ - - nl: /nl/ - - pl: /pl/ - - pt: /pt/ - - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - - ta: /ta/ - - tr: /tr/ - - uk: /uk/ - - zh: /zh/ -markdown_extensions: -- toc: - permalink: true -- markdown.extensions.codehilite: - guess_lang: false -- mdx_include: - base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: - alternate_style: true -- attr_list -- md_in_html -extra: - analytics: - provider: google - property: G-YNEVN69SC3 - social: - - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/fastapi - - icon: fontawesome/brands/discord - link: https://discord.gg/VQjSZaeJmf - - icon: fontawesome/brands/twitter - link: https://twitter.com/fastapi - - icon: fontawesome/brands/linkedin - link: https://www.linkedin.com/in/tiangolo - - icon: fontawesome/brands/dev - link: https://dev.to/tiangolo - - icon: fontawesome/brands/medium - link: https://medium.com/@tiangolo - - icon: fontawesome/solid/globe - link: https://tiangolo.com - alternate: - - link: / - name: en - English - - link: /az/ - name: az - - link: /cs/ - name: cs - - link: /de/ - name: de - - link: /em/ - name: 😉 - - link: /es/ - name: es - español - - link: /fa/ - name: fa - - link: /fr/ - name: fr - français - - link: /he/ - name: he - - link: /hy/ - name: hy - - link: /id/ - name: id - - link: /it/ - name: it - italiano - - link: /ja/ - name: ja - 日本語 - - link: /ko/ - name: ko - 한국어 - - link: /lo/ - name: lo - ພາສາລາວ - - link: /nl/ - name: nl - - link: /pl/ - name: pl - - link: /pt/ - name: pt - português - - link: /ru/ - name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - - link: /ta/ - name: ta - தமிழ் - - link: /tr/ - name: tr - Türkçe - - link: /uk/ - name: uk - українська мова - - link: /zh/ - name: zh - 汉语 -extra_css: -- https://fastapi.tiangolo.com/css/termynal.css -- https://fastapi.tiangolo.com/css/custom.css -extra_javascript: -- https://fastapi.tiangolo.com/js/termynal.js -- https://fastapi.tiangolo.com/js/custom.js -hooks: -- ../../scripts/mkdocs_hooks.py +INHERIT: ../en/mkdocs.yml diff --git a/docs/cs/overrides/.gitignore b/docs/cs/overrides/.gitignore deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/de/mkdocs.yml b/docs/de/mkdocs.yml index bdbaa36e3..de18856f4 100644 --- a/docs/de/mkdocs.yml +++ b/docs/de/mkdocs.yml @@ -1,164 +1 @@ -site_name: FastAPI -site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production -site_url: https://fastapi.tiangolo.com/de/ -theme: - name: material - custom_dir: overrides - palette: - - media: '(prefers-color-scheme: light)' - scheme: default - primary: teal - accent: amber - toggle: - icon: material/lightbulb - name: Switch to dark mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - primary: teal - accent: amber - toggle: - icon: material/lightbulb-outline - name: Switch to light mode - features: - - search.suggest - - search.highlight - - content.tabs.link - - navigation.indexes - icon: - repo: fontawesome/brands/github-alt - logo: https://fastapi.tiangolo.com/img/icon-white.svg - favicon: https://fastapi.tiangolo.com/img/favicon.png - language: de -repo_name: tiangolo/fastapi -repo_url: https://github.com/tiangolo/fastapi -edit_uri: '' -plugins: -- search -- markdownextradata: - data: data -nav: -- FastAPI: index.md -- Languages: - - en: / - - az: /az/ - - cs: /cs/ - - de: /de/ - - em: /em/ - - es: /es/ - - fa: /fa/ - - fr: /fr/ - - he: /he/ - - hy: /hy/ - - id: /id/ - - it: /it/ - - ja: /ja/ - - ko: /ko/ - - lo: /lo/ - - nl: /nl/ - - pl: /pl/ - - pt: /pt/ - - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - - ta: /ta/ - - tr: /tr/ - - uk: /uk/ - - zh: /zh/ -- features.md -markdown_extensions: -- toc: - permalink: true -- markdown.extensions.codehilite: - guess_lang: false -- mdx_include: - base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: - alternate_style: true -- attr_list -- md_in_html -extra: - analytics: - provider: google - property: G-YNEVN69SC3 - social: - - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/fastapi - - icon: fontawesome/brands/discord - link: https://discord.gg/VQjSZaeJmf - - icon: fontawesome/brands/twitter - link: https://twitter.com/fastapi - - icon: fontawesome/brands/linkedin - link: https://www.linkedin.com/in/tiangolo - - icon: fontawesome/brands/dev - link: https://dev.to/tiangolo - - icon: fontawesome/brands/medium - link: https://medium.com/@tiangolo - - icon: fontawesome/solid/globe - link: https://tiangolo.com - alternate: - - link: / - name: en - English - - link: /az/ - name: az - - link: /cs/ - name: cs - - link: /de/ - name: de - - link: /em/ - name: 😉 - - link: /es/ - name: es - español - - link: /fa/ - name: fa - - link: /fr/ - name: fr - français - - link: /he/ - name: he - - link: /hy/ - name: hy - - link: /id/ - name: id - - link: /it/ - name: it - italiano - - link: /ja/ - name: ja - 日本語 - - link: /ko/ - name: ko - 한국어 - - link: /lo/ - name: lo - ພາສາລາວ - - link: /nl/ - name: nl - - link: /pl/ - name: pl - - link: /pt/ - name: pt - português - - link: /ru/ - name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - - link: /ta/ - name: ta - தமிழ் - - link: /tr/ - name: tr - Türkçe - - link: /uk/ - name: uk - українська мова - - link: /zh/ - name: zh - 汉语 -extra_css: -- https://fastapi.tiangolo.com/css/termynal.css -- https://fastapi.tiangolo.com/css/custom.css -extra_javascript: -- https://fastapi.tiangolo.com/js/termynal.js -- https://fastapi.tiangolo.com/js/custom.js -hooks: -- ../../scripts/mkdocs_hooks.py +INHERIT: ../en/mkdocs.yml diff --git a/docs/de/overrides/.gitignore b/docs/de/overrides/.gitignore deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/em/mkdocs.yml b/docs/em/mkdocs.yml index 8b6b3997c..de18856f4 100644 --- a/docs/em/mkdocs.yml +++ b/docs/em/mkdocs.yml @@ -1,270 +1 @@ -site_name: FastAPI -site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production -site_url: https://fastapi.tiangolo.com/em/ -theme: - name: material - custom_dir: overrides - palette: - - media: '(prefers-color-scheme: light)' - scheme: default - primary: teal - accent: amber - toggle: - icon: material/lightbulb - name: Switch to dark mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - primary: teal - accent: amber - toggle: - icon: material/lightbulb-outline - name: Switch to light mode - features: - - search.suggest - - search.highlight - - content.tabs.link - - navigation.indexes - icon: - repo: fontawesome/brands/github-alt - logo: https://fastapi.tiangolo.com/img/icon-white.svg - favicon: https://fastapi.tiangolo.com/img/favicon.png - language: en -repo_name: tiangolo/fastapi -repo_url: https://github.com/tiangolo/fastapi -edit_uri: '' -plugins: -- search -- markdownextradata: - data: data -nav: -- FastAPI: index.md -- Languages: - - en: / - - az: /az/ - - cs: /cs/ - - de: /de/ - - em: /em/ - - es: /es/ - - fa: /fa/ - - fr: /fr/ - - he: /he/ - - hy: /hy/ - - id: /id/ - - it: /it/ - - ja: /ja/ - - ko: /ko/ - - lo: /lo/ - - nl: /nl/ - - pl: /pl/ - - pt: /pt/ - - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - - ta: /ta/ - - tr: /tr/ - - uk: /uk/ - - zh: /zh/ -- features.md -- fastapi-people.md -- python-types.md -- 🔰 - 👩‍💻 🦮: - - tutorial/index.md - - tutorial/first-steps.md - - tutorial/path-params.md - - tutorial/query-params.md - - tutorial/body.md - - tutorial/query-params-str-validations.md - - tutorial/path-params-numeric-validations.md - - tutorial/body-multiple-params.md - - tutorial/body-fields.md - - tutorial/body-nested-models.md - - tutorial/schema-extra-example.md - - tutorial/extra-data-types.md - - tutorial/cookie-params.md - - tutorial/header-params.md - - tutorial/response-model.md - - tutorial/extra-models.md - - tutorial/response-status-code.md - - tutorial/request-forms.md - - tutorial/request-files.md - - tutorial/request-forms-and-files.md - - tutorial/handling-errors.md - - tutorial/path-operation-configuration.md - - tutorial/encoder.md - - tutorial/body-updates.md - - 🔗: - - tutorial/dependencies/index.md - - tutorial/dependencies/classes-as-dependencies.md - - tutorial/dependencies/sub-dependencies.md - - tutorial/dependencies/dependencies-in-path-operation-decorators.md - - tutorial/dependencies/global-dependencies.md - - tutorial/dependencies/dependencies-with-yield.md - - 💂‍♂: - - tutorial/security/index.md - - tutorial/security/first-steps.md - - tutorial/security/get-current-user.md - - tutorial/security/simple-oauth2.md - - tutorial/security/oauth2-jwt.md - - tutorial/middleware.md - - tutorial/cors.md - - tutorial/sql-databases.md - - tutorial/bigger-applications.md - - tutorial/background-tasks.md - - tutorial/metadata.md - - tutorial/static-files.md - - tutorial/testing.md - - tutorial/debugging.md -- 🏧 👩‍💻 🦮: - - advanced/index.md - - advanced/path-operation-advanced-configuration.md - - advanced/additional-status-codes.md - - advanced/response-directly.md - - advanced/custom-response.md - - advanced/additional-responses.md - - advanced/response-cookies.md - - advanced/response-headers.md - - advanced/response-change-status-code.md - - advanced/advanced-dependencies.md - - 🏧 💂‍♂: - - advanced/security/index.md - - advanced/security/oauth2-scopes.md - - advanced/security/http-basic-auth.md - - advanced/using-request-directly.md - - advanced/dataclasses.md - - advanced/middleware.md - - advanced/sql-databases-peewee.md - - advanced/async-sql-databases.md - - advanced/nosql-databases.md - - advanced/sub-applications.md - - advanced/behind-a-proxy.md - - advanced/templates.md - - advanced/graphql.md - - advanced/websockets.md - - advanced/events.md - - advanced/custom-request-and-route.md - - advanced/testing-websockets.md - - advanced/testing-events.md - - advanced/testing-dependencies.md - - advanced/testing-database.md - - advanced/async-tests.md - - advanced/settings.md - - advanced/conditional-openapi.md - - advanced/extending-openapi.md - - advanced/openapi-callbacks.md - - advanced/wsgi.md - - advanced/generate-clients.md -- async.md -- 🛠️: - - deployment/index.md - - deployment/versions.md - - deployment/https.md - - deployment/manually.md - - deployment/concepts.md - - deployment/deta.md - - deployment/server-workers.md - - deployment/docker.md -- project-generation.md -- alternatives.md -- history-design-future.md -- external-links.md -- benchmarks.md -- help-fastapi.md -- contributing.md -- release-notes.md -markdown_extensions: -- toc: - permalink: true -- markdown.extensions.codehilite: - guess_lang: false -- mdx_include: - base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: - alternate_style: true -- attr_list -- md_in_html -extra: - analytics: - provider: google - property: G-YNEVN69SC3 - social: - - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/fastapi - - icon: fontawesome/brands/discord - link: https://discord.gg/VQjSZaeJmf - - icon: fontawesome/brands/twitter - link: https://twitter.com/fastapi - - icon: fontawesome/brands/linkedin - link: https://www.linkedin.com/in/tiangolo - - icon: fontawesome/brands/dev - link: https://dev.to/tiangolo - - icon: fontawesome/brands/medium - link: https://medium.com/@tiangolo - - icon: fontawesome/solid/globe - link: https://tiangolo.com - alternate: - - link: / - name: en - English - - link: /az/ - name: az - - link: /cs/ - name: cs - - link: /de/ - name: de - - link: /em/ - name: 😉 - - link: /es/ - name: es - español - - link: /fa/ - name: fa - - link: /fr/ - name: fr - français - - link: /he/ - name: he - - link: /hy/ - name: hy - - link: /id/ - name: id - - link: /it/ - name: it - italiano - - link: /ja/ - name: ja - 日本語 - - link: /ko/ - name: ko - 한국어 - - link: /lo/ - name: lo - ພາສາລາວ - - link: /nl/ - name: nl - - link: /pl/ - name: pl - - link: /pt/ - name: pt - português - - link: /ru/ - name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - - link: /ta/ - name: ta - தமிழ் - - link: /tr/ - name: tr - Türkçe - - link: /uk/ - name: uk - українська мова - - link: /zh/ - name: zh - 汉语 -extra_css: -- https://fastapi.tiangolo.com/css/termynal.css -- https://fastapi.tiangolo.com/css/custom.css -extra_javascript: -- https://fastapi.tiangolo.com/js/termynal.js -- https://fastapi.tiangolo.com/js/custom.js -hooks: -- ../../scripts/mkdocs_hooks.py +INHERIT: ../en/mkdocs.yml diff --git a/docs/em/overrides/.gitignore b/docs/em/overrides/.gitignore deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/en/docs/contributing.md b/docs/en/docs/contributing.md index 660914a08..f968489ae 100644 --- a/docs/en/docs/contributing.md +++ b/docs/en/docs/contributing.md @@ -195,6 +195,21 @@ It will serve the documentation on `http://127.0.0.1:8008`. That way, you can edit the documentation/source files and see the changes live. +!!! tip + Alternatively, you can perform the same steps that scripts does manually. + + Go into the language directory, for the main docs in English it's at `docs/en/`: + + ```console + $ cd docs/en/ + ``` + + Then run `mkdocs` in that directory: + + ```console + $ mkdocs serve --dev-addr 8008 + ``` + #### Typer CLI (optional) The instructions here show you how to use the script at `./scripts/docs.py` with the `python` program directly. @@ -245,13 +260,15 @@ Here are the steps to help with translations. Check the docs about adding a pull request review to approve it or request changes. -* Check in the issues to see if there's one coordinating translations for your language. +* Check if there's a GitHub Discussion to coordinate translations for your language. You can subscribe to it, and when there's a new pull request to review, an automatic comment will be added to the discussion. * Add a single pull request per page translated. That will make it much easier for others to review it. For the languages I don't speak, I'll wait for several others to review the translation before merging. * You can also check if there are translations for your language and add a review to them, that will help me know that the translation is correct and I can merge it. + * You could check in the GitHub Discussions for your language. + * Or you can filter the existing PRs by the ones with the label for your language, for example, for Spanish, the label is `lang-es`. * Use the same Python examples and only translate the text in the docs. You don't have to change anything for this to work. @@ -283,11 +300,24 @@ $ python ./scripts/docs.py live es -Now you can go to http://127.0.0.1:8008 and see your changes live. +!!! tip + Alternatively, you can perform the same steps that scripts does manually. -If you look at the FastAPI docs website, you will see that every language has all the pages. But some pages are not translated and have a notification about the missing translation. + Go into the language directory, for the Spanish translations it's at `docs/es/`: -But when you run it locally like this, you will only see the pages that are already translated. + ```console + $ cd docs/es/ + ``` + + Then run `mkdocs` in that directory: + + ```console + $ mkdocs serve --dev-addr 8008 + ``` + +Now you can go to http://127.0.0.1:8008 and see your changes live. + +You will see that every language has all the pages. But some pages are not translated and have a notification about the missing translation. Now let's say that you want to add a translation for the section [Features](features.md){.internal-link target=_blank}. @@ -306,46 +336,6 @@ docs/es/docs/features.md !!! tip Notice that the only change in the path and file name is the language code, from `en` to `es`. -* Now open the MkDocs config file for English at: - -``` -docs/en/mkdocs.yml -``` - -* Find the place where that `docs/features.md` is located in the config file. Somewhere like: - -```YAML hl_lines="8" -site_name: FastAPI -# More stuff -nav: -- FastAPI: index.md -- Languages: - - en: / - - es: /es/ -- features.md -``` - -* Open the MkDocs config file for the language you are editing, e.g.: - -``` -docs/es/mkdocs.yml -``` - -* Add it there at the exact same location it was for English, e.g.: - -```YAML hl_lines="8" -site_name: FastAPI -# More stuff -nav: -- FastAPI: index.md -- Languages: - - en: / - - es: /es/ -- features.md -``` - -Make sure that if there are other entries, the new entry with your translation is exactly in the same order as in the English version. - If you go to your browser you will see that now the docs show your new section. 🎉 Now you can translate it all and see how it looks as you save the file. @@ -367,55 +357,32 @@ The next step is to run the script to generate a new translation directory: $ python ./scripts/docs.py new-lang ht Successfully initialized: docs/ht -Updating ht -Updating en ``` Now you can check in your code editor the newly created directory `docs/ht/`. -!!! tip - Create a first pull request with just this, to set up the configuration for the new language, before adding translations. +That command created a file `docs/ht/mkdocs.yml` with a simple config that inherits everything from the `en` version: - That way others can help with other pages while you work on the first one. 🚀 - -Start by translating the main page, `docs/ht/index.md`. - -Then you can continue with the previous instructions, for an "Existing Language". - -##### New Language not supported - -If when running the live server script you get an error about the language not being supported, something like: - -``` - raise TemplateNotFound(template) -jinja2.exceptions.TemplateNotFound: partials/language/xx.html +```yaml +INHERIT: ../en/mkdocs.yml ``` -That means that the theme doesn't support that language (in this case, with a fake 2-letter code of `xx`). - -But don't worry, you can set the theme language to English and then translate the content of the docs. +!!! tip + You could also simply create that file with those contents manually. -If you need to do that, edit the `mkdocs.yml` for your new language, it will have something like: +That command also created a dummy file `docs/ht/index.md` for the main page, you can start by translating that one. -```YAML hl_lines="5" -site_name: FastAPI -# More stuff -theme: - # More stuff - language: xx -``` +You can continue with the previous instructions for an "Existing Language" for that process. -Change that language from `xx` (from your language code) to `en`. - -Then you can start the live server again. +You can make the first pull request with those two files, `docs/ht/mkdocs.yml` and `docs/ht/index.md`. 🎉 #### Preview the result -When you use the script at `./scripts/docs.py` with the `live` command it only shows the files and translations available for the current language. +You can use the `./scripts/docs.py` with the `live` command to preview the results (or `mkdocs serve`). -But once you are done, you can test it all as it would look online. +Once you are done, you can also test it all as it would look online, including all the other languages. To do that, first build all the docs: @@ -425,19 +392,14 @@ To do that, first build all the docs: // Use the command "build-all", this will take a bit $ python ./scripts/docs.py build-all -Updating es -Updating en Building docs for: en Building docs for: es Successfully built docs for: es -Copying en index.md to README.md ``` -That generates all the docs at `./docs_build/` for each language. This includes adding any files with missing translations, with a note saying that "this file doesn't have a translation yet". But you don't have to do anything with that directory. - -Then it builds all those independent MkDocs sites for each language, combines them, and generates the final output at `./site/`. +This builds all those independent MkDocs sites for each language, combines them, and generates the final output at `./site/`. Then you can serve that with the command `serve`: diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index 40dfb1661..a21848a66 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -3,7 +3,7 @@ site_description: FastAPI framework, high performance, easy to learn, fast to co site_url: https://fastapi.tiangolo.com/ theme: name: material - custom_dir: overrides + custom_dir: ../en/overrides palette: - media: '(prefers-color-scheme: light)' scheme: default @@ -35,7 +35,7 @@ edit_uri: '' plugins: - search - markdownextradata: - data: data + data: ../en/data nav: - FastAPI: index.md - Languages: @@ -60,7 +60,6 @@ nav: - ru: /ru/ - sq: /sq/ - sv: /sv/ - - ta: /ta/ - tr: /tr/ - uk: /uk/ - zh: /zh/ @@ -252,8 +251,6 @@ extra: name: sq - shqip - link: /sv/ name: sv - svenska - - link: /ta/ - name: ta - தமிழ் - link: /tr/ name: tr - Türkçe - link: /uk/ diff --git a/docs/es/mkdocs.yml b/docs/es/mkdocs.yml index d8aa9c494..de18856f4 100644 --- a/docs/es/mkdocs.yml +++ b/docs/es/mkdocs.yml @@ -1,173 +1 @@ -site_name: FastAPI -site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production -site_url: https://fastapi.tiangolo.com/es/ -theme: - name: material - custom_dir: overrides - palette: - - media: '(prefers-color-scheme: light)' - scheme: default - primary: teal - accent: amber - toggle: - icon: material/lightbulb - name: Switch to dark mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - primary: teal - accent: amber - toggle: - icon: material/lightbulb-outline - name: Switch to light mode - features: - - search.suggest - - search.highlight - - content.tabs.link - - navigation.indexes - icon: - repo: fontawesome/brands/github-alt - logo: https://fastapi.tiangolo.com/img/icon-white.svg - favicon: https://fastapi.tiangolo.com/img/favicon.png - language: es -repo_name: tiangolo/fastapi -repo_url: https://github.com/tiangolo/fastapi -edit_uri: '' -plugins: -- search -- markdownextradata: - data: data -nav: -- FastAPI: index.md -- Languages: - - en: / - - az: /az/ - - cs: /cs/ - - de: /de/ - - em: /em/ - - es: /es/ - - fa: /fa/ - - fr: /fr/ - - he: /he/ - - hy: /hy/ - - id: /id/ - - it: /it/ - - ja: /ja/ - - ko: /ko/ - - lo: /lo/ - - nl: /nl/ - - pl: /pl/ - - pt: /pt/ - - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - - ta: /ta/ - - tr: /tr/ - - uk: /uk/ - - zh: /zh/ -- features.md -- python-types.md -- Tutorial - Guía de Usuario: - - tutorial/index.md - - tutorial/first-steps.md - - tutorial/path-params.md - - tutorial/query-params.md -- Guía de Usuario Avanzada: - - advanced/index.md -- async.md -markdown_extensions: -- toc: - permalink: true -- markdown.extensions.codehilite: - guess_lang: false -- mdx_include: - base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: - alternate_style: true -- attr_list -- md_in_html -extra: - analytics: - provider: google - property: G-YNEVN69SC3 - social: - - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/fastapi - - icon: fontawesome/brands/discord - link: https://discord.gg/VQjSZaeJmf - - icon: fontawesome/brands/twitter - link: https://twitter.com/fastapi - - icon: fontawesome/brands/linkedin - link: https://www.linkedin.com/in/tiangolo - - icon: fontawesome/brands/dev - link: https://dev.to/tiangolo - - icon: fontawesome/brands/medium - link: https://medium.com/@tiangolo - - icon: fontawesome/solid/globe - link: https://tiangolo.com - alternate: - - link: / - name: en - English - - link: /az/ - name: az - - link: /cs/ - name: cs - - link: /de/ - name: de - - link: /em/ - name: 😉 - - link: /es/ - name: es - español - - link: /fa/ - name: fa - - link: /fr/ - name: fr - français - - link: /he/ - name: he - - link: /hy/ - name: hy - - link: /id/ - name: id - - link: /it/ - name: it - italiano - - link: /ja/ - name: ja - 日本語 - - link: /ko/ - name: ko - 한국어 - - link: /lo/ - name: lo - ພາສາລາວ - - link: /nl/ - name: nl - - link: /pl/ - name: pl - - link: /pt/ - name: pt - português - - link: /ru/ - name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - - link: /ta/ - name: ta - தமிழ் - - link: /tr/ - name: tr - Türkçe - - link: /uk/ - name: uk - українська мова - - link: /zh/ - name: zh - 汉语 -extra_css: -- https://fastapi.tiangolo.com/css/termynal.css -- https://fastapi.tiangolo.com/css/custom.css -extra_javascript: -- https://fastapi.tiangolo.com/js/termynal.js -- https://fastapi.tiangolo.com/js/custom.js -hooks: -- ../../scripts/mkdocs_hooks.py +INHERIT: ../en/mkdocs.yml diff --git a/docs/es/overrides/.gitignore b/docs/es/overrides/.gitignore deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/fa/mkdocs.yml b/docs/fa/mkdocs.yml index 287521ab3..de18856f4 100644 --- a/docs/fa/mkdocs.yml +++ b/docs/fa/mkdocs.yml @@ -1,163 +1 @@ -site_name: FastAPI -site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production -site_url: https://fastapi.tiangolo.com/fa/ -theme: - name: material - custom_dir: overrides - palette: - - media: '(prefers-color-scheme: light)' - scheme: default - primary: teal - accent: amber - toggle: - icon: material/lightbulb - name: Switch to dark mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - primary: teal - accent: amber - toggle: - icon: material/lightbulb-outline - name: Switch to light mode - features: - - search.suggest - - search.highlight - - content.tabs.link - - navigation.indexes - icon: - repo: fontawesome/brands/github-alt - logo: https://fastapi.tiangolo.com/img/icon-white.svg - favicon: https://fastapi.tiangolo.com/img/favicon.png - language: fa -repo_name: tiangolo/fastapi -repo_url: https://github.com/tiangolo/fastapi -edit_uri: '' -plugins: -- search -- markdownextradata: - data: data -nav: -- FastAPI: index.md -- Languages: - - en: / - - az: /az/ - - cs: /cs/ - - de: /de/ - - em: /em/ - - es: /es/ - - fa: /fa/ - - fr: /fr/ - - he: /he/ - - hy: /hy/ - - id: /id/ - - it: /it/ - - ja: /ja/ - - ko: /ko/ - - lo: /lo/ - - nl: /nl/ - - pl: /pl/ - - pt: /pt/ - - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - - ta: /ta/ - - tr: /tr/ - - uk: /uk/ - - zh: /zh/ -markdown_extensions: -- toc: - permalink: true -- markdown.extensions.codehilite: - guess_lang: false -- mdx_include: - base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: - alternate_style: true -- attr_list -- md_in_html -extra: - analytics: - provider: google - property: G-YNEVN69SC3 - social: - - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/fastapi - - icon: fontawesome/brands/discord - link: https://discord.gg/VQjSZaeJmf - - icon: fontawesome/brands/twitter - link: https://twitter.com/fastapi - - icon: fontawesome/brands/linkedin - link: https://www.linkedin.com/in/tiangolo - - icon: fontawesome/brands/dev - link: https://dev.to/tiangolo - - icon: fontawesome/brands/medium - link: https://medium.com/@tiangolo - - icon: fontawesome/solid/globe - link: https://tiangolo.com - alternate: - - link: / - name: en - English - - link: /az/ - name: az - - link: /cs/ - name: cs - - link: /de/ - name: de - - link: /em/ - name: 😉 - - link: /es/ - name: es - español - - link: /fa/ - name: fa - - link: /fr/ - name: fr - français - - link: /he/ - name: he - - link: /hy/ - name: hy - - link: /id/ - name: id - - link: /it/ - name: it - italiano - - link: /ja/ - name: ja - 日本語 - - link: /ko/ - name: ko - 한국어 - - link: /lo/ - name: lo - ພາສາລາວ - - link: /nl/ - name: nl - - link: /pl/ - name: pl - - link: /pt/ - name: pt - português - - link: /ru/ - name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - - link: /ta/ - name: ta - தமிழ் - - link: /tr/ - name: tr - Türkçe - - link: /uk/ - name: uk - українська мова - - link: /zh/ - name: zh - 汉语 -extra_css: -- https://fastapi.tiangolo.com/css/termynal.css -- https://fastapi.tiangolo.com/css/custom.css -extra_javascript: -- https://fastapi.tiangolo.com/js/termynal.js -- https://fastapi.tiangolo.com/js/custom.js -hooks: -- ../../scripts/mkdocs_hooks.py +INHERIT: ../en/mkdocs.yml diff --git a/docs/fa/overrides/.gitignore b/docs/fa/overrides/.gitignore deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/fr/mkdocs.yml b/docs/fr/mkdocs.yml index 67e5383ed..de18856f4 100644 --- a/docs/fr/mkdocs.yml +++ b/docs/fr/mkdocs.yml @@ -1,192 +1 @@ -site_name: FastAPI -site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production -site_url: https://fastapi.tiangolo.com/fr/ -theme: - name: material - custom_dir: overrides - palette: - - media: '(prefers-color-scheme: light)' - scheme: default - primary: teal - accent: amber - toggle: - icon: material/lightbulb - name: Switch to dark mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - primary: teal - accent: amber - toggle: - icon: material/lightbulb-outline - name: Switch to light mode - features: - - search.suggest - - search.highlight - - content.tabs.link - - navigation.indexes - icon: - repo: fontawesome/brands/github-alt - logo: https://fastapi.tiangolo.com/img/icon-white.svg - favicon: https://fastapi.tiangolo.com/img/favicon.png - language: fr -repo_name: tiangolo/fastapi -repo_url: https://github.com/tiangolo/fastapi -edit_uri: '' -plugins: -- search -- markdownextradata: - data: data -nav: -- FastAPI: index.md -- Languages: - - en: / - - az: /az/ - - cs: /cs/ - - de: /de/ - - em: /em/ - - es: /es/ - - fa: /fa/ - - fr: /fr/ - - he: /he/ - - hy: /hy/ - - id: /id/ - - it: /it/ - - ja: /ja/ - - ko: /ko/ - - lo: /lo/ - - nl: /nl/ - - pl: /pl/ - - pt: /pt/ - - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - - ta: /ta/ - - tr: /tr/ - - uk: /uk/ - - zh: /zh/ -- features.md -- fastapi-people.md -- python-types.md -- Tutoriel - Guide utilisateur: - - tutorial/first-steps.md - - tutorial/path-params.md - - tutorial/query-params.md - - tutorial/body.md - - tutorial/background-tasks.md - - tutorial/debugging.md -- Guide utilisateur avancé: - - advanced/index.md - - advanced/path-operation-advanced-configuration.md - - advanced/additional-status-codes.md - - advanced/response-directly.md - - advanced/additional-responses.md -- async.md -- Déploiement: - - deployment/index.md - - deployment/versions.md - - deployment/https.md - - deployment/deta.md - - deployment/docker.md - - deployment/manually.md -- project-generation.md -- alternatives.md -- history-design-future.md -- external-links.md -- help-fastapi.md -markdown_extensions: -- toc: - permalink: true -- markdown.extensions.codehilite: - guess_lang: false -- mdx_include: - base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: - alternate_style: true -- attr_list -- md_in_html -extra: - analytics: - provider: google - property: G-YNEVN69SC3 - social: - - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/fastapi - - icon: fontawesome/brands/discord - link: https://discord.gg/VQjSZaeJmf - - icon: fontawesome/brands/twitter - link: https://twitter.com/fastapi - - icon: fontawesome/brands/linkedin - link: https://www.linkedin.com/in/tiangolo - - icon: fontawesome/brands/dev - link: https://dev.to/tiangolo - - icon: fontawesome/brands/medium - link: https://medium.com/@tiangolo - - icon: fontawesome/solid/globe - link: https://tiangolo.com - alternate: - - link: / - name: en - English - - link: /az/ - name: az - - link: /cs/ - name: cs - - link: /de/ - name: de - - link: /em/ - name: 😉 - - link: /es/ - name: es - español - - link: /fa/ - name: fa - - link: /fr/ - name: fr - français - - link: /he/ - name: he - - link: /hy/ - name: hy - - link: /id/ - name: id - - link: /it/ - name: it - italiano - - link: /ja/ - name: ja - 日本語 - - link: /ko/ - name: ko - 한국어 - - link: /lo/ - name: lo - ພາສາລາວ - - link: /nl/ - name: nl - - link: /pl/ - name: pl - - link: /pt/ - name: pt - português - - link: /ru/ - name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - - link: /ta/ - name: ta - தமிழ் - - link: /tr/ - name: tr - Türkçe - - link: /uk/ - name: uk - українська мова - - link: /zh/ - name: zh - 汉语 -extra_css: -- https://fastapi.tiangolo.com/css/termynal.css -- https://fastapi.tiangolo.com/css/custom.css -extra_javascript: -- https://fastapi.tiangolo.com/js/termynal.js -- https://fastapi.tiangolo.com/js/custom.js -hooks: -- ../../scripts/mkdocs_hooks.py +INHERIT: ../en/mkdocs.yml diff --git a/docs/fr/overrides/.gitignore b/docs/fr/overrides/.gitignore deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/he/mkdocs.yml b/docs/he/mkdocs.yml index b390875ea..de18856f4 100644 --- a/docs/he/mkdocs.yml +++ b/docs/he/mkdocs.yml @@ -1,163 +1 @@ -site_name: FastAPI -site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production -site_url: https://fastapi.tiangolo.com/he/ -theme: - name: material - custom_dir: overrides - palette: - - media: '(prefers-color-scheme: light)' - scheme: default - primary: teal - accent: amber - toggle: - icon: material/lightbulb - name: Switch to dark mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - primary: teal - accent: amber - toggle: - icon: material/lightbulb-outline - name: Switch to light mode - features: - - search.suggest - - search.highlight - - content.tabs.link - - navigation.indexes - icon: - repo: fontawesome/brands/github-alt - logo: https://fastapi.tiangolo.com/img/icon-white.svg - favicon: https://fastapi.tiangolo.com/img/favicon.png - language: he -repo_name: tiangolo/fastapi -repo_url: https://github.com/tiangolo/fastapi -edit_uri: '' -plugins: -- search -- markdownextradata: - data: data -nav: -- FastAPI: index.md -- Languages: - - en: / - - az: /az/ - - cs: /cs/ - - de: /de/ - - em: /em/ - - es: /es/ - - fa: /fa/ - - fr: /fr/ - - he: /he/ - - hy: /hy/ - - id: /id/ - - it: /it/ - - ja: /ja/ - - ko: /ko/ - - lo: /lo/ - - nl: /nl/ - - pl: /pl/ - - pt: /pt/ - - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - - ta: /ta/ - - tr: /tr/ - - uk: /uk/ - - zh: /zh/ -markdown_extensions: -- toc: - permalink: true -- markdown.extensions.codehilite: - guess_lang: false -- mdx_include: - base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: - alternate_style: true -- attr_list -- md_in_html -extra: - analytics: - provider: google - property: G-YNEVN69SC3 - social: - - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/fastapi - - icon: fontawesome/brands/discord - link: https://discord.gg/VQjSZaeJmf - - icon: fontawesome/brands/twitter - link: https://twitter.com/fastapi - - icon: fontawesome/brands/linkedin - link: https://www.linkedin.com/in/tiangolo - - icon: fontawesome/brands/dev - link: https://dev.to/tiangolo - - icon: fontawesome/brands/medium - link: https://medium.com/@tiangolo - - icon: fontawesome/solid/globe - link: https://tiangolo.com - alternate: - - link: / - name: en - English - - link: /az/ - name: az - - link: /cs/ - name: cs - - link: /de/ - name: de - - link: /em/ - name: 😉 - - link: /es/ - name: es - español - - link: /fa/ - name: fa - - link: /fr/ - name: fr - français - - link: /he/ - name: he - - link: /hy/ - name: hy - - link: /id/ - name: id - - link: /it/ - name: it - italiano - - link: /ja/ - name: ja - 日本語 - - link: /ko/ - name: ko - 한국어 - - link: /lo/ - name: lo - ພາສາລາວ - - link: /nl/ - name: nl - - link: /pl/ - name: pl - - link: /pt/ - name: pt - português - - link: /ru/ - name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - - link: /ta/ - name: ta - தமிழ் - - link: /tr/ - name: tr - Türkçe - - link: /uk/ - name: uk - українська мова - - link: /zh/ - name: zh - 汉语 -extra_css: -- https://fastapi.tiangolo.com/css/termynal.css -- https://fastapi.tiangolo.com/css/custom.css -extra_javascript: -- https://fastapi.tiangolo.com/js/termynal.js -- https://fastapi.tiangolo.com/js/custom.js -hooks: -- ../../scripts/mkdocs_hooks.py +INHERIT: ../en/mkdocs.yml diff --git a/docs/he/overrides/.gitignore b/docs/he/overrides/.gitignore deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/hy/mkdocs.yml b/docs/hy/mkdocs.yml index e5af7dd30..de18856f4 100644 --- a/docs/hy/mkdocs.yml +++ b/docs/hy/mkdocs.yml @@ -1,163 +1 @@ -site_name: FastAPI -site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production -site_url: https://fastapi.tiangolo.com/hy/ -theme: - name: material - custom_dir: overrides - palette: - - media: '(prefers-color-scheme: light)' - scheme: default - primary: teal - accent: amber - toggle: - icon: material/lightbulb - name: Switch to dark mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - primary: teal - accent: amber - toggle: - icon: material/lightbulb-outline - name: Switch to light mode - features: - - search.suggest - - search.highlight - - content.tabs.link - - navigation.indexes - icon: - repo: fontawesome/brands/github-alt - logo: https://fastapi.tiangolo.com/img/icon-white.svg - favicon: https://fastapi.tiangolo.com/img/favicon.png - language: hy -repo_name: tiangolo/fastapi -repo_url: https://github.com/tiangolo/fastapi -edit_uri: '' -plugins: -- search -- markdownextradata: - data: data -nav: -- FastAPI: index.md -- Languages: - - en: / - - az: /az/ - - cs: /cs/ - - de: /de/ - - em: /em/ - - es: /es/ - - fa: /fa/ - - fr: /fr/ - - he: /he/ - - hy: /hy/ - - id: /id/ - - it: /it/ - - ja: /ja/ - - ko: /ko/ - - lo: /lo/ - - nl: /nl/ - - pl: /pl/ - - pt: /pt/ - - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - - ta: /ta/ - - tr: /tr/ - - uk: /uk/ - - zh: /zh/ -markdown_extensions: -- toc: - permalink: true -- markdown.extensions.codehilite: - guess_lang: false -- mdx_include: - base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: - alternate_style: true -- attr_list -- md_in_html -extra: - analytics: - provider: google - property: G-YNEVN69SC3 - social: - - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/fastapi - - icon: fontawesome/brands/discord - link: https://discord.gg/VQjSZaeJmf - - icon: fontawesome/brands/twitter - link: https://twitter.com/fastapi - - icon: fontawesome/brands/linkedin - link: https://www.linkedin.com/in/tiangolo - - icon: fontawesome/brands/dev - link: https://dev.to/tiangolo - - icon: fontawesome/brands/medium - link: https://medium.com/@tiangolo - - icon: fontawesome/solid/globe - link: https://tiangolo.com - alternate: - - link: / - name: en - English - - link: /az/ - name: az - - link: /cs/ - name: cs - - link: /de/ - name: de - - link: /em/ - name: 😉 - - link: /es/ - name: es - español - - link: /fa/ - name: fa - - link: /fr/ - name: fr - français - - link: /he/ - name: he - - link: /hy/ - name: hy - - link: /id/ - name: id - - link: /it/ - name: it - italiano - - link: /ja/ - name: ja - 日本語 - - link: /ko/ - name: ko - 한국어 - - link: /lo/ - name: lo - ພາສາລາວ - - link: /nl/ - name: nl - - link: /pl/ - name: pl - - link: /pt/ - name: pt - português - - link: /ru/ - name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - - link: /ta/ - name: ta - தமிழ் - - link: /tr/ - name: tr - Türkçe - - link: /uk/ - name: uk - українська мова - - link: /zh/ - name: zh - 汉语 -extra_css: -- https://fastapi.tiangolo.com/css/termynal.css -- https://fastapi.tiangolo.com/css/custom.css -extra_javascript: -- https://fastapi.tiangolo.com/js/termynal.js -- https://fastapi.tiangolo.com/js/custom.js -hooks: -- ../../scripts/mkdocs_hooks.py +INHERIT: ../en/mkdocs.yml diff --git a/docs/hy/overrides/.gitignore b/docs/hy/overrides/.gitignore deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/id/mkdocs.yml b/docs/id/mkdocs.yml index 6cc2cf045..de18856f4 100644 --- a/docs/id/mkdocs.yml +++ b/docs/id/mkdocs.yml @@ -1,163 +1 @@ -site_name: FastAPI -site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production -site_url: https://fastapi.tiangolo.com/id/ -theme: - name: material - custom_dir: overrides - palette: - - media: '(prefers-color-scheme: light)' - scheme: default - primary: teal - accent: amber - toggle: - icon: material/lightbulb - name: Switch to dark mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - primary: teal - accent: amber - toggle: - icon: material/lightbulb-outline - name: Switch to light mode - features: - - search.suggest - - search.highlight - - content.tabs.link - - navigation.indexes - icon: - repo: fontawesome/brands/github-alt - logo: https://fastapi.tiangolo.com/img/icon-white.svg - favicon: https://fastapi.tiangolo.com/img/favicon.png - language: id -repo_name: tiangolo/fastapi -repo_url: https://github.com/tiangolo/fastapi -edit_uri: '' -plugins: -- search -- markdownextradata: - data: data -nav: -- FastAPI: index.md -- Languages: - - en: / - - az: /az/ - - cs: /cs/ - - de: /de/ - - em: /em/ - - es: /es/ - - fa: /fa/ - - fr: /fr/ - - he: /he/ - - hy: /hy/ - - id: /id/ - - it: /it/ - - ja: /ja/ - - ko: /ko/ - - lo: /lo/ - - nl: /nl/ - - pl: /pl/ - - pt: /pt/ - - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - - ta: /ta/ - - tr: /tr/ - - uk: /uk/ - - zh: /zh/ -markdown_extensions: -- toc: - permalink: true -- markdown.extensions.codehilite: - guess_lang: false -- mdx_include: - base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: - alternate_style: true -- attr_list -- md_in_html -extra: - analytics: - provider: google - property: G-YNEVN69SC3 - social: - - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/fastapi - - icon: fontawesome/brands/discord - link: https://discord.gg/VQjSZaeJmf - - icon: fontawesome/brands/twitter - link: https://twitter.com/fastapi - - icon: fontawesome/brands/linkedin - link: https://www.linkedin.com/in/tiangolo - - icon: fontawesome/brands/dev - link: https://dev.to/tiangolo - - icon: fontawesome/brands/medium - link: https://medium.com/@tiangolo - - icon: fontawesome/solid/globe - link: https://tiangolo.com - alternate: - - link: / - name: en - English - - link: /az/ - name: az - - link: /cs/ - name: cs - - link: /de/ - name: de - - link: /em/ - name: 😉 - - link: /es/ - name: es - español - - link: /fa/ - name: fa - - link: /fr/ - name: fr - français - - link: /he/ - name: he - - link: /hy/ - name: hy - - link: /id/ - name: id - - link: /it/ - name: it - italiano - - link: /ja/ - name: ja - 日本語 - - link: /ko/ - name: ko - 한국어 - - link: /lo/ - name: lo - ພາສາລາວ - - link: /nl/ - name: nl - - link: /pl/ - name: pl - - link: /pt/ - name: pt - português - - link: /ru/ - name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - - link: /ta/ - name: ta - தமிழ் - - link: /tr/ - name: tr - Türkçe - - link: /uk/ - name: uk - українська мова - - link: /zh/ - name: zh - 汉语 -extra_css: -- https://fastapi.tiangolo.com/css/termynal.css -- https://fastapi.tiangolo.com/css/custom.css -extra_javascript: -- https://fastapi.tiangolo.com/js/termynal.js -- https://fastapi.tiangolo.com/js/custom.js -hooks: -- ../../scripts/mkdocs_hooks.py +INHERIT: ../en/mkdocs.yml diff --git a/docs/id/overrides/.gitignore b/docs/id/overrides/.gitignore deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/it/mkdocs.yml b/docs/it/mkdocs.yml index f7de769ee..de18856f4 100644 --- a/docs/it/mkdocs.yml +++ b/docs/it/mkdocs.yml @@ -1,163 +1 @@ -site_name: FastAPI -site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production -site_url: https://fastapi.tiangolo.com/it/ -theme: - name: material - custom_dir: overrides - palette: - - media: '(prefers-color-scheme: light)' - scheme: default - primary: teal - accent: amber - toggle: - icon: material/lightbulb - name: Switch to dark mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - primary: teal - accent: amber - toggle: - icon: material/lightbulb-outline - name: Switch to light mode - features: - - search.suggest - - search.highlight - - content.tabs.link - - navigation.indexes - icon: - repo: fontawesome/brands/github-alt - logo: https://fastapi.tiangolo.com/img/icon-white.svg - favicon: https://fastapi.tiangolo.com/img/favicon.png - language: it -repo_name: tiangolo/fastapi -repo_url: https://github.com/tiangolo/fastapi -edit_uri: '' -plugins: -- search -- markdownextradata: - data: data -nav: -- FastAPI: index.md -- Languages: - - en: / - - az: /az/ - - cs: /cs/ - - de: /de/ - - em: /em/ - - es: /es/ - - fa: /fa/ - - fr: /fr/ - - he: /he/ - - hy: /hy/ - - id: /id/ - - it: /it/ - - ja: /ja/ - - ko: /ko/ - - lo: /lo/ - - nl: /nl/ - - pl: /pl/ - - pt: /pt/ - - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - - ta: /ta/ - - tr: /tr/ - - uk: /uk/ - - zh: /zh/ -markdown_extensions: -- toc: - permalink: true -- markdown.extensions.codehilite: - guess_lang: false -- mdx_include: - base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: - alternate_style: true -- attr_list -- md_in_html -extra: - analytics: - provider: google - property: G-YNEVN69SC3 - social: - - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/fastapi - - icon: fontawesome/brands/discord - link: https://discord.gg/VQjSZaeJmf - - icon: fontawesome/brands/twitter - link: https://twitter.com/fastapi - - icon: fontawesome/brands/linkedin - link: https://www.linkedin.com/in/tiangolo - - icon: fontawesome/brands/dev - link: https://dev.to/tiangolo - - icon: fontawesome/brands/medium - link: https://medium.com/@tiangolo - - icon: fontawesome/solid/globe - link: https://tiangolo.com - alternate: - - link: / - name: en - English - - link: /az/ - name: az - - link: /cs/ - name: cs - - link: /de/ - name: de - - link: /em/ - name: 😉 - - link: /es/ - name: es - español - - link: /fa/ - name: fa - - link: /fr/ - name: fr - français - - link: /he/ - name: he - - link: /hy/ - name: hy - - link: /id/ - name: id - - link: /it/ - name: it - italiano - - link: /ja/ - name: ja - 日本語 - - link: /ko/ - name: ko - 한국어 - - link: /lo/ - name: lo - ພາສາລາວ - - link: /nl/ - name: nl - - link: /pl/ - name: pl - - link: /pt/ - name: pt - português - - link: /ru/ - name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - - link: /ta/ - name: ta - தமிழ் - - link: /tr/ - name: tr - Türkçe - - link: /uk/ - name: uk - українська мова - - link: /zh/ - name: zh - 汉语 -extra_css: -- https://fastapi.tiangolo.com/css/termynal.css -- https://fastapi.tiangolo.com/css/custom.css -extra_javascript: -- https://fastapi.tiangolo.com/js/termynal.js -- https://fastapi.tiangolo.com/js/custom.js -hooks: -- ../../scripts/mkdocs_hooks.py +INHERIT: ../en/mkdocs.yml diff --git a/docs/it/overrides/.gitignore b/docs/it/overrides/.gitignore deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/ja/mkdocs.yml b/docs/ja/mkdocs.yml index f21d731f9..de18856f4 100644 --- a/docs/ja/mkdocs.yml +++ b/docs/ja/mkdocs.yml @@ -1,207 +1 @@ -site_name: FastAPI -site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production -site_url: https://fastapi.tiangolo.com/ja/ -theme: - name: material - custom_dir: overrides - palette: - - media: '(prefers-color-scheme: light)' - scheme: default - primary: teal - accent: amber - toggle: - icon: material/lightbulb - name: Switch to dark mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - primary: teal - accent: amber - toggle: - icon: material/lightbulb-outline - name: Switch to light mode - features: - - search.suggest - - search.highlight - - content.tabs.link - - navigation.indexes - icon: - repo: fontawesome/brands/github-alt - logo: https://fastapi.tiangolo.com/img/icon-white.svg - favicon: https://fastapi.tiangolo.com/img/favicon.png - language: ja -repo_name: tiangolo/fastapi -repo_url: https://github.com/tiangolo/fastapi -edit_uri: '' -plugins: -- search -- markdownextradata: - data: data -nav: -- FastAPI: index.md -- Languages: - - en: / - - az: /az/ - - cs: /cs/ - - de: /de/ - - em: /em/ - - es: /es/ - - fa: /fa/ - - fr: /fr/ - - he: /he/ - - hy: /hy/ - - id: /id/ - - it: /it/ - - ja: /ja/ - - ko: /ko/ - - lo: /lo/ - - nl: /nl/ - - pl: /pl/ - - pt: /pt/ - - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - - ta: /ta/ - - tr: /tr/ - - uk: /uk/ - - zh: /zh/ -- features.md -- fastapi-people.md -- チュートリアル - ユーザーガイド: - - tutorial/index.md - - tutorial/first-steps.md - - tutorial/path-params.md - - tutorial/query-params.md - - tutorial/body.md - - tutorial/query-params-str-validations.md - - tutorial/cookie-params.md - - tutorial/header-params.md - - tutorial/request-forms.md - - tutorial/body-updates.md - - セキュリティ: - - tutorial/security/first-steps.md - - tutorial/security/oauth2-jwt.md - - tutorial/middleware.md - - tutorial/cors.md - - tutorial/static-files.md - - tutorial/testing.md - - tutorial/debugging.md -- 高度なユーザーガイド: - - advanced/index.md - - advanced/path-operation-advanced-configuration.md - - advanced/additional-status-codes.md - - advanced/response-directly.md - - advanced/custom-response.md - - advanced/nosql-databases.md - - advanced/websockets.md - - advanced/conditional-openapi.md -- async.md -- デプロイ: - - deployment/index.md - - deployment/versions.md - - deployment/deta.md - - deployment/docker.md - - deployment/manually.md -- project-generation.md -- alternatives.md -- history-design-future.md -- external-links.md -- benchmarks.md -- help-fastapi.md -- contributing.md -markdown_extensions: -- toc: - permalink: true -- markdown.extensions.codehilite: - guess_lang: false -- mdx_include: - base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: - alternate_style: true -- attr_list -- md_in_html -extra: - analytics: - provider: google - property: G-YNEVN69SC3 - social: - - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/fastapi - - icon: fontawesome/brands/discord - link: https://discord.gg/VQjSZaeJmf - - icon: fontawesome/brands/twitter - link: https://twitter.com/fastapi - - icon: fontawesome/brands/linkedin - link: https://www.linkedin.com/in/tiangolo - - icon: fontawesome/brands/dev - link: https://dev.to/tiangolo - - icon: fontawesome/brands/medium - link: https://medium.com/@tiangolo - - icon: fontawesome/solid/globe - link: https://tiangolo.com - alternate: - - link: / - name: en - English - - link: /az/ - name: az - - link: /cs/ - name: cs - - link: /de/ - name: de - - link: /em/ - name: 😉 - - link: /es/ - name: es - español - - link: /fa/ - name: fa - - link: /fr/ - name: fr - français - - link: /he/ - name: he - - link: /hy/ - name: hy - - link: /id/ - name: id - - link: /it/ - name: it - italiano - - link: /ja/ - name: ja - 日本語 - - link: /ko/ - name: ko - 한국어 - - link: /lo/ - name: lo - ພາສາລາວ - - link: /nl/ - name: nl - - link: /pl/ - name: pl - - link: /pt/ - name: pt - português - - link: /ru/ - name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - - link: /ta/ - name: ta - தமிழ் - - link: /tr/ - name: tr - Türkçe - - link: /uk/ - name: uk - українська мова - - link: /zh/ - name: zh - 汉语 -extra_css: -- https://fastapi.tiangolo.com/css/termynal.css -- https://fastapi.tiangolo.com/css/custom.css -extra_javascript: -- https://fastapi.tiangolo.com/js/termynal.js -- https://fastapi.tiangolo.com/js/custom.js -hooks: -- ../../scripts/mkdocs_hooks.py +INHERIT: ../en/mkdocs.yml diff --git a/docs/ja/overrides/.gitignore b/docs/ja/overrides/.gitignore deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/ko/mkdocs.yml b/docs/ko/mkdocs.yml index 0a1e6b639..de18856f4 100644 --- a/docs/ko/mkdocs.yml +++ b/docs/ko/mkdocs.yml @@ -1,177 +1 @@ -site_name: FastAPI -site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production -site_url: https://fastapi.tiangolo.com/ko/ -theme: - name: material - custom_dir: overrides - palette: - - media: '(prefers-color-scheme: light)' - scheme: default - primary: teal - accent: amber - toggle: - icon: material/lightbulb - name: Switch to dark mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - primary: teal - accent: amber - toggle: - icon: material/lightbulb-outline - name: Switch to light mode - features: - - search.suggest - - search.highlight - - content.tabs.link - - navigation.indexes - icon: - repo: fontawesome/brands/github-alt - logo: https://fastapi.tiangolo.com/img/icon-white.svg - favicon: https://fastapi.tiangolo.com/img/favicon.png - language: en -repo_name: tiangolo/fastapi -repo_url: https://github.com/tiangolo/fastapi -edit_uri: '' -plugins: -- search -- markdownextradata: - data: data -nav: -- FastAPI: index.md -- Languages: - - en: / - - az: /az/ - - cs: /cs/ - - de: /de/ - - em: /em/ - - es: /es/ - - fa: /fa/ - - fr: /fr/ - - he: /he/ - - hy: /hy/ - - id: /id/ - - it: /it/ - - ja: /ja/ - - ko: /ko/ - - lo: /lo/ - - nl: /nl/ - - pl: /pl/ - - pt: /pt/ - - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - - ta: /ta/ - - tr: /tr/ - - uk: /uk/ - - zh: /zh/ -- 자습서 - 사용자 안내서: - - tutorial/index.md - - tutorial/first-steps.md - - tutorial/path-params.md - - tutorial/query-params.md - - tutorial/header-params.md - - tutorial/path-params-numeric-validations.md - - tutorial/response-status-code.md - - tutorial/request-files.md - - tutorial/request-forms-and-files.md - - tutorial/encoder.md - - tutorial/cors.md - - 의존성: - - tutorial/dependencies/classes-as-dependencies.md -markdown_extensions: -- toc: - permalink: true -- markdown.extensions.codehilite: - guess_lang: false -- mdx_include: - base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: - alternate_style: true -- attr_list -- md_in_html -extra: - analytics: - provider: google - property: G-YNEVN69SC3 - social: - - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/fastapi - - icon: fontawesome/brands/discord - link: https://discord.gg/VQjSZaeJmf - - icon: fontawesome/brands/twitter - link: https://twitter.com/fastapi - - icon: fontawesome/brands/linkedin - link: https://www.linkedin.com/in/tiangolo - - icon: fontawesome/brands/dev - link: https://dev.to/tiangolo - - icon: fontawesome/brands/medium - link: https://medium.com/@tiangolo - - icon: fontawesome/solid/globe - link: https://tiangolo.com - alternate: - - link: / - name: en - English - - link: /az/ - name: az - - link: /cs/ - name: cs - - link: /de/ - name: de - - link: /em/ - name: 😉 - - link: /es/ - name: es - español - - link: /fa/ - name: fa - - link: /fr/ - name: fr - français - - link: /he/ - name: he - - link: /hy/ - name: hy - - link: /id/ - name: id - - link: /it/ - name: it - italiano - - link: /ja/ - name: ja - 日本語 - - link: /ko/ - name: ko - 한국어 - - link: /lo/ - name: lo - ພາສາລາວ - - link: /nl/ - name: nl - - link: /pl/ - name: pl - - link: /pt/ - name: pt - português - - link: /ru/ - name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - - link: /ta/ - name: ta - தமிழ் - - link: /tr/ - name: tr - Türkçe - - link: /uk/ - name: uk - українська мова - - link: /zh/ - name: zh - 汉语 -extra_css: -- https://fastapi.tiangolo.com/css/termynal.css -- https://fastapi.tiangolo.com/css/custom.css -extra_javascript: -- https://fastapi.tiangolo.com/js/termynal.js -- https://fastapi.tiangolo.com/js/custom.js -hooks: -- ../../scripts/mkdocs_hooks.py +INHERIT: ../en/mkdocs.yml diff --git a/docs/ko/overrides/.gitignore b/docs/ko/overrides/.gitignore deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/lo/mkdocs.yml b/docs/lo/mkdocs.yml index 7f9253d6c..de18856f4 100644 --- a/docs/lo/mkdocs.yml +++ b/docs/lo/mkdocs.yml @@ -1,163 +1 @@ -site_name: FastAPI -site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production -site_url: https://fastapi.tiangolo.com/lo/ -theme: - name: material - custom_dir: overrides - palette: - - media: '(prefers-color-scheme: light)' - scheme: default - primary: teal - accent: amber - toggle: - icon: material/lightbulb - name: Switch to dark mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - primary: teal - accent: amber - toggle: - icon: material/lightbulb-outline - name: Switch to light mode - features: - - search.suggest - - search.highlight - - content.tabs.link - - navigation.indexes - icon: - repo: fontawesome/brands/github-alt - logo: https://fastapi.tiangolo.com/img/icon-white.svg - favicon: https://fastapi.tiangolo.com/img/favicon.png - language: en -repo_name: tiangolo/fastapi -repo_url: https://github.com/tiangolo/fastapi -edit_uri: '' -plugins: -- search -- markdownextradata: - data: data -nav: -- FastAPI: index.md -- Languages: - - en: / - - az: /az/ - - cs: /cs/ - - de: /de/ - - em: /em/ - - es: /es/ - - fa: /fa/ - - fr: /fr/ - - he: /he/ - - hy: /hy/ - - id: /id/ - - it: /it/ - - ja: /ja/ - - ko: /ko/ - - lo: /lo/ - - nl: /nl/ - - pl: /pl/ - - pt: /pt/ - - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - - ta: /ta/ - - tr: /tr/ - - uk: /uk/ - - zh: /zh/ -markdown_extensions: -- toc: - permalink: true -- markdown.extensions.codehilite: - guess_lang: false -- mdx_include: - base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: - alternate_style: true -- attr_list -- md_in_html -extra: - analytics: - provider: google - property: G-YNEVN69SC3 - social: - - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/fastapi - - icon: fontawesome/brands/discord - link: https://discord.gg/VQjSZaeJmf - - icon: fontawesome/brands/twitter - link: https://twitter.com/fastapi - - icon: fontawesome/brands/linkedin - link: https://www.linkedin.com/in/tiangolo - - icon: fontawesome/brands/dev - link: https://dev.to/tiangolo - - icon: fontawesome/brands/medium - link: https://medium.com/@tiangolo - - icon: fontawesome/solid/globe - link: https://tiangolo.com - alternate: - - link: / - name: en - English - - link: /az/ - name: az - - link: /cs/ - name: cs - - link: /de/ - name: de - - link: /em/ - name: 😉 - - link: /es/ - name: es - español - - link: /fa/ - name: fa - - link: /fr/ - name: fr - français - - link: /he/ - name: he - - link: /hy/ - name: hy - - link: /id/ - name: id - - link: /it/ - name: it - italiano - - link: /ja/ - name: ja - 日本語 - - link: /ko/ - name: ko - 한국어 - - link: /lo/ - name: lo - ພາສາລາວ - - link: /nl/ - name: nl - - link: /pl/ - name: pl - - link: /pt/ - name: pt - português - - link: /ru/ - name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - - link: /ta/ - name: ta - தமிழ் - - link: /tr/ - name: tr - Türkçe - - link: /uk/ - name: uk - українська мова - - link: /zh/ - name: zh - 汉语 -extra_css: -- https://fastapi.tiangolo.com/css/termynal.css -- https://fastapi.tiangolo.com/css/custom.css -extra_javascript: -- https://fastapi.tiangolo.com/js/termynal.js -- https://fastapi.tiangolo.com/js/custom.js -hooks: -- ../../scripts/mkdocs_hooks.py +INHERIT: ../en/mkdocs.yml diff --git a/docs/lo/overrides/.gitignore b/docs/lo/overrides/.gitignore deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/nl/mkdocs.yml b/docs/nl/mkdocs.yml index e74e1a6e3..de18856f4 100644 --- a/docs/nl/mkdocs.yml +++ b/docs/nl/mkdocs.yml @@ -1,163 +1 @@ -site_name: FastAPI -site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production -site_url: https://fastapi.tiangolo.com/nl/ -theme: - name: material - custom_dir: overrides - palette: - - media: '(prefers-color-scheme: light)' - scheme: default - primary: teal - accent: amber - toggle: - icon: material/lightbulb - name: Switch to dark mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - primary: teal - accent: amber - toggle: - icon: material/lightbulb-outline - name: Switch to light mode - features: - - search.suggest - - search.highlight - - content.tabs.link - - navigation.indexes - icon: - repo: fontawesome/brands/github-alt - logo: https://fastapi.tiangolo.com/img/icon-white.svg - favicon: https://fastapi.tiangolo.com/img/favicon.png - language: nl -repo_name: tiangolo/fastapi -repo_url: https://github.com/tiangolo/fastapi -edit_uri: '' -plugins: -- search -- markdownextradata: - data: data -nav: -- FastAPI: index.md -- Languages: - - en: / - - az: /az/ - - cs: /cs/ - - de: /de/ - - em: /em/ - - es: /es/ - - fa: /fa/ - - fr: /fr/ - - he: /he/ - - hy: /hy/ - - id: /id/ - - it: /it/ - - ja: /ja/ - - ko: /ko/ - - lo: /lo/ - - nl: /nl/ - - pl: /pl/ - - pt: /pt/ - - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - - ta: /ta/ - - tr: /tr/ - - uk: /uk/ - - zh: /zh/ -markdown_extensions: -- toc: - permalink: true -- markdown.extensions.codehilite: - guess_lang: false -- mdx_include: - base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: - alternate_style: true -- attr_list -- md_in_html -extra: - analytics: - provider: google - property: G-YNEVN69SC3 - social: - - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/fastapi - - icon: fontawesome/brands/discord - link: https://discord.gg/VQjSZaeJmf - - icon: fontawesome/brands/twitter - link: https://twitter.com/fastapi - - icon: fontawesome/brands/linkedin - link: https://www.linkedin.com/in/tiangolo - - icon: fontawesome/brands/dev - link: https://dev.to/tiangolo - - icon: fontawesome/brands/medium - link: https://medium.com/@tiangolo - - icon: fontawesome/solid/globe - link: https://tiangolo.com - alternate: - - link: / - name: en - English - - link: /az/ - name: az - - link: /cs/ - name: cs - - link: /de/ - name: de - - link: /em/ - name: 😉 - - link: /es/ - name: es - español - - link: /fa/ - name: fa - - link: /fr/ - name: fr - français - - link: /he/ - name: he - - link: /hy/ - name: hy - - link: /id/ - name: id - - link: /it/ - name: it - italiano - - link: /ja/ - name: ja - 日本語 - - link: /ko/ - name: ko - 한국어 - - link: /lo/ - name: lo - ພາສາລາວ - - link: /nl/ - name: nl - - link: /pl/ - name: pl - - link: /pt/ - name: pt - português - - link: /ru/ - name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - - link: /ta/ - name: ta - தமிழ் - - link: /tr/ - name: tr - Türkçe - - link: /uk/ - name: uk - українська мова - - link: /zh/ - name: zh - 汉语 -extra_css: -- https://fastapi.tiangolo.com/css/termynal.css -- https://fastapi.tiangolo.com/css/custom.css -extra_javascript: -- https://fastapi.tiangolo.com/js/termynal.js -- https://fastapi.tiangolo.com/js/custom.js -hooks: -- ../../scripts/mkdocs_hooks.py +INHERIT: ../en/mkdocs.yml diff --git a/docs/nl/overrides/.gitignore b/docs/nl/overrides/.gitignore deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/pl/mkdocs.yml b/docs/pl/mkdocs.yml index 588eddf97..de18856f4 100644 --- a/docs/pl/mkdocs.yml +++ b/docs/pl/mkdocs.yml @@ -1,167 +1 @@ -site_name: FastAPI -site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production -site_url: https://fastapi.tiangolo.com/pl/ -theme: - name: material - custom_dir: overrides - palette: - - media: '(prefers-color-scheme: light)' - scheme: default - primary: teal - accent: amber - toggle: - icon: material/lightbulb - name: Switch to dark mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - primary: teal - accent: amber - toggle: - icon: material/lightbulb-outline - name: Switch to light mode - features: - - search.suggest - - search.highlight - - content.tabs.link - - navigation.indexes - icon: - repo: fontawesome/brands/github-alt - logo: https://fastapi.tiangolo.com/img/icon-white.svg - favicon: https://fastapi.tiangolo.com/img/favicon.png - language: pl -repo_name: tiangolo/fastapi -repo_url: https://github.com/tiangolo/fastapi -edit_uri: '' -plugins: -- search -- markdownextradata: - data: data -nav: -- FastAPI: index.md -- Languages: - - en: / - - az: /az/ - - cs: /cs/ - - de: /de/ - - em: /em/ - - es: /es/ - - fa: /fa/ - - fr: /fr/ - - he: /he/ - - hy: /hy/ - - id: /id/ - - it: /it/ - - ja: /ja/ - - ko: /ko/ - - lo: /lo/ - - nl: /nl/ - - pl: /pl/ - - pt: /pt/ - - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - - ta: /ta/ - - tr: /tr/ - - uk: /uk/ - - zh: /zh/ -- features.md -- Samouczek: - - tutorial/index.md - - tutorial/first-steps.md -markdown_extensions: -- toc: - permalink: true -- markdown.extensions.codehilite: - guess_lang: false -- mdx_include: - base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: - alternate_style: true -- attr_list -- md_in_html -extra: - analytics: - provider: google - property: G-YNEVN69SC3 - social: - - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/fastapi - - icon: fontawesome/brands/discord - link: https://discord.gg/VQjSZaeJmf - - icon: fontawesome/brands/twitter - link: https://twitter.com/fastapi - - icon: fontawesome/brands/linkedin - link: https://www.linkedin.com/in/tiangolo - - icon: fontawesome/brands/dev - link: https://dev.to/tiangolo - - icon: fontawesome/brands/medium - link: https://medium.com/@tiangolo - - icon: fontawesome/solid/globe - link: https://tiangolo.com - alternate: - - link: / - name: en - English - - link: /az/ - name: az - - link: /cs/ - name: cs - - link: /de/ - name: de - - link: /em/ - name: 😉 - - link: /es/ - name: es - español - - link: /fa/ - name: fa - - link: /fr/ - name: fr - français - - link: /he/ - name: he - - link: /hy/ - name: hy - - link: /id/ - name: id - - link: /it/ - name: it - italiano - - link: /ja/ - name: ja - 日本語 - - link: /ko/ - name: ko - 한국어 - - link: /lo/ - name: lo - ພາສາລາວ - - link: /nl/ - name: nl - - link: /pl/ - name: pl - - link: /pt/ - name: pt - português - - link: /ru/ - name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - - link: /ta/ - name: ta - தமிழ் - - link: /tr/ - name: tr - Türkçe - - link: /uk/ - name: uk - українська мова - - link: /zh/ - name: zh - 汉语 -extra_css: -- https://fastapi.tiangolo.com/css/termynal.css -- https://fastapi.tiangolo.com/css/custom.css -extra_javascript: -- https://fastapi.tiangolo.com/js/termynal.js -- https://fastapi.tiangolo.com/js/custom.js -hooks: -- ../../scripts/mkdocs_hooks.py +INHERIT: ../en/mkdocs.yml diff --git a/docs/pl/overrides/.gitignore b/docs/pl/overrides/.gitignore deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/pt/mkdocs.yml b/docs/pt/mkdocs.yml index 54520642e..de18856f4 100644 --- a/docs/pt/mkdocs.yml +++ b/docs/pt/mkdocs.yml @@ -1,204 +1 @@ -site_name: FastAPI -site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production -site_url: https://fastapi.tiangolo.com/pt/ -theme: - name: material - custom_dir: overrides - palette: - - media: '(prefers-color-scheme: light)' - scheme: default - primary: teal - accent: amber - toggle: - icon: material/lightbulb - name: Switch to dark mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - primary: teal - accent: amber - toggle: - icon: material/lightbulb-outline - name: Switch to light mode - features: - - search.suggest - - search.highlight - - content.tabs.link - - navigation.indexes - icon: - repo: fontawesome/brands/github-alt - logo: https://fastapi.tiangolo.com/img/icon-white.svg - favicon: https://fastapi.tiangolo.com/img/favicon.png - language: pt -repo_name: tiangolo/fastapi -repo_url: https://github.com/tiangolo/fastapi -edit_uri: '' -plugins: -- search -- markdownextradata: - data: data -nav: -- FastAPI: index.md -- Languages: - - en: / - - az: /az/ - - cs: /cs/ - - de: /de/ - - em: /em/ - - es: /es/ - - fa: /fa/ - - fr: /fr/ - - he: /he/ - - hy: /hy/ - - id: /id/ - - it: /it/ - - ja: /ja/ - - ko: /ko/ - - lo: /lo/ - - nl: /nl/ - - pl: /pl/ - - pt: /pt/ - - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - - ta: /ta/ - - tr: /tr/ - - uk: /uk/ - - zh: /zh/ -- features.md -- fastapi-people.md -- Tutorial - Guia de Usuário: - - tutorial/index.md - - tutorial/first-steps.md - - tutorial/path-params.md - - tutorial/query-params.md - - tutorial/body.md - - tutorial/body-multiple-params.md - - tutorial/body-fields.md - - tutorial/body-nested-models.md - - tutorial/extra-data-types.md - - tutorial/extra-models.md - - tutorial/query-params-str-validations.md - - tutorial/path-params-numeric-validations.md - - tutorial/path-operation-configuration.md - - tutorial/cookie-params.md - - tutorial/header-params.md - - tutorial/response-status-code.md - - tutorial/request-forms.md - - tutorial/request-forms-and-files.md - - tutorial/handling-errors.md - - tutorial/encoder.md - - Segurança: - - tutorial/security/index.md - - tutorial/background-tasks.md - - tutorial/static-files.md - - Guia de Usuário Avançado: - - advanced/index.md - - advanced/events.md -- Implantação: - - deployment/index.md - - deployment/versions.md - - deployment/https.md - - deployment/deta.md - - deployment/docker.md -- alternatives.md -- history-design-future.md -- external-links.md -- benchmarks.md -- help-fastapi.md -markdown_extensions: -- toc: - permalink: true -- markdown.extensions.codehilite: - guess_lang: false -- mdx_include: - base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: - alternate_style: true -- attr_list -- md_in_html -extra: - analytics: - provider: google - property: G-YNEVN69SC3 - social: - - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/fastapi - - icon: fontawesome/brands/discord - link: https://discord.gg/VQjSZaeJmf - - icon: fontawesome/brands/twitter - link: https://twitter.com/fastapi - - icon: fontawesome/brands/linkedin - link: https://www.linkedin.com/in/tiangolo - - icon: fontawesome/brands/dev - link: https://dev.to/tiangolo - - icon: fontawesome/brands/medium - link: https://medium.com/@tiangolo - - icon: fontawesome/solid/globe - link: https://tiangolo.com - alternate: - - link: / - name: en - English - - link: /az/ - name: az - - link: /cs/ - name: cs - - link: /de/ - name: de - - link: /em/ - name: 😉 - - link: /es/ - name: es - español - - link: /fa/ - name: fa - - link: /fr/ - name: fr - français - - link: /he/ - name: he - - link: /hy/ - name: hy - - link: /id/ - name: id - - link: /it/ - name: it - italiano - - link: /ja/ - name: ja - 日本語 - - link: /ko/ - name: ko - 한국어 - - link: /lo/ - name: lo - ພາສາລາວ - - link: /nl/ - name: nl - - link: /pl/ - name: pl - - link: /pt/ - name: pt - português - - link: /ru/ - name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - - link: /ta/ - name: ta - தமிழ் - - link: /tr/ - name: tr - Türkçe - - link: /uk/ - name: uk - українська мова - - link: /zh/ - name: zh - 汉语 -extra_css: -- https://fastapi.tiangolo.com/css/termynal.css -- https://fastapi.tiangolo.com/css/custom.css -extra_javascript: -- https://fastapi.tiangolo.com/js/termynal.js -- https://fastapi.tiangolo.com/js/custom.js -hooks: -- ../../scripts/mkdocs_hooks.py +INHERIT: ../en/mkdocs.yml diff --git a/docs/pt/overrides/.gitignore b/docs/pt/overrides/.gitignore deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/ru/mkdocs.yml b/docs/ru/mkdocs.yml index 66c7687b0..de18856f4 100644 --- a/docs/ru/mkdocs.yml +++ b/docs/ru/mkdocs.yml @@ -1,202 +1 @@ -site_name: FastAPI -site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production -site_url: https://fastapi.tiangolo.com/ru/ -theme: - name: material - custom_dir: overrides - palette: - - media: '(prefers-color-scheme: light)' - scheme: default - primary: teal - accent: amber - toggle: - icon: material/lightbulb - name: Switch to dark mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - primary: teal - accent: amber - toggle: - icon: material/lightbulb-outline - name: Switch to light mode - features: - - search.suggest - - search.highlight - - content.tabs.link - - navigation.indexes - icon: - repo: fontawesome/brands/github-alt - logo: https://fastapi.tiangolo.com/img/icon-white.svg - favicon: https://fastapi.tiangolo.com/img/favicon.png - language: ru -repo_name: tiangolo/fastapi -repo_url: https://github.com/tiangolo/fastapi -edit_uri: '' -plugins: -- search -- markdownextradata: - data: data -nav: -- FastAPI: index.md -- Languages: - - en: / - - az: /az/ - - cs: /cs/ - - de: /de/ - - em: /em/ - - es: /es/ - - fa: /fa/ - - fr: /fr/ - - he: /he/ - - hy: /hy/ - - id: /id/ - - it: /it/ - - ja: /ja/ - - ko: /ko/ - - lo: /lo/ - - nl: /nl/ - - pl: /pl/ - - pt: /pt/ - - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - - ta: /ta/ - - tr: /tr/ - - uk: /uk/ - - zh: /zh/ -- features.md -- fastapi-people.md -- python-types.md -- Учебник - руководство пользователя: - - tutorial/index.md - - tutorial/first-steps.md - - tutorial/path-params.md - - tutorial/query-params-str-validations.md - - tutorial/path-params-numeric-validations.md - - tutorial/body-fields.md - - tutorial/background-tasks.md - - tutorial/extra-data-types.md - - tutorial/cookie-params.md - - tutorial/testing.md - - tutorial/extra-models.md - - tutorial/response-status-code.md - - tutorial/query-params.md - - tutorial/body-multiple-params.md - - tutorial/metadata.md - - tutorial/path-operation-configuration.md - - tutorial/cors.md - - tutorial/static-files.md - - tutorial/debugging.md - - tutorial/schema-extra-example.md - - tutorial/body-nested-models.md -- async.md -- Развёртывание: - - deployment/index.md - - deployment/versions.md - - deployment/concepts.md - - deployment/https.md - - deployment/manually.md -- project-generation.md -- alternatives.md -- history-design-future.md -- external-links.md -- benchmarks.md -- help-fastapi.md -- contributing.md -markdown_extensions: -- toc: - permalink: true -- markdown.extensions.codehilite: - guess_lang: false -- mdx_include: - base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: - alternate_style: true -- attr_list -- md_in_html -extra: - analytics: - provider: google - property: G-YNEVN69SC3 - social: - - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/fastapi - - icon: fontawesome/brands/discord - link: https://discord.gg/VQjSZaeJmf - - icon: fontawesome/brands/twitter - link: https://twitter.com/fastapi - - icon: fontawesome/brands/linkedin - link: https://www.linkedin.com/in/tiangolo - - icon: fontawesome/brands/dev - link: https://dev.to/tiangolo - - icon: fontawesome/brands/medium - link: https://medium.com/@tiangolo - - icon: fontawesome/solid/globe - link: https://tiangolo.com - alternate: - - link: / - name: en - English - - link: /az/ - name: az - - link: /cs/ - name: cs - - link: /de/ - name: de - - link: /em/ - name: 😉 - - link: /es/ - name: es - español - - link: /fa/ - name: fa - - link: /fr/ - name: fr - français - - link: /he/ - name: he - - link: /hy/ - name: hy - - link: /id/ - name: id - - link: /it/ - name: it - italiano - - link: /ja/ - name: ja - 日本語 - - link: /ko/ - name: ko - 한국어 - - link: /lo/ - name: lo - ພາສາລາວ - - link: /nl/ - name: nl - - link: /pl/ - name: pl - - link: /pt/ - name: pt - português - - link: /ru/ - name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - - link: /ta/ - name: ta - தமிழ் - - link: /tr/ - name: tr - Türkçe - - link: /uk/ - name: uk - українська мова - - link: /zh/ - name: zh - 汉语 -extra_css: -- https://fastapi.tiangolo.com/css/termynal.css -- https://fastapi.tiangolo.com/css/custom.css -extra_javascript: -- https://fastapi.tiangolo.com/js/termynal.js -- https://fastapi.tiangolo.com/js/custom.js -hooks: -- ../../scripts/mkdocs_hooks.py +INHERIT: ../en/mkdocs.yml diff --git a/docs/ru/overrides/.gitignore b/docs/ru/overrides/.gitignore deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/sq/mkdocs.yml b/docs/sq/mkdocs.yml index 64f3dec2e..de18856f4 100644 --- a/docs/sq/mkdocs.yml +++ b/docs/sq/mkdocs.yml @@ -1,163 +1 @@ -site_name: FastAPI -site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production -site_url: https://fastapi.tiangolo.com/sq/ -theme: - name: material - custom_dir: overrides - palette: - - media: '(prefers-color-scheme: light)' - scheme: default - primary: teal - accent: amber - toggle: - icon: material/lightbulb - name: Switch to dark mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - primary: teal - accent: amber - toggle: - icon: material/lightbulb-outline - name: Switch to light mode - features: - - search.suggest - - search.highlight - - content.tabs.link - - navigation.indexes - icon: - repo: fontawesome/brands/github-alt - logo: https://fastapi.tiangolo.com/img/icon-white.svg - favicon: https://fastapi.tiangolo.com/img/favicon.png - language: en -repo_name: tiangolo/fastapi -repo_url: https://github.com/tiangolo/fastapi -edit_uri: '' -plugins: -- search -- markdownextradata: - data: data -nav: -- FastAPI: index.md -- Languages: - - en: / - - az: /az/ - - cs: /cs/ - - de: /de/ - - em: /em/ - - es: /es/ - - fa: /fa/ - - fr: /fr/ - - he: /he/ - - hy: /hy/ - - id: /id/ - - it: /it/ - - ja: /ja/ - - ko: /ko/ - - lo: /lo/ - - nl: /nl/ - - pl: /pl/ - - pt: /pt/ - - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - - ta: /ta/ - - tr: /tr/ - - uk: /uk/ - - zh: /zh/ -markdown_extensions: -- toc: - permalink: true -- markdown.extensions.codehilite: - guess_lang: false -- mdx_include: - base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: - alternate_style: true -- attr_list -- md_in_html -extra: - analytics: - provider: google - property: G-YNEVN69SC3 - social: - - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/fastapi - - icon: fontawesome/brands/discord - link: https://discord.gg/VQjSZaeJmf - - icon: fontawesome/brands/twitter - link: https://twitter.com/fastapi - - icon: fontawesome/brands/linkedin - link: https://www.linkedin.com/in/tiangolo - - icon: fontawesome/brands/dev - link: https://dev.to/tiangolo - - icon: fontawesome/brands/medium - link: https://medium.com/@tiangolo - - icon: fontawesome/solid/globe - link: https://tiangolo.com - alternate: - - link: / - name: en - English - - link: /az/ - name: az - - link: /cs/ - name: cs - - link: /de/ - name: de - - link: /em/ - name: 😉 - - link: /es/ - name: es - español - - link: /fa/ - name: fa - - link: /fr/ - name: fr - français - - link: /he/ - name: he - - link: /hy/ - name: hy - - link: /id/ - name: id - - link: /it/ - name: it - italiano - - link: /ja/ - name: ja - 日本語 - - link: /ko/ - name: ko - 한국어 - - link: /lo/ - name: lo - ພາສາລາວ - - link: /nl/ - name: nl - - link: /pl/ - name: pl - - link: /pt/ - name: pt - português - - link: /ru/ - name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - - link: /ta/ - name: ta - தமிழ் - - link: /tr/ - name: tr - Türkçe - - link: /uk/ - name: uk - українська мова - - link: /zh/ - name: zh - 汉语 -extra_css: -- https://fastapi.tiangolo.com/css/termynal.css -- https://fastapi.tiangolo.com/css/custom.css -extra_javascript: -- https://fastapi.tiangolo.com/js/termynal.js -- https://fastapi.tiangolo.com/js/custom.js -hooks: -- ../../scripts/mkdocs_hooks.py +INHERIT: ../en/mkdocs.yml diff --git a/docs/sq/overrides/.gitignore b/docs/sq/overrides/.gitignore deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/sv/mkdocs.yml b/docs/sv/mkdocs.yml index 8604a06f6..de18856f4 100644 --- a/docs/sv/mkdocs.yml +++ b/docs/sv/mkdocs.yml @@ -1,163 +1 @@ -site_name: FastAPI -site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production -site_url: https://fastapi.tiangolo.com/sv/ -theme: - name: material - custom_dir: overrides - palette: - - media: '(prefers-color-scheme: light)' - scheme: default - primary: teal - accent: amber - toggle: - icon: material/lightbulb - name: Switch to dark mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - primary: teal - accent: amber - toggle: - icon: material/lightbulb-outline - name: Switch to light mode - features: - - search.suggest - - search.highlight - - content.tabs.link - - navigation.indexes - icon: - repo: fontawesome/brands/github-alt - logo: https://fastapi.tiangolo.com/img/icon-white.svg - favicon: https://fastapi.tiangolo.com/img/favicon.png - language: sv -repo_name: tiangolo/fastapi -repo_url: https://github.com/tiangolo/fastapi -edit_uri: '' -plugins: -- search -- markdownextradata: - data: data -nav: -- FastAPI: index.md -- Languages: - - en: / - - az: /az/ - - cs: /cs/ - - de: /de/ - - em: /em/ - - es: /es/ - - fa: /fa/ - - fr: /fr/ - - he: /he/ - - hy: /hy/ - - id: /id/ - - it: /it/ - - ja: /ja/ - - ko: /ko/ - - lo: /lo/ - - nl: /nl/ - - pl: /pl/ - - pt: /pt/ - - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - - ta: /ta/ - - tr: /tr/ - - uk: /uk/ - - zh: /zh/ -markdown_extensions: -- toc: - permalink: true -- markdown.extensions.codehilite: - guess_lang: false -- mdx_include: - base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: - alternate_style: true -- attr_list -- md_in_html -extra: - analytics: - provider: google - property: G-YNEVN69SC3 - social: - - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/fastapi - - icon: fontawesome/brands/discord - link: https://discord.gg/VQjSZaeJmf - - icon: fontawesome/brands/twitter - link: https://twitter.com/fastapi - - icon: fontawesome/brands/linkedin - link: https://www.linkedin.com/in/tiangolo - - icon: fontawesome/brands/dev - link: https://dev.to/tiangolo - - icon: fontawesome/brands/medium - link: https://medium.com/@tiangolo - - icon: fontawesome/solid/globe - link: https://tiangolo.com - alternate: - - link: / - name: en - English - - link: /az/ - name: az - - link: /cs/ - name: cs - - link: /de/ - name: de - - link: /em/ - name: 😉 - - link: /es/ - name: es - español - - link: /fa/ - name: fa - - link: /fr/ - name: fr - français - - link: /he/ - name: he - - link: /hy/ - name: hy - - link: /id/ - name: id - - link: /it/ - name: it - italiano - - link: /ja/ - name: ja - 日本語 - - link: /ko/ - name: ko - 한국어 - - link: /lo/ - name: lo - ພາສາລາວ - - link: /nl/ - name: nl - - link: /pl/ - name: pl - - link: /pt/ - name: pt - português - - link: /ru/ - name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - - link: /ta/ - name: ta - தமிழ் - - link: /tr/ - name: tr - Türkçe - - link: /uk/ - name: uk - українська мова - - link: /zh/ - name: zh - 汉语 -extra_css: -- https://fastapi.tiangolo.com/css/termynal.css -- https://fastapi.tiangolo.com/css/custom.css -extra_javascript: -- https://fastapi.tiangolo.com/js/termynal.js -- https://fastapi.tiangolo.com/js/custom.js -hooks: -- ../../scripts/mkdocs_hooks.py +INHERIT: ../en/mkdocs.yml diff --git a/docs/sv/overrides/.gitignore b/docs/sv/overrides/.gitignore deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/ta/mkdocs.yml b/docs/ta/mkdocs.yml deleted file mode 100644 index 4000d9a41..000000000 --- a/docs/ta/mkdocs.yml +++ /dev/null @@ -1,163 +0,0 @@ -site_name: FastAPI -site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production -site_url: https://fastapi.tiangolo.com/ta/ -theme: - name: material - custom_dir: overrides - palette: - - media: '(prefers-color-scheme: light)' - scheme: default - primary: teal - accent: amber - toggle: - icon: material/lightbulb - name: Switch to dark mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - primary: teal - accent: amber - toggle: - icon: material/lightbulb-outline - name: Switch to light mode - features: - - search.suggest - - search.highlight - - content.tabs.link - - navigation.indexes - icon: - repo: fontawesome/brands/github-alt - logo: https://fastapi.tiangolo.com/img/icon-white.svg - favicon: https://fastapi.tiangolo.com/img/favicon.png - language: en -repo_name: tiangolo/fastapi -repo_url: https://github.com/tiangolo/fastapi -edit_uri: '' -plugins: -- search -- markdownextradata: - data: data -nav: -- FastAPI: index.md -- Languages: - - en: / - - az: /az/ - - cs: /cs/ - - de: /de/ - - em: /em/ - - es: /es/ - - fa: /fa/ - - fr: /fr/ - - he: /he/ - - hy: /hy/ - - id: /id/ - - it: /it/ - - ja: /ja/ - - ko: /ko/ - - lo: /lo/ - - nl: /nl/ - - pl: /pl/ - - pt: /pt/ - - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - - ta: /ta/ - - tr: /tr/ - - uk: /uk/ - - zh: /zh/ -markdown_extensions: -- toc: - permalink: true -- markdown.extensions.codehilite: - guess_lang: false -- mdx_include: - base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: - alternate_style: true -- attr_list -- md_in_html -extra: - analytics: - provider: google - property: G-YNEVN69SC3 - social: - - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/fastapi - - icon: fontawesome/brands/discord - link: https://discord.gg/VQjSZaeJmf - - icon: fontawesome/brands/twitter - link: https://twitter.com/fastapi - - icon: fontawesome/brands/linkedin - link: https://www.linkedin.com/in/tiangolo - - icon: fontawesome/brands/dev - link: https://dev.to/tiangolo - - icon: fontawesome/brands/medium - link: https://medium.com/@tiangolo - - icon: fontawesome/solid/globe - link: https://tiangolo.com - alternate: - - link: / - name: en - English - - link: /az/ - name: az - - link: /cs/ - name: cs - - link: /de/ - name: de - - link: /em/ - name: 😉 - - link: /es/ - name: es - español - - link: /fa/ - name: fa - - link: /fr/ - name: fr - français - - link: /he/ - name: he - - link: /hy/ - name: hy - - link: /id/ - name: id - - link: /it/ - name: it - italiano - - link: /ja/ - name: ja - 日本語 - - link: /ko/ - name: ko - 한국어 - - link: /lo/ - name: lo - ພາສາລາວ - - link: /nl/ - name: nl - - link: /pl/ - name: pl - - link: /pt/ - name: pt - português - - link: /ru/ - name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - - link: /ta/ - name: ta - தமிழ் - - link: /tr/ - name: tr - Türkçe - - link: /uk/ - name: uk - українська мова - - link: /zh/ - name: zh - 汉语 -extra_css: -- https://fastapi.tiangolo.com/css/termynal.css -- https://fastapi.tiangolo.com/css/custom.css -extra_javascript: -- https://fastapi.tiangolo.com/js/termynal.js -- https://fastapi.tiangolo.com/js/custom.js -hooks: -- ../../scripts/mkdocs_hooks.py diff --git a/docs/ta/overrides/.gitignore b/docs/ta/overrides/.gitignore deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/tr/mkdocs.yml b/docs/tr/mkdocs.yml index 408b3ec29..de18856f4 100644 --- a/docs/tr/mkdocs.yml +++ b/docs/tr/mkdocs.yml @@ -1,168 +1 @@ -site_name: FastAPI -site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production -site_url: https://fastapi.tiangolo.com/tr/ -theme: - name: material - custom_dir: overrides - palette: - - media: '(prefers-color-scheme: light)' - scheme: default - primary: teal - accent: amber - toggle: - icon: material/lightbulb - name: Switch to dark mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - primary: teal - accent: amber - toggle: - icon: material/lightbulb-outline - name: Switch to light mode - features: - - search.suggest - - search.highlight - - content.tabs.link - - navigation.indexes - icon: - repo: fontawesome/brands/github-alt - logo: https://fastapi.tiangolo.com/img/icon-white.svg - favicon: https://fastapi.tiangolo.com/img/favicon.png - language: tr -repo_name: tiangolo/fastapi -repo_url: https://github.com/tiangolo/fastapi -edit_uri: '' -plugins: -- search -- markdownextradata: - data: data -nav: -- FastAPI: index.md -- Languages: - - en: / - - az: /az/ - - cs: /cs/ - - de: /de/ - - em: /em/ - - es: /es/ - - fa: /fa/ - - fr: /fr/ - - he: /he/ - - hy: /hy/ - - id: /id/ - - it: /it/ - - ja: /ja/ - - ko: /ko/ - - lo: /lo/ - - nl: /nl/ - - pl: /pl/ - - pt: /pt/ - - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - - ta: /ta/ - - tr: /tr/ - - uk: /uk/ - - zh: /zh/ -- features.md -- fastapi-people.md -- python-types.md -- Tutorial - User Guide: - - tutorial/first-steps.md -markdown_extensions: -- toc: - permalink: true -- markdown.extensions.codehilite: - guess_lang: false -- mdx_include: - base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: - alternate_style: true -- attr_list -- md_in_html -extra: - analytics: - provider: google - property: G-YNEVN69SC3 - social: - - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/fastapi - - icon: fontawesome/brands/discord - link: https://discord.gg/VQjSZaeJmf - - icon: fontawesome/brands/twitter - link: https://twitter.com/fastapi - - icon: fontawesome/brands/linkedin - link: https://www.linkedin.com/in/tiangolo - - icon: fontawesome/brands/dev - link: https://dev.to/tiangolo - - icon: fontawesome/brands/medium - link: https://medium.com/@tiangolo - - icon: fontawesome/solid/globe - link: https://tiangolo.com - alternate: - - link: / - name: en - English - - link: /az/ - name: az - - link: /cs/ - name: cs - - link: /de/ - name: de - - link: /em/ - name: 😉 - - link: /es/ - name: es - español - - link: /fa/ - name: fa - - link: /fr/ - name: fr - français - - link: /he/ - name: he - - link: /hy/ - name: hy - - link: /id/ - name: id - - link: /it/ - name: it - italiano - - link: /ja/ - name: ja - 日本語 - - link: /ko/ - name: ko - 한국어 - - link: /lo/ - name: lo - ພາສາລາວ - - link: /nl/ - name: nl - - link: /pl/ - name: pl - - link: /pt/ - name: pt - português - - link: /ru/ - name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - - link: /ta/ - name: ta - தமிழ் - - link: /tr/ - name: tr - Türkçe - - link: /uk/ - name: uk - українська мова - - link: /zh/ - name: zh - 汉语 -extra_css: -- https://fastapi.tiangolo.com/css/termynal.css -- https://fastapi.tiangolo.com/css/custom.css -extra_javascript: -- https://fastapi.tiangolo.com/js/termynal.js -- https://fastapi.tiangolo.com/js/custom.js -hooks: -- ../../scripts/mkdocs_hooks.py +INHERIT: ../en/mkdocs.yml diff --git a/docs/tr/overrides/.gitignore b/docs/tr/overrides/.gitignore deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/uk/mkdocs.yml b/docs/uk/mkdocs.yml index 49516cebf..de18856f4 100644 --- a/docs/uk/mkdocs.yml +++ b/docs/uk/mkdocs.yml @@ -1,163 +1 @@ -site_name: FastAPI -site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production -site_url: https://fastapi.tiangolo.com/uk/ -theme: - name: material - custom_dir: overrides - palette: - - media: '(prefers-color-scheme: light)' - scheme: default - primary: teal - accent: amber - toggle: - icon: material/lightbulb - name: Switch to dark mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - primary: teal - accent: amber - toggle: - icon: material/lightbulb-outline - name: Switch to light mode - features: - - search.suggest - - search.highlight - - content.tabs.link - - navigation.indexes - icon: - repo: fontawesome/brands/github-alt - logo: https://fastapi.tiangolo.com/img/icon-white.svg - favicon: https://fastapi.tiangolo.com/img/favicon.png - language: uk -repo_name: tiangolo/fastapi -repo_url: https://github.com/tiangolo/fastapi -edit_uri: '' -plugins: -- search -- markdownextradata: - data: data -nav: -- FastAPI: index.md -- Languages: - - en: / - - az: /az/ - - cs: /cs/ - - de: /de/ - - em: /em/ - - es: /es/ - - fa: /fa/ - - fr: /fr/ - - he: /he/ - - hy: /hy/ - - id: /id/ - - it: /it/ - - ja: /ja/ - - ko: /ko/ - - lo: /lo/ - - nl: /nl/ - - pl: /pl/ - - pt: /pt/ - - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - - ta: /ta/ - - tr: /tr/ - - uk: /uk/ - - zh: /zh/ -markdown_extensions: -- toc: - permalink: true -- markdown.extensions.codehilite: - guess_lang: false -- mdx_include: - base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: - alternate_style: true -- attr_list -- md_in_html -extra: - analytics: - provider: google - property: G-YNEVN69SC3 - social: - - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/fastapi - - icon: fontawesome/brands/discord - link: https://discord.gg/VQjSZaeJmf - - icon: fontawesome/brands/twitter - link: https://twitter.com/fastapi - - icon: fontawesome/brands/linkedin - link: https://www.linkedin.com/in/tiangolo - - icon: fontawesome/brands/dev - link: https://dev.to/tiangolo - - icon: fontawesome/brands/medium - link: https://medium.com/@tiangolo - - icon: fontawesome/solid/globe - link: https://tiangolo.com - alternate: - - link: / - name: en - English - - link: /az/ - name: az - - link: /cs/ - name: cs - - link: /de/ - name: de - - link: /em/ - name: 😉 - - link: /es/ - name: es - español - - link: /fa/ - name: fa - - link: /fr/ - name: fr - français - - link: /he/ - name: he - - link: /hy/ - name: hy - - link: /id/ - name: id - - link: /it/ - name: it - italiano - - link: /ja/ - name: ja - 日本語 - - link: /ko/ - name: ko - 한국어 - - link: /lo/ - name: lo - ພາສາລາວ - - link: /nl/ - name: nl - - link: /pl/ - name: pl - - link: /pt/ - name: pt - português - - link: /ru/ - name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - - link: /ta/ - name: ta - தமிழ் - - link: /tr/ - name: tr - Türkçe - - link: /uk/ - name: uk - українська мова - - link: /zh/ - name: zh - 汉语 -extra_css: -- https://fastapi.tiangolo.com/css/termynal.css -- https://fastapi.tiangolo.com/css/custom.css -extra_javascript: -- https://fastapi.tiangolo.com/js/termynal.js -- https://fastapi.tiangolo.com/js/custom.js -hooks: -- ../../scripts/mkdocs_hooks.py +INHERIT: ../en/mkdocs.yml diff --git a/docs/uk/overrides/.gitignore b/docs/uk/overrides/.gitignore deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/zh/mkdocs.yml b/docs/zh/mkdocs.yml index 39f989790..de18856f4 100644 --- a/docs/zh/mkdocs.yml +++ b/docs/zh/mkdocs.yml @@ -1,228 +1 @@ -site_name: FastAPI -site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production -site_url: https://fastapi.tiangolo.com/zh/ -theme: - name: material - custom_dir: overrides - palette: - - media: '(prefers-color-scheme: light)' - scheme: default - primary: teal - accent: amber - toggle: - icon: material/lightbulb - name: Switch to dark mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - primary: teal - accent: amber - toggle: - icon: material/lightbulb-outline - name: Switch to light mode - features: - - search.suggest - - search.highlight - - content.tabs.link - - navigation.indexes - icon: - repo: fontawesome/brands/github-alt - logo: https://fastapi.tiangolo.com/img/icon-white.svg - favicon: https://fastapi.tiangolo.com/img/favicon.png - language: zh -repo_name: tiangolo/fastapi -repo_url: https://github.com/tiangolo/fastapi -edit_uri: '' -plugins: -- search -- markdownextradata: - data: data -nav: -- FastAPI: index.md -- Languages: - - en: / - - az: /az/ - - cs: /cs/ - - de: /de/ - - em: /em/ - - es: /es/ - - fa: /fa/ - - fr: /fr/ - - he: /he/ - - hy: /hy/ - - id: /id/ - - it: /it/ - - ja: /ja/ - - ko: /ko/ - - lo: /lo/ - - nl: /nl/ - - pl: /pl/ - - pt: /pt/ - - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - - ta: /ta/ - - tr: /tr/ - - uk: /uk/ - - zh: /zh/ -- features.md -- fastapi-people.md -- python-types.md -- 教程 - 用户指南: - - tutorial/index.md - - tutorial/first-steps.md - - tutorial/path-params.md - - tutorial/query-params.md - - tutorial/body.md - - tutorial/query-params-str-validations.md - - tutorial/path-params-numeric-validations.md - - tutorial/body-multiple-params.md - - tutorial/body-fields.md - - tutorial/middleware.md - - tutorial/body-nested-models.md - - tutorial/header-params.md - - tutorial/response-model.md - - tutorial/extra-models.md - - tutorial/response-status-code.md - - tutorial/schema-extra-example.md - - tutorial/extra-data-types.md - - tutorial/cookie-params.md - - tutorial/request-forms.md - - tutorial/request-files.md - - tutorial/request-forms-and-files.md - - tutorial/handling-errors.md - - tutorial/path-operation-configuration.md - - tutorial/encoder.md - - tutorial/body-updates.md - - 依赖项: - - tutorial/dependencies/index.md - - tutorial/dependencies/classes-as-dependencies.md - - tutorial/dependencies/sub-dependencies.md - - tutorial/dependencies/dependencies-in-path-operation-decorators.md - - tutorial/dependencies/global-dependencies.md - - 安全性: - - tutorial/security/index.md - - tutorial/security/first-steps.md - - tutorial/security/get-current-user.md - - tutorial/security/simple-oauth2.md - - tutorial/security/oauth2-jwt.md - - tutorial/cors.md - - tutorial/sql-databases.md - - tutorial/bigger-applications.md - - tutorial/metadata.md - - tutorial/static-files.md - - tutorial/testing.md - - tutorial/debugging.md -- 高级用户指南: - - advanced/index.md - - advanced/path-operation-advanced-configuration.md - - advanced/additional-status-codes.md - - advanced/response-directly.md - - advanced/custom-response.md - - advanced/response-cookies.md - - advanced/response-change-status-code.md - - advanced/settings.md - - advanced/response-headers.md - - advanced/websockets.md - - advanced/wsgi.md - - 高级安全: - - advanced/security/index.md -- contributing.md -- help-fastapi.md -- benchmarks.md -markdown_extensions: -- toc: - permalink: true -- markdown.extensions.codehilite: - guess_lang: false -- mdx_include: - base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: - alternate_style: true -- attr_list -- md_in_html -extra: - analytics: - provider: google - property: G-YNEVN69SC3 - social: - - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/fastapi - - icon: fontawesome/brands/discord - link: https://discord.gg/VQjSZaeJmf - - icon: fontawesome/brands/twitter - link: https://twitter.com/fastapi - - icon: fontawesome/brands/linkedin - link: https://www.linkedin.com/in/tiangolo - - icon: fontawesome/brands/dev - link: https://dev.to/tiangolo - - icon: fontawesome/brands/medium - link: https://medium.com/@tiangolo - - icon: fontawesome/solid/globe - link: https://tiangolo.com - alternate: - - link: / - name: en - English - - link: /az/ - name: az - - link: /cs/ - name: cs - - link: /de/ - name: de - - link: /em/ - name: 😉 - - link: /es/ - name: es - español - - link: /fa/ - name: fa - - link: /fr/ - name: fr - français - - link: /he/ - name: he - - link: /hy/ - name: hy - - link: /id/ - name: id - - link: /it/ - name: it - italiano - - link: /ja/ - name: ja - 日本語 - - link: /ko/ - name: ko - 한국어 - - link: /lo/ - name: lo - ພາສາລາວ - - link: /nl/ - name: nl - - link: /pl/ - name: pl - - link: /pt/ - name: pt - português - - link: /ru/ - name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - - link: /ta/ - name: ta - தமிழ் - - link: /tr/ - name: tr - Türkçe - - link: /uk/ - name: uk - українська мова - - link: /zh/ - name: zh - 汉语 -extra_css: -- https://fastapi.tiangolo.com/css/termynal.css -- https://fastapi.tiangolo.com/css/custom.css -extra_javascript: -- https://fastapi.tiangolo.com/js/termynal.js -- https://fastapi.tiangolo.com/js/custom.js -hooks: -- ../../scripts/mkdocs_hooks.py +INHERIT: ../en/mkdocs.yml diff --git a/docs/zh/overrides/.gitignore b/docs/zh/overrides/.gitignore deleted file mode 100644 index e69de29bb..000000000 diff --git a/scripts/docs.py b/scripts/docs.py index c464f8dbe..5615a8572 100644 --- a/scripts/docs.py +++ b/scripts/docs.py @@ -1,4 +1,5 @@ import json +import logging import os import re import shutil @@ -6,7 +7,7 @@ import subprocess from http.server import HTTPServer, SimpleHTTPRequestHandler from multiprocessing import Pool from pathlib import Path -from typing import Dict, List, Optional, Tuple +from typing import Any, Dict, List, Optional, Union import mkdocs.commands.build import mkdocs.commands.serve @@ -16,6 +17,8 @@ import typer import yaml from jinja2 import Template +logging.basicConfig(level=logging.INFO) + app = typer.Typer() mkdocs_name = "mkdocs.yml" @@ -27,19 +30,21 @@ missing_translation_snippet = """ docs_path = Path("docs") en_docs_path = Path("docs/en") en_config_path: Path = en_docs_path / mkdocs_name +site_path = Path("site").absolute() +build_site_path = Path("site_build").absolute() -def get_en_config() -> dict: +def get_en_config() -> Dict[str, Any]: return mkdocs.utils.yaml_load(en_config_path.read_text(encoding="utf-8")) -def get_lang_paths(): +def get_lang_paths() -> List[Path]: return sorted(docs_path.iterdir()) -def lang_callback(lang: Optional[str]): +def lang_callback(lang: Optional[str]) -> Union[str, None]: if lang is None: - return + return None if not lang.isalpha() or len(lang) != 2: typer.echo("Use a 2 letter language code, like: es") raise typer.Abort() @@ -54,35 +59,6 @@ def complete_existing_lang(incomplete: str): yield lang_path.name -def get_base_lang_config(lang: str): - en_config = get_en_config() - fastapi_url_base = "https://fastapi.tiangolo.com/" - new_config = en_config.copy() - new_config["site_url"] = en_config["site_url"] + f"{lang}/" - new_config["theme"]["logo"] = fastapi_url_base + en_config["theme"]["logo"] - new_config["theme"]["favicon"] = fastapi_url_base + en_config["theme"]["favicon"] - new_config["theme"]["language"] = lang - new_config["nav"] = en_config["nav"][:2] - extra_css = [] - css: str - for css in en_config["extra_css"]: - if css.startswith("http"): - extra_css.append(css) - else: - extra_css.append(fastapi_url_base + css) - new_config["extra_css"] = extra_css - - extra_js = [] - js: str - for js in en_config["extra_javascript"]: - if js.startswith("http"): - extra_js.append(js) - else: - extra_js.append(fastapi_url_base + js) - new_config["extra_javascript"] = extra_js - return new_config - - @app.command() def new_lang(lang: str = typer.Argument(..., callback=lang_callback)): """ @@ -95,12 +71,8 @@ def new_lang(lang: str = typer.Argument(..., callback=lang_callback)): typer.echo(f"The language was already created: {lang}") raise typer.Abort() new_path.mkdir() - new_config = get_base_lang_config(lang) new_config_path: Path = Path(new_path) / mkdocs_name - new_config_path.write_text( - yaml.dump(new_config, sort_keys=False, width=200, allow_unicode=True), - encoding="utf-8", - ) + new_config_path.write_text("INHERIT: ../en/mkdocs.yml\n", encoding="utf-8") new_config_docs_path: Path = new_path / "docs" new_config_docs_path.mkdir() en_index_path: Path = en_docs_path / "docs" / "index.md" @@ -108,11 +80,8 @@ def new_lang(lang: str = typer.Argument(..., callback=lang_callback)): en_index_content = en_index_path.read_text(encoding="utf-8") new_index_content = f"{missing_translation_snippet}\n\n{en_index_content}" new_index_path.write_text(new_index_content, encoding="utf-8") - new_overrides_gitignore_path = new_path / "overrides" / ".gitignore" - new_overrides_gitignore_path.parent.mkdir(parents=True, exist_ok=True) - new_overrides_gitignore_path.write_text("") typer.secho(f"Successfully initialized: {new_path}", color=typer.colors.GREEN) - update_languages(lang=None) + update_languages() @app.command() @@ -120,95 +89,29 @@ def build_lang( lang: str = typer.Argument( ..., callback=lang_callback, autocompletion=complete_existing_lang ) -): +) -> None: """ - Build the docs for a language, filling missing pages with translation notifications. + Build the docs for a language. """ lang_path: Path = Path("docs") / lang if not lang_path.is_dir(): typer.echo(f"The language translation doesn't seem to exist yet: {lang}") raise typer.Abort() typer.echo(f"Building docs for: {lang}") - build_dir_path = Path("docs_build") - build_dir_path.mkdir(exist_ok=True) - build_lang_path = build_dir_path / lang - en_lang_path = Path("docs/en") - site_path = Path("site").absolute() - build_site_path = Path("site_build").absolute() build_site_dist_path = build_site_path / lang if lang == "en": dist_path = site_path + # Don't remove en dist_path as it might already contain other languages. + # When running build_all(), that function already removes site_path. + # All this is only relevant locally, on GitHub Actions all this is done through + # artifacts and multiple workflows, so it doesn't matter if directories are + # removed or not. else: - dist_path: Path = site_path / lang - shutil.rmtree(build_lang_path, ignore_errors=True) - shutil.copytree(lang_path, build_lang_path) - if not lang == "en": - shutil.copytree(en_docs_path / "data", build_lang_path / "data") - overrides_src = en_docs_path / "overrides" - overrides_dest = build_lang_path / "overrides" - for path in overrides_src.iterdir(): - dest_path = overrides_dest / path.name - if not dest_path.exists(): - shutil.copy(path, dest_path) - en_config_path: Path = en_lang_path / mkdocs_name - en_config: dict = mkdocs.utils.yaml_load( - en_config_path.read_text(encoding="utf-8") - ) - nav = en_config["nav"] - lang_config_path: Path = lang_path / mkdocs_name - lang_config: dict = mkdocs.utils.yaml_load( - lang_config_path.read_text(encoding="utf-8") - ) - lang_nav = lang_config["nav"] - # Exclude first 2 entries FastAPI and Languages, for custom handling - use_nav = nav[2:] - lang_use_nav = lang_nav[2:] - file_to_nav = get_file_to_nav_map(use_nav) - sections = get_sections(use_nav) - lang_file_to_nav = get_file_to_nav_map(lang_use_nav) - use_lang_file_to_nav = get_file_to_nav_map(lang_use_nav) - for file in file_to_nav: - file_path = Path(file) - lang_file_path: Path = build_lang_path / "docs" / file_path - en_file_path: Path = en_lang_path / "docs" / file_path - lang_file_path.parent.mkdir(parents=True, exist_ok=True) - if not lang_file_path.is_file(): - en_text = en_file_path.read_text(encoding="utf-8") - lang_text = get_text_with_translate_missing(en_text) - lang_file_path.write_text(lang_text, encoding="utf-8") - file_key = file_to_nav[file] - use_lang_file_to_nav[file] = file_key - if file_key: - composite_key = () - new_key = () - for key_part in file_key: - composite_key += (key_part,) - key_first_file = sections[composite_key] - if key_first_file in lang_file_to_nav: - new_key = lang_file_to_nav[key_first_file] - else: - new_key += (key_part,) - use_lang_file_to_nav[file] = new_key - key_to_section = {(): []} - for file, orig_file_key in file_to_nav.items(): - if file in use_lang_file_to_nav: - file_key = use_lang_file_to_nav[file] - else: - file_key = orig_file_key - section = get_key_section(key_to_section=key_to_section, key=file_key) - section.append(file) - new_nav = key_to_section[()] - export_lang_nav = [lang_nav[0], nav[1]] + new_nav - lang_config["nav"] = export_lang_nav - build_lang_config_path: Path = build_lang_path / mkdocs_name - build_lang_config_path.write_text( - yaml.dump(lang_config, sort_keys=False, width=200, allow_unicode=True), - encoding="utf-8", - ) + dist_path = site_path / lang + shutil.rmtree(dist_path, ignore_errors=True) current_dir = os.getcwd() - os.chdir(build_lang_path) + os.chdir(lang_path) shutil.rmtree(build_site_dist_path, ignore_errors=True) - shutil.rmtree(dist_path, ignore_errors=True) subprocess.run(["mkdocs", "build", "--site-dir", build_site_dist_path], check=True) shutil.copytree(build_site_dist_path, dist_path, dirs_exist_ok=True) os.chdir(current_dir) @@ -227,7 +130,7 @@ index_sponsors_template = """ """ -def generate_readme_content(): +def generate_readme_content() -> str: en_index = en_docs_path / "docs" / "index.md" content = en_index.read_text("utf-8") match_start = re.search(r"", content) @@ -247,7 +150,7 @@ def generate_readme_content(): @app.command() -def generate_readme(): +def generate_readme() -> None: """ Generate README.md content from main index.md """ @@ -258,7 +161,7 @@ def generate_readme(): @app.command() -def verify_readme(): +def verify_readme() -> None: """ Verify README.md content from main index.md """ @@ -275,12 +178,13 @@ def verify_readme(): @app.command() -def build_all(): +def build_all() -> None: """ Build mkdocs site for en, and then build each language inside, end result is located at directory ./site/ with each language inside. """ - update_languages(lang=None) + update_languages() + shutil.rmtree(site_path, ignore_errors=True) langs = [lang.name for lang in get_lang_paths() if lang.is_dir()] cpu_count = os.cpu_count() or 1 process_pool_size = cpu_count * 4 @@ -289,34 +193,16 @@ def build_all(): p.map(build_lang, langs) -def update_single_lang(lang: str): - lang_path = docs_path / lang - typer.echo(f"Updating {lang_path.name}") - update_config(lang_path.name) - - @app.command() -def update_languages( - lang: str = typer.Argument( - None, callback=lang_callback, autocompletion=complete_existing_lang - ) -): +def update_languages() -> None: """ Update the mkdocs.yml file Languages section including all the available languages. - - The LANG argument is a 2-letter language code. If it's not provided, update all the - mkdocs.yml files (for all the languages). """ - if lang is None: - for lang_path in get_lang_paths(): - if lang_path.is_dir(): - update_single_lang(lang_path.name) - else: - update_single_lang(lang) + update_config() @app.command() -def serve(): +def serve() -> None: """ A quick server to preview a built site with translations. @@ -342,7 +228,7 @@ def live( lang: str = typer.Argument( None, callback=lang_callback, autocompletion=complete_existing_lang ) -): +) -> None: """ Serve with livereload a docs site for a specific language. @@ -359,18 +245,8 @@ def live( mkdocs.commands.serve.serve(dev_addr="127.0.0.1:8008") -def update_config(lang: str): - lang_path: Path = docs_path / lang - config_path = lang_path / mkdocs_name - current_config: dict = mkdocs.utils.yaml_load( - config_path.read_text(encoding="utf-8") - ) - if lang == "en": - config = get_en_config() - else: - config = get_base_lang_config(lang) - config["nav"] = current_config["nav"] - config["theme"]["language"] = current_config["theme"]["language"] +def update_config() -> None: + config = get_en_config() languages = [{"en": "/"}] alternate: List[Dict[str, str]] = config["extra"].get("alternate", []) alternate_dict = {alt["link"]: alt["name"] for alt in alternate} @@ -390,7 +266,7 @@ def update_config(lang: str): new_alternate.append({"link": url, "name": use_name}) config["nav"][1] = {"Languages": languages} config["extra"]["alternate"] = new_alternate - config_path.write_text( + en_config_path.write_text( yaml.dump(config, sort_keys=False, width=200, allow_unicode=True), encoding="utf-8", ) @@ -405,56 +281,5 @@ def langs_json(): print(json.dumps(langs)) -def get_key_section( - *, key_to_section: Dict[Tuple[str, ...], list], key: Tuple[str, ...] -) -> list: - if key in key_to_section: - return key_to_section[key] - super_key = key[:-1] - title = key[-1] - super_section = get_key_section(key_to_section=key_to_section, key=super_key) - new_section = [] - super_section.append({title: new_section}) - key_to_section[key] = new_section - return new_section - - -def get_text_with_translate_missing(text: str) -> str: - lines = text.splitlines() - lines.insert(1, missing_translation_snippet) - new_text = "\n".join(lines) - return new_text - - -def get_file_to_nav_map(nav: list) -> Dict[str, Tuple[str, ...]]: - file_to_nav = {} - for item in nav: - if type(item) is str: - file_to_nav[item] = () - elif type(item) is dict: - item_key = list(item.keys())[0] - sub_nav = item[item_key] - sub_file_to_nav = get_file_to_nav_map(sub_nav) - for k, v in sub_file_to_nav.items(): - file_to_nav[k] = (item_key,) + v - return file_to_nav - - -def get_sections(nav: list) -> Dict[Tuple[str, ...], str]: - sections = {} - for item in nav: - if type(item) is str: - continue - elif type(item) is dict: - item_key = list(item.keys())[0] - sub_nav = item[item_key] - sections[(item_key,)] = sub_nav[0] - sub_sections = get_sections(sub_nav) - for k, v in sub_sections.items(): - new_key = (item_key,) + k - sections[new_key] = v - return sections - - if __name__ == "__main__": app() diff --git a/scripts/mkdocs_hooks.py b/scripts/mkdocs_hooks.py index f09e9a99d..008751f8a 100644 --- a/scripts/mkdocs_hooks.py +++ b/scripts/mkdocs_hooks.py @@ -1,11 +1,88 @@ +from functools import lru_cache +from pathlib import Path from typing import Any, List, Union +import material from mkdocs.config.defaults import MkDocsConfig -from mkdocs.structure.files import Files +from mkdocs.structure.files import File, Files from mkdocs.structure.nav import Link, Navigation, Section from mkdocs.structure.pages import Page +@lru_cache() +def get_missing_translation_content(docs_dir: str) -> str: + docs_dir_path = Path(docs_dir) + missing_translation_path = docs_dir_path.parent.parent / "missing-translation.md" + return missing_translation_path.read_text(encoding="utf-8") + + +@lru_cache() +def get_mkdocs_material_langs() -> List[str]: + material_path = Path(material.__file__).parent + material_langs_path = material_path / "partials" / "languages" + langs = [file.stem for file in material_langs_path.glob("*.html")] + return langs + + +class EnFile(File): + pass + + +def on_config(config: MkDocsConfig, **kwargs: Any) -> MkDocsConfig: + available_langs = get_mkdocs_material_langs() + dir_path = Path(config.docs_dir) + lang = dir_path.parent.name + if lang in available_langs: + config.theme["language"] = lang + if not (config.site_url or "").endswith(f"{lang}/") and not lang == "en": + config.site_url = f"{config.site_url}{lang}/" + return config + + +def resolve_file(*, item: str, files: Files, config: MkDocsConfig) -> None: + item_path = Path(config.docs_dir) / item + if not item_path.is_file(): + en_src_dir = (Path(config.docs_dir) / "../../en/docs").resolve() + potential_path = en_src_dir / item + if potential_path.is_file(): + files.append( + EnFile( + path=item, + src_dir=str(en_src_dir), + dest_dir=config.site_dir, + use_directory_urls=config.use_directory_urls, + ) + ) + + +def resolve_files(*, items: List[Any], files: Files, config: MkDocsConfig) -> None: + for item in items: + if isinstance(item, str): + resolve_file(item=item, files=files, config=config) + elif isinstance(item, dict): + assert len(item) == 1 + values = list(item.values()) + if not values: + continue + if isinstance(values[0], str): + resolve_file(item=values[0], files=files, config=config) + elif isinstance(values[0], list): + resolve_files(items=values[0], files=files, config=config) + else: + raise ValueError(f"Unexpected value: {values}") + + +def on_files(files: Files, *, config: MkDocsConfig) -> Files: + resolve_files(items=config.nav or [], files=files, config=config) + if "logo" in config.theme: + resolve_file(item=config.theme["logo"], files=files, config=config) + if "favicon" in config.theme: + resolve_file(item=config.theme["favicon"], files=files, config=config) + resolve_files(items=config.extra_css, files=files, config=config) + resolve_files(items=config.extra_javascript, files=files, config=config) + return files + + def generate_renamed_section_items( items: List[Union[Page, Section, Link]], *, config: MkDocsConfig ) -> List[Union[Page, Section, Link]]: @@ -36,3 +113,20 @@ def on_nav( ) -> Navigation: new_items = generate_renamed_section_items(nav.items, config=config) return Navigation(items=new_items, pages=nav.pages) + + +def on_pre_page(page: Page, *, config: MkDocsConfig, files: Files) -> Page: + return page + + +def on_page_markdown( + markdown: str, *, page: Page, config: MkDocsConfig, files: Files +) -> str: + if isinstance(page.file, EnFile): + missing_translation_content = get_missing_translation_content(config.docs_dir) + header = "" + body = markdown + if markdown.startswith("#"): + header, _, body = markdown.partition("\n\n") + return f"{header}\n\n{missing_translation_content}\n\n{body}" + return markdown From be8e704e46e26bc0ef8f331e90ef5574860897af Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 25 Jun 2023 12:34:39 +0000 Subject: [PATCH 12/58] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 88f2ddf2d..b8c9a1106 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✨ Refactor docs for building scripts, use MkDocs hooks, simplify (remove) configs for languages. PR [#9742](https://github.com/tiangolo/fastapi/pull/9742) by [@tiangolo](https://github.com/tiangolo). * 🔨 Add MkDocs hook that renames sections based on the first index file. PR [#9737](https://github.com/tiangolo/fastapi/pull/9737) by [@tiangolo](https://github.com/tiangolo). * 👷 Make cron jobs run only on main repo, not on forks, to avoid error notifications from missing tokens. PR [#9735](https://github.com/tiangolo/fastapi/pull/9735) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update MkDocs for other languages. PR [#9734](https://github.com/tiangolo/fastapi/pull/9734) by [@tiangolo](https://github.com/tiangolo). From b107b6a0968d4fad394b94ff883a5bcfd00bebca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 25 Jun 2023 14:57:19 +0200 Subject: [PATCH 13/58] =?UTF-8?q?=F0=9F=94=A5=20Remove=20languages=20witho?= =?UTF-8?q?ut=20translations=20(#9743)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🔥 Remove lang directories for empty translations * 🔥 Remove untranslated langs from main config --- docs/az/docs/index.md | 465 ----------------------------------------- docs/az/mkdocs.yml | 1 - docs/cs/docs/index.md | 473 ------------------------------------------ docs/cs/mkdocs.yml | 1 - docs/en/mkdocs.yml | 27 --- docs/hy/docs/index.md | 467 ----------------------------------------- docs/hy/mkdocs.yml | 1 - docs/it/docs/index.md | 462 ----------------------------------------- docs/it/mkdocs.yml | 1 - docs/lo/docs/index.md | 469 ----------------------------------------- docs/lo/mkdocs.yml | 1 - docs/nl/docs/index.md | 467 ----------------------------------------- docs/nl/mkdocs.yml | 1 - docs/sq/docs/index.md | 465 ----------------------------------------- docs/sq/mkdocs.yml | 1 - docs/sv/docs/index.md | 467 ----------------------------------------- docs/sv/mkdocs.yml | 1 - docs/uk/docs/index.md | 465 ----------------------------------------- docs/uk/mkdocs.yml | 1 - 19 files changed, 4236 deletions(-) delete mode 100644 docs/az/docs/index.md delete mode 100644 docs/az/mkdocs.yml delete mode 100644 docs/cs/docs/index.md delete mode 100644 docs/cs/mkdocs.yml delete mode 100644 docs/hy/docs/index.md delete mode 100644 docs/hy/mkdocs.yml delete mode 100644 docs/it/docs/index.md delete mode 100644 docs/it/mkdocs.yml delete mode 100644 docs/lo/docs/index.md delete mode 100644 docs/lo/mkdocs.yml delete mode 100644 docs/nl/docs/index.md delete mode 100644 docs/nl/mkdocs.yml delete mode 100644 docs/sq/docs/index.md delete mode 100644 docs/sq/mkdocs.yml delete mode 100644 docs/sv/docs/index.md delete mode 100644 docs/sv/mkdocs.yml delete mode 100644 docs/uk/docs/index.md delete mode 100644 docs/uk/mkdocs.yml diff --git a/docs/az/docs/index.md b/docs/az/docs/index.md deleted file mode 100644 index 8b1c65194..000000000 --- a/docs/az/docs/index.md +++ /dev/null @@ -1,465 +0,0 @@ - -{!../../../docs/missing-translation.md!} - - -

- FastAPI -

-

- FastAPI framework, high performance, easy to learn, fast to code, ready for production -

-

- - Test - - - Coverage - - - Package version - -

- ---- - -**Documentation**: https://fastapi.tiangolo.com - -**Source Code**: https://github.com/tiangolo/fastapi - ---- - -FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.6+ based on standard Python type hints. - -The key features are: - -* **Fast**: Very high performance, on par with **NodeJS** and **Go** (thanks to Starlette and Pydantic). [One of the fastest Python frameworks available](#performance). - -* **Fast to code**: Increase the speed to develop features by about 200% to 300%. * -* **Fewer bugs**: Reduce about 40% of human (developer) induced errors. * -* **Intuitive**: Great editor support. Completion everywhere. Less time debugging. -* **Easy**: Designed to be easy to use and learn. Less time reading docs. -* **Short**: Minimize code duplication. Multiple features from each parameter declaration. Fewer bugs. -* **Robust**: Get production-ready code. With automatic interactive documentation. -* **Standards-based**: Based on (and fully compatible with) the open standards for APIs: OpenAPI (previously known as Swagger) and JSON Schema. - -* estimation based on tests on an internal development team, building production applications. - -## Sponsors - - - -{% if sponsors %} -{% for sponsor in sponsors.gold -%} - -{% endfor -%} -{%- for sponsor in sponsors.silver -%} - -{% endfor %} -{% endif %} - - - -Other sponsors - -## Opinions - -"_[...] I'm using **FastAPI** a ton these days. [...] I'm actually planning to use it for all of my team's **ML services at Microsoft**. Some of them are getting integrated into the core **Windows** product and some **Office** products._" - -
Kabir Khan - Microsoft (ref)
- ---- - -"_We adopted the **FastAPI** library to spawn a **REST** server that can be queried to obtain **predictions**. [for Ludwig]_" - -
Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - Uber (ref)
- ---- - -"_**Netflix** is pleased to announce the open-source release of our **crisis management** orchestration framework: **Dispatch**! [built with **FastAPI**]_" - -
Kevin Glisson, Marc Vilanova, Forest Monsen - Netflix (ref)
- ---- - -"_I’m over the moon excited about **FastAPI**. It’s so fun!_" - -
Brian Okken - Python Bytes podcast host (ref)
- ---- - -"_Honestly, what you've built looks super solid and polished. In many ways, it's what I wanted **Hug** to be - it's really inspiring to see someone build that._" - -
Timothy Crosley - Hug creator (ref)
- ---- - -"_If you're looking to learn one **modern framework** for building REST APIs, check out **FastAPI** [...] It's fast, easy to use and easy to learn [...]_" - -"_We've switched over to **FastAPI** for our **APIs** [...] I think you'll like it [...]_" - -
Ines Montani - Matthew Honnibal - Explosion AI founders - spaCy creators (ref) - (ref)
- ---- - -## **Typer**, the FastAPI of CLIs - - - -If you are building a CLI app to be used in the terminal instead of a web API, check out **Typer**. - -**Typer** is FastAPI's little sibling. And it's intended to be the **FastAPI of CLIs**. ⌨️ 🚀 - -## Requirements - -Python 3.7+ - -FastAPI stands on the shoulders of giants: - -* Starlette for the web parts. -* Pydantic for the data parts. - -## Installation - -
- -```console -$ pip install fastapi - ----> 100% -``` - -
- -You will also need an ASGI server, for production such as Uvicorn or Hypercorn. - -
- -```console -$ pip install "uvicorn[standard]" - ----> 100% -``` - -
- -## Example - -### Create it - -* Create a file `main.py` with: - -```Python -from typing import Optional - -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int, q: Optional[str] = None): - return {"item_id": item_id, "q": q} -``` - -
-Or use async def... - -If your code uses `async` / `await`, use `async def`: - -```Python hl_lines="9 14" -from typing import Optional - -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -async def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -async def read_item(item_id: int, q: Optional[str] = None): - return {"item_id": item_id, "q": q} -``` - -**Note**: - -If you don't know, check the _"In a hurry?"_ section about `async` and `await` in the docs. - -
- -### Run it - -Run the server with: - -
- -```console -$ uvicorn main:app --reload - -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -INFO: Started reloader process [28720] -INFO: Started server process [28722] -INFO: Waiting for application startup. -INFO: Application startup complete. -``` - -
- -
-About the command uvicorn main:app --reload... - -The command `uvicorn main:app` refers to: - -* `main`: the file `main.py` (the Python "module"). -* `app`: the object created inside of `main.py` with the line `app = FastAPI()`. -* `--reload`: make the server restart after code changes. Only do this for development. - -
- -### Check it - -Open your browser at http://127.0.0.1:8000/items/5?q=somequery. - -You will see the JSON response as: - -```JSON -{"item_id": 5, "q": "somequery"} -``` - -You already created an API that: - -* Receives HTTP requests in the _paths_ `/` and `/items/{item_id}`. -* Both _paths_ take `GET` operations (also known as HTTP _methods_). -* The _path_ `/items/{item_id}` has a _path parameter_ `item_id` that should be an `int`. -* The _path_ `/items/{item_id}` has an optional `str` _query parameter_ `q`. - -### Interactive API docs - -Now go to http://127.0.0.1:8000/docs. - -You will see the automatic interactive API documentation (provided by Swagger UI): - -![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) - -### Alternative API docs - -And now, go to http://127.0.0.1:8000/redoc. - -You will see the alternative automatic documentation (provided by ReDoc): - -![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) - -## Example upgrade - -Now modify the file `main.py` to receive a body from a `PUT` request. - -Declare the body using standard Python types, thanks to Pydantic. - -```Python hl_lines="4 9-12 25-27" -from typing import Optional - -from fastapi import FastAPI -from pydantic import BaseModel - -app = FastAPI() - - -class Item(BaseModel): - name: str - price: float - is_offer: Optional[bool] = None - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int, q: Optional[str] = None): - return {"item_id": item_id, "q": q} - - -@app.put("/items/{item_id}") -def update_item(item_id: int, item: Item): - return {"item_name": item.name, "item_id": item_id} -``` - -The server should reload automatically (because you added `--reload` to the `uvicorn` command above). - -### Interactive API docs upgrade - -Now go to http://127.0.0.1:8000/docs. - -* The interactive API documentation will be automatically updated, including the new body: - -![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) - -* Click on the button "Try it out", it allows you to fill the parameters and directly interact with the API: - -![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) - -* Then click on the "Execute" button, the user interface will communicate with your API, send the parameters, get the results and show them on the screen: - -![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) - -### Alternative API docs upgrade - -And now, go to http://127.0.0.1:8000/redoc. - -* The alternative documentation will also reflect the new query parameter and body: - -![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) - -### Recap - -In summary, you declare **once** the types of parameters, body, etc. as function parameters. - -You do that with standard modern Python types. - -You don't have to learn a new syntax, the methods or classes of a specific library, etc. - -Just standard **Python 3.6+**. - -For example, for an `int`: - -```Python -item_id: int -``` - -or for a more complex `Item` model: - -```Python -item: Item -``` - -...and with that single declaration you get: - -* Editor support, including: - * Completion. - * Type checks. -* Validation of data: - * Automatic and clear errors when the data is invalid. - * Validation even for deeply nested JSON objects. -* Conversion of input data: coming from the network to Python data and types. Reading from: - * JSON. - * Path parameters. - * Query parameters. - * Cookies. - * Headers. - * Forms. - * Files. -* Conversion of output data: converting from Python data and types to network data (as JSON): - * Convert Python types (`str`, `int`, `float`, `bool`, `list`, etc). - * `datetime` objects. - * `UUID` objects. - * Database models. - * ...and many more. -* Automatic interactive API documentation, including 2 alternative user interfaces: - * Swagger UI. - * ReDoc. - ---- - -Coming back to the previous code example, **FastAPI** will: - -* Validate that there is an `item_id` in the path for `GET` and `PUT` requests. -* Validate that the `item_id` is of type `int` for `GET` and `PUT` requests. - * If it is not, the client will see a useful, clear error. -* Check if there is an optional query parameter named `q` (as in `http://127.0.0.1:8000/items/foo?q=somequery`) for `GET` requests. - * As the `q` parameter is declared with `= None`, it is optional. - * Without the `None` it would be required (as is the body in the case with `PUT`). -* For `PUT` requests to `/items/{item_id}`, Read the body as JSON: - * Check that it has a required attribute `name` that should be a `str`. - * Check that it has a required attribute `price` that has to be a `float`. - * Check that it has an optional attribute `is_offer`, that should be a `bool`, if present. - * All this would also work for deeply nested JSON objects. -* Convert from and to JSON automatically. -* Document everything with OpenAPI, that can be used by: - * Interactive documentation systems. - * Automatic client code generation systems, for many languages. -* Provide 2 interactive documentation web interfaces directly. - ---- - -We just scratched the surface, but you already get the idea of how it all works. - -Try changing the line with: - -```Python - return {"item_name": item.name, "item_id": item_id} -``` - -...from: - -```Python - ... "item_name": item.name ... -``` - -...to: - -```Python - ... "item_price": item.price ... -``` - -...and see how your editor will auto-complete the attributes and know their types: - -![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) - -For a more complete example including more features, see the Tutorial - User Guide. - -**Spoiler alert**: the tutorial - user guide includes: - -* Declaration of **parameters** from other different places as: **headers**, **cookies**, **form fields** and **files**. -* How to set **validation constraints** as `maximum_length` or `regex`. -* A very powerful and easy to use **Dependency Injection** system. -* Security and authentication, including support for **OAuth2** with **JWT tokens** and **HTTP Basic** auth. -* More advanced (but equally easy) techniques for declaring **deeply nested JSON models** (thanks to Pydantic). -* Many extra features (thanks to Starlette) as: - * **WebSockets** - * **GraphQL** - * extremely easy tests based on `requests` and `pytest` - * **CORS** - * **Cookie Sessions** - * ...and more. - -## Performance - -Independent TechEmpower benchmarks show **FastAPI** applications running under Uvicorn as one of the fastest Python frameworks available, only below Starlette and Uvicorn themselves (used internally by FastAPI). (*) - -To understand more about it, see the section Benchmarks. - -## Optional Dependencies - -Used by Pydantic: - -* email_validator - for email validation. - -Used by Starlette: - -* httpx - Required if you want to use the `TestClient`. -* jinja2 - Required if you want to use the default template configuration. -* python-multipart - Required if you want to support form "parsing", with `request.form()`. -* itsdangerous - Required for `SessionMiddleware` support. -* pyyaml - Required for Starlette's `SchemaGenerator` support (you probably don't need it with FastAPI). -* graphene - Required for `GraphQLApp` support. -* ujson - Required if you want to use `UJSONResponse`. - -Used by FastAPI / Starlette: - -* uvicorn - for the server that loads and serves your application. -* orjson - Required if you want to use `ORJSONResponse`. - -You can install all of these with `pip install fastapi[all]`. - -## License - -This project is licensed under the terms of the MIT license. diff --git a/docs/az/mkdocs.yml b/docs/az/mkdocs.yml deleted file mode 100644 index de18856f4..000000000 --- a/docs/az/mkdocs.yml +++ /dev/null @@ -1 +0,0 @@ -INHERIT: ../en/mkdocs.yml diff --git a/docs/cs/docs/index.md b/docs/cs/docs/index.md deleted file mode 100644 index bde72f851..000000000 --- a/docs/cs/docs/index.md +++ /dev/null @@ -1,473 +0,0 @@ - -{!../../../docs/missing-translation.md!} - - -

- FastAPI -

-

- FastAPI framework, high performance, easy to learn, fast to code, ready for production -

-

- - Test - - - Coverage - - - Package version - - - Supported Python versions - -

- ---- - -**Documentation**: https://fastapi.tiangolo.com - -**Source Code**: https://github.com/tiangolo/fastapi - ---- - -FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.7+ based on standard Python type hints. - -The key features are: - -* **Fast**: Very high performance, on par with **NodeJS** and **Go** (thanks to Starlette and Pydantic). [One of the fastest Python frameworks available](#performance). -* **Fast to code**: Increase the speed to develop features by about 200% to 300%. * -* **Fewer bugs**: Reduce about 40% of human (developer) induced errors. * -* **Intuitive**: Great editor support. Completion everywhere. Less time debugging. -* **Easy**: Designed to be easy to use and learn. Less time reading docs. -* **Short**: Minimize code duplication. Multiple features from each parameter declaration. Fewer bugs. -* **Robust**: Get production-ready code. With automatic interactive documentation. -* **Standards-based**: Based on (and fully compatible with) the open standards for APIs: OpenAPI (previously known as Swagger) and JSON Schema. - -* estimation based on tests on an internal development team, building production applications. - -## Sponsors - - - -{% if sponsors %} -{% for sponsor in sponsors.gold -%} - -{% endfor -%} -{%- for sponsor in sponsors.silver -%} - -{% endfor %} -{% endif %} - - - -Other sponsors - -## Opinions - -"_[...] I'm using **FastAPI** a ton these days. [...] I'm actually planning to use it for all of my team's **ML services at Microsoft**. Some of them are getting integrated into the core **Windows** product and some **Office** products._" - -
Kabir Khan - Microsoft (ref)
- ---- - -"_We adopted the **FastAPI** library to spawn a **REST** server that can be queried to obtain **predictions**. [for Ludwig]_" - -
Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - Uber (ref)
- ---- - -"_**Netflix** is pleased to announce the open-source release of our **crisis management** orchestration framework: **Dispatch**! [built with **FastAPI**]_" - -
Kevin Glisson, Marc Vilanova, Forest Monsen - Netflix (ref)
- ---- - -"_I’m over the moon excited about **FastAPI**. It’s so fun!_" - -
Brian Okken - Python Bytes podcast host (ref)
- ---- - -"_Honestly, what you've built looks super solid and polished. In many ways, it's what I wanted **Hug** to be - it's really inspiring to see someone build that._" - -
Timothy Crosley - Hug creator (ref)
- ---- - -"_If you're looking to learn one **modern framework** for building REST APIs, check out **FastAPI** [...] It's fast, easy to use and easy to learn [...]_" - -"_We've switched over to **FastAPI** for our **APIs** [...] I think you'll like it [...]_" - -
Ines Montani - Matthew Honnibal - Explosion AI founders - spaCy creators (ref) - (ref)
- ---- - -"_If anyone is looking to build a production Python API, I would highly recommend **FastAPI**. It is **beautifully designed**, **simple to use** and **highly scalable**, it has become a **key component** in our API first development strategy and is driving many automations and services such as our Virtual TAC Engineer._" - -
Deon Pillsbury - Cisco (ref)
- ---- - -## **Typer**, the FastAPI of CLIs - - - -If you are building a CLI app to be used in the terminal instead of a web API, check out **Typer**. - -**Typer** is FastAPI's little sibling. And it's intended to be the **FastAPI of CLIs**. ⌨️ 🚀 - -## Requirements - -Python 3.7+ - -FastAPI stands on the shoulders of giants: - -* Starlette for the web parts. -* Pydantic for the data parts. - -## Installation - -
- -```console -$ pip install fastapi - ----> 100% -``` - -
- -You will also need an ASGI server, for production such as Uvicorn or Hypercorn. - -
- -```console -$ pip install "uvicorn[standard]" - ----> 100% -``` - -
- -## Example - -### Create it - -* Create a file `main.py` with: - -```Python -from typing import Union - -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} -``` - -
-Or use async def... - -If your code uses `async` / `await`, use `async def`: - -```Python hl_lines="9 14" -from typing import Union - -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -async def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -async def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} -``` - -**Note**: - -If you don't know, check the _"In a hurry?"_ section about `async` and `await` in the docs. - -
- -### Run it - -Run the server with: - -
- -```console -$ uvicorn main:app --reload - -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -INFO: Started reloader process [28720] -INFO: Started server process [28722] -INFO: Waiting for application startup. -INFO: Application startup complete. -``` - -
- -
-About the command uvicorn main:app --reload... - -The command `uvicorn main:app` refers to: - -* `main`: the file `main.py` (the Python "module"). -* `app`: the object created inside of `main.py` with the line `app = FastAPI()`. -* `--reload`: make the server restart after code changes. Only do this for development. - -
- -### Check it - -Open your browser at http://127.0.0.1:8000/items/5?q=somequery. - -You will see the JSON response as: - -```JSON -{"item_id": 5, "q": "somequery"} -``` - -You already created an API that: - -* Receives HTTP requests in the _paths_ `/` and `/items/{item_id}`. -* Both _paths_ take `GET` operations (also known as HTTP _methods_). -* The _path_ `/items/{item_id}` has a _path parameter_ `item_id` that should be an `int`. -* The _path_ `/items/{item_id}` has an optional `str` _query parameter_ `q`. - -### Interactive API docs - -Now go to http://127.0.0.1:8000/docs. - -You will see the automatic interactive API documentation (provided by Swagger UI): - -![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) - -### Alternative API docs - -And now, go to http://127.0.0.1:8000/redoc. - -You will see the alternative automatic documentation (provided by ReDoc): - -![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) - -## Example upgrade - -Now modify the file `main.py` to receive a body from a `PUT` request. - -Declare the body using standard Python types, thanks to Pydantic. - -```Python hl_lines="4 9-12 25-27" -from typing import Union - -from fastapi import FastAPI -from pydantic import BaseModel - -app = FastAPI() - - -class Item(BaseModel): - name: str - price: float - is_offer: Union[bool, None] = None - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} - - -@app.put("/items/{item_id}") -def update_item(item_id: int, item: Item): - return {"item_name": item.name, "item_id": item_id} -``` - -The server should reload automatically (because you added `--reload` to the `uvicorn` command above). - -### Interactive API docs upgrade - -Now go to http://127.0.0.1:8000/docs. - -* The interactive API documentation will be automatically updated, including the new body: - -![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) - -* Click on the button "Try it out", it allows you to fill the parameters and directly interact with the API: - -![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) - -* Then click on the "Execute" button, the user interface will communicate with your API, send the parameters, get the results and show them on the screen: - -![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) - -### Alternative API docs upgrade - -And now, go to http://127.0.0.1:8000/redoc. - -* The alternative documentation will also reflect the new query parameter and body: - -![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) - -### Recap - -In summary, you declare **once** the types of parameters, body, etc. as function parameters. - -You do that with standard modern Python types. - -You don't have to learn a new syntax, the methods or classes of a specific library, etc. - -Just standard **Python 3.7+**. - -For example, for an `int`: - -```Python -item_id: int -``` - -or for a more complex `Item` model: - -```Python -item: Item -``` - -...and with that single declaration you get: - -* Editor support, including: - * Completion. - * Type checks. -* Validation of data: - * Automatic and clear errors when the data is invalid. - * Validation even for deeply nested JSON objects. -* Conversion of input data: coming from the network to Python data and types. Reading from: - * JSON. - * Path parameters. - * Query parameters. - * Cookies. - * Headers. - * Forms. - * Files. -* Conversion of output data: converting from Python data and types to network data (as JSON): - * Convert Python types (`str`, `int`, `float`, `bool`, `list`, etc). - * `datetime` objects. - * `UUID` objects. - * Database models. - * ...and many more. -* Automatic interactive API documentation, including 2 alternative user interfaces: - * Swagger UI. - * ReDoc. - ---- - -Coming back to the previous code example, **FastAPI** will: - -* Validate that there is an `item_id` in the path for `GET` and `PUT` requests. -* Validate that the `item_id` is of type `int` for `GET` and `PUT` requests. - * If it is not, the client will see a useful, clear error. -* Check if there is an optional query parameter named `q` (as in `http://127.0.0.1:8000/items/foo?q=somequery`) for `GET` requests. - * As the `q` parameter is declared with `= None`, it is optional. - * Without the `None` it would be required (as is the body in the case with `PUT`). -* For `PUT` requests to `/items/{item_id}`, Read the body as JSON: - * Check that it has a required attribute `name` that should be a `str`. - * Check that it has a required attribute `price` that has to be a `float`. - * Check that it has an optional attribute `is_offer`, that should be a `bool`, if present. - * All this would also work for deeply nested JSON objects. -* Convert from and to JSON automatically. -* Document everything with OpenAPI, that can be used by: - * Interactive documentation systems. - * Automatic client code generation systems, for many languages. -* Provide 2 interactive documentation web interfaces directly. - ---- - -We just scratched the surface, but you already get the idea of how it all works. - -Try changing the line with: - -```Python - return {"item_name": item.name, "item_id": item_id} -``` - -...from: - -```Python - ... "item_name": item.name ... -``` - -...to: - -```Python - ... "item_price": item.price ... -``` - -...and see how your editor will auto-complete the attributes and know their types: - -![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) - -For a more complete example including more features, see the Tutorial - User Guide. - -**Spoiler alert**: the tutorial - user guide includes: - -* Declaration of **parameters** from other different places as: **headers**, **cookies**, **form fields** and **files**. -* How to set **validation constraints** as `maximum_length` or `regex`. -* A very powerful and easy to use **Dependency Injection** system. -* Security and authentication, including support for **OAuth2** with **JWT tokens** and **HTTP Basic** auth. -* More advanced (but equally easy) techniques for declaring **deeply nested JSON models** (thanks to Pydantic). -* **GraphQL** integration with Strawberry and other libraries. -* Many extra features (thanks to Starlette) as: - * **WebSockets** - * extremely easy tests based on HTTPX and `pytest` - * **CORS** - * **Cookie Sessions** - * ...and more. - -## Performance - -Independent TechEmpower benchmarks show **FastAPI** applications running under Uvicorn as one of the fastest Python frameworks available, only below Starlette and Uvicorn themselves (used internally by FastAPI). (*) - -To understand more about it, see the section Benchmarks. - -## Optional Dependencies - -Used by Pydantic: - -* ujson - for faster JSON "parsing". -* email_validator - for email validation. - -Used by Starlette: - -* httpx - Required if you want to use the `TestClient`. -* jinja2 - Required if you want to use the default template configuration. -* python-multipart - Required if you want to support form "parsing", with `request.form()`. -* itsdangerous - Required for `SessionMiddleware` support. -* pyyaml - Required for Starlette's `SchemaGenerator` support (you probably don't need it with FastAPI). -* ujson - Required if you want to use `UJSONResponse`. - -Used by FastAPI / Starlette: - -* uvicorn - for the server that loads and serves your application. -* orjson - Required if you want to use `ORJSONResponse`. - -You can install all of these with `pip install "fastapi[all]"`. - -## License - -This project is licensed under the terms of the MIT license. diff --git a/docs/cs/mkdocs.yml b/docs/cs/mkdocs.yml deleted file mode 100644 index de18856f4..000000000 --- a/docs/cs/mkdocs.yml +++ /dev/null @@ -1 +0,0 @@ -INHERIT: ../en/mkdocs.yml diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index a21848a66..c39d656ff 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -40,28 +40,19 @@ nav: - FastAPI: index.md - Languages: - en: / - - az: /az/ - - cs: /cs/ - de: /de/ - em: /em/ - es: /es/ - fa: /fa/ - fr: /fr/ - he: /he/ - - hy: /hy/ - id: /id/ - - it: /it/ - ja: /ja/ - ko: /ko/ - - lo: /lo/ - - nl: /nl/ - pl: /pl/ - pt: /pt/ - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - tr: /tr/ - - uk: /uk/ - zh: /zh/ - features.md - fastapi-people.md @@ -211,10 +202,6 @@ extra: alternate: - link: / name: en - English - - link: /az/ - name: az - - link: /cs/ - name: cs - link: /de/ name: de - link: /em/ @@ -227,34 +214,20 @@ extra: name: fr - français - link: /he/ name: he - - link: /hy/ - name: hy - link: /id/ name: id - - link: /it/ - name: it - italiano - link: /ja/ name: ja - 日本語 - link: /ko/ name: ko - 한국어 - - link: /lo/ - name: lo - ພາສາລາວ - - link: /nl/ - name: nl - link: /pl/ name: pl - link: /pt/ name: pt - português - link: /ru/ name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - link: /tr/ name: tr - Türkçe - - link: /uk/ - name: uk - українська мова - link: /zh/ name: zh - 汉语 extra_css: diff --git a/docs/hy/docs/index.md b/docs/hy/docs/index.md deleted file mode 100644 index cc82b33cf..000000000 --- a/docs/hy/docs/index.md +++ /dev/null @@ -1,467 +0,0 @@ - -{!../../../docs/missing-translation.md!} - - -

- FastAPI -

-

- FastAPI framework, high performance, easy to learn, fast to code, ready for production -

-

- - Test - - - Coverage - - - Package version - - - Supported Python versions - -

- ---- - -**Documentation**: https://fastapi.tiangolo.com - -**Source Code**: https://github.com/tiangolo/fastapi - ---- - -FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.7+ based on standard Python type hints. - -The key features are: - -* **Fast**: Very high performance, on par with **NodeJS** and **Go** (thanks to Starlette and Pydantic). [One of the fastest Python frameworks available](#performance). -* **Fast to code**: Increase the speed to develop features by about 200% to 300%. * -* **Fewer bugs**: Reduce about 40% of human (developer) induced errors. * -* **Intuitive**: Great editor support. Completion everywhere. Less time debugging. -* **Easy**: Designed to be easy to use and learn. Less time reading docs. -* **Short**: Minimize code duplication. Multiple features from each parameter declaration. Fewer bugs. -* **Robust**: Get production-ready code. With automatic interactive documentation. -* **Standards-based**: Based on (and fully compatible with) the open standards for APIs: OpenAPI (previously known as Swagger) and JSON Schema. - -* estimation based on tests on an internal development team, building production applications. - -## Sponsors - - - -{% if sponsors %} -{% for sponsor in sponsors.gold -%} - -{% endfor -%} -{%- for sponsor in sponsors.silver -%} - -{% endfor %} -{% endif %} - - - -Other sponsors - -## Opinions - -"_[...] I'm using **FastAPI** a ton these days. [...] I'm actually planning to use it for all of my team's **ML services at Microsoft**. Some of them are getting integrated into the core **Windows** product and some **Office** products._" - -
Kabir Khan - Microsoft (ref)
- ---- - -"_We adopted the **FastAPI** library to spawn a **REST** server that can be queried to obtain **predictions**. [for Ludwig]_" - -
Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - Uber (ref)
- ---- - -"_**Netflix** is pleased to announce the open-source release of our **crisis management** orchestration framework: **Dispatch**! [built with **FastAPI**]_" - -
Kevin Glisson, Marc Vilanova, Forest Monsen - Netflix (ref)
- ---- - -"_I’m over the moon excited about **FastAPI**. It’s so fun!_" - -
Brian Okken - Python Bytes podcast host (ref)
- ---- - -"_Honestly, what you've built looks super solid and polished. In many ways, it's what I wanted **Hug** to be - it's really inspiring to see someone build that._" - -
Timothy Crosley - Hug creator (ref)
- ---- - -"_If you're looking to learn one **modern framework** for building REST APIs, check out **FastAPI** [...] It's fast, easy to use and easy to learn [...]_" - -"_We've switched over to **FastAPI** for our **APIs** [...] I think you'll like it [...]_" - -
Ines Montani - Matthew Honnibal - Explosion AI founders - spaCy creators (ref) - (ref)
- ---- - -## **Typer**, the FastAPI of CLIs - - - -If you are building a CLI app to be used in the terminal instead of a web API, check out **Typer**. - -**Typer** is FastAPI's little sibling. And it's intended to be the **FastAPI of CLIs**. ⌨️ 🚀 - -## Requirements - -Python 3.7+ - -FastAPI stands on the shoulders of giants: - -* Starlette for the web parts. -* Pydantic for the data parts. - -## Installation - -
- -```console -$ pip install fastapi - ----> 100% -``` - -
- -You will also need an ASGI server, for production such as Uvicorn or Hypercorn. - -
- -```console -$ pip install "uvicorn[standard]" - ----> 100% -``` - -
- -## Example - -### Create it - -* Create a file `main.py` with: - -```Python -from typing import Union - -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} -``` - -
-Or use async def... - -If your code uses `async` / `await`, use `async def`: - -```Python hl_lines="9 14" -from typing import Union - -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -async def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -async def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} -``` - -**Note**: - -If you don't know, check the _"In a hurry?"_ section about `async` and `await` in the docs. - -
- -### Run it - -Run the server with: - -
- -```console -$ uvicorn main:app --reload - -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -INFO: Started reloader process [28720] -INFO: Started server process [28722] -INFO: Waiting for application startup. -INFO: Application startup complete. -``` - -
- -
-About the command uvicorn main:app --reload... - -The command `uvicorn main:app` refers to: - -* `main`: the file `main.py` (the Python "module"). -* `app`: the object created inside of `main.py` with the line `app = FastAPI()`. -* `--reload`: make the server restart after code changes. Only do this for development. - -
- -### Check it - -Open your browser at http://127.0.0.1:8000/items/5?q=somequery. - -You will see the JSON response as: - -```JSON -{"item_id": 5, "q": "somequery"} -``` - -You already created an API that: - -* Receives HTTP requests in the _paths_ `/` and `/items/{item_id}`. -* Both _paths_ take `GET` operations (also known as HTTP _methods_). -* The _path_ `/items/{item_id}` has a _path parameter_ `item_id` that should be an `int`. -* The _path_ `/items/{item_id}` has an optional `str` _query parameter_ `q`. - -### Interactive API docs - -Now go to http://127.0.0.1:8000/docs. - -You will see the automatic interactive API documentation (provided by Swagger UI): - -![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) - -### Alternative API docs - -And now, go to http://127.0.0.1:8000/redoc. - -You will see the alternative automatic documentation (provided by ReDoc): - -![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) - -## Example upgrade - -Now modify the file `main.py` to receive a body from a `PUT` request. - -Declare the body using standard Python types, thanks to Pydantic. - -```Python hl_lines="4 9-12 25-27" -from typing import Union - -from fastapi import FastAPI -from pydantic import BaseModel - -app = FastAPI() - - -class Item(BaseModel): - name: str - price: float - is_offer: Union[bool, None] = None - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} - - -@app.put("/items/{item_id}") -def update_item(item_id: int, item: Item): - return {"item_name": item.name, "item_id": item_id} -``` - -The server should reload automatically (because you added `--reload` to the `uvicorn` command above). - -### Interactive API docs upgrade - -Now go to http://127.0.0.1:8000/docs. - -* The interactive API documentation will be automatically updated, including the new body: - -![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) - -* Click on the button "Try it out", it allows you to fill the parameters and directly interact with the API: - -![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) - -* Then click on the "Execute" button, the user interface will communicate with your API, send the parameters, get the results and show them on the screen: - -![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) - -### Alternative API docs upgrade - -And now, go to http://127.0.0.1:8000/redoc. - -* The alternative documentation will also reflect the new query parameter and body: - -![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) - -### Recap - -In summary, you declare **once** the types of parameters, body, etc. as function parameters. - -You do that with standard modern Python types. - -You don't have to learn a new syntax, the methods or classes of a specific library, etc. - -Just standard **Python 3.7+**. - -For example, for an `int`: - -```Python -item_id: int -``` - -or for a more complex `Item` model: - -```Python -item: Item -``` - -...and with that single declaration you get: - -* Editor support, including: - * Completion. - * Type checks. -* Validation of data: - * Automatic and clear errors when the data is invalid. - * Validation even for deeply nested JSON objects. -* Conversion of input data: coming from the network to Python data and types. Reading from: - * JSON. - * Path parameters. - * Query parameters. - * Cookies. - * Headers. - * Forms. - * Files. -* Conversion of output data: converting from Python data and types to network data (as JSON): - * Convert Python types (`str`, `int`, `float`, `bool`, `list`, etc). - * `datetime` objects. - * `UUID` objects. - * Database models. - * ...and many more. -* Automatic interactive API documentation, including 2 alternative user interfaces: - * Swagger UI. - * ReDoc. - ---- - -Coming back to the previous code example, **FastAPI** will: - -* Validate that there is an `item_id` in the path for `GET` and `PUT` requests. -* Validate that the `item_id` is of type `int` for `GET` and `PUT` requests. - * If it is not, the client will see a useful, clear error. -* Check if there is an optional query parameter named `q` (as in `http://127.0.0.1:8000/items/foo?q=somequery`) for `GET` requests. - * As the `q` parameter is declared with `= None`, it is optional. - * Without the `None` it would be required (as is the body in the case with `PUT`). -* For `PUT` requests to `/items/{item_id}`, Read the body as JSON: - * Check that it has a required attribute `name` that should be a `str`. - * Check that it has a required attribute `price` that has to be a `float`. - * Check that it has an optional attribute `is_offer`, that should be a `bool`, if present. - * All this would also work for deeply nested JSON objects. -* Convert from and to JSON automatically. -* Document everything with OpenAPI, that can be used by: - * Interactive documentation systems. - * Automatic client code generation systems, for many languages. -* Provide 2 interactive documentation web interfaces directly. - ---- - -We just scratched the surface, but you already get the idea of how it all works. - -Try changing the line with: - -```Python - return {"item_name": item.name, "item_id": item_id} -``` - -...from: - -```Python - ... "item_name": item.name ... -``` - -...to: - -```Python - ... "item_price": item.price ... -``` - -...and see how your editor will auto-complete the attributes and know their types: - -![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) - -For a more complete example including more features, see the Tutorial - User Guide. - -**Spoiler alert**: the tutorial - user guide includes: - -* Declaration of **parameters** from other different places as: **headers**, **cookies**, **form fields** and **files**. -* How to set **validation constraints** as `maximum_length` or `regex`. -* A very powerful and easy to use **Dependency Injection** system. -* Security and authentication, including support for **OAuth2** with **JWT tokens** and **HTTP Basic** auth. -* More advanced (but equally easy) techniques for declaring **deeply nested JSON models** (thanks to Pydantic). -* **GraphQL** integration with Strawberry and other libraries. -* Many extra features (thanks to Starlette) as: - * **WebSockets** - * extremely easy tests based on HTTPX and `pytest` - * **CORS** - * **Cookie Sessions** - * ...and more. - -## Performance - -Independent TechEmpower benchmarks show **FastAPI** applications running under Uvicorn as one of the fastest Python frameworks available, only below Starlette and Uvicorn themselves (used internally by FastAPI). (*) - -To understand more about it, see the section Benchmarks. - -## Optional Dependencies - -Used by Pydantic: - -* ujson - for faster JSON "parsing". -* email_validator - for email validation. - -Used by Starlette: - -* httpx - Required if you want to use the `TestClient`. -* jinja2 - Required if you want to use the default template configuration. -* python-multipart - Required if you want to support form "parsing", with `request.form()`. -* itsdangerous - Required for `SessionMiddleware` support. -* pyyaml - Required for Starlette's `SchemaGenerator` support (you probably don't need it with FastAPI). -* ujson - Required if you want to use `UJSONResponse`. - -Used by FastAPI / Starlette: - -* uvicorn - for the server that loads and serves your application. -* orjson - Required if you want to use `ORJSONResponse`. - -You can install all of these with `pip install "fastapi[all]"`. - -## License - -This project is licensed under the terms of the MIT license. diff --git a/docs/hy/mkdocs.yml b/docs/hy/mkdocs.yml deleted file mode 100644 index de18856f4..000000000 --- a/docs/hy/mkdocs.yml +++ /dev/null @@ -1 +0,0 @@ -INHERIT: ../en/mkdocs.yml diff --git a/docs/it/docs/index.md b/docs/it/docs/index.md deleted file mode 100644 index 42c9a7e8c..000000000 --- a/docs/it/docs/index.md +++ /dev/null @@ -1,462 +0,0 @@ - -{!../../../docs/missing-translation.md!} - - -

- FastAPI -

-

- FastAPI framework, high performance, easy to learn, fast to code, ready for production -

-

- - Build Status - - - Coverage - - - Package version - -

- ---- - -**Documentation**: https://fastapi.tiangolo.com - -**Source Code**: https://github.com/tiangolo/fastapi - ---- - -FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.6+ based on standard Python type hints. - -The key features are: - -* **Fast**: Very high performance, on par with **NodeJS** and **Go** (thanks to Starlette and Pydantic). [One of the fastest Python frameworks available](#performance). - -* **Fast to code**: Increase the speed to develop features by about 200% to 300%. * -* **Fewer bugs**: Reduce about 40% of human (developer) induced errors. * -* **Intuitive**: Great editor support. Completion everywhere. Less time debugging. -* **Easy**: Designed to be easy to use and learn. Less time reading docs. -* **Short**: Minimize code duplication. Multiple features from each parameter declaration. Fewer bugs. -* **Robust**: Get production-ready code. With automatic interactive documentation. -* **Standards-based**: Based on (and fully compatible with) the open standards for APIs: OpenAPI (previously known as Swagger) and JSON Schema. - -* estimation based on tests on an internal development team, building production applications. - -## Sponsors - - - -{% if sponsors %} -{% for sponsor in sponsors.gold -%} - -{% endfor -%} -{%- for sponsor in sponsors.silver -%} - -{% endfor %} -{% endif %} - - - -Other sponsors - -## Opinions - -"_[...] I'm using **FastAPI** a ton these days. [...] I'm actually planning to use it for all of my team's **ML services at Microsoft**. Some of them are getting integrated into the core **Windows** product and some **Office** products._" - -
Kabir Khan - Microsoft (ref)
- ---- - -"_We adopted the **FastAPI** library to spawn a **REST** server that can be queried to obtain **predictions**. [for Ludwig]_" - -
Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - Uber (ref)
- ---- - -"_**Netflix** is pleased to announce the open-source release of our **crisis management** orchestration framework: **Dispatch**! [built with **FastAPI**]_" - -
Kevin Glisson, Marc Vilanova, Forest Monsen - Netflix (ref)
- ---- - -"_I’m over the moon excited about **FastAPI**. It’s so fun!_" - -
Brian Okken - Python Bytes podcast host (ref)
- ---- - -"_Honestly, what you've built looks super solid and polished. In many ways, it's what I wanted **Hug** to be - it's really inspiring to see someone build that._" - -
Timothy Crosley - Hug creator (ref)
- ---- - -"_If you're looking to learn one **modern framework** for building REST APIs, check out **FastAPI** [...] It's fast, easy to use and easy to learn [...]_" - -"_We've switched over to **FastAPI** for our **APIs** [...] I think you'll like it [...]_" - -
Ines Montani - Matthew Honnibal - Explosion AI founders - spaCy creators (ref) - (ref)
- ---- - -## **Typer**, the FastAPI of CLIs - - - -If you are building a CLI app to be used in the terminal instead of a web API, check out **Typer**. - -**Typer** is FastAPI's little sibling. And it's intended to be the **FastAPI of CLIs**. ⌨️ 🚀 - -## Requirements - -Python 3.7+ - -FastAPI stands on the shoulders of giants: - -* Starlette for the web parts. -* Pydantic for the data parts. - -## Installation - -
- -```console -$ pip install fastapi - ----> 100% -``` - -
- -You will also need an ASGI server, for production such as Uvicorn or Hypercorn. - -
- -```console -$ pip install "uvicorn[standard]" - ----> 100% -``` - -
- -## Example - -### Create it - -* Create a file `main.py` with: - -```Python -from fastapi import FastAPI -from typing import Optional - -app = FastAPI() - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int, q: str = Optional[None]): - return {"item_id": item_id, "q": q} -``` - -
-Or use async def... - -If your code uses `async` / `await`, use `async def`: - -```Python hl_lines="7 12" -from fastapi import FastAPI -from typing import Optional - -app = FastAPI() - - -@app.get("/") -async def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -async def read_item(item_id: int, q: Optional[str] = None): - return {"item_id": item_id, "q": q} -``` - -**Note**: - -If you don't know, check the _"In a hurry?"_ section about `async` and `await` in the docs. - -
- -### Run it - -Run the server with: - -
- -```console -$ uvicorn main:app --reload - -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -INFO: Started reloader process [28720] -INFO: Started server process [28722] -INFO: Waiting for application startup. -INFO: Application startup complete. -``` - -
- -
-About the command uvicorn main:app --reload... - -The command `uvicorn main:app` refers to: - -* `main`: the file `main.py` (the Python "module"). -* `app`: the object created inside of `main.py` with the line `app = FastAPI()`. -* `--reload`: make the server restart after code changes. Only do this for development. - -
- -### Check it - -Open your browser at http://127.0.0.1:8000/items/5?q=somequery. - -You will see the JSON response as: - -```JSON -{"item_id": 5, "q": "somequery"} -``` - -You already created an API that: - -* Receives HTTP requests in the _paths_ `/` and `/items/{item_id}`. -* Both _paths_ take `GET` operations (also known as HTTP _methods_). -* The _path_ `/items/{item_id}` has a _path parameter_ `item_id` that should be an `int`. -* The _path_ `/items/{item_id}` has an optional `str` _query parameter_ `q`. - -### Interactive API docs - -Now go to http://127.0.0.1:8000/docs. - -You will see the automatic interactive API documentation (provided by Swagger UI): - -![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) - -### Alternative API docs - -And now, go to http://127.0.0.1:8000/redoc. - -You will see the alternative automatic documentation (provided by ReDoc): - -![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) - -## Example upgrade - -Now modify the file `main.py` to receive a body from a `PUT` request. - -Declare the body using standard Python types, thanks to Pydantic. - -```Python hl_lines="2 7-10 23-25" -from fastapi import FastAPI -from pydantic import BaseModel -from typing import Optional - -app = FastAPI() - - -class Item(BaseModel): - name: str - price: float - is_offer: bool = Optional[None] - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int, q: Optional[str] = None): - return {"item_id": item_id, "q": q} - - -@app.put("/items/{item_id}") -def update_item(item_id: int, item: Item): - return {"item_name": item.name, "item_id": item_id} -``` - -The server should reload automatically (because you added `--reload` to the `uvicorn` command above). - -### Interactive API docs upgrade - -Now go to http://127.0.0.1:8000/docs. - -* The interactive API documentation will be automatically updated, including the new body: - -![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) - -* Click on the button "Try it out", it allows you to fill the parameters and directly interact with the API: - -![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) - -* Then click on the "Execute" button, the user interface will communicate with your API, send the parameters, get the results and show them on the screen: - -![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) - -### Alternative API docs upgrade - -And now, go to http://127.0.0.1:8000/redoc. - -* The alternative documentation will also reflect the new query parameter and body: - -![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) - -### Recap - -In summary, you declare **once** the types of parameters, body, etc. as function parameters. - -You do that with standard modern Python types. - -You don't have to learn a new syntax, the methods or classes of a specific library, etc. - -Just standard **Python 3.6+**. - -For example, for an `int`: - -```Python -item_id: int -``` - -or for a more complex `Item` model: - -```Python -item: Item -``` - -...and with that single declaration you get: - -* Editor support, including: - * Completion. - * Type checks. -* Validation of data: - * Automatic and clear errors when the data is invalid. - * Validation even for deeply nested JSON objects. -* Conversion of input data: coming from the network to Python data and types. Reading from: - * JSON. - * Path parameters. - * Query parameters. - * Cookies. - * Headers. - * Forms. - * Files. -* Conversion of output data: converting from Python data and types to network data (as JSON): - * Convert Python types (`str`, `int`, `float`, `bool`, `list`, etc). - * `datetime` objects. - * `UUID` objects. - * Database models. - * ...and many more. -* Automatic interactive API documentation, including 2 alternative user interfaces: - * Swagger UI. - * ReDoc. - ---- - -Coming back to the previous code example, **FastAPI** will: - -* Validate that there is an `item_id` in the path for `GET` and `PUT` requests. -* Validate that the `item_id` is of type `int` for `GET` and `PUT` requests. - * If it is not, the client will see a useful, clear error. -* Check if there is an optional query parameter named `q` (as in `http://127.0.0.1:8000/items/foo?q=somequery`) for `GET` requests. - * As the `q` parameter is declared with `= None`, it is optional. - * Without the `None` it would be required (as is the body in the case with `PUT`). -* For `PUT` requests to `/items/{item_id}`, Read the body as JSON: - * Check that it has a required attribute `name` that should be a `str`. - * Check that it has a required attribute `price` that has to be a `float`. - * Check that it has an optional attribute `is_offer`, that should be a `bool`, if present. - * All this would also work for deeply nested JSON objects. -* Convert from and to JSON automatically. -* Document everything with OpenAPI, that can be used by: - * Interactive documentation systems. - * Automatic client code generation systems, for many languages. -* Provide 2 interactive documentation web interfaces directly. - ---- - -We just scratched the surface, but you already get the idea of how it all works. - -Try changing the line with: - -```Python - return {"item_name": item.name, "item_id": item_id} -``` - -...from: - -```Python - ... "item_name": item.name ... -``` - -...to: - -```Python - ... "item_price": item.price ... -``` - -...and see how your editor will auto-complete the attributes and know their types: - -![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) - -For a more complete example including more features, see the Tutorial - User Guide. - -**Spoiler alert**: the tutorial - user guide includes: - -* Declaration of **parameters** from other different places as: **headers**, **cookies**, **form fields** and **files**. -* How to set **validation constraints** as `maximum_length` or `regex`. -* A very powerful and easy to use **Dependency Injection** system. -* Security and authentication, including support for **OAuth2** with **JWT tokens** and **HTTP Basic** auth. -* More advanced (but equally easy) techniques for declaring **deeply nested JSON models** (thanks to Pydantic). -* Many extra features (thanks to Starlette) as: - * **WebSockets** - * **GraphQL** - * extremely easy tests based on HTTPX and `pytest` - * **CORS** - * **Cookie Sessions** - * ...and more. - -## Performance - -Independent TechEmpower benchmarks show **FastAPI** applications running under Uvicorn as one of the fastest Python frameworks available, only below Starlette and Uvicorn themselves (used internally by FastAPI). (*) - -To understand more about it, see the section Benchmarks. - -## Optional Dependencies - -Used by Pydantic: - -* email_validator - for email validation. - -Used by Starlette: - -* httpx - Required if you want to use the `TestClient`. -* jinja2 - Required if you want to use the default template configuration. -* python-multipart - Required if you want to support form "parsing", with `request.form()`. -* itsdangerous - Required for `SessionMiddleware` support. -* pyyaml - Required for Starlette's `SchemaGenerator` support (you probably don't need it with FastAPI). -* graphene - Required for `GraphQLApp` support. -* ujson - Required if you want to use `UJSONResponse`. - -Used by FastAPI / Starlette: - -* uvicorn - for the server that loads and serves your application. -* orjson - Required if you want to use `ORJSONResponse`. - -You can install all of these with `pip install fastapi[all]`. - -## License - -This project is licensed under the terms of the MIT license. diff --git a/docs/it/mkdocs.yml b/docs/it/mkdocs.yml deleted file mode 100644 index de18856f4..000000000 --- a/docs/it/mkdocs.yml +++ /dev/null @@ -1 +0,0 @@ -INHERIT: ../en/mkdocs.yml diff --git a/docs/lo/docs/index.md b/docs/lo/docs/index.md deleted file mode 100644 index 9a81f14d1..000000000 --- a/docs/lo/docs/index.md +++ /dev/null @@ -1,469 +0,0 @@ -

- FastAPI -

-

- FastAPI framework, high performance, easy to learn, fast to code, ready for production -

-

- - Test - - - Coverage - - - Package version - - - Supported Python versions - -

- ---- - -**Documentation**: https://fastapi.tiangolo.com - -**Source Code**: https://github.com/tiangolo/fastapi - ---- - -FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.7+ based on standard Python type hints. - -The key features are: - -* **Fast**: Very high performance, on par with **NodeJS** and **Go** (thanks to Starlette and Pydantic). [One of the fastest Python frameworks available](#performance). -* **Fast to code**: Increase the speed to develop features by about 200% to 300%. * -* **Fewer bugs**: Reduce about 40% of human (developer) induced errors. * -* **Intuitive**: Great editor support. Completion everywhere. Less time debugging. -* **Easy**: Designed to be easy to use and learn. Less time reading docs. -* **Short**: Minimize code duplication. Multiple features from each parameter declaration. Fewer bugs. -* **Robust**: Get production-ready code. With automatic interactive documentation. -* **Standards-based**: Based on (and fully compatible with) the open standards for APIs: OpenAPI (previously known as Swagger) and JSON Schema. - -* estimation based on tests on an internal development team, building production applications. - -## Sponsors - - - -{% if sponsors %} -{% for sponsor in sponsors.gold -%} - -{% endfor -%} -{%- for sponsor in sponsors.silver -%} - -{% endfor %} -{% endif %} - - - -Other sponsors - -## Opinions - -"_[...] I'm using **FastAPI** a ton these days. [...] I'm actually planning to use it for all of my team's **ML services at Microsoft**. Some of them are getting integrated into the core **Windows** product and some **Office** products._" - -
Kabir Khan - Microsoft (ref)
- ---- - -"_We adopted the **FastAPI** library to spawn a **REST** server that can be queried to obtain **predictions**. [for Ludwig]_" - -
Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - Uber (ref)
- ---- - -"_**Netflix** is pleased to announce the open-source release of our **crisis management** orchestration framework: **Dispatch**! [built with **FastAPI**]_" - -
Kevin Glisson, Marc Vilanova, Forest Monsen - Netflix (ref)
- ---- - -"_I’m over the moon excited about **FastAPI**. It’s so fun!_" - -
Brian Okken - Python Bytes podcast host (ref)
- ---- - -"_Honestly, what you've built looks super solid and polished. In many ways, it's what I wanted **Hug** to be - it's really inspiring to see someone build that._" - -
Timothy Crosley - Hug creator (ref)
- ---- - -"_If you're looking to learn one **modern framework** for building REST APIs, check out **FastAPI** [...] It's fast, easy to use and easy to learn [...]_" - -"_We've switched over to **FastAPI** for our **APIs** [...] I think you'll like it [...]_" - -
Ines Montani - Matthew Honnibal - Explosion AI founders - spaCy creators (ref) - (ref)
- ---- - -"_If anyone is looking to build a production Python API, I would highly recommend **FastAPI**. It is **beautifully designed**, **simple to use** and **highly scalable**, it has become a **key component** in our API first development strategy and is driving many automations and services such as our Virtual TAC Engineer._" - -
Deon Pillsbury - Cisco (ref)
- ---- - -## **Typer**, the FastAPI of CLIs - - - -If you are building a CLI app to be used in the terminal instead of a web API, check out **Typer**. - -**Typer** is FastAPI's little sibling. And it's intended to be the **FastAPI of CLIs**. ⌨️ 🚀 - -## Requirements - -Python 3.7+ - -FastAPI stands on the shoulders of giants: - -* Starlette for the web parts. -* Pydantic for the data parts. - -## Installation - -
- -```console -$ pip install fastapi - ----> 100% -``` - -
- -You will also need an ASGI server, for production such as Uvicorn or Hypercorn. - -
- -```console -$ pip install "uvicorn[standard]" - ----> 100% -``` - -
- -## Example - -### Create it - -* Create a file `main.py` with: - -```Python -from typing import Union - -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} -``` - -
-Or use async def... - -If your code uses `async` / `await`, use `async def`: - -```Python hl_lines="9 14" -from typing import Union - -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -async def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -async def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} -``` - -**Note**: - -If you don't know, check the _"In a hurry?"_ section about `async` and `await` in the docs. - -
- -### Run it - -Run the server with: - -
- -```console -$ uvicorn main:app --reload - -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -INFO: Started reloader process [28720] -INFO: Started server process [28722] -INFO: Waiting for application startup. -INFO: Application startup complete. -``` - -
- -
-About the command uvicorn main:app --reload... - -The command `uvicorn main:app` refers to: - -* `main`: the file `main.py` (the Python "module"). -* `app`: the object created inside of `main.py` with the line `app = FastAPI()`. -* `--reload`: make the server restart after code changes. Only do this for development. - -
- -### Check it - -Open your browser at http://127.0.0.1:8000/items/5?q=somequery. - -You will see the JSON response as: - -```JSON -{"item_id": 5, "q": "somequery"} -``` - -You already created an API that: - -* Receives HTTP requests in the _paths_ `/` and `/items/{item_id}`. -* Both _paths_ take `GET` operations (also known as HTTP _methods_). -* The _path_ `/items/{item_id}` has a _path parameter_ `item_id` that should be an `int`. -* The _path_ `/items/{item_id}` has an optional `str` _query parameter_ `q`. - -### Interactive API docs - -Now go to http://127.0.0.1:8000/docs. - -You will see the automatic interactive API documentation (provided by Swagger UI): - -![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) - -### Alternative API docs - -And now, go to http://127.0.0.1:8000/redoc. - -You will see the alternative automatic documentation (provided by ReDoc): - -![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) - -## Example upgrade - -Now modify the file `main.py` to receive a body from a `PUT` request. - -Declare the body using standard Python types, thanks to Pydantic. - -```Python hl_lines="4 9-12 25-27" -from typing import Union - -from fastapi import FastAPI -from pydantic import BaseModel - -app = FastAPI() - - -class Item(BaseModel): - name: str - price: float - is_offer: Union[bool, None] = None - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} - - -@app.put("/items/{item_id}") -def update_item(item_id: int, item: Item): - return {"item_name": item.name, "item_id": item_id} -``` - -The server should reload automatically (because you added `--reload` to the `uvicorn` command above). - -### Interactive API docs upgrade - -Now go to http://127.0.0.1:8000/docs. - -* The interactive API documentation will be automatically updated, including the new body: - -![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) - -* Click on the button "Try it out", it allows you to fill the parameters and directly interact with the API: - -![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) - -* Then click on the "Execute" button, the user interface will communicate with your API, send the parameters, get the results and show them on the screen: - -![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) - -### Alternative API docs upgrade - -And now, go to http://127.0.0.1:8000/redoc. - -* The alternative documentation will also reflect the new query parameter and body: - -![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) - -### Recap - -In summary, you declare **once** the types of parameters, body, etc. as function parameters. - -You do that with standard modern Python types. - -You don't have to learn a new syntax, the methods or classes of a specific library, etc. - -Just standard **Python 3.7+**. - -For example, for an `int`: - -```Python -item_id: int -``` - -or for a more complex `Item` model: - -```Python -item: Item -``` - -...and with that single declaration you get: - -* Editor support, including: - * Completion. - * Type checks. -* Validation of data: - * Automatic and clear errors when the data is invalid. - * Validation even for deeply nested JSON objects. -* Conversion of input data: coming from the network to Python data and types. Reading from: - * JSON. - * Path parameters. - * Query parameters. - * Cookies. - * Headers. - * Forms. - * Files. -* Conversion of output data: converting from Python data and types to network data (as JSON): - * Convert Python types (`str`, `int`, `float`, `bool`, `list`, etc). - * `datetime` objects. - * `UUID` objects. - * Database models. - * ...and many more. -* Automatic interactive API documentation, including 2 alternative user interfaces: - * Swagger UI. - * ReDoc. - ---- - -Coming back to the previous code example, **FastAPI** will: - -* Validate that there is an `item_id` in the path for `GET` and `PUT` requests. -* Validate that the `item_id` is of type `int` for `GET` and `PUT` requests. - * If it is not, the client will see a useful, clear error. -* Check if there is an optional query parameter named `q` (as in `http://127.0.0.1:8000/items/foo?q=somequery`) for `GET` requests. - * As the `q` parameter is declared with `= None`, it is optional. - * Without the `None` it would be required (as is the body in the case with `PUT`). -* For `PUT` requests to `/items/{item_id}`, Read the body as JSON: - * Check that it has a required attribute `name` that should be a `str`. - * Check that it has a required attribute `price` that has to be a `float`. - * Check that it has an optional attribute `is_offer`, that should be a `bool`, if present. - * All this would also work for deeply nested JSON objects. -* Convert from and to JSON automatically. -* Document everything with OpenAPI, that can be used by: - * Interactive documentation systems. - * Automatic client code generation systems, for many languages. -* Provide 2 interactive documentation web interfaces directly. - ---- - -We just scratched the surface, but you already get the idea of how it all works. - -Try changing the line with: - -```Python - return {"item_name": item.name, "item_id": item_id} -``` - -...from: - -```Python - ... "item_name": item.name ... -``` - -...to: - -```Python - ... "item_price": item.price ... -``` - -...and see how your editor will auto-complete the attributes and know their types: - -![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) - -For a more complete example including more features, see the Tutorial - User Guide. - -**Spoiler alert**: the tutorial - user guide includes: - -* Declaration of **parameters** from other different places as: **headers**, **cookies**, **form fields** and **files**. -* How to set **validation constraints** as `maximum_length` or `regex`. -* A very powerful and easy to use **Dependency Injection** system. -* Security and authentication, including support for **OAuth2** with **JWT tokens** and **HTTP Basic** auth. -* More advanced (but equally easy) techniques for declaring **deeply nested JSON models** (thanks to Pydantic). -* **GraphQL** integration with Strawberry and other libraries. -* Many extra features (thanks to Starlette) as: - * **WebSockets** - * extremely easy tests based on HTTPX and `pytest` - * **CORS** - * **Cookie Sessions** - * ...and more. - -## Performance - -Independent TechEmpower benchmarks show **FastAPI** applications running under Uvicorn as one of the fastest Python frameworks available, only below Starlette and Uvicorn themselves (used internally by FastAPI). (*) - -To understand more about it, see the section Benchmarks. - -## Optional Dependencies - -Used by Pydantic: - -* ujson - for faster JSON "parsing". -* email_validator - for email validation. - -Used by Starlette: - -* httpx - Required if you want to use the `TestClient`. -* jinja2 - Required if you want to use the default template configuration. -* python-multipart - Required if you want to support form "parsing", with `request.form()`. -* itsdangerous - Required for `SessionMiddleware` support. -* pyyaml - Required for Starlette's `SchemaGenerator` support (you probably don't need it with FastAPI). -* ujson - Required if you want to use `UJSONResponse`. - -Used by FastAPI / Starlette: - -* uvicorn - for the server that loads and serves your application. -* orjson - Required if you want to use `ORJSONResponse`. - -You can install all of these with `pip install "fastapi[all]"`. - -## License - -This project is licensed under the terms of the MIT license. diff --git a/docs/lo/mkdocs.yml b/docs/lo/mkdocs.yml deleted file mode 100644 index de18856f4..000000000 --- a/docs/lo/mkdocs.yml +++ /dev/null @@ -1 +0,0 @@ -INHERIT: ../en/mkdocs.yml diff --git a/docs/nl/docs/index.md b/docs/nl/docs/index.md deleted file mode 100644 index 47d62f8c4..000000000 --- a/docs/nl/docs/index.md +++ /dev/null @@ -1,467 +0,0 @@ - -{!../../../docs/missing-translation.md!} - - -

- FastAPI -

-

- FastAPI framework, high performance, easy to learn, fast to code, ready for production -

-

- - Test - - - Coverage - - - Package version - - - Supported Python versions - -

- ---- - -**Documentation**: https://fastapi.tiangolo.com - -**Source Code**: https://github.com/tiangolo/fastapi - ---- - -FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.6+ based on standard Python type hints. - -The key features are: - -* **Fast**: Very high performance, on par with **NodeJS** and **Go** (thanks to Starlette and Pydantic). [One of the fastest Python frameworks available](#performance). - -* **Fast to code**: Increase the speed to develop features by about 200% to 300%. * -* **Fewer bugs**: Reduce about 40% of human (developer) induced errors. * -* **Intuitive**: Great editor support. Completion everywhere. Less time debugging. -* **Easy**: Designed to be easy to use and learn. Less time reading docs. -* **Short**: Minimize code duplication. Multiple features from each parameter declaration. Fewer bugs. -* **Robust**: Get production-ready code. With automatic interactive documentation. -* **Standards-based**: Based on (and fully compatible with) the open standards for APIs: OpenAPI (previously known as Swagger) and JSON Schema. - -* estimation based on tests on an internal development team, building production applications. - -## Sponsors - - - -{% if sponsors %} -{% for sponsor in sponsors.gold -%} - -{% endfor -%} -{%- for sponsor in sponsors.silver -%} - -{% endfor %} -{% endif %} - - - -Other sponsors - -## Opinions - -"_[...] I'm using **FastAPI** a ton these days. [...] I'm actually planning to use it for all of my team's **ML services at Microsoft**. Some of them are getting integrated into the core **Windows** product and some **Office** products._" - -
Kabir Khan - Microsoft (ref)
- ---- - -"_We adopted the **FastAPI** library to spawn a **REST** server that can be queried to obtain **predictions**. [for Ludwig]_" - -
Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - Uber (ref)
- ---- - -"_**Netflix** is pleased to announce the open-source release of our **crisis management** orchestration framework: **Dispatch**! [built with **FastAPI**]_" - -
Kevin Glisson, Marc Vilanova, Forest Monsen - Netflix (ref)
- ---- - -"_I’m over the moon excited about **FastAPI**. It’s so fun!_" - -
Brian Okken - Python Bytes podcast host (ref)
- ---- - -"_Honestly, what you've built looks super solid and polished. In many ways, it's what I wanted **Hug** to be - it's really inspiring to see someone build that._" - -
Timothy Crosley - Hug creator (ref)
- ---- - -"_If you're looking to learn one **modern framework** for building REST APIs, check out **FastAPI** [...] It's fast, easy to use and easy to learn [...]_" - -"_We've switched over to **FastAPI** for our **APIs** [...] I think you'll like it [...]_" - -
Ines Montani - Matthew Honnibal - Explosion AI founders - spaCy creators (ref) - (ref)
- ---- - -## **Typer**, the FastAPI of CLIs - - - -If you are building a CLI app to be used in the terminal instead of a web API, check out **Typer**. - -**Typer** is FastAPI's little sibling. And it's intended to be the **FastAPI of CLIs**. ⌨️ 🚀 - -## Requirements - -Python 3.7+ - -FastAPI stands on the shoulders of giants: - -* Starlette for the web parts. -* Pydantic for the data parts. - -## Installation - -
- -```console -$ pip install fastapi - ----> 100% -``` - -
- -You will also need an ASGI server, for production such as Uvicorn or Hypercorn. - -
- -```console -$ pip install "uvicorn[standard]" - ----> 100% -``` - -
- -## Example - -### Create it - -* Create a file `main.py` with: - -```Python -from typing import Union - -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} -``` - -
-Or use async def... - -If your code uses `async` / `await`, use `async def`: - -```Python hl_lines="9 14" -from typing import Union - -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -async def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -async def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} -``` - -**Note**: - -If you don't know, check the _"In a hurry?"_ section about `async` and `await` in the docs. - -
- -### Run it - -Run the server with: - -
- -```console -$ uvicorn main:app --reload - -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -INFO: Started reloader process [28720] -INFO: Started server process [28722] -INFO: Waiting for application startup. -INFO: Application startup complete. -``` - -
- -
-About the command uvicorn main:app --reload... - -The command `uvicorn main:app` refers to: - -* `main`: the file `main.py` (the Python "module"). -* `app`: the object created inside of `main.py` with the line `app = FastAPI()`. -* `--reload`: make the server restart after code changes. Only do this for development. - -
- -### Check it - -Open your browser at http://127.0.0.1:8000/items/5?q=somequery. - -You will see the JSON response as: - -```JSON -{"item_id": 5, "q": "somequery"} -``` - -You already created an API that: - -* Receives HTTP requests in the _paths_ `/` and `/items/{item_id}`. -* Both _paths_ take `GET` operations (also known as HTTP _methods_). -* The _path_ `/items/{item_id}` has a _path parameter_ `item_id` that should be an `int`. -* The _path_ `/items/{item_id}` has an optional `str` _query parameter_ `q`. - -### Interactive API docs - -Now go to http://127.0.0.1:8000/docs. - -You will see the automatic interactive API documentation (provided by Swagger UI): - -![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) - -### Alternative API docs - -And now, go to http://127.0.0.1:8000/redoc. - -You will see the alternative automatic documentation (provided by ReDoc): - -![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) - -## Example upgrade - -Now modify the file `main.py` to receive a body from a `PUT` request. - -Declare the body using standard Python types, thanks to Pydantic. - -```Python hl_lines="4 9-12 25-27" -from typing import Union - -from fastapi import FastAPI -from pydantic import BaseModel - -app = FastAPI() - - -class Item(BaseModel): - name: str - price: float - is_offer: Union[bool, None] = None - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} - - -@app.put("/items/{item_id}") -def update_item(item_id: int, item: Item): - return {"item_name": item.name, "item_id": item_id} -``` - -The server should reload automatically (because you added `--reload` to the `uvicorn` command above). - -### Interactive API docs upgrade - -Now go to http://127.0.0.1:8000/docs. - -* The interactive API documentation will be automatically updated, including the new body: - -![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) - -* Click on the button "Try it out", it allows you to fill the parameters and directly interact with the API: - -![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) - -* Then click on the "Execute" button, the user interface will communicate with your API, send the parameters, get the results and show them on the screen: - -![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) - -### Alternative API docs upgrade - -And now, go to http://127.0.0.1:8000/redoc. - -* The alternative documentation will also reflect the new query parameter and body: - -![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) - -### Recap - -In summary, you declare **once** the types of parameters, body, etc. as function parameters. - -You do that with standard modern Python types. - -You don't have to learn a new syntax, the methods or classes of a specific library, etc. - -Just standard **Python 3.6+**. - -For example, for an `int`: - -```Python -item_id: int -``` - -or for a more complex `Item` model: - -```Python -item: Item -``` - -...and with that single declaration you get: - -* Editor support, including: - * Completion. - * Type checks. -* Validation of data: - * Automatic and clear errors when the data is invalid. - * Validation even for deeply nested JSON objects. -* Conversion of input data: coming from the network to Python data and types. Reading from: - * JSON. - * Path parameters. - * Query parameters. - * Cookies. - * Headers. - * Forms. - * Files. -* Conversion of output data: converting from Python data and types to network data (as JSON): - * Convert Python types (`str`, `int`, `float`, `bool`, `list`, etc). - * `datetime` objects. - * `UUID` objects. - * Database models. - * ...and many more. -* Automatic interactive API documentation, including 2 alternative user interfaces: - * Swagger UI. - * ReDoc. - ---- - -Coming back to the previous code example, **FastAPI** will: - -* Validate that there is an `item_id` in the path for `GET` and `PUT` requests. -* Validate that the `item_id` is of type `int` for `GET` and `PUT` requests. - * If it is not, the client will see a useful, clear error. -* Check if there is an optional query parameter named `q` (as in `http://127.0.0.1:8000/items/foo?q=somequery`) for `GET` requests. - * As the `q` parameter is declared with `= None`, it is optional. - * Without the `None` it would be required (as is the body in the case with `PUT`). -* For `PUT` requests to `/items/{item_id}`, Read the body as JSON: - * Check that it has a required attribute `name` that should be a `str`. - * Check that it has a required attribute `price` that has to be a `float`. - * Check that it has an optional attribute `is_offer`, that should be a `bool`, if present. - * All this would also work for deeply nested JSON objects. -* Convert from and to JSON automatically. -* Document everything with OpenAPI, that can be used by: - * Interactive documentation systems. - * Automatic client code generation systems, for many languages. -* Provide 2 interactive documentation web interfaces directly. - ---- - -We just scratched the surface, but you already get the idea of how it all works. - -Try changing the line with: - -```Python - return {"item_name": item.name, "item_id": item_id} -``` - -...from: - -```Python - ... "item_name": item.name ... -``` - -...to: - -```Python - ... "item_price": item.price ... -``` - -...and see how your editor will auto-complete the attributes and know their types: - -![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) - -For a more complete example including more features, see the Tutorial - User Guide. - -**Spoiler alert**: the tutorial - user guide includes: - -* Declaration of **parameters** from other different places as: **headers**, **cookies**, **form fields** and **files**. -* How to set **validation constraints** as `maximum_length` or `regex`. -* A very powerful and easy to use **Dependency Injection** system. -* Security and authentication, including support for **OAuth2** with **JWT tokens** and **HTTP Basic** auth. -* More advanced (but equally easy) techniques for declaring **deeply nested JSON models** (thanks to Pydantic). -* **GraphQL** integration with Strawberry and other libraries. -* Many extra features (thanks to Starlette) as: - * **WebSockets** - * extremely easy tests based on HTTPX and `pytest` - * **CORS** - * **Cookie Sessions** - * ...and more. - -## Performance - -Independent TechEmpower benchmarks show **FastAPI** applications running under Uvicorn as one of the fastest Python frameworks available, only below Starlette and Uvicorn themselves (used internally by FastAPI). (*) - -To understand more about it, see the section Benchmarks. - -## Optional Dependencies - -Used by Pydantic: - -* email_validator - for email validation. - -Used by Starlette: - -* httpx - Required if you want to use the `TestClient`. -* jinja2 - Required if you want to use the default template configuration. -* python-multipart - Required if you want to support form "parsing", with `request.form()`. -* itsdangerous - Required for `SessionMiddleware` support. -* pyyaml - Required for Starlette's `SchemaGenerator` support (you probably don't need it with FastAPI). -* ujson - Required if you want to use `UJSONResponse`. - -Used by FastAPI / Starlette: - -* uvicorn - for the server that loads and serves your application. -* orjson - Required if you want to use `ORJSONResponse`. - -You can install all of these with `pip install "fastapi[all]"`. - -## License - -This project is licensed under the terms of the MIT license. diff --git a/docs/nl/mkdocs.yml b/docs/nl/mkdocs.yml deleted file mode 100644 index de18856f4..000000000 --- a/docs/nl/mkdocs.yml +++ /dev/null @@ -1 +0,0 @@ -INHERIT: ../en/mkdocs.yml diff --git a/docs/sq/docs/index.md b/docs/sq/docs/index.md deleted file mode 100644 index a83b7b519..000000000 --- a/docs/sq/docs/index.md +++ /dev/null @@ -1,465 +0,0 @@ - -{!../../../docs/missing-translation.md!} - - -

- FastAPI -

-

- FastAPI framework, high performance, easy to learn, fast to code, ready for production -

-

- - Test - - - Coverage - - - Package version - -

- ---- - -**Documentation**: https://fastapi.tiangolo.com - -**Source Code**: https://github.com/tiangolo/fastapi - ---- - -FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.6+ based on standard Python type hints. - -The key features are: - -* **Fast**: Very high performance, on par with **NodeJS** and **Go** (thanks to Starlette and Pydantic). [One of the fastest Python frameworks available](#performance). - -* **Fast to code**: Increase the speed to develop features by about 200% to 300%. * -* **Fewer bugs**: Reduce about 40% of human (developer) induced errors. * -* **Intuitive**: Great editor support. Completion everywhere. Less time debugging. -* **Easy**: Designed to be easy to use and learn. Less time reading docs. -* **Short**: Minimize code duplication. Multiple features from each parameter declaration. Fewer bugs. -* **Robust**: Get production-ready code. With automatic interactive documentation. -* **Standards-based**: Based on (and fully compatible with) the open standards for APIs: OpenAPI (previously known as Swagger) and JSON Schema. - -* estimation based on tests on an internal development team, building production applications. - -## Sponsors - - - -{% if sponsors %} -{% for sponsor in sponsors.gold -%} - -{% endfor -%} -{%- for sponsor in sponsors.silver -%} - -{% endfor %} -{% endif %} - - - -Other sponsors - -## Opinions - -"_[...] I'm using **FastAPI** a ton these days. [...] I'm actually planning to use it for all of my team's **ML services at Microsoft**. Some of them are getting integrated into the core **Windows** product and some **Office** products._" - -
Kabir Khan - Microsoft (ref)
- ---- - -"_We adopted the **FastAPI** library to spawn a **REST** server that can be queried to obtain **predictions**. [for Ludwig]_" - -
Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - Uber (ref)
- ---- - -"_**Netflix** is pleased to announce the open-source release of our **crisis management** orchestration framework: **Dispatch**! [built with **FastAPI**]_" - -
Kevin Glisson, Marc Vilanova, Forest Monsen - Netflix (ref)
- ---- - -"_I’m over the moon excited about **FastAPI**. It’s so fun!_" - -
Brian Okken - Python Bytes podcast host (ref)
- ---- - -"_Honestly, what you've built looks super solid and polished. In many ways, it's what I wanted **Hug** to be - it's really inspiring to see someone build that._" - -
Timothy Crosley - Hug creator (ref)
- ---- - -"_If you're looking to learn one **modern framework** for building REST APIs, check out **FastAPI** [...] It's fast, easy to use and easy to learn [...]_" - -"_We've switched over to **FastAPI** for our **APIs** [...] I think you'll like it [...]_" - -
Ines Montani - Matthew Honnibal - Explosion AI founders - spaCy creators (ref) - (ref)
- ---- - -## **Typer**, the FastAPI of CLIs - - - -If you are building a CLI app to be used in the terminal instead of a web API, check out **Typer**. - -**Typer** is FastAPI's little sibling. And it's intended to be the **FastAPI of CLIs**. ⌨️ 🚀 - -## Requirements - -Python 3.7+ - -FastAPI stands on the shoulders of giants: - -* Starlette for the web parts. -* Pydantic for the data parts. - -## Installation - -
- -```console -$ pip install fastapi - ----> 100% -``` - -
- -You will also need an ASGI server, for production such as Uvicorn or Hypercorn. - -
- -```console -$ pip install "uvicorn[standard]" - ----> 100% -``` - -
- -## Example - -### Create it - -* Create a file `main.py` with: - -```Python -from typing import Union - -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} -``` - -
-Or use async def... - -If your code uses `async` / `await`, use `async def`: - -```Python hl_lines="9 14" -from typing import Union - -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -async def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -async def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} -``` - -**Note**: - -If you don't know, check the _"In a hurry?"_ section about `async` and `await` in the docs. - -
- -### Run it - -Run the server with: - -
- -```console -$ uvicorn main:app --reload - -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -INFO: Started reloader process [28720] -INFO: Started server process [28722] -INFO: Waiting for application startup. -INFO: Application startup complete. -``` - -
- -
-About the command uvicorn main:app --reload... - -The command `uvicorn main:app` refers to: - -* `main`: the file `main.py` (the Python "module"). -* `app`: the object created inside of `main.py` with the line `app = FastAPI()`. -* `--reload`: make the server restart after code changes. Only do this for development. - -
- -### Check it - -Open your browser at http://127.0.0.1:8000/items/5?q=somequery. - -You will see the JSON response as: - -```JSON -{"item_id": 5, "q": "somequery"} -``` - -You already created an API that: - -* Receives HTTP requests in the _paths_ `/` and `/items/{item_id}`. -* Both _paths_ take `GET` operations (also known as HTTP _methods_). -* The _path_ `/items/{item_id}` has a _path parameter_ `item_id` that should be an `int`. -* The _path_ `/items/{item_id}` has an optional `str` _query parameter_ `q`. - -### Interactive API docs - -Now go to http://127.0.0.1:8000/docs. - -You will see the automatic interactive API documentation (provided by Swagger UI): - -![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) - -### Alternative API docs - -And now, go to http://127.0.0.1:8000/redoc. - -You will see the alternative automatic documentation (provided by ReDoc): - -![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) - -## Example upgrade - -Now modify the file `main.py` to receive a body from a `PUT` request. - -Declare the body using standard Python types, thanks to Pydantic. - -```Python hl_lines="4 9-12 25-27" -from typing import Union - -from fastapi import FastAPI -from pydantic import BaseModel - -app = FastAPI() - - -class Item(BaseModel): - name: str - price: float - is_offer: Union[bool, None] = None - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} - - -@app.put("/items/{item_id}") -def update_item(item_id: int, item: Item): - return {"item_name": item.name, "item_id": item_id} -``` - -The server should reload automatically (because you added `--reload` to the `uvicorn` command above). - -### Interactive API docs upgrade - -Now go to http://127.0.0.1:8000/docs. - -* The interactive API documentation will be automatically updated, including the new body: - -![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) - -* Click on the button "Try it out", it allows you to fill the parameters and directly interact with the API: - -![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) - -* Then click on the "Execute" button, the user interface will communicate with your API, send the parameters, get the results and show them on the screen: - -![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) - -### Alternative API docs upgrade - -And now, go to http://127.0.0.1:8000/redoc. - -* The alternative documentation will also reflect the new query parameter and body: - -![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) - -### Recap - -In summary, you declare **once** the types of parameters, body, etc. as function parameters. - -You do that with standard modern Python types. - -You don't have to learn a new syntax, the methods or classes of a specific library, etc. - -Just standard **Python 3.6+**. - -For example, for an `int`: - -```Python -item_id: int -``` - -or for a more complex `Item` model: - -```Python -item: Item -``` - -...and with that single declaration you get: - -* Editor support, including: - * Completion. - * Type checks. -* Validation of data: - * Automatic and clear errors when the data is invalid. - * Validation even for deeply nested JSON objects. -* Conversion of input data: coming from the network to Python data and types. Reading from: - * JSON. - * Path parameters. - * Query parameters. - * Cookies. - * Headers. - * Forms. - * Files. -* Conversion of output data: converting from Python data and types to network data (as JSON): - * Convert Python types (`str`, `int`, `float`, `bool`, `list`, etc). - * `datetime` objects. - * `UUID` objects. - * Database models. - * ...and many more. -* Automatic interactive API documentation, including 2 alternative user interfaces: - * Swagger UI. - * ReDoc. - ---- - -Coming back to the previous code example, **FastAPI** will: - -* Validate that there is an `item_id` in the path for `GET` and `PUT` requests. -* Validate that the `item_id` is of type `int` for `GET` and `PUT` requests. - * If it is not, the client will see a useful, clear error. -* Check if there is an optional query parameter named `q` (as in `http://127.0.0.1:8000/items/foo?q=somequery`) for `GET` requests. - * As the `q` parameter is declared with `= None`, it is optional. - * Without the `None` it would be required (as is the body in the case with `PUT`). -* For `PUT` requests to `/items/{item_id}`, Read the body as JSON: - * Check that it has a required attribute `name` that should be a `str`. - * Check that it has a required attribute `price` that has to be a `float`. - * Check that it has an optional attribute `is_offer`, that should be a `bool`, if present. - * All this would also work for deeply nested JSON objects. -* Convert from and to JSON automatically. -* Document everything with OpenAPI, that can be used by: - * Interactive documentation systems. - * Automatic client code generation systems, for many languages. -* Provide 2 interactive documentation web interfaces directly. - ---- - -We just scratched the surface, but you already get the idea of how it all works. - -Try changing the line with: - -```Python - return {"item_name": item.name, "item_id": item_id} -``` - -...from: - -```Python - ... "item_name": item.name ... -``` - -...to: - -```Python - ... "item_price": item.price ... -``` - -...and see how your editor will auto-complete the attributes and know their types: - -![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) - -For a more complete example including more features, see the Tutorial - User Guide. - -**Spoiler alert**: the tutorial - user guide includes: - -* Declaration of **parameters** from other different places as: **headers**, **cookies**, **form fields** and **files**. -* How to set **validation constraints** as `maximum_length` or `regex`. -* A very powerful and easy to use **Dependency Injection** system. -* Security and authentication, including support for **OAuth2** with **JWT tokens** and **HTTP Basic** auth. -* More advanced (but equally easy) techniques for declaring **deeply nested JSON models** (thanks to Pydantic). -* Many extra features (thanks to Starlette) as: - * **WebSockets** - * **GraphQL** - * extremely easy tests based on HTTPX and `pytest` - * **CORS** - * **Cookie Sessions** - * ...and more. - -## Performance - -Independent TechEmpower benchmarks show **FastAPI** applications running under Uvicorn as one of the fastest Python frameworks available, only below Starlette and Uvicorn themselves (used internally by FastAPI). (*) - -To understand more about it, see the section Benchmarks. - -## Optional Dependencies - -Used by Pydantic: - -* email_validator - for email validation. - -Used by Starlette: - -* httpx - Required if you want to use the `TestClient`. -* jinja2 - Required if you want to use the default template configuration. -* python-multipart - Required if you want to support form "parsing", with `request.form()`. -* itsdangerous - Required for `SessionMiddleware` support. -* pyyaml - Required for Starlette's `SchemaGenerator` support (you probably don't need it with FastAPI). -* graphene - Required for `GraphQLApp` support. -* ujson - Required if you want to use `UJSONResponse`. - -Used by FastAPI / Starlette: - -* uvicorn - for the server that loads and serves your application. -* orjson - Required if you want to use `ORJSONResponse`. - -You can install all of these with `pip install fastapi[all]`. - -## License - -This project is licensed under the terms of the MIT license. diff --git a/docs/sq/mkdocs.yml b/docs/sq/mkdocs.yml deleted file mode 100644 index de18856f4..000000000 --- a/docs/sq/mkdocs.yml +++ /dev/null @@ -1 +0,0 @@ -INHERIT: ../en/mkdocs.yml diff --git a/docs/sv/docs/index.md b/docs/sv/docs/index.md deleted file mode 100644 index 47d62f8c4..000000000 --- a/docs/sv/docs/index.md +++ /dev/null @@ -1,467 +0,0 @@ - -{!../../../docs/missing-translation.md!} - - -

- FastAPI -

-

- FastAPI framework, high performance, easy to learn, fast to code, ready for production -

-

- - Test - - - Coverage - - - Package version - - - Supported Python versions - -

- ---- - -**Documentation**: https://fastapi.tiangolo.com - -**Source Code**: https://github.com/tiangolo/fastapi - ---- - -FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.6+ based on standard Python type hints. - -The key features are: - -* **Fast**: Very high performance, on par with **NodeJS** and **Go** (thanks to Starlette and Pydantic). [One of the fastest Python frameworks available](#performance). - -* **Fast to code**: Increase the speed to develop features by about 200% to 300%. * -* **Fewer bugs**: Reduce about 40% of human (developer) induced errors. * -* **Intuitive**: Great editor support. Completion everywhere. Less time debugging. -* **Easy**: Designed to be easy to use and learn. Less time reading docs. -* **Short**: Minimize code duplication. Multiple features from each parameter declaration. Fewer bugs. -* **Robust**: Get production-ready code. With automatic interactive documentation. -* **Standards-based**: Based on (and fully compatible with) the open standards for APIs: OpenAPI (previously known as Swagger) and JSON Schema. - -* estimation based on tests on an internal development team, building production applications. - -## Sponsors - - - -{% if sponsors %} -{% for sponsor in sponsors.gold -%} - -{% endfor -%} -{%- for sponsor in sponsors.silver -%} - -{% endfor %} -{% endif %} - - - -Other sponsors - -## Opinions - -"_[...] I'm using **FastAPI** a ton these days. [...] I'm actually planning to use it for all of my team's **ML services at Microsoft**. Some of them are getting integrated into the core **Windows** product and some **Office** products._" - -
Kabir Khan - Microsoft (ref)
- ---- - -"_We adopted the **FastAPI** library to spawn a **REST** server that can be queried to obtain **predictions**. [for Ludwig]_" - -
Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - Uber (ref)
- ---- - -"_**Netflix** is pleased to announce the open-source release of our **crisis management** orchestration framework: **Dispatch**! [built with **FastAPI**]_" - -
Kevin Glisson, Marc Vilanova, Forest Monsen - Netflix (ref)
- ---- - -"_I’m over the moon excited about **FastAPI**. It’s so fun!_" - -
Brian Okken - Python Bytes podcast host (ref)
- ---- - -"_Honestly, what you've built looks super solid and polished. In many ways, it's what I wanted **Hug** to be - it's really inspiring to see someone build that._" - -
Timothy Crosley - Hug creator (ref)
- ---- - -"_If you're looking to learn one **modern framework** for building REST APIs, check out **FastAPI** [...] It's fast, easy to use and easy to learn [...]_" - -"_We've switched over to **FastAPI** for our **APIs** [...] I think you'll like it [...]_" - -
Ines Montani - Matthew Honnibal - Explosion AI founders - spaCy creators (ref) - (ref)
- ---- - -## **Typer**, the FastAPI of CLIs - - - -If you are building a CLI app to be used in the terminal instead of a web API, check out **Typer**. - -**Typer** is FastAPI's little sibling. And it's intended to be the **FastAPI of CLIs**. ⌨️ 🚀 - -## Requirements - -Python 3.7+ - -FastAPI stands on the shoulders of giants: - -* Starlette for the web parts. -* Pydantic for the data parts. - -## Installation - -
- -```console -$ pip install fastapi - ----> 100% -``` - -
- -You will also need an ASGI server, for production such as Uvicorn or Hypercorn. - -
- -```console -$ pip install "uvicorn[standard]" - ----> 100% -``` - -
- -## Example - -### Create it - -* Create a file `main.py` with: - -```Python -from typing import Union - -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} -``` - -
-Or use async def... - -If your code uses `async` / `await`, use `async def`: - -```Python hl_lines="9 14" -from typing import Union - -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -async def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -async def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} -``` - -**Note**: - -If you don't know, check the _"In a hurry?"_ section about `async` and `await` in the docs. - -
- -### Run it - -Run the server with: - -
- -```console -$ uvicorn main:app --reload - -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -INFO: Started reloader process [28720] -INFO: Started server process [28722] -INFO: Waiting for application startup. -INFO: Application startup complete. -``` - -
- -
-About the command uvicorn main:app --reload... - -The command `uvicorn main:app` refers to: - -* `main`: the file `main.py` (the Python "module"). -* `app`: the object created inside of `main.py` with the line `app = FastAPI()`. -* `--reload`: make the server restart after code changes. Only do this for development. - -
- -### Check it - -Open your browser at http://127.0.0.1:8000/items/5?q=somequery. - -You will see the JSON response as: - -```JSON -{"item_id": 5, "q": "somequery"} -``` - -You already created an API that: - -* Receives HTTP requests in the _paths_ `/` and `/items/{item_id}`. -* Both _paths_ take `GET` operations (also known as HTTP _methods_). -* The _path_ `/items/{item_id}` has a _path parameter_ `item_id` that should be an `int`. -* The _path_ `/items/{item_id}` has an optional `str` _query parameter_ `q`. - -### Interactive API docs - -Now go to http://127.0.0.1:8000/docs. - -You will see the automatic interactive API documentation (provided by Swagger UI): - -![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) - -### Alternative API docs - -And now, go to http://127.0.0.1:8000/redoc. - -You will see the alternative automatic documentation (provided by ReDoc): - -![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) - -## Example upgrade - -Now modify the file `main.py` to receive a body from a `PUT` request. - -Declare the body using standard Python types, thanks to Pydantic. - -```Python hl_lines="4 9-12 25-27" -from typing import Union - -from fastapi import FastAPI -from pydantic import BaseModel - -app = FastAPI() - - -class Item(BaseModel): - name: str - price: float - is_offer: Union[bool, None] = None - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} - - -@app.put("/items/{item_id}") -def update_item(item_id: int, item: Item): - return {"item_name": item.name, "item_id": item_id} -``` - -The server should reload automatically (because you added `--reload` to the `uvicorn` command above). - -### Interactive API docs upgrade - -Now go to http://127.0.0.1:8000/docs. - -* The interactive API documentation will be automatically updated, including the new body: - -![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) - -* Click on the button "Try it out", it allows you to fill the parameters and directly interact with the API: - -![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) - -* Then click on the "Execute" button, the user interface will communicate with your API, send the parameters, get the results and show them on the screen: - -![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) - -### Alternative API docs upgrade - -And now, go to http://127.0.0.1:8000/redoc. - -* The alternative documentation will also reflect the new query parameter and body: - -![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) - -### Recap - -In summary, you declare **once** the types of parameters, body, etc. as function parameters. - -You do that with standard modern Python types. - -You don't have to learn a new syntax, the methods or classes of a specific library, etc. - -Just standard **Python 3.6+**. - -For example, for an `int`: - -```Python -item_id: int -``` - -or for a more complex `Item` model: - -```Python -item: Item -``` - -...and with that single declaration you get: - -* Editor support, including: - * Completion. - * Type checks. -* Validation of data: - * Automatic and clear errors when the data is invalid. - * Validation even for deeply nested JSON objects. -* Conversion of input data: coming from the network to Python data and types. Reading from: - * JSON. - * Path parameters. - * Query parameters. - * Cookies. - * Headers. - * Forms. - * Files. -* Conversion of output data: converting from Python data and types to network data (as JSON): - * Convert Python types (`str`, `int`, `float`, `bool`, `list`, etc). - * `datetime` objects. - * `UUID` objects. - * Database models. - * ...and many more. -* Automatic interactive API documentation, including 2 alternative user interfaces: - * Swagger UI. - * ReDoc. - ---- - -Coming back to the previous code example, **FastAPI** will: - -* Validate that there is an `item_id` in the path for `GET` and `PUT` requests. -* Validate that the `item_id` is of type `int` for `GET` and `PUT` requests. - * If it is not, the client will see a useful, clear error. -* Check if there is an optional query parameter named `q` (as in `http://127.0.0.1:8000/items/foo?q=somequery`) for `GET` requests. - * As the `q` parameter is declared with `= None`, it is optional. - * Without the `None` it would be required (as is the body in the case with `PUT`). -* For `PUT` requests to `/items/{item_id}`, Read the body as JSON: - * Check that it has a required attribute `name` that should be a `str`. - * Check that it has a required attribute `price` that has to be a `float`. - * Check that it has an optional attribute `is_offer`, that should be a `bool`, if present. - * All this would also work for deeply nested JSON objects. -* Convert from and to JSON automatically. -* Document everything with OpenAPI, that can be used by: - * Interactive documentation systems. - * Automatic client code generation systems, for many languages. -* Provide 2 interactive documentation web interfaces directly. - ---- - -We just scratched the surface, but you already get the idea of how it all works. - -Try changing the line with: - -```Python - return {"item_name": item.name, "item_id": item_id} -``` - -...from: - -```Python - ... "item_name": item.name ... -``` - -...to: - -```Python - ... "item_price": item.price ... -``` - -...and see how your editor will auto-complete the attributes and know their types: - -![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) - -For a more complete example including more features, see the Tutorial - User Guide. - -**Spoiler alert**: the tutorial - user guide includes: - -* Declaration of **parameters** from other different places as: **headers**, **cookies**, **form fields** and **files**. -* How to set **validation constraints** as `maximum_length` or `regex`. -* A very powerful and easy to use **Dependency Injection** system. -* Security and authentication, including support for **OAuth2** with **JWT tokens** and **HTTP Basic** auth. -* More advanced (but equally easy) techniques for declaring **deeply nested JSON models** (thanks to Pydantic). -* **GraphQL** integration with Strawberry and other libraries. -* Many extra features (thanks to Starlette) as: - * **WebSockets** - * extremely easy tests based on HTTPX and `pytest` - * **CORS** - * **Cookie Sessions** - * ...and more. - -## Performance - -Independent TechEmpower benchmarks show **FastAPI** applications running under Uvicorn as one of the fastest Python frameworks available, only below Starlette and Uvicorn themselves (used internally by FastAPI). (*) - -To understand more about it, see the section Benchmarks. - -## Optional Dependencies - -Used by Pydantic: - -* email_validator - for email validation. - -Used by Starlette: - -* httpx - Required if you want to use the `TestClient`. -* jinja2 - Required if you want to use the default template configuration. -* python-multipart - Required if you want to support form "parsing", with `request.form()`. -* itsdangerous - Required for `SessionMiddleware` support. -* pyyaml - Required for Starlette's `SchemaGenerator` support (you probably don't need it with FastAPI). -* ujson - Required if you want to use `UJSONResponse`. - -Used by FastAPI / Starlette: - -* uvicorn - for the server that loads and serves your application. -* orjson - Required if you want to use `ORJSONResponse`. - -You can install all of these with `pip install "fastapi[all]"`. - -## License - -This project is licensed under the terms of the MIT license. diff --git a/docs/sv/mkdocs.yml b/docs/sv/mkdocs.yml deleted file mode 100644 index de18856f4..000000000 --- a/docs/sv/mkdocs.yml +++ /dev/null @@ -1 +0,0 @@ -INHERIT: ../en/mkdocs.yml diff --git a/docs/uk/docs/index.md b/docs/uk/docs/index.md deleted file mode 100644 index a83b7b519..000000000 --- a/docs/uk/docs/index.md +++ /dev/null @@ -1,465 +0,0 @@ - -{!../../../docs/missing-translation.md!} - - -

- FastAPI -

-

- FastAPI framework, high performance, easy to learn, fast to code, ready for production -

-

- - Test - - - Coverage - - - Package version - -

- ---- - -**Documentation**: https://fastapi.tiangolo.com - -**Source Code**: https://github.com/tiangolo/fastapi - ---- - -FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.6+ based on standard Python type hints. - -The key features are: - -* **Fast**: Very high performance, on par with **NodeJS** and **Go** (thanks to Starlette and Pydantic). [One of the fastest Python frameworks available](#performance). - -* **Fast to code**: Increase the speed to develop features by about 200% to 300%. * -* **Fewer bugs**: Reduce about 40% of human (developer) induced errors. * -* **Intuitive**: Great editor support. Completion everywhere. Less time debugging. -* **Easy**: Designed to be easy to use and learn. Less time reading docs. -* **Short**: Minimize code duplication. Multiple features from each parameter declaration. Fewer bugs. -* **Robust**: Get production-ready code. With automatic interactive documentation. -* **Standards-based**: Based on (and fully compatible with) the open standards for APIs: OpenAPI (previously known as Swagger) and JSON Schema. - -* estimation based on tests on an internal development team, building production applications. - -## Sponsors - - - -{% if sponsors %} -{% for sponsor in sponsors.gold -%} - -{% endfor -%} -{%- for sponsor in sponsors.silver -%} - -{% endfor %} -{% endif %} - - - -Other sponsors - -## Opinions - -"_[...] I'm using **FastAPI** a ton these days. [...] I'm actually planning to use it for all of my team's **ML services at Microsoft**. Some of them are getting integrated into the core **Windows** product and some **Office** products._" - -
Kabir Khan - Microsoft (ref)
- ---- - -"_We adopted the **FastAPI** library to spawn a **REST** server that can be queried to obtain **predictions**. [for Ludwig]_" - -
Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - Uber (ref)
- ---- - -"_**Netflix** is pleased to announce the open-source release of our **crisis management** orchestration framework: **Dispatch**! [built with **FastAPI**]_" - -
Kevin Glisson, Marc Vilanova, Forest Monsen - Netflix (ref)
- ---- - -"_I’m over the moon excited about **FastAPI**. It’s so fun!_" - -
Brian Okken - Python Bytes podcast host (ref)
- ---- - -"_Honestly, what you've built looks super solid and polished. In many ways, it's what I wanted **Hug** to be - it's really inspiring to see someone build that._" - -
Timothy Crosley - Hug creator (ref)
- ---- - -"_If you're looking to learn one **modern framework** for building REST APIs, check out **FastAPI** [...] It's fast, easy to use and easy to learn [...]_" - -"_We've switched over to **FastAPI** for our **APIs** [...] I think you'll like it [...]_" - -
Ines Montani - Matthew Honnibal - Explosion AI founders - spaCy creators (ref) - (ref)
- ---- - -## **Typer**, the FastAPI of CLIs - - - -If you are building a CLI app to be used in the terminal instead of a web API, check out **Typer**. - -**Typer** is FastAPI's little sibling. And it's intended to be the **FastAPI of CLIs**. ⌨️ 🚀 - -## Requirements - -Python 3.7+ - -FastAPI stands on the shoulders of giants: - -* Starlette for the web parts. -* Pydantic for the data parts. - -## Installation - -
- -```console -$ pip install fastapi - ----> 100% -``` - -
- -You will also need an ASGI server, for production such as Uvicorn or Hypercorn. - -
- -```console -$ pip install "uvicorn[standard]" - ----> 100% -``` - -
- -## Example - -### Create it - -* Create a file `main.py` with: - -```Python -from typing import Union - -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} -``` - -
-Or use async def... - -If your code uses `async` / `await`, use `async def`: - -```Python hl_lines="9 14" -from typing import Union - -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -async def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -async def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} -``` - -**Note**: - -If you don't know, check the _"In a hurry?"_ section about `async` and `await` in the docs. - -
- -### Run it - -Run the server with: - -
- -```console -$ uvicorn main:app --reload - -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -INFO: Started reloader process [28720] -INFO: Started server process [28722] -INFO: Waiting for application startup. -INFO: Application startup complete. -``` - -
- -
-About the command uvicorn main:app --reload... - -The command `uvicorn main:app` refers to: - -* `main`: the file `main.py` (the Python "module"). -* `app`: the object created inside of `main.py` with the line `app = FastAPI()`. -* `--reload`: make the server restart after code changes. Only do this for development. - -
- -### Check it - -Open your browser at http://127.0.0.1:8000/items/5?q=somequery. - -You will see the JSON response as: - -```JSON -{"item_id": 5, "q": "somequery"} -``` - -You already created an API that: - -* Receives HTTP requests in the _paths_ `/` and `/items/{item_id}`. -* Both _paths_ take `GET` operations (also known as HTTP _methods_). -* The _path_ `/items/{item_id}` has a _path parameter_ `item_id` that should be an `int`. -* The _path_ `/items/{item_id}` has an optional `str` _query parameter_ `q`. - -### Interactive API docs - -Now go to http://127.0.0.1:8000/docs. - -You will see the automatic interactive API documentation (provided by Swagger UI): - -![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) - -### Alternative API docs - -And now, go to http://127.0.0.1:8000/redoc. - -You will see the alternative automatic documentation (provided by ReDoc): - -![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) - -## Example upgrade - -Now modify the file `main.py` to receive a body from a `PUT` request. - -Declare the body using standard Python types, thanks to Pydantic. - -```Python hl_lines="4 9-12 25-27" -from typing import Union - -from fastapi import FastAPI -from pydantic import BaseModel - -app = FastAPI() - - -class Item(BaseModel): - name: str - price: float - is_offer: Union[bool, None] = None - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} - - -@app.put("/items/{item_id}") -def update_item(item_id: int, item: Item): - return {"item_name": item.name, "item_id": item_id} -``` - -The server should reload automatically (because you added `--reload` to the `uvicorn` command above). - -### Interactive API docs upgrade - -Now go to http://127.0.0.1:8000/docs. - -* The interactive API documentation will be automatically updated, including the new body: - -![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) - -* Click on the button "Try it out", it allows you to fill the parameters and directly interact with the API: - -![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) - -* Then click on the "Execute" button, the user interface will communicate with your API, send the parameters, get the results and show them on the screen: - -![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) - -### Alternative API docs upgrade - -And now, go to http://127.0.0.1:8000/redoc. - -* The alternative documentation will also reflect the new query parameter and body: - -![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) - -### Recap - -In summary, you declare **once** the types of parameters, body, etc. as function parameters. - -You do that with standard modern Python types. - -You don't have to learn a new syntax, the methods or classes of a specific library, etc. - -Just standard **Python 3.6+**. - -For example, for an `int`: - -```Python -item_id: int -``` - -or for a more complex `Item` model: - -```Python -item: Item -``` - -...and with that single declaration you get: - -* Editor support, including: - * Completion. - * Type checks. -* Validation of data: - * Automatic and clear errors when the data is invalid. - * Validation even for deeply nested JSON objects. -* Conversion of input data: coming from the network to Python data and types. Reading from: - * JSON. - * Path parameters. - * Query parameters. - * Cookies. - * Headers. - * Forms. - * Files. -* Conversion of output data: converting from Python data and types to network data (as JSON): - * Convert Python types (`str`, `int`, `float`, `bool`, `list`, etc). - * `datetime` objects. - * `UUID` objects. - * Database models. - * ...and many more. -* Automatic interactive API documentation, including 2 alternative user interfaces: - * Swagger UI. - * ReDoc. - ---- - -Coming back to the previous code example, **FastAPI** will: - -* Validate that there is an `item_id` in the path for `GET` and `PUT` requests. -* Validate that the `item_id` is of type `int` for `GET` and `PUT` requests. - * If it is not, the client will see a useful, clear error. -* Check if there is an optional query parameter named `q` (as in `http://127.0.0.1:8000/items/foo?q=somequery`) for `GET` requests. - * As the `q` parameter is declared with `= None`, it is optional. - * Without the `None` it would be required (as is the body in the case with `PUT`). -* For `PUT` requests to `/items/{item_id}`, Read the body as JSON: - * Check that it has a required attribute `name` that should be a `str`. - * Check that it has a required attribute `price` that has to be a `float`. - * Check that it has an optional attribute `is_offer`, that should be a `bool`, if present. - * All this would also work for deeply nested JSON objects. -* Convert from and to JSON automatically. -* Document everything with OpenAPI, that can be used by: - * Interactive documentation systems. - * Automatic client code generation systems, for many languages. -* Provide 2 interactive documentation web interfaces directly. - ---- - -We just scratched the surface, but you already get the idea of how it all works. - -Try changing the line with: - -```Python - return {"item_name": item.name, "item_id": item_id} -``` - -...from: - -```Python - ... "item_name": item.name ... -``` - -...to: - -```Python - ... "item_price": item.price ... -``` - -...and see how your editor will auto-complete the attributes and know their types: - -![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) - -For a more complete example including more features, see the Tutorial - User Guide. - -**Spoiler alert**: the tutorial - user guide includes: - -* Declaration of **parameters** from other different places as: **headers**, **cookies**, **form fields** and **files**. -* How to set **validation constraints** as `maximum_length` or `regex`. -* A very powerful and easy to use **Dependency Injection** system. -* Security and authentication, including support for **OAuth2** with **JWT tokens** and **HTTP Basic** auth. -* More advanced (but equally easy) techniques for declaring **deeply nested JSON models** (thanks to Pydantic). -* Many extra features (thanks to Starlette) as: - * **WebSockets** - * **GraphQL** - * extremely easy tests based on HTTPX and `pytest` - * **CORS** - * **Cookie Sessions** - * ...and more. - -## Performance - -Independent TechEmpower benchmarks show **FastAPI** applications running under Uvicorn as one of the fastest Python frameworks available, only below Starlette and Uvicorn themselves (used internally by FastAPI). (*) - -To understand more about it, see the section Benchmarks. - -## Optional Dependencies - -Used by Pydantic: - -* email_validator - for email validation. - -Used by Starlette: - -* httpx - Required if you want to use the `TestClient`. -* jinja2 - Required if you want to use the default template configuration. -* python-multipart - Required if you want to support form "parsing", with `request.form()`. -* itsdangerous - Required for `SessionMiddleware` support. -* pyyaml - Required for Starlette's `SchemaGenerator` support (you probably don't need it with FastAPI). -* graphene - Required for `GraphQLApp` support. -* ujson - Required if you want to use `UJSONResponse`. - -Used by FastAPI / Starlette: - -* uvicorn - for the server that loads and serves your application. -* orjson - Required if you want to use `ORJSONResponse`. - -You can install all of these with `pip install fastapi[all]`. - -## License - -This project is licensed under the terms of the MIT license. diff --git a/docs/uk/mkdocs.yml b/docs/uk/mkdocs.yml deleted file mode 100644 index de18856f4..000000000 --- a/docs/uk/mkdocs.yml +++ /dev/null @@ -1 +0,0 @@ -INHERIT: ../en/mkdocs.yml From afc237ad530297041f21d8ecdbe66ee1b7d52802 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 25 Jun 2023 12:57:53 +0000 Subject: [PATCH 14/58] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index b8c9a1106..900822809 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔥 Remove languages without translations. PR [#9743](https://github.com/tiangolo/fastapi/pull/9743) by [@tiangolo](https://github.com/tiangolo). * ✨ Refactor docs for building scripts, use MkDocs hooks, simplify (remove) configs for languages. PR [#9742](https://github.com/tiangolo/fastapi/pull/9742) by [@tiangolo](https://github.com/tiangolo). * 🔨 Add MkDocs hook that renames sections based on the first index file. PR [#9737](https://github.com/tiangolo/fastapi/pull/9737) by [@tiangolo](https://github.com/tiangolo). * 👷 Make cron jobs run only on main repo, not on forks, to avoid error notifications from missing tokens. PR [#9735](https://github.com/tiangolo/fastapi/pull/9735) by [@tiangolo](https://github.com/tiangolo). From ed297bb2e044a0cc5cb013673447778057b731d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 26 Jun 2023 16:05:43 +0200 Subject: [PATCH 15/58] =?UTF-8?q?=E2=9C=A8=20Add=20Material=20for=20MkDocs?= =?UTF-8?q?=20Insiders=20features=20and=20cards=20(#9748)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ➕ Add dependencies for MkDocs Insiders * 🙈 Add Insider's .cache to .gitignore * 🔧 Update MkDocs configs for Insiders * 💄 Add custom Insiders card layout, while the custom logo is provided from upstream * 🔨 Update docs.py script to dynamically enable insiders if it's installed * 👷 Add cache for MkDocs Material Insiders' cards * 🔊 Add a small log to the docs CLI * 🔊 Tweak logs, only after exporting languages * 🐛 Fix accessing non existing env var * 🔧 Invalidate deps cache * 🔧 Tweak cache IDs * 👷 Update cache for installing insiders * 🔊 Log insiders * 💚 Invalidate cache * 👷 Tweak cache keys * 👷 Trigger CI and test cache * 🔥 Remove cache comment * ⚡️ Optimize cache usage for first runs of docs * 👷 Tweak cache for MkDocs Material cards * 💚 Trigger CI to test cache --- .github/workflows/build-docs.yml | 12 +- .gitignore | 1 + docs/en/layouts/custom.yml | 228 ++++++++++++++++++++++++++++++ docs/en/mkdocs.insiders.yml | 7 + docs/en/mkdocs.maybe-insiders.yml | 3 + docs/en/mkdocs.no-insiders.yml | 0 docs/en/mkdocs.yml | 10 +- requirements-docs.txt | 6 + scripts/docs.py | 20 +++ 9 files changed, 283 insertions(+), 4 deletions(-) create mode 100644 docs/en/layouts/custom.yml create mode 100644 docs/en/mkdocs.insiders.yml create mode 100644 docs/en/mkdocs.maybe-insiders.yml create mode 100644 docs/en/mkdocs.no-insiders.yml diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index c2880ef71..a155ecfec 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -44,10 +44,14 @@ jobs: id: cache with: path: ${{ env.pythonLocation }} - key: ${{ runner.os }}-python-docs-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml', 'requirements-docs.txt') }}-v03 + key: ${{ runner.os }}-python-docs-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml', 'requirements-docs.txt') }}-v05 - name: Install docs extras if: steps.cache.outputs.cache-hit != 'true' run: pip install -r requirements-docs.txt + # Install MkDocs Material Insiders here just to put it in the cache for the rest of the steps + - 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: Export Language Codes id: show-langs run: | @@ -76,7 +80,7 @@ jobs: id: cache with: path: ${{ env.pythonLocation }} - key: ${{ runner.os }}-python-docs-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml', 'requirements-docs.txt') }}-v03 + key: ${{ runner.os }}-python-docs-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml', 'requirements-docs.txt') }}-v05 - name: Install docs extras if: steps.cache.outputs.cache-hit != 'true' run: pip install -r requirements-docs.txt @@ -85,6 +89,10 @@ jobs: 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 + - uses: actions/cache@v3 + with: + key: mkdocs-cards-${{ matrix.lang }}-${{ github.ref }} + path: docs/${{ matrix.lang }}/.cache - name: Build Docs run: python ./scripts/docs.py build-lang ${{ matrix.lang }} - uses: actions/upload-artifact@v3 diff --git a/.gitignore b/.gitignore index 3cb64c047..d380d16b7 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ archive.zip # vim temporary files *~ .*.sw? +.cache diff --git a/docs/en/layouts/custom.yml b/docs/en/layouts/custom.yml new file mode 100644 index 000000000..aad81af28 --- /dev/null +++ b/docs/en/layouts/custom.yml @@ -0,0 +1,228 @@ +# Copyright (c) 2016-2023 Martin Donath + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. + +# ----------------------------------------------------------------------------- +# Configuration +# ----------------------------------------------------------------------------- + +# The same default card with a a configurable logo + +# Definitions +definitions: + + # Background image + - &background_image >- + {{ layout.background_image or "" }} + + # Background color (default: indigo) + - &background_color >- + {%- if layout.background_color -%} + {{ layout.background_color }} + {%- else -%} + {%- set palette = config.theme.palette or {} -%} + {%- if not palette is mapping -%} + {%- set palette = palette | first -%} + {%- endif -%} + {%- set primary = palette.get("primary", "indigo") -%} + {%- set primary = primary.replace(" ", "-") -%} + {{ { + "red": "#ef5552", + "pink": "#e92063", + "purple": "#ab47bd", + "deep-purple": "#7e56c2", + "indigo": "#4051b5", + "blue": "#2094f3", + "light-blue": "#02a6f2", + "cyan": "#00bdd6", + "teal": "#009485", + "green": "#4cae4f", + "light-green": "#8bc34b", + "lime": "#cbdc38", + "yellow": "#ffec3d", + "amber": "#ffc105", + "orange": "#ffa724", + "deep-orange": "#ff6e42", + "brown": "#795649", + "grey": "#757575", + "blue-grey": "#546d78", + "black": "#000000", + "white": "#ffffff" + }[primary] or "#4051b5" }} + {%- endif -%} + + # Text color (default: white) + - &color >- + {%- if layout.color -%} + {{ layout.color }} + {%- else -%} + {%- set palette = config.theme.palette or {} -%} + {%- if not palette is mapping -%} + {%- set palette = palette | first -%} + {%- endif -%} + {%- set primary = palette.get("primary", "indigo") -%} + {%- set primary = primary.replace(" ", "-") -%} + {{ { + "red": "#ffffff", + "pink": "#ffffff", + "purple": "#ffffff", + "deep-purple": "#ffffff", + "indigo": "#ffffff", + "blue": "#ffffff", + "light-blue": "#ffffff", + "cyan": "#ffffff", + "teal": "#ffffff", + "green": "#ffffff", + "light-green": "#ffffff", + "lime": "#000000", + "yellow": "#000000", + "amber": "#000000", + "orange": "#000000", + "deep-orange": "#ffffff", + "brown": "#ffffff", + "grey": "#ffffff", + "blue-grey": "#ffffff", + "black": "#ffffff", + "white": "#000000" + }[primary] or "#ffffff" }} + {%- endif -%} + + # Font family (default: Roboto) + - &font_family >- + {%- if layout.font_family -%} + {{ layout.font_family }} + {%- elif config.theme.font != false -%} + {{ config.theme.font.get("text", "Roboto") }} + {%- else -%} + Roboto + {%- endif -%} + + # Site name + - &site_name >- + {{ config.site_name }} + + # Page title + - &page_title >- + {{ page.meta.get("title", page.title) }} + + # Page title with site name + - &page_title_with_site_name >- + {%- if not page.is_homepage -%} + {{ page.meta.get("title", page.title) }} - {{ config.site_name }} + {%- else -%} + {{ page.meta.get("title", page.title) }} + {%- endif -%} + + # Page description + - &page_description >- + {{ page.meta.get("description", config.site_description) or "" }} + + + # Start of custom modified logic + # Logo + - &logo >- + {%- if layout.logo -%} + {{ layout.logo }} + {%- elif config.theme.logo -%} + {{ config.docs_dir }}/{{ config.theme.logo }} + {%- endif -%} + # End of custom modified logic + + # Logo (icon) + - &logo_icon >- + {{ config.theme.icon.logo or "" }} + +# Meta tags +tags: + + # Open Graph + og:type: website + og:title: *page_title_with_site_name + og:description: *page_description + og:image: "{{ image.url }}" + og:image:type: "{{ image.type }}" + og:image:width: "{{ image.width }}" + og:image:height: "{{ image.height }}" + og:url: "{{ page.canonical_url }}" + + # Twitter + twitter:card: summary_large_image + twitter.title: *page_title_with_site_name + twitter:description: *page_description + twitter:image: "{{ image.url }}" + +# ----------------------------------------------------------------------------- +# Specification +# ----------------------------------------------------------------------------- + +# Card size and layers +size: { width: 1200, height: 630 } +layers: + + # Background + - background: + image: *background_image + color: *background_color + + # Logo + - size: { width: 144, height: 144 } + offset: { x: 992, y: 64 } + background: + image: *logo + icon: + value: *logo_icon + color: *color + + # Site name + - size: { width: 832, height: 42 } + offset: { x: 64, y: 64 } + typography: + content: *site_name + color: *color + font: + family: *font_family + style: Bold + + # Page title + - size: { width: 832, height: 310 } + offset: { x: 62, y: 160 } + typography: + content: *page_title + align: start + color: *color + line: + amount: 3 + height: 1.25 + font: + family: *font_family + style: Bold + + # Page description + - size: { width: 832, height: 64 } + offset: { x: 64, y: 512 } + typography: + content: *page_description + align: start + color: *color + line: + amount: 2 + height: 1.5 + font: + family: *font_family + style: Regular diff --git a/docs/en/mkdocs.insiders.yml b/docs/en/mkdocs.insiders.yml new file mode 100644 index 000000000..d204974b8 --- /dev/null +++ b/docs/en/mkdocs.insiders.yml @@ -0,0 +1,7 @@ +plugins: + social: + cards_layout_dir: ../en/layouts + cards_layout: custom + cards_layout_options: + logo: ../en/docs/img/icon-white.svg + typeset: diff --git a/docs/en/mkdocs.maybe-insiders.yml b/docs/en/mkdocs.maybe-insiders.yml new file mode 100644 index 000000000..8e6271334 --- /dev/null +++ b/docs/en/mkdocs.maybe-insiders.yml @@ -0,0 +1,3 @@ +# Define this here and not in the main mkdocs.yml file because that one is auto +# updated and written, and the script would remove the env var +INHERIT: !ENV [INSIDERS_FILE, '../en/mkdocs.no-insiders.yml'] diff --git a/docs/en/mkdocs.no-insiders.yml b/docs/en/mkdocs.no-insiders.yml new file mode 100644 index 000000000..e69de29bb diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index c39d656ff..bdadb167e 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -1,3 +1,4 @@ +INHERIT: ../en/mkdocs.maybe-insiders.yml site_name: FastAPI site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production site_url: https://fastapi.tiangolo.com/ @@ -24,6 +25,11 @@ theme: - search.highlight - content.tabs.link - navigation.indexes + - content.tooltips + - navigation.path + - content.code.annotate + - content.code.copy + - content.code.select icon: repo: fontawesome/brands/github-alt logo: img/icon-white.svg @@ -33,8 +39,8 @@ repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' plugins: -- search -- markdownextradata: + search: null + markdownextradata: data: ../en/data nav: - FastAPI: index.md diff --git a/requirements-docs.txt b/requirements-docs.txt index 211212fba..2c5f71ec0 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -6,3 +6,9 @@ mkdocs-markdownextradata-plugin >=0.1.7,<0.3.0 typer-cli >=0.0.13,<0.0.14 typer[all] >=0.6.1,<0.8.0 pyyaml >=5.3.1,<7.0.0 +# For Material for MkDocs, Chinese search +jieba==0.42.1 +# For image processing by Material for MkDocs +pillow==9.5.0 +# For image processing by Material for MkDocs +cairosvg==2.7.0 diff --git a/scripts/docs.py b/scripts/docs.py index 5615a8572..20838be6a 100644 --- a/scripts/docs.py +++ b/scripts/docs.py @@ -4,7 +4,9 @@ import os import re import shutil import subprocess +from functools import lru_cache from http.server import HTTPServer, SimpleHTTPRequestHandler +from importlib import metadata from multiprocessing import Pool from pathlib import Path from typing import Any, Dict, List, Optional, Union @@ -34,6 +36,12 @@ site_path = Path("site").absolute() build_site_path = Path("site_build").absolute() +@lru_cache() +def is_mkdocs_insiders() -> bool: + version = metadata.version("mkdocs-material") + return "insiders" in version + + def get_en_config() -> Dict[str, Any]: return mkdocs.utils.yaml_load(en_config_path.read_text(encoding="utf-8")) @@ -59,6 +67,14 @@ def complete_existing_lang(incomplete: str): yield lang_path.name +@app.callback() +def callback() -> None: + if is_mkdocs_insiders(): + os.environ["INSIDERS_FILE"] = "../en/mkdocs.insiders.yml" + # For MacOS with insiders and Cairo + os.environ["DYLD_FALLBACK_LIBRARY_PATH"] = "/opt/homebrew/lib" + + @app.command() def new_lang(lang: str = typer.Argument(..., callback=lang_callback)): """ @@ -93,6 +109,10 @@ def build_lang( """ Build the docs for a language. """ + insiders_env_file = os.environ.get("INSIDERS_FILE") + print(f"Insiders file {insiders_env_file}") + if is_mkdocs_insiders(): + print("Using insiders") lang_path: Path = Path("docs") / lang if not lang_path.is_dir(): typer.echo(f"The language translation doesn't seem to exist yet: {lang}") From d1c5c5c97c147016623edcc65e30e19769ca0ada Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 26 Jun 2023 14:06:24 +0000 Subject: [PATCH 16/58] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 900822809..f0d321e3b 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✨ Add Material for MkDocs Insiders features and cards. PR [#9748](https://github.com/tiangolo/fastapi/pull/9748) by [@tiangolo](https://github.com/tiangolo). * 🔥 Remove languages without translations. PR [#9743](https://github.com/tiangolo/fastapi/pull/9743) by [@tiangolo](https://github.com/tiangolo). * ✨ Refactor docs for building scripts, use MkDocs hooks, simplify (remove) configs for languages. PR [#9742](https://github.com/tiangolo/fastapi/pull/9742) by [@tiangolo](https://github.com/tiangolo). * 🔨 Add MkDocs hook that renames sections based on the first index file. PR [#9737](https://github.com/tiangolo/fastapi/pull/9737) by [@tiangolo](https://github.com/tiangolo). From 872af100f5e8c81b109fe29e7d70a36f520193ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 26 Jun 2023 18:02:34 +0200 Subject: [PATCH 17/58] =?UTF-8?q?=F0=9F=93=9D=20Fix=20form=20for=20the=20F?= =?UTF-8?q?astAPI=20and=20friends=20newsletter=20(#9749)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/newsletter.md | 4 ++-- docs/en/mkdocs.yml | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/en/docs/newsletter.md b/docs/en/docs/newsletter.md index 6403f31e6..782db1353 100644 --- a/docs/en/docs/newsletter.md +++ b/docs/en/docs/newsletter.md @@ -1,5 +1,5 @@ # FastAPI and friends newsletter - + - + diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index bdadb167e..21300b9dc 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -165,6 +165,7 @@ nav: - external-links.md - benchmarks.md - help-fastapi.md +- newsletter.md - contributing.md - release-notes.md markdown_extensions: From 47524eee1bd16d2680ff0a49706ad72d742e4e30 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 26 Jun 2023 16:03:19 +0000 Subject: [PATCH 18/58] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index f0d321e3b..e55bf48c7 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Fix form for the FastAPI and friends newsletter. PR [#9749](https://github.com/tiangolo/fastapi/pull/9749) by [@tiangolo](https://github.com/tiangolo). * ✨ Add Material for MkDocs Insiders features and cards. PR [#9748](https://github.com/tiangolo/fastapi/pull/9748) by [@tiangolo](https://github.com/tiangolo). * 🔥 Remove languages without translations. PR [#9743](https://github.com/tiangolo/fastapi/pull/9743) by [@tiangolo](https://github.com/tiangolo). * ✨ Refactor docs for building scripts, use MkDocs hooks, simplify (remove) configs for languages. PR [#9742](https://github.com/tiangolo/fastapi/pull/9742) by [@tiangolo](https://github.com/tiangolo). From 81772b46a8bcd1fe531508c67240489533c26417 Mon Sep 17 00:00:00 2001 From: Sergei Glazkov Date: Tue, 27 Jun 2023 04:00:19 +0300 Subject: [PATCH 19/58] =?UTF-8?q?=F0=9F=8C=90=20Add=20Russian=20translatio?= =?UTF-8?q?n=20for=20`docs/ru/docs/tutorial/response-model.md`=20(#9675)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: s.glazkov Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Alexandr Co-authored-by: ivan-abc <36765187+ivan-abc@users.noreply.github.com> Co-authored-by: Sebastián Ramírez --- docs/ru/docs/tutorial/response-model.md | 480 ++++++++++++++++++++++++ 1 file changed, 480 insertions(+) create mode 100644 docs/ru/docs/tutorial/response-model.md diff --git a/docs/ru/docs/tutorial/response-model.md b/docs/ru/docs/tutorial/response-model.md new file mode 100644 index 000000000..c5e111790 --- /dev/null +++ b/docs/ru/docs/tutorial/response-model.md @@ -0,0 +1,480 @@ +# Модель ответа - Возвращаемый тип + +Вы можете объявить тип ответа, указав аннотацию **возвращаемого значения** для *функции операции пути*. + +FastAPI позволяет использовать **аннотации типов** таким же способом, как и для ввода данных в **параметры** функции, вы можете использовать модели Pydantic, списки, словари, скалярные типы (такие, как int, bool и т.д.). + +=== "Python 3.10+" + + ```Python hl_lines="16 21" + {!> ../../../docs_src/response_model/tutorial001_01_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="18 23" + {!> ../../../docs_src/response_model/tutorial001_01_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="18 23" + {!> ../../../docs_src/response_model/tutorial001_01.py!} + ``` + +FastAPI будет использовать этот возвращаемый тип для: + +* **Валидации** ответа. + * Если данные невалидны (например, отсутствует одно из полей), это означает, что код *вашего* приложения работает некорректно и функция возвращает не то, что вы ожидаете. В таком случае приложение вернет server error вместо того, чтобы отправить неправильные данные. Таким образом, вы и ваши пользователи можете быть уверены, что получите корректные данные в том виде, в котором они ожидаются. +* Добавьте **JSON схему** для ответа внутри *операции пути* OpenAPI. + * Она будет использована для **автоматически генерируемой документации**. + * А также - для автоматической кодогенерации пользователями. + +Но самое важное: + +* Ответ будет **ограничен и отфильтрован** - т.е. в нем останутся только те данные, которые определены в возвращаемом типе. + * Это особенно важно для **безопасности**, далее мы рассмотрим эту тему подробнее. + +## Параметр `response_model` + +Бывают случаи, когда вам необходимо (или просто хочется) возвращать данные, которые не полностью соответствуют объявленному типу. + +Допустим, вы хотите, чтобы ваша функция **возвращала словарь (dict)** или объект из базы данных, но при этом **объявляете выходной тип как модель Pydantic**. Тогда именно указанная модель будет использована для автоматической документации, валидации и т.п. для объекта, который вы вернули (например, словаря или объекта из базы данных). + +Но если указать аннотацию возвращаемого типа, статическая проверка типов будет выдавать ошибку (абсолютно корректную в данном случае). Она будет говорить о том, что ваша функция должна возвращать данные одного типа (например, dict), а в аннотации вы объявили другой тип (например, модель Pydantic). + +В таком случае можно использовать параметр `response_model` внутри *декоратора операции пути* вместо аннотации возвращаемого значения функции. + +Параметр `response_model` может быть указан для любой *операции пути*: + +* `@app.get()` +* `@app.post()` +* `@app.put()` +* `@app.delete()` +* и др. + +=== "Python 3.10+" + + ```Python hl_lines="17 22 24-27" + {!> ../../../docs_src/response_model/tutorial001_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="17 22 24-27" + {!> ../../../docs_src/response_model/tutorial001_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="17 22 24-27" + {!> ../../../docs_src/response_model/tutorial001.py!} + ``` + +!!! note "Технические детали" + Помните, что параметр `response_model` является параметром именно декоратора http-методов (`get`, `post`, и т.п.). Не следует его указывать для *функций операций пути*, как вы бы поступили с другими параметрами или с телом запроса. + +`response_model` принимает те же типы, которые можно указать для какого-либо поля в модели Pydantic. Таким образом, это может быть как одиночная модель Pydantic, так и `список (list)` моделей Pydantic. Например, `List[Item]`. + +FastAPI будет использовать значение `response_model` для того, чтобы автоматически генерировать документацию, производить валидацию и т.п. А также для **конвертации и фильтрации выходных данных** в объявленный тип. + +!!! tip "Подсказка" + Если вы используете анализаторы типов со строгой проверкой (например, mypy), можно указать `Any` в качестве типа возвращаемого значения функции. + + Таким образом вы информируете ваш редактор кода, что намеренно возвращаете данные неопределенного типа. Но возможности FastAPI, такие как автоматическая генерация документации, валидация, фильтрация и т.д. все так же будут работать, просто используя параметр `response_model`. + +### Приоритет `response_model` + +Если одновременно указать аннотацию типа для ответа функции и параметр `response_model` - последний будет иметь больший приоритет и FastAPI будет использовать именно его. + +Таким образом вы можете объявить корректные аннотации типов к вашим функциям, даже если они возвращают тип, отличающийся от указанного в `response_model`. Они будут считаны во время статической проверки типов вашими помощниками, например, mypy. При этом вы все так же используете возможности FastAPI для автоматической документации, валидации и т.д. благодаря `response_model`. + +Вы можете указать значение `response_model=None`, чтобы отключить создание модели ответа для данной *операции пути*. Это может понадобиться, если вы добавляете аннотации типов для данных, не являющихся валидными полями Pydantic. Мы увидим пример кода для такого случая в одном из разделов ниже. + +## Получить и вернуть один и тот же тип данных + +Здесь мы объявили модель `UserIn`, которая хранит пользовательский пароль в открытом виде: + +=== "Python 3.10+" + + ```Python hl_lines="7 9" + {!> ../../../docs_src/response_model/tutorial002_py310.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="9 11" + {!> ../../../docs_src/response_model/tutorial002.py!} + ``` + +!!! info "Информация" + Чтобы использовать `EmailStr`, прежде необходимо установить `email_validator`. + Используйте `pip install email-validator` + или `pip install pydantic[email]`. + +Далее мы используем нашу модель в аннотациях типа как для аргумента функции, так и для выходного значения: + +=== "Python 3.10+" + + ```Python hl_lines="16" + {!> ../../../docs_src/response_model/tutorial002_py310.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="18" + {!> ../../../docs_src/response_model/tutorial002.py!} + ``` + +Теперь всякий раз, когда клиент создает пользователя с паролем, API будет возвращать его пароль в ответе. + +В данном случае это не такая уж большая проблема, поскольку ответ получит тот же самый пользователь, который и создал пароль. + +Но что если мы захотим использовать эту модель для какой-либо другой *операции пути*? Мы можем, сами того не желая, отправить пароль любому другому пользователю. + +!!! danger "Осторожно" + Никогда не храните пароли пользователей в открытом виде, а также никогда не возвращайте их в ответе, как в примере выше. В противном случае - убедитесь, что вы хорошо продумали и учли все возможные риски такого подхода и вам известно, что вы делаете. + +## Создание модели для ответа + +Вместо этого мы можем создать входную модель, хранящую пароль в открытом виде и выходную модель без пароля: + +=== "Python 3.10+" + + ```Python hl_lines="9 11 16" + {!> ../../../docs_src/response_model/tutorial003_py310.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="9 11 16" + {!> ../../../docs_src/response_model/tutorial003.py!} + ``` + +В таком случае, даже несмотря на то, что наша *функция операции пути* возвращает тот же самый объект пользователя с паролем, полученным на вход: + +=== "Python 3.10+" + + ```Python hl_lines="24" + {!> ../../../docs_src/response_model/tutorial003_py310.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="24" + {!> ../../../docs_src/response_model/tutorial003.py!} + ``` + +...мы указали в `response_model` модель `UserOut`, в которой отсутствует поле, содержащее пароль - и он будет исключен из ответа: + +=== "Python 3.10+" + + ```Python hl_lines="22" + {!> ../../../docs_src/response_model/tutorial003_py310.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="22" + {!> ../../../docs_src/response_model/tutorial003.py!} + ``` + +Таким образом **FastAPI** позаботится о фильтрации ответа и исключит из него всё, что не указано в выходной модели (при помощи Pydantic). + +### `response_model` или возвращаемый тип данных + +В нашем примере модели входных данных и выходных данных различаются. И если мы укажем аннотацию типа выходного значения функции как `UserOut` - проверка типов выдаст ошибку из-за того, что мы возвращаем некорректный тип. Поскольку это 2 разных класса. + +Поэтому в нашем примере мы можем объявить тип ответа только в параметре `response_model`. + +...но продолжайте читать дальше, чтобы узнать как можно это обойти. + +## Возвращаемый тип и Фильтрация данных + +Продолжим рассматривать предыдущий пример. Мы хотели **аннотировать входные данные одним типом**, а выходное значение - **другим типом**. + +Мы хотим, чтобы FastAPI продолжал **фильтровать** данные, используя `response_model`. + +В прошлом примере, т.к. входной и выходной типы являлись разными классами, мы были вынуждены использовать параметр `response_model`. И как следствие, мы лишались помощи статических анализаторов для проверки ответа функции. + +Но в подавляющем большинстве случаев мы будем хотеть, чтобы модель ответа лишь **фильтровала/удаляла** некоторые данные из ответа, как в нашем примере. + +И в таких случаях мы можем использовать классы и наследование, чтобы пользоваться преимуществами **аннотаций типов** и получать более полную статическую проверку типов. Но при этом все так же получать **фильтрацию ответа** от FastAPI. + +=== "Python 3.10+" + + ```Python hl_lines="7-10 13-14 18" + {!> ../../../docs_src/response_model/tutorial003_01_py310.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="9-13 15-16 20" + {!> ../../../docs_src/response_model/tutorial003_01.py!} + ``` + +Таким образом, мы получаем поддержку редактора кода и mypy в части типов, сохраняя при этом фильтрацию данных от FastAPI. + +Как это возможно? Давайте разберемся. 🤓 + +### Аннотации типов и инструменты для их проверки + +Для начала давайте рассмотрим как наш редактор кода, mypy и другие помощники разработчика видят аннотации типов. + +У модели `BaseUser` есть некоторые поля. Затем `UserIn` наследуется от `BaseUser` и добавляет новое поле `password`. Таким образом модель будет включать в себя все поля из первой модели (родителя), а также свои собственные. + +Мы аннотируем возвращаемый тип функции как `BaseUser`, но фактически мы будем возвращать объект типа `UserIn`. + +Редакторы, mypy и другие инструменты не будут иметь возражений против такого подхода, поскольку `UserIn` является подклассом `BaseUser`. Это означает, что такой тип будет *корректным*, т.к. ответ может быть чем угодно, если это будет `BaseUser`. + +### Фильтрация Данных FastAPI + +FastAPI знает тип ответа функции, так что вы можете быть уверены, что на выходе будут **только** те поля, которые вы указали. + +FastAPI совместно с Pydantic выполнит некоторую магию "под капотом", чтобы убедиться, что те же самые правила наследования классов не используются для фильтрации возвращаемых данных, в противном случае вы могли бы в конечном итоге вернуть гораздо больше данных, чем ожидали. + +Таким образом, вы можете получить все самое лучшее из обоих миров: аннотации типов с **поддержкой инструментов для разработки** и **фильтрацию данных**. + +## Автоматическая документация + +Если посмотреть на сгенерированную документацию, вы можете убедиться, что в ней присутствуют обе JSON схемы - как для входной модели, так и для выходной: + + + +И также обе модели будут использованы в интерактивной документации API: + + + +## Другие аннотации типов + +Бывают случаи, когда вы возвращаете что-то, что не является валидным типом для Pydantic и вы указываете аннотацию ответа функции только для того, чтобы работала поддержка различных инструментов (редактор кода, mypy и др.). + +### Возвращаем Response + +Самый частый сценарий использования - это [возвращать Response напрямую, как описано в расширенной документации](../advanced/response-directly.md){.internal-link target=_blank}. + +```Python hl_lines="8 10-11" +{!> ../../../docs_src/response_model/tutorial003_02.py!} +``` + +Это поддерживается FastAPI по-умолчанию, т.к. аннотация проставлена в классе (или подклассе) `Response`. + +И ваши помощники разработки также будут счастливы, т.к. оба класса `RedirectResponse` и `JSONResponse` являются подклассами `Response`. Таким образом мы получаем корректную аннотацию типа. + +### Подкласс Response в аннотации типа + +Вы также можете указать подкласс `Response` в аннотации типа: + +```Python hl_lines="8-9" +{!> ../../../docs_src/response_model/tutorial003_03.py!} +``` + +Это сработает, потому что `RedirectResponse` является подклассом `Response` и FastAPI автоматически обработает этот простейший случай. + +### Некорректные аннотации типов + +Но когда вы возвращаете какой-либо другой произвольный объект, который не является допустимым типом Pydantic (например, объект из базы данных), и вы аннотируете его подобным образом для функции, FastAPI попытается создать из этого типа модель Pydantic и потерпит неудачу. + +То же самое произошло бы, если бы у вас было что-то вроде Union различных типов и один или несколько из них не являлись бы допустимыми типами для Pydantic. Например, такой вариант приведет к ошибке 💥: + +=== "Python 3.10+" + + ```Python hl_lines="8" + {!> ../../../docs_src/response_model/tutorial003_04_py310.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="10" + {!> ../../../docs_src/response_model/tutorial003_04.py!} + ``` + +...такой код вызовет ошибку, потому что в аннотации указан неподдерживаемый Pydantic тип. А также этот тип не является классом или подклассом `Response`. + +### Возможно ли отключить генерацию модели ответа? + +Продолжим рассматривать предыдущий пример. Допустим, что вы хотите отказаться от автоматической валидации ответа, документации, фильтрации и т.д. + +Но в то же время, хотите сохранить аннотацию возвращаемого типа для функции, чтобы обеспечить работу помощников и анализаторов типов (например, mypy). + +В таком случае, вы можете отключить генерацию модели ответа, указав `response_model=None`: + +=== "Python 3.10+" + + ```Python hl_lines="7" + {!> ../../../docs_src/response_model/tutorial003_05_py310.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="9" + {!> ../../../docs_src/response_model/tutorial003_05.py!} + ``` + +Тогда FastAPI не станет генерировать модель ответа и вы сможете сохранить такую аннотацию типа, которая вам требуется, никак не влияя на работу FastAPI. 🤓 + +## Параметры модели ответа + +Модель ответа может иметь значения по умолчанию, например: + +=== "Python 3.10+" + + ```Python hl_lines="9 11-12" + {!> ../../../docs_src/response_model/tutorial004_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="11 13-14" + {!> ../../../docs_src/response_model/tutorial004_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="11 13-14" + {!> ../../../docs_src/response_model/tutorial004.py!} + ``` + +* `description: Union[str, None] = None` (или `str | None = None` в Python 3.10), где `None` является значением по умолчанию. +* `tax: float = 10.5`, где `10.5` является значением по умолчанию. +* `tags: List[str] = []`, где пустой список `[]` является значением по умолчанию. + +но вы, возможно, хотели бы исключить их из ответа, если данные поля не были заданы явно. + +Например, у вас есть модель с множеством необязательных полей в NoSQL базе данных, но вы не хотите отправлять в качестве ответа очень длинный JSON с множеством значений по умолчанию. + +### Используйте параметр `response_model_exclude_unset` + +Установите для *декоратора операции пути* параметр `response_model_exclude_unset=True`: + +=== "Python 3.10+" + + ```Python hl_lines="22" + {!> ../../../docs_src/response_model/tutorial004_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="24" + {!> ../../../docs_src/response_model/tutorial004_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="24" + {!> ../../../docs_src/response_model/tutorial004.py!} + ``` + +и тогда значения по умолчанию не будут включены в ответ. В нем будут только те поля, значения которых фактически были установлены. + +Итак, если вы отправите запрос на данную *операцию пути* для элемента, с ID = `Foo` - ответ (с исключенными значениями по-умолчанию) будет таким: + +```JSON +{ + "name": "Foo", + "price": 50.2 +} +``` + +!!! info "Информация" + "Под капотом" FastAPI использует метод `.dict()` у объектов моделей Pydantic с параметром `exclude_unset`, чтобы достичь такого эффекта. + +!!! info "Информация" + Вы также можете использовать: + + * `response_model_exclude_defaults=True` + * `response_model_exclude_none=True` + + как описано в документации Pydantic для параметров `exclude_defaults` и `exclude_none`. + +#### Если значение поля отличается от значения по-умолчанию + +Если для некоторых полей модели, имеющих значения по-умолчанию, значения были явно установлены - как для элемента с ID = `Bar`, ответ будет таким: + +```Python hl_lines="3 5" +{ + "name": "Bar", + "description": "The bartenders", + "price": 62, + "tax": 20.2 +} +``` + +они не будут исключены из ответа. + +#### Если значение поля совпадает с его значением по умолчанию + +Если данные содержат те же значения, которые являются для этих полей по умолчанию, но были установлены явно - как для элемента с ID = `baz`, ответ будет таким: + +```Python hl_lines="3 5-6" +{ + "name": "Baz", + "description": None, + "price": 50.2, + "tax": 10.5, + "tags": [] +} +``` + +FastAPI достаточно умен (на самом деле, это заслуга Pydantic), чтобы понять, что, хотя `description`, `tax` и `tags` хранят такие же данные, какие должны быть по умолчанию - для них эти значения были установлены явно (а не получены из значений по умолчанию). + +И поэтому, они также будут включены в JSON ответа. + +!!! tip "Подсказка" + Значением по умолчанию может быть что угодно, не только `None`. + + Им может быть и список (`[]`), значение 10.5 типа `float`, и т.п. + +### `response_model_include` и `response_model_exclude` + +Вы также можете использовать параметры *декоратора операции пути*, такие, как `response_model_include` и `response_model_exclude`. + +Они принимают аргументы типа `set`, состоящий из строк (`str`) с названиями атрибутов, которые либо требуется включить в ответ (при этом исключив все остальные), либо наоборот исключить (оставив в ответе все остальные поля). + +Это можно использовать как быстрый способ исключить данные из ответа, не создавая отдельную модель Pydantic. + +!!! tip "Подсказка" + Но по-прежнему рекомендуется следовать изложенным выше советам и использовать несколько моделей вместо данных параметров. + + Потому как JSON схема OpenAPI, генерируемая вашим приложением (а также документация) все еще будет содержать все поля, даже если вы использовали `response_model_include` или `response_model_exclude` и исключили некоторые атрибуты. + + То же самое применимо к параметру `response_model_by_alias`. + +=== "Python 3.10+" + + ```Python hl_lines="29 35" + {!> ../../../docs_src/response_model/tutorial005_py310.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="31 37" + {!> ../../../docs_src/response_model/tutorial005.py!} + ``` + +!!! tip "Подсказка" + При помощи кода `{"name","description"}` создается объект множества (`set`) с двумя строковыми значениями. + + Того же самого можно достичь используя `set(["name", "description"])`. + +#### Что если использовать `list` вместо `set`? + +Если вы забыли про `set` и использовали структуру `list` или `tuple`, FastAPI автоматически преобразует этот объект в `set`, чтобы обеспечить корректную работу: + +=== "Python 3.10+" + + ```Python hl_lines="29 35" + {!> ../../../docs_src/response_model/tutorial006_py310.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="31 37" + {!> ../../../docs_src/response_model/tutorial006.py!} + ``` + +## Резюме + +Используйте параметр `response_model` у *декоратора операции пути* для того, чтобы задать модель ответа и в большей степени для того, чтобы быть уверенным, что приватная информация будет отфильтрована. + +А также используйте `response_model_exclude_unset`, чтобы возвращать только те значения, которые были заданы явно. From 317cef3f8a4ff41a019e8558e97b4c64a2769f51 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 27 Jun 2023 01:00:55 +0000 Subject: [PATCH 20/58] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index e55bf48c7..75ce0bbe6 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Russian translation for `docs/ru/docs/tutorial/response-model.md`. PR [#9675](https://github.com/tiangolo/fastapi/pull/9675) by [@glsglsgls](https://github.com/glsglsgls). * 📝 Fix form for the FastAPI and friends newsletter. PR [#9749](https://github.com/tiangolo/fastapi/pull/9749) by [@tiangolo](https://github.com/tiangolo). * ✨ Add Material for MkDocs Insiders features and cards. PR [#9748](https://github.com/tiangolo/fastapi/pull/9748) by [@tiangolo](https://github.com/tiangolo). * 🔥 Remove languages without translations. PR [#9743](https://github.com/tiangolo/fastapi/pull/9743) by [@tiangolo](https://github.com/tiangolo). From 6ba4492670381eb5cc08f49a19a83a51485ddc07 Mon Sep 17 00:00:00 2001 From: mojtaba <121169359+mojtabapaso@users.noreply.github.com> Date: Tue, 27 Jun 2023 04:32:00 +0330 Subject: [PATCH 21/58] =?UTF-8?q?=F0=9F=8C=90=20Add=20Persian=20translatio?= =?UTF-8?q?n=20for=20`docs/fa/docs/advanced/sub-applications.md`=20(#9692)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Amin Alaee Co-authored-by: Sebastián Ramírez --- docs/fa/docs/advanced/sub-applications.md | 72 +++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 docs/fa/docs/advanced/sub-applications.md diff --git a/docs/fa/docs/advanced/sub-applications.md b/docs/fa/docs/advanced/sub-applications.md new file mode 100644 index 000000000..f3a948414 --- /dev/null +++ b/docs/fa/docs/advanced/sub-applications.md @@ -0,0 +1,72 @@ +# زیر برنامه ها - اتصال + +اگر نیاز دارید که دو برنامه مستقل FastAPI، با OpenAPI مستقل و رابط‌های کاربری اسناد خود داشته باشید، می‌توانید یک برنامه +اصلی داشته باشید و یک (یا چند) زیر برنامه را به آن متصل کنید. + +## اتصال (mount) به یک برنامه **FastAPI** + +کلمه "Mounting" به معنای افزودن یک برنامه کاملاً مستقل در یک مسیر خاص است، که پس از آن مدیریت همه چیز در آن مسیر، با path operations (عملیات های مسیر) اعلام شده در آن زیر برنامه می باشد. + +### برنامه سطح بالا + +ابتدا برنامه اصلی سطح بالا، **FastAPI** و path operations آن را ایجاد کنید: + + +```Python hl_lines="3 6-8" +{!../../../docs_src/sub_applications/tutorial001.py!} +``` + +### زیر برنامه + +سپس، زیر برنامه خود و path operations آن را ایجاد کنید. + +این زیر برنامه فقط یکی دیگر از برنامه های استاندارد FastAPI است، اما این برنامه ای است که متصل می شود: + +```Python hl_lines="11 14-16" +{!../../../docs_src/sub_applications/tutorial001.py!} +``` + +### اتصال زیر برنامه + +در برنامه سطح بالا `app` اتصال زیر برنامه `subapi` در این نمونه `/subapi` در مسیر قرار میدهد و میشود: + +```Python hl_lines="11 19" +{!../../../docs_src/sub_applications/tutorial001.py!} +``` + +### اسناد API خودکار را بررسی کنید + +برنامه را با استفاده از ‘uvicorn‘ اجرا کنید، اگر فایل شما ‘main.py‘ نام دارد، دستور زیر را وارد کنید: +
+ +```console +$ uvicorn main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +صفحه مستندات را در آدرس http://127.0.0.1:8000/docs باز کنید. + +اسناد API خودکار برنامه اصلی را مشاهده خواهید کرد که فقط شامل path operations خود می شود: + + + +و سپس اسناد زیر برنامه را در آدرس http://127.0.0.1:8000/subapi/docs. باز کنید. + +اسناد API خودکار برای زیر برنامه را خواهید دید، که فقط شامل path operations خود می شود، همه در زیر مسیر `/subapi` قرار دارند: + + + +اگر سعی کنید با هر یک از این دو رابط کاربری تعامل داشته باشید، آنها به درستی کار می کنند، زیرا مرورگر می تواند با هر یک از برنامه ها یا زیر برنامه های خاص صحبت کند. + +### جرئیات فنی : `root_path` + +هنگامی که یک زیر برنامه را همانطور که در بالا توضیح داده شد متصل می کنید, FastAPI با استفاده از مکانیزمی از مشخصات ASGI به نام `root_path` ارتباط مسیر mount را برای زیر برنامه انجام می دهد. + +به این ترتیب، زیر برنامه می داند که از آن پیشوند مسیر برای رابط کاربری اسناد (docs UI) استفاده کند. + +و زیر برنامه ها نیز می تواند زیر برنامه های متصل شده خود را داشته باشد و همه چیز به درستی کار کند، زیرا FastAPI تمام این مسیرهای `root_path` را به طور خودکار مدیریت می کند. + +در بخش [پشت پراکسی](./behind-a-proxy.md){.internal-link target=_blank}. درباره `root_path` و نحوه استفاده درست از آن بیشتر خواهید آموخت. From a95af9466967cefdd1433650745264b4d2db5a4f Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 27 Jun 2023 01:02:34 +0000 Subject: [PATCH 22/58] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 75ce0bbe6..190526bd1 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Persian translation for `docs/fa/docs/advanced/sub-applications.md`. PR [#9692](https://github.com/tiangolo/fastapi/pull/9692) by [@mojtabapaso](https://github.com/mojtabapaso). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/response-model.md`. PR [#9675](https://github.com/tiangolo/fastapi/pull/9675) by [@glsglsgls](https://github.com/glsglsgls). * 📝 Fix form for the FastAPI and friends newsletter. PR [#9749](https://github.com/tiangolo/fastapi/pull/9749) by [@tiangolo](https://github.com/tiangolo). * ✨ Add Material for MkDocs Insiders features and cards. PR [#9748](https://github.com/tiangolo/fastapi/pull/9748) by [@tiangolo](https://github.com/tiangolo). From eb312758d8c10ac723f4954cd0b86e07fa9ac3eb Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 27 Jun 2023 03:06:02 +0200 Subject: [PATCH 23/58] =?UTF-8?q?=E2=AC=86=20[pre-commit.ci]=20pre-commit?= =?UTF-8?q?=20autoupdate=20(#9259)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.3.1 → v3.7.0](https://github.com/asottile/pyupgrade/compare/v3.3.1...v3.7.0) - [github.com/charliermarsh/ruff-pre-commit: v0.0.272 → v0.0.275](https://github.com/charliermarsh/ruff-pre-commit/compare/v0.0.272...v0.0.275) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2a8a03136..9f7085f72 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,14 +14,14 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/asottile/pyupgrade - rev: v3.3.1 + rev: v3.7.0 hooks: - id: pyupgrade args: - --py3-plus - --keep-runtime-typing - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.0.272 + rev: v0.0.275 hooks: - id: ruff args: From 5e7d45af16ba0b6fb1a954d44f714a6016ec9e7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 27 Jun 2023 03:06:27 +0200 Subject: [PATCH 24/58] =?UTF-8?q?=F0=9F=94=A5=20Remove=20missing=20transla?= =?UTF-8?q?tion=20dummy=20pages,=20no=20longer=20necessary=20(#9751)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/de/docs/index.md | 463 ----------------------------------------- docs/id/docs/index.md | 465 ------------------------------------------ docs/tr/docs/index.md | 4 - 3 files changed, 932 deletions(-) delete mode 100644 docs/de/docs/index.md delete mode 100644 docs/id/docs/index.md diff --git a/docs/de/docs/index.md b/docs/de/docs/index.md deleted file mode 100644 index f1c873d75..000000000 --- a/docs/de/docs/index.md +++ /dev/null @@ -1,463 +0,0 @@ - -{!../../../docs/missing-translation.md!} - - -

- FastAPI -

-

- FastAPI framework, high performance, easy to learn, fast to code, ready for production -

-

- - Test - - - Coverage - - - Package version - -

- ---- - -**Documentation**: https://fastapi.tiangolo.com - -**Source Code**: https://github.com/tiangolo/fastapi - ---- - -FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.6+ based on standard Python type hints. - -The key features are: - -* **Fast**: Very high performance, on par with **NodeJS** and **Go** (thanks to Starlette and Pydantic). [One of the fastest Python frameworks available](#performance). - -* **Fast to code**: Increase the speed to develop features by about 200% to 300%. * -* **Fewer bugs**: Reduce about 40% of human (developer) induced errors. * -* **Intuitive**: Great editor support. Completion everywhere. Less time debugging. -* **Easy**: Designed to be easy to use and learn. Less time reading docs. -* **Short**: Minimize code duplication. Multiple features from each parameter declaration. Fewer bugs. -* **Robust**: Get production-ready code. With automatic interactive documentation. -* **Standards-based**: Based on (and fully compatible with) the open standards for APIs: OpenAPI (previously known as Swagger) and JSON Schema. - -* estimation based on tests on an internal development team, building production applications. - -## Sponsors - - - -{% if sponsors %} -{% for sponsor in sponsors.gold -%} - -{% endfor -%} -{%- for sponsor in sponsors.silver -%} - -{% endfor %} -{% endif %} - - - -Other sponsors - -## Opinions - -"_[...] I'm using **FastAPI** a ton these days. [...] I'm actually planning to use it for all of my team's **ML services at Microsoft**. Some of them are getting integrated into the core **Windows** product and some **Office** products._" - -
Kabir Khan - Microsoft (ref)
- ---- - -"_We adopted the **FastAPI** library to spawn a **REST** server that can be queried to obtain **predictions**. [for Ludwig]_" - -
Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - Uber (ref)
- ---- - -"_**Netflix** is pleased to announce the open-source release of our **crisis management** orchestration framework: **Dispatch**! [built with **FastAPI**]_" - -
Kevin Glisson, Marc Vilanova, Forest Monsen - Netflix (ref)
- ---- - -"_I’m over the moon excited about **FastAPI**. It’s so fun!_" - -
Brian Okken - Python Bytes podcast host (ref)
- ---- - -"_Honestly, what you've built looks super solid and polished. In many ways, it's what I wanted **Hug** to be - it's really inspiring to see someone build that._" - -
Timothy Crosley - Hug creator (ref)
- ---- - -"_If you're looking to learn one **modern framework** for building REST APIs, check out **FastAPI** [...] It's fast, easy to use and easy to learn [...]_" - -"_We've switched over to **FastAPI** for our **APIs** [...] I think you'll like it [...]_" - -
Ines Montani - Matthew Honnibal - Explosion AI founders - spaCy creators (ref) - (ref)
- ---- - -## **Typer**, the FastAPI of CLIs - - - -If you are building a CLI app to be used in the terminal instead of a web API, check out **Typer**. - -**Typer** is FastAPI's little sibling. And it's intended to be the **FastAPI of CLIs**. ⌨️ 🚀 - -## Requirements - -Python 3.7+ - -FastAPI stands on the shoulders of giants: - -* Starlette for the web parts. -* Pydantic for the data parts. - -## Installation - -
- -```console -$ pip install fastapi - ----> 100% -``` - -
- -You will also need an ASGI server, for production such as Uvicorn or Hypercorn. - -
- -```console -$ pip install "uvicorn[standard]" - ----> 100% -``` - -
- -## Example - -### Create it - -* Create a file `main.py` with: - -```Python -from typing import Union - -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} -``` - -
-Or use async def... - -If your code uses `async` / `await`, use `async def`: - -```Python hl_lines="9 14" -from typing import Union - -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -async def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -async def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} -``` - -**Note**: - -If you don't know, check the _"In a hurry?"_ section about `async` and `await` in the docs. - -
- -### Run it - -Run the server with: - -
- -```console -$ uvicorn main:app --reload - -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -INFO: Started reloader process [28720] -INFO: Started server process [28722] -INFO: Waiting for application startup. -INFO: Application startup complete. -``` - -
- -
-About the command uvicorn main:app --reload... - -The command `uvicorn main:app` refers to: - -* `main`: the file `main.py` (the Python "module"). -* `app`: the object created inside of `main.py` with the line `app = FastAPI()`. -* `--reload`: make the server restart after code changes. Only do this for development. - -
- -### Check it - -Open your browser at http://127.0.0.1:8000/items/5?q=somequery. - -You will see the JSON response as: - -```JSON -{"item_id": 5, "q": "somequery"} -``` - -You already created an API that: - -* Receives HTTP requests in the _paths_ `/` and `/items/{item_id}`. -* Both _paths_ take `GET` operations (also known as HTTP _methods_). -* The _path_ `/items/{item_id}` has a _path parameter_ `item_id` that should be an `int`. -* The _path_ `/items/{item_id}` has an optional `str` _query parameter_ `q`. - -### Interactive API docs - -Now go to http://127.0.0.1:8000/docs. - -You will see the automatic interactive API documentation (provided by Swagger UI): - -![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) - -### Alternative API docs - -And now, go to http://127.0.0.1:8000/redoc. - -You will see the alternative automatic documentation (provided by ReDoc): - -![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) - -## Example upgrade - -Now modify the file `main.py` to receive a body from a `PUT` request. - -Declare the body using standard Python types, thanks to Pydantic. - -```Python hl_lines="4 9-12 25-27" -from typing import Union - -from fastapi import FastAPI -from pydantic import BaseModel - -app = FastAPI() - - -class Item(BaseModel): - name: str - price: float - is_offer: Union[bool, None] = None - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} - - -@app.put("/items/{item_id}") -def update_item(item_id: int, item: Item): - return {"item_name": item.name, "item_id": item_id} -``` - -The server should reload automatically (because you added `--reload` to the `uvicorn` command above). - -### Interactive API docs upgrade - -Now go to http://127.0.0.1:8000/docs. - -* The interactive API documentation will be automatically updated, including the new body: - -![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) - -* Click on the button "Try it out", it allows you to fill the parameters and directly interact with the API: - -![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) - -* Then click on the "Execute" button, the user interface will communicate with your API, send the parameters, get the results and show them on the screen: - -![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) - -### Alternative API docs upgrade - -And now, go to http://127.0.0.1:8000/redoc. - -* The alternative documentation will also reflect the new query parameter and body: - -![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) - -### Recap - -In summary, you declare **once** the types of parameters, body, etc. as function parameters. - -You do that with standard modern Python types. - -You don't have to learn a new syntax, the methods or classes of a specific library, etc. - -Just standard **Python 3.6+**. - -For example, for an `int`: - -```Python -item_id: int -``` - -or for a more complex `Item` model: - -```Python -item: Item -``` - -...and with that single declaration you get: - -* Editor support, including: - * Completion. - * Type checks. -* Validation of data: - * Automatic and clear errors when the data is invalid. - * Validation even for deeply nested JSON objects. -* Conversion of input data: coming from the network to Python data and types. Reading from: - * JSON. - * Path parameters. - * Query parameters. - * Cookies. - * Headers. - * Forms. - * Files. -* Conversion of output data: converting from Python data and types to network data (as JSON): - * Convert Python types (`str`, `int`, `float`, `bool`, `list`, etc). - * `datetime` objects. - * `UUID` objects. - * Database models. - * ...and many more. -* Automatic interactive API documentation, including 2 alternative user interfaces: - * Swagger UI. - * ReDoc. - ---- - -Coming back to the previous code example, **FastAPI** will: - -* Validate that there is an `item_id` in the path for `GET` and `PUT` requests. -* Validate that the `item_id` is of type `int` for `GET` and `PUT` requests. - * If it is not, the client will see a useful, clear error. -* Check if there is an optional query parameter named `q` (as in `http://127.0.0.1:8000/items/foo?q=somequery`) for `GET` requests. - * As the `q` parameter is declared with `= None`, it is optional. - * Without the `None` it would be required (as is the body in the case with `PUT`). -* For `PUT` requests to `/items/{item_id}`, Read the body as JSON: - * Check that it has a required attribute `name` that should be a `str`. - * Check that it has a required attribute `price` that has to be a `float`. - * Check that it has an optional attribute `is_offer`, that should be a `bool`, if present. - * All this would also work for deeply nested JSON objects. -* Convert from and to JSON automatically. -* Document everything with OpenAPI, that can be used by: - * Interactive documentation systems. - * Automatic client code generation systems, for many languages. -* Provide 2 interactive documentation web interfaces directly. - ---- - -We just scratched the surface, but you already get the idea of how it all works. - -Try changing the line with: - -```Python - return {"item_name": item.name, "item_id": item_id} -``` - -...from: - -```Python - ... "item_name": item.name ... -``` - -...to: - -```Python - ... "item_price": item.price ... -``` - -...and see how your editor will auto-complete the attributes and know their types: - -![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) - -For a more complete example including more features, see the Tutorial - User Guide. - -**Spoiler alert**: the tutorial - user guide includes: - -* Declaration of **parameters** from other different places as: **headers**, **cookies**, **form fields** and **files**. -* How to set **validation constraints** as `maximum_length` or `regex`. -* A very powerful and easy to use **Dependency Injection** system. -* Security and authentication, including support for **OAuth2** with **JWT tokens** and **HTTP Basic** auth. -* More advanced (but equally easy) techniques for declaring **deeply nested JSON models** (thanks to Pydantic). -* Many extra features (thanks to Starlette) as: - * **WebSockets** - * extremely easy tests based on `requests` and `pytest` - * **CORS** - * **Cookie Sessions** - * ...and more. - -## Performance - -Independent TechEmpower benchmarks show **FastAPI** applications running under Uvicorn as one of the fastest Python frameworks available, only below Starlette and Uvicorn themselves (used internally by FastAPI). (*) - -To understand more about it, see the section Benchmarks. - -## Optional Dependencies - -Used by Pydantic: - -* email_validator - for email validation. - -Used by Starlette: - -* httpx - Required if you want to use the `TestClient`. -* jinja2 - Required if you want to use the default template configuration. -* python-multipart - Required if you want to support form "parsing", with `request.form()`. -* itsdangerous - Required for `SessionMiddleware` support. -* pyyaml - Required for Starlette's `SchemaGenerator` support (you probably don't need it with FastAPI). -* ujson - Required if you want to use `UJSONResponse`. - -Used by FastAPI / Starlette: - -* uvicorn - for the server that loads and serves your application. -* orjson - Required if you want to use `ORJSONResponse`. - -You can install all of these with `pip install fastapi[all]`. - -## License - -This project is licensed under the terms of the MIT license. diff --git a/docs/id/docs/index.md b/docs/id/docs/index.md deleted file mode 100644 index ed551f910..000000000 --- a/docs/id/docs/index.md +++ /dev/null @@ -1,465 +0,0 @@ - -{!../../../docs/missing-translation.md!} - - -

- FastAPI -

-

- FastAPI framework, high performance, easy to learn, fast to code, ready for production -

-

- - Test - - - Coverage - - - Package version - -

- ---- - -**Documentation**: https://fastapi.tiangolo.com - -**Source Code**: https://github.com/tiangolo/fastapi - ---- - -FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.6+ based on standard Python type hints. - -The key features are: - -* **Fast**: Very high performance, on par with **NodeJS** and **Go** (thanks to Starlette and Pydantic). [One of the fastest Python frameworks available](#performance). - -* **Fast to code**: Increase the speed to develop features by about 200% to 300%. * -* **Fewer bugs**: Reduce about 40% of human (developer) induced errors. * -* **Intuitive**: Great editor support. Completion everywhere. Less time debugging. -* **Easy**: Designed to be easy to use and learn. Less time reading docs. -* **Short**: Minimize code duplication. Multiple features from each parameter declaration. Fewer bugs. -* **Robust**: Get production-ready code. With automatic interactive documentation. -* **Standards-based**: Based on (and fully compatible with) the open standards for APIs: OpenAPI (previously known as Swagger) and JSON Schema. - -* estimation based on tests on an internal development team, building production applications. - -## Sponsors - - - -{% if sponsors %} -{% for sponsor in sponsors.gold -%} - -{% endfor -%} -{%- for sponsor in sponsors.silver -%} - -{% endfor %} -{% endif %} - - - -Other sponsors - -## Opinions - -"_[...] I'm using **FastAPI** a ton these days. [...] I'm actually planning to use it for all of my team's **ML services at Microsoft**. Some of them are getting integrated into the core **Windows** product and some **Office** products._" - -
Kabir Khan - Microsoft (ref)
- ---- - -"_We adopted the **FastAPI** library to spawn a **REST** server that can be queried to obtain **predictions**. [for Ludwig]_" - -
Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - Uber (ref)
- ---- - -"_**Netflix** is pleased to announce the open-source release of our **crisis management** orchestration framework: **Dispatch**! [built with **FastAPI**]_" - -
Kevin Glisson, Marc Vilanova, Forest Monsen - Netflix (ref)
- ---- - -"_I’m over the moon excited about **FastAPI**. It’s so fun!_" - -
Brian Okken - Python Bytes podcast host (ref)
- ---- - -"_Honestly, what you've built looks super solid and polished. In many ways, it's what I wanted **Hug** to be - it's really inspiring to see someone build that._" - -
Timothy Crosley - Hug creator (ref)
- ---- - -"_If you're looking to learn one **modern framework** for building REST APIs, check out **FastAPI** [...] It's fast, easy to use and easy to learn [...]_" - -"_We've switched over to **FastAPI** for our **APIs** [...] I think you'll like it [...]_" - -
Ines Montani - Matthew Honnibal - Explosion AI founders - spaCy creators (ref) - (ref)
- ---- - -## **Typer**, the FastAPI of CLIs - - - -If you are building a CLI app to be used in the terminal instead of a web API, check out **Typer**. - -**Typer** is FastAPI's little sibling. And it's intended to be the **FastAPI of CLIs**. ⌨️ 🚀 - -## Requirements - -Python 3.7+ - -FastAPI stands on the shoulders of giants: - -* Starlette for the web parts. -* Pydantic for the data parts. - -## Installation - -
- -```console -$ pip install fastapi - ----> 100% -``` - -
- -You will also need an ASGI server, for production such as Uvicorn or Hypercorn. - -
- -```console -$ pip install "uvicorn[standard]" - ----> 100% -``` - -
- -## Example - -### Create it - -* Create a file `main.py` with: - -```Python -from typing import Optional - -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int, q: Optional[str] = None): - return {"item_id": item_id, "q": q} -``` - -
-Or use async def... - -If your code uses `async` / `await`, use `async def`: - -```Python hl_lines="9 14" -from typing import Optional - -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -async def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -async def read_item(item_id: int, q: Optional[str] = None): - return {"item_id": item_id, "q": q} -``` - -**Note**: - -If you don't know, check the _"In a hurry?"_ section about `async` and `await` in the docs. - -
- -### Run it - -Run the server with: - -
- -```console -$ uvicorn main:app --reload - -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -INFO: Started reloader process [28720] -INFO: Started server process [28722] -INFO: Waiting for application startup. -INFO: Application startup complete. -``` - -
- -
-About the command uvicorn main:app --reload... - -The command `uvicorn main:app` refers to: - -* `main`: the file `main.py` (the Python "module"). -* `app`: the object created inside of `main.py` with the line `app = FastAPI()`. -* `--reload`: make the server restart after code changes. Only do this for development. - -
- -### Check it - -Open your browser at http://127.0.0.1:8000/items/5?q=somequery. - -You will see the JSON response as: - -```JSON -{"item_id": 5, "q": "somequery"} -``` - -You already created an API that: - -* Receives HTTP requests in the _paths_ `/` and `/items/{item_id}`. -* Both _paths_ take `GET` operations (also known as HTTP _methods_). -* The _path_ `/items/{item_id}` has a _path parameter_ `item_id` that should be an `int`. -* The _path_ `/items/{item_id}` has an optional `str` _query parameter_ `q`. - -### Interactive API docs - -Now go to http://127.0.0.1:8000/docs. - -You will see the automatic interactive API documentation (provided by Swagger UI): - -![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) - -### Alternative API docs - -And now, go to http://127.0.0.1:8000/redoc. - -You will see the alternative automatic documentation (provided by ReDoc): - -![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) - -## Example upgrade - -Now modify the file `main.py` to receive a body from a `PUT` request. - -Declare the body using standard Python types, thanks to Pydantic. - -```Python hl_lines="4 9-12 25-27" -from typing import Optional - -from fastapi import FastAPI -from pydantic import BaseModel - -app = FastAPI() - - -class Item(BaseModel): - name: str - price: float - is_offer: Optional[bool] = None - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int, q: Optional[str] = None): - return {"item_id": item_id, "q": q} - - -@app.put("/items/{item_id}") -def update_item(item_id: int, item: Item): - return {"item_name": item.name, "item_id": item_id} -``` - -The server should reload automatically (because you added `--reload` to the `uvicorn` command above). - -### Interactive API docs upgrade - -Now go to http://127.0.0.1:8000/docs. - -* The interactive API documentation will be automatically updated, including the new body: - -![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) - -* Click on the button "Try it out", it allows you to fill the parameters and directly interact with the API: - -![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) - -* Then click on the "Execute" button, the user interface will communicate with your API, send the parameters, get the results and show them on the screen: - -![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) - -### Alternative API docs upgrade - -And now, go to http://127.0.0.1:8000/redoc. - -* The alternative documentation will also reflect the new query parameter and body: - -![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) - -### Recap - -In summary, you declare **once** the types of parameters, body, etc. as function parameters. - -You do that with standard modern Python types. - -You don't have to learn a new syntax, the methods or classes of a specific library, etc. - -Just standard **Python 3.6+**. - -For example, for an `int`: - -```Python -item_id: int -``` - -or for a more complex `Item` model: - -```Python -item: Item -``` - -...and with that single declaration you get: - -* Editor support, including: - * Completion. - * Type checks. -* Validation of data: - * Automatic and clear errors when the data is invalid. - * Validation even for deeply nested JSON objects. -* Conversion of input data: coming from the network to Python data and types. Reading from: - * JSON. - * Path parameters. - * Query parameters. - * Cookies. - * Headers. - * Forms. - * Files. -* Conversion of output data: converting from Python data and types to network data (as JSON): - * Convert Python types (`str`, `int`, `float`, `bool`, `list`, etc). - * `datetime` objects. - * `UUID` objects. - * Database models. - * ...and many more. -* Automatic interactive API documentation, including 2 alternative user interfaces: - * Swagger UI. - * ReDoc. - ---- - -Coming back to the previous code example, **FastAPI** will: - -* Validate that there is an `item_id` in the path for `GET` and `PUT` requests. -* Validate that the `item_id` is of type `int` for `GET` and `PUT` requests. - * If it is not, the client will see a useful, clear error. -* Check if there is an optional query parameter named `q` (as in `http://127.0.0.1:8000/items/foo?q=somequery`) for `GET` requests. - * As the `q` parameter is declared with `= None`, it is optional. - * Without the `None` it would be required (as is the body in the case with `PUT`). -* For `PUT` requests to `/items/{item_id}`, Read the body as JSON: - * Check that it has a required attribute `name` that should be a `str`. - * Check that it has a required attribute `price` that has to be a `float`. - * Check that it has an optional attribute `is_offer`, that should be a `bool`, if present. - * All this would also work for deeply nested JSON objects. -* Convert from and to JSON automatically. -* Document everything with OpenAPI, that can be used by: - * Interactive documentation systems. - * Automatic client code generation systems, for many languages. -* Provide 2 interactive documentation web interfaces directly. - ---- - -We just scratched the surface, but you already get the idea of how it all works. - -Try changing the line with: - -```Python - return {"item_name": item.name, "item_id": item_id} -``` - -...from: - -```Python - ... "item_name": item.name ... -``` - -...to: - -```Python - ... "item_price": item.price ... -``` - -...and see how your editor will auto-complete the attributes and know their types: - -![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) - -For a more complete example including more features, see the Tutorial - User Guide. - -**Spoiler alert**: the tutorial - user guide includes: - -* Declaration of **parameters** from other different places as: **headers**, **cookies**, **form fields** and **files**. -* How to set **validation constraints** as `maximum_length` or `regex`. -* A very powerful and easy to use **Dependency Injection** system. -* Security and authentication, including support for **OAuth2** with **JWT tokens** and **HTTP Basic** auth. -* More advanced (but equally easy) techniques for declaring **deeply nested JSON models** (thanks to Pydantic). -* Many extra features (thanks to Starlette) as: - * **WebSockets** - * **GraphQL** - * extremely easy tests based on HTTPX and `pytest` - * **CORS** - * **Cookie Sessions** - * ...and more. - -## Performance - -Independent TechEmpower benchmarks show **FastAPI** applications running under Uvicorn as one of the fastest Python frameworks available, only below Starlette and Uvicorn themselves (used internally by FastAPI). (*) - -To understand more about it, see the section Benchmarks. - -## Optional Dependencies - -Used by Pydantic: - -* email_validator - for email validation. - -Used by Starlette: - -* httpx - Required if you want to use the `TestClient`. -* jinja2 - Required if you want to use the default template configuration. -* python-multipart - Required if you want to support form "parsing", with `request.form()`. -* itsdangerous - Required for `SessionMiddleware` support. -* pyyaml - Required for Starlette's `SchemaGenerator` support (you probably don't need it with FastAPI). -* graphene - Required for `GraphQLApp` support. -* ujson - Required if you want to use `UJSONResponse`. - -Used by FastAPI / Starlette: - -* uvicorn - for the server that loads and serves your application. -* orjson - Required if you want to use `ORJSONResponse`. - -You can install all of these with `pip install fastapi[all]`. - -## License - -This project is licensed under the terms of the MIT license. diff --git a/docs/tr/docs/index.md b/docs/tr/docs/index.md index 2339337f3..e74efbc2f 100644 --- a/docs/tr/docs/index.md +++ b/docs/tr/docs/index.md @@ -1,7 +1,3 @@ - -{!../../../docs/missing-translation.md!} - -

FastAPI

From dffca555ff69fa445c7435809352ba28333f8b7b Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 27 Jun 2023 01:06:48 +0000 Subject: [PATCH 25/58] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 190526bd1..522ee253c 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#9259](https://github.com/tiangolo/fastapi/pull/9259) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * 🌐 Add Persian translation for `docs/fa/docs/advanced/sub-applications.md`. PR [#9692](https://github.com/tiangolo/fastapi/pull/9692) by [@mojtabapaso](https://github.com/mojtabapaso). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/response-model.md`. PR [#9675](https://github.com/tiangolo/fastapi/pull/9675) by [@glsglsgls](https://github.com/glsglsgls). * 📝 Fix form for the FastAPI and friends newsletter. PR [#9749](https://github.com/tiangolo/fastapi/pull/9749) by [@tiangolo](https://github.com/tiangolo). From 6c143b930dfe66c2a83073e801296ea651126ca1 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 27 Jun 2023 01:07:03 +0000 Subject: [PATCH 26/58] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 522ee253c..f8ad8d179 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔥 Remove missing translation dummy pages, no longer necessary. PR [#9751](https://github.com/tiangolo/fastapi/pull/9751) by [@tiangolo](https://github.com/tiangolo). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#9259](https://github.com/tiangolo/fastapi/pull/9259) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * 🌐 Add Persian translation for `docs/fa/docs/advanced/sub-applications.md`. PR [#9692](https://github.com/tiangolo/fastapi/pull/9692) by [@mojtabapaso](https://github.com/mojtabapaso). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/response-model.md`. PR [#9675](https://github.com/tiangolo/fastapi/pull/9675) by [@glsglsgls](https://github.com/glsglsgls). From 9debdc97ef854aef1911059ad8d89c54d9661453 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 27 Jun 2023 03:08:43 +0200 Subject: [PATCH 27/58] =?UTF-8?q?=E2=AC=86=20Bump=20mkdocs-material=20from?= =?UTF-8?q?=209.1.16=20to=209.1.17=20(#9746)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.1.16 to 9.1.17. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.1.16...9.1.17) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index 2c5f71ec0..df60ca4df 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,6 +1,6 @@ -e . mkdocs==1.4.3 -mkdocs-material==9.1.16 +mkdocs-material==9.1.17 mdx-include >=1.4.1,<2.0.0 mkdocs-markdownextradata-plugin >=0.1.7,<0.3.0 typer-cli >=0.0.13,<0.0.14 From 706d74b6ad60ee773b8705ba7d15636369d8b4c8 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 27 Jun 2023 01:10:40 +0000 Subject: [PATCH 28/58] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index f8ad8d179..f14ca10f3 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Bump mkdocs-material from 9.1.16 to 9.1.17. PR [#9746](https://github.com/tiangolo/fastapi/pull/9746) by [@dependabot[bot]](https://github.com/apps/dependabot). * 🔥 Remove missing translation dummy pages, no longer necessary. PR [#9751](https://github.com/tiangolo/fastapi/pull/9751) by [@tiangolo](https://github.com/tiangolo). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#9259](https://github.com/tiangolo/fastapi/pull/9259) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * 🌐 Add Persian translation for `docs/fa/docs/advanced/sub-applications.md`. PR [#9692](https://github.com/tiangolo/fastapi/pull/9692) by [@mojtabapaso](https://github.com/mojtabapaso). From 782b1c49a9eeed34004b44e5d3b67f3315392c68 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 27 Jun 2023 03:13:10 +0200 Subject: [PATCH 29/58] =?UTF-8?q?=E2=AC=86=20Update=20httpx=20requirement?= =?UTF-8?q?=20from=20<0.24.0,>=3D0.23.0=20to=20>=3D0.23.0,<0.25.0=20(#9724?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updates the requirements on [httpx](https://github.com/encode/httpx) to permit the latest version. - [Release notes](https://github.com/encode/httpx/releases) - [Changelog](https://github.com/encode/httpx/blob/master/CHANGELOG.md) - [Commits](https://github.com/encode/httpx/compare/0.23.0...0.24.1) --- updated-dependencies: - dependency-name: httpx dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index 5cedde84d..4b34fcc2c 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -4,7 +4,7 @@ coverage[toml] >= 6.5.0,< 8.0 mypy ==1.4.0 ruff ==0.0.275 black == 23.3.0 -httpx >=0.23.0,<0.24.0 +httpx >=0.23.0,<0.25.0 email_validator >=1.1.1,<2.0.0 # TODO: once removing databases from tutorial, upgrade SQLAlchemy # probably when including SQLModel From d409c05d6ff3411640e92b0d39a9fb2b3f1cbea7 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 27 Jun 2023 01:14:01 +0000 Subject: [PATCH 30/58] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index f14ca10f3..808caf2d4 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Update httpx requirement from <0.24.0,>=0.23.0 to >=0.23.0,<0.25.0. PR [#9724](https://github.com/tiangolo/fastapi/pull/9724) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump mkdocs-material from 9.1.16 to 9.1.17. PR [#9746](https://github.com/tiangolo/fastapi/pull/9746) by [@dependabot[bot]](https://github.com/apps/dependabot). * 🔥 Remove missing translation dummy pages, no longer necessary. PR [#9751](https://github.com/tiangolo/fastapi/pull/9751) by [@tiangolo](https://github.com/tiangolo). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#9259](https://github.com/tiangolo/fastapi/pull/9259) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). From 1f21b16e03260d6935c73fa609b7017357abaf34 Mon Sep 17 00:00:00 2001 From: Carson Crane Date: Wed, 28 Jun 2023 09:39:10 -0700 Subject: [PATCH 31/58] =?UTF-8?q?=E2=9C=A8=20Add=20support=20for=20`deque`?= =?UTF-8?q?=20objects=20and=20children=20in=20`jsonable=5Fencoder`=20(#943?= =?UTF-8?q?3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- fastapi/encoders.py | 4 ++-- tests/test_jsonable_encoder.py | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/fastapi/encoders.py b/fastapi/encoders.py index 2f95bcbf6..94f41bfa1 100644 --- a/fastapi/encoders.py +++ b/fastapi/encoders.py @@ -1,5 +1,5 @@ import dataclasses -from collections import defaultdict +from collections import defaultdict, deque from enum import Enum from pathlib import PurePath from types import GeneratorType @@ -124,7 +124,7 @@ def jsonable_encoder( ) encoded_dict[encoded_key] = encoded_value return encoded_dict - if isinstance(obj, (list, set, frozenset, GeneratorType, tuple)): + if isinstance(obj, (list, set, frozenset, GeneratorType, tuple, deque)): encoded_list = [] for item in obj: encoded_list.append( diff --git a/tests/test_jsonable_encoder.py b/tests/test_jsonable_encoder.py index f4fdcf601..1f43c33c7 100644 --- a/tests/test_jsonable_encoder.py +++ b/tests/test_jsonable_encoder.py @@ -1,3 +1,4 @@ +from collections import deque from dataclasses import dataclass from datetime import datetime, timezone from enum import Enum @@ -237,3 +238,12 @@ def test_encode_model_with_path(model_with_path): def test_encode_root(): model = ModelWithRoot(__root__="Foo") assert jsonable_encoder(model) == "Foo" + + +def test_encode_deque_encodes_child_models(): + class Model(BaseModel): + test: str + + dq = deque([Model(test="test")]) + + assert jsonable_encoder(dq)[0]["test"] == "test" From 0f390cd4b57b47fb53989094dc87d18ce337058a Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 28 Jun 2023 16:39:44 +0000 Subject: [PATCH 32/58] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 808caf2d4..5565b8022 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✨ Add support for `deque` objects and children in `jsonable_encoder`. PR [#9433](https://github.com/tiangolo/fastapi/pull/9433) by [@cranium](https://github.com/cranium). * ⬆ Update httpx requirement from <0.24.0,>=0.23.0 to >=0.23.0,<0.25.0. PR [#9724](https://github.com/tiangolo/fastapi/pull/9724) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump mkdocs-material from 9.1.16 to 9.1.17. PR [#9746](https://github.com/tiangolo/fastapi/pull/9746) by [@dependabot[bot]](https://github.com/apps/dependabot). * 🔥 Remove missing translation dummy pages, no longer necessary. PR [#9751](https://github.com/tiangolo/fastapi/pull/9751) by [@tiangolo](https://github.com/tiangolo). From 0a8423d792bda91ab74c9c8b0021c9a9388cbd46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 30 Jun 2023 18:23:02 +0200 Subject: [PATCH 33/58] =?UTF-8?q?=F0=9F=94=A8=20Enable=20linenums=20in=20M?= =?UTF-8?q?kDocs=20Material=20during=20local=20live=20development=20to=20s?= =?UTF-8?q?implify=20highlighting=20code=20(#9769)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/mkdocs.maybe-insiders.yml | 3 +++ docs/en/mkdocs.yml | 20 ++++++++++---------- scripts/docs.py | 2 ++ 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/docs/en/mkdocs.maybe-insiders.yml b/docs/en/mkdocs.maybe-insiders.yml index 8e6271334..37fd9338e 100644 --- a/docs/en/mkdocs.maybe-insiders.yml +++ b/docs/en/mkdocs.maybe-insiders.yml @@ -1,3 +1,6 @@ # Define this here and not in the main mkdocs.yml file because that one is auto # updated and written, and the script would remove the env var INHERIT: !ENV [INSIDERS_FILE, '../en/mkdocs.no-insiders.yml'] +markdown_extensions: + pymdownx.highlight: + linenums: !ENV [LINENUMS, false] diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index 21300b9dc..64dc40372 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -169,24 +169,24 @@ nav: - contributing.md - release-notes.md markdown_extensions: -- toc: + toc: permalink: true -- markdown.extensions.codehilite: + markdown.extensions.codehilite: guess_lang: false -- mdx_include: + mdx_include: base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: + admonition: + codehilite: + extra: + pymdownx.superfences: custom_fences: - name: mermaid class: mermaid format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: + pymdownx.tabbed: alternate_style: true -- attr_list -- md_in_html + attr_list: + md_in_html: extra: analytics: provider: google diff --git a/scripts/docs.py b/scripts/docs.py index 20838be6a..968dd9a3d 100644 --- a/scripts/docs.py +++ b/scripts/docs.py @@ -258,6 +258,8 @@ def live( Takes an optional LANG argument with the name of the language to serve, by default en. """ + # Enable line numbers during local development to make it easier to highlight + os.environ["LINENUMS"] = "true" if lang is None: lang = "en" lang_path: Path = docs_path / lang From 02fc9e8a63361fa25e8787d5a0da069890da62c6 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 30 Jun 2023 16:23:36 +0000 Subject: [PATCH 34/58] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 5565b8022..fca7502cc 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔨 Enable linenums in MkDocs Material during local live development to simplify highlighting code. PR [#9769](https://github.com/tiangolo/fastapi/pull/9769) by [@tiangolo](https://github.com/tiangolo). * ✨ Add support for `deque` objects and children in `jsonable_encoder`. PR [#9433](https://github.com/tiangolo/fastapi/pull/9433) by [@cranium](https://github.com/cranium). * ⬆ Update httpx requirement from <0.24.0,>=0.23.0 to >=0.23.0,<0.25.0. PR [#9724](https://github.com/tiangolo/fastapi/pull/9724) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump mkdocs-material from 9.1.16 to 9.1.17. PR [#9746](https://github.com/tiangolo/fastapi/pull/9746) by [@dependabot[bot]](https://github.com/apps/dependabot). From 7dad5a820bfe99e49a6cfaefde537e09644c2c2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 30 Jun 2023 20:25:16 +0200 Subject: [PATCH 35/58] =?UTF-8?q?=E2=9C=A8=20Add=20support=20for=20OpenAPI?= =?UTF-8?q?=203.1.0=20(#9770)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ Update OpenAPI models for JSON Schema 2020-12 and OpenAPI 3.1.0 * ✨ Add support for summary and webhooks * ✨ Update JSON Schema for UploadFiles * ⏪️ Revert making paths optional, to ensure always correctness * ⏪️ Keep UploadFile as format: binary for compatibility with the rest of Pydantic bytes fields in v1 * ✨ Update version of OpenAPI generated to 3.1.0 * ✨ Update the version of Swagger UI * 📝 Update docs about extending OpenAPI * 📝 Update docs and links to refer to OpenAPI 3.1.0 * ✨ Update logic for handling webhooks * ♻️ Update parameter functions and classes, deprecate example and make examples the main field * ✅ Update tests for OpenAPI 3.1.0 * 📝 Update examples for OpenAPI metadata * ✅ Add and update tests for OpenAPI metadata * 📝 Add source example for webhooks * 📝 Update docs for metadata * 📝 Update docs for Schema extra * 📝 Add docs for webhooks * 🔧 Add webhooks docs to MkDocs * ✅ Update tests for extending OpenAPI * ✅ Add tests for webhooks * ♻️ Refactor generation of OpenAPI and JSON Schema with params * 📝 Update source examples for field examples * ✅ Update tests for examples * ➕ Make sure the minimum version of typing-extensions installed has deprecated() (already a dependency of Pydantic) * ✏️ Fix typo in Webhooks example code * 🔥 Remove commented out code of removed nullable field * 🗑️ Add deprecation warnings for example argument * ✅ Update tests to check for deprecation warnings * ✅ Add test for webhooks with security schemes, for coverage * 🍱 Update image for metadata, with new summary * 🍱 Add docs image for Webhooks * 📝 Update docs for webhooks, add docs UI image --- docs/en/docs/advanced/additional-responses.md | 4 +- docs/en/docs/advanced/behind-a-proxy.md | 4 +- docs/en/docs/advanced/extending-openapi.md | 16 +- docs/en/docs/advanced/openapi-callbacks.md | 4 +- docs/en/docs/advanced/openapi-webhooks.md | 51 ++ .../path-operation-advanced-configuration.md | 2 +- .../en/docs/img/tutorial/metadata/image01.png | Bin 90062 -> 86437 bytes .../img/tutorial/openapi-webhooks/image01.png | Bin 0 -> 86925 bytes docs/en/docs/tutorial/first-steps.md | 2 +- docs/en/docs/tutorial/metadata.md | 15 +- docs/en/docs/tutorial/path-params.md | 2 +- docs/en/docs/tutorial/schema-extra-example.md | 118 ++-- docs/en/mkdocs.yml | 1 + docs_src/extending_openapi/tutorial001.py | 3 +- docs_src/metadata/tutorial001.py | 1 + docs_src/metadata/tutorial001_1.py | 38 ++ docs_src/openapi_webhooks/tutorial001.py | 25 + docs_src/schema_extra_example/tutorial001.py | 14 +- .../schema_extra_example/tutorial001_py310.py | 14 +- docs_src/schema_extra_example/tutorial002.py | 8 +- .../schema_extra_example/tutorial002_py310.py | 8 +- docs_src/schema_extra_example/tutorial003.py | 14 +- .../schema_extra_example/tutorial003_an.py | 14 +- .../tutorial003_an_py310.py | 14 +- .../tutorial003_an_py39.py | 14 +- .../schema_extra_example/tutorial003_py310.py | 14 +- docs_src/schema_extra_example/tutorial004.py | 10 +- .../schema_extra_example/tutorial004_an.py | 10 +- .../tutorial004_an_py310.py | 10 +- .../tutorial004_an_py39.py | 10 +- .../schema_extra_example/tutorial004_py310.py | 10 +- fastapi/applications.py | 8 +- fastapi/openapi/docs.py | 4 +- fastapi/openapi/models.py | 93 ++- fastapi/openapi/utils.py | 38 +- fastapi/param_functions.py | 73 ++- fastapi/params.py | 108 +++- pyproject.toml | 1 + tests/test_additional_properties.py | 2 +- tests/test_additional_response_extra.py | 2 +- tests/test_additional_responses_bad.py | 2 +- ...onal_responses_custom_model_in_callback.py | 2 +- ...tional_responses_custom_validationerror.py | 2 +- ...ional_responses_default_validationerror.py | 2 +- ...est_additional_responses_response_class.py | 2 +- tests/test_additional_responses_router.py | 2 +- tests/test_annotated.py | 2 +- tests/test_application.py | 2 +- tests/test_custom_route_class.py | 2 +- tests/test_dependency_duplicates.py | 2 +- tests/test_deprecated_openapi_prefix.py | 2 +- tests/test_duplicate_models_openapi.py | 2 +- tests/test_enforce_once_required_parameter.py | 2 +- tests/test_extra_routes.py | 2 +- tests/test_filter_pydantic_sub_model.py | 2 +- tests/test_generate_unique_id_function.py | 14 +- tests/test_get_request_body.py | 2 +- .../test_include_router_defaults_overrides.py | 2 +- .../test_modules_same_name_body/test_main.py | 2 +- tests/test_multi_body_errors.py | 2 +- tests/test_multi_query_errors.py | 2 +- .../test_openapi_query_parameter_extension.py | 2 +- tests/test_openapi_route_extensions.py | 2 +- tests/test_openapi_servers.py | 2 +- tests/test_param_in_path_and_dependency.py | 2 +- tests/test_param_include_in_schema.py | 2 +- tests/test_put_no_body.py | 2 +- tests/test_repeated_dependency_schema.py | 2 +- tests/test_repeated_parameter_alias.py | 2 +- tests/test_reponse_set_reponse_code_empty.py | 2 +- ...test_request_body_parameters_media_type.py | 2 +- tests/test_response_by_alias.py | 2 +- tests/test_response_class_no_mediatype.py | 2 +- tests/test_response_code_no_body.py | 2 +- ...est_response_model_as_return_annotation.py | 2 +- tests/test_response_model_sub_types.py | 2 +- tests/test_schema_extra_examples.py | 572 ++++++++---------- tests/test_security_api_key_cookie.py | 2 +- ...est_security_api_key_cookie_description.py | 2 +- .../test_security_api_key_cookie_optional.py | 2 +- tests/test_security_api_key_header.py | 2 +- ...est_security_api_key_header_description.py | 2 +- .../test_security_api_key_header_optional.py | 2 +- tests/test_security_api_key_query.py | 2 +- ...test_security_api_key_query_description.py | 2 +- tests/test_security_api_key_query_optional.py | 2 +- tests/test_security_http_base.py | 2 +- tests/test_security_http_base_description.py | 2 +- tests/test_security_http_base_optional.py | 2 +- tests/test_security_http_basic_optional.py | 2 +- tests/test_security_http_basic_realm.py | 2 +- ...t_security_http_basic_realm_description.py | 2 +- tests/test_security_http_bearer.py | 2 +- .../test_security_http_bearer_description.py | 2 +- tests/test_security_http_bearer_optional.py | 2 +- tests/test_security_http_digest.py | 2 +- .../test_security_http_digest_description.py | 2 +- tests/test_security_http_digest_optional.py | 2 +- tests/test_security_oauth2.py | 2 +- ...curity_oauth2_authorization_code_bearer.py | 2 +- ...2_authorization_code_bearer_description.py | 2 +- tests/test_security_oauth2_optional.py | 2 +- ...st_security_oauth2_optional_description.py | 2 +- ...ecurity_oauth2_password_bearer_optional.py | 2 +- ...h2_password_bearer_optional_description.py | 2 +- tests/test_security_openid_connect.py | 2 +- ...est_security_openid_connect_description.py | 2 +- .../test_security_openid_connect_optional.py | 2 +- tests/test_starlette_exception.py | 2 +- tests/test_sub_callbacks.py | 2 +- tests/test_tuples.py | 2 +- .../test_tutorial001.py | 2 +- .../test_tutorial002.py | 2 +- .../test_tutorial003.py | 2 +- .../test_tutorial004.py | 2 +- .../test_tutorial001.py | 2 +- .../test_behind_a_proxy/test_tutorial001.py | 2 +- .../test_behind_a_proxy/test_tutorial002.py | 2 +- .../test_behind_a_proxy/test_tutorial003.py | 2 +- .../test_behind_a_proxy/test_tutorial004.py | 2 +- .../test_bigger_applications/test_main.py | 2 +- .../test_bigger_applications/test_main_an.py | 2 +- .../test_main_an_py39.py | 2 +- .../test_body/test_tutorial001.py | 2 +- .../test_body/test_tutorial001_py310.py | 2 +- .../test_body_fields/test_tutorial001.py | 2 +- .../test_body_fields/test_tutorial001_an.py | 2 +- .../test_tutorial001_an_py310.py | 2 +- .../test_tutorial001_an_py39.py | 2 +- .../test_tutorial001_py310.py | 2 +- .../test_tutorial001.py | 2 +- .../test_tutorial001_an.py | 2 +- .../test_tutorial001_an_py310.py | 2 +- .../test_tutorial001_an_py39.py | 2 +- .../test_tutorial001_py310.py | 2 +- .../test_tutorial003.py | 2 +- .../test_tutorial003_an.py | 2 +- .../test_tutorial003_an_py310.py | 2 +- .../test_tutorial003_an_py39.py | 2 +- .../test_tutorial003_py310.py | 2 +- .../test_tutorial009.py | 2 +- .../test_tutorial009_py39.py | 2 +- .../test_body_updates/test_tutorial001.py | 2 +- .../test_tutorial001_py310.py | 2 +- .../test_tutorial001_py39.py | 2 +- .../test_tutorial001.py | 2 +- .../test_cookie_params/test_tutorial001.py | 2 +- .../test_cookie_params/test_tutorial001_an.py | 2 +- .../test_tutorial001_an_py310.py | 2 +- .../test_tutorial001_an_py39.py | 2 +- .../test_tutorial001_py310.py | 2 +- .../test_custom_response/test_tutorial001.py | 2 +- .../test_custom_response/test_tutorial001b.py | 2 +- .../test_custom_response/test_tutorial004.py | 2 +- .../test_custom_response/test_tutorial005.py | 2 +- .../test_custom_response/test_tutorial006.py | 2 +- .../test_custom_response/test_tutorial006b.py | 2 +- .../test_custom_response/test_tutorial006c.py | 2 +- .../test_dataclasses/test_tutorial001.py | 2 +- .../test_dataclasses/test_tutorial002.py | 2 +- .../test_dataclasses/test_tutorial003.py | 2 +- .../test_dependencies/test_tutorial001.py | 2 +- .../test_dependencies/test_tutorial001_an.py | 2 +- .../test_tutorial001_an_py310.py | 2 +- .../test_tutorial001_an_py39.py | 2 +- .../test_tutorial001_py310.py | 2 +- .../test_dependencies/test_tutorial004.py | 2 +- .../test_dependencies/test_tutorial004_an.py | 2 +- .../test_tutorial004_an_py310.py | 2 +- .../test_tutorial004_an_py39.py | 2 +- .../test_tutorial004_py310.py | 2 +- .../test_dependencies/test_tutorial006.py | 2 +- .../test_dependencies/test_tutorial006_an.py | 2 +- .../test_tutorial006_an_py39.py | 2 +- .../test_dependencies/test_tutorial012.py | 2 +- .../test_dependencies/test_tutorial012_an.py | 2 +- .../test_tutorial012_an_py39.py | 2 +- .../test_events/test_tutorial001.py | 2 +- .../test_events/test_tutorial002.py | 2 +- .../test_events/test_tutorial003.py | 2 +- .../test_tutorial001.py | 5 +- .../test_extra_data_types/test_tutorial001.py | 2 +- .../test_tutorial001_an.py | 2 +- .../test_tutorial001_an_py310.py | 2 +- .../test_tutorial001_an_py39.py | 2 +- .../test_tutorial001_py310.py | 2 +- .../test_extra_models/test_tutorial003.py | 2 +- .../test_tutorial003_py310.py | 2 +- .../test_extra_models/test_tutorial004.py | 2 +- .../test_tutorial004_py39.py | 2 +- .../test_extra_models/test_tutorial005.py | 2 +- .../test_tutorial005_py39.py | 2 +- .../test_first_steps/test_tutorial001.py | 2 +- .../test_generate_clients/test_tutorial003.py | 2 +- .../test_handling_errors/test_tutorial001.py | 2 +- .../test_handling_errors/test_tutorial002.py | 2 +- .../test_handling_errors/test_tutorial003.py | 2 +- .../test_handling_errors/test_tutorial004.py | 2 +- .../test_handling_errors/test_tutorial005.py | 2 +- .../test_handling_errors/test_tutorial006.py | 2 +- .../test_header_params/test_tutorial001.py | 2 +- .../test_header_params/test_tutorial001_an.py | 2 +- .../test_tutorial001_an_py310.py | 2 +- .../test_tutorial001_py310.py | 2 +- .../test_header_params/test_tutorial002.py | 2 +- .../test_header_params/test_tutorial002_an.py | 2 +- .../test_tutorial002_an_py310.py | 2 +- .../test_tutorial002_an_py39.py | 2 +- .../test_tutorial002_py310.py | 2 +- .../test_header_params/test_tutorial003.py | 2 +- .../test_header_params/test_tutorial003_an.py | 2 +- .../test_tutorial003_an_py310.py | 2 +- .../test_tutorial003_an_py39.py | 2 +- .../test_tutorial003_py310.py | 2 +- .../test_metadata/test_tutorial001.py | 3 +- .../test_metadata/test_tutorial001_1.py | 49 ++ .../test_metadata/test_tutorial004.py | 2 +- .../test_tutorial001.py | 2 +- .../test_openapi_webhooks/__init__.py | 0 .../test_openapi_webhooks/test_tutorial001.py | 117 ++++ .../test_tutorial001.py | 2 +- .../test_tutorial002.py | 2 +- .../test_tutorial003.py | 2 +- .../test_tutorial004.py | 2 +- .../test_tutorial005.py | 2 +- .../test_tutorial006.py | 2 +- .../test_tutorial007.py | 2 +- .../test_tutorial002b.py | 2 +- .../test_tutorial005.py | 2 +- .../test_tutorial005_py310.py | 2 +- .../test_tutorial005_py39.py | 2 +- .../test_tutorial006.py | 2 +- .../test_path_params/test_tutorial004.py | 2 +- .../test_path_params/test_tutorial005.py | 2 +- .../test_query_params/test_tutorial005.py | 2 +- .../test_query_params/test_tutorial006.py | 2 +- .../test_tutorial006_py310.py | 2 +- .../test_tutorial010.py | 2 +- .../test_tutorial010_an.py | 2 +- .../test_tutorial010_an_py310.py | 2 +- .../test_tutorial010_an_py39.py | 2 +- .../test_tutorial010_py310.py | 2 +- .../test_tutorial011.py | 2 +- .../test_tutorial011_an.py | 2 +- .../test_tutorial011_an_py310.py | 2 +- .../test_tutorial011_an_py39.py | 2 +- .../test_tutorial011_py310.py | 2 +- .../test_tutorial011_py39.py | 2 +- .../test_tutorial012.py | 2 +- .../test_tutorial012_an.py | 2 +- .../test_tutorial012_an_py39.py | 2 +- .../test_tutorial012_py39.py | 2 +- .../test_tutorial013.py | 2 +- .../test_tutorial013_an.py | 2 +- .../test_tutorial013_an_py39.py | 2 +- .../test_tutorial014.py | 2 +- .../test_tutorial014_an.py | 2 +- .../test_tutorial014_an_py310.py | 2 +- .../test_tutorial014_an_py39.py | 2 +- .../test_tutorial014_py310.py | 2 +- .../test_request_files/test_tutorial001.py | 2 +- .../test_request_files/test_tutorial001_02.py | 2 +- .../test_tutorial001_02_an.py | 2 +- .../test_tutorial001_02_an_py310.py | 2 +- .../test_tutorial001_02_an_py39.py | 2 +- .../test_tutorial001_02_py310.py | 2 +- .../test_request_files/test_tutorial001_03.py | 2 +- .../test_tutorial001_03_an.py | 2 +- .../test_tutorial001_03_an_py39.py | 2 +- .../test_request_files/test_tutorial001_an.py | 2 +- .../test_tutorial001_an_py39.py | 2 +- .../test_request_files/test_tutorial002.py | 2 +- .../test_request_files/test_tutorial002_an.py | 2 +- .../test_tutorial002_an_py39.py | 2 +- .../test_tutorial002_py39.py | 2 +- .../test_request_files/test_tutorial003.py | 2 +- .../test_request_files/test_tutorial003_an.py | 2 +- .../test_tutorial003_an_py39.py | 2 +- .../test_tutorial003_py39.py | 2 +- .../test_request_forms/test_tutorial001.py | 2 +- .../test_request_forms/test_tutorial001_an.py | 2 +- .../test_tutorial001_an_py39.py | 2 +- .../test_tutorial001.py | 2 +- .../test_tutorial001_an.py | 2 +- .../test_tutorial001_an_py39.py | 2 +- .../test_response_model/test_tutorial003.py | 2 +- .../test_tutorial003_01.py | 2 +- .../test_tutorial003_01_py310.py | 2 +- .../test_tutorial003_02.py | 2 +- .../test_tutorial003_03.py | 2 +- .../test_tutorial003_05.py | 2 +- .../test_tutorial003_05_py310.py | 2 +- .../test_tutorial003_py310.py | 2 +- .../test_response_model/test_tutorial004.py | 2 +- .../test_tutorial004_py310.py | 2 +- .../test_tutorial004_py39.py | 2 +- .../test_response_model/test_tutorial005.py | 2 +- .../test_tutorial005_py310.py | 2 +- .../test_response_model/test_tutorial006.py | 2 +- .../test_tutorial006_py310.py | 2 +- .../test_tutorial004.py | 51 +- .../test_tutorial004_an.py | 51 +- .../test_tutorial004_an_py310.py | 51 +- .../test_tutorial004_an_py39.py | 51 +- .../test_tutorial004_py310.py | 51 +- .../test_security/test_tutorial001.py | 2 +- .../test_security/test_tutorial001_an.py | 2 +- .../test_security/test_tutorial001_an_py39.py | 2 +- .../test_security/test_tutorial003.py | 2 +- .../test_security/test_tutorial003_an.py | 2 +- .../test_tutorial003_an_py310.py | 2 +- .../test_security/test_tutorial003_an_py39.py | 2 +- .../test_security/test_tutorial003_py310.py | 2 +- .../test_security/test_tutorial005.py | 2 +- .../test_security/test_tutorial005_an.py | 2 +- .../test_tutorial005_an_py310.py | 2 +- .../test_security/test_tutorial005_an_py39.py | 2 +- .../test_security/test_tutorial005_py310.py | 2 +- .../test_security/test_tutorial005_py39.py | 2 +- .../test_security/test_tutorial006.py | 2 +- .../test_security/test_tutorial006_an.py | 2 +- .../test_security/test_tutorial006_an_py39.py | 2 +- .../test_sql_databases/test_sql_databases.py | 2 +- .../test_sql_databases_middleware.py | 2 +- .../test_sql_databases_middleware_py310.py | 2 +- .../test_sql_databases_middleware_py39.py | 2 +- .../test_sql_databases_py310.py | 2 +- .../test_sql_databases_py39.py | 2 +- .../test_sql_databases_peewee.py | 2 +- .../test_sub_applications/test_tutorial001.py | 4 +- tests/test_tutorial/test_testing/test_main.py | 2 +- .../test_testing/test_tutorial001.py | 2 +- tests/test_union_body.py | 2 +- tests/test_union_inherited_body.py | 2 +- tests/test_webhooks_security.py | 126 ++++ 335 files changed, 1564 insertions(+), 922 deletions(-) create mode 100644 docs/en/docs/advanced/openapi-webhooks.md create mode 100644 docs/en/docs/img/tutorial/openapi-webhooks/image01.png create mode 100644 docs_src/metadata/tutorial001_1.py create mode 100644 docs_src/openapi_webhooks/tutorial001.py create mode 100644 tests/test_tutorial/test_metadata/test_tutorial001_1.py create mode 100644 tests/test_tutorial/test_openapi_webhooks/__init__.py create mode 100644 tests/test_tutorial/test_openapi_webhooks/test_tutorial001.py create mode 100644 tests/test_webhooks_security.py diff --git a/docs/en/docs/advanced/additional-responses.md b/docs/en/docs/advanced/additional-responses.md index dca5f6a98..624036ce9 100644 --- a/docs/en/docs/advanced/additional-responses.md +++ b/docs/en/docs/advanced/additional-responses.md @@ -236,5 +236,5 @@ For example: To see what exactly you can include in the responses, you can check these sections in the OpenAPI specification: -* OpenAPI Responses Object, it includes the `Response Object`. -* OpenAPI Response Object, you can include anything from this directly in each response inside your `responses` parameter. Including `description`, `headers`, `content` (inside of this is that you declare different media types and JSON Schemas), and `links`. +* OpenAPI Responses Object, it includes the `Response Object`. +* OpenAPI Response Object, you can include anything from this directly in each response inside your `responses` parameter. Including `description`, `headers`, `content` (inside of this is that you declare different media types and JSON Schemas), and `links`. diff --git a/docs/en/docs/advanced/behind-a-proxy.md b/docs/en/docs/advanced/behind-a-proxy.md index 03198851a..e7af77f3d 100644 --- a/docs/en/docs/advanced/behind-a-proxy.md +++ b/docs/en/docs/advanced/behind-a-proxy.md @@ -46,7 +46,7 @@ The docs UI would also need the OpenAPI schema to declare that this API `server` ```JSON hl_lines="4-8" { - "openapi": "3.0.2", + "openapi": "3.1.0", // More stuff here "servers": [ { @@ -298,7 +298,7 @@ Will generate an OpenAPI schema like: ```JSON hl_lines="5-7" { - "openapi": "3.0.2", + "openapi": "3.1.0", // More stuff here "servers": [ { diff --git a/docs/en/docs/advanced/extending-openapi.md b/docs/en/docs/advanced/extending-openapi.md index 36619696b..c47f939af 100644 --- a/docs/en/docs/advanced/extending-openapi.md +++ b/docs/en/docs/advanced/extending-openapi.md @@ -29,10 +29,14 @@ And that function `get_openapi()` receives as parameters: * `title`: The OpenAPI title, shown in the docs. * `version`: The version of your API, e.g. `2.5.0`. -* `openapi_version`: The version of the OpenAPI specification used. By default, the latest: `3.0.2`. -* `description`: The description of your API. +* `openapi_version`: The version of the OpenAPI specification used. By default, the latest: `3.1.0`. +* `summary`: A short summary of the API. +* `description`: The description of your API, this can include markdown and will be shown in the docs. * `routes`: A list of routes, these are each of the registered *path operations*. They are taken from `app.routes`. +!!! info + The parameter `summary` is available in OpenAPI 3.1.0 and above, supported by FastAPI 0.99.0 and above. + ## Overriding the defaults Using the information above, you can use the same utility function to generate the OpenAPI schema and override each part that you need. @@ -51,7 +55,7 @@ First, write all your **FastAPI** application as normally: Then, use the same utility function to generate the OpenAPI schema, inside a `custom_openapi()` function: -```Python hl_lines="2 15-20" +```Python hl_lines="2 15-21" {!../../../docs_src/extending_openapi/tutorial001.py!} ``` @@ -59,7 +63,7 @@ Then, use the same utility function to generate the OpenAPI schema, inside a `cu Now you can add the ReDoc extension, adding a custom `x-logo` to the `info` "object" in the OpenAPI schema: -```Python hl_lines="21-23" +```Python hl_lines="22-24" {!../../../docs_src/extending_openapi/tutorial001.py!} ``` @@ -71,7 +75,7 @@ That way, your application won't have to generate the schema every time a user o It will be generated only once, and then the same cached schema will be used for the next requests. -```Python hl_lines="13-14 24-25" +```Python hl_lines="13-14 25-26" {!../../../docs_src/extending_openapi/tutorial001.py!} ``` @@ -79,7 +83,7 @@ It will be generated only once, and then the same cached schema will be used for Now you can replace the `.openapi()` method with your new function. -```Python hl_lines="28" +```Python hl_lines="29" {!../../../docs_src/extending_openapi/tutorial001.py!} ``` diff --git a/docs/en/docs/advanced/openapi-callbacks.md b/docs/en/docs/advanced/openapi-callbacks.md index 71924ce8b..37339eae5 100644 --- a/docs/en/docs/advanced/openapi-callbacks.md +++ b/docs/en/docs/advanced/openapi-callbacks.md @@ -103,11 +103,11 @@ It should look just like a normal FastAPI *path operation*: There are 2 main differences from a normal *path operation*: * It doesn't need to have any actual code, because your app will never call this code. It's only used to document the *external API*. So, the function could just have `pass`. -* The *path* can contain an OpenAPI 3 expression (see more below) where it can use variables with parameters and parts of the original request sent to *your API*. +* The *path* can contain an OpenAPI 3 expression (see more below) where it can use variables with parameters and parts of the original request sent to *your API*. ### The callback path expression -The callback *path* can have an OpenAPI 3 expression that can contain parts of the original request sent to *your API*. +The callback *path* can have an OpenAPI 3 expression that can contain parts of the original request sent to *your API*. In this case, it's the `str`: diff --git a/docs/en/docs/advanced/openapi-webhooks.md b/docs/en/docs/advanced/openapi-webhooks.md new file mode 100644 index 000000000..63cbdc610 --- /dev/null +++ b/docs/en/docs/advanced/openapi-webhooks.md @@ -0,0 +1,51 @@ +# OpenAPI Webhooks + +There are cases where you want to tell your API **users** that your app could call *their* app (sending a request) with some data, normally to **notify** of some type of **event**. + +This means that instead of the normal process of your users sending requests to your API, it's **your API** (or your app) that could **send requests to their system** (to their API, their app). + +This is normally called a **webhook**. + +## Webhooks steps + +The process normally is that **you define** in your code what is the message that you will send, the **body of the request**. + +You also define in some way at which **moments** your app will send those requests or events. + +And **your users** define in some way (for example in a web dashboard somewhere) the **URL** where your app should send those requests. + +All the **logic** about how to register the URLs for webhooks and the code to actually send those requests is up to you. You write it however you want to in **your own code**. + +## Documenting webhooks with **FastAPI** and OpenAPI + +With **FastAPI**, using OpenAPI, you can define the names of these webhooks, the types of HTTP operations that your app can send (e.g. `POST`, `PUT`, etc.) and the request **bodies** that your app would send. + +This can make it a lot easier for your users to **implement their APIs** to receive your **webhook** requests, they might even be able to autogenerate some of their own API code. + +!!! info + Webhooks are available in OpenAPI 3.1.0 and above, supported by FastAPI `0.99.0` and above. + +## An app with webhooks + +When you create a **FastAPI** application, there is a `webhooks` attribute that you can use to define *webhooks*, the same way you would define *path operations*, for example with `@app.webhooks.post()`. + +```Python hl_lines="9-13 36-53" +{!../../../docs_src/openapi_webhooks/tutorial001.py!} +``` + +The webhooks that you define will end up in the **OpenAPI** schema and the automatic **docs UI**. + +!!! info + The `app.webhooks` object is actually just an `APIRouter`, the same type you would use when structuring your app with multiple files. + +Notice that with webhooks you are actually not declaring a *path* (like `/items/`), the text you pass there is just an **identifier** of the webhook (the name of the event), for example in `@app.webhooks.post("new-subscription")`, the webhook name is `new-subscription`. + +This is because it is expected that **your users** would define the actual **URL path** where they want to receive the webhook request in some other way (e.g. a web dashboard). + +### Check the docs + +Now you can start your app with Uvicorn and go to http://127.0.0.1:8000/docs. + +You will see your docs have the normal *path operations* and now also some **webhooks**: + + diff --git a/docs/en/docs/advanced/path-operation-advanced-configuration.md b/docs/en/docs/advanced/path-operation-advanced-configuration.md index a1c902ef2..6d9a5fe70 100644 --- a/docs/en/docs/advanced/path-operation-advanced-configuration.md +++ b/docs/en/docs/advanced/path-operation-advanced-configuration.md @@ -97,7 +97,7 @@ And if you see the resulting OpenAPI (at `/openapi.json` in your API), you will ```JSON hl_lines="22" { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": { "title": "FastAPI", "version": "0.1.0" diff --git a/docs/en/docs/img/tutorial/metadata/image01.png b/docs/en/docs/img/tutorial/metadata/image01.png index b7708a3fd98ad2170d880781896623ec8ed80204..4146a8607b5b4278b9d826c5d9e45ee9ff183ce3 100644 GIT binary patch literal 86437 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 literal 90062 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 diff --git a/docs/en/docs/img/tutorial/openapi-webhooks/image01.png b/docs/en/docs/img/tutorial/openapi-webhooks/image01.png new file mode 100644 index 0000000000000000000000000000000000000000..25ced48186d8395b0406b9a66e75afb17aa83fd5 GIT binary patch literal 86925 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�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 literal 0 HcmV?d00001 diff --git a/docs/en/docs/tutorial/first-steps.md b/docs/en/docs/tutorial/first-steps.md index 6ca5f39eb..cfa159329 100644 --- a/docs/en/docs/tutorial/first-steps.md +++ b/docs/en/docs/tutorial/first-steps.md @@ -99,7 +99,7 @@ It will show a JSON starting with something like: ```JSON { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": { "title": "FastAPI", "version": "0.1.0" diff --git a/docs/en/docs/tutorial/metadata.md b/docs/en/docs/tutorial/metadata.md index cf13e7470..e75b4a0b9 100644 --- a/docs/en/docs/tutorial/metadata.md +++ b/docs/en/docs/tutorial/metadata.md @@ -9,15 +9,16 @@ You can set the following fields that are used in the OpenAPI specification and | Parameter | Type | Description | |------------|------|-------------| | `title` | `str` | The title of the API. | +| `summary` | `str` | A short summary of the API. Available since OpenAPI 3.1.0, FastAPI 0.99.0. | | `description` | `str` | A short description of the API. It can use Markdown. | | `version` | `string` | The version of the API. This is the version of your own application, not of OpenAPI. For example `2.5.0`. | | `terms_of_service` | `str` | A URL to the Terms of Service for the API. If provided, this has to be a URL. | | `contact` | `dict` | The contact information for the exposed API. It can contain several fields.
contact fields
ParameterTypeDescription
namestrThe identifying name of the contact person/organization.
urlstrThe URL pointing to the contact information. MUST be in the format of a URL.
emailstrThe email address of the contact person/organization. MUST be in the format of an email address.
| -| `license_info` | `dict` | The license information for the exposed API. It can contain several fields.
license_info fields
ParameterTypeDescription
namestrREQUIRED (if a license_info is set). The license name used for the API.
urlstrA URL to the license used for the API. MUST be in the format of a URL.
| +| `license_info` | `dict` | The license information for the exposed API. It can contain several fields.
license_info fields
ParameterTypeDescription
namestrREQUIRED (if a license_info is set). The license name used for the API.
identifierstrAn SPDX license expression for the API. The identifier field is mutually exclusive of the url field. Available since OpenAPI 3.1.0, FastAPI 0.99.0.
urlstrA URL to the license used for the API. MUST be in the format of a URL.
| You can set them as follows: -```Python hl_lines="3-16 19-31" +```Python hl_lines="3-16 19-32" {!../../../docs_src/metadata/tutorial001.py!} ``` @@ -28,6 +29,16 @@ With this configuration, the automatic API docs would look like: +## License identifier + +Since OpenAPI 3.1.0 and FastAPI 0.99.0, you can also set the `license_info` with an `identifier` instead of a `url`. + +For example: + +```Python hl_lines="31" +{!../../../docs_src/metadata/tutorial001_1.py!} +``` + ## Metadata for tags You can also add additional metadata for the different tags used to group your path operations with the parameter `openapi_tags`. diff --git a/docs/en/docs/tutorial/path-params.md b/docs/en/docs/tutorial/path-params.md index a0d70692e..6594a7a8b 100644 --- a/docs/en/docs/tutorial/path-params.md +++ b/docs/en/docs/tutorial/path-params.md @@ -83,7 +83,7 @@ And when you open your browser at
OpenAPI standard, there are many compatible tools. +And because the generated schema is from the OpenAPI standard, there are many compatible tools. Because of this, **FastAPI** itself provides an alternative API documentation (using ReDoc), which you can access at http://127.0.0.1:8000/redoc: diff --git a/docs/en/docs/tutorial/schema-extra-example.md b/docs/en/docs/tutorial/schema-extra-example.md index e0f7ed256..6cf8bf181 100644 --- a/docs/en/docs/tutorial/schema-extra-example.md +++ b/docs/en/docs/tutorial/schema-extra-example.md @@ -6,17 +6,17 @@ Here are several ways to do it. ## Pydantic `schema_extra` -You can declare an `example` for a Pydantic model using `Config` and `schema_extra`, as described in Pydantic's docs: Schema customization: +You can declare `examples` for a Pydantic model using `Config` and `schema_extra`, as described in Pydantic's docs: Schema customization: === "Python 3.10+" - ```Python hl_lines="13-21" + ```Python hl_lines="13-23" {!> ../../../docs_src/schema_extra_example/tutorial001_py310.py!} ``` === "Python 3.6+" - ```Python hl_lines="15-23" + ```Python hl_lines="15-25" {!> ../../../docs_src/schema_extra_example/tutorial001.py!} ``` @@ -27,11 +27,16 @@ That extra info will be added as-is to the output **JSON Schema** for that model For example you could use it to add metadata for a frontend user interface, etc. -## `Field` additional arguments +!!! info + OpenAPI 3.1.0 (used since FastAPI 0.99.0) added support for `examples`, which is part of the **JSON Schema** standard. + + Before that, it only supported the keyword `example` with a single example. That is still supported by OpenAPI 3.1.0, but is deprecated and is not part of the JSON Schema standard. So you are encouraged to migrate `example` to `examples`. 🤓 + + You can read more at the end of this page. -When using `Field()` with Pydantic models, you can also declare extra info for the **JSON Schema** by passing any other arbitrary arguments to the function. +## `Field` additional arguments -You can use this to add `example` for each field: +When using `Field()` with Pydantic models, you can also declare additional `examples`: === "Python 3.10+" @@ -45,10 +50,7 @@ You can use this to add `example` for each field: {!> ../../../docs_src/schema_extra_example/tutorial002.py!} ``` -!!! warning - Keep in mind that those extra arguments passed won't add any validation, only extra information, for documentation purposes. - -## `example` and `examples` in OpenAPI +## `examples` in OpenAPI When using any of: @@ -60,27 +62,27 @@ When using any of: * `Form()` * `File()` -you can also declare a data `example` or a group of `examples` with additional information that will be added to **OpenAPI**. +you can also declare a group of `examples` with additional information that will be added to **OpenAPI**. -### `Body` with `example` +### `Body` with `examples` -Here we pass an `example` of the data expected in `Body()`: +Here we pass `examples` containing one example of the data expected in `Body()`: === "Python 3.10+" - ```Python hl_lines="22-27" + ```Python hl_lines="22-29" {!> ../../../docs_src/schema_extra_example/tutorial003_an_py310.py!} ``` === "Python 3.9+" - ```Python hl_lines="22-27" + ```Python hl_lines="22-29" {!> ../../../docs_src/schema_extra_example/tutorial003_an_py39.py!} ``` === "Python 3.6+" - ```Python hl_lines="23-28" + ```Python hl_lines="23-30" {!> ../../../docs_src/schema_extra_example/tutorial003_an.py!} ``` @@ -89,7 +91,7 @@ Here we pass an `example` of the data expected in `Body()`: !!! tip Prefer to use the `Annotated` version if possible. - ```Python hl_lines="18-23" + ```Python hl_lines="18-25" {!> ../../../docs_src/schema_extra_example/tutorial003_py310.py!} ``` @@ -98,7 +100,7 @@ Here we pass an `example` of the data expected in `Body()`: !!! tip Prefer to use the `Annotated` version if possible. - ```Python hl_lines="20-25" + ```Python hl_lines="20-27" {!> ../../../docs_src/schema_extra_example/tutorial003.py!} ``` @@ -110,16 +112,7 @@ With any of the methods above it would look like this in the `/docs`: ### `Body` with multiple `examples` -Alternatively to the single `example`, you can pass `examples` using a `dict` with **multiple examples**, each with extra information that will be added to **OpenAPI** too. - -The keys of the `dict` identify each example, and each value is another `dict`. - -Each specific example `dict` in the `examples` can contain: - -* `summary`: Short description for the example. -* `description`: A long description that can contain Markdown text. -* `value`: This is the actual example shown, e.g. a `dict`. -* `externalValue`: alternative to `value`, a URL pointing to the example. Although this might not be supported by as many tools as `value`. +You can of course also pass multiple `examples`: === "Python 3.10+" @@ -165,25 +158,76 @@ With `examples` added to `Body()` the `/docs` would look like: ## Technical Details +!!! tip + If you are already using **FastAPI** version **0.99.0 or above**, you can probably **skip** these details. + + They are more relevant for older versions, before OpenAPI 3.1.0 was available. + + You can consider this a brief OpenAPI and JSON Schema **history lesson**. 🤓 + !!! warning These are very technical details about the standards **JSON Schema** and **OpenAPI**. If the ideas above already work for you, that might be enough, and you probably don't need these details, feel free to skip them. -When you add an example inside of a Pydantic model, using `schema_extra` or `Field(example="something")` that example is added to the **JSON Schema** for that Pydantic model. +Before OpenAPI 3.1.0, OpenAPI used an older and modified version of **JSON Schema**. -And that **JSON Schema** of the Pydantic model is included in the **OpenAPI** of your API, and then it's used in the docs UI. +JSON Schema didn't have `examples`, so OpenAPI added it's own `example` field to its own modified version. + +OpenAPI also added `example` and `examples` fields to other parts of the specification: + +* `Parameter Object` (in the specification) that was used by FastAPI's: + * `Path()` + * `Query()` + * `Header()` + * `Cookie()` +* `Request Body Object`, in the field `content`, on the `Media Type Object` (in the specification) that was used by FastAPI's: + * `Body()` + * `File()` + * `Form()` -**JSON Schema** doesn't really have a field `example` in the standards. Recent versions of JSON Schema define a field `examples`, but OpenAPI 3.0.3 is based on an older version of JSON Schema that didn't have `examples`. +### OpenAPI's `examples` field -So, OpenAPI 3.0.3 defined its own `example` for the modified version of **JSON Schema** it uses, for the same purpose (but it's a single `example`, not `examples`), and that's what is used by the API docs UI (using Swagger UI). +The shape of this field `examples` from OpenAPI is a `dict` with **multiple examples**, each with extra information that will be added to **OpenAPI** too. + +The keys of the `dict` identify each example, and each value is another `dict`. + +Each specific example `dict` in the `examples` can contain: + +* `summary`: Short description for the example. +* `description`: A long description that can contain Markdown text. +* `value`: This is the actual example shown, e.g. a `dict`. +* `externalValue`: alternative to `value`, a URL pointing to the example. Although this might not be supported by as many tools as `value`. + +This applies to those other parts of the OpenAPI specification apart from JSON Schema. + +### JSON Schema's `examples` field + +But then JSON Schema added an `examples` field to a new version of the specification. + +And then the new OpenAPI 3.1.0 was based on the latest version (JSON Schema 2020-12) that included this new field `examples`. + +And now this new `examples` field takes precedence over the old single (and custom) `example` field, that is now deprecated. + +This new `examples` field in JSON Schema is **just a `list`** of examples, not a dict with extra metadata as in the other places in OpenAPI (described above). + +!!! info + Even after OpenAPI 3.1.0 was released with this new simpler integration with JSON Schema, for a while, Swagger UI, the tool that provides the automatic docs, didn't support OpenAPI 3.1.0 (it does since version 5.0.0 🎉). + + Because of that, versions of FastAPI previous to 0.99.0 still used versions of OpenAPI lower than 3.1.0. + +### Pydantic and FastAPI `examples` + +When you add `examples` inside of a Pydantic model, using `schema_extra` or `Field(examples=["something"])` that example is added to the **JSON Schema** for that Pydantic model. + +And that **JSON Schema** of the Pydantic model is included in the **OpenAPI** of your API, and then it's used in the docs UI. -So, although `example` is not part of JSON Schema, it is part of OpenAPI's custom version of JSON Schema, and that's what will be used by the docs UI. +In versions of FastAPI before 0.99.0 (0.99.0 and above use the newer OpenAPI 3.1.0) when you used `example` or `examples` with any of the other utilities (`Query()`, `Body()`, etc.) those examples were not added to the JSON Schema that describes that data (not even to OpenAPI's own version of JSON Schema), they were added directly to the *path operation* declaration in OpenAPI (outside the parts of OpenAPI that use JSON Schema). -But when you use `example` or `examples` with any of the other utilities (`Query()`, `Body()`, etc.) those examples are not added to the JSON Schema that describes that data (not even to OpenAPI's own version of JSON Schema), they are added directly to the *path operation* declaration in OpenAPI (outside the parts of OpenAPI that use JSON Schema). +But now that FastAPI 0.99.0 and above uses OpenAPI 3.1.0, that uses JSON Schema 2020-12, and Swagger UI 5.0.0 and above, everything is more consistent and the examples are included in JSON Schema. -For `Path()`, `Query()`, `Header()`, and `Cookie()`, the `example` or `examples` are added to the OpenAPI definition, to the `Parameter Object` (in the specification). +### Summary -And for `Body()`, `File()`, and `Form()`, the `example` or `examples` are equivalently added to the OpenAPI definition, to the `Request Body Object`, in the field `content`, on the `Media Type Object` (in the specification). +I used to say I didn't like history that much... and look at me now giving "tech history" lessons. 😅 -On the other hand, there's a newer version of OpenAPI: **3.1.0**, recently released. It is based on the latest JSON Schema and most of the modifications from OpenAPI's custom version of JSON Schema are removed, in exchange of the features from the recent versions of JSON Schema, so all these small differences are reduced. Nevertheless, Swagger UI currently doesn't support OpenAPI 3.1.0, so, for now, it's better to continue using the ideas above. +In short, **upgrade to FastAPI 0.99.0 or above**, and things are much **simpler, consistent, and intuitive**, and you don't have to know all these historic details. 😎 diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index 64dc40372..030bbe5d3 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -147,6 +147,7 @@ nav: - advanced/conditional-openapi.md - advanced/extending-openapi.md - advanced/openapi-callbacks.md + - advanced/openapi-webhooks.md - advanced/wsgi.md - advanced/generate-clients.md - async.md diff --git a/docs_src/extending_openapi/tutorial001.py b/docs_src/extending_openapi/tutorial001.py index 561e95898..35e31c0e0 100644 --- a/docs_src/extending_openapi/tutorial001.py +++ b/docs_src/extending_openapi/tutorial001.py @@ -15,7 +15,8 @@ def custom_openapi(): openapi_schema = get_openapi( title="Custom title", version="2.5.0", - description="This is a very custom OpenAPI schema", + summary="This is a very custom OpenAPI schema", + description="Here's a longer description of the custom **OpenAPI** schema", routes=app.routes, ) openapi_schema["info"]["x-logo"] = { diff --git a/docs_src/metadata/tutorial001.py b/docs_src/metadata/tutorial001.py index 3fba9e7d1..76656e81b 100644 --- a/docs_src/metadata/tutorial001.py +++ b/docs_src/metadata/tutorial001.py @@ -18,6 +18,7 @@ You will be able to: app = FastAPI( title="ChimichangApp", description=description, + summary="Deadpool's favorite app. Nuff said.", version="0.0.1", terms_of_service="http://example.com/terms/", contact={ diff --git a/docs_src/metadata/tutorial001_1.py b/docs_src/metadata/tutorial001_1.py new file mode 100644 index 000000000..a8f5b9458 --- /dev/null +++ b/docs_src/metadata/tutorial001_1.py @@ -0,0 +1,38 @@ +from fastapi import FastAPI + +description = """ +ChimichangApp API helps you do awesome stuff. 🚀 + +## Items + +You can **read items**. + +## Users + +You will be able to: + +* **Create users** (_not implemented_). +* **Read users** (_not implemented_). +""" + +app = FastAPI( + title="ChimichangApp", + description=description, + summary="Deadpool's favorite app. Nuff said.", + version="0.0.1", + terms_of_service="http://example.com/terms/", + contact={ + "name": "Deadpoolio the Amazing", + "url": "http://x-force.example.com/contact/", + "email": "dp@x-force.example.com", + }, + license_info={ + "name": "Apache 2.0", + "identifier": "MIT", + }, +) + + +@app.get("/items/") +async def read_items(): + return [{"name": "Katana"}] diff --git a/docs_src/openapi_webhooks/tutorial001.py b/docs_src/openapi_webhooks/tutorial001.py new file mode 100644 index 000000000..5016f5b00 --- /dev/null +++ b/docs_src/openapi_webhooks/tutorial001.py @@ -0,0 +1,25 @@ +from datetime import datetime + +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Subscription(BaseModel): + username: str + montly_fee: float + start_date: datetime + + +@app.webhooks.post("new-subscription") +def new_subscription(body: Subscription): + """ + When a new user subscribes to your service we'll send you a POST request with this + data to the URL that you register for the event `new-subscription` in the dashboard. + """ + + +@app.get("/users/") +def read_users(): + return ["Rick", "Morty"] diff --git a/docs_src/schema_extra_example/tutorial001.py b/docs_src/schema_extra_example/tutorial001.py index a5ae28127..6ab96ff85 100644 --- a/docs_src/schema_extra_example/tutorial001.py +++ b/docs_src/schema_extra_example/tutorial001.py @@ -14,12 +14,14 @@ class Item(BaseModel): class Config: schema_extra = { - "example": { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - } + "examples": [ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + } + ] } diff --git a/docs_src/schema_extra_example/tutorial001_py310.py b/docs_src/schema_extra_example/tutorial001_py310.py index 77ceedd60..ec83f1112 100644 --- a/docs_src/schema_extra_example/tutorial001_py310.py +++ b/docs_src/schema_extra_example/tutorial001_py310.py @@ -12,12 +12,14 @@ class Item(BaseModel): class Config: schema_extra = { - "example": { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - } + "examples": [ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + } + ] } diff --git a/docs_src/schema_extra_example/tutorial002.py b/docs_src/schema_extra_example/tutorial002.py index 6de434f81..70f06567c 100644 --- a/docs_src/schema_extra_example/tutorial002.py +++ b/docs_src/schema_extra_example/tutorial002.py @@ -7,10 +7,10 @@ app = FastAPI() class Item(BaseModel): - name: str = Field(example="Foo") - description: Union[str, None] = Field(default=None, example="A very nice Item") - price: float = Field(example=35.4) - tax: Union[float, None] = Field(default=None, example=3.2) + name: str = Field(examples=["Foo"]) + description: Union[str, None] = Field(default=None, examples=["A very nice Item"]) + price: float = Field(examples=[35.4]) + tax: Union[float, None] = Field(default=None, examples=[3.2]) @app.put("/items/{item_id}") diff --git a/docs_src/schema_extra_example/tutorial002_py310.py b/docs_src/schema_extra_example/tutorial002_py310.py index e84928bb1..27d786867 100644 --- a/docs_src/schema_extra_example/tutorial002_py310.py +++ b/docs_src/schema_extra_example/tutorial002_py310.py @@ -5,10 +5,10 @@ app = FastAPI() class Item(BaseModel): - name: str = Field(example="Foo") - description: str | None = Field(default=None, example="A very nice Item") - price: float = Field(example=35.4) - tax: float | None = Field(default=None, example=3.2) + name: str = Field(examples=["Foo"]) + description: str | None = Field(default=None, examples=["A very nice Item"]) + price: float = Field(examples=[35.4]) + tax: float | None = Field(default=None, examples=[3.2]) @app.put("/items/{item_id}") diff --git a/docs_src/schema_extra_example/tutorial003.py b/docs_src/schema_extra_example/tutorial003.py index ce1736bba..385f3de8a 100644 --- a/docs_src/schema_extra_example/tutorial003.py +++ b/docs_src/schema_extra_example/tutorial003.py @@ -17,12 +17,14 @@ class Item(BaseModel): async def update_item( item_id: int, item: Item = Body( - example={ - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, + examples=[ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + } + ], ), ): results = {"item_id": item_id, "item": item} diff --git a/docs_src/schema_extra_example/tutorial003_an.py b/docs_src/schema_extra_example/tutorial003_an.py index 1dec555a9..23675aba1 100644 --- a/docs_src/schema_extra_example/tutorial003_an.py +++ b/docs_src/schema_extra_example/tutorial003_an.py @@ -20,12 +20,14 @@ async def update_item( item: Annotated[ Item, Body( - example={ - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, + examples=[ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + } + ], ), ], ): diff --git a/docs_src/schema_extra_example/tutorial003_an_py310.py b/docs_src/schema_extra_example/tutorial003_an_py310.py index 9edaddfb8..bbd2e171e 100644 --- a/docs_src/schema_extra_example/tutorial003_an_py310.py +++ b/docs_src/schema_extra_example/tutorial003_an_py310.py @@ -19,12 +19,14 @@ async def update_item( item: Annotated[ Item, Body( - example={ - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, + examples=[ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + } + ], ), ], ): diff --git a/docs_src/schema_extra_example/tutorial003_an_py39.py b/docs_src/schema_extra_example/tutorial003_an_py39.py index fe08847d9..472808561 100644 --- a/docs_src/schema_extra_example/tutorial003_an_py39.py +++ b/docs_src/schema_extra_example/tutorial003_an_py39.py @@ -19,12 +19,14 @@ async def update_item( item: Annotated[ Item, Body( - example={ - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, + examples=[ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + } + ], ), ], ): diff --git a/docs_src/schema_extra_example/tutorial003_py310.py b/docs_src/schema_extra_example/tutorial003_py310.py index 1e137101d..2d31619be 100644 --- a/docs_src/schema_extra_example/tutorial003_py310.py +++ b/docs_src/schema_extra_example/tutorial003_py310.py @@ -15,12 +15,14 @@ class Item(BaseModel): async def update_item( item_id: int, item: Item = Body( - example={ - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, + examples=[ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + } + ], ), ): results = {"item_id": item_id, "item": item} diff --git a/docs_src/schema_extra_example/tutorial004.py b/docs_src/schema_extra_example/tutorial004.py index b67edf30c..eb49293fd 100644 --- a/docs_src/schema_extra_example/tutorial004.py +++ b/docs_src/schema_extra_example/tutorial004.py @@ -18,8 +18,8 @@ async def update_item( *, item_id: int, item: Item = Body( - examples={ - "normal": { + examples=[ + { "summary": "A normal example", "description": "A **normal** item works correctly.", "value": { @@ -29,7 +29,7 @@ async def update_item( "tax": 3.2, }, }, - "converted": { + { "summary": "An example with converted data", "description": "FastAPI can convert price `strings` to actual `numbers` automatically", "value": { @@ -37,14 +37,14 @@ async def update_item( "price": "35.4", }, }, - "invalid": { + { "summary": "Invalid data is rejected with an error", "value": { "name": "Baz", "price": "thirty five point four", }, }, - }, + ], ), ): results = {"item_id": item_id, "item": item} diff --git a/docs_src/schema_extra_example/tutorial004_an.py b/docs_src/schema_extra_example/tutorial004_an.py index 82c9a92ac..567ec4702 100644 --- a/docs_src/schema_extra_example/tutorial004_an.py +++ b/docs_src/schema_extra_example/tutorial004_an.py @@ -21,8 +21,8 @@ async def update_item( item: Annotated[ Item, Body( - examples={ - "normal": { + examples=[ + { "summary": "A normal example", "description": "A **normal** item works correctly.", "value": { @@ -32,7 +32,7 @@ async def update_item( "tax": 3.2, }, }, - "converted": { + { "summary": "An example with converted data", "description": "FastAPI can convert price `strings` to actual `numbers` automatically", "value": { @@ -40,14 +40,14 @@ async def update_item( "price": "35.4", }, }, - "invalid": { + { "summary": "Invalid data is rejected with an error", "value": { "name": "Baz", "price": "thirty five point four", }, }, - }, + ], ), ], ): diff --git a/docs_src/schema_extra_example/tutorial004_an_py310.py b/docs_src/schema_extra_example/tutorial004_an_py310.py index 01f1a486c..026654835 100644 --- a/docs_src/schema_extra_example/tutorial004_an_py310.py +++ b/docs_src/schema_extra_example/tutorial004_an_py310.py @@ -20,8 +20,8 @@ async def update_item( item: Annotated[ Item, Body( - examples={ - "normal": { + examples=[ + { "summary": "A normal example", "description": "A **normal** item works correctly.", "value": { @@ -31,7 +31,7 @@ async def update_item( "tax": 3.2, }, }, - "converted": { + { "summary": "An example with converted data", "description": "FastAPI can convert price `strings` to actual `numbers` automatically", "value": { @@ -39,14 +39,14 @@ async def update_item( "price": "35.4", }, }, - "invalid": { + { "summary": "Invalid data is rejected with an error", "value": { "name": "Baz", "price": "thirty five point four", }, }, - }, + ], ), ], ): diff --git a/docs_src/schema_extra_example/tutorial004_an_py39.py b/docs_src/schema_extra_example/tutorial004_an_py39.py index d50e8aa5f..06219ede8 100644 --- a/docs_src/schema_extra_example/tutorial004_an_py39.py +++ b/docs_src/schema_extra_example/tutorial004_an_py39.py @@ -20,8 +20,8 @@ async def update_item( item: Annotated[ Item, Body( - examples={ - "normal": { + examples=[ + { "summary": "A normal example", "description": "A **normal** item works correctly.", "value": { @@ -31,7 +31,7 @@ async def update_item( "tax": 3.2, }, }, - "converted": { + { "summary": "An example with converted data", "description": "FastAPI can convert price `strings` to actual `numbers` automatically", "value": { @@ -39,14 +39,14 @@ async def update_item( "price": "35.4", }, }, - "invalid": { + { "summary": "Invalid data is rejected with an error", "value": { "name": "Baz", "price": "thirty five point four", }, }, - }, + ], ), ], ): diff --git a/docs_src/schema_extra_example/tutorial004_py310.py b/docs_src/schema_extra_example/tutorial004_py310.py index 100a30860..ef2b9d8cb 100644 --- a/docs_src/schema_extra_example/tutorial004_py310.py +++ b/docs_src/schema_extra_example/tutorial004_py310.py @@ -16,8 +16,8 @@ async def update_item( *, item_id: int, item: Item = Body( - examples={ - "normal": { + examples=[ + { "summary": "A normal example", "description": "A **normal** item works correctly.", "value": { @@ -27,7 +27,7 @@ async def update_item( "tax": 3.2, }, }, - "converted": { + { "summary": "An example with converted data", "description": "FastAPI can convert price `strings` to actual `numbers` automatically", "value": { @@ -35,14 +35,14 @@ async def update_item( "price": "35.4", }, }, - "invalid": { + { "summary": "Invalid data is rejected with an error", "value": { "name": "Baz", "price": "thirty five point four", }, }, - }, + ], ), ): results = {"item_id": item_id, "item": item} diff --git a/fastapi/applications.py b/fastapi/applications.py index 9b161c5ec..88f861c1e 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -55,6 +55,7 @@ class FastAPI(Starlette): debug: bool = False, routes: Optional[List[BaseRoute]] = None, title: str = "FastAPI", + summary: Optional[str] = None, description: str = "", version: str = "0.1.0", openapi_url: Optional[str] = "/openapi.json", @@ -85,6 +86,7 @@ class FastAPI(Starlette): root_path_in_servers: bool = True, responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, callbacks: Optional[List[BaseRoute]] = None, + webhooks: Optional[routing.APIRouter] = None, deprecated: Optional[bool] = None, include_in_schema: bool = True, swagger_ui_parameters: Optional[Dict[str, Any]] = None, @@ -95,6 +97,7 @@ class FastAPI(Starlette): ) -> None: self.debug = debug self.title = title + self.summary = summary self.description = description self.version = version self.terms_of_service = terms_of_service @@ -110,7 +113,7 @@ class FastAPI(Starlette): self.swagger_ui_parameters = swagger_ui_parameters self.servers = servers or [] self.extra = extra - self.openapi_version = "3.0.2" + self.openapi_version = "3.1.0" self.openapi_schema: Optional[Dict[str, Any]] = None if self.openapi_url: assert self.title, "A title must be provided for OpenAPI, e.g.: 'My API'" @@ -123,6 +126,7 @@ class FastAPI(Starlette): "automatic. Check the docs at " "https://fastapi.tiangolo.com/advanced/sub-applications/" ) + self.webhooks = webhooks or routing.APIRouter() self.root_path = root_path or openapi_prefix self.state: State = State() self.dependency_overrides: Dict[Callable[..., Any], Callable[..., Any]] = {} @@ -215,11 +219,13 @@ class FastAPI(Starlette): title=self.title, version=self.version, openapi_version=self.openapi_version, + summary=self.summary, description=self.description, terms_of_service=self.terms_of_service, contact=self.contact, license_info=self.license_info, routes=self.routes, + webhooks=self.webhooks.routes, tags=self.openapi_tags, servers=self.servers, ) diff --git a/fastapi/openapi/docs.py b/fastapi/openapi/docs.py index bf335118f..81f67dcc5 100644 --- a/fastapi/openapi/docs.py +++ b/fastapi/openapi/docs.py @@ -17,8 +17,8 @@ def get_swagger_ui_html( *, openapi_url: str, title: str, - swagger_js_url: str = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@4/swagger-ui-bundle.js", - swagger_css_url: str = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@4/swagger-ui.css", + swagger_js_url: str = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js", + swagger_css_url: str = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css", swagger_favicon_url: str = "https://fastapi.tiangolo.com/img/favicon.png", oauth2_redirect_url: Optional[str] = None, init_oauth: Optional[Dict[str, Any]] = None, diff --git a/fastapi/openapi/models.py b/fastapi/openapi/models.py index 81a24f389..7420d3b55 100644 --- a/fastapi/openapi/models.py +++ b/fastapi/openapi/models.py @@ -1,9 +1,10 @@ from enum import Enum -from typing import Any, Callable, Dict, Iterable, List, Optional, Union +from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Union from fastapi.logger import logger from pydantic import AnyUrl, BaseModel, Field -from typing_extensions import Literal +from typing_extensions import Annotated, Literal +from typing_extensions import deprecated as typing_deprecated try: import email_validator # type: ignore @@ -37,6 +38,7 @@ class Contact(BaseModel): class License(BaseModel): name: str + identifier: Optional[str] = None url: Optional[AnyUrl] = None class Config: @@ -45,6 +47,7 @@ class License(BaseModel): class Info(BaseModel): title: str + summary: Optional[str] = None description: Optional[str] = None termsOfService: Optional[str] = None contact: Optional[Contact] = None @@ -56,7 +59,7 @@ class Info(BaseModel): class ServerVariable(BaseModel): - enum: Optional[List[str]] = None + enum: Annotated[Optional[List[str]], Field(min_items=1)] = None default: str description: Optional[str] = None @@ -102,9 +105,42 @@ class ExternalDocumentation(BaseModel): class Schema(BaseModel): + # Ref: JSON Schema 2020-12: https://json-schema.org/draft/2020-12/json-schema-core.html#name-the-json-schema-core-vocabu + # Core Vocabulary + schema_: Optional[str] = Field(default=None, alias="$schema") + vocabulary: Optional[str] = Field(default=None, alias="$vocabulary") + id: Optional[str] = Field(default=None, alias="$id") + anchor: Optional[str] = Field(default=None, alias="$anchor") + dynamicAnchor: Optional[str] = Field(default=None, alias="$dynamicAnchor") ref: Optional[str] = Field(default=None, alias="$ref") - title: Optional[str] = None - multipleOf: Optional[float] = None + dynamicRef: Optional[str] = Field(default=None, alias="$dynamicRef") + defs: Optional[Dict[str, "Schema"]] = Field(default=None, alias="$defs") + comment: Optional[str] = Field(default=None, alias="$comment") + # Ref: JSON Schema 2020-12: https://json-schema.org/draft/2020-12/json-schema-core.html#name-a-vocabulary-for-applying-s + # A Vocabulary for Applying Subschemas + allOf: Optional[List["Schema"]] = None + anyOf: Optional[List["Schema"]] = None + oneOf: Optional[List["Schema"]] = None + not_: Optional["Schema"] = Field(default=None, alias="not") + if_: Optional["Schema"] = Field(default=None, alias="if") + then: Optional["Schema"] = None + else_: Optional["Schema"] = Field(default=None, alias="else") + dependentSchemas: Optional[Dict[str, "Schema"]] = None + prefixItems: Optional[List["Schema"]] = None + items: Optional[Union["Schema", List["Schema"]]] = None + contains: Optional["Schema"] = None + properties: Optional[Dict[str, "Schema"]] = None + patternProperties: Optional[Dict[str, "Schema"]] = None + additionalProperties: Optional["Schema"] = None + propertyNames: Optional["Schema"] = None + unevaluatedItems: Optional["Schema"] = None + unevaluatedProperties: Optional["Schema"] = None + # Ref: JSON Schema Validation 2020-12: https://json-schema.org/draft/2020-12/json-schema-validation.html#name-a-vocabulary-for-structural + # A Vocabulary for Structural Validation + type: Optional[str] = None + enum: Optional[List[Any]] = None + const: Optional[Any] = None + multipleOf: Optional[float] = Field(default=None, gt=0) maximum: Optional[float] = None exclusiveMaximum: Optional[float] = None minimum: Optional[float] = None @@ -115,29 +151,41 @@ class Schema(BaseModel): maxItems: Optional[int] = Field(default=None, ge=0) minItems: Optional[int] = Field(default=None, ge=0) uniqueItems: Optional[bool] = None + maxContains: Optional[int] = Field(default=None, ge=0) + minContains: Optional[int] = Field(default=None, ge=0) maxProperties: Optional[int] = Field(default=None, ge=0) minProperties: Optional[int] = Field(default=None, ge=0) required: Optional[List[str]] = None - enum: Optional[List[Any]] = None - type: Optional[str] = None - allOf: Optional[List["Schema"]] = None - oneOf: Optional[List["Schema"]] = None - anyOf: Optional[List["Schema"]] = None - not_: Optional["Schema"] = Field(default=None, alias="not") - items: Optional[Union["Schema", List["Schema"]]] = None - properties: Optional[Dict[str, "Schema"]] = None - additionalProperties: Optional[Union["Schema", Reference, bool]] = None - description: Optional[str] = None + dependentRequired: Optional[Dict[str, Set[str]]] = None + # Ref: JSON Schema Validation 2020-12: https://json-schema.org/draft/2020-12/json-schema-validation.html#name-vocabularies-for-semantic-c + # Vocabularies for Semantic Content With "format" format: Optional[str] = None + # Ref: JSON Schema Validation 2020-12: https://json-schema.org/draft/2020-12/json-schema-validation.html#name-a-vocabulary-for-the-conten + # A Vocabulary for the Contents of String-Encoded Data + contentEncoding: Optional[str] = None + contentMediaType: Optional[str] = None + contentSchema: Optional["Schema"] = None + # Ref: JSON Schema Validation 2020-12: https://json-schema.org/draft/2020-12/json-schema-validation.html#name-a-vocabulary-for-basic-meta + # A Vocabulary for Basic Meta-Data Annotations + title: Optional[str] = None + description: Optional[str] = None default: Optional[Any] = None - nullable: Optional[bool] = None - discriminator: Optional[Discriminator] = None + deprecated: Optional[bool] = None readOnly: Optional[bool] = None writeOnly: Optional[bool] = None + examples: Optional[List[Any]] = None + # Ref: OpenAPI 3.1.0: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#schema-object + # Schema Object + discriminator: Optional[Discriminator] = None xml: Optional[XML] = None externalDocs: Optional[ExternalDocumentation] = None - example: Optional[Any] = None - deprecated: Optional[bool] = None + example: Annotated[ + Optional[Any], + typing_deprecated( + "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " + "although still supported. Use examples instead." + ), + ] = None class Config: extra: str = "allow" @@ -248,7 +296,7 @@ class Operation(BaseModel): parameters: Optional[List[Union[Parameter, Reference]]] = None requestBody: Optional[Union[RequestBody, Reference]] = None # Using Any for Specification Extensions - responses: Dict[str, Union[Response, Any]] + responses: Optional[Dict[str, Union[Response, Any]]] = None callbacks: Optional[Dict[str, Union[Dict[str, "PathItem"], Reference]]] = None deprecated: Optional[bool] = None security: Optional[List[Dict[str, List[str]]]] = None @@ -375,6 +423,7 @@ class Components(BaseModel): links: Optional[Dict[str, Union[Link, Reference]]] = None # Using Any for Specification Extensions callbacks: Optional[Dict[str, Union[Dict[str, PathItem], Reference, Any]]] = None + pathItems: Optional[Dict[str, Union[PathItem, Reference]]] = None class Config: extra = "allow" @@ -392,9 +441,11 @@ class Tag(BaseModel): class OpenAPI(BaseModel): openapi: str info: Info + jsonSchemaDialect: Optional[str] = None servers: Optional[List[Server]] = None # Using Any for Specification Extensions - paths: Dict[str, Union[PathItem, Any]] + paths: Optional[Dict[str, Union[PathItem, Any]]] = None + webhooks: Optional[Dict[str, Union[PathItem, Reference]]] = None components: Optional[Components] = None security: Optional[List[Dict[str, List[str]]]] = None tags: Optional[List[Tag]] = None diff --git a/fastapi/openapi/utils.py b/fastapi/openapi/utils.py index 6d736647b..609fe4389 100644 --- a/fastapi/openapi/utils.py +++ b/fastapi/openapi/utils.py @@ -106,9 +106,7 @@ def get_openapi_operation_parameters( } if field_info.description: parameter["description"] = field_info.description - if field_info.examples: - parameter["examples"] = jsonable_encoder(field_info.examples) - elif field_info.example != Undefined: + if field_info.example != Undefined: parameter["example"] = jsonable_encoder(field_info.example) if field_info.deprecated: parameter["deprecated"] = field_info.deprecated @@ -134,9 +132,7 @@ def get_openapi_operation_request_body( if required: request_body_oai["required"] = required request_media_content: Dict[str, Any] = {"schema": body_schema} - if field_info.examples: - request_media_content["examples"] = jsonable_encoder(field_info.examples) - elif field_info.example != Undefined: + if field_info.example != Undefined: request_media_content["example"] = jsonable_encoder(field_info.example) request_body_oai["content"] = {request_media_type: request_media_content} return request_body_oai @@ -392,9 +388,11 @@ def get_openapi( *, title: str, version: str, - openapi_version: str = "3.0.2", + openapi_version: str = "3.1.0", + summary: Optional[str] = None, description: Optional[str] = None, routes: Sequence[BaseRoute], + webhooks: Optional[Sequence[BaseRoute]] = None, tags: Optional[List[Dict[str, Any]]] = None, servers: Optional[List[Dict[str, Union[str, Any]]]] = None, terms_of_service: Optional[str] = None, @@ -402,6 +400,8 @@ def get_openapi( license_info: Optional[Dict[str, Union[str, Any]]] = None, ) -> Dict[str, Any]: info: Dict[str, Any] = {"title": title, "version": version} + if summary: + info["summary"] = summary if description: info["description"] = description if terms_of_service: @@ -415,13 +415,14 @@ def get_openapi( output["servers"] = servers components: Dict[str, Dict[str, Any]] = {} paths: Dict[str, Dict[str, Any]] = {} + webhook_paths: Dict[str, Dict[str, Any]] = {} operation_ids: Set[str] = set() - flat_models = get_flat_models_from_routes(routes) + flat_models = get_flat_models_from_routes(list(routes or []) + list(webhooks or [])) model_name_map = get_model_name_map(flat_models) definitions = get_model_definitions( flat_models=flat_models, model_name_map=model_name_map ) - for route in routes: + for route in routes or []: if isinstance(route, routing.APIRoute): result = get_openapi_path( route=route, model_name_map=model_name_map, operation_ids=operation_ids @@ -436,11 +437,30 @@ def get_openapi( ) if path_definitions: definitions.update(path_definitions) + for webhook in webhooks or []: + if isinstance(webhook, routing.APIRoute): + result = get_openapi_path( + route=webhook, + model_name_map=model_name_map, + operation_ids=operation_ids, + ) + if result: + path, security_schemes, path_definitions = result + if path: + webhook_paths.setdefault(webhook.path_format, {}).update(path) + if security_schemes: + components.setdefault("securitySchemes", {}).update( + security_schemes + ) + if path_definitions: + definitions.update(path_definitions) if definitions: components["schemas"] = {k: definitions[k] for k in sorted(definitions)} if components: output["components"] = components output["paths"] = paths + if webhook_paths: + output["webhooks"] = webhook_paths if tags: output["tags"] = tags return jsonable_encoder(OpenAPI(**output), by_alias=True, exclude_none=True) # type: ignore diff --git a/fastapi/param_functions.py b/fastapi/param_functions.py index 75f054e9d..2f5818c85 100644 --- a/fastapi/param_functions.py +++ b/fastapi/param_functions.py @@ -1,7 +1,8 @@ -from typing import Any, Callable, Dict, Optional, Sequence +from typing import Any, Callable, List, Optional, Sequence from fastapi import params from pydantic.fields import Undefined +from typing_extensions import Annotated, deprecated def Path( # noqa: N802 @@ -17,8 +18,14 @@ def Path( # noqa: N802 min_length: Optional[int] = None, max_length: Optional[int] = None, regex: Optional[str] = None, - example: Any = Undefined, - examples: Optional[Dict[str, Any]] = None, + examples: Optional[List[Any]] = None, + example: Annotated[ + Optional[Any], + deprecated( + "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " + "although still supported. Use examples instead." + ), + ] = Undefined, deprecated: Optional[bool] = None, include_in_schema: bool = True, **extra: Any, @@ -56,8 +63,14 @@ def Query( # noqa: N802 min_length: Optional[int] = None, max_length: Optional[int] = None, regex: Optional[str] = None, - example: Any = Undefined, - examples: Optional[Dict[str, Any]] = None, + examples: Optional[List[Any]] = None, + example: Annotated[ + Optional[Any], + deprecated( + "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " + "although still supported. Use examples instead." + ), + ] = Undefined, deprecated: Optional[bool] = None, include_in_schema: bool = True, **extra: Any, @@ -96,8 +109,14 @@ def Header( # noqa: N802 min_length: Optional[int] = None, max_length: Optional[int] = None, regex: Optional[str] = None, - example: Any = Undefined, - examples: Optional[Dict[str, Any]] = None, + examples: Optional[List[Any]] = None, + example: Annotated[ + Optional[Any], + deprecated( + "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " + "although still supported. Use examples instead." + ), + ] = Undefined, deprecated: Optional[bool] = None, include_in_schema: bool = True, **extra: Any, @@ -136,8 +155,14 @@ def Cookie( # noqa: N802 min_length: Optional[int] = None, max_length: Optional[int] = None, regex: Optional[str] = None, - example: Any = Undefined, - examples: Optional[Dict[str, Any]] = None, + examples: Optional[List[Any]] = None, + example: Annotated[ + Optional[Any], + deprecated( + "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " + "although still supported. Use examples instead." + ), + ] = Undefined, deprecated: Optional[bool] = None, include_in_schema: bool = True, **extra: Any, @@ -177,8 +202,14 @@ def Body( # noqa: N802 min_length: Optional[int] = None, max_length: Optional[int] = None, regex: Optional[str] = None, - example: Any = Undefined, - examples: Optional[Dict[str, Any]] = None, + examples: Optional[List[Any]] = None, + example: Annotated[ + Optional[Any], + deprecated( + "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " + "although still supported. Use examples instead." + ), + ] = Undefined, **extra: Any, ) -> Any: return params.Body( @@ -215,8 +246,14 @@ def Form( # noqa: N802 min_length: Optional[int] = None, max_length: Optional[int] = None, regex: Optional[str] = None, - example: Any = Undefined, - examples: Optional[Dict[str, Any]] = None, + examples: Optional[List[Any]] = None, + example: Annotated[ + Optional[Any], + deprecated( + "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " + "although still supported. Use examples instead." + ), + ] = Undefined, **extra: Any, ) -> Any: return params.Form( @@ -252,8 +289,14 @@ def File( # noqa: N802 min_length: Optional[int] = None, max_length: Optional[int] = None, regex: Optional[str] = None, - example: Any = Undefined, - examples: Optional[Dict[str, Any]] = None, + examples: Optional[List[Any]] = None, + example: Annotated[ + Optional[Any], + deprecated( + "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " + "although still supported. Use examples instead." + ), + ] = Undefined, **extra: Any, ) -> Any: return params.File( diff --git a/fastapi/params.py b/fastapi/params.py index 16c5c309a..4069f2cda 100644 --- a/fastapi/params.py +++ b/fastapi/params.py @@ -1,7 +1,9 @@ +import warnings from enum import Enum -from typing import Any, Callable, Dict, Optional, Sequence +from typing import Any, Callable, List, Optional, Sequence from pydantic.fields import FieldInfo, Undefined +from typing_extensions import Annotated, deprecated class ParamTypes(Enum): @@ -28,16 +30,30 @@ class Param(FieldInfo): min_length: Optional[int] = None, max_length: Optional[int] = None, regex: Optional[str] = None, - example: Any = Undefined, - examples: Optional[Dict[str, Any]] = None, + examples: Optional[List[Any]] = None, + example: Annotated[ + Optional[Any], + deprecated( + "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " + "although still supported. Use examples instead." + ), + ] = Undefined, deprecated: Optional[bool] = None, include_in_schema: bool = True, **extra: Any, ): self.deprecated = deprecated + if example is not Undefined: + warnings.warn( + "`example` has been depreacated, please use `examples` instead", + category=DeprecationWarning, + stacklevel=1, + ) self.example = example - self.examples = examples self.include_in_schema = include_in_schema + extra_kwargs = {**extra} + if examples: + extra_kwargs["examples"] = examples super().__init__( default=default, alias=alias, @@ -50,7 +66,7 @@ class Param(FieldInfo): min_length=min_length, max_length=max_length, regex=regex, - **extra, + **extra_kwargs, ) def __repr__(self) -> str: @@ -74,8 +90,14 @@ class Path(Param): min_length: Optional[int] = None, max_length: Optional[int] = None, regex: Optional[str] = None, - example: Any = Undefined, - examples: Optional[Dict[str, Any]] = None, + examples: Optional[List[Any]] = None, + example: Annotated[ + Optional[Any], + deprecated( + "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " + "although still supported. Use examples instead." + ), + ] = Undefined, deprecated: Optional[bool] = None, include_in_schema: bool = True, **extra: Any, @@ -119,8 +141,14 @@ class Query(Param): min_length: Optional[int] = None, max_length: Optional[int] = None, regex: Optional[str] = None, - example: Any = Undefined, - examples: Optional[Dict[str, Any]] = None, + examples: Optional[List[Any]] = None, + example: Annotated[ + Optional[Any], + deprecated( + "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " + "although still supported. Use examples instead." + ), + ] = Undefined, deprecated: Optional[bool] = None, include_in_schema: bool = True, **extra: Any, @@ -163,8 +191,14 @@ class Header(Param): min_length: Optional[int] = None, max_length: Optional[int] = None, regex: Optional[str] = None, - example: Any = Undefined, - examples: Optional[Dict[str, Any]] = None, + examples: Optional[List[Any]] = None, + example: Annotated[ + Optional[Any], + deprecated( + "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " + "although still supported. Use examples instead." + ), + ] = Undefined, deprecated: Optional[bool] = None, include_in_schema: bool = True, **extra: Any, @@ -207,8 +241,14 @@ class Cookie(Param): min_length: Optional[int] = None, max_length: Optional[int] = None, regex: Optional[str] = None, - example: Any = Undefined, - examples: Optional[Dict[str, Any]] = None, + examples: Optional[List[Any]] = None, + example: Annotated[ + Optional[Any], + deprecated( + "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " + "although still supported. Use examples instead." + ), + ] = Undefined, deprecated: Optional[bool] = None, include_in_schema: bool = True, **extra: Any, @@ -250,14 +290,28 @@ class Body(FieldInfo): min_length: Optional[int] = None, max_length: Optional[int] = None, regex: Optional[str] = None, - example: Any = Undefined, - examples: Optional[Dict[str, Any]] = None, + examples: Optional[List[Any]] = None, + example: Annotated[ + Optional[Any], + deprecated( + "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " + "although still supported. Use examples instead." + ), + ] = Undefined, **extra: Any, ): self.embed = embed self.media_type = media_type + if example is not Undefined: + warnings.warn( + "`example` has been depreacated, please use `examples` instead", + category=DeprecationWarning, + stacklevel=1, + ) self.example = example - self.examples = examples + extra_kwargs = {**extra} + if examples is not None: + extra_kwargs["examples"] = examples super().__init__( default=default, alias=alias, @@ -270,7 +324,7 @@ class Body(FieldInfo): min_length=min_length, max_length=max_length, regex=regex, - **extra, + **extra_kwargs, ) def __repr__(self) -> str: @@ -293,8 +347,14 @@ class Form(Body): min_length: Optional[int] = None, max_length: Optional[int] = None, regex: Optional[str] = None, - example: Any = Undefined, - examples: Optional[Dict[str, Any]] = None, + examples: Optional[List[Any]] = None, + example: Annotated[ + Optional[Any], + deprecated( + "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " + "although still supported. Use examples instead." + ), + ] = Undefined, **extra: Any, ): super().__init__( @@ -333,8 +393,14 @@ class File(Form): min_length: Optional[int] = None, max_length: Optional[int] = None, regex: Optional[str] = None, - example: Any = Undefined, - examples: Optional[Dict[str, Any]] = None, + examples: Optional[List[Any]] = None, + example: Annotated[ + Optional[Any], + deprecated( + "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " + "although still supported. Use examples instead." + ), + ] = Undefined, **extra: Any, ): super().__init__( diff --git a/pyproject.toml b/pyproject.toml index 5c0d3c48e..61dbf7629 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,6 +43,7 @@ classifiers = [ dependencies = [ "starlette>=0.27.0,<0.28.0", "pydantic>=1.7.4,!=1.8,!=1.8.1,<2.0.0", + "typing-extensions>=4.5.0" ] dynamic = ["version"] diff --git a/tests/test_additional_properties.py b/tests/test_additional_properties.py index 516a569e4..be14d10ed 100644 --- a/tests/test_additional_properties.py +++ b/tests/test_additional_properties.py @@ -29,7 +29,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/foo": { diff --git a/tests/test_additional_response_extra.py b/tests/test_additional_response_extra.py index d62638c8f..55be19bad 100644 --- a/tests/test_additional_response_extra.py +++ b/tests/test_additional_response_extra.py @@ -30,7 +30,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_additional_responses_bad.py b/tests/test_additional_responses_bad.py index d2eb4d7a1..36df07f46 100644 --- a/tests/test_additional_responses_bad.py +++ b/tests/test_additional_responses_bad.py @@ -11,7 +11,7 @@ async def a(): openapi_schema = { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/a": { diff --git a/tests/test_additional_responses_custom_model_in_callback.py b/tests/test_additional_responses_custom_model_in_callback.py index 5c08eaa6d..397159142 100644 --- a/tests/test_additional_responses_custom_model_in_callback.py +++ b/tests/test_additional_responses_custom_model_in_callback.py @@ -32,7 +32,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/": { diff --git a/tests/test_additional_responses_custom_validationerror.py b/tests/test_additional_responses_custom_validationerror.py index 052602768..9fec5c96d 100644 --- a/tests/test_additional_responses_custom_validationerror.py +++ b/tests/test_additional_responses_custom_validationerror.py @@ -37,7 +37,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/a/{id}": { diff --git a/tests/test_additional_responses_default_validationerror.py b/tests/test_additional_responses_default_validationerror.py index 58de46ff6..153f04f57 100644 --- a/tests/test_additional_responses_default_validationerror.py +++ b/tests/test_additional_responses_default_validationerror.py @@ -16,7 +16,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/a/{id}": { diff --git a/tests/test_additional_responses_response_class.py b/tests/test_additional_responses_response_class.py index 6746760f0..68753561c 100644 --- a/tests/test_additional_responses_response_class.py +++ b/tests/test_additional_responses_response_class.py @@ -42,7 +42,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/a": { diff --git a/tests/test_additional_responses_router.py b/tests/test_additional_responses_router.py index 58d54b733..71cabc7c3 100644 --- a/tests/test_additional_responses_router.py +++ b/tests/test_additional_responses_router.py @@ -85,7 +85,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/a": { diff --git a/tests/test_annotated.py b/tests/test_annotated.py index a4f42b038..5a70c4541 100644 --- a/tests/test_annotated.py +++ b/tests/test_annotated.py @@ -118,7 +118,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/default": { diff --git a/tests/test_application.py b/tests/test_application.py index e5f2f4387..b036e67af 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -55,7 +55,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/api_route": { diff --git a/tests/test_custom_route_class.py b/tests/test_custom_route_class.py index d1b18ef1d..55374584b 100644 --- a/tests/test_custom_route_class.py +++ b/tests/test_custom_route_class.py @@ -75,7 +75,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/a/": { diff --git a/tests/test_dependency_duplicates.py b/tests/test_dependency_duplicates.py index 285fdf1ab..4f4f3166c 100644 --- a/tests/test_dependency_duplicates.py +++ b/tests/test_dependency_duplicates.py @@ -86,7 +86,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/with-duplicates": { diff --git a/tests/test_deprecated_openapi_prefix.py b/tests/test_deprecated_openapi_prefix.py index 688b9837f..ec7366d2a 100644 --- a/tests/test_deprecated_openapi_prefix.py +++ b/tests/test_deprecated_openapi_prefix.py @@ -22,7 +22,7 @@ def test_openapi(): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/app": { diff --git a/tests/test_duplicate_models_openapi.py b/tests/test_duplicate_models_openapi.py index 116b2006a..83e86d231 100644 --- a/tests/test_duplicate_models_openapi.py +++ b/tests/test_duplicate_models_openapi.py @@ -36,7 +36,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/": { diff --git a/tests/test_enforce_once_required_parameter.py b/tests/test_enforce_once_required_parameter.py index bf05aa585..b64f8341b 100644 --- a/tests/test_enforce_once_required_parameter.py +++ b/tests/test_enforce_once_required_parameter.py @@ -57,7 +57,7 @@ expected_schema = { } }, "info": {"title": "FastAPI", "version": "0.1.0"}, - "openapi": "3.0.2", + "openapi": "3.1.0", "paths": { "/foo": { "get": { diff --git a/tests/test_extra_routes.py b/tests/test_extra_routes.py index c0db62c19..fa95d061c 100644 --- a/tests/test_extra_routes.py +++ b/tests/test_extra_routes.py @@ -99,7 +99,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_filter_pydantic_sub_model.py b/tests/test_filter_pydantic_sub_model.py index 15b15f862..6ee928d07 100644 --- a/tests/test_filter_pydantic_sub_model.py +++ b/tests/test_filter_pydantic_sub_model.py @@ -66,7 +66,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/model/{name}": { diff --git a/tests/test_generate_unique_id_function.py b/tests/test_generate_unique_id_function.py index 0b519f859..c5ef5182b 100644 --- a/tests/test_generate_unique_id_function.py +++ b/tests/test_generate_unique_id_function.py @@ -48,7 +48,7 @@ def test_top_level_generate_unique_id(): response = client.get("/openapi.json") data = response.json() assert data == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/": { @@ -249,7 +249,7 @@ def test_router_overrides_generate_unique_id(): response = client.get("/openapi.json") data = response.json() assert data == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/": { @@ -450,7 +450,7 @@ def test_router_include_overrides_generate_unique_id(): response = client.get("/openapi.json") data = response.json() assert data == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/": { @@ -661,7 +661,7 @@ def test_subrouter_top_level_include_overrides_generate_unique_id(): response = client.get("/openapi.json") data = response.json() assert data == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/": { @@ -928,7 +928,7 @@ def test_router_path_operation_overrides_generate_unique_id(): response = client.get("/openapi.json") data = response.json() assert data == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/": { @@ -1136,7 +1136,7 @@ def test_app_path_operation_overrides_generate_unique_id(): response = client.get("/openapi.json") data = response.json() assert data == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/": { @@ -1353,7 +1353,7 @@ def test_callback_override_generate_unique_id(): response = client.get("/openapi.json") data = response.json() assert data == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/": { diff --git a/tests/test_get_request_body.py b/tests/test_get_request_body.py index 541147fa8..cc567b88f 100644 --- a/tests/test_get_request_body.py +++ b/tests/test_get_request_body.py @@ -29,7 +29,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/product": { diff --git a/tests/test_include_router_defaults_overrides.py b/tests/test_include_router_defaults_overrides.py index ced56c84d..33baa25e6 100644 --- a/tests/test_include_router_defaults_overrides.py +++ b/tests/test_include_router_defaults_overrides.py @@ -443,7 +443,7 @@ def test_openapi(): assert issubclass(w[-1].category, UserWarning) assert "Duplicate Operation ID" in str(w[-1].message) assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/override1": { diff --git a/tests/test_modules_same_name_body/test_main.py b/tests/test_modules_same_name_body/test_main.py index 1ebcaee8c..cc165bdca 100644 --- a/tests/test_modules_same_name_body/test_main.py +++ b/tests/test_modules_same_name_body/test_main.py @@ -35,7 +35,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/a/compute": { diff --git a/tests/test_multi_body_errors.py b/tests/test_multi_body_errors.py index 358684bc5..96043aa35 100644 --- a/tests/test_multi_body_errors.py +++ b/tests/test_multi_body_errors.py @@ -80,7 +80,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_multi_query_errors.py b/tests/test_multi_query_errors.py index e7a833f2b..c1f0678d0 100644 --- a/tests/test_multi_query_errors.py +++ b/tests/test_multi_query_errors.py @@ -46,7 +46,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_openapi_query_parameter_extension.py b/tests/test_openapi_query_parameter_extension.py index 8a9086ebe..6f62e6726 100644 --- a/tests/test_openapi_query_parameter_extension.py +++ b/tests/test_openapi_query_parameter_extension.py @@ -42,7 +42,7 @@ def test_openapi(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/": { diff --git a/tests/test_openapi_route_extensions.py b/tests/test_openapi_route_extensions.py index 943dc43f2..3a3099436 100644 --- a/tests/test_openapi_route_extensions.py +++ b/tests/test_openapi_route_extensions.py @@ -22,7 +22,7 @@ def test_openapi(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/": { diff --git a/tests/test_openapi_servers.py b/tests/test_openapi_servers.py index 26abeaa12..11cd795ac 100644 --- a/tests/test_openapi_servers.py +++ b/tests/test_openapi_servers.py @@ -30,7 +30,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "servers": [ {"url": "/", "description": "Default, relative server"}, diff --git a/tests/test_param_in_path_and_dependency.py b/tests/test_param_in_path_and_dependency.py index 0aef7ac7b..08eb0f40f 100644 --- a/tests/test_param_in_path_and_dependency.py +++ b/tests/test_param_in_path_and_dependency.py @@ -25,7 +25,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") data = response.json() assert data == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/{user_id}": { diff --git a/tests/test_param_include_in_schema.py b/tests/test_param_include_in_schema.py index d0c29f7b2..26201e9e2 100644 --- a/tests/test_param_include_in_schema.py +++ b/tests/test_param_include_in_schema.py @@ -34,7 +34,7 @@ async def hidden_query( openapi_schema = { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/hidden_cookie": { diff --git a/tests/test_put_no_body.py b/tests/test_put_no_body.py index a02d1152c..8f4c82532 100644 --- a/tests/test_put_no_body.py +++ b/tests/test_put_no_body.py @@ -28,7 +28,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_repeated_dependency_schema.py b/tests/test_repeated_dependency_schema.py index ca0305184..d7d0dfa05 100644 --- a/tests/test_repeated_dependency_schema.py +++ b/tests/test_repeated_dependency_schema.py @@ -50,7 +50,7 @@ schema = { } }, "info": {"title": "FastAPI", "version": "0.1.0"}, - "openapi": "3.0.2", + "openapi": "3.1.0", "paths": { "/": { "get": { diff --git a/tests/test_repeated_parameter_alias.py b/tests/test_repeated_parameter_alias.py index c656a161d..fd72eaab2 100644 --- a/tests/test_repeated_parameter_alias.py +++ b/tests/test_repeated_parameter_alias.py @@ -58,7 +58,7 @@ def test_openapi_schema(): } }, "info": {"title": "FastAPI", "version": "0.1.0"}, - "openapi": "3.0.2", + "openapi": "3.1.0", "paths": { "/{repeated_alias}": { "get": { diff --git a/tests/test_reponse_set_reponse_code_empty.py b/tests/test_reponse_set_reponse_code_empty.py index 14770fed0..bf3aa758c 100644 --- a/tests/test_reponse_set_reponse_code_empty.py +++ b/tests/test_reponse_set_reponse_code_empty.py @@ -32,7 +32,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/{id}": { diff --git a/tests/test_request_body_parameters_media_type.py b/tests/test_request_body_parameters_media_type.py index 32f6c6a72..8424bf551 100644 --- a/tests/test_request_body_parameters_media_type.py +++ b/tests/test_request_body_parameters_media_type.py @@ -41,7 +41,7 @@ def test_openapi_schema(): assert response.status_code == 200, response.text # insert_assert(response.json()) assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/products": { diff --git a/tests/test_response_by_alias.py b/tests/test_response_by_alias.py index 1861a40fa..c3ff5b1d2 100644 --- a/tests/test_response_by_alias.py +++ b/tests/test_response_by_alias.py @@ -138,7 +138,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/dict": { diff --git a/tests/test_response_class_no_mediatype.py b/tests/test_response_class_no_mediatype.py index 2d75c7535..706929ac3 100644 --- a/tests/test_response_class_no_mediatype.py +++ b/tests/test_response_class_no_mediatype.py @@ -42,7 +42,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/a": { diff --git a/tests/test_response_code_no_body.py b/tests/test_response_code_no_body.py index 3851a325d..3ca8708f1 100644 --- a/tests/test_response_code_no_body.py +++ b/tests/test_response_code_no_body.py @@ -50,7 +50,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/a": { diff --git a/tests/test_response_model_as_return_annotation.py b/tests/test_response_model_as_return_annotation.py index 7decdff7d..7a0cf47ec 100644 --- a/tests/test_response_model_as_return_annotation.py +++ b/tests/test_response_model_as_return_annotation.py @@ -507,7 +507,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/no_response_model-no_annotation-return_model": { diff --git a/tests/test_response_model_sub_types.py b/tests/test_response_model_sub_types.py index e462006ff..660bcee1b 100644 --- a/tests/test_response_model_sub_types.py +++ b/tests/test_response_model_sub_types.py @@ -50,7 +50,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/valid1": { diff --git a/tests/test_schema_extra_examples.py b/tests/test_schema_extra_examples.py index 41021a983..45caa1615 100644 --- a/tests/test_schema_extra_examples.py +++ b/tests/test_schema_extra_examples.py @@ -1,240 +1,220 @@ from typing import Union +import pytest from fastapi import Body, Cookie, FastAPI, Header, Path, Query from fastapi.testclient import TestClient from pydantic import BaseModel -app = FastAPI() - -class Item(BaseModel): - data: str - - class Config: - schema_extra = {"example": {"data": "Data in schema_extra"}} - - -@app.post("/schema_extra/") -def schema_extra(item: Item): - return item - - -@app.post("/example/") -def example(item: Item = Body(example={"data": "Data in Body example"})): - return item - - -@app.post("/examples/") -def examples( - item: Item = Body( - examples={ - "example1": { - "summary": "example1 summary", - "value": {"data": "Data in Body examples, example1"}, - }, - "example2": {"value": {"data": "Data in Body examples, example2"}}, - }, - ) -): - return item - - -@app.post("/example_examples/") -def example_examples( - item: Item = Body( - example={"data": "Overridden example"}, - examples={ - "example1": {"value": {"data": "examples example_examples 1"}}, - "example2": {"value": {"data": "examples example_examples 2"}}, - }, - ) -): - return item - - -# TODO: enable these tests once/if Form(embed=False) is supported -# TODO: In that case, define if File() should support example/examples too -# @app.post("/form_example") -# def form_example(firstname: str = Form(example="John")): -# return firstname - - -# @app.post("/form_examples") -# def form_examples( -# lastname: str = Form( -# ..., -# examples={ -# "example1": {"summary": "last name summary", "value": "Doe"}, -# "example2": {"value": "Doesn't"}, -# }, -# ), -# ): -# return lastname - - -# @app.post("/form_example_examples") -# def form_example_examples( -# lastname: str = Form( -# ..., -# example="Doe overridden", -# examples={ -# "example1": {"summary": "last name summary", "value": "Doe"}, -# "example2": {"value": "Doesn't"}, -# }, -# ), -# ): -# return lastname - - -@app.get("/path_example/{item_id}") -def path_example( - item_id: str = Path( - example="item_1", - ), -): - return item_id - - -@app.get("/path_examples/{item_id}") -def path_examples( - item_id: str = Path( - examples={ - "example1": {"summary": "item ID summary", "value": "item_1"}, - "example2": {"value": "item_2"}, - }, - ), -): - return item_id - - -@app.get("/path_example_examples/{item_id}") -def path_example_examples( - item_id: str = Path( - example="item_overridden", - examples={ - "example1": {"summary": "item ID summary", "value": "item_1"}, - "example2": {"value": "item_2"}, - }, - ), -): - return item_id - - -@app.get("/query_example/") -def query_example( - data: Union[str, None] = Query( - default=None, - example="query1", - ), -): - return data - - -@app.get("/query_examples/") -def query_examples( - data: Union[str, None] = Query( - default=None, - examples={ - "example1": {"summary": "Query example 1", "value": "query1"}, - "example2": {"value": "query2"}, - }, - ), -): - return data - - -@app.get("/query_example_examples/") -def query_example_examples( - data: Union[str, None] = Query( - default=None, - example="query_overridden", - examples={ - "example1": {"summary": "Query example 1", "value": "query1"}, - "example2": {"value": "query2"}, - }, - ), -): - return data - - -@app.get("/header_example/") -def header_example( - data: Union[str, None] = Header( - default=None, - example="header1", - ), -): - return data - - -@app.get("/header_examples/") -def header_examples( - data: Union[str, None] = Header( - default=None, - examples={ - "example1": {"summary": "header example 1", "value": "header1"}, - "example2": {"value": "header2"}, - }, - ), -): - return data - - -@app.get("/header_example_examples/") -def header_example_examples( - data: Union[str, None] = Header( - default=None, - example="header_overridden", - examples={ - "example1": {"summary": "Query example 1", "value": "header1"}, - "example2": {"value": "header2"}, - }, - ), -): - return data - - -@app.get("/cookie_example/") -def cookie_example( - data: Union[str, None] = Cookie( - default=None, - example="cookie1", - ), -): - return data - - -@app.get("/cookie_examples/") -def cookie_examples( - data: Union[str, None] = Cookie( - default=None, - examples={ - "example1": {"summary": "cookie example 1", "value": "cookie1"}, - "example2": {"value": "cookie2"}, - }, - ), -): - return data - - -@app.get("/cookie_example_examples/") -def cookie_example_examples( - data: Union[str, None] = Cookie( - default=None, - example="cookie_overridden", - examples={ - "example1": {"summary": "Query example 1", "value": "cookie1"}, - "example2": {"value": "cookie2"}, - }, - ), -): - return data - - -client = TestClient(app) +def create_app(): + app = FastAPI() + + class Item(BaseModel): + data: str + + class Config: + schema_extra = {"example": {"data": "Data in schema_extra"}} + + @app.post("/schema_extra/") + def schema_extra(item: Item): + return item + + with pytest.warns(DeprecationWarning): + + @app.post("/example/") + def example(item: Item = Body(example={"data": "Data in Body example"})): + return item + + @app.post("/examples/") + def examples( + item: Item = Body( + examples=[ + {"data": "Data in Body examples, example1"}, + {"data": "Data in Body examples, example2"}, + ], + ) + ): + return item + + with pytest.warns(DeprecationWarning): + + @app.post("/example_examples/") + def example_examples( + item: Item = Body( + example={"data": "Overridden example"}, + examples=[ + {"data": "examples example_examples 1"}, + {"data": "examples example_examples 2"}, + ], + ) + ): + return item + + # TODO: enable these tests once/if Form(embed=False) is supported + # TODO: In that case, define if File() should support example/examples too + # @app.post("/form_example") + # def form_example(firstname: str = Form(example="John")): + # return firstname + + # @app.post("/form_examples") + # def form_examples( + # lastname: str = Form( + # ..., + # examples={ + # "example1": {"summary": "last name summary", "value": "Doe"}, + # "example2": {"value": "Doesn't"}, + # }, + # ), + # ): + # return lastname + + # @app.post("/form_example_examples") + # def form_example_examples( + # lastname: str = Form( + # ..., + # example="Doe overridden", + # examples={ + # "example1": {"summary": "last name summary", "value": "Doe"}, + # "example2": {"value": "Doesn't"}, + # }, + # ), + # ): + # return lastname + + with pytest.warns(DeprecationWarning): + + @app.get("/path_example/{item_id}") + def path_example( + item_id: str = Path( + example="item_1", + ), + ): + return item_id + + @app.get("/path_examples/{item_id}") + def path_examples( + item_id: str = Path( + examples=["item_1", "item_2"], + ), + ): + return item_id + + with pytest.warns(DeprecationWarning): + + @app.get("/path_example_examples/{item_id}") + def path_example_examples( + item_id: str = Path( + example="item_overridden", + examples=["item_1", "item_2"], + ), + ): + return item_id + + with pytest.warns(DeprecationWarning): + + @app.get("/query_example/") + def query_example( + data: Union[str, None] = Query( + default=None, + example="query1", + ), + ): + return data + + @app.get("/query_examples/") + def query_examples( + data: Union[str, None] = Query( + default=None, + examples=["query1", "query2"], + ), + ): + return data + + with pytest.warns(DeprecationWarning): + + @app.get("/query_example_examples/") + def query_example_examples( + data: Union[str, None] = Query( + default=None, + example="query_overridden", + examples=["query1", "query2"], + ), + ): + return data + + with pytest.warns(DeprecationWarning): + + @app.get("/header_example/") + def header_example( + data: Union[str, None] = Header( + default=None, + example="header1", + ), + ): + return data + + @app.get("/header_examples/") + def header_examples( + data: Union[str, None] = Header( + default=None, + examples=[ + "header1", + "header2", + ], + ), + ): + return data + + with pytest.warns(DeprecationWarning): + + @app.get("/header_example_examples/") + def header_example_examples( + data: Union[str, None] = Header( + default=None, + example="header_overridden", + examples=["header1", "header2"], + ), + ): + return data + + with pytest.warns(DeprecationWarning): + + @app.get("/cookie_example/") + def cookie_example( + data: Union[str, None] = Cookie( + default=None, + example="cookie1", + ), + ): + return data + + @app.get("/cookie_examples/") + def cookie_examples( + data: Union[str, None] = Cookie( + default=None, + examples=["cookie1", "cookie2"], + ), + ): + return data + + with pytest.warns(DeprecationWarning): + + @app.get("/cookie_example_examples/") + def cookie_example_examples( + data: Union[str, None] = Cookie( + default=None, + example="cookie_overridden", + examples=["cookie1", "cookie2"], + ), + ): + return data + + return app def test_call_api(): + app = create_app() + client = TestClient(app) response = client.post("/schema_extra/", json={"data": "Foo"}) assert response.status_code == 200, response.text response = client.post("/example/", json={"data": "Foo"}) @@ -277,10 +257,12 @@ def test_openapi_schema(): * Body(example={}) overrides schema_extra in pydantic model * Body(examples{}) overrides Body(example={}) and schema_extra in pydantic model """ + app = create_app() + client = TestClient(app) response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/schema_extra/": { @@ -351,20 +333,14 @@ def test_openapi_schema(): "requestBody": { "content": { "application/json": { - "schema": {"$ref": "#/components/schemas/Item"}, - "examples": { - "example1": { - "summary": "example1 summary", - "value": { - "data": "Data in Body examples, example1" - }, - }, - "example2": { - "value": { - "data": "Data in Body examples, example2" - } - }, - }, + "schema": { + "allOf": [{"$ref": "#/components/schemas/Item"}], + "title": "Item", + "examples": [ + {"data": "Data in Body examples, example1"}, + {"data": "Data in Body examples, example2"}, + ], + } } }, "required": True, @@ -394,15 +370,15 @@ def test_openapi_schema(): "requestBody": { "content": { "application/json": { - "schema": {"$ref": "#/components/schemas/Item"}, - "examples": { - "example1": { - "value": {"data": "examples example_examples 1"} - }, - "example2": { - "value": {"data": "examples example_examples 2"} - }, + "schema": { + "allOf": [{"$ref": "#/components/schemas/Item"}], + "title": "Item", + "examples": [ + {"data": "examples example_examples 1"}, + {"data": "examples example_examples 2"}, + ], }, + "example": {"data": "Overridden example"}, } }, "required": True, @@ -463,13 +439,10 @@ def test_openapi_schema(): "parameters": [ { "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "examples": { - "example1": { - "summary": "item ID summary", - "value": "item_1", - }, - "example2": {"value": "item_2"}, + "schema": { + "title": "Item Id", + "type": "string", + "examples": ["item_1", "item_2"], }, "name": "item_id", "in": "path", @@ -500,14 +473,12 @@ def test_openapi_schema(): "parameters": [ { "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "examples": { - "example1": { - "summary": "item ID summary", - "value": "item_1", - }, - "example2": {"value": "item_2"}, + "schema": { + "title": "Item Id", + "type": "string", + "examples": ["item_1", "item_2"], }, + "example": "item_overridden", "name": "item_id", "in": "path", } @@ -568,13 +539,10 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": {"title": "Data", "type": "string"}, - "examples": { - "example1": { - "summary": "Query example 1", - "value": "query1", - }, - "example2": {"value": "query2"}, + "schema": { + "type": "string", + "title": "Data", + "examples": ["query1", "query2"], }, "name": "data", "in": "query", @@ -605,14 +573,12 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": {"title": "Data", "type": "string"}, - "examples": { - "example1": { - "summary": "Query example 1", - "value": "query1", - }, - "example2": {"value": "query2"}, + "schema": { + "type": "string", + "title": "Data", + "examples": ["query1", "query2"], }, + "example": "query_overridden", "name": "data", "in": "query", } @@ -642,7 +608,7 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": {"title": "Data", "type": "string"}, + "schema": {"type": "string", "title": "Data"}, "example": "header1", "name": "data", "in": "header", @@ -673,13 +639,10 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": {"title": "Data", "type": "string"}, - "examples": { - "example1": { - "summary": "header example 1", - "value": "header1", - }, - "example2": {"value": "header2"}, + "schema": { + "type": "string", + "title": "Data", + "examples": ["header1", "header2"], }, "name": "data", "in": "header", @@ -710,14 +673,12 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": {"title": "Data", "type": "string"}, - "examples": { - "example1": { - "summary": "Query example 1", - "value": "header1", - }, - "example2": {"value": "header2"}, + "schema": { + "type": "string", + "title": "Data", + "examples": ["header1", "header2"], }, + "example": "header_overridden", "name": "data", "in": "header", } @@ -747,7 +708,7 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": {"title": "Data", "type": "string"}, + "schema": {"type": "string", "title": "Data"}, "example": "cookie1", "name": "data", "in": "cookie", @@ -778,13 +739,10 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": {"title": "Data", "type": "string"}, - "examples": { - "example1": { - "summary": "cookie example 1", - "value": "cookie1", - }, - "example2": {"value": "cookie2"}, + "schema": { + "type": "string", + "title": "Data", + "examples": ["cookie1", "cookie2"], }, "name": "data", "in": "cookie", @@ -815,14 +773,12 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": {"title": "Data", "type": "string"}, - "examples": { - "example1": { - "summary": "Query example 1", - "value": "cookie1", - }, - "example2": {"value": "cookie2"}, + "schema": { + "type": "string", + "title": "Data", + "examples": ["cookie1", "cookie2"], }, + "example": "cookie_overridden", "name": "data", "in": "cookie", } diff --git a/tests/test_security_api_key_cookie.py b/tests/test_security_api_key_cookie.py index b1c648c55..4ddb8e2ee 100644 --- a/tests/test_security_api_key_cookie.py +++ b/tests/test_security_api_key_cookie.py @@ -41,7 +41,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/me": { diff --git a/tests/test_security_api_key_cookie_description.py b/tests/test_security_api_key_cookie_description.py index ac8b4abf8..d99d616e0 100644 --- a/tests/test_security_api_key_cookie_description.py +++ b/tests/test_security_api_key_cookie_description.py @@ -41,7 +41,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/me": { diff --git a/tests/test_security_api_key_cookie_optional.py b/tests/test_security_api_key_cookie_optional.py index b8c440c9d..cb5590168 100644 --- a/tests/test_security_api_key_cookie_optional.py +++ b/tests/test_security_api_key_cookie_optional.py @@ -48,7 +48,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/me": { diff --git a/tests/test_security_api_key_header.py b/tests/test_security_api_key_header.py index 96ad80b54..1ff883703 100644 --- a/tests/test_security_api_key_header.py +++ b/tests/test_security_api_key_header.py @@ -41,7 +41,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/me": { diff --git a/tests/test_security_api_key_header_description.py b/tests/test_security_api_key_header_description.py index 382f53dd7..27f9d0f29 100644 --- a/tests/test_security_api_key_header_description.py +++ b/tests/test_security_api_key_header_description.py @@ -41,7 +41,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/me": { diff --git a/tests/test_security_api_key_header_optional.py b/tests/test_security_api_key_header_optional.py index adfb20ba0..6f9682a64 100644 --- a/tests/test_security_api_key_header_optional.py +++ b/tests/test_security_api_key_header_optional.py @@ -47,7 +47,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/me": { diff --git a/tests/test_security_api_key_query.py b/tests/test_security_api_key_query.py index da98eafd6..dc7a0a621 100644 --- a/tests/test_security_api_key_query.py +++ b/tests/test_security_api_key_query.py @@ -41,7 +41,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/me": { diff --git a/tests/test_security_api_key_query_description.py b/tests/test_security_api_key_query_description.py index 3c08afc5f..35dc7743a 100644 --- a/tests/test_security_api_key_query_description.py +++ b/tests/test_security_api_key_query_description.py @@ -41,7 +41,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/me": { diff --git a/tests/test_security_api_key_query_optional.py b/tests/test_security_api_key_query_optional.py index 99a26cfd0..4cc134bd4 100644 --- a/tests/test_security_api_key_query_optional.py +++ b/tests/test_security_api_key_query_optional.py @@ -47,7 +47,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/me": { diff --git a/tests/test_security_http_base.py b/tests/test_security_http_base.py index d3a754203..51928bafd 100644 --- a/tests/test_security_http_base.py +++ b/tests/test_security_http_base.py @@ -31,7 +31,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/me": { diff --git a/tests/test_security_http_base_description.py b/tests/test_security_http_base_description.py index 3d7d15016..bc79f3242 100644 --- a/tests/test_security_http_base_description.py +++ b/tests/test_security_http_base_description.py @@ -31,7 +31,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/me": { diff --git a/tests/test_security_http_base_optional.py b/tests/test_security_http_base_optional.py index 180c9110e..dd4d76843 100644 --- a/tests/test_security_http_base_optional.py +++ b/tests/test_security_http_base_optional.py @@ -37,7 +37,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/me": { diff --git a/tests/test_security_http_basic_optional.py b/tests/test_security_http_basic_optional.py index 7e7fcac32..9b6cb6c45 100644 --- a/tests/test_security_http_basic_optional.py +++ b/tests/test_security_http_basic_optional.py @@ -54,7 +54,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/me": { diff --git a/tests/test_security_http_basic_realm.py b/tests/test_security_http_basic_realm.py index 470afd662..9fc33971a 100644 --- a/tests/test_security_http_basic_realm.py +++ b/tests/test_security_http_basic_realm.py @@ -52,7 +52,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/me": { diff --git a/tests/test_security_http_basic_realm_description.py b/tests/test_security_http_basic_realm_description.py index 44289007b..02122442e 100644 --- a/tests/test_security_http_basic_realm_description.py +++ b/tests/test_security_http_basic_realm_description.py @@ -52,7 +52,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/me": { diff --git a/tests/test_security_http_bearer.py b/tests/test_security_http_bearer.py index f24869fc3..5b9e2d691 100644 --- a/tests/test_security_http_bearer.py +++ b/tests/test_security_http_bearer.py @@ -37,7 +37,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/me": { diff --git a/tests/test_security_http_bearer_description.py b/tests/test_security_http_bearer_description.py index 6d5ad0b8e..2f11c3a14 100644 --- a/tests/test_security_http_bearer_description.py +++ b/tests/test_security_http_bearer_description.py @@ -37,7 +37,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/me": { diff --git a/tests/test_security_http_bearer_optional.py b/tests/test_security_http_bearer_optional.py index b596ac730..943da2ee2 100644 --- a/tests/test_security_http_bearer_optional.py +++ b/tests/test_security_http_bearer_optional.py @@ -43,7 +43,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/me": { diff --git a/tests/test_security_http_digest.py b/tests/test_security_http_digest.py index 2a25efe02..133d35763 100644 --- a/tests/test_security_http_digest.py +++ b/tests/test_security_http_digest.py @@ -39,7 +39,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/me": { diff --git a/tests/test_security_http_digest_description.py b/tests/test_security_http_digest_description.py index 721f7cfde..4e31a0c00 100644 --- a/tests/test_security_http_digest_description.py +++ b/tests/test_security_http_digest_description.py @@ -39,7 +39,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/me": { diff --git a/tests/test_security_http_digest_optional.py b/tests/test_security_http_digest_optional.py index d4c3597bc..1e6eb8bd7 100644 --- a/tests/test_security_http_digest_optional.py +++ b/tests/test_security_http_digest_optional.py @@ -45,7 +45,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/me": { diff --git a/tests/test_security_oauth2.py b/tests/test_security_oauth2.py index 23dce7cfa..73d1b7d94 100644 --- a/tests/test_security_oauth2.py +++ b/tests/test_security_oauth2.py @@ -135,7 +135,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/login": { diff --git a/tests/test_security_oauth2_authorization_code_bearer.py b/tests/test_security_oauth2_authorization_code_bearer.py index 6df81528d..f2097b149 100644 --- a/tests/test_security_oauth2_authorization_code_bearer.py +++ b/tests/test_security_oauth2_authorization_code_bearer.py @@ -41,7 +41,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_security_oauth2_authorization_code_bearer_description.py b/tests/test_security_oauth2_authorization_code_bearer_description.py index c119abde4..5386fbbd9 100644 --- a/tests/test_security_oauth2_authorization_code_bearer_description.py +++ b/tests/test_security_oauth2_authorization_code_bearer_description.py @@ -44,7 +44,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_security_oauth2_optional.py b/tests/test_security_oauth2_optional.py index 3ef9f4a8d..b24c1b58f 100644 --- a/tests/test_security_oauth2_optional.py +++ b/tests/test_security_oauth2_optional.py @@ -139,7 +139,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/login": { diff --git a/tests/test_security_oauth2_optional_description.py b/tests/test_security_oauth2_optional_description.py index b6425fde4..cda635151 100644 --- a/tests/test_security_oauth2_optional_description.py +++ b/tests/test_security_oauth2_optional_description.py @@ -140,7 +140,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/login": { diff --git a/tests/test_security_oauth2_password_bearer_optional.py b/tests/test_security_oauth2_password_bearer_optional.py index e5dcbb553..4c9362c3e 100644 --- a/tests/test_security_oauth2_password_bearer_optional.py +++ b/tests/test_security_oauth2_password_bearer_optional.py @@ -41,7 +41,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_security_oauth2_password_bearer_optional_description.py b/tests/test_security_oauth2_password_bearer_optional_description.py index 9ff48e715..6e6ea846c 100644 --- a/tests/test_security_oauth2_password_bearer_optional_description.py +++ b/tests/test_security_oauth2_password_bearer_optional_description.py @@ -45,7 +45,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_security_openid_connect.py b/tests/test_security_openid_connect.py index 206de6574..1e322e640 100644 --- a/tests/test_security_openid_connect.py +++ b/tests/test_security_openid_connect.py @@ -47,7 +47,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/me": { diff --git a/tests/test_security_openid_connect_description.py b/tests/test_security_openid_connect_description.py index 5884de793..44cf57f86 100644 --- a/tests/test_security_openid_connect_description.py +++ b/tests/test_security_openid_connect_description.py @@ -49,7 +49,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/me": { diff --git a/tests/test_security_openid_connect_optional.py b/tests/test_security_openid_connect_optional.py index 8ac719118..e817434b0 100644 --- a/tests/test_security_openid_connect_optional.py +++ b/tests/test_security_openid_connect_optional.py @@ -53,7 +53,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/me": { diff --git a/tests/test_starlette_exception.py b/tests/test_starlette_exception.py index 96f835b93..229fe8016 100644 --- a/tests/test_starlette_exception.py +++ b/tests/test_starlette_exception.py @@ -80,7 +80,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/http-no-body-statuscode-exception": { diff --git a/tests/test_sub_callbacks.py b/tests/test_sub_callbacks.py index c5a0237e8..dce3ea5e2 100644 --- a/tests/test_sub_callbacks.py +++ b/tests/test_sub_callbacks.py @@ -87,7 +87,7 @@ def test_openapi_schema(): with client: response = client.get("/openapi.json") assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/invoices/": { diff --git a/tests/test_tuples.py b/tests/test_tuples.py index 4fa1f7a13..c37a25ca6 100644 --- a/tests/test_tuples.py +++ b/tests/test_tuples.py @@ -86,7 +86,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/model-with-tuple/": { diff --git a/tests/test_tutorial/test_additional_responses/test_tutorial001.py b/tests/test_tutorial/test_additional_responses/test_tutorial001.py index 3d6267023..3afeaff84 100644 --- a/tests/test_tutorial/test_additional_responses/test_tutorial001.py +++ b/tests/test_tutorial/test_additional_responses/test_tutorial001.py @@ -21,7 +21,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_additional_responses/test_tutorial002.py b/tests/test_tutorial/test_additional_responses/test_tutorial002.py index 6182ed507..8e084e152 100644 --- a/tests/test_tutorial/test_additional_responses/test_tutorial002.py +++ b/tests/test_tutorial/test_additional_responses/test_tutorial002.py @@ -27,7 +27,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_additional_responses/test_tutorial003.py b/tests/test_tutorial/test_additional_responses/test_tutorial003.py index 77568d9d4..bd34d2938 100644 --- a/tests/test_tutorial/test_additional_responses/test_tutorial003.py +++ b/tests/test_tutorial/test_additional_responses/test_tutorial003.py @@ -21,7 +21,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_additional_responses/test_tutorial004.py b/tests/test_tutorial/test_additional_responses/test_tutorial004.py index 3fbd91e5c..5fc8b81ca 100644 --- a/tests/test_tutorial/test_additional_responses/test_tutorial004.py +++ b/tests/test_tutorial/test_additional_responses/test_tutorial004.py @@ -27,7 +27,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_async_sql_databases/test_tutorial001.py b/tests/test_tutorial/test_async_sql_databases/test_tutorial001.py index 3da362a50..8126cdcc6 100644 --- a/tests/test_tutorial/test_async_sql_databases/test_tutorial001.py +++ b/tests/test_tutorial/test_async_sql_databases/test_tutorial001.py @@ -22,7 +22,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/notes/": { diff --git a/tests/test_tutorial/test_behind_a_proxy/test_tutorial001.py b/tests/test_tutorial/test_behind_a_proxy/test_tutorial001.py index 7533a1b68..a070f850f 100644 --- a/tests/test_tutorial/test_behind_a_proxy/test_tutorial001.py +++ b/tests/test_tutorial/test_behind_a_proxy/test_tutorial001.py @@ -15,7 +15,7 @@ def test_openapi(): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/app": { diff --git a/tests/test_tutorial/test_behind_a_proxy/test_tutorial002.py b/tests/test_tutorial/test_behind_a_proxy/test_tutorial002.py index 930ab3bf5..ce791e215 100644 --- a/tests/test_tutorial/test_behind_a_proxy/test_tutorial002.py +++ b/tests/test_tutorial/test_behind_a_proxy/test_tutorial002.py @@ -15,7 +15,7 @@ def test_openapi(): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/app": { diff --git a/tests/test_tutorial/test_behind_a_proxy/test_tutorial003.py b/tests/test_tutorial/test_behind_a_proxy/test_tutorial003.py index ae8f1a495..0ae9f4f93 100644 --- a/tests/test_tutorial/test_behind_a_proxy/test_tutorial003.py +++ b/tests/test_tutorial/test_behind_a_proxy/test_tutorial003.py @@ -15,7 +15,7 @@ def test_openapi(): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "servers": [ {"url": "/api/v1"}, diff --git a/tests/test_tutorial/test_behind_a_proxy/test_tutorial004.py b/tests/test_tutorial/test_behind_a_proxy/test_tutorial004.py index e67ad1cb1..576a411a4 100644 --- a/tests/test_tutorial/test_behind_a_proxy/test_tutorial004.py +++ b/tests/test_tutorial/test_behind_a_proxy/test_tutorial004.py @@ -15,7 +15,7 @@ def test_openapi(): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "servers": [ {"url": "https://stag.example.com", "description": "Staging environment"}, diff --git a/tests/test_tutorial/test_bigger_applications/test_main.py b/tests/test_tutorial/test_bigger_applications/test_main.py index a13decd75..7da663435 100644 --- a/tests/test_tutorial/test_bigger_applications/test_main.py +++ b/tests/test_tutorial/test_bigger_applications/test_main.py @@ -166,7 +166,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/": { diff --git a/tests/test_tutorial/test_bigger_applications/test_main_an.py b/tests/test_tutorial/test_bigger_applications/test_main_an.py index 64e19c3f3..8f42d9dd1 100644 --- a/tests/test_tutorial/test_bigger_applications/test_main_an.py +++ b/tests/test_tutorial/test_bigger_applications/test_main_an.py @@ -166,7 +166,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/": { diff --git a/tests/test_tutorial/test_bigger_applications/test_main_an_py39.py b/tests/test_tutorial/test_bigger_applications/test_main_an_py39.py index 70c86b4d7..44694e371 100644 --- a/tests/test_tutorial/test_bigger_applications/test_main_an_py39.py +++ b/tests/test_tutorial/test_bigger_applications/test_main_an_py39.py @@ -181,7 +181,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/": { diff --git a/tests/test_tutorial/test_body/test_tutorial001.py b/tests/test_tutorial/test_body/test_tutorial001.py index cd1209ade..469198e0f 100644 --- a/tests/test_tutorial/test_body/test_tutorial001.py +++ b/tests/test_tutorial/test_body/test_tutorial001.py @@ -200,7 +200,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_body/test_tutorial001_py310.py b/tests/test_tutorial/test_body/test_tutorial001_py310.py index 5ebcbbf57..a68b4e044 100644 --- a/tests/test_tutorial/test_body/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_body/test_tutorial001_py310.py @@ -215,7 +215,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_body_fields/test_tutorial001.py b/tests/test_tutorial/test_body_fields/test_tutorial001.py index a7ea0e949..4999cbf6b 100644 --- a/tests/test_tutorial/test_body_fields/test_tutorial001.py +++ b/tests/test_tutorial/test_body_fields/test_tutorial001.py @@ -64,7 +64,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_body_fields/test_tutorial001_an.py b/tests/test_tutorial/test_body_fields/test_tutorial001_an.py index 32f996ecb..011946d07 100644 --- a/tests/test_tutorial/test_body_fields/test_tutorial001_an.py +++ b/tests/test_tutorial/test_body_fields/test_tutorial001_an.py @@ -64,7 +64,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_body_fields/test_tutorial001_an_py310.py b/tests/test_tutorial/test_body_fields/test_tutorial001_an_py310.py index 20e032fcd..e7dcb54e9 100644 --- a/tests/test_tutorial/test_body_fields/test_tutorial001_an_py310.py +++ b/tests/test_tutorial/test_body_fields/test_tutorial001_an_py310.py @@ -72,7 +72,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_body_fields/test_tutorial001_an_py39.py b/tests/test_tutorial/test_body_fields/test_tutorial001_an_py39.py index e3baf5f2b..f1015a03b 100644 --- a/tests/test_tutorial/test_body_fields/test_tutorial001_an_py39.py +++ b/tests/test_tutorial/test_body_fields/test_tutorial001_an_py39.py @@ -72,7 +72,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_body_fields/test_tutorial001_py310.py b/tests/test_tutorial/test_body_fields/test_tutorial001_py310.py index 4c2f48674..29c8ef4e9 100644 --- a/tests/test_tutorial/test_body_fields/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_body_fields/test_tutorial001_py310.py @@ -72,7 +72,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial001.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial001.py index 496ab38fb..ce41a4283 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial001.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial001.py @@ -50,7 +50,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an.py index 74a8a9b2e..acc4cfadc 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an.py @@ -50,7 +50,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py310.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py310.py index 9c764b6d1..a8dc02a6c 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py310.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py310.py @@ -58,7 +58,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py39.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py39.py index 0cca29433..f31fee78e 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py39.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py39.py @@ -58,7 +58,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial001_py310.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_py310.py index 3b61e717e..0e46df253 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_py310.py @@ -58,7 +58,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial003.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial003.py index b34377a28..8555cf88c 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial003.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial003.py @@ -90,7 +90,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an.py index 9b8d5e15b..f4d300cc5 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an.py @@ -90,7 +90,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py310.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py310.py index f8af555fc..afe2b2c20 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py310.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py310.py @@ -98,7 +98,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py39.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py39.py index 06e2c3146..033d5892e 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py39.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py39.py @@ -98,7 +98,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_py310.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_py310.py index 82c5fb101..8fcc00013 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_py310.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_py310.py @@ -98,7 +98,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_body_nested_models/test_tutorial009.py b/tests/test_tutorial/test_body_nested_models/test_tutorial009.py index 378c24197..ac39cd93f 100644 --- a/tests/test_tutorial/test_body_nested_models/test_tutorial009.py +++ b/tests/test_tutorial/test_body_nested_models/test_tutorial009.py @@ -31,7 +31,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/index-weights/": { diff --git a/tests/test_tutorial/test_body_nested_models/test_tutorial009_py39.py b/tests/test_tutorial/test_body_nested_models/test_tutorial009_py39.py index 5ca63a92b..0800abe29 100644 --- a/tests/test_tutorial/test_body_nested_models/test_tutorial009_py39.py +++ b/tests/test_tutorial/test_body_nested_models/test_tutorial009_py39.py @@ -41,7 +41,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/index-weights/": { diff --git a/tests/test_tutorial/test_body_updates/test_tutorial001.py b/tests/test_tutorial/test_body_updates/test_tutorial001.py index 939bf44e0..151b4b917 100644 --- a/tests/test_tutorial/test_body_updates/test_tutorial001.py +++ b/tests/test_tutorial/test_body_updates/test_tutorial001.py @@ -34,7 +34,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_body_updates/test_tutorial001_py310.py b/tests/test_tutorial/test_body_updates/test_tutorial001_py310.py index 5f50f2071..c4b4b9df3 100644 --- a/tests/test_tutorial/test_body_updates/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_body_updates/test_tutorial001_py310.py @@ -44,7 +44,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_body_updates/test_tutorial001_py39.py b/tests/test_tutorial/test_body_updates/test_tutorial001_py39.py index d4fdabce6..940b4b3b8 100644 --- a/tests/test_tutorial/test_body_updates/test_tutorial001_py39.py +++ b/tests/test_tutorial/test_body_updates/test_tutorial001_py39.py @@ -44,7 +44,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_conditional_openapi/test_tutorial001.py b/tests/test_tutorial/test_conditional_openapi/test_tutorial001.py index c1d8fd805..a43394ab1 100644 --- a/tests/test_tutorial/test_conditional_openapi/test_tutorial001.py +++ b/tests/test_tutorial/test_conditional_openapi/test_tutorial001.py @@ -33,7 +33,7 @@ def test_default_openapi(): assert response.status_code == 200, response.text response = client.get("/openapi.json") assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/": { diff --git a/tests/test_tutorial/test_cookie_params/test_tutorial001.py b/tests/test_tutorial/test_cookie_params/test_tutorial001.py index c3511d129..902bed843 100644 --- a/tests/test_tutorial/test_cookie_params/test_tutorial001.py +++ b/tests/test_tutorial/test_cookie_params/test_tutorial001.py @@ -30,7 +30,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_cookie_params/test_tutorial001_an.py b/tests/test_tutorial/test_cookie_params/test_tutorial001_an.py index f4f94c09d..aa5807844 100644 --- a/tests/test_tutorial/test_cookie_params/test_tutorial001_an.py +++ b/tests/test_tutorial/test_cookie_params/test_tutorial001_an.py @@ -30,7 +30,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py310.py b/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py310.py index a80f10f81..ffb55d4e1 100644 --- a/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py310.py +++ b/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py310.py @@ -36,7 +36,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py39.py b/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py39.py index 1be898c09..9bc38effd 100644 --- a/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py39.py +++ b/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py39.py @@ -36,7 +36,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_cookie_params/test_tutorial001_py310.py b/tests/test_tutorial/test_cookie_params/test_tutorial001_py310.py index 7ba542c90..bb2953ef6 100644 --- a/tests/test_tutorial/test_cookie_params/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_cookie_params/test_tutorial001_py310.py @@ -36,7 +36,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_custom_response/test_tutorial001.py b/tests/test_tutorial/test_custom_response/test_tutorial001.py index da2ca8d62..fc8362467 100644 --- a/tests/test_tutorial/test_custom_response/test_tutorial001.py +++ b/tests/test_tutorial/test_custom_response/test_tutorial001.py @@ -15,7 +15,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_custom_response/test_tutorial001b.py b/tests/test_tutorial/test_custom_response/test_tutorial001b.py index f681f5a9d..91e5c501e 100644 --- a/tests/test_tutorial/test_custom_response/test_tutorial001b.py +++ b/tests/test_tutorial/test_custom_response/test_tutorial001b.py @@ -15,7 +15,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_custom_response/test_tutorial004.py b/tests/test_tutorial/test_custom_response/test_tutorial004.py index ef0ba3446..de60574f5 100644 --- a/tests/test_tutorial/test_custom_response/test_tutorial004.py +++ b/tests/test_tutorial/test_custom_response/test_tutorial004.py @@ -27,7 +27,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_custom_response/test_tutorial005.py b/tests/test_tutorial/test_custom_response/test_tutorial005.py index e4b5c1546..889bf3e92 100644 --- a/tests/test_tutorial/test_custom_response/test_tutorial005.py +++ b/tests/test_tutorial/test_custom_response/test_tutorial005.py @@ -15,7 +15,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/": { diff --git a/tests/test_tutorial/test_custom_response/test_tutorial006.py b/tests/test_tutorial/test_custom_response/test_tutorial006.py index 9f1b07bee..2d0a2cd3f 100644 --- a/tests/test_tutorial/test_custom_response/test_tutorial006.py +++ b/tests/test_tutorial/test_custom_response/test_tutorial006.py @@ -15,7 +15,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/typer": { diff --git a/tests/test_tutorial/test_custom_response/test_tutorial006b.py b/tests/test_tutorial/test_custom_response/test_tutorial006b.py index cf204cbc0..1739fd457 100644 --- a/tests/test_tutorial/test_custom_response/test_tutorial006b.py +++ b/tests/test_tutorial/test_custom_response/test_tutorial006b.py @@ -15,7 +15,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/fastapi": { diff --git a/tests/test_tutorial/test_custom_response/test_tutorial006c.py b/tests/test_tutorial/test_custom_response/test_tutorial006c.py index f196899ac..51aa1833d 100644 --- a/tests/test_tutorial/test_custom_response/test_tutorial006c.py +++ b/tests/test_tutorial/test_custom_response/test_tutorial006c.py @@ -15,7 +15,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/pydantic": { diff --git a/tests/test_tutorial/test_dataclasses/test_tutorial001.py b/tests/test_tutorial/test_dataclasses/test_tutorial001.py index 26ae04c51..e20c0efe9 100644 --- a/tests/test_tutorial/test_dataclasses/test_tutorial001.py +++ b/tests/test_tutorial/test_dataclasses/test_tutorial001.py @@ -34,7 +34,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_dataclasses/test_tutorial002.py b/tests/test_tutorial/test_dataclasses/test_tutorial002.py index 675e826b1..e122239d8 100644 --- a/tests/test_tutorial/test_dataclasses/test_tutorial002.py +++ b/tests/test_tutorial/test_dataclasses/test_tutorial002.py @@ -22,7 +22,7 @@ def test_openapi_schema(): assert response.status_code == 200 data = response.json() assert data == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/next": { diff --git a/tests/test_tutorial/test_dataclasses/test_tutorial003.py b/tests/test_tutorial/test_dataclasses/test_tutorial003.py index f3378fe62..204426e8b 100644 --- a/tests/test_tutorial/test_dataclasses/test_tutorial003.py +++ b/tests/test_tutorial/test_dataclasses/test_tutorial003.py @@ -55,7 +55,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/authors/{author_id}/items/": { diff --git a/tests/test_tutorial/test_dependencies/test_tutorial001.py b/tests/test_tutorial/test_dependencies/test_tutorial001.py index 974b9304f..a8e564ebe 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial001.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial001.py @@ -26,7 +26,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_dependencies/test_tutorial001_an.py b/tests/test_tutorial/test_dependencies/test_tutorial001_an.py index b1ca27ff8..4e6a329f4 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial001_an.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial001_an.py @@ -26,7 +26,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_dependencies/test_tutorial001_an_py310.py b/tests/test_tutorial/test_dependencies/test_tutorial001_an_py310.py index 70bed03f6..205aee908 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial001_an_py310.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial001_an_py310.py @@ -34,7 +34,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_dependencies/test_tutorial001_an_py39.py b/tests/test_tutorial/test_dependencies/test_tutorial001_an_py39.py index 9c5723be8..73593ea55 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial001_an_py39.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial001_an_py39.py @@ -34,7 +34,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_dependencies/test_tutorial001_py310.py b/tests/test_tutorial/test_dependencies/test_tutorial001_py310.py index 1bcde4e9f..10bf84fb5 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial001_py310.py @@ -34,7 +34,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_dependencies/test_tutorial004.py b/tests/test_tutorial/test_dependencies/test_tutorial004.py index 298bc290d..d16fd9ef7 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial004.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial004.py @@ -64,7 +64,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_dependencies/test_tutorial004_an.py b/tests/test_tutorial/test_dependencies/test_tutorial004_an.py index f985be8df..46fe97fb2 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial004_an.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial004_an.py @@ -64,7 +64,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_dependencies/test_tutorial004_an_py310.py b/tests/test_tutorial/test_dependencies/test_tutorial004_an_py310.py index fc0286702..c6a0fc665 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial004_an_py310.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial004_an_py310.py @@ -72,7 +72,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_dependencies/test_tutorial004_an_py39.py b/tests/test_tutorial/test_dependencies/test_tutorial004_an_py39.py index 1e37673ed..30431cd29 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial004_an_py39.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial004_an_py39.py @@ -72,7 +72,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_dependencies/test_tutorial004_py310.py b/tests/test_tutorial/test_dependencies/test_tutorial004_py310.py index ab936ccdc..9793c8c33 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial004_py310.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial004_py310.py @@ -72,7 +72,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_dependencies/test_tutorial006.py b/tests/test_tutorial/test_dependencies/test_tutorial006.py index 2e9c82d71..6fac9f8eb 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial006.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial006.py @@ -54,7 +54,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_dependencies/test_tutorial006_an.py b/tests/test_tutorial/test_dependencies/test_tutorial006_an.py index 919066dca..810537e48 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial006_an.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial006_an.py @@ -54,7 +54,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_dependencies/test_tutorial006_an_py39.py b/tests/test_tutorial/test_dependencies/test_tutorial006_an_py39.py index c23718479..f17cbcfc7 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial006_an_py39.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial006_an_py39.py @@ -66,7 +66,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_dependencies/test_tutorial012.py b/tests/test_tutorial/test_dependencies/test_tutorial012.py index b92b96c01..af1fcde55 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial012.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial012.py @@ -99,7 +99,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_dependencies/test_tutorial012_an.py b/tests/test_tutorial/test_dependencies/test_tutorial012_an.py index 2ddb7bb53..c33d51d87 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial012_an.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial012_an.py @@ -99,7 +99,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_dependencies/test_tutorial012_an_py39.py b/tests/test_tutorial/test_dependencies/test_tutorial012_an_py39.py index 595c83a53..d7bd756b5 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial012_an_py39.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial012_an_py39.py @@ -115,7 +115,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_events/test_tutorial001.py b/tests/test_tutorial/test_events/test_tutorial001.py index 52f9beed5..a5bb299ac 100644 --- a/tests/test_tutorial/test_events/test_tutorial001.py +++ b/tests/test_tutorial/test_events/test_tutorial001.py @@ -15,7 +15,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_events/test_tutorial002.py b/tests/test_tutorial/test_events/test_tutorial002.py index 882d41aa5..81cbf4ab6 100644 --- a/tests/test_tutorial/test_events/test_tutorial002.py +++ b/tests/test_tutorial/test_events/test_tutorial002.py @@ -17,7 +17,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_events/test_tutorial003.py b/tests/test_tutorial/test_events/test_tutorial003.py index b2820b63c..0ad1a1f8b 100644 --- a/tests/test_tutorial/test_events/test_tutorial003.py +++ b/tests/test_tutorial/test_events/test_tutorial003.py @@ -22,7 +22,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/predict": { diff --git a/tests/test_tutorial/test_extending_openapi/test_tutorial001.py b/tests/test_tutorial/test_extending_openapi/test_tutorial001.py index 6e71bb2de..a85a31350 100644 --- a/tests/test_tutorial/test_extending_openapi/test_tutorial001.py +++ b/tests/test_tutorial/test_extending_openapi/test_tutorial001.py @@ -15,11 +15,12 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": { "title": "Custom title", + "summary": "This is a very custom OpenAPI schema", + "description": "Here's a longer description of the custom **OpenAPI** schema", "version": "2.5.0", - "description": "This is a very custom OpenAPI schema", "x-logo": { "url": "https://fastapi.tiangolo.com/img/logo-margin/logo-teal.png" }, diff --git a/tests/test_tutorial/test_extra_data_types/test_tutorial001.py b/tests/test_tutorial/test_extra_data_types/test_tutorial001.py index 07a834990..39d2005ab 100644 --- a/tests/test_tutorial/test_extra_data_types/test_tutorial001.py +++ b/tests/test_tutorial/test_extra_data_types/test_tutorial001.py @@ -30,7 +30,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_extra_data_types/test_tutorial001_an.py b/tests/test_tutorial/test_extra_data_types/test_tutorial001_an.py index 76836d447..3e497a291 100644 --- a/tests/test_tutorial/test_extra_data_types/test_tutorial001_an.py +++ b/tests/test_tutorial/test_extra_data_types/test_tutorial001_an.py @@ -30,7 +30,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py310.py b/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py310.py index 158ee01b3..b539cf3d6 100644 --- a/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py310.py +++ b/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py310.py @@ -39,7 +39,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py39.py b/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py39.py index 5be6452ee..efd31e63d 100644 --- a/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py39.py +++ b/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py39.py @@ -39,7 +39,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_extra_data_types/test_tutorial001_py310.py b/tests/test_tutorial/test_extra_data_types/test_tutorial001_py310.py index 5413fe428..733d9f406 100644 --- a/tests/test_tutorial/test_extra_data_types/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_extra_data_types/test_tutorial001_py310.py @@ -39,7 +39,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_extra_models/test_tutorial003.py b/tests/test_tutorial/test_extra_models/test_tutorial003.py index f08bf4c50..21192b7db 100644 --- a/tests/test_tutorial/test_extra_models/test_tutorial003.py +++ b/tests/test_tutorial/test_extra_models/test_tutorial003.py @@ -28,7 +28,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_extra_models/test_tutorial003_py310.py b/tests/test_tutorial/test_extra_models/test_tutorial003_py310.py index 407c71787..c17ddbbe1 100644 --- a/tests/test_tutorial/test_extra_models/test_tutorial003_py310.py +++ b/tests/test_tutorial/test_extra_models/test_tutorial003_py310.py @@ -38,7 +38,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_extra_models/test_tutorial004.py b/tests/test_tutorial/test_extra_models/test_tutorial004.py index 47790ba8f..71f6a8c70 100644 --- a/tests/test_tutorial/test_extra_models/test_tutorial004.py +++ b/tests/test_tutorial/test_extra_models/test_tutorial004.py @@ -18,7 +18,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_extra_models/test_tutorial004_py39.py b/tests/test_tutorial/test_extra_models/test_tutorial004_py39.py index a98700172..5475b92e1 100644 --- a/tests/test_tutorial/test_extra_models/test_tutorial004_py39.py +++ b/tests/test_tutorial/test_extra_models/test_tutorial004_py39.py @@ -27,7 +27,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_extra_models/test_tutorial005.py b/tests/test_tutorial/test_extra_models/test_tutorial005.py index 7c094b253..b0861c37f 100644 --- a/tests/test_tutorial/test_extra_models/test_tutorial005.py +++ b/tests/test_tutorial/test_extra_models/test_tutorial005.py @@ -15,7 +15,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/keyword-weights/": { diff --git a/tests/test_tutorial/test_extra_models/test_tutorial005_py39.py b/tests/test_tutorial/test_extra_models/test_tutorial005_py39.py index b40386450..7278e93c3 100644 --- a/tests/test_tutorial/test_extra_models/test_tutorial005_py39.py +++ b/tests/test_tutorial/test_extra_models/test_tutorial005_py39.py @@ -24,7 +24,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/keyword-weights/": { diff --git a/tests/test_tutorial/test_first_steps/test_tutorial001.py b/tests/test_tutorial/test_first_steps/test_tutorial001.py index ea37aec53..6cc9fc228 100644 --- a/tests/test_tutorial/test_first_steps/test_tutorial001.py +++ b/tests/test_tutorial/test_first_steps/test_tutorial001.py @@ -23,7 +23,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/": { diff --git a/tests/test_tutorial/test_generate_clients/test_tutorial003.py b/tests/test_tutorial/test_generate_clients/test_tutorial003.py index 8b22eab9e..1cd9678a1 100644 --- a/tests/test_tutorial/test_generate_clients/test_tutorial003.py +++ b/tests/test_tutorial/test_generate_clients/test_tutorial003.py @@ -32,7 +32,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_handling_errors/test_tutorial001.py b/tests/test_tutorial/test_handling_errors/test_tutorial001.py index 99a1053ca..8809c135b 100644 --- a/tests/test_tutorial/test_handling_errors/test_tutorial001.py +++ b/tests/test_tutorial/test_handling_errors/test_tutorial001.py @@ -22,7 +22,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_handling_errors/test_tutorial002.py b/tests/test_tutorial/test_handling_errors/test_tutorial002.py index 091c74f4d..efd86ebde 100644 --- a/tests/test_tutorial/test_handling_errors/test_tutorial002.py +++ b/tests/test_tutorial/test_handling_errors/test_tutorial002.py @@ -22,7 +22,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items-header/{item_id}": { diff --git a/tests/test_tutorial/test_handling_errors/test_tutorial003.py b/tests/test_tutorial/test_handling_errors/test_tutorial003.py index 1639cb1d8..4763f68f3 100644 --- a/tests/test_tutorial/test_handling_errors/test_tutorial003.py +++ b/tests/test_tutorial/test_handling_errors/test_tutorial003.py @@ -23,7 +23,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/unicorns/{name}": { diff --git a/tests/test_tutorial/test_handling_errors/test_tutorial004.py b/tests/test_tutorial/test_handling_errors/test_tutorial004.py index 246f3b94c..0c0988c64 100644 --- a/tests/test_tutorial/test_handling_errors/test_tutorial004.py +++ b/tests/test_tutorial/test_handling_errors/test_tutorial004.py @@ -32,7 +32,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_handling_errors/test_tutorial005.py b/tests/test_tutorial/test_handling_errors/test_tutorial005.py index af47fd1a4..f356178ac 100644 --- a/tests/test_tutorial/test_handling_errors/test_tutorial005.py +++ b/tests/test_tutorial/test_handling_errors/test_tutorial005.py @@ -31,7 +31,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_handling_errors/test_tutorial006.py b/tests/test_tutorial/test_handling_errors/test_tutorial006.py index 4a39bd102..4dd1adf43 100644 --- a/tests/test_tutorial/test_handling_errors/test_tutorial006.py +++ b/tests/test_tutorial/test_handling_errors/test_tutorial006.py @@ -35,7 +35,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_header_params/test_tutorial001.py b/tests/test_tutorial/test_header_params/test_tutorial001.py index 80f502d6a..030159dcf 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial001.py +++ b/tests/test_tutorial/test_header_params/test_tutorial001.py @@ -24,7 +24,7 @@ def test_openapi(): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_header_params/test_tutorial001_an.py b/tests/test_tutorial/test_header_params/test_tutorial001_an.py index f0ad7b816..3755ab758 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial001_an.py +++ b/tests/test_tutorial/test_header_params/test_tutorial001_an.py @@ -24,7 +24,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_header_params/test_tutorial001_an_py310.py b/tests/test_tutorial/test_header_params/test_tutorial001_an_py310.py index d095c7123..207b3b02b 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial001_an_py310.py +++ b/tests/test_tutorial/test_header_params/test_tutorial001_an_py310.py @@ -32,7 +32,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_header_params/test_tutorial001_py310.py b/tests/test_tutorial/test_header_params/test_tutorial001_py310.py index bf176bba2..bf51982b7 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_header_params/test_tutorial001_py310.py @@ -32,7 +32,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_header_params/test_tutorial002.py b/tests/test_tutorial/test_header_params/test_tutorial002.py index 516abda8b..545fc836b 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial002.py +++ b/tests/test_tutorial/test_header_params/test_tutorial002.py @@ -35,7 +35,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_header_params/test_tutorial002_an.py b/tests/test_tutorial/test_header_params/test_tutorial002_an.py index 97493e604..cfd581e33 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial002_an.py +++ b/tests/test_tutorial/test_header_params/test_tutorial002_an.py @@ -35,7 +35,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_header_params/test_tutorial002_an_py310.py b/tests/test_tutorial/test_header_params/test_tutorial002_an_py310.py index e0c60342a..c8d61e42e 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial002_an_py310.py +++ b/tests/test_tutorial/test_header_params/test_tutorial002_an_py310.py @@ -43,7 +43,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_header_params/test_tutorial002_an_py39.py b/tests/test_tutorial/test_header_params/test_tutorial002_an_py39.py index c1bc5faf8..85150d4a9 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial002_an_py39.py +++ b/tests/test_tutorial/test_header_params/test_tutorial002_an_py39.py @@ -46,7 +46,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_header_params/test_tutorial002_py310.py b/tests/test_tutorial/test_header_params/test_tutorial002_py310.py index 81871b8c6..f189d85b5 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial002_py310.py +++ b/tests/test_tutorial/test_header_params/test_tutorial002_py310.py @@ -46,7 +46,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_header_params/test_tutorial003.py b/tests/test_tutorial/test_header_params/test_tutorial003.py index 99dd9e25f..b2fc17b8f 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial003.py +++ b/tests/test_tutorial/test_header_params/test_tutorial003.py @@ -26,7 +26,7 @@ def test_openapi_schema(): assert response.status_code == 200 # insert_assert(response.json()) assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_header_params/test_tutorial003_an.py b/tests/test_tutorial/test_header_params/test_tutorial003_an.py index 4477da7a8..87fa839e2 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial003_an.py +++ b/tests/test_tutorial/test_header_params/test_tutorial003_an.py @@ -26,7 +26,7 @@ def test_openapi_schema(): assert response.status_code == 200 # insert_assert(response.json()) assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_header_params/test_tutorial003_an_py310.py b/tests/test_tutorial/test_header_params/test_tutorial003_an_py310.py index b52304a2b..ef6c268c5 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial003_an_py310.py +++ b/tests/test_tutorial/test_header_params/test_tutorial003_an_py310.py @@ -34,7 +34,7 @@ def test_openapi_schema(client: TestClient): assert response.status_code == 200 # insert_assert(response.json()) assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_header_params/test_tutorial003_an_py39.py b/tests/test_tutorial/test_header_params/test_tutorial003_an_py39.py index dffdd1622..6525fd50c 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial003_an_py39.py +++ b/tests/test_tutorial/test_header_params/test_tutorial003_an_py39.py @@ -34,7 +34,7 @@ def test_openapi_schema(client: TestClient): assert response.status_code == 200 # insert_assert(response.json()) assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_header_params/test_tutorial003_py310.py b/tests/test_tutorial/test_header_params/test_tutorial003_py310.py index 64ef7b22a..b404ce5d8 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial003_py310.py +++ b/tests/test_tutorial/test_header_params/test_tutorial003_py310.py @@ -34,7 +34,7 @@ def test_openapi_schema(client: TestClient): assert response.status_code == 200 # insert_assert(response.json()) assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_metadata/test_tutorial001.py b/tests/test_tutorial/test_metadata/test_tutorial001.py index f1ddc3259..04e8ff82b 100644 --- a/tests/test_tutorial/test_metadata/test_tutorial001.py +++ b/tests/test_tutorial/test_metadata/test_tutorial001.py @@ -15,9 +15,10 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": { "title": "ChimichangApp", + "summary": "Deadpool's favorite app. Nuff said.", "description": "\nChimichangApp API helps you do awesome stuff. 🚀\n\n## Items\n\nYou can **read items**.\n\n## Users\n\nYou will be able to:\n\n* **Create users** (_not implemented_).\n* **Read users** (_not implemented_).\n", "termsOfService": "http://example.com/terms/", "contact": { diff --git a/tests/test_tutorial/test_metadata/test_tutorial001_1.py b/tests/test_tutorial/test_metadata/test_tutorial001_1.py new file mode 100644 index 000000000..3efb1c432 --- /dev/null +++ b/tests/test_tutorial/test_metadata/test_tutorial001_1.py @@ -0,0 +1,49 @@ +from fastapi.testclient import TestClient + +from docs_src.metadata.tutorial001_1 import app + +client = TestClient(app) + + +def test_items(): + response = client.get("/items/") + assert response.status_code == 200, response.text + assert response.json() == [{"name": "Katana"}] + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": { + "title": "ChimichangApp", + "summary": "Deadpool's favorite app. Nuff said.", + "description": "\nChimichangApp API helps you do awesome stuff. 🚀\n\n## Items\n\nYou can **read items**.\n\n## Users\n\nYou will be able to:\n\n* **Create users** (_not implemented_).\n* **Read users** (_not implemented_).\n", + "termsOfService": "http://example.com/terms/", + "contact": { + "name": "Deadpoolio the Amazing", + "url": "http://x-force.example.com/contact/", + "email": "dp@x-force.example.com", + }, + "license": { + "name": "Apache 2.0", + "identifier": "MIT", + }, + "version": "0.0.1", + }, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + } + }, + } diff --git a/tests/test_tutorial/test_metadata/test_tutorial004.py b/tests/test_tutorial/test_metadata/test_tutorial004.py index f7f47a558..507220371 100644 --- a/tests/test_tutorial/test_metadata/test_tutorial004.py +++ b/tests/test_tutorial/test_metadata/test_tutorial004.py @@ -16,7 +16,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/": { diff --git a/tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py b/tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py index c6cdc6064..a6e898c49 100644 --- a/tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py +++ b/tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py @@ -22,7 +22,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/invoices/": { diff --git a/tests/test_tutorial/test_openapi_webhooks/__init__.py b/tests/test_tutorial/test_openapi_webhooks/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_tutorial/test_openapi_webhooks/test_tutorial001.py b/tests/test_tutorial/test_openapi_webhooks/test_tutorial001.py new file mode 100644 index 000000000..9111fdb2f --- /dev/null +++ b/tests/test_tutorial/test_openapi_webhooks/test_tutorial001.py @@ -0,0 +1,117 @@ +from fastapi.testclient import TestClient + +from docs_src.openapi_webhooks.tutorial001 import app + +client = TestClient(app) + + +def test_get(): + response = client.get("/users/") + assert response.status_code == 200, response.text + assert response.json() == ["Rick", "Morty"] + + +def test_dummy_webhook(): + # Just for coverage + app.webhooks.routes[0].endpoint({}) + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/": { + "get": { + "summary": "Read Users", + "operationId": "read_users_users__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + } + }, + "webhooks": { + "new-subscription": { + "post": { + "summary": "New Subscription", + "description": "When a new user subscribes to your service we'll send you a POST request with this\ndata to the URL that you register for the event `new-subscription` in the dashboard.", + "operationId": "new_subscriptionnew_subscription_post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Subscription"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": {"$ref": "#/components/schemas/ValidationError"}, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "Subscription": { + "properties": { + "username": {"type": "string", "title": "Username"}, + "montly_fee": {"type": "number", "title": "Montly Fee"}, + "start_date": { + "type": "string", + "format": "date-time", + "title": "Start Date", + }, + }, + "type": "object", + "required": ["username", "montly_fee", "start_date"], + "title": "Subscription", + }, + "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", + }, + } + }, + } diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial001.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial001.py index c1cdbee24..95542398e 100644 --- a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial001.py +++ b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial001.py @@ -15,7 +15,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial002.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial002.py index fdaddd018..d1388c367 100644 --- a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial002.py +++ b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial002.py @@ -15,7 +15,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial003.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial003.py index 782c64a84..313bb2a04 100644 --- a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial003.py +++ b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial003.py @@ -15,7 +15,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": {}, } diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py index f5fd868eb..cd9fc520e 100644 --- a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py +++ b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py @@ -21,7 +21,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial005.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial005.py index 52379c01e..07e2d7d20 100644 --- a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial005.py +++ b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial005.py @@ -14,7 +14,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial006.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial006.py index deb6b0910..f92c59015 100644 --- a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial006.py +++ b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial006.py @@ -22,7 +22,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007.py index 470956a77..3b88a38c2 100644 --- a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007.py +++ b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007.py @@ -56,7 +56,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_path_operation_configurations/test_tutorial002b.py b/tests/test_tutorial/test_path_operation_configurations/test_tutorial002b.py index 76e44b5e5..58dec5769 100644 --- a/tests/test_tutorial/test_path_operation_configurations/test_tutorial002b.py +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial002b.py @@ -21,7 +21,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005.py b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005.py index cf8e203a0..30278caf8 100644 --- a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005.py +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005.py @@ -21,7 +21,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py310.py b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py310.py index 497fc9024..cf59d354c 100644 --- a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py310.py +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py310.py @@ -30,7 +30,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py39.py b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py39.py index 09fac44c4..a93ea8807 100644 --- a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py39.py +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py39.py @@ -30,7 +30,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_path_operation_configurations/test_tutorial006.py b/tests/test_tutorial/test_path_operation_configurations/test_tutorial006.py index e90771f24..91180d109 100644 --- a/tests/test_tutorial/test_path_operation_configurations/test_tutorial006.py +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial006.py @@ -24,7 +24,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_path_params/test_tutorial004.py b/tests/test_tutorial/test_path_params/test_tutorial004.py index ab0455bf5..acbeaca76 100644 --- a/tests/test_tutorial/test_path_params/test_tutorial004.py +++ b/tests/test_tutorial/test_path_params/test_tutorial004.py @@ -23,7 +23,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/files/{file_path}": { diff --git a/tests/test_tutorial/test_path_params/test_tutorial005.py b/tests/test_tutorial/test_path_params/test_tutorial005.py index 3401f2253..b9b58c961 100644 --- a/tests/test_tutorial/test_path_params/test_tutorial005.py +++ b/tests/test_tutorial/test_path_params/test_tutorial005.py @@ -51,7 +51,7 @@ def test_openapi(): assert response.status_code == 200, response.text data = response.json() assert data == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/models/{model_name}": { diff --git a/tests/test_tutorial/test_query_params/test_tutorial005.py b/tests/test_tutorial/test_query_params/test_tutorial005.py index 3c408449b..6c2cba7e1 100644 --- a/tests/test_tutorial/test_query_params/test_tutorial005.py +++ b/tests/test_tutorial/test_query_params/test_tutorial005.py @@ -35,7 +35,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_query_params/test_tutorial006.py b/tests/test_tutorial/test_query_params/test_tutorial006.py index 7fe58a990..626637903 100644 --- a/tests/test_tutorial/test_query_params/test_tutorial006.py +++ b/tests/test_tutorial/test_query_params/test_tutorial006.py @@ -60,7 +60,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_query_params/test_tutorial006_py310.py b/tests/test_tutorial/test_query_params/test_tutorial006_py310.py index b90c0a6c8..b6fb2f39e 100644 --- a/tests/test_tutorial/test_query_params/test_tutorial006_py310.py +++ b/tests/test_tutorial/test_query_params/test_tutorial006_py310.py @@ -67,7 +67,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010.py index c41f554ba..370ae0ff0 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010.py @@ -45,7 +45,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an.py index dc8028f81..1f76ef314 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an.py @@ -45,7 +45,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py310.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py310.py index 496b95b79..3a06b4bc7 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py310.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py310.py @@ -55,7 +55,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py39.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py39.py index 2005e5043..1e6f93093 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py39.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py39.py @@ -55,7 +55,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_py310.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_py310.py index 8147d768e..63524d291 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_py310.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_py310.py @@ -55,7 +55,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011.py index d6d69c169..164ec1193 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011.py @@ -23,7 +23,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an.py index 3a53d422e..2afaafd92 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an.py @@ -23,7 +23,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py310.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py310.py index f00df2879..fafd38337 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py310.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py310.py @@ -33,7 +33,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py39.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py39.py index 895fb8e9f..f3fb47528 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py39.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py39.py @@ -33,7 +33,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py310.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py310.py index 4f4b1fd55..21f348f2b 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py310.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py310.py @@ -33,7 +33,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py39.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py39.py index d85bb6231..f2c2a5a33 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py39.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py39.py @@ -33,7 +33,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial012.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial012.py index 7bc020540..1436db384 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial012.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial012.py @@ -23,7 +23,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_an.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_an.py index be5557f6a..270763f1d 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_an.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_an.py @@ -23,7 +23,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_an_py39.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_an_py39.py index d9512e193..548391683 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_an_py39.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_an_py39.py @@ -33,7 +33,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_py39.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_py39.py index b2a2c8d1d..e7d745154 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_py39.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_py39.py @@ -33,7 +33,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial013.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial013.py index 4a0b9e8b5..1ba1fdf61 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial013.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial013.py @@ -23,7 +23,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial013_an.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial013_an.py index 71e4638ae..343261748 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial013_an.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial013_an.py @@ -23,7 +23,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial013_an_py39.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial013_an_py39.py index 4e90db358..537d6325b 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial013_an_py39.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial013_an_py39.py @@ -33,7 +33,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial014.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014.py index 7686c07b3..7bce7590c 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial014.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014.py @@ -21,7 +21,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an.py index e739044a8..2182e87b7 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an.py @@ -21,7 +21,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an_py310.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an_py310.py index 73f0ba78b..344004d01 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an_py310.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an_py310.py @@ -31,7 +31,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an_py39.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an_py39.py index e2c149992..5d4f6df3d 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an_py39.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an_py39.py @@ -31,7 +31,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_py310.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_py310.py index 07f30b739..dad49fb12 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_py310.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_py310.py @@ -31,7 +31,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_request_files/test_tutorial001.py b/tests/test_tutorial/test_request_files/test_tutorial001.py index 3269801ef..84c736180 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001.py @@ -66,7 +66,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/files/": { diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_02.py b/tests/test_tutorial/test_request_files/test_tutorial001_02.py index 4b6edfa06..8ebe4eafd 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001_02.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001_02.py @@ -43,7 +43,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/files/": { diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_02_an.py b/tests/test_tutorial/test_request_files/test_tutorial001_02_an.py index 0c34620e3..5da8b320b 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001_02_an.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001_02_an.py @@ -43,7 +43,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/files/": { diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py310.py b/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py310.py index 04442c76f..166f59b1a 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py310.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py310.py @@ -55,7 +55,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/files/": { diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py39.py b/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py39.py index f5249ef5b..02ea604b2 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py39.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py39.py @@ -55,7 +55,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/files/": { diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_02_py310.py b/tests/test_tutorial/test_request_files/test_tutorial001_02_py310.py index f690d107b..c753e14d1 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001_02_py310.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001_02_py310.py @@ -55,7 +55,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/files/": { diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_03.py b/tests/test_tutorial/test_request_files/test_tutorial001_03.py index 4af659a11..f02170814 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001_03.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001_03.py @@ -31,7 +31,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/files/": { diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_03_an.py b/tests/test_tutorial/test_request_files/test_tutorial001_03_an.py index 91dbc60b9..acfb749ce 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001_03_an.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001_03_an.py @@ -31,7 +31,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/files/": { diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_03_an_py39.py b/tests/test_tutorial/test_request_files/test_tutorial001_03_an_py39.py index 7c4ad326c..36e5faac1 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001_03_an_py39.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001_03_an_py39.py @@ -39,7 +39,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/files/": { diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_an.py b/tests/test_tutorial/test_request_files/test_tutorial001_an.py index 80c288ed6..6eb2d55dc 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001_an.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001_an.py @@ -66,7 +66,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/files/": { diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_an_py39.py b/tests/test_tutorial/test_request_files/test_tutorial001_an_py39.py index 4dc1f752c..4e3ef6869 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001_an_py39.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001_an_py39.py @@ -76,7 +76,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/files/": { diff --git a/tests/test_tutorial/test_request_files/test_tutorial002.py b/tests/test_tutorial/test_request_files/test_tutorial002.py index 6868be328..65a8a9e61 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial002.py +++ b/tests/test_tutorial/test_request_files/test_tutorial002.py @@ -77,7 +77,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/files/": { diff --git a/tests/test_tutorial/test_request_files/test_tutorial002_an.py b/tests/test_tutorial/test_request_files/test_tutorial002_an.py index ca1f62ea3..52a8e1964 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial002_an.py +++ b/tests/test_tutorial/test_request_files/test_tutorial002_an.py @@ -77,7 +77,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/files/": { diff --git a/tests/test_tutorial/test_request_files/test_tutorial002_an_py39.py b/tests/test_tutorial/test_request_files/test_tutorial002_an_py39.py index e593ae75d..6594e0116 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial002_an_py39.py +++ b/tests/test_tutorial/test_request_files/test_tutorial002_an_py39.py @@ -96,7 +96,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/files/": { diff --git a/tests/test_tutorial/test_request_files/test_tutorial002_py39.py b/tests/test_tutorial/test_request_files/test_tutorial002_py39.py index bacd25b97..bfe964604 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial002_py39.py +++ b/tests/test_tutorial/test_request_files/test_tutorial002_py39.py @@ -96,7 +96,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/files/": { diff --git a/tests/test_tutorial/test_request_files/test_tutorial003.py b/tests/test_tutorial/test_request_files/test_tutorial003.py index e2d69184b..85cd03a59 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial003.py +++ b/tests/test_tutorial/test_request_files/test_tutorial003.py @@ -54,7 +54,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/files/": { diff --git a/tests/test_tutorial/test_request_files/test_tutorial003_an.py b/tests/test_tutorial/test_request_files/test_tutorial003_an.py index f199d4d2f..0327a2db6 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial003_an.py +++ b/tests/test_tutorial/test_request_files/test_tutorial003_an.py @@ -54,7 +54,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/files/": { diff --git a/tests/test_tutorial/test_request_files/test_tutorial003_an_py39.py b/tests/test_tutorial/test_request_files/test_tutorial003_an_py39.py index 51fa83470..3aa68c621 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial003_an_py39.py +++ b/tests/test_tutorial/test_request_files/test_tutorial003_an_py39.py @@ -82,7 +82,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/files/": { diff --git a/tests/test_tutorial/test_request_files/test_tutorial003_py39.py b/tests/test_tutorial/test_request_files/test_tutorial003_py39.py index 32b028909..238bb39cd 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial003_py39.py +++ b/tests/test_tutorial/test_request_files/test_tutorial003_py39.py @@ -82,7 +82,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/files/": { diff --git a/tests/test_tutorial/test_request_forms/test_tutorial001.py b/tests/test_tutorial/test_request_forms/test_tutorial001.py index 721c97fb1..4a2a7abe9 100644 --- a/tests/test_tutorial/test_request_forms/test_tutorial001.py +++ b/tests/test_tutorial/test_request_forms/test_tutorial001.py @@ -70,7 +70,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/login/": { diff --git a/tests/test_tutorial/test_request_forms/test_tutorial001_an.py b/tests/test_tutorial/test_request_forms/test_tutorial001_an.py index 6b8abab19..347361344 100644 --- a/tests/test_tutorial/test_request_forms/test_tutorial001_an.py +++ b/tests/test_tutorial/test_request_forms/test_tutorial001_an.py @@ -70,7 +70,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/login/": { diff --git a/tests/test_tutorial/test_request_forms/test_tutorial001_an_py39.py b/tests/test_tutorial/test_request_forms/test_tutorial001_an_py39.py index 5862524ad..e65a8823e 100644 --- a/tests/test_tutorial/test_request_forms/test_tutorial001_an_py39.py +++ b/tests/test_tutorial/test_request_forms/test_tutorial001_an_py39.py @@ -81,7 +81,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/login/": { diff --git a/tests/test_tutorial/test_request_forms_and_files/test_tutorial001.py b/tests/test_tutorial/test_request_forms_and_files/test_tutorial001.py index dcc44cf09..be12656d2 100644 --- a/tests/test_tutorial/test_request_forms_and_files/test_tutorial001.py +++ b/tests/test_tutorial/test_request_forms_and_files/test_tutorial001.py @@ -112,7 +112,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/files/": { diff --git a/tests/test_tutorial/test_request_forms_and_files/test_tutorial001_an.py b/tests/test_tutorial/test_request_forms_and_files/test_tutorial001_an.py index f11e36984..a5fcb3a94 100644 --- a/tests/test_tutorial/test_request_forms_and_files/test_tutorial001_an.py +++ b/tests/test_tutorial/test_request_forms_and_files/test_tutorial001_an.py @@ -112,7 +112,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/files/": { diff --git a/tests/test_tutorial/test_request_forms_and_files/test_tutorial001_an_py39.py b/tests/test_tutorial/test_request_forms_and_files/test_tutorial001_an_py39.py index 9b5e8681c..6eacb2fcf 100644 --- a/tests/test_tutorial/test_request_forms_and_files/test_tutorial001_an_py39.py +++ b/tests/test_tutorial/test_request_forms_and_files/test_tutorial001_an_py39.py @@ -131,7 +131,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/files/": { diff --git a/tests/test_tutorial/test_response_model/test_tutorial003.py b/tests/test_tutorial/test_response_model/test_tutorial003.py index 4a95cb2b5..9cb0419a3 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial003.py +++ b/tests/test_tutorial/test_response_model/test_tutorial003.py @@ -27,7 +27,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/user/": { diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_01.py b/tests/test_tutorial/test_response_model/test_tutorial003_01.py index a055bc688..8b8fe514a 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial003_01.py +++ b/tests/test_tutorial/test_response_model/test_tutorial003_01.py @@ -27,7 +27,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/user/": { diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_01_py310.py b/tests/test_tutorial/test_response_model/test_tutorial003_01_py310.py index 8cb4ac208..01dc8e71c 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial003_01_py310.py +++ b/tests/test_tutorial/test_response_model/test_tutorial003_01_py310.py @@ -36,7 +36,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/user/": { diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_02.py b/tests/test_tutorial/test_response_model/test_tutorial003_02.py index 6ccb054b8..eabd20345 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial003_02.py +++ b/tests/test_tutorial/test_response_model/test_tutorial003_02.py @@ -21,7 +21,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/portal": { diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_03.py b/tests/test_tutorial/test_response_model/test_tutorial003_03.py index ba4c0f275..970ff5845 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial003_03.py +++ b/tests/test_tutorial/test_response_model/test_tutorial003_03.py @@ -15,7 +15,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/teleport": { diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_05.py b/tests/test_tutorial/test_response_model/test_tutorial003_05.py index d7c232e75..c7a39cc74 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial003_05.py +++ b/tests/test_tutorial/test_response_model/test_tutorial003_05.py @@ -21,7 +21,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/portal": { diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_05_py310.py b/tests/test_tutorial/test_response_model/test_tutorial003_05_py310.py index a89f1dad8..f80d62572 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial003_05_py310.py +++ b/tests/test_tutorial/test_response_model/test_tutorial003_05_py310.py @@ -31,7 +31,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/portal": { diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_py310.py b/tests/test_tutorial/test_response_model/test_tutorial003_py310.py index 1f4ec9057..602147b13 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial003_py310.py +++ b/tests/test_tutorial/test_response_model/test_tutorial003_py310.py @@ -36,7 +36,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/user/": { diff --git a/tests/test_tutorial/test_response_model/test_tutorial004.py b/tests/test_tutorial/test_response_model/test_tutorial004.py index 8cb7dc9cf..07af29207 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial004.py +++ b/tests/test_tutorial/test_response_model/test_tutorial004.py @@ -36,7 +36,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_response_model/test_tutorial004_py310.py b/tests/test_tutorial/test_response_model/test_tutorial004_py310.py index 2ba8143ba..90147fbdd 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial004_py310.py +++ b/tests/test_tutorial/test_response_model/test_tutorial004_py310.py @@ -44,7 +44,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_response_model/test_tutorial004_py39.py b/tests/test_tutorial/test_response_model/test_tutorial004_py39.py index 97df0a238..740a49590 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial004_py39.py +++ b/tests/test_tutorial/test_response_model/test_tutorial004_py39.py @@ -44,7 +44,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_response_model/test_tutorial005.py b/tests/test_tutorial/test_response_model/test_tutorial005.py index 76662f793..e8c8946c5 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial005.py +++ b/tests/test_tutorial/test_response_model/test_tutorial005.py @@ -25,7 +25,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}/name": { diff --git a/tests/test_tutorial/test_response_model/test_tutorial005_py310.py b/tests/test_tutorial/test_response_model/test_tutorial005_py310.py index 1b1cf4175..388e030bd 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial005_py310.py +++ b/tests/test_tutorial/test_response_model/test_tutorial005_py310.py @@ -35,7 +35,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}/name": { diff --git a/tests/test_tutorial/test_response_model/test_tutorial006.py b/tests/test_tutorial/test_response_model/test_tutorial006.py index 3a759fde4..548a3dbd8 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial006.py +++ b/tests/test_tutorial/test_response_model/test_tutorial006.py @@ -25,7 +25,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}/name": { diff --git a/tests/test_tutorial/test_response_model/test_tutorial006_py310.py b/tests/test_tutorial/test_response_model/test_tutorial006_py310.py index 07e84cc82..075bb8079 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial006_py310.py +++ b/tests/test_tutorial/test_response_model/test_tutorial006_py310.py @@ -35,7 +35,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}/name": { diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial004.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial004.py index 784517d69..dea136fb2 100644 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial004.py +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial004.py @@ -23,7 +23,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { @@ -41,31 +41,34 @@ def test_openapi_schema(): "requestBody": { "content": { "application/json": { - "schema": {"$ref": "#/components/schemas/Item"}, - "examples": { - "normal": { - "summary": "A normal example", - "description": "A **normal** item works correctly.", - "value": { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, + "schema": { + "allOf": [{"$ref": "#/components/schemas/Item"}], + "title": "Item", + "examples": [ + { + "summary": "A normal example", + "description": "A **normal** item works correctly.", + "value": { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, }, - }, - "converted": { - "summary": "An example with converted data", - "description": "FastAPI can convert price `strings` to actual `numbers` automatically", - "value": {"name": "Bar", "price": "35.4"}, - }, - "invalid": { - "summary": "Invalid data is rejected with an error", - "value": { - "name": "Baz", - "price": "thirty five point four", + { + "summary": "An example with converted data", + "description": "FastAPI can convert price `strings` to actual `numbers` automatically", + "value": {"name": "Bar", "price": "35.4"}, }, - }, - }, + { + "summary": "Invalid data is rejected with an error", + "value": { + "name": "Baz", + "price": "thirty five point four", + }, + }, + ], + } } }, "required": True, diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an.py index 222a4edfe..571feb19f 100644 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an.py +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an.py @@ -23,7 +23,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { @@ -41,31 +41,34 @@ def test_openapi_schema(): "requestBody": { "content": { "application/json": { - "schema": {"$ref": "#/components/schemas/Item"}, - "examples": { - "normal": { - "summary": "A normal example", - "description": "A **normal** item works correctly.", - "value": { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, + "schema": { + "allOf": [{"$ref": "#/components/schemas/Item"}], + "title": "Item", + "examples": [ + { + "summary": "A normal example", + "description": "A **normal** item works correctly.", + "value": { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, }, - }, - "converted": { - "summary": "An example with converted data", - "description": "FastAPI can convert price `strings` to actual `numbers` automatically", - "value": {"name": "Bar", "price": "35.4"}, - }, - "invalid": { - "summary": "Invalid data is rejected with an error", - "value": { - "name": "Baz", - "price": "thirty five point four", + { + "summary": "An example with converted data", + "description": "FastAPI can convert price `strings` to actual `numbers` automatically", + "value": {"name": "Bar", "price": "35.4"}, }, - }, - }, + { + "summary": "Invalid data is rejected with an error", + "value": { + "name": "Baz", + "price": "thirty five point four", + }, + }, + ], + } } }, "required": True, diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py310.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py310.py index 1eacd640a..e25531794 100644 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py310.py +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py310.py @@ -32,7 +32,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { @@ -50,31 +50,34 @@ def test_openapi_schema(client: TestClient): "requestBody": { "content": { "application/json": { - "schema": {"$ref": "#/components/schemas/Item"}, - "examples": { - "normal": { - "summary": "A normal example", - "description": "A **normal** item works correctly.", - "value": { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, + "schema": { + "allOf": [{"$ref": "#/components/schemas/Item"}], + "title": "Item", + "examples": [ + { + "summary": "A normal example", + "description": "A **normal** item works correctly.", + "value": { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, }, - }, - "converted": { - "summary": "An example with converted data", - "description": "FastAPI can convert price `strings` to actual `numbers` automatically", - "value": {"name": "Bar", "price": "35.4"}, - }, - "invalid": { - "summary": "Invalid data is rejected with an error", - "value": { - "name": "Baz", - "price": "thirty five point four", + { + "summary": "An example with converted data", + "description": "FastAPI can convert price `strings` to actual `numbers` automatically", + "value": {"name": "Bar", "price": "35.4"}, }, - }, - }, + { + "summary": "Invalid data is rejected with an error", + "value": { + "name": "Baz", + "price": "thirty five point four", + }, + }, + ], + } } }, "required": True, diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py39.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py39.py index 632f2cbe0..dafc5afad 100644 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py39.py +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py39.py @@ -32,7 +32,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { @@ -50,31 +50,34 @@ def test_openapi_schema(client: TestClient): "requestBody": { "content": { "application/json": { - "schema": {"$ref": "#/components/schemas/Item"}, - "examples": { - "normal": { - "summary": "A normal example", - "description": "A **normal** item works correctly.", - "value": { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, + "schema": { + "allOf": [{"$ref": "#/components/schemas/Item"}], + "title": "Item", + "examples": [ + { + "summary": "A normal example", + "description": "A **normal** item works correctly.", + "value": { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, }, - }, - "converted": { - "summary": "An example with converted data", - "description": "FastAPI can convert price `strings` to actual `numbers` automatically", - "value": {"name": "Bar", "price": "35.4"}, - }, - "invalid": { - "summary": "Invalid data is rejected with an error", - "value": { - "name": "Baz", - "price": "thirty five point four", + { + "summary": "An example with converted data", + "description": "FastAPI can convert price `strings` to actual `numbers` automatically", + "value": {"name": "Bar", "price": "35.4"}, }, - }, - }, + { + "summary": "Invalid data is rejected with an error", + "value": { + "name": "Baz", + "price": "thirty five point four", + }, + }, + ], + } } }, "required": True, diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_py310.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_py310.py index c99cb75c8..29004218b 100644 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_py310.py +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_py310.py @@ -32,7 +32,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { @@ -50,31 +50,34 @@ def test_openapi_schema(client: TestClient): "requestBody": { "content": { "application/json": { - "schema": {"$ref": "#/components/schemas/Item"}, - "examples": { - "normal": { - "summary": "A normal example", - "description": "A **normal** item works correctly.", - "value": { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, + "schema": { + "allOf": [{"$ref": "#/components/schemas/Item"}], + "title": "Item", + "examples": [ + { + "summary": "A normal example", + "description": "A **normal** item works correctly.", + "value": { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, }, - }, - "converted": { - "summary": "An example with converted data", - "description": "FastAPI can convert price `strings` to actual `numbers` automatically", - "value": {"name": "Bar", "price": "35.4"}, - }, - "invalid": { - "summary": "Invalid data is rejected with an error", - "value": { - "name": "Baz", - "price": "thirty five point four", + { + "summary": "An example with converted data", + "description": "FastAPI can convert price `strings` to actual `numbers` automatically", + "value": {"name": "Bar", "price": "35.4"}, }, - }, - }, + { + "summary": "Invalid data is rejected with an error", + "value": { + "name": "Baz", + "price": "thirty five point four", + }, + }, + ], + } } }, "required": True, diff --git a/tests/test_tutorial/test_security/test_tutorial001.py b/tests/test_tutorial/test_security/test_tutorial001.py index a7f55b78b..417bed8f7 100644 --- a/tests/test_tutorial/test_security/test_tutorial001.py +++ b/tests/test_tutorial/test_security/test_tutorial001.py @@ -29,7 +29,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_security/test_tutorial001_an.py b/tests/test_tutorial/test_security/test_tutorial001_an.py index fc48703aa..59460da7f 100644 --- a/tests/test_tutorial/test_security/test_tutorial001_an.py +++ b/tests/test_tutorial/test_security/test_tutorial001_an.py @@ -29,7 +29,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_security/test_tutorial001_an_py39.py b/tests/test_tutorial/test_security/test_tutorial001_an_py39.py index 345e0be0f..d8e712773 100644 --- a/tests/test_tutorial/test_security/test_tutorial001_an_py39.py +++ b/tests/test_tutorial/test_security/test_tutorial001_an_py39.py @@ -40,7 +40,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_security/test_tutorial003.py b/tests/test_tutorial/test_security/test_tutorial003.py index c10f928eb..cb5cdaa04 100644 --- a/tests/test_tutorial/test_security/test_tutorial003.py +++ b/tests/test_tutorial/test_security/test_tutorial003.py @@ -70,7 +70,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/token": { diff --git a/tests/test_tutorial/test_security/test_tutorial003_an.py b/tests/test_tutorial/test_security/test_tutorial003_an.py index 41872fda0..26e68a029 100644 --- a/tests/test_tutorial/test_security/test_tutorial003_an.py +++ b/tests/test_tutorial/test_security/test_tutorial003_an.py @@ -70,7 +70,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/token": { diff --git a/tests/test_tutorial/test_security/test_tutorial003_an_py310.py b/tests/test_tutorial/test_security/test_tutorial003_an_py310.py index 02bd748c8..1250d4afb 100644 --- a/tests/test_tutorial/test_security/test_tutorial003_an_py310.py +++ b/tests/test_tutorial/test_security/test_tutorial003_an_py310.py @@ -86,7 +86,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/token": { diff --git a/tests/test_tutorial/test_security/test_tutorial003_an_py39.py b/tests/test_tutorial/test_security/test_tutorial003_an_py39.py index 7e74afafb..b74cfdc54 100644 --- a/tests/test_tutorial/test_security/test_tutorial003_an_py39.py +++ b/tests/test_tutorial/test_security/test_tutorial003_an_py39.py @@ -86,7 +86,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/token": { diff --git a/tests/test_tutorial/test_security/test_tutorial003_py310.py b/tests/test_tutorial/test_security/test_tutorial003_py310.py index a463751f5..8a75d2321 100644 --- a/tests/test_tutorial/test_security/test_tutorial003_py310.py +++ b/tests/test_tutorial/test_security/test_tutorial003_py310.py @@ -86,7 +86,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/token": { diff --git a/tests/test_tutorial/test_security/test_tutorial005.py b/tests/test_tutorial/test_security/test_tutorial005.py index ccb5b3c9f..4e4b6afe8 100644 --- a/tests/test_tutorial/test_security/test_tutorial005.py +++ b/tests/test_tutorial/test_security/test_tutorial005.py @@ -179,7 +179,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/token": { diff --git a/tests/test_tutorial/test_security/test_tutorial005_an.py b/tests/test_tutorial/test_security/test_tutorial005_an.py index e851f47fe..51cc8329a 100644 --- a/tests/test_tutorial/test_security/test_tutorial005_an.py +++ b/tests/test_tutorial/test_security/test_tutorial005_an.py @@ -179,7 +179,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/token": { diff --git a/tests/test_tutorial/test_security/test_tutorial005_an_py310.py b/tests/test_tutorial/test_security/test_tutorial005_an_py310.py index cca581980..b0d0fed12 100644 --- a/tests/test_tutorial/test_security/test_tutorial005_an_py310.py +++ b/tests/test_tutorial/test_security/test_tutorial005_an_py310.py @@ -207,7 +207,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/token": { diff --git a/tests/test_tutorial/test_security/test_tutorial005_an_py39.py b/tests/test_tutorial/test_security/test_tutorial005_an_py39.py index eae851457..26deaaf3c 100644 --- a/tests/test_tutorial/test_security/test_tutorial005_an_py39.py +++ b/tests/test_tutorial/test_security/test_tutorial005_an_py39.py @@ -207,7 +207,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/token": { diff --git a/tests/test_tutorial/test_security/test_tutorial005_py310.py b/tests/test_tutorial/test_security/test_tutorial005_py310.py index cdbd8f75e..e93f34c3b 100644 --- a/tests/test_tutorial/test_security/test_tutorial005_py310.py +++ b/tests/test_tutorial/test_security/test_tutorial005_py310.py @@ -207,7 +207,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/token": { diff --git a/tests/test_tutorial/test_security/test_tutorial005_py39.py b/tests/test_tutorial/test_security/test_tutorial005_py39.py index 45b2a2341..737a8548f 100644 --- a/tests/test_tutorial/test_security/test_tutorial005_py39.py +++ b/tests/test_tutorial/test_security/test_tutorial005_py39.py @@ -207,7 +207,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/token": { diff --git a/tests/test_tutorial/test_security/test_tutorial006.py b/tests/test_tutorial/test_security/test_tutorial006.py index 73cbdc538..dc459b6fd 100644 --- a/tests/test_tutorial/test_security/test_tutorial006.py +++ b/tests/test_tutorial/test_security/test_tutorial006.py @@ -42,7 +42,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/me": { diff --git a/tests/test_tutorial/test_security/test_tutorial006_an.py b/tests/test_tutorial/test_security/test_tutorial006_an.py index 5f970ed01..52ddd938f 100644 --- a/tests/test_tutorial/test_security/test_tutorial006_an.py +++ b/tests/test_tutorial/test_security/test_tutorial006_an.py @@ -42,7 +42,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/me": { diff --git a/tests/test_tutorial/test_security/test_tutorial006_an_py39.py b/tests/test_tutorial/test_security/test_tutorial006_an_py39.py index 7d7a851ac..52b22e573 100644 --- a/tests/test_tutorial/test_security/test_tutorial006_an_py39.py +++ b/tests/test_tutorial/test_security/test_tutorial006_an_py39.py @@ -54,7 +54,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/me": { diff --git a/tests/test_tutorial/test_sql_databases/test_sql_databases.py b/tests/test_tutorial/test_sql_databases/test_sql_databases.py index a2628f3c3..d927940da 100644 --- a/tests/test_tutorial/test_sql_databases/test_sql_databases.py +++ b/tests/test_tutorial/test_sql_databases/test_sql_databases.py @@ -89,7 +89,7 @@ def test_openapi_schema(client): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/": { diff --git a/tests/test_tutorial/test_sql_databases/test_sql_databases_middleware.py b/tests/test_tutorial/test_sql_databases/test_sql_databases_middleware.py index 02501b1a2..08d7b3533 100644 --- a/tests/test_tutorial/test_sql_databases/test_sql_databases_middleware.py +++ b/tests/test_tutorial/test_sql_databases/test_sql_databases_middleware.py @@ -91,7 +91,7 @@ def test_openapi_schema(client): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/": { diff --git a/tests/test_tutorial/test_sql_databases/test_sql_databases_middleware_py310.py b/tests/test_tutorial/test_sql_databases/test_sql_databases_middleware_py310.py index 67b92ea4a..493fb3b6b 100644 --- a/tests/test_tutorial/test_sql_databases/test_sql_databases_middleware_py310.py +++ b/tests/test_tutorial/test_sql_databases/test_sql_databases_middleware_py310.py @@ -105,7 +105,7 @@ def test_openapi_schema(client): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/": { diff --git a/tests/test_tutorial/test_sql_databases/test_sql_databases_middleware_py39.py b/tests/test_tutorial/test_sql_databases/test_sql_databases_middleware_py39.py index a2af20ea2..7b56685bc 100644 --- a/tests/test_tutorial/test_sql_databases/test_sql_databases_middleware_py39.py +++ b/tests/test_tutorial/test_sql_databases/test_sql_databases_middleware_py39.py @@ -105,7 +105,7 @@ def test_openapi_schema(client): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/": { diff --git a/tests/test_tutorial/test_sql_databases/test_sql_databases_py310.py b/tests/test_tutorial/test_sql_databases/test_sql_databases_py310.py index 2f918cfd8..43c2b272f 100644 --- a/tests/test_tutorial/test_sql_databases/test_sql_databases_py310.py +++ b/tests/test_tutorial/test_sql_databases/test_sql_databases_py310.py @@ -104,7 +104,7 @@ def test_openapi_schema(client): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/": { diff --git a/tests/test_tutorial/test_sql_databases/test_sql_databases_py39.py b/tests/test_tutorial/test_sql_databases/test_sql_databases_py39.py index f2eefe41d..fd33517db 100644 --- a/tests/test_tutorial/test_sql_databases/test_sql_databases_py39.py +++ b/tests/test_tutorial/test_sql_databases/test_sql_databases_py39.py @@ -104,7 +104,7 @@ def test_openapi_schema(client): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/": { diff --git a/tests/test_tutorial/test_sql_databases_peewee/test_sql_databases_peewee.py b/tests/test_tutorial/test_sql_databases_peewee/test_sql_databases_peewee.py index d2470a2db..ac6c427ca 100644 --- a/tests/test_tutorial/test_sql_databases_peewee/test_sql_databases_peewee.py +++ b/tests/test_tutorial/test_sql_databases_peewee/test_sql_databases_peewee.py @@ -97,7 +97,7 @@ def test_openapi_schema(client): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/": { diff --git a/tests/test_tutorial/test_sub_applications/test_tutorial001.py b/tests/test_tutorial/test_sub_applications/test_tutorial001.py index 00e9aec57..0790d207b 100644 --- a/tests/test_tutorial/test_sub_applications/test_tutorial001.py +++ b/tests/test_tutorial/test_sub_applications/test_tutorial001.py @@ -5,7 +5,7 @@ from docs_src.sub_applications.tutorial001 import app client = TestClient(app) openapi_schema_main = { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/app": { @@ -23,7 +23,7 @@ openapi_schema_main = { }, } openapi_schema_sub = { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/sub": { diff --git a/tests/test_tutorial/test_testing/test_main.py b/tests/test_tutorial/test_testing/test_main.py index 937ce75e4..fe3498081 100644 --- a/tests/test_tutorial/test_testing/test_main.py +++ b/tests/test_tutorial/test_testing/test_main.py @@ -9,7 +9,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/": { diff --git a/tests/test_tutorial/test_testing/test_tutorial001.py b/tests/test_tutorial/test_testing/test_tutorial001.py index f3db70af2..471e896c9 100644 --- a/tests/test_tutorial/test_testing/test_tutorial001.py +++ b/tests/test_tutorial/test_testing/test_tutorial001.py @@ -9,7 +9,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/": { diff --git a/tests/test_union_body.py b/tests/test_union_body.py index bc1e74432..57a14b574 100644 --- a/tests/test_union_body.py +++ b/tests/test_union_body.py @@ -39,7 +39,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_union_inherited_body.py b/tests/test_union_inherited_body.py index 988b920aa..c2a37d3dd 100644 --- a/tests/test_union_inherited_body.py +++ b/tests/test_union_inherited_body.py @@ -39,7 +39,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_webhooks_security.py b/tests/test_webhooks_security.py new file mode 100644 index 000000000..a1c7b18fb --- /dev/null +++ b/tests/test_webhooks_security.py @@ -0,0 +1,126 @@ +from datetime import datetime + +from fastapi import FastAPI, Security +from fastapi.security import HTTPBearer +from fastapi.testclient import TestClient +from pydantic import BaseModel +from typing_extensions import Annotated + +app = FastAPI() + +bearer_scheme = HTTPBearer() + + +class Subscription(BaseModel): + username: str + montly_fee: float + start_date: datetime + + +@app.webhooks.post("new-subscription") +def new_subscription( + body: Subscription, token: Annotated[str, Security(bearer_scheme)] +): + """ + When a new user subscribes to your service we'll send you a POST request with this + data to the URL that you register for the event `new-subscription` in the dashboard. + """ + + +client = TestClient(app) + + +def test_dummy_webhook(): + # Just for coverage + new_subscription(body={}, token="Bearer 123") + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + # insert_assert(response.json()) + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": {}, + "webhooks": { + "new-subscription": { + "post": { + "summary": "New Subscription", + "description": "When a new user subscribes to your service we'll send you a POST request with this\ndata to the URL that you register for the event `new-subscription` in the dashboard.", + "operationId": "new_subscriptionnew_subscription_post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Subscription"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "security": [{"HTTPBearer": []}], + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": {"$ref": "#/components/schemas/ValidationError"}, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "Subscription": { + "properties": { + "username": {"type": "string", "title": "Username"}, + "montly_fee": {"type": "number", "title": "Montly Fee"}, + "start_date": { + "type": "string", + "format": "date-time", + "title": "Start Date", + }, + }, + "type": "object", + "required": ["username", "montly_fee", "start_date"], + "title": "Subscription", + }, + "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", + }, + }, + "securitySchemes": {"HTTPBearer": {"type": "http", "scheme": "bearer"}}, + }, + } From b757211299b19e2715513826c41b7f48da2e51eb Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 30 Jun 2023 18:25:53 +0000 Subject: [PATCH 36/58] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index fca7502cc..edf8ef13e 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✨ Add support for OpenAPI 3.1.0. PR [#9770](https://github.com/tiangolo/fastapi/pull/9770) by [@tiangolo](https://github.com/tiangolo). * 🔨 Enable linenums in MkDocs Material during local live development to simplify highlighting code. PR [#9769](https://github.com/tiangolo/fastapi/pull/9769) by [@tiangolo](https://github.com/tiangolo). * ✨ Add support for `deque` objects and children in `jsonable_encoder`. PR [#9433](https://github.com/tiangolo/fastapi/pull/9433) by [@cranium](https://github.com/cranium). * ⬆ Update httpx requirement from <0.24.0,>=0.23.0 to >=0.23.0,<0.25.0. PR [#9724](https://github.com/tiangolo/fastapi/pull/9724) by [@dependabot[bot]](https://github.com/apps/dependabot). From efc2bcc57ac6a711a835f55159252c220db74213 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 30 Jun 2023 20:54:25 +0200 Subject: [PATCH 37/58] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index edf8ef13e..18abf0ee4 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,16 +2,32 @@ ## Latest Changes +### Features + * ✨ Add support for OpenAPI 3.1.0. PR [#9770](https://github.com/tiangolo/fastapi/pull/9770) by [@tiangolo](https://github.com/tiangolo). -* 🔨 Enable linenums in MkDocs Material during local live development to simplify highlighting code. PR [#9769](https://github.com/tiangolo/fastapi/pull/9769) by [@tiangolo](https://github.com/tiangolo). + * New support for documenting **webhooks**, read the new docs here: Advanced User Guide: OpenAPI Webhooks. + * Upgrade OpenAPI 3.1.0, this uses JSON Schema 2020-12. + * Upgrade Swagger UI to version 5.x.x, that supports OpenAPI 3.1.0. + * Updated `examples` field in `Query()`, `Cookie()`, `Body()`, etc. based on the latest JSON Schema and OpenAPI. Now it takes a list of examples and they are included directly in the JSON Schema, not outside. Read more about it (including the historical technical details) in the updated docs: Tutorial: Declare Request Example Data. + * ✨ Add support for `deque` objects and children in `jsonable_encoder`. PR [#9433](https://github.com/tiangolo/fastapi/pull/9433) by [@cranium](https://github.com/cranium). + +### Docs + +* 📝 Fix form for the FastAPI and friends newsletter. PR [#9749](https://github.com/tiangolo/fastapi/pull/9749) by [@tiangolo](https://github.com/tiangolo). + +### Translations + +* 🌐 Add Persian translation for `docs/fa/docs/advanced/sub-applications.md`. PR [#9692](https://github.com/tiangolo/fastapi/pull/9692) by [@mojtabapaso](https://github.com/mojtabapaso). +* 🌐 Add Russian translation for `docs/ru/docs/tutorial/response-model.md`. PR [#9675](https://github.com/tiangolo/fastapi/pull/9675) by [@glsglsgls](https://github.com/glsglsgls). + +### Internal + +* 🔨 Enable linenums in MkDocs Material during local live development to simplify highlighting code. PR [#9769](https://github.com/tiangolo/fastapi/pull/9769) by [@tiangolo](https://github.com/tiangolo). * ⬆ Update httpx requirement from <0.24.0,>=0.23.0 to >=0.23.0,<0.25.0. PR [#9724](https://github.com/tiangolo/fastapi/pull/9724) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump mkdocs-material from 9.1.16 to 9.1.17. PR [#9746](https://github.com/tiangolo/fastapi/pull/9746) by [@dependabot[bot]](https://github.com/apps/dependabot). * 🔥 Remove missing translation dummy pages, no longer necessary. PR [#9751](https://github.com/tiangolo/fastapi/pull/9751) by [@tiangolo](https://github.com/tiangolo). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#9259](https://github.com/tiangolo/fastapi/pull/9259) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). -* 🌐 Add Persian translation for `docs/fa/docs/advanced/sub-applications.md`. PR [#9692](https://github.com/tiangolo/fastapi/pull/9692) by [@mojtabapaso](https://github.com/mojtabapaso). -* 🌐 Add Russian translation for `docs/ru/docs/tutorial/response-model.md`. PR [#9675](https://github.com/tiangolo/fastapi/pull/9675) by [@glsglsgls](https://github.com/glsglsgls). -* 📝 Fix form for the FastAPI and friends newsletter. PR [#9749](https://github.com/tiangolo/fastapi/pull/9749) by [@tiangolo](https://github.com/tiangolo). * ✨ Add Material for MkDocs Insiders features and cards. PR [#9748](https://github.com/tiangolo/fastapi/pull/9748) by [@tiangolo](https://github.com/tiangolo). * 🔥 Remove languages without translations. PR [#9743](https://github.com/tiangolo/fastapi/pull/9743) by [@tiangolo](https://github.com/tiangolo). * ✨ Refactor docs for building scripts, use MkDocs hooks, simplify (remove) configs for languages. PR [#9742](https://github.com/tiangolo/fastapi/pull/9742) by [@tiangolo](https://github.com/tiangolo). From 983f1d34dbc9443fcd12159647aabdeabc5032a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 30 Jun 2023 20:55:17 +0200 Subject: [PATCH 38/58] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.99.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 3 +++ fastapi/__init__.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 18abf0ee4..b68bd5f00 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,9 @@ ## Latest Changes + +## 0.99.0 + ### Features * ✨ Add support for OpenAPI 3.1.0. PR [#9770](https://github.com/tiangolo/fastapi/pull/9770) by [@tiangolo](https://github.com/tiangolo). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 038e1ba86..9c4316e69 100644 --- a/fastapi/__init__.py +++ b/fastapi/__init__.py @@ -1,6 +1,6 @@ """FastAPI framework, high performance, easy to learn, fast to code, ready for production""" -__version__ = "0.98.0" +__version__ = "0.99.0" from starlette import status as status From 4d83f984cc42831e3883285e9bedc4d69f4c1ac2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 1 Jul 2023 18:43:29 +0200 Subject: [PATCH 39/58] =?UTF-8?q?=F0=9F=93=9D=20Update=20source=20examples?= =?UTF-8?q?=20to=20use=20new=20JSON=20Schema=20examples=20field=20(#9776)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 📝 Update source examples to use new JSON Schema examples field * ✅ Update tests for JSON Schema examples * 📝 Update highlights in JSON Schema examples --- docs/en/docs/tutorial/schema-extra-example.md | 10 +++---- docs_src/schema_extra_example/tutorial004.py | 27 ++++++------------- .../schema_extra_example/tutorial004_an.py | 27 ++++++------------- .../tutorial004_an_py310.py | 27 ++++++------------- .../tutorial004_an_py39.py | 27 ++++++------------- .../schema_extra_example/tutorial004_py310.py | 27 ++++++------------- .../test_tutorial004.py | 25 +++++------------ .../test_tutorial004_an.py | 25 +++++------------ .../test_tutorial004_an_py310.py | 25 +++++------------ .../test_tutorial004_an_py39.py | 25 +++++------------ .../test_tutorial004_py310.py | 25 +++++------------ 11 files changed, 80 insertions(+), 190 deletions(-) diff --git a/docs/en/docs/tutorial/schema-extra-example.md b/docs/en/docs/tutorial/schema-extra-example.md index 6cf8bf181..86ccb1f5a 100644 --- a/docs/en/docs/tutorial/schema-extra-example.md +++ b/docs/en/docs/tutorial/schema-extra-example.md @@ -116,19 +116,19 @@ You can of course also pass multiple `examples`: === "Python 3.10+" - ```Python hl_lines="23-49" + ```Python hl_lines="23-38" {!> ../../../docs_src/schema_extra_example/tutorial004_an_py310.py!} ``` === "Python 3.9+" - ```Python hl_lines="23-49" + ```Python hl_lines="23-38" {!> ../../../docs_src/schema_extra_example/tutorial004_an_py39.py!} ``` === "Python 3.6+" - ```Python hl_lines="24-50" + ```Python hl_lines="24-39" {!> ../../../docs_src/schema_extra_example/tutorial004_an.py!} ``` @@ -137,7 +137,7 @@ You can of course also pass multiple `examples`: !!! tip Prefer to use the `Annotated` version if possible. - ```Python hl_lines="19-45" + ```Python hl_lines="19-34" {!> ../../../docs_src/schema_extra_example/tutorial004_py310.py!} ``` @@ -146,7 +146,7 @@ You can of course also pass multiple `examples`: !!! tip Prefer to use the `Annotated` version if possible. - ```Python hl_lines="21-47" + ```Python hl_lines="21-36" {!> ../../../docs_src/schema_extra_example/tutorial004.py!} ``` diff --git a/docs_src/schema_extra_example/tutorial004.py b/docs_src/schema_extra_example/tutorial004.py index eb49293fd..75514a3e9 100644 --- a/docs_src/schema_extra_example/tutorial004.py +++ b/docs_src/schema_extra_example/tutorial004.py @@ -20,29 +20,18 @@ async def update_item( item: Item = Body( examples=[ { - "summary": "A normal example", - "description": "A **normal** item works correctly.", - "value": { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, }, { - "summary": "An example with converted data", - "description": "FastAPI can convert price `strings` to actual `numbers` automatically", - "value": { - "name": "Bar", - "price": "35.4", - }, + "name": "Bar", + "price": "35.4", }, { - "summary": "Invalid data is rejected with an error", - "value": { - "name": "Baz", - "price": "thirty five point four", - }, + "name": "Baz", + "price": "thirty five point four", }, ], ), diff --git a/docs_src/schema_extra_example/tutorial004_an.py b/docs_src/schema_extra_example/tutorial004_an.py index 567ec4702..e817302a2 100644 --- a/docs_src/schema_extra_example/tutorial004_an.py +++ b/docs_src/schema_extra_example/tutorial004_an.py @@ -23,29 +23,18 @@ async def update_item( Body( examples=[ { - "summary": "A normal example", - "description": "A **normal** item works correctly.", - "value": { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, }, { - "summary": "An example with converted data", - "description": "FastAPI can convert price `strings` to actual `numbers` automatically", - "value": { - "name": "Bar", - "price": "35.4", - }, + "name": "Bar", + "price": "35.4", }, { - "summary": "Invalid data is rejected with an error", - "value": { - "name": "Baz", - "price": "thirty five point four", - }, + "name": "Baz", + "price": "thirty five point four", }, ], ), diff --git a/docs_src/schema_extra_example/tutorial004_an_py310.py b/docs_src/schema_extra_example/tutorial004_an_py310.py index 026654835..650da3187 100644 --- a/docs_src/schema_extra_example/tutorial004_an_py310.py +++ b/docs_src/schema_extra_example/tutorial004_an_py310.py @@ -22,29 +22,18 @@ async def update_item( Body( examples=[ { - "summary": "A normal example", - "description": "A **normal** item works correctly.", - "value": { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, }, { - "summary": "An example with converted data", - "description": "FastAPI can convert price `strings` to actual `numbers` automatically", - "value": { - "name": "Bar", - "price": "35.4", - }, + "name": "Bar", + "price": "35.4", }, { - "summary": "Invalid data is rejected with an error", - "value": { - "name": "Baz", - "price": "thirty five point four", - }, + "name": "Baz", + "price": "thirty five point four", }, ], ), diff --git a/docs_src/schema_extra_example/tutorial004_an_py39.py b/docs_src/schema_extra_example/tutorial004_an_py39.py index 06219ede8..dc5a8fe49 100644 --- a/docs_src/schema_extra_example/tutorial004_an_py39.py +++ b/docs_src/schema_extra_example/tutorial004_an_py39.py @@ -22,29 +22,18 @@ async def update_item( Body( examples=[ { - "summary": "A normal example", - "description": "A **normal** item works correctly.", - "value": { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, }, { - "summary": "An example with converted data", - "description": "FastAPI can convert price `strings` to actual `numbers` automatically", - "value": { - "name": "Bar", - "price": "35.4", - }, + "name": "Bar", + "price": "35.4", }, { - "summary": "Invalid data is rejected with an error", - "value": { - "name": "Baz", - "price": "thirty five point four", - }, + "name": "Baz", + "price": "thirty five point four", }, ], ), diff --git a/docs_src/schema_extra_example/tutorial004_py310.py b/docs_src/schema_extra_example/tutorial004_py310.py index ef2b9d8cb..05996ac2a 100644 --- a/docs_src/schema_extra_example/tutorial004_py310.py +++ b/docs_src/schema_extra_example/tutorial004_py310.py @@ -18,29 +18,18 @@ async def update_item( item: Item = Body( examples=[ { - "summary": "A normal example", - "description": "A **normal** item works correctly.", - "value": { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, }, { - "summary": "An example with converted data", - "description": "FastAPI can convert price `strings` to actual `numbers` automatically", - "value": { - "name": "Bar", - "price": "35.4", - }, + "name": "Bar", + "price": "35.4", }, { - "summary": "Invalid data is rejected with an error", - "value": { - "name": "Baz", - "price": "thirty five point four", - }, + "name": "Baz", + "price": "thirty five point four", }, ], ), diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial004.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial004.py index dea136fb2..313cd51d6 100644 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial004.py +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial004.py @@ -46,26 +46,15 @@ def test_openapi_schema(): "title": "Item", "examples": [ { - "summary": "A normal example", - "description": "A **normal** item works correctly.", - "value": { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, }, + {"name": "Bar", "price": "35.4"}, { - "summary": "An example with converted data", - "description": "FastAPI can convert price `strings` to actual `numbers` automatically", - "value": {"name": "Bar", "price": "35.4"}, - }, - { - "summary": "Invalid data is rejected with an error", - "value": { - "name": "Baz", - "price": "thirty five point four", - }, + "name": "Baz", + "price": "thirty five point four", }, ], } diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an.py index 571feb19f..353401b78 100644 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an.py +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an.py @@ -46,26 +46,15 @@ def test_openapi_schema(): "title": "Item", "examples": [ { - "summary": "A normal example", - "description": "A **normal** item works correctly.", - "value": { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, }, + {"name": "Bar", "price": "35.4"}, { - "summary": "An example with converted data", - "description": "FastAPI can convert price `strings` to actual `numbers` automatically", - "value": {"name": "Bar", "price": "35.4"}, - }, - { - "summary": "Invalid data is rejected with an error", - "value": { - "name": "Baz", - "price": "thirty five point four", - }, + "name": "Baz", + "price": "thirty five point four", }, ], } diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py310.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py310.py index e25531794..79f4e1e1e 100644 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py310.py +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py310.py @@ -55,26 +55,15 @@ def test_openapi_schema(client: TestClient): "title": "Item", "examples": [ { - "summary": "A normal example", - "description": "A **normal** item works correctly.", - "value": { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, }, + {"name": "Bar", "price": "35.4"}, { - "summary": "An example with converted data", - "description": "FastAPI can convert price `strings` to actual `numbers` automatically", - "value": {"name": "Bar", "price": "35.4"}, - }, - { - "summary": "Invalid data is rejected with an error", - "value": { - "name": "Baz", - "price": "thirty five point four", - }, + "name": "Baz", + "price": "thirty five point four", }, ], } diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py39.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py39.py index dafc5afad..1ee120705 100644 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py39.py +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py39.py @@ -55,26 +55,15 @@ def test_openapi_schema(client: TestClient): "title": "Item", "examples": [ { - "summary": "A normal example", - "description": "A **normal** item works correctly.", - "value": { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, }, + {"name": "Bar", "price": "35.4"}, { - "summary": "An example with converted data", - "description": "FastAPI can convert price `strings` to actual `numbers` automatically", - "value": {"name": "Bar", "price": "35.4"}, - }, - { - "summary": "Invalid data is rejected with an error", - "value": { - "name": "Baz", - "price": "thirty five point four", - }, + "name": "Baz", + "price": "thirty five point four", }, ], } diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_py310.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_py310.py index 29004218b..b77368400 100644 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_py310.py +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_py310.py @@ -55,26 +55,15 @@ def test_openapi_schema(client: TestClient): "title": "Item", "examples": [ { - "summary": "A normal example", - "description": "A **normal** item works correctly.", - "value": { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, }, + {"name": "Bar", "price": "35.4"}, { - "summary": "An example with converted data", - "description": "FastAPI can convert price `strings` to actual `numbers` automatically", - "value": {"name": "Bar", "price": "35.4"}, - }, - { - "summary": "Invalid data is rejected with an error", - "value": { - "name": "Baz", - "price": "thirty five point four", - }, + "name": "Baz", + "price": "thirty five point four", }, ], } From 0f105d90769bb2fe4c79ff0f571f33db803fb16f Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 1 Jul 2023 16:44:12 +0000 Subject: [PATCH 40/58] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index b68bd5f00..983c0e218 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Update source examples to use new JSON Schema examples field. PR [#9776](https://github.com/tiangolo/fastapi/pull/9776) by [@tiangolo](https://github.com/tiangolo). ## 0.99.0 From 07e1dea467c9654ea771bfef23cb3bf9654feb01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 2 Jul 2023 17:58:23 +0200 Subject: [PATCH 41/58] =?UTF-8?q?=F0=9F=90=9B=20Fix=20JSON=20Schema=20acce?= =?UTF-8?q?pting=20bools=20as=20valid=20JSON=20Schemas,=20e.g.=20`addition?= =?UTF-8?q?alProperties:=20false`=20(#9781)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🐛 Fix JSON Schema accepting bools as valid JSON Schemas, e.g. additionalProperties: false * ✅ Add test to ensure additionalProperties can be false * ♻️ Tweak OpenAPI models to support Pydantic v1's JSON Schema for tuples --- fastapi/openapi/models.py | 46 +++++---- tests/test_additional_properties_bool.py | 115 +++++++++++++++++++++++ 2 files changed, 142 insertions(+), 19 deletions(-) create mode 100644 tests/test_additional_properties_bool.py diff --git a/fastapi/openapi/models.py b/fastapi/openapi/models.py index 7420d3b55..a2ea53607 100644 --- a/fastapi/openapi/models.py +++ b/fastapi/openapi/models.py @@ -114,27 +114,30 @@ class Schema(BaseModel): dynamicAnchor: Optional[str] = Field(default=None, alias="$dynamicAnchor") ref: Optional[str] = Field(default=None, alias="$ref") dynamicRef: Optional[str] = Field(default=None, alias="$dynamicRef") - defs: Optional[Dict[str, "Schema"]] = Field(default=None, alias="$defs") + defs: Optional[Dict[str, "SchemaOrBool"]] = Field(default=None, alias="$defs") comment: Optional[str] = Field(default=None, alias="$comment") # Ref: JSON Schema 2020-12: https://json-schema.org/draft/2020-12/json-schema-core.html#name-a-vocabulary-for-applying-s # A Vocabulary for Applying Subschemas - allOf: Optional[List["Schema"]] = None - anyOf: Optional[List["Schema"]] = None - oneOf: Optional[List["Schema"]] = None - not_: Optional["Schema"] = Field(default=None, alias="not") - if_: Optional["Schema"] = Field(default=None, alias="if") - then: Optional["Schema"] = None - else_: Optional["Schema"] = Field(default=None, alias="else") - dependentSchemas: Optional[Dict[str, "Schema"]] = None - prefixItems: Optional[List["Schema"]] = None - items: Optional[Union["Schema", List["Schema"]]] = None - contains: Optional["Schema"] = None - properties: Optional[Dict[str, "Schema"]] = None - patternProperties: Optional[Dict[str, "Schema"]] = None - additionalProperties: Optional["Schema"] = None - propertyNames: Optional["Schema"] = None - unevaluatedItems: Optional["Schema"] = None - unevaluatedProperties: Optional["Schema"] = None + allOf: Optional[List["SchemaOrBool"]] = None + anyOf: Optional[List["SchemaOrBool"]] = None + oneOf: Optional[List["SchemaOrBool"]] = None + not_: Optional["SchemaOrBool"] = Field(default=None, alias="not") + if_: Optional["SchemaOrBool"] = Field(default=None, alias="if") + then: Optional["SchemaOrBool"] = None + else_: Optional["SchemaOrBool"] = Field(default=None, alias="else") + dependentSchemas: Optional[Dict[str, "SchemaOrBool"]] = None + prefixItems: Optional[List["SchemaOrBool"]] = None + # TODO: uncomment and remove below when deprecating Pydantic v1 + # It generales a list of schemas for tuples, before prefixItems was available + # items: Optional["SchemaOrBool"] = None + items: Optional[Union["SchemaOrBool", List["SchemaOrBool"]]] = None + contains: Optional["SchemaOrBool"] = None + properties: Optional[Dict[str, "SchemaOrBool"]] = None + patternProperties: Optional[Dict[str, "SchemaOrBool"]] = None + additionalProperties: Optional["SchemaOrBool"] = None + propertyNames: Optional["SchemaOrBool"] = None + unevaluatedItems: Optional["SchemaOrBool"] = None + unevaluatedProperties: Optional["SchemaOrBool"] = None # Ref: JSON Schema Validation 2020-12: https://json-schema.org/draft/2020-12/json-schema-validation.html#name-a-vocabulary-for-structural # A Vocabulary for Structural Validation type: Optional[str] = None @@ -164,7 +167,7 @@ class Schema(BaseModel): # A Vocabulary for the Contents of String-Encoded Data contentEncoding: Optional[str] = None contentMediaType: Optional[str] = None - contentSchema: Optional["Schema"] = None + contentSchema: Optional["SchemaOrBool"] = None # Ref: JSON Schema Validation 2020-12: https://json-schema.org/draft/2020-12/json-schema-validation.html#name-a-vocabulary-for-basic-meta # A Vocabulary for Basic Meta-Data Annotations title: Optional[str] = None @@ -191,6 +194,11 @@ class Schema(BaseModel): extra: str = "allow" +# Ref: https://json-schema.org/draft/2020-12/json-schema-core.html#name-json-schema-documents +# A JSON Schema MUST be an object or a boolean. +SchemaOrBool = Union[Schema, bool] + + class Example(BaseModel): summary: Optional[str] = None description: Optional[str] = None diff --git a/tests/test_additional_properties_bool.py b/tests/test_additional_properties_bool.py new file mode 100644 index 000000000..e35c26342 --- /dev/null +++ b/tests/test_additional_properties_bool.py @@ -0,0 +1,115 @@ +from typing import Union + +from fastapi import FastAPI +from fastapi.testclient import TestClient +from pydantic import BaseModel + + +class FooBaseModel(BaseModel): + class Config: + extra = "forbid" + + +class Foo(FooBaseModel): + pass + + +app = FastAPI() + + +@app.post("/") +async def post( + foo: Union[Foo, None] = None, +): + return foo + + +client = TestClient(app) + + +def test_call_invalid(): + response = client.post("/", json={"foo": {"bar": "baz"}}) + assert response.status_code == 422 + + +def test_call_valid(): + response = client.post("/", json={}) + assert response.status_code == 200 + assert response.json() == {} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/": { + "post": { + "summary": "Post", + "operationId": "post__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Foo"} + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "Foo": { + "properties": {}, + "additionalProperties": False, + "type": "object", + "title": "Foo", + }, + "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", + }, + } + }, + } From 6bd4f5353154b53920c4f313d2b635107c696be5 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 2 Jul 2023 15:59:00 +0000 Subject: [PATCH 42/58] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 983c0e218..c13023ce3 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🐛 Fix JSON Schema accepting bools as valid JSON Schemas, e.g. `additionalProperties: false`. PR [#9781](https://github.com/tiangolo/fastapi/pull/9781) by [@tiangolo](https://github.com/tiangolo). * 📝 Update source examples to use new JSON Schema examples field. PR [#9776](https://github.com/tiangolo/fastapi/pull/9776) by [@tiangolo](https://github.com/tiangolo). ## 0.99.0 From 8a198fc1ed2647f1047099e8b19864ec13040e48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 2 Jul 2023 18:00:12 +0200 Subject: [PATCH 43/58] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index c13023ce3..f22146f4b 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,7 +2,15 @@ ## Latest Changes + +## 0.99.1 + +### Fixes + * 🐛 Fix JSON Schema accepting bools as valid JSON Schemas, e.g. `additionalProperties: false`. PR [#9781](https://github.com/tiangolo/fastapi/pull/9781) by [@tiangolo](https://github.com/tiangolo). + +### Docs + * 📝 Update source examples to use new JSON Schema examples field. PR [#9776](https://github.com/tiangolo/fastapi/pull/9776) by [@tiangolo](https://github.com/tiangolo). ## 0.99.0 From dd4e78ca7b09abdf0d4646fe4697316c021a8b2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 2 Jul 2023 18:00:39 +0200 Subject: [PATCH 44/58] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.99.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 9c4316e69..2d1bac2e1 100644 --- a/fastapi/__init__.py +++ b/fastapi/__init__.py @@ -1,6 +1,6 @@ """FastAPI framework, high performance, easy to learn, fast to code, ready for production""" -__version__ = "0.99.0" +__version__ = "0.99.1" from starlette import status as status From 0976185af96ab2ee39c949c0456be616b01f8669 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 7 Jul 2023 19:12:13 +0200 Subject: [PATCH 45/58] =?UTF-8?q?=E2=9C=A8=20Add=20support=20for=20Pydanti?= =?UTF-8?q?c=20v2=20(#9816)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ Pydantic v2 migration, initial implementation (#9500) * ✨ Add compat layer, for Pydantic v1 and v2 * ✨ Re-export Pydantic needed internals from compat, to later patch them for v1 * ♻️ Refactor internals to use new compatibility layers and run with Pydantic v2 * 📝 Update examples to run with Pydantic v2 * ✅ Update tests to use Pydantic v2 * 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks * ✅ Temporarily disable Peewee tests, afterwards I'll enable them only for Pydantic v1 * 🐛 Fix JSON Schema generation and OpenAPI ref template * 🐛 Fix model field creation with defaults from Pydantic v2 * 🐛 Fix body field creation, with new FieldInfo * ✨ Use and check new ResponseValidationError for server validation errors * ✅ Fix test_schema_extra_examples tests with ResponseValidationError * ✅ Add dirty-equals to tests for compatibility with Pydantic v1 and v2 * ✨ Add util to regenerate errors with custom loc * ✨ Generate validation errors with loc * ✅ Update tests for compatibility with Pydantic v1 and v2 * ✅ Update tests for Pydantic v2 in tests/test_filter_pydantic_sub_model.py * ✅ Refactor tests in tests/test_dependency_overrides.py for Pydantic v2, separate parameterized into independent tests to use insert_assert * ✅ Refactor OpenAPI test for tests/test_infer_param_optionality.py for consistency, and make it compatible with Pydantic v1 and v2 * ✅ Update tests for tests/test_multi_query_errors.py for Pydantic v1 and v2 * ✅ Update tests for tests/test_multi_body_errors.py for Pydantic v1 and v2 * ✅ Update tests for tests/test_multi_body_errors.py for Pydantic v1 and v2 * 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks * ♻️ Refactor tests for tests/test_path.py to inline pytest parameters, to make it easier to make them compatible with Pydantic v2 * ✅ Refactor and udpate tests for tests/test_path.py for Pydantic v1 and v2 * ♻️ Refactor and update tests for tests/test_query.py with compatibility for Pydantic v1 and v2 * ✅ Fix test with optional field without default None * ✅ Update tests for compatibility with Pydantic v2 * ✅ Update tutorial tests for Pydantic v2 * ♻️ Update OAuth2 dependencies for Pydantic v2 * ♻️ Refactor str check when checking for sequence types * ♻️ Rename regex to pattern to keep in sync with Pydantic v2 * ♻️ Refactor _compat.py, start moving conditional imports and declarations to specifics of Pydantic v1 or v2 * ✅ Update tests for OAuth2 security optional * ✅ Refactor tests for OAuth2 optional for Pydantic v2 * ✅ Refactor tests for OAuth2 security for compatibility with Pydantic v2 * 🐛 Fix location in compat layer for Pydantic v2 ModelField * ✅ Refactor tests for Pydantic v2 in tests/test_tutorial/test_bigger_applications/test_main_an_py39.py * 🐛 Add missing markers in Python 3.9 tests * ✅ Refactor tests for bigger apps for consistency with annotated ones and with support for Pydantic v2 * 🐛 Fix jsonable_encoder with new Pydantic v2 data types and Url * 🐛 Fix invalid JSON error for compatibility with Pydantic v2 * ✅ Update tests for behind_a_proxy for Pydantic v2 * ✅ Update tests for tests/test_tutorial/test_body/test_tutorial001_py310.py for Pydantic v2 * ✅ Update tests for tests/test_tutorial/test_body/test_tutorial001.py with Pydantic v2 and consistency with Python 3.10 tests * ✅ Fix tests for tutorial/body_fields for Pydantic v2 * ✅ Refactor tests for tutorial/body_multiple_params with Pydantic v2 * ✅ Update tests for tutorial/body_nested_models for Pydantic v2 * ✅ Update tests for tutorial/body_updates for Pydantic v2 * ✅ Update test for tutorial/cookie_params for Pydantic v2 * ✅ Fix tests for tests/test_tutorial/test_custom_request_and_route/test_tutorial002.py for Pydantic v2 * ✅ Update tests for tutorial/dataclasses for Pydantic v2 * ✅ Update tests for tutorial/dependencies for Pydantic v2 * ✅ Update tests for tutorial/extra_data_types for Pydantic v2 * ✅ Update tests for tutorial/handling_errors for Pydantic v2 * ✅ Fix test markers for Python 3.9 * ✅ Update tests for tutorial/header_params for Pydantic v2 * ✅ Update tests for Pydantic v2 in tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py * ✅ Fix extra tests for Pydantic v2 * ✅ Refactor test for parameters, to later fix Pydantic v2 * ✅ Update tests for tutorial/query_params for Pydantic v2 * ♻️ Update examples in docs to use new pattern instead of the old regex * ✅ Fix several tests for Pydantic v2 * ✅ Update and fix test for ResponseValidationError * 🐛 Fix check for sequences vs scalars, include bytes as scalar * 🐛 Fix check for complex data types, include UploadFile * 🐛 Add list to sequence annotation types * 🐛 Fix checks for uploads and add utils to find if an annotation is an upload (or bytes) * ✨ Add UnionType and NoneType to compat layer * ✅ Update tests for request_files for compatibility with Pydantic v2 and consistency with other tests * ✅ Fix testsw for request_forms for Pydantic v2 * ✅ Fix tests for request_forms_and_files for Pydantic v2 * ✅ Fix tests in tutorial/security for compatibility with Pydantic v2 * ⬆️ Upgrade required version of email_validator * ✅ Fix tests for params repr * ✅ Add Pydantic v2 pytest markers * Use match_pydantic_error_url * 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks * Use field_serializer instead of encoders in some tests * Show Undefined as ... in repr * Mark custom encoders test with xfail * Update test to reflect new serialization of Decimal as str * Use `model_validate` instead of `from_orm` * Update JSON schema to reflect required nullable * Add dirty-equals to pyproject.toml * Fix locs and error creation for use with pydantic 2.0a4 * Use the type adapter for serialization. This is hacky. * 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks * ✅ Refactor test_multi_body_errors for compatibility with Pydantic v1 and v2 * ✅ Refactor test_custom_encoder for Pydantic v1 and v2 * ✅ Set input to None for now, for compatibility with current tests * 🐛 Fix passing serialization params to model field when handling the response * ♻️ Refactor exceptions to not depend on Pydantic ValidationError class * ♻️ Revert/refactor params to simplify repr * ✅ Tweak tests for custom class encoders for Pydantic v1 and v2 * ✅ Tweak tests for jsonable_encoder for Pydantic v1 and v2 * ✅ Tweak test for compatibility with Pydantic v1 and v2 * 🐛 Fix filtering data with subclasses * 🐛 Workaround examples in OpenAPI schema * ✅ Add skip marker for SQL tutorial, needs to be updated either way * ✅ Update test for broken JSON * ✅ Fix test for broken JSON * ✅ Update tests for timedeltas * ✅ Fix test for plain text validation errors * ✅ Add markers for Pydantic v1 exclusive tests (for now) * ✅ Update test for path_params with enums for compatibility with Pydantic v1 and v2 * ✅ Update tests for extra examples in OpenAPI * ✅ Fix tests for response_model with compatibility with Pydantic v1 and v2 * 🐛 Fix required double serialization for different types of models * ✅ Fix tests for response model with compatibility with new Pydantic v2 * 🐛 Import Undefined from compat layer * ✅ Fix tests for response_model for Pydantic v2 * ✅ Fix tests for schema_extra for Pydantic v2 * ✅ Add markers and update tests for Pydantic v2 * 💡 Comment out logic for double encoding that breaks other usecases * ✅ Update errors for int parsing * ♻️ Refactor re-enabling compatibility for Pydantic v1 * ♻️ Refactor OpenAPI utils to re-enable support for Pydantic v1 * ♻️ Refactor dependencies/utils and _compat for compatibility with Pydantic v1 * 🐛 Fix and tweak compatibility with Pydantic v1 and v2 in dependencies/utils * ✅ Tweak tests and examples for Pydantic v1 * ♻️ Tweak call to ModelField.validate for compatibility with Pydantic v1 * ✨ Use new global override TypeAdapter from_attributes * ✅ Update tests after updating from_attributes * 🔧 Update pytest config to avoid collecting tests from docs, useful for editor-integrated tests * ✅ Add test for data filtering, including inheritance and models in fields or lists of models * ♻️ Make OpenAPI models compatible with both Pydantic v1 and v2 * ♻️ Fix compatibility for Pydantic v1 and v2 in jsonable_encoder * ♻️ Fix compatibility in params with Pydantic v1 and v2 * ♻️ Fix compatibility when creating a FieldInfo in Pydantic v1 and v2 in utils.py * ♻️ Fix generation of flat_models and JSON Schema definitions in _compat.py for Pydantic v1 and v2 * ♻️ Update handling of ErrorWrappers for Pydantic v1 * ♻️ Refactor checks and handling of types an sequences * ♻️ Refactor and cleanup comments with compatibility for Pydantic v1 and v2 * ♻️ Update UploadFile for compatibility with both Pydantic v1 and v2 * 🔥 Remove commented out unneeded code * 🐛 Fix mock of get_annotation_from_field_info for Pydantic v1 and v2 * 🐛 Fix params with compatibility for Pydantic v1 and v2, with schemas and new pattern vs regex * 🐛 Fix check if field is sequence for Pydantic v1 * ✅ Fix tests for custom_schema_fields, for compatibility with Pydantic v1 and v2 * ✅ Simplify and fix tests for jsonable_encoder with compatibility for Pydantic v1 and v2 * ✅ Fix tests for orm_mode with Pydantic v1 and compatibility with Pydantic v2 * ♻️ Refactor logic for normalizing Pydantic v1 ErrorWrappers * ♻️ Workaround for params with examples, before defining what to deprecate in Pydantic v1 and v2 for examples with JSON Schema vs OpenAPI * ✅ Fix tests for Pydantic v1 and v2 for response_by_alias * ✅ Fix test for schema_extra with compatibility with Pydantic v1 and v2 * ♻️ Tweak error regeneration with loc * ♻️ Update error handling and serializationwith compatibility for Pydantic v1 and v2 * ♻️ Re-enable custom encoders for Pydantic v1 * ♻️ Update ErrorWrapper reserialization in Pydantic v1, do it outside of FastAPI ValidationExceptions * ✅ Update test for filter_submodel, re-structure to simplify testing while keeping division of Pydantic v1 and v2 * ✅ Refactor Pydantic v1 only test that requires modifying environment variables * 🔥 Update test for plaintext error responses, for Pydantic v1 and v2 * ⏪️ Revert changes in DB tutorial to use Pydantic v1 (the new guide will have SQLModel) * ✅ Mark current SQL DB tutorial tests as Pydantic only * ♻️ Update datastructures for compatibility with Pydantic v1, not requiring pydantic-core * ♻️ Update encoders.py for compatibility with Pydantic v1 * ⏪️ Revert changes to Peewee, the docs for that are gonna live in a new HowTo section, not in the main tutorials * ♻️ Simplify response body kwargs generation * 🔥 Clean up comments * 🔥 Clean some tests and comments * ✅ Refactor tests to match new Pydantic error string URLs * ✅ Refactor tests for recursive models for Pydantic v1 and v2 * ✅ Update tests for Peewee, re-enable, Pydantic-v1-only * ♻️ Update FastAPI params to take regex and pattern arguments * ⏪️ Revert tutorial examples for pattern, it will be done in a subsequent PR * ⏪️ Revert changes in schema extra examples, it will be added later in a docs-specific PR * 💡 Add TODO comment to document str validations with pattern * 🔥 Remove unneeded comment * 📌 Upgrade Pydantic pin dependency * ⬆️ Upgrade email_validator dependency * 🐛 Tweak type annotations in _compat.py * 🔇 Tweak mypy errors for compat, for Pydantic v1 re-imports * 🐛 Tweak and fix type annotations * ➕ Update requirements-test.txt, re-add dirty-equals * 🔥 Remove unnecessary config * 🐛 Tweak type annotations * 🔥 Remove unnecessary type in dependencies/utils.py * 💡 Update comment in routing.py --------- Co-authored-by: David Montague <35119617+dmontagu@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> * 👷 Add CI for both Pydantic v1 and v2 (#9688) * 👷 Test and install Pydantic v1 and v2 in CI * 💚 Tweak CI config for Pydantic v1 and v2 * 💚 Fix Pydantic v2 specification in CI * 🐛 Fix type annotations for compatibility with Python 3.7 * 💚 Install Pydantic v2 for lints * 🐛 Fix type annotations for Pydantic v2 * 💚 Re-use test cache for lint * ♻️ Refactor internals for test coverage and performance (#9691) * ♻️ Tweak import of Annotated from typing_extensions, they are installed anyway * ♻️ Refactor _compat to define functions for Pydantic v1 or v2 once instead of checking inside * ✅ Add test for UploadFile for Pydantic v2 * ♻️ Refactor types and remove logic for impossible cases * ✅ Add missing tests from test refactor for path params * ✅ Add tests for new decimal encoder * 💡 Add TODO comment for decimals in encoders * 🔥 Remove unneeded dummy function * 🔥 Remove section of code in field_annotation_is_scalar covered by sub-call to field_annotation_is_complex * ♻️ Refactor and tweak variables and types in _compat * ✅ Add tests for corner cases and compat with Pydantic v1 and v2 * ♻️ Refactor type annotations * 🔖 Release version 0.100.0-beta1 * ♻️ Refactor parts that use optional requirements to make them compatible with installations without them (#9707) * ♻️ Refactor parts that use optional requirements to make them compatible with installations without them * ♻️ Update JSON Schema for email field without email-validator installed * 🐛 Fix support for Pydantic v2.0, small changes in their final release (#9771) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Sebastián Ramírez * 🔖 Release version 0.100.0-beta2 * ✨ OpenAPI 3.1.0 with Pydantic v2, merge `master` (#9773) * ➕ Add dirty-equals as a testing dependency (#9778) ➕ Add dirty-equals as a testing dependency, it seems it got lsot at some point * 🔀 Merge master, fix valid JSON Schema accepting bools (#9782) * ⏪️ Revert usage of custom logic for TypeAdapter JSON Schema, solved on the Pydantic side (#9787) ⏪️ Revert usage of custom logic for TypeAdapter JSON Schema, solved on Pydantic side * ♻️ Deprecate parameter `regex`, use `pattern` instead (#9786) * 📝 Update docs to deprecate regex, recommend pattern * ♻️ Update examples to use new pattern instead of regex * 📝 Add new example with deprecated regex * ♻️ Add deprecation notes and warnings for regex * ✅ Add tests for regex deprecation * ✅ Update tests for compatibility with Pydantic v1 * ✨ Update docs to use Pydantic v2 settings and add note and example about v1 (#9788) * ➕ Add pydantic-settings to all extras * 📝 Update docs for Pydantic settings * 📝 Update Settings source examples to use Pydantic v2, and add a Pydantic v1 version * ✅ Add tests for settings with Pydantic v1 and v2 * 🔥 Remove solved TODO comment * ♻️ Update conditional OpenAPI to use new Pydantic v2 settings * ✅ Update tests to import Annotated from typing_extensions for Python < 3.9 (#9795) * ➕ Add pydantic-extra-types to fastapi[extra] * ➕ temp: Install Pydantic from source to test JSON Schema metadata fixes (#9777) * ➕ Install Pydantic from source, from branch for JSON Schema with metadata * ➕ Update dependencies, install Pydantic main * ➕ Fix dependency URL for Pydantic from source * ➕ Add pydantic-settings for test requirements * 💡 Add TODO comments to re-enable Pydantic main (not from source) (#9796) * ✨ Add new Pydantic Field param options to Query, Cookie, Body, etc. (#9797) * 📝 Add docs for Pydantic v2 for `docs/en/docs/advanced/path-operation-advanced-configuration.md` (#9798) * 📝 Update docs in examples for settings with Pydantic v2 (#9799) * 📝 Update JSON Schema `examples` docs with Pydantic v2 (#9800) * ♻️ Use new Pydantic v2 JSON Schema generator (#9813) Co-authored-by: David Montague <35119617+dmontagu@users.noreply.github.com> * ♻️ Tweak type annotations and Pydantic version range (#9801) * 📌 Re-enable GA Pydantic, for v2, require minimum 2.0.2 (#9814) * 🔖 Release version 0.100.0-beta3 * 🔥 Remove duplicate type declaration from merge conflicts (#9832) * 👷‍♂️ Run tests with Pydantic v2 GA (#9830) 👷 Run tests for Pydantic v2 GA * 📝 Add notes to docs expecting Pydantic v2 and future updates (#9833) * 📝 Update index with new extras * 📝 Update release notes --------- Co-authored-by: David Montague <35119617+dmontagu@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Pastukhov Nikita --- .github/workflows/test.yml | 13 +- README.md | 2 + docs/en/docs/advanced/async-sql-databases.md | 7 + docs/en/docs/advanced/nosql-databases.md | 7 + .../path-operation-advanced-configuration.md | 34 +- docs/en/docs/advanced/settings.md | 72 +- docs/en/docs/advanced/sql-databases-peewee.md | 7 + docs/en/docs/advanced/testing-database.md | 7 + docs/en/docs/index.md | 2 + docs/en/docs/release-notes.md | 73 + .../tutorial/query-params-str-validations.md | 18 +- docs/en/docs/tutorial/schema-extra-example.md | 36 +- docs/en/docs/tutorial/sql-databases.md | 7 + docs_src/conditional_openapi/tutorial001.py | 2 +- docs_src/extra_models/tutorial003.py | 4 +- docs_src/extra_models/tutorial003_py310.py | 4 +- .../tutorial007.py | 4 +- .../tutorial007_pv1.py | 34 + .../tutorial004.py | 2 +- .../tutorial004_an.py | 2 +- .../tutorial004_an_py310.py | 2 +- .../tutorial004_an_py310_regex.py | 17 + .../tutorial004_an_py39.py | 2 +- .../tutorial004_py310.py | 2 +- .../tutorial010.py | 2 +- .../tutorial010_an.py | 2 +- .../tutorial010_an_py310.py | 2 +- .../tutorial010_an_py39.py | 2 +- .../tutorial010_py310.py | 2 +- docs_src/schema_extra_example/tutorial001.py | 5 +- .../schema_extra_example/tutorial001_pv1.py | 31 + .../schema_extra_example/tutorial001_py310.py | 5 +- .../tutorial001_py310_pv1.py | 29 + docs_src/settings/app01/config.py | 2 +- docs_src/settings/app02/config.py | 2 +- docs_src/settings/app02_an/config.py | 2 +- docs_src/settings/app02_an_py39/config.py | 2 +- docs_src/settings/app03/config.py | 2 +- docs_src/settings/app03_an/config.py | 5 +- docs_src/settings/app03_an/config_pv1.py | 10 + docs_src/settings/app03_an_py39/config.py | 2 +- docs_src/settings/tutorial001.py | 2 +- docs_src/settings/tutorial001_pv1.py | 21 + fastapi/__init__.py | 2 +- fastapi/_compat.py | 616 +++++++ fastapi/applications.py | 43 +- fastapi/datastructures.py | 35 +- fastapi/dependencies/models.py | 2 +- fastapi/dependencies/utils.py | 196 +-- fastapi/encoders.py | 102 +- fastapi/exceptions.py | 28 +- fastapi/openapi/constants.py | 1 + fastapi/openapi/models.py | 249 ++- fastapi/openapi/utils.py | 124 +- fastapi/param_functions.py | 263 ++- fastapi/params.py | 381 ++++- fastapi/routing.py | 131 +- fastapi/security/oauth2.py | 28 +- fastapi/types.py | 10 +- fastapi/utils.py | 126 +- pyproject.toml | 10 +- requirements-tests.txt | 4 +- tests/test_additional_properties_bool.py | 26 +- ...onal_responses_custom_model_in_callback.py | 26 +- tests/test_annotated.py | 49 +- tests/test_application.py | 23 +- tests/test_compat.py | 93 ++ tests/test_custom_schema_fields.py | 15 +- tests/test_datastructures.py | 6 + tests/test_datetime_custom_encoder.py | 53 +- tests/test_dependency_duplicates.py | 35 +- tests/test_dependency_overrides.py | 666 +++++--- tests/test_extra_routes.py | 10 +- .../__init__.py | 0 .../test_filter_pydantic_sub_model/app_pv1.py | 35 + .../test_filter_pydantic_sub_model_pv1.py} | 52 +- tests/test_filter_pydantic_sub_model_pv2.py | 182 +++ tests/test_infer_param_optionality.py | 277 +++- tests/test_inherited_custom_class.py | 86 +- tests/test_jsonable_encoder.py | 152 +- tests/test_multi_body_errors.py | 160 +- tests/test_multi_query_errors.py | 55 +- .../test_openapi_query_parameter_extension.py | 21 +- tests/test_openapi_servers.py | 15 +- tests/test_params_repr.py | 135 +- tests/test_path.py | 1429 ++++++++++++++--- tests/test_query.py | 460 +++++- tests/test_read_with_orm_mode.py | 83 +- tests/test_regex_deprecated_body.py | 182 +++ tests/test_regex_deprecated_params.py | 165 ++ ...test_request_body_parameters_media_type.py | 1 - tests/test_response_by_alias.py | 28 +- ...est_response_model_as_return_annotation.py | 16 +- tests/test_response_model_data_filter.py | 81 + ...sponse_model_data_filter_no_inheritance.py | 83 + tests/test_schema_extra_examples.py | 227 ++- tests/test_security_oauth2.py | 209 ++- tests/test_security_oauth2_optional.py | 209 ++- ...st_security_oauth2_optional_description.py | 209 ++- tests/test_skip_defaults.py | 2 +- tests/test_sub_callbacks.py | 43 +- tests/test_tuples.py | 91 +- .../test_tutorial002.py | 12 +- .../test_tutorial004.py | 12 +- .../test_tutorial001.py | 4 + .../test_behind_a_proxy/test_tutorial003.py | 18 +- .../test_behind_a_proxy/test_tutorial004.py | 18 +- .../test_bigger_applications/test_main.py | 476 ++++-- .../test_bigger_applications/test_main_an.py | 476 ++++-- .../test_main_an_py39.py | 470 ++++-- .../test_body/test_tutorial001.py | 459 ++++-- .../test_body/test_tutorial001_py310.py | 432 +++-- .../test_body_fields/test_tutorial001.py | 153 +- .../test_body_fields/test_tutorial001_an.py | 153 +- .../test_tutorial001_an_py310.py | 145 +- .../test_tutorial001_an_py39.py | 145 +- .../test_tutorial001_py310.py | 145 +- .../test_tutorial001.py | 147 +- .../test_tutorial001_an.py | 147 +- .../test_tutorial001_an_py310.py | 140 +- .../test_tutorial001_an_py39.py | 140 +- .../test_tutorial001_py310.py | 140 +- .../test_tutorial003.py | 250 ++- .../test_tutorial003_an.py | 250 ++- .../test_tutorial003_an_py310.py | 242 ++- .../test_tutorial003_an_py39.py | 242 ++- .../test_tutorial003_py310.py | 242 ++- .../test_tutorial009.py | 50 +- .../test_tutorial009_py39.py | 35 +- .../test_body_updates/test_tutorial001.py | 49 +- .../test_tutorial001_py310.py | 34 +- .../test_tutorial001_py39.py | 34 +- .../test_tutorial001.py | 23 +- .../test_cookie_params/test_tutorial001.py | 12 +- .../test_cookie_params/test_tutorial001_an.py | 12 +- .../test_tutorial001_an_py310.py | 12 +- .../test_tutorial001_an_py39.py | 12 +- .../test_tutorial001_py310.py | 12 +- .../test_tutorial002.py | 43 +- .../test_dataclasses/test_tutorial001.py | 57 +- .../test_dataclasses/test_tutorial002.py | 44 +- .../test_dataclasses/test_tutorial003.py | 33 +- .../test_dependencies/test_tutorial001.py | 23 +- .../test_dependencies/test_tutorial001_an.py | 23 +- .../test_tutorial001_an_py310.py | 23 +- .../test_tutorial001_an_py39.py | 23 +- .../test_tutorial001_py310.py | 23 +- .../test_dependencies/test_tutorial004.py | 12 +- .../test_dependencies/test_tutorial004_an.py | 12 +- .../test_tutorial004_an_py310.py | 12 +- .../test_tutorial004_an_py39.py | 12 +- .../test_tutorial004_py310.py | 12 +- .../test_dependencies/test_tutorial006.py | 52 +- .../test_dependencies/test_tutorial006_an.py | 52 +- .../test_tutorial006_an_py39.py | 52 +- .../test_dependencies/test_tutorial012.py | 102 +- .../test_dependencies/test_tutorial012_an.py | 102 +- .../test_tutorial012_an_py39.py | 102 +- .../test_extra_data_types/test_tutorial001.py | 108 +- .../test_tutorial001_an.py | 108 +- .../test_tutorial001_an_py310.py | 108 +- .../test_tutorial001_an_py39.py | 108 +- .../test_tutorial001_py310.py | 108 +- .../test_handling_errors/test_tutorial004.py | 18 +- .../test_handling_errors/test_tutorial005.py | 38 +- .../test_handling_errors/test_tutorial006.py | 35 +- .../test_header_params/test_tutorial001.py | 14 +- .../test_header_params/test_tutorial001_an.py | 12 +- .../test_tutorial001_an_py310.py | 12 +- .../test_tutorial001_py310.py | 12 +- .../test_header_params/test_tutorial002.py | 12 +- .../test_header_params/test_tutorial002_an.py | 12 +- .../test_tutorial002_an_py310.py | 12 +- .../test_tutorial002_an_py39.py | 12 +- .../test_tutorial002_py310.py | 12 +- .../test_header_params/test_tutorial003.py | 24 +- .../test_header_params/test_tutorial003_an.py | 24 +- .../test_tutorial003_an_py310.py | 24 +- .../test_tutorial003_an_py39.py | 30 +- .../test_tutorial003_py310.py | 24 +- .../test_tutorial001.py | 43 +- .../test_tutorial004.py | 23 +- .../test_tutorial007.py | 33 +- .../test_tutorial007_pv1.py | 106 ++ .../test_tutorial005.py | 23 +- .../test_tutorial005_py310.py | 23 +- .../test_tutorial005_py39.py | 23 +- .../test_path_params/test_tutorial005.py | 110 +- .../test_query_params/test_tutorial005.py | 55 +- .../test_query_params/test_tutorial006.py | 129 +- .../test_tutorial006_py310.py | 123 +- .../test_tutorial010.py | 125 +- .../test_tutorial010_an.py | 125 +- .../test_tutorial010_an_py310.py | 120 +- .../test_tutorial010_an_py39.py | 120 +- .../test_tutorial010_py310.py | 120 +- .../test_tutorial011.py | 23 +- .../test_tutorial011_an.py | 23 +- .../test_tutorial011_an_py310.py | 23 +- .../test_tutorial011_an_py39.py | 23 +- .../test_tutorial011_py310.py | 23 +- .../test_tutorial011_py39.py | 23 +- .../test_request_files/test_tutorial001.py | 52 +- .../test_request_files/test_tutorial001_02.py | 67 +- .../test_tutorial001_02_an.py | 67 +- .../test_tutorial001_02_an_py310.py | 67 +- .../test_tutorial001_02_an_py39.py | 67 +- .../test_tutorial001_02_py310.py | 67 +- .../test_request_files/test_tutorial001_an.py | 63 +- .../test_tutorial001_an_py39.py | 63 +- .../test_request_files/test_tutorial002.py | 63 +- .../test_request_files/test_tutorial002_an.py | 63 +- .../test_tutorial002_an_py39.py | 63 +- .../test_tutorial002_py39.py | 52 +- .../test_request_forms/test_tutorial001.py | 190 ++- .../test_request_forms/test_tutorial001_an.py | 190 ++- .../test_tutorial001_an_py39.py | 183 ++- .../test_tutorial001.py | 248 ++- .../test_tutorial001_an.py | 248 ++- .../test_tutorial001_an_py39.py | 227 ++- .../test_response_model/test_tutorial003.py | 23 +- .../test_tutorial003_01.py | 23 +- .../test_tutorial003_01_py310.py | 23 +- .../test_tutorial003_py310.py | 23 +- .../test_response_model/test_tutorial004.py | 12 +- .../test_tutorial004_py310.py | 12 +- .../test_tutorial004_py39.py | 12 +- .../test_response_model/test_tutorial005.py | 12 +- .../test_tutorial005_py310.py | 12 +- .../test_response_model/test_tutorial006.py | 12 +- .../test_tutorial006_py310.py | 12 +- .../test_tutorial001.py | 133 ++ .../test_tutorial001_pv1.py | 127 ++ .../test_tutorial001_py310.py | 135 ++ .../test_tutorial001_py310_pv1.py | 129 ++ .../test_tutorial004.py | 80 +- .../test_tutorial004_an.py | 80 +- .../test_tutorial004_an_py310.py | 80 +- .../test_tutorial004_an_py39.py | 80 +- .../test_tutorial004_py310.py | 80 +- .../test_security/test_tutorial003.py | 45 +- .../test_security/test_tutorial003_an.py | 45 +- .../test_tutorial003_an_py310.py | 45 +- .../test_security/test_tutorial003_an_py39.py | 45 +- .../test_security/test_tutorial003_py310.py | 45 +- .../test_security/test_tutorial005.py | 78 +- .../test_security/test_tutorial005_an.py | 78 +- .../test_tutorial005_an_py310.py | 78 +- .../test_security/test_tutorial005_an_py39.py | 78 +- .../test_security/test_tutorial005_py310.py | 78 +- .../test_security/test_tutorial005_py39.py | 78 +- .../test_tutorial/test_settings/test_app02.py | 11 +- .../test_settings/test_tutorial001.py | 19 + .../test_settings/test_tutorial001_pv1.py | 19 + .../test_sql_databases/test_sql_databases.py | 41 +- .../test_sql_databases_middleware.py | 41 +- .../test_sql_databases_middleware_py310.py | 41 +- .../test_sql_databases_middleware_py39.py | 41 +- .../test_sql_databases_py310.py | 41 +- .../test_sql_databases_py39.py | 41 +- .../test_testing_databases.py | 4 + .../test_testing_databases_py310.py | 4 +- .../test_testing_databases_py39.py | 4 +- .../test_sql_databases_peewee.py | 10 + tests/test_union_body.py | 14 +- tests/test_union_inherited_body.py | 25 +- tests/test_validate_response.py | 11 +- tests/test_validate_response_dataclass.py | 8 +- .../__init__.py | 0 .../app_pv1.py} | 30 - .../app_pv2.py | 51 + .../test_validate_response_recursive_pv1.py | 33 + .../test_validate_response_recursive_pv2.py | 33 + tests/utils.py | 3 + 274 files changed, 16751 insertions(+), 4601 deletions(-) create mode 100644 docs_src/path_operation_advanced_configuration/tutorial007_pv1.py create mode 100644 docs_src/query_params_str_validations/tutorial004_an_py310_regex.py create mode 100644 docs_src/schema_extra_example/tutorial001_pv1.py create mode 100644 docs_src/schema_extra_example/tutorial001_py310_pv1.py create mode 100644 docs_src/settings/app03_an/config_pv1.py create mode 100644 docs_src/settings/tutorial001_pv1.py create mode 100644 fastapi/_compat.py create mode 100644 tests/test_compat.py create mode 100644 tests/test_filter_pydantic_sub_model/__init__.py create mode 100644 tests/test_filter_pydantic_sub_model/app_pv1.py rename tests/{test_filter_pydantic_sub_model.py => test_filter_pydantic_sub_model/test_filter_pydantic_sub_model_pv1.py} (81%) create mode 100644 tests/test_filter_pydantic_sub_model_pv2.py create mode 100644 tests/test_regex_deprecated_body.py create mode 100644 tests/test_regex_deprecated_params.py create mode 100644 tests/test_response_model_data_filter.py create mode 100644 tests/test_response_model_data_filter_no_inheritance.py create mode 100644 tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007_pv1.py create mode 100644 tests/test_tutorial/test_schema_extra_example/test_tutorial001.py create mode 100644 tests/test_tutorial/test_schema_extra_example/test_tutorial001_pv1.py create mode 100644 tests/test_tutorial/test_schema_extra_example/test_tutorial001_py310.py create mode 100644 tests/test_tutorial/test_schema_extra_example/test_tutorial001_py310_pv1.py create mode 100644 tests/test_tutorial/test_settings/test_tutorial001.py create mode 100644 tests/test_tutorial/test_settings/test_tutorial001_pv1.py create mode 100644 tests/test_validate_response_recursive/__init__.py rename tests/{test_validate_response_recursive.py => test_validate_response_recursive/app_pv1.py} (58%) create mode 100644 tests/test_validate_response_recursive/app_pv2.py create mode 100644 tests/test_validate_response_recursive/test_validate_response_recursive_pv1.py create mode 100644 tests/test_validate_response_recursive/test_validate_response_recursive_pv2.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 84f101424..b95358d01 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,10 +25,12 @@ jobs: id: cache with: path: ${{ env.pythonLocation }} - key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml', 'requirements-tests.txt') }}-test-v03 + key: ${{ runner.os }}-python-${{ env.pythonLocation }}-pydantic-v2-${{ hashFiles('pyproject.toml', 'requirements-tests.txt') }}-test-v03 - name: Install Dependencies if: steps.cache.outputs.cache-hit != 'true' run: pip install -r requirements-tests.txt + - name: Install Pydantic v2 + run: pip install "pydantic>=2.0.2,<3.0.0" - name: Lint run: bash scripts/lint.sh @@ -37,6 +39,7 @@ jobs: strategy: matrix: python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + pydantic-version: ["pydantic-v1", "pydantic-v2"] fail-fast: false steps: - uses: actions/checkout@v3 @@ -51,10 +54,16 @@ jobs: id: cache with: path: ${{ env.pythonLocation }} - key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml', 'requirements-tests.txt') }}-test-v03 + key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ matrix.pydantic-version }}-${{ hashFiles('pyproject.toml', 'requirements-tests.txt') }}-test-v03 - name: Install Dependencies if: steps.cache.outputs.cache-hit != 'true' run: pip install -r requirements-tests.txt + - name: Install Pydantic v1 + if: matrix.pydantic-version == 'pydantic-v1' + run: pip install "pydantic>=1.10.0,<2.0.0" + - name: Install Pydantic v2 + if: matrix.pydantic-version == 'pydantic-v2' + run: pip install "pydantic>=2.0.2,<3.0.0" - run: mkdir coverage - name: Test run: bash scripts/test.sh diff --git a/README.md b/README.md index 7dc199367..36c71081e 100644 --- a/README.md +++ b/README.md @@ -447,6 +447,8 @@ To understand more about it, see the section email_validator - for email validation. +* pydantic-settings - for settings management. +* pydantic-extra-types - for extra types to be used with Pydantic. Used by Starlette: diff --git a/docs/en/docs/advanced/async-sql-databases.md b/docs/en/docs/advanced/async-sql-databases.md index 93c288e1b..12549a190 100644 --- a/docs/en/docs/advanced/async-sql-databases.md +++ b/docs/en/docs/advanced/async-sql-databases.md @@ -1,5 +1,12 @@ # Async SQL (Relational) Databases +!!! info + These docs are about to be updated. 🎉 + + The current version assumes Pydantic v1. + + The new docs will include Pydantic v2 and will use SQLModel once it is updated to use Pydantic v2 as well. + You can also use `encode/databases` with **FastAPI** to connect to databases using `async` and `await`. It is compatible with: diff --git a/docs/en/docs/advanced/nosql-databases.md b/docs/en/docs/advanced/nosql-databases.md index 6cc5a9385..606db35c7 100644 --- a/docs/en/docs/advanced/nosql-databases.md +++ b/docs/en/docs/advanced/nosql-databases.md @@ -1,5 +1,12 @@ # NoSQL (Distributed / Big Data) Databases +!!! info + These docs are about to be updated. 🎉 + + The current version assumes Pydantic v1. + + The new docs will hopefully use Pydantic v2 and will use ODMantic with MongoDB. + **FastAPI** can also be integrated with any NoSQL. Here we'll see an example using **Couchbase**, a document based NoSQL database. diff --git a/docs/en/docs/advanced/path-operation-advanced-configuration.md b/docs/en/docs/advanced/path-operation-advanced-configuration.md index 6d9a5fe70..7ca88d43e 100644 --- a/docs/en/docs/advanced/path-operation-advanced-configuration.md +++ b/docs/en/docs/advanced/path-operation-advanced-configuration.md @@ -150,9 +150,20 @@ And you could do this even if the data type in the request is not JSON. For example, in this application we don't use FastAPI's integrated functionality to extract the JSON Schema from Pydantic models nor the automatic validation for JSON. In fact, we are declaring the request content type as YAML, not JSON: -```Python hl_lines="17-22 24" -{!../../../docs_src/path_operation_advanced_configuration/tutorial007.py!} -``` +=== "Pydantic v2" + + ```Python hl_lines="17-22 24" + {!> ../../../docs_src/path_operation_advanced_configuration/tutorial007.py!} + ``` + +=== "Pydantic v1" + + ```Python hl_lines="17-22 24" + {!> ../../../docs_src/path_operation_advanced_configuration/tutorial007_pv1.py!} + ``` + +!!! info + In Pydantic version 1 the method to get the JSON Schema for a model was called `Item.schema()`, in Pydantic version 2, the method is called `Item.model_schema_json()`. Nevertheless, although we are not using the default integrated functionality, we are still using a Pydantic model to manually generate the JSON Schema for the data that we want to receive in YAML. @@ -160,9 +171,20 @@ Then we use the request directly, and extract the body as `bytes`. This means th And then in our code, we parse that YAML content directly, and then we are again using the same Pydantic model to validate the YAML content: -```Python hl_lines="26-33" -{!../../../docs_src/path_operation_advanced_configuration/tutorial007.py!} -``` +=== "Pydantic v2" + + ```Python hl_lines="26-33" + {!> ../../../docs_src/path_operation_advanced_configuration/tutorial007.py!} + ``` + +=== "Pydantic v1" + + ```Python hl_lines="26-33" + {!> ../../../docs_src/path_operation_advanced_configuration/tutorial007_pv1.py!} + ``` + +!!! info + In Pydantic version 1 the method to parse and validate an object was `Item.parse_obj()`, in Pydantic version 2, the method is called `Item.model_validate()`. !!! tip Here we re-use the same Pydantic model. diff --git a/docs/en/docs/advanced/settings.md b/docs/en/docs/advanced/settings.md index 60ec9c92c..8f6c7da93 100644 --- a/docs/en/docs/advanced/settings.md +++ b/docs/en/docs/advanced/settings.md @@ -125,7 +125,34 @@ That means that any value read in Python from an environment variable will be a ## Pydantic `Settings` -Fortunately, Pydantic provides a great utility to handle these settings coming from environment variables with Pydantic: Settings management. +Fortunately, Pydantic provides a great utility to handle these settings coming from environment variables with Pydantic: Settings management. + +### Install `pydantic-settings` + +First, install the `pydantic-settings` package: + +
+ +```console +$ pip install pydantic-settings +---> 100% +``` + +
+ +It also comes included when you install the `all` extras with: + +
+ +```console +$ pip install "fastapi[all]" +---> 100% +``` + +
+ +!!! info + In Pydantic v1 it came included with the main package. Now it is distributed as this independent package so that you can choose to install it or not if you don't need that functionality. ### Create the `Settings` object @@ -135,9 +162,20 @@ The same way as with Pydantic models, you declare class attributes with type ann You can use all the same validation features and tools you use for Pydantic models, like different data types and additional validations with `Field()`. -```Python hl_lines="2 5-8 11" -{!../../../docs_src/settings/tutorial001.py!} -``` +=== "Pydantic v2" + + ```Python hl_lines="2 5-8 11" + {!> ../../../docs_src/settings/tutorial001.py!} + ``` + +=== "Pydantic v1" + + !!! info + In Pydantic v1 you would import `BaseSettings` directly from `pydantic` instead of from `pydantic_settings`. + + ```Python hl_lines="2 5-8 11" + {!> ../../../docs_src/settings/tutorial001_pv1.py!} + ``` !!! tip If you want something quick to copy and paste, don't use this example, use the last one below. @@ -306,14 +344,28 @@ APP_NAME="ChimichangApp" And then update your `config.py` with: -```Python hl_lines="9-10" -{!../../../docs_src/settings/app03/config.py!} -``` +=== "Pydantic v2" + + ```Python hl_lines="9" + {!> ../../../docs_src/settings/app03_an/config.py!} + ``` -Here we create a class `Config` inside of your Pydantic `Settings` class, and set the `env_file` to the filename with the dotenv file we want to use. + !!! tip + The `model_config` attribute is used just for Pydantic configuration. You can read more at Pydantic Model Config. -!!! tip - The `Config` class is used just for Pydantic configuration. You can read more at Pydantic Model Config +=== "Pydantic v1" + + ```Python hl_lines="9-10" + {!> ../../../docs_src/settings/app03_an/config_pv1.py!} + ``` + + !!! tip + The `Config` class is used just for Pydantic configuration. You can read more at Pydantic Model Config. + +!!! info + In Pydantic version 1 the configuration was done in an internal class `Config`, in Pydantic version 2 it's done in an attribute `model_config`. This attribute takes a `dict`, and to get autocompletion and inline errors you can import and use `SettingsConfigDict` to define that `dict`. + +Here we define the config `env_file` inside of your Pydantic `Settings` class, and set the value to the filename with the dotenv file we want to use. ### Creating the `Settings` only once with `lru_cache` diff --git a/docs/en/docs/advanced/sql-databases-peewee.md b/docs/en/docs/advanced/sql-databases-peewee.md index b4ea61367..6a469634f 100644 --- a/docs/en/docs/advanced/sql-databases-peewee.md +++ b/docs/en/docs/advanced/sql-databases-peewee.md @@ -5,6 +5,13 @@ Feel free to skip this. + Peewee is not recommended with FastAPI as it doesn't play well with anything async Python. There are several better alternatives. + +!!! info + These docs assume Pydantic v1. + + Because Pewee doesn't play well with anything async and there are better alternatives, I won't update these docs for Pydantic v2, they are kept for now only for historical purposes. + If you are starting a project from scratch, you are probably better off with SQLAlchemy ORM ([SQL (Relational) Databases](../tutorial/sql-databases.md){.internal-link target=_blank}), or any other async ORM. If you already have a code base that uses Peewee ORM, you can check here how to use it with **FastAPI**. diff --git a/docs/en/docs/advanced/testing-database.md b/docs/en/docs/advanced/testing-database.md index 13a6959b6..1c0669b9c 100644 --- a/docs/en/docs/advanced/testing-database.md +++ b/docs/en/docs/advanced/testing-database.md @@ -1,5 +1,12 @@ # Testing a Database +!!! info + These docs are about to be updated. 🎉 + + The current version assumes Pydantic v1, and SQLAlchemy versions less than 2.0. + + The new docs will include Pydantic v2 and will use SQLModel (which is also based on SQLAlchemy) once it is updated to use Pydantic v2 as well. + You can use the same dependency overrides from [Testing Dependencies with Overrides](testing-dependencies.md){.internal-link target=_blank} to alter a database for testing. You could want to set up a different database for testing, rollback the data after the tests, pre-fill it with some testing data, etc. diff --git a/docs/en/docs/index.md b/docs/en/docs/index.md index afd6d7138..ebd74bc8f 100644 --- a/docs/en/docs/index.md +++ b/docs/en/docs/index.md @@ -446,6 +446,8 @@ To understand more about it, see the section email_validator - for email validation. +* pydantic-settings - for settings management. +* pydantic-extra-types - for extra types to be used with Pydantic. Used by Starlette: diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index f22146f4b..f4ce74404 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,79 @@ ## Latest Changes +✨ Support for **Pydantic v2** ✨ + +Pydantic version 2 has the **core** re-written in **Rust** and includes a lot of improvements and features, for example: + +* Improved **correctness** in corner cases. +* **Safer** types. +* Better **performance** and **less energy** consumption. +* Better **extensibility**. +* etc. + +...all this while keeping the **same Python API**. In most of the cases, for simple models, you can simply upgrade the Pydantic version and get all the benefits. 🚀 + +In some cases, for pure data validation and processing, you can get performance improvements of **20x** or more. This means 2,000% or more. 🤯 + +When you use **FastAPI**, there's a lot more going on, processing the request and response, handling dependencies, executing **your own code**, and particularly, **waiting for the network**. But you will probably still get some nice performance improvements just from the upgrade. + +The focus of this release is **compatibility** with Pydantic v1 and v2, to make sure your current apps keep working. Later there will be more focus on refactors, correctness, code improvements, and then **performance** improvements. Some third-party early beta testers that ran benchmarks on the beta releases of FastAPI reported improvements of **2x - 3x**. Which is not bad for just doing `pip install --upgrade fastapi pydantic`. This was not an official benchmark and I didn't check it myself, but it's a good sign. + +### Migration + +Check out the [Pydantic migration guide](https://docs.pydantic.dev/2.0/migration/). + +For the things that need changes in your Pydantic models, the Pydantic team built [`bump-pydantic`](https://github.com/pydantic/bump-pydantic). + +A command line tool that will **process your code** and update most of the things **automatically** for you. Make sure you have your code in git first, and review each of the changes to make sure everything is correct before committing the changes. + +### Pydantic v1 + +**This version of FastAPI still supports Pydantic v1**. And although Pydantic v1 will be deprecated at some point, ti will still be supported for a while. + +This means that you can install the new Pydantic v2, and if something fails, you can install Pydantic v1 while you fix any problems you might have, but having the latest FastAPI. + +There are **tests for both Pydantic v1 and v2**, and test **coverage** is kept at **100%**. + +### Changes + +* There are **new parameter** fields supported by Pydantic `Field()` for: + + * `Path()` + * `Query()` + * `Header()` + * `Cookie()` + * `Body()` + * `Form()` + * `File()` + +* The new parameter fields are: + + * `default_factory` + * `alias_priority` + * `validation_alias` + * `serialization_alias` + * `discriminator` + * `strict` + * `multiple_of` + * `allow_inf_nan` + * `max_digits` + * `decimal_places` + * `json_schema_extra` + +...you can read about them in the Pydantic docs. + +* The parameter `regex` has been deprecated and replaced by `pattern`. + * You can read more about it in the docs for [Query Parameters and String Validations: Add regular expressions](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#add-regular-expressions). +* New Pydantic models use an improved and simplified attribute `model_config` that takes a simple dict instead of an internal class `Config` for their configuration. + * You can read more about it in the docs for [Declare Request Example Data](https://fastapi.tiangolo.com/tutorial/schema-extra-example/). +* The attribute `schema_extra` for the internal class `Config` has been replaced by the key `json_schema_extra` in the new `model_config` dict. + * You can read more about it in the docs for [Declare Request Example Data](https://fastapi.tiangolo.com/tutorial/schema-extra-example/). +* When you install `"fastapi[all]"` it now also includes: + * pydantic-settings - for settings management. + * pydantic-extra-types - for extra types to be used with Pydantic. +* Now Pydantic Settings is an additional optional package (included in `"fastapi[all]"`). To use settings you should now import `from pydantic_settings import BaseSettings` instead of importing from `pydantic` directly. + * You can read more about it in the docs for [Settings and Environment Variables](https://fastapi.tiangolo.com/advanced/settings/). ## 0.99.1 diff --git a/docs/en/docs/tutorial/query-params-str-validations.md b/docs/en/docs/tutorial/query-params-str-validations.md index 549e6c75b..f87adddcb 100644 --- a/docs/en/docs/tutorial/query-params-str-validations.md +++ b/docs/en/docs/tutorial/query-params-str-validations.md @@ -277,7 +277,7 @@ You can also add a parameter `min_length`: ## Add regular expressions -You can define a regular expression that the parameter should match: +You can define a regular expression `pattern` that the parameter should match: === "Python 3.10+" @@ -315,7 +315,7 @@ You can define a ../../../docs_src/query_params_str_validations/tutorial004_an_py310_regex.py!} + ``` + +But know that this is deprecated and it should be updated to use the new parameter `pattern`. 🤓 + ## Default values You can, of course, use default values other than `None`. diff --git a/docs/en/docs/tutorial/schema-extra-example.md b/docs/en/docs/tutorial/schema-extra-example.md index 86ccb1f5a..39d184763 100644 --- a/docs/en/docs/tutorial/schema-extra-example.md +++ b/docs/en/docs/tutorial/schema-extra-example.md @@ -4,24 +4,48 @@ You can declare examples of the data your app can receive. Here are several ways to do it. -## Pydantic `schema_extra` +## Extra JSON Schema data in Pydantic models -You can declare `examples` for a Pydantic model using `Config` and `schema_extra`, as described in Pydantic's docs: Schema customization: +You can declare `examples` for a Pydantic model that will be added to the generated JSON Schema. -=== "Python 3.10+" +=== "Python 3.10+ Pydantic v2" - ```Python hl_lines="13-23" + ```Python hl_lines="13-24" {!> ../../../docs_src/schema_extra_example/tutorial001_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.10+ Pydantic v1" - ```Python hl_lines="15-25" + ```Python hl_lines="13-23" + {!> ../../../docs_src/schema_extra_example/tutorial001_py310_pv1.py!} + ``` + +=== "Python 3.6+ Pydantic v2" + + ```Python hl_lines="15-26" {!> ../../../docs_src/schema_extra_example/tutorial001.py!} ``` +=== "Python 3.6+ Pydantic v1" + + ```Python hl_lines="15-25" + {!> ../../../docs_src/schema_extra_example/tutorial001_pv1.py!} + ``` + That extra info will be added as-is to the output **JSON Schema** for that model, and it will be used in the API docs. +=== "Pydantic v2" + + In Pydantic version 2, you would use the attribute `model_config`, that takes a `dict` as described in Pydantic's docs: Model Config. + + You can set `"json_schema_extra"` with a `dict` containing any additonal data you would like to show up in the generated JSON Schema, including `examples`. + +=== "Pydantic v1" + + In Pydantic version 1, you would use an internal class `Config` and `schema_extra`, as described in Pydantic's docs: Schema customization. + + You can set `schema_extra` with a `dict` containing any additonal data you would like to show up in the generated JSON Schema, including `examples`. + !!! tip You could use the same technique to extend the JSON Schema and add your own custom extra info. diff --git a/docs/en/docs/tutorial/sql-databases.md b/docs/en/docs/tutorial/sql-databases.md index fd66c5add..6e0e5dc06 100644 --- a/docs/en/docs/tutorial/sql-databases.md +++ b/docs/en/docs/tutorial/sql-databases.md @@ -1,5 +1,12 @@ # SQL (Relational) Databases +!!! info + These docs are about to be updated. 🎉 + + The current version assumes Pydantic v1, and SQLAlchemy versions less than 2.0. + + The new docs will include Pydantic v2 and will use SQLModel (which is also based on SQLAlchemy) once it is updated to use Pydantic v2 as well. + **FastAPI** doesn't require you to use a SQL (relational) database. But you can use any relational database that you want. diff --git a/docs_src/conditional_openapi/tutorial001.py b/docs_src/conditional_openapi/tutorial001.py index 717e723e8..eedb0d274 100644 --- a/docs_src/conditional_openapi/tutorial001.py +++ b/docs_src/conditional_openapi/tutorial001.py @@ -1,5 +1,5 @@ from fastapi import FastAPI -from pydantic import BaseSettings +from pydantic_settings import BaseSettings class Settings(BaseSettings): diff --git a/docs_src/extra_models/tutorial003.py b/docs_src/extra_models/tutorial003.py index 065439acc..06675cbc0 100644 --- a/docs_src/extra_models/tutorial003.py +++ b/docs_src/extra_models/tutorial003.py @@ -12,11 +12,11 @@ class BaseItem(BaseModel): class CarItem(BaseItem): - type = "car" + type: str = "car" class PlaneItem(BaseItem): - type = "plane" + type: str = "plane" size: int diff --git a/docs_src/extra_models/tutorial003_py310.py b/docs_src/extra_models/tutorial003_py310.py index 065439acc..06675cbc0 100644 --- a/docs_src/extra_models/tutorial003_py310.py +++ b/docs_src/extra_models/tutorial003_py310.py @@ -12,11 +12,11 @@ class BaseItem(BaseModel): class CarItem(BaseItem): - type = "car" + type: str = "car" class PlaneItem(BaseItem): - type = "plane" + type: str = "plane" size: int diff --git a/docs_src/path_operation_advanced_configuration/tutorial007.py b/docs_src/path_operation_advanced_configuration/tutorial007.py index d51752bb8..972ddbd2c 100644 --- a/docs_src/path_operation_advanced_configuration/tutorial007.py +++ b/docs_src/path_operation_advanced_configuration/tutorial007.py @@ -16,7 +16,7 @@ class Item(BaseModel): "/items/", openapi_extra={ "requestBody": { - "content": {"application/x-yaml": {"schema": Item.schema()}}, + "content": {"application/x-yaml": {"schema": Item.model_json_schema()}}, "required": True, }, }, @@ -28,7 +28,7 @@ async def create_item(request: Request): except yaml.YAMLError: raise HTTPException(status_code=422, detail="Invalid YAML") try: - item = Item.parse_obj(data) + item = Item.model_validate(data) except ValidationError as e: raise HTTPException(status_code=422, detail=e.errors()) return item diff --git a/docs_src/path_operation_advanced_configuration/tutorial007_pv1.py b/docs_src/path_operation_advanced_configuration/tutorial007_pv1.py new file mode 100644 index 000000000..d51752bb8 --- /dev/null +++ b/docs_src/path_operation_advanced_configuration/tutorial007_pv1.py @@ -0,0 +1,34 @@ +from typing import List + +import yaml +from fastapi import FastAPI, HTTPException, Request +from pydantic import BaseModel, ValidationError + +app = FastAPI() + + +class Item(BaseModel): + name: str + tags: List[str] + + +@app.post( + "/items/", + openapi_extra={ + "requestBody": { + "content": {"application/x-yaml": {"schema": Item.schema()}}, + "required": True, + }, + }, +) +async def create_item(request: Request): + raw_body = await request.body() + try: + data = yaml.safe_load(raw_body) + except yaml.YAMLError: + raise HTTPException(status_code=422, detail="Invalid YAML") + try: + item = Item.parse_obj(data) + except ValidationError as e: + raise HTTPException(status_code=422, detail=e.errors()) + return item diff --git a/docs_src/query_params_str_validations/tutorial004.py b/docs_src/query_params_str_validations/tutorial004.py index 5a7129816..3639b6c38 100644 --- a/docs_src/query_params_str_validations/tutorial004.py +++ b/docs_src/query_params_str_validations/tutorial004.py @@ -8,7 +8,7 @@ app = FastAPI() @app.get("/items/") async def read_items( q: Union[str, None] = Query( - default=None, min_length=3, max_length=50, regex="^fixedquery$" + default=None, min_length=3, max_length=50, pattern="^fixedquery$" ) ): results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} diff --git a/docs_src/query_params_str_validations/tutorial004_an.py b/docs_src/query_params_str_validations/tutorial004_an.py index 5346b997b..24698c7b3 100644 --- a/docs_src/query_params_str_validations/tutorial004_an.py +++ b/docs_src/query_params_str_validations/tutorial004_an.py @@ -9,7 +9,7 @@ app = FastAPI() @app.get("/items/") async def read_items( q: Annotated[ - Union[str, None], Query(min_length=3, max_length=50, regex="^fixedquery$") + Union[str, None], Query(min_length=3, max_length=50, pattern="^fixedquery$") ] = None ): results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} diff --git a/docs_src/query_params_str_validations/tutorial004_an_py310.py b/docs_src/query_params_str_validations/tutorial004_an_py310.py index 8fd375b3d..b7b629ee8 100644 --- a/docs_src/query_params_str_validations/tutorial004_an_py310.py +++ b/docs_src/query_params_str_validations/tutorial004_an_py310.py @@ -8,7 +8,7 @@ app = FastAPI() @app.get("/items/") async def read_items( q: Annotated[ - str | None, Query(min_length=3, max_length=50, regex="^fixedquery$") + str | None, Query(min_length=3, max_length=50, pattern="^fixedquery$") ] = None ): results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} diff --git a/docs_src/query_params_str_validations/tutorial004_an_py310_regex.py b/docs_src/query_params_str_validations/tutorial004_an_py310_regex.py new file mode 100644 index 000000000..8fd375b3d --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial004_an_py310_regex.py @@ -0,0 +1,17 @@ +from typing import Annotated + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: Annotated[ + str | None, Query(min_length=3, max_length=50, regex="^fixedquery$") + ] = None +): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial004_an_py39.py b/docs_src/query_params_str_validations/tutorial004_an_py39.py index 2fd82db75..8e9a6fc32 100644 --- a/docs_src/query_params_str_validations/tutorial004_an_py39.py +++ b/docs_src/query_params_str_validations/tutorial004_an_py39.py @@ -8,7 +8,7 @@ app = FastAPI() @app.get("/items/") async def read_items( q: Annotated[ - Union[str, None], Query(min_length=3, max_length=50, regex="^fixedquery$") + Union[str, None], Query(min_length=3, max_length=50, pattern="^fixedquery$") ] = None ): results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} diff --git a/docs_src/query_params_str_validations/tutorial004_py310.py b/docs_src/query_params_str_validations/tutorial004_py310.py index 180a2e511..f80798bcb 100644 --- a/docs_src/query_params_str_validations/tutorial004_py310.py +++ b/docs_src/query_params_str_validations/tutorial004_py310.py @@ -6,7 +6,7 @@ app = FastAPI() @app.get("/items/") async def read_items( q: str - | None = Query(default=None, min_length=3, max_length=50, regex="^fixedquery$") + | None = Query(default=None, min_length=3, max_length=50, pattern="^fixedquery$") ): results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} if q: diff --git a/docs_src/query_params_str_validations/tutorial010.py b/docs_src/query_params_str_validations/tutorial010.py index 35443d194..3314f8b6d 100644 --- a/docs_src/query_params_str_validations/tutorial010.py +++ b/docs_src/query_params_str_validations/tutorial010.py @@ -14,7 +14,7 @@ async def read_items( description="Query string for the items to search in the database that have a good match", min_length=3, max_length=50, - regex="^fixedquery$", + pattern="^fixedquery$", deprecated=True, ) ): diff --git a/docs_src/query_params_str_validations/tutorial010_an.py b/docs_src/query_params_str_validations/tutorial010_an.py index 8995f3f57..c5df00897 100644 --- a/docs_src/query_params_str_validations/tutorial010_an.py +++ b/docs_src/query_params_str_validations/tutorial010_an.py @@ -16,7 +16,7 @@ async def read_items( description="Query string for the items to search in the database that have a good match", min_length=3, max_length=50, - regex="^fixedquery$", + pattern="^fixedquery$", deprecated=True, ), ] = None diff --git a/docs_src/query_params_str_validations/tutorial010_an_py310.py b/docs_src/query_params_str_validations/tutorial010_an_py310.py index cfa81926c..a8e8c099b 100644 --- a/docs_src/query_params_str_validations/tutorial010_an_py310.py +++ b/docs_src/query_params_str_validations/tutorial010_an_py310.py @@ -15,7 +15,7 @@ async def read_items( description="Query string for the items to search in the database that have a good match", min_length=3, max_length=50, - regex="^fixedquery$", + pattern="^fixedquery$", deprecated=True, ), ] = None diff --git a/docs_src/query_params_str_validations/tutorial010_an_py39.py b/docs_src/query_params_str_validations/tutorial010_an_py39.py index 220eaabf4..955880dd6 100644 --- a/docs_src/query_params_str_validations/tutorial010_an_py39.py +++ b/docs_src/query_params_str_validations/tutorial010_an_py39.py @@ -15,7 +15,7 @@ async def read_items( description="Query string for the items to search in the database that have a good match", min_length=3, max_length=50, - regex="^fixedquery$", + pattern="^fixedquery$", deprecated=True, ), ] = None diff --git a/docs_src/query_params_str_validations/tutorial010_py310.py b/docs_src/query_params_str_validations/tutorial010_py310.py index f2839516e..9ea7b3c49 100644 --- a/docs_src/query_params_str_validations/tutorial010_py310.py +++ b/docs_src/query_params_str_validations/tutorial010_py310.py @@ -13,7 +13,7 @@ async def read_items( description="Query string for the items to search in the database that have a good match", min_length=3, max_length=50, - regex="^fixedquery$", + pattern="^fixedquery$", deprecated=True, ) ): diff --git a/docs_src/schema_extra_example/tutorial001.py b/docs_src/schema_extra_example/tutorial001.py index 6ab96ff85..32a66db3a 100644 --- a/docs_src/schema_extra_example/tutorial001.py +++ b/docs_src/schema_extra_example/tutorial001.py @@ -12,8 +12,8 @@ class Item(BaseModel): price: float tax: Union[float, None] = None - class Config: - schema_extra = { + model_config = { + "json_schema_extra": { "examples": [ { "name": "Foo", @@ -23,6 +23,7 @@ class Item(BaseModel): } ] } + } @app.put("/items/{item_id}") diff --git a/docs_src/schema_extra_example/tutorial001_pv1.py b/docs_src/schema_extra_example/tutorial001_pv1.py new file mode 100644 index 000000000..6ab96ff85 --- /dev/null +++ b/docs_src/schema_extra_example/tutorial001_pv1.py @@ -0,0 +1,31 @@ +from typing import Union + +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + price: float + tax: Union[float, None] = None + + class Config: + schema_extra = { + "examples": [ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + } + ] + } + + +@app.put("/items/{item_id}") +async def update_item(item_id: int, item: Item): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/schema_extra_example/tutorial001_py310.py b/docs_src/schema_extra_example/tutorial001_py310.py index ec83f1112..84aa5fc12 100644 --- a/docs_src/schema_extra_example/tutorial001_py310.py +++ b/docs_src/schema_extra_example/tutorial001_py310.py @@ -10,8 +10,8 @@ class Item(BaseModel): price: float tax: float | None = None - class Config: - schema_extra = { + model_config = { + "json_schema_extra": { "examples": [ { "name": "Foo", @@ -21,6 +21,7 @@ class Item(BaseModel): } ] } + } @app.put("/items/{item_id}") diff --git a/docs_src/schema_extra_example/tutorial001_py310_pv1.py b/docs_src/schema_extra_example/tutorial001_py310_pv1.py new file mode 100644 index 000000000..ec83f1112 --- /dev/null +++ b/docs_src/schema_extra_example/tutorial001_py310_pv1.py @@ -0,0 +1,29 @@ +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + + class Config: + schema_extra = { + "examples": [ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + } + ] + } + + +@app.put("/items/{item_id}") +async def update_item(item_id: int, item: Item): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/settings/app01/config.py b/docs_src/settings/app01/config.py index defede9db..b31b8811d 100644 --- a/docs_src/settings/app01/config.py +++ b/docs_src/settings/app01/config.py @@ -1,4 +1,4 @@ -from pydantic import BaseSettings +from pydantic_settings import BaseSettings class Settings(BaseSettings): diff --git a/docs_src/settings/app02/config.py b/docs_src/settings/app02/config.py index 9a7829135..e17b5035d 100644 --- a/docs_src/settings/app02/config.py +++ b/docs_src/settings/app02/config.py @@ -1,4 +1,4 @@ -from pydantic import BaseSettings +from pydantic_settings import BaseSettings class Settings(BaseSettings): diff --git a/docs_src/settings/app02_an/config.py b/docs_src/settings/app02_an/config.py index 9a7829135..e17b5035d 100644 --- a/docs_src/settings/app02_an/config.py +++ b/docs_src/settings/app02_an/config.py @@ -1,4 +1,4 @@ -from pydantic import BaseSettings +from pydantic_settings import BaseSettings class Settings(BaseSettings): diff --git a/docs_src/settings/app02_an_py39/config.py b/docs_src/settings/app02_an_py39/config.py index 9a7829135..e17b5035d 100644 --- a/docs_src/settings/app02_an_py39/config.py +++ b/docs_src/settings/app02_an_py39/config.py @@ -1,4 +1,4 @@ -from pydantic import BaseSettings +from pydantic_settings import BaseSettings class Settings(BaseSettings): diff --git a/docs_src/settings/app03/config.py b/docs_src/settings/app03/config.py index e1c3ee300..942aea3e5 100644 --- a/docs_src/settings/app03/config.py +++ b/docs_src/settings/app03/config.py @@ -1,4 +1,4 @@ -from pydantic import BaseSettings +from pydantic_settings import BaseSettings class Settings(BaseSettings): diff --git a/docs_src/settings/app03_an/config.py b/docs_src/settings/app03_an/config.py index e1c3ee300..08f8f88c2 100644 --- a/docs_src/settings/app03_an/config.py +++ b/docs_src/settings/app03_an/config.py @@ -1,4 +1,4 @@ -from pydantic import BaseSettings +from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): @@ -6,5 +6,4 @@ class Settings(BaseSettings): admin_email: str items_per_user: int = 50 - class Config: - env_file = ".env" + model_config = SettingsConfigDict(env_file=".env") diff --git a/docs_src/settings/app03_an/config_pv1.py b/docs_src/settings/app03_an/config_pv1.py new file mode 100644 index 000000000..e1c3ee300 --- /dev/null +++ b/docs_src/settings/app03_an/config_pv1.py @@ -0,0 +1,10 @@ +from pydantic import BaseSettings + + +class Settings(BaseSettings): + app_name: str = "Awesome API" + admin_email: str + items_per_user: int = 50 + + class Config: + env_file = ".env" diff --git a/docs_src/settings/app03_an_py39/config.py b/docs_src/settings/app03_an_py39/config.py index e1c3ee300..942aea3e5 100644 --- a/docs_src/settings/app03_an_py39/config.py +++ b/docs_src/settings/app03_an_py39/config.py @@ -1,4 +1,4 @@ -from pydantic import BaseSettings +from pydantic_settings import BaseSettings class Settings(BaseSettings): diff --git a/docs_src/settings/tutorial001.py b/docs_src/settings/tutorial001.py index 0cfd1b663..d48c4c060 100644 --- a/docs_src/settings/tutorial001.py +++ b/docs_src/settings/tutorial001.py @@ -1,5 +1,5 @@ from fastapi import FastAPI -from pydantic import BaseSettings +from pydantic_settings import BaseSettings class Settings(BaseSettings): diff --git a/docs_src/settings/tutorial001_pv1.py b/docs_src/settings/tutorial001_pv1.py new file mode 100644 index 000000000..0cfd1b663 --- /dev/null +++ b/docs_src/settings/tutorial001_pv1.py @@ -0,0 +1,21 @@ +from fastapi import FastAPI +from pydantic import BaseSettings + + +class Settings(BaseSettings): + app_name: str = "Awesome API" + admin_email: str + items_per_user: int = 50 + + +settings = Settings() +app = FastAPI() + + +@app.get("/info") +async def info(): + return { + "app_name": settings.app_name, + "admin_email": settings.admin_email, + "items_per_user": settings.items_per_user, + } diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 2d1bac2e1..5eb3c4de2 100644 --- a/fastapi/__init__.py +++ b/fastapi/__init__.py @@ -1,6 +1,6 @@ """FastAPI framework, high performance, easy to learn, fast to code, ready for production""" -__version__ = "0.99.1" +__version__ = "0.100.0-beta3" from starlette import status as status diff --git a/fastapi/_compat.py b/fastapi/_compat.py new file mode 100644 index 000000000..2233fe33c --- /dev/null +++ b/fastapi/_compat.py @@ -0,0 +1,616 @@ +from collections import deque +from copy import copy +from dataclasses import dataclass, is_dataclass +from enum import Enum +from typing import ( + Any, + Callable, + Deque, + Dict, + FrozenSet, + List, + Mapping, + Sequence, + Set, + Tuple, + Type, + Union, +) + +from fastapi.exceptions import RequestErrorModel +from fastapi.types import IncEx, ModelNameMap, UnionType +from pydantic import BaseModel, create_model +from pydantic.version import VERSION as PYDANTIC_VERSION +from starlette.datastructures import UploadFile +from typing_extensions import Annotated, Literal, get_args, get_origin + +PYDANTIC_V2 = PYDANTIC_VERSION.startswith("2.") + + +sequence_annotation_to_type = { + Sequence: list, + List: list, + list: list, + Tuple: tuple, + tuple: tuple, + Set: set, + set: set, + FrozenSet: frozenset, + frozenset: frozenset, + Deque: deque, + deque: deque, +} + +sequence_types = tuple(sequence_annotation_to_type.keys()) + +if PYDANTIC_V2: + from pydantic import PydanticSchemaGenerationError as PydanticSchemaGenerationError + from pydantic import TypeAdapter + from pydantic import ValidationError as ValidationError + from pydantic._internal._schema_generation_shared import ( # type: ignore[attr-defined] + GetJsonSchemaHandler as GetJsonSchemaHandler, + ) + from pydantic._internal._typing_extra import eval_type_lenient + from pydantic._internal._utils import lenient_issubclass as lenient_issubclass + from pydantic.fields import FieldInfo + from pydantic.json_schema import GenerateJsonSchema as GenerateJsonSchema + from pydantic.json_schema import JsonSchemaValue as JsonSchemaValue + from pydantic_core import CoreSchema as CoreSchema + from pydantic_core import MultiHostUrl as MultiHostUrl + from pydantic_core import PydanticUndefined, PydanticUndefinedType + from pydantic_core import Url as Url + from pydantic_core.core_schema import ( + general_plain_validator_function as general_plain_validator_function, + ) + + Required = PydanticUndefined + Undefined = PydanticUndefined + UndefinedType = PydanticUndefinedType + evaluate_forwardref = eval_type_lenient + Validator = Any + + class BaseConfig: + pass + + class ErrorWrapper(Exception): + pass + + @dataclass + class ModelField: + field_info: FieldInfo + name: str + mode: Literal["validation", "serialization"] = "validation" + + @property + def alias(self) -> str: + a = self.field_info.alias + return a if a is not None else self.name + + @property + def required(self) -> bool: + return self.field_info.is_required() + + @property + def default(self) -> Any: + return self.get_default() + + @property + def type_(self) -> Any: + return self.field_info.annotation + + def __post_init__(self) -> None: + self._type_adapter: TypeAdapter[Any] = TypeAdapter( + Annotated[self.field_info.annotation, self.field_info] + ) + + def get_default(self) -> Any: + if self.field_info.is_required(): + return Undefined + return self.field_info.get_default(call_default_factory=True) + + def validate( + self, + value: Any, + values: Dict[str, Any] = {}, # noqa: B006 + *, + loc: Tuple[Union[int, str], ...] = (), + ) -> Tuple[Any, Union[List[Dict[str, Any]], None]]: + try: + return ( + self._type_adapter.validate_python(value, from_attributes=True), + None, + ) + except ValidationError as exc: + return None, _regenerate_error_with_loc( + errors=exc.errors(), loc_prefix=loc + ) + + def serialize( + self, + value: Any, + *, + mode: Literal["json", "python"] = "json", + include: Union[IncEx, None] = None, + exclude: Union[IncEx, None] = None, + by_alias: bool = True, + exclude_unset: bool = False, + exclude_defaults: bool = False, + exclude_none: bool = False, + ) -> Any: + # What calls this code passes a value that already called + # self._type_adapter.validate_python(value) + return self._type_adapter.dump_python( + value, + mode=mode, + include=include, + exclude=exclude, + by_alias=by_alias, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + ) + + def __hash__(self) -> int: + # Each ModelField is unique for our purposes, to allow making a dict from + # ModelField to its JSON Schema. + return id(self) + + def get_annotation_from_field_info( + annotation: Any, field_info: FieldInfo, field_name: str + ) -> Any: + return annotation + + def _normalize_errors(errors: Sequence[Any]) -> List[Dict[str, Any]]: + return errors # type: ignore[return-value] + + def _model_rebuild(model: Type[BaseModel]) -> None: + model.model_rebuild() + + def _model_dump( + model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any + ) -> Any: + return model.model_dump(mode=mode, **kwargs) + + def _get_model_config(model: BaseModel) -> Any: + return model.model_config + + def get_schema_from_model_field( + *, + field: ModelField, + schema_generator: GenerateJsonSchema, + model_name_map: ModelNameMap, + field_mapping: Dict[ + Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue + ], + ) -> Dict[str, Any]: + # This expects that GenerateJsonSchema was already used to generate the definitions + json_schema = field_mapping[(field, field.mode)] + if "$ref" not in json_schema: + # TODO remove when deprecating Pydantic v1 + # Ref: https://github.com/pydantic/pydantic/blob/d61792cc42c80b13b23e3ffa74bc37ec7c77f7d1/pydantic/schema.py#L207 + json_schema[ + "title" + ] = field.field_info.title or field.alias.title().replace("_", " ") + return json_schema + + def get_compat_model_name_map(fields: List[ModelField]) -> ModelNameMap: + return {} + + def get_definitions( + *, + fields: List[ModelField], + schema_generator: GenerateJsonSchema, + model_name_map: ModelNameMap, + ) -> Tuple[ + Dict[ + Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue + ], + Dict[str, Dict[str, Any]], + ]: + inputs = [ + (field, field.mode, field._type_adapter.core_schema) for field in fields + ] + field_mapping, definitions = schema_generator.generate_definitions( + inputs=inputs + ) + return field_mapping, definitions # type: ignore[return-value] + + def is_scalar_field(field: ModelField) -> bool: + from fastapi import params + + return field_annotation_is_scalar( + field.field_info.annotation + ) and not isinstance(field.field_info, params.Body) + + def is_sequence_field(field: ModelField) -> bool: + return field_annotation_is_sequence(field.field_info.annotation) + + def is_scalar_sequence_field(field: ModelField) -> bool: + return field_annotation_is_scalar_sequence(field.field_info.annotation) + + def is_bytes_field(field: ModelField) -> bool: + return is_bytes_or_nonable_bytes_annotation(field.type_) + + def is_bytes_sequence_field(field: ModelField) -> bool: + return is_bytes_sequence_annotation(field.type_) + + def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo: + return type(field_info).from_annotation(annotation) + + def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]: + origin_type = ( + get_origin(field.field_info.annotation) or field.field_info.annotation + ) + assert issubclass(origin_type, sequence_types) # type: ignore[arg-type] + return sequence_annotation_to_type[origin_type](value) # type: ignore[no-any-return] + + def get_missing_field_error(loc: Tuple[str, ...]) -> Dict[str, Any]: + error = ValidationError.from_exception_data( + "Field required", [{"type": "missing", "loc": loc, "input": {}}] + ).errors()[0] + error["input"] = None + return error # type: ignore[return-value] + + def create_body_model( + *, fields: Sequence[ModelField], model_name: str + ) -> Type[BaseModel]: + field_params = {f.name: (f.field_info.annotation, f.field_info) for f in fields} + BodyModel: Type[BaseModel] = create_model(model_name, **field_params) # type: ignore[call-overload] + return BodyModel + +else: + from fastapi.openapi.constants import REF_PREFIX as REF_PREFIX + from pydantic import AnyUrl as Url # noqa: F401 + from pydantic import ( # type: ignore[assignment] + BaseConfig as BaseConfig, # noqa: F401 + ) + from pydantic import ValidationError as ValidationError # noqa: F401 + from pydantic.class_validators import ( # type: ignore[no-redef] + Validator as Validator, # noqa: F401 + ) + from pydantic.error_wrappers import ( # type: ignore[no-redef] + ErrorWrapper as ErrorWrapper, # noqa: F401 + ) + from pydantic.errors import MissingError + from pydantic.fields import ( # type: ignore[attr-defined] + SHAPE_FROZENSET, + SHAPE_LIST, + SHAPE_SEQUENCE, + SHAPE_SET, + SHAPE_SINGLETON, + SHAPE_TUPLE, + SHAPE_TUPLE_ELLIPSIS, + ) + from pydantic.fields import FieldInfo as FieldInfo + from pydantic.fields import ( # type: ignore[no-redef,attr-defined] + ModelField as ModelField, # noqa: F401 + ) + from pydantic.fields import ( # type: ignore[no-redef,attr-defined] + Required as Required, # noqa: F401 + ) + from pydantic.fields import ( # type: ignore[no-redef,attr-defined] + Undefined as Undefined, + ) + from pydantic.fields import ( # type: ignore[no-redef, attr-defined] + UndefinedType as UndefinedType, # noqa: F401 + ) + from pydantic.networks import ( # type: ignore[no-redef] + MultiHostDsn as MultiHostUrl, # noqa: F401 + ) + from pydantic.schema import ( + field_schema, + get_flat_models_from_fields, + get_model_name_map, + model_process_schema, + ) + from pydantic.schema import ( # type: ignore[no-redef] # noqa: F401 + get_annotation_from_field_info as get_annotation_from_field_info, + ) + from pydantic.typing import ( # type: ignore[no-redef] + evaluate_forwardref as evaluate_forwardref, # noqa: F401 + ) + from pydantic.utils import ( # type: ignore[no-redef] + lenient_issubclass as lenient_issubclass, # noqa: F401 + ) + + GetJsonSchemaHandler = Any # type: ignore[assignment,misc] + JsonSchemaValue = Dict[str, Any] # type: ignore[misc] + CoreSchema = Any # type: ignore[assignment,misc] + + sequence_shapes = { + SHAPE_LIST, + SHAPE_SET, + SHAPE_FROZENSET, + SHAPE_TUPLE, + SHAPE_SEQUENCE, + SHAPE_TUPLE_ELLIPSIS, + } + sequence_shape_to_type = { + SHAPE_LIST: list, + SHAPE_SET: set, + SHAPE_TUPLE: tuple, + SHAPE_SEQUENCE: list, + SHAPE_TUPLE_ELLIPSIS: list, + } + + @dataclass + class GenerateJsonSchema: # type: ignore[no-redef] + ref_template: str + + class PydanticSchemaGenerationError(Exception): # type: ignore[no-redef] + pass + + def general_plain_validator_function( # type: ignore[misc] + function: Callable[..., Any], + *, + ref: Union[str, None] = None, + metadata: Any = None, + serialization: Any = None, + ) -> Any: + return {} + + def get_model_definitions( + *, + flat_models: Set[Union[Type[BaseModel], Type[Enum]]], + model_name_map: Dict[Union[Type[BaseModel], Type[Enum]], str], + ) -> Dict[str, Any]: + definitions: Dict[str, Dict[str, Any]] = {} + for model in flat_models: + m_schema, m_definitions, m_nested_models = model_process_schema( + model, model_name_map=model_name_map, ref_prefix=REF_PREFIX + ) + definitions.update(m_definitions) + model_name = model_name_map[model] + if "description" in m_schema: + m_schema["description"] = m_schema["description"].split("\f")[0] + definitions[model_name] = m_schema + return definitions + + def is_pv1_scalar_field(field: ModelField) -> bool: + from fastapi import params + + field_info = field.field_info + if not ( + field.shape == SHAPE_SINGLETON # type: ignore[attr-defined] + and not lenient_issubclass(field.type_, BaseModel) + and not lenient_issubclass(field.type_, dict) + and not field_annotation_is_sequence(field.type_) + and not is_dataclass(field.type_) + and not isinstance(field_info, params.Body) + ): + return False + if field.sub_fields: # type: ignore[attr-defined] + if not all( + is_pv1_scalar_field(f) + for f in field.sub_fields # type: ignore[attr-defined] + ): + return False + return True + + def is_pv1_scalar_sequence_field(field: ModelField) -> bool: + if (field.shape in sequence_shapes) and not lenient_issubclass( # type: ignore[attr-defined] + field.type_, BaseModel + ): + if field.sub_fields is not None: # type: ignore[attr-defined] + for sub_field in field.sub_fields: # type: ignore[attr-defined] + if not is_pv1_scalar_field(sub_field): + return False + return True + if _annotation_is_sequence(field.type_): + return True + return False + + def _normalize_errors(errors: Sequence[Any]) -> List[Dict[str, Any]]: + use_errors: List[Any] = [] + for error in errors: + if isinstance(error, ErrorWrapper): + new_errors = ValidationError( # type: ignore[call-arg] + errors=[error], model=RequestErrorModel + ).errors() + use_errors.extend(new_errors) + elif isinstance(error, list): + use_errors.extend(_normalize_errors(error)) + else: + use_errors.append(error) + return use_errors + + def _model_rebuild(model: Type[BaseModel]) -> None: + model.update_forward_refs() + + def _model_dump( + model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any + ) -> Any: + return model.dict(**kwargs) + + def _get_model_config(model: BaseModel) -> Any: + return model.__config__ # type: ignore[attr-defined] + + def get_schema_from_model_field( + *, + field: ModelField, + schema_generator: GenerateJsonSchema, + model_name_map: ModelNameMap, + field_mapping: Dict[ + Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue + ], + ) -> Dict[str, Any]: + # This expects that GenerateJsonSchema was already used to generate the definitions + return field_schema( # type: ignore[no-any-return] + field, model_name_map=model_name_map, ref_prefix=REF_PREFIX + )[0] + + def get_compat_model_name_map(fields: List[ModelField]) -> ModelNameMap: + models = get_flat_models_from_fields(fields, known_models=set()) + return get_model_name_map(models) # type: ignore[no-any-return] + + def get_definitions( + *, + fields: List[ModelField], + schema_generator: GenerateJsonSchema, + model_name_map: ModelNameMap, + ) -> Tuple[ + Dict[ + Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue + ], + Dict[str, Dict[str, Any]], + ]: + models = get_flat_models_from_fields(fields, known_models=set()) + return {}, get_model_definitions( + flat_models=models, model_name_map=model_name_map + ) + + def is_scalar_field(field: ModelField) -> bool: + return is_pv1_scalar_field(field) + + def is_sequence_field(field: ModelField) -> bool: + return field.shape in sequence_shapes or _annotation_is_sequence(field.type_) # type: ignore[attr-defined] + + def is_scalar_sequence_field(field: ModelField) -> bool: + return is_pv1_scalar_sequence_field(field) + + def is_bytes_field(field: ModelField) -> bool: + return lenient_issubclass(field.type_, bytes) + + def is_bytes_sequence_field(field: ModelField) -> bool: + return field.shape in sequence_shapes and lenient_issubclass(field.type_, bytes) # type: ignore[attr-defined] + + def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo: + return copy(field_info) + + def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]: + return sequence_shape_to_type[field.shape](value) # type: ignore[no-any-return,attr-defined] + + def get_missing_field_error(loc: Tuple[str, ...]) -> Dict[str, Any]: + missing_field_error = ErrorWrapper(MissingError(), loc=loc) # type: ignore[call-arg] + new_error = ValidationError([missing_field_error], RequestErrorModel) + return new_error.errors()[0] # type: ignore[return-value] + + def create_body_model( + *, fields: Sequence[ModelField], model_name: str + ) -> Type[BaseModel]: + BodyModel = create_model(model_name) + for f in fields: + BodyModel.__fields__[f.name] = f # type: ignore[index] + return BodyModel + + +def _regenerate_error_with_loc( + *, errors: Sequence[Any], loc_prefix: Tuple[Union[str, int], ...] +) -> List[Dict[str, Any]]: + updated_loc_errors: List[Any] = [ + {**err, "loc": loc_prefix + err.get("loc", ())} + for err in _normalize_errors(errors) + ] + + return updated_loc_errors + + +def _annotation_is_sequence(annotation: Union[Type[Any], None]) -> bool: + if lenient_issubclass(annotation, (str, bytes)): + return False + return lenient_issubclass(annotation, sequence_types) + + +def field_annotation_is_sequence(annotation: Union[Type[Any], None]) -> bool: + return _annotation_is_sequence(annotation) or _annotation_is_sequence( + get_origin(annotation) + ) + + +def value_is_sequence(value: Any) -> bool: + return isinstance(value, sequence_types) and not isinstance(value, (str, bytes)) # type: ignore[arg-type] + + +def _annotation_is_complex(annotation: Union[Type[Any], None]) -> bool: + return ( + lenient_issubclass(annotation, (BaseModel, Mapping, UploadFile)) + or _annotation_is_sequence(annotation) + or is_dataclass(annotation) + ) + + +def field_annotation_is_complex(annotation: Union[Type[Any], None]) -> bool: + origin = get_origin(annotation) + if origin is Union or origin is UnionType: + return any(field_annotation_is_complex(arg) for arg in get_args(annotation)) + + return ( + _annotation_is_complex(annotation) + or _annotation_is_complex(origin) + or hasattr(origin, "__pydantic_core_schema__") + or hasattr(origin, "__get_pydantic_core_schema__") + ) + + +def field_annotation_is_scalar(annotation: Any) -> bool: + # handle Ellipsis here to make tuple[int, ...] work nicely + return annotation is Ellipsis or not field_annotation_is_complex(annotation) + + +def field_annotation_is_scalar_sequence(annotation: Union[Type[Any], None]) -> bool: + origin = get_origin(annotation) + if origin is Union or origin is UnionType: + at_least_one_scalar_sequence = False + for arg in get_args(annotation): + if field_annotation_is_scalar_sequence(arg): + at_least_one_scalar_sequence = True + continue + elif not field_annotation_is_scalar(arg): + return False + return at_least_one_scalar_sequence + return field_annotation_is_sequence(annotation) and all( + field_annotation_is_scalar(sub_annotation) + for sub_annotation in get_args(annotation) + ) + + +def is_bytes_or_nonable_bytes_annotation(annotation: Any) -> bool: + if lenient_issubclass(annotation, bytes): + return True + origin = get_origin(annotation) + if origin is Union or origin is UnionType: + for arg in get_args(annotation): + if lenient_issubclass(arg, bytes): + return True + return False + + +def is_uploadfile_or_nonable_uploadfile_annotation(annotation: Any) -> bool: + if lenient_issubclass(annotation, UploadFile): + return True + origin = get_origin(annotation) + if origin is Union or origin is UnionType: + for arg in get_args(annotation): + if lenient_issubclass(arg, UploadFile): + return True + return False + + +def is_bytes_sequence_annotation(annotation: Any) -> bool: + origin = get_origin(annotation) + if origin is Union or origin is UnionType: + at_least_one = False + for arg in get_args(annotation): + if is_bytes_sequence_annotation(arg): + at_least_one = True + continue + return at_least_one + return field_annotation_is_sequence(annotation) and all( + is_bytes_or_nonable_bytes_annotation(sub_annotation) + for sub_annotation in get_args(annotation) + ) + + +def is_uploadfile_sequence_annotation(annotation: Any) -> bool: + origin = get_origin(annotation) + if origin is Union or origin is UnionType: + at_least_one = False + for arg in get_args(annotation): + if is_uploadfile_sequence_annotation(arg): + at_least_one = True + continue + return at_least_one + return field_annotation_is_sequence(annotation) and all( + is_uploadfile_or_nonable_uploadfile_annotation(sub_annotation) + for sub_annotation in get_args(annotation) + ) diff --git a/fastapi/applications.py b/fastapi/applications.py index 88f861c1e..e32cfa03d 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -15,7 +15,6 @@ from typing import ( from fastapi import routing from fastapi.datastructures import Default, DefaultPlaceholder -from fastapi.encoders import DictIntStrAny, SetIntStr from fastapi.exception_handlers import ( http_exception_handler, request_validation_exception_handler, @@ -31,7 +30,7 @@ from fastapi.openapi.docs import ( ) from fastapi.openapi.utils import get_openapi from fastapi.params import Depends -from fastapi.types import DecoratedCallable +from fastapi.types import DecoratedCallable, IncEx from fastapi.utils import generate_unique_id from starlette.applications import Starlette from starlette.datastructures import State @@ -305,8 +304,8 @@ class FastAPI(Starlette): deprecated: Optional[bool] = None, methods: Optional[List[str]] = None, operation_id: Optional[str] = None, - response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, + response_model_include: Optional[IncEx] = None, + response_model_exclude: Optional[IncEx] = None, response_model_by_alias: bool = True, response_model_exclude_unset: bool = False, response_model_exclude_defaults: bool = False, @@ -363,8 +362,8 @@ class FastAPI(Starlette): deprecated: Optional[bool] = None, methods: Optional[List[str]] = None, operation_id: Optional[str] = None, - response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, + response_model_include: Optional[IncEx] = None, + response_model_exclude: Optional[IncEx] = None, response_model_by_alias: bool = True, response_model_exclude_unset: bool = False, response_model_exclude_defaults: bool = False, @@ -484,8 +483,8 @@ class FastAPI(Starlette): responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, deprecated: Optional[bool] = None, operation_id: Optional[str] = None, - response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, + response_model_include: Optional[IncEx] = None, + response_model_exclude: Optional[IncEx] = None, response_model_by_alias: bool = True, response_model_exclude_unset: bool = False, response_model_exclude_defaults: bool = False, @@ -539,8 +538,8 @@ class FastAPI(Starlette): responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, deprecated: Optional[bool] = None, operation_id: Optional[str] = None, - response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, + response_model_include: Optional[IncEx] = None, + response_model_exclude: Optional[IncEx] = None, response_model_by_alias: bool = True, response_model_exclude_unset: bool = False, response_model_exclude_defaults: bool = False, @@ -594,8 +593,8 @@ class FastAPI(Starlette): responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, deprecated: Optional[bool] = None, operation_id: Optional[str] = None, - response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, + response_model_include: Optional[IncEx] = None, + response_model_exclude: Optional[IncEx] = None, response_model_by_alias: bool = True, response_model_exclude_unset: bool = False, response_model_exclude_defaults: bool = False, @@ -649,8 +648,8 @@ class FastAPI(Starlette): responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, deprecated: Optional[bool] = None, operation_id: Optional[str] = None, - response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, + response_model_include: Optional[IncEx] = None, + response_model_exclude: Optional[IncEx] = None, response_model_by_alias: bool = True, response_model_exclude_unset: bool = False, response_model_exclude_defaults: bool = False, @@ -704,8 +703,8 @@ class FastAPI(Starlette): responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, deprecated: Optional[bool] = None, operation_id: Optional[str] = None, - response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, + response_model_include: Optional[IncEx] = None, + response_model_exclude: Optional[IncEx] = None, response_model_by_alias: bool = True, response_model_exclude_unset: bool = False, response_model_exclude_defaults: bool = False, @@ -759,8 +758,8 @@ class FastAPI(Starlette): responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, deprecated: Optional[bool] = None, operation_id: Optional[str] = None, - response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, + response_model_include: Optional[IncEx] = None, + response_model_exclude: Optional[IncEx] = None, response_model_by_alias: bool = True, response_model_exclude_unset: bool = False, response_model_exclude_defaults: bool = False, @@ -814,8 +813,8 @@ class FastAPI(Starlette): responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, deprecated: Optional[bool] = None, operation_id: Optional[str] = None, - response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, + response_model_include: Optional[IncEx] = None, + response_model_exclude: Optional[IncEx] = None, response_model_by_alias: bool = True, response_model_exclude_unset: bool = False, response_model_exclude_defaults: bool = False, @@ -869,8 +868,8 @@ class FastAPI(Starlette): responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, deprecated: Optional[bool] = None, operation_id: Optional[str] = None, - response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, + response_model_include: Optional[IncEx] = None, + response_model_exclude: Optional[IncEx] = None, response_model_by_alias: bool = True, response_model_exclude_unset: bool = False, response_model_exclude_defaults: bool = False, diff --git a/fastapi/datastructures.py b/fastapi/datastructures.py index b20a25ab6..3c96c56c7 100644 --- a/fastapi/datastructures.py +++ b/fastapi/datastructures.py @@ -1,5 +1,12 @@ -from typing import Any, Callable, Dict, Iterable, Type, TypeVar - +from typing import Any, Callable, Dict, Iterable, Type, TypeVar, cast + +from fastapi._compat import ( + PYDANTIC_V2, + CoreSchema, + GetJsonSchemaHandler, + JsonSchemaValue, + general_plain_validator_function, +) from starlette.datastructures import URL as URL # noqa: F401 from starlette.datastructures import Address as Address # noqa: F401 from starlette.datastructures import FormData as FormData # noqa: F401 @@ -21,8 +28,28 @@ class UploadFile(StarletteUploadFile): return v @classmethod - def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: - field_schema.update({"type": "string", "format": "binary"}) + def _validate(cls, __input_value: Any, _: Any) -> "UploadFile": + if not isinstance(__input_value, StarletteUploadFile): + raise ValueError(f"Expected UploadFile, received: {type(__input_value)}") + return cast(UploadFile, __input_value) + + if not PYDANTIC_V2: + + @classmethod + def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: + field_schema.update({"type": "string", "format": "binary"}) + + @classmethod + def __get_pydantic_json_schema__( + cls, core_schema: CoreSchema, handler: GetJsonSchemaHandler + ) -> JsonSchemaValue: + return {"type": "string", "format": "binary"} + + @classmethod + def __get_pydantic_core_schema__( + cls, source: Type[Any], handler: Callable[[Any], CoreSchema] + ) -> CoreSchema: + return general_plain_validator_function(cls._validate) class DefaultPlaceholder: diff --git a/fastapi/dependencies/models.py b/fastapi/dependencies/models.py index 443590b9c..61ef00638 100644 --- a/fastapi/dependencies/models.py +++ b/fastapi/dependencies/models.py @@ -1,7 +1,7 @@ from typing import Any, Callable, List, Optional, Sequence +from fastapi._compat import ModelField from fastapi.security.base import SecurityBase -from pydantic.fields import ModelField class SecurityRequirement: diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index f131001ce..e2915268c 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -1,7 +1,6 @@ -import dataclasses import inspect from contextlib import contextmanager -from copy import copy, deepcopy +from copy import deepcopy from typing import ( Any, Callable, @@ -20,6 +19,31 @@ from typing import ( import anyio from fastapi import params +from fastapi._compat import ( + PYDANTIC_V2, + ErrorWrapper, + ModelField, + Required, + Undefined, + _regenerate_error_with_loc, + copy_field_info, + create_body_model, + evaluate_forwardref, + field_annotation_is_scalar, + get_annotation_from_field_info, + get_missing_field_error, + is_bytes_field, + is_bytes_sequence_field, + is_scalar_field, + is_scalar_sequence_field, + is_sequence_field, + is_uploadfile_or_nonable_uploadfile_annotation, + is_uploadfile_sequence_annotation, + lenient_issubclass, + sequence_types, + serialize_sequence_value, + value_is_sequence, +) from fastapi.concurrency import ( AsyncExitStack, asynccontextmanager, @@ -31,50 +55,14 @@ from fastapi.security.base import SecurityBase from fastapi.security.oauth2 import OAuth2, SecurityScopes from fastapi.security.open_id_connect_url import OpenIdConnect from fastapi.utils import create_response_field, get_path_param_names -from pydantic import BaseModel, create_model -from pydantic.error_wrappers import ErrorWrapper -from pydantic.errors import MissingError -from pydantic.fields import ( - SHAPE_FROZENSET, - SHAPE_LIST, - SHAPE_SEQUENCE, - SHAPE_SET, - SHAPE_SINGLETON, - SHAPE_TUPLE, - SHAPE_TUPLE_ELLIPSIS, - FieldInfo, - ModelField, - Required, - Undefined, -) -from pydantic.schema import get_annotation_from_field_info -from pydantic.typing import evaluate_forwardref, get_args, get_origin -from pydantic.utils import lenient_issubclass +from pydantic.fields import FieldInfo from starlette.background import BackgroundTasks from starlette.concurrency import run_in_threadpool from starlette.datastructures import FormData, Headers, QueryParams, UploadFile from starlette.requests import HTTPConnection, Request from starlette.responses import Response from starlette.websockets import WebSocket -from typing_extensions import Annotated - -sequence_shapes = { - SHAPE_LIST, - SHAPE_SET, - SHAPE_FROZENSET, - SHAPE_TUPLE, - SHAPE_SEQUENCE, - SHAPE_TUPLE_ELLIPSIS, -} -sequence_types = (list, set, tuple) -sequence_shape_to_type = { - SHAPE_LIST: list, - SHAPE_SET: set, - SHAPE_TUPLE: tuple, - SHAPE_SEQUENCE: list, - SHAPE_TUPLE_ELLIPSIS: list, -} - +from typing_extensions import Annotated, get_args, get_origin multipart_not_installed_error = ( 'Form data requires "python-multipart" to be installed. \n' @@ -216,36 +204,6 @@ def get_flat_params(dependant: Dependant) -> List[ModelField]: ) -def is_scalar_field(field: ModelField) -> bool: - field_info = field.field_info - if not ( - field.shape == SHAPE_SINGLETON - and not lenient_issubclass(field.type_, BaseModel) - and not lenient_issubclass(field.type_, sequence_types + (dict,)) - and not dataclasses.is_dataclass(field.type_) - and not isinstance(field_info, params.Body) - ): - return False - if field.sub_fields: - if not all(is_scalar_field(f) for f in field.sub_fields): - return False - return True - - -def is_scalar_sequence_field(field: ModelField) -> bool: - if (field.shape in sequence_shapes) and not lenient_issubclass( - field.type_, BaseModel - ): - if field.sub_fields is not None: - for sub_field in field.sub_fields: - if not is_scalar_field(sub_field): - return False - return True - if lenient_issubclass(field.type_, sequence_types): - return True - return False - - def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature: signature = inspect.signature(call) globalns = getattr(call, "__globals__", {}) @@ -364,12 +322,11 @@ def analyze_param( is_path_param: bool, ) -> Tuple[Any, Optional[params.Depends], Optional[ModelField]]: field_info = None - used_default_field_info = False depends = None type_annotation: Any = Any if ( annotation is not inspect.Signature.empty - and get_origin(annotation) is Annotated # type: ignore[comparison-overlap] + and get_origin(annotation) is Annotated ): annotated_args = get_args(annotation) type_annotation = annotated_args[0] @@ -384,7 +341,9 @@ def analyze_param( fastapi_annotation = next(iter(fastapi_annotations), None) if isinstance(fastapi_annotation, FieldInfo): # Copy `field_info` because we mutate `field_info.default` below. - field_info = copy(fastapi_annotation) + field_info = copy_field_info( + field_info=fastapi_annotation, annotation=annotation + ) assert field_info.default is Undefined or field_info.default is Required, ( f"`{field_info.__class__.__name__}` default value cannot be set in" f" `Annotated` for {param_name!r}. Set the default value with `=` instead." @@ -415,6 +374,8 @@ def analyze_param( f" together for {param_name!r}" ) field_info = value + if PYDANTIC_V2: + field_info.annotation = type_annotation if depends is not None and depends.dependency is None: depends.dependency = type_annotation @@ -433,10 +394,15 @@ def analyze_param( # We might check here that `default_value is Required`, but the fact is that the same # parameter might sometimes be a path parameter and sometimes not. See # `tests/test_infer_param_optionality.py` for an example. - field_info = params.Path() + field_info = params.Path(annotation=type_annotation) + elif is_uploadfile_or_nonable_uploadfile_annotation( + type_annotation + ) or is_uploadfile_sequence_annotation(type_annotation): + field_info = params.File(annotation=type_annotation, default=default_value) + elif not field_annotation_is_scalar(annotation=type_annotation): + field_info = params.Body(annotation=type_annotation, default=default_value) else: - field_info = params.Query(default=default_value) - used_default_field_info = True + field_info = params.Query(annotation=type_annotation, default=default_value) field = None if field_info is not None: @@ -450,8 +416,8 @@ def analyze_param( and getattr(field_info, "in_", None) is None ): field_info.in_ = params.ParamTypes.query - annotation = get_annotation_from_field_info( - annotation if annotation is not inspect.Signature.empty else Any, + use_annotation = get_annotation_from_field_info( + type_annotation, field_info, param_name, ) @@ -459,19 +425,15 @@ def analyze_param( alias = param_name.replace("_", "-") else: alias = field_info.alias or param_name + field_info.alias = alias field = create_response_field( name=param_name, - type_=annotation, + type_=use_annotation, default=field_info.default, alias=alias, required=field_info.default in (Required, Undefined), field_info=field_info, ) - if used_default_field_info: - if lenient_issubclass(field.type_, UploadFile): - field.field_info = params.File(field_info.default) - elif not is_scalar_field(field=field): - field.field_info = params.Body(field_info.default) return type_annotation, depends, field @@ -554,13 +516,13 @@ async def solve_dependencies( dependency_cache: Optional[Dict[Tuple[Callable[..., Any], Tuple[str]], Any]] = None, ) -> Tuple[ Dict[str, Any], - List[ErrorWrapper], + List[Any], Optional[BackgroundTasks], Response, Dict[Tuple[Callable[..., Any], Tuple[str]], Any], ]: values: Dict[str, Any] = {} - errors: List[ErrorWrapper] = [] + errors: List[Any] = [] if response is None: response = Response() del response.headers["content-length"] @@ -674,7 +636,7 @@ async def solve_dependencies( def request_params_to_args( required_params: Sequence[ModelField], received_params: Union[Mapping[str, Any], QueryParams, Headers], -) -> Tuple[Dict[str, Any], List[ErrorWrapper]]: +) -> Tuple[Dict[str, Any], List[Any]]: values = {} errors = [] for field in required_params: @@ -688,23 +650,19 @@ def request_params_to_args( assert isinstance( field_info, params.Param ), "Params must be subclasses of Param" + loc = (field_info.in_.value, field.alias) if value is None: if field.required: - errors.append( - ErrorWrapper( - MissingError(), loc=(field_info.in_.value, field.alias) - ) - ) + errors.append(get_missing_field_error(loc=loc)) else: values[field.name] = deepcopy(field.default) continue - v_, errors_ = field.validate( - value, values, loc=(field_info.in_.value, field.alias) - ) + v_, errors_ = field.validate(value, values, loc=loc) if isinstance(errors_, ErrorWrapper): errors.append(errors_) elif isinstance(errors_, list): - errors.extend(errors_) + new_errors = _regenerate_error_with_loc(errors=errors_, loc_prefix=()) + errors.extend(new_errors) else: values[field.name] = v_ return values, errors @@ -713,9 +671,9 @@ def request_params_to_args( async def request_body_to_args( required_params: List[ModelField], received_body: Optional[Union[Dict[str, Any], FormData]], -) -> Tuple[Dict[str, Any], List[ErrorWrapper]]: +) -> Tuple[Dict[str, Any], List[Dict[str, Any]]]: values = {} - errors = [] + errors: List[Dict[str, Any]] = [] if required_params: field = required_params[0] field_info = field.field_info @@ -733,9 +691,7 @@ async def request_body_to_args( value: Optional[Any] = None if received_body is not None: - if ( - field.shape in sequence_shapes or field.type_ in sequence_types - ) and isinstance(received_body, FormData): + if (is_sequence_field(field)) and isinstance(received_body, FormData): value = received_body.getlist(field.alias) else: try: @@ -748,7 +704,7 @@ async def request_body_to_args( or (isinstance(field_info, params.Form) and value == "") or ( isinstance(field_info, params.Form) - and field.shape in sequence_shapes + and is_sequence_field(field) and len(value) == 0 ) ): @@ -759,16 +715,17 @@ async def request_body_to_args( continue if ( isinstance(field_info, params.File) - and lenient_issubclass(field.type_, bytes) + and is_bytes_field(field) and isinstance(value, UploadFile) ): value = await value.read() elif ( - field.shape in sequence_shapes + is_bytes_sequence_field(field) and isinstance(field_info, params.File) - and lenient_issubclass(field.type_, bytes) - and isinstance(value, sequence_types) + and value_is_sequence(value) ): + # For types + assert isinstance(value, sequence_types) # type: ignore[arg-type] results: List[Union[bytes, str]] = [] async def process_fn( @@ -780,24 +737,19 @@ async def request_body_to_args( async with anyio.create_task_group() as tg: for sub_value in value: tg.start_soon(process_fn, sub_value.read) - value = sequence_shape_to_type[field.shape](results) + value = serialize_sequence_value(field=field, value=results) v_, errors_ = field.validate(value, values, loc=loc) - if isinstance(errors_, ErrorWrapper): - errors.append(errors_) - elif isinstance(errors_, list): + if isinstance(errors_, list): errors.extend(errors_) + elif errors_: + errors.append(errors_) else: values[field.name] = v_ return values, errors -def get_missing_field_error(loc: Tuple[str, ...]) -> ErrorWrapper: - missing_field_error = ErrorWrapper(MissingError(), loc=loc) - return missing_field_error - - def get_body_field(*, dependant: Dependant, name: str) -> Optional[ModelField]: flat_dependant = get_flat_dependant(dependant) if not flat_dependant.body_params: @@ -815,12 +767,16 @@ def get_body_field(*, dependant: Dependant, name: str) -> Optional[ModelField]: for param in flat_dependant.body_params: setattr(param.field_info, "embed", True) # noqa: B010 model_name = "Body_" + name - BodyModel: Type[BaseModel] = create_model(model_name) - for f in flat_dependant.body_params: - BodyModel.__fields__[f.name] = f + BodyModel = create_body_model( + fields=flat_dependant.body_params, model_name=model_name + ) required = any(True for f in flat_dependant.body_params if f.required) - - BodyFieldInfo_kwargs: Dict[str, Any] = {"default": None} + BodyFieldInfo_kwargs: Dict[str, Any] = { + "annotation": BodyModel, + "alias": "body", + } + if not required: + BodyFieldInfo_kwargs["default"] = None if any(isinstance(f.field_info, params.File) for f in flat_dependant.body_params): BodyFieldInfo: Type[params.Body] = params.File elif any(isinstance(f.field_info, params.Form) for f in flat_dependant.body_params): diff --git a/fastapi/encoders.py b/fastapi/encoders.py index 94f41bfa1..b542749f2 100644 --- a/fastapi/encoders.py +++ b/fastapi/encoders.py @@ -1,15 +1,87 @@ import dataclasses +import datetime from collections import defaultdict, deque +from decimal import Decimal from enum import Enum -from pathlib import PurePath +from ipaddress import ( + IPv4Address, + IPv4Interface, + IPv4Network, + IPv6Address, + IPv6Interface, + IPv6Network, +) +from pathlib import Path, PurePath +from re import Pattern from types import GeneratorType -from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union +from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union +from uuid import UUID +from fastapi.types import IncEx from pydantic import BaseModel -from pydantic.json import ENCODERS_BY_TYPE +from pydantic.color import Color +from pydantic.networks import NameEmail +from pydantic.types import SecretBytes, SecretStr -SetIntStr = Set[Union[int, str]] -DictIntStrAny = Dict[Union[int, str], Any] +from ._compat import PYDANTIC_V2, MultiHostUrl, Url, _model_dump + + +# Taken from Pydantic v1 as is +def isoformat(o: Union[datetime.date, datetime.time]) -> str: + return o.isoformat() + + +# Taken from Pydantic v1 as is +# TODO: pv2 should this return strings instead? +def decimal_encoder(dec_value: Decimal) -> Union[int, float]: + """ + Encodes a Decimal as int of there's no exponent, otherwise float + + This is useful when we use ConstrainedDecimal to represent Numeric(x,0) + where a integer (but not int typed) is used. Encoding this as a float + results in failed round-tripping between encode and parse. + Our Id type is a prime example of this. + + >>> decimal_encoder(Decimal("1.0")) + 1.0 + + >>> decimal_encoder(Decimal("1")) + 1 + """ + if dec_value.as_tuple().exponent >= 0: # type: ignore[operator] + return int(dec_value) + else: + return float(dec_value) + + +ENCODERS_BY_TYPE: Dict[Type[Any], Callable[[Any], Any]] = { + bytes: lambda o: o.decode(), + Color: str, + datetime.date: isoformat, + datetime.datetime: isoformat, + datetime.time: isoformat, + datetime.timedelta: lambda td: td.total_seconds(), + Decimal: decimal_encoder, + Enum: lambda o: o.value, + frozenset: list, + deque: list, + GeneratorType: list, + IPv4Address: str, + IPv4Interface: str, + IPv4Network: str, + IPv6Address: str, + IPv6Interface: str, + IPv6Network: str, + NameEmail: str, + Path: str, + Pattern: lambda o: o.pattern, + SecretBytes: str, + SecretStr: str, + set: list, + UUID: str, + Url: str, + MultiHostUrl: str, +} def generate_encoders_by_class_tuples( @@ -28,8 +100,8 @@ encoders_by_class_tuples = generate_encoders_by_class_tuples(ENCODERS_BY_TYPE) def jsonable_encoder( obj: Any, - include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, + include: Optional[IncEx] = None, + exclude: Optional[IncEx] = None, by_alias: bool = True, exclude_unset: bool = False, exclude_defaults: bool = False, @@ -50,10 +122,15 @@ def jsonable_encoder( if exclude is not None and not isinstance(exclude, (set, dict)): exclude = set(exclude) if isinstance(obj, BaseModel): - encoder = getattr(obj.__config__, "json_encoders", {}) - if custom_encoder: - encoder.update(custom_encoder) - obj_dict = obj.dict( + # TODO: remove when deprecating Pydantic v1 + encoders: Dict[Any, Any] = {} + if not PYDANTIC_V2: + encoders = getattr(obj.__config__, "json_encoders", {}) # type: ignore[attr-defined] + if custom_encoder: + encoders.update(custom_encoder) + obj_dict = _model_dump( + obj, + mode="json", include=include, exclude=exclude, by_alias=by_alias, @@ -67,7 +144,8 @@ def jsonable_encoder( obj_dict, exclude_none=exclude_none, exclude_defaults=exclude_defaults, - custom_encoder=encoder, + # TODO: remove when deprecating Pydantic v1 + custom_encoder=encoders, sqlalchemy_safe=sqlalchemy_safe, ) if dataclasses.is_dataclass(obj): diff --git a/fastapi/exceptions.py b/fastapi/exceptions.py index cac5330a2..c1692f396 100644 --- a/fastapi/exceptions.py +++ b/fastapi/exceptions.py @@ -1,7 +1,6 @@ from typing import Any, Dict, Optional, Sequence, Type -from pydantic import BaseModel, ValidationError, create_model -from pydantic.error_wrappers import ErrorList +from pydantic import BaseModel, create_model from starlette.exceptions import HTTPException as StarletteHTTPException from starlette.exceptions import WebSocketException as WebSocketException # noqa: F401 @@ -26,12 +25,25 @@ class FastAPIError(RuntimeError): """ -class RequestValidationError(ValidationError): - def __init__(self, errors: Sequence[ErrorList], *, body: Any = None) -> None: +class ValidationException(Exception): + def __init__(self, errors: Sequence[Any]) -> None: + self._errors = errors + + def errors(self) -> Sequence[Any]: + return self._errors + + +class RequestValidationError(ValidationException): + def __init__(self, errors: Sequence[Any], *, body: Any = None) -> None: + super().__init__(errors) self.body = body - super().__init__(errors, RequestErrorModel) -class WebSocketRequestValidationError(ValidationError): - def __init__(self, errors: Sequence[ErrorList]) -> None: - super().__init__(errors, WebSocketErrorModel) +class WebSocketRequestValidationError(ValidationException): + pass + + +class ResponseValidationError(ValidationException): + def __init__(self, errors: Sequence[Any], *, body: Any = None) -> None: + super().__init__(errors) + self.body = body diff --git a/fastapi/openapi/constants.py b/fastapi/openapi/constants.py index 1897ad750..d724ee3cf 100644 --- a/fastapi/openapi/constants.py +++ b/fastapi/openapi/constants.py @@ -1,2 +1,3 @@ METHODS_WITH_BODY = {"GET", "HEAD", "POST", "PUT", "DELETE", "PATCH"} REF_PREFIX = "#/components/schemas/" +REF_TEMPLATE = "#/components/schemas/{model}" diff --git a/fastapi/openapi/models.py b/fastapi/openapi/models.py index a2ea53607..2268dd229 100644 --- a/fastapi/openapi/models.py +++ b/fastapi/openapi/models.py @@ -1,13 +1,21 @@ from enum import Enum -from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Union - +from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Type, Union + +from fastapi._compat import ( + PYDANTIC_V2, + CoreSchema, + GetJsonSchemaHandler, + JsonSchemaValue, + _model_rebuild, + general_plain_validator_function, +) from fastapi.logger import logger from pydantic import AnyUrl, BaseModel, Field from typing_extensions import Annotated, Literal from typing_extensions import deprecated as typing_deprecated try: - import email_validator # type: ignore + import email_validator assert email_validator # make autoflake ignore the unused import from pydantic import EmailStr @@ -26,14 +34,39 @@ except ImportError: # pragma: no cover ) return str(v) + @classmethod + def _validate(cls, __input_value: Any, _: Any) -> str: + logger.warning( + "email-validator not installed, email fields will be treated as str.\n" + "To install, run: pip install email-validator" + ) + return str(__input_value) + + @classmethod + def __get_pydantic_json_schema__( + cls, core_schema: CoreSchema, handler: GetJsonSchemaHandler + ) -> JsonSchemaValue: + return {"type": "string", "format": "email"} + + @classmethod + def __get_pydantic_core_schema__( + cls, source: Type[Any], handler: Callable[[Any], CoreSchema] + ) -> CoreSchema: + return general_plain_validator_function(cls._validate) + class Contact(BaseModel): name: Optional[str] = None url: Optional[AnyUrl] = None email: Optional[EmailStr] = None - class Config: - extra = "allow" + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" class License(BaseModel): @@ -41,8 +74,13 @@ class License(BaseModel): identifier: Optional[str] = None url: Optional[AnyUrl] = None - class Config: - extra = "allow" + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" class Info(BaseModel): @@ -54,17 +92,27 @@ class Info(BaseModel): license: Optional[License] = None version: str - class Config: - extra = "allow" + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" class ServerVariable(BaseModel): - enum: Annotated[Optional[List[str]], Field(min_items=1)] = None + enum: Annotated[Optional[List[str]], Field(min_length=1)] = None default: str description: Optional[str] = None - class Config: - extra = "allow" + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" class Server(BaseModel): @@ -72,8 +120,13 @@ class Server(BaseModel): description: Optional[str] = None variables: Optional[Dict[str, ServerVariable]] = None - class Config: - extra = "allow" + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" class Reference(BaseModel): @@ -92,16 +145,26 @@ class XML(BaseModel): attribute: Optional[bool] = None wrapped: Optional[bool] = None - class Config: - extra = "allow" + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" class ExternalDocumentation(BaseModel): description: Optional[str] = None url: AnyUrl - class Config: - extra = "allow" + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" class Schema(BaseModel): @@ -190,8 +253,13 @@ class Schema(BaseModel): ), ] = None - class Config: - extra: str = "allow" + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" # Ref: https://json-schema.org/draft/2020-12/json-schema-core.html#name-json-schema-documents @@ -205,8 +273,13 @@ class Example(BaseModel): value: Optional[Any] = None externalValue: Optional[AnyUrl] = None - class Config: - extra = "allow" + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" class ParameterInType(Enum): @@ -223,8 +296,13 @@ class Encoding(BaseModel): explode: Optional[bool] = None allowReserved: Optional[bool] = None - class Config: - extra = "allow" + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" class MediaType(BaseModel): @@ -233,8 +311,13 @@ class MediaType(BaseModel): examples: Optional[Dict[str, Union[Example, Reference]]] = None encoding: Optional[Dict[str, Encoding]] = None - class Config: - extra = "allow" + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" class ParameterBase(BaseModel): @@ -251,8 +334,13 @@ class ParameterBase(BaseModel): # Serialization rules for more complex scenarios content: Optional[Dict[str, MediaType]] = None - class Config: - extra = "allow" + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" class Parameter(ParameterBase): @@ -269,8 +357,13 @@ class RequestBody(BaseModel): content: Dict[str, MediaType] required: Optional[bool] = None - class Config: - extra = "allow" + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" class Link(BaseModel): @@ -281,8 +374,13 @@ class Link(BaseModel): description: Optional[str] = None server: Optional[Server] = None - class Config: - extra = "allow" + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" class Response(BaseModel): @@ -291,8 +389,13 @@ class Response(BaseModel): content: Optional[Dict[str, MediaType]] = None links: Optional[Dict[str, Union[Link, Reference]]] = None - class Config: - extra = "allow" + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" class Operation(BaseModel): @@ -310,8 +413,13 @@ class Operation(BaseModel): security: Optional[List[Dict[str, List[str]]]] = None servers: Optional[List[Server]] = None - class Config: - extra = "allow" + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" class PathItem(BaseModel): @@ -329,8 +437,13 @@ class PathItem(BaseModel): servers: Optional[List[Server]] = None parameters: Optional[List[Union[Parameter, Reference]]] = None - class Config: - extra = "allow" + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" class SecuritySchemeType(Enum): @@ -344,8 +457,13 @@ class SecurityBase(BaseModel): type_: SecuritySchemeType = Field(alias="type") description: Optional[str] = None - class Config: - extra = "allow" + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" class APIKeyIn(Enum): @@ -374,8 +492,13 @@ class OAuthFlow(BaseModel): refreshUrl: Optional[str] = None scopes: Dict[str, str] = {} - class Config: - extra = "allow" + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" class OAuthFlowImplicit(OAuthFlow): @@ -401,8 +524,13 @@ class OAuthFlows(BaseModel): clientCredentials: Optional[OAuthFlowClientCredentials] = None authorizationCode: Optional[OAuthFlowAuthorizationCode] = None - class Config: - extra = "allow" + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" class OAuth2(SecurityBase): @@ -433,8 +561,13 @@ class Components(BaseModel): callbacks: Optional[Dict[str, Union[Dict[str, PathItem], Reference, Any]]] = None pathItems: Optional[Dict[str, Union[PathItem, Reference]]] = None - class Config: - extra = "allow" + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" class Tag(BaseModel): @@ -442,8 +575,13 @@ class Tag(BaseModel): description: Optional[str] = None externalDocs: Optional[ExternalDocumentation] = None - class Config: - extra = "allow" + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" class OpenAPI(BaseModel): @@ -459,10 +597,15 @@ class OpenAPI(BaseModel): tags: Optional[List[Tag]] = None externalDocs: Optional[ExternalDocumentation] = None - class Config: - extra = "allow" + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" -Schema.update_forward_refs() -Operation.update_forward_refs() -Encoding.update_forward_refs() +_model_rebuild(Schema) +_model_rebuild(Operation) +_model_rebuild(Encoding) diff --git a/fastapi/openapi/utils.py b/fastapi/openapi/utils.py index 609fe4389..e295361e6 100644 --- a/fastapi/openapi/utils.py +++ b/fastapi/openapi/utils.py @@ -1,35 +1,37 @@ import http.client import inspect import warnings -from enum import Enum from typing import Any, Dict, List, Optional, Sequence, Set, Tuple, Type, Union, cast from fastapi import routing +from fastapi._compat import ( + GenerateJsonSchema, + JsonSchemaValue, + ModelField, + Undefined, + get_compat_model_name_map, + get_definitions, + get_schema_from_model_field, + lenient_issubclass, +) from fastapi.datastructures import DefaultPlaceholder from fastapi.dependencies.models import Dependant from fastapi.dependencies.utils import get_flat_dependant, get_flat_params from fastapi.encoders import jsonable_encoder -from fastapi.openapi.constants import METHODS_WITH_BODY, REF_PREFIX +from fastapi.openapi.constants import METHODS_WITH_BODY, REF_PREFIX, REF_TEMPLATE from fastapi.openapi.models import OpenAPI from fastapi.params import Body, Param from fastapi.responses import Response +from fastapi.types import ModelNameMap from fastapi.utils import ( deep_dict_update, generate_operation_id_for_path, - get_model_definitions, is_body_allowed_for_status_code, ) -from pydantic import BaseModel -from pydantic.fields import ModelField, Undefined -from pydantic.schema import ( - field_schema, - get_flat_models_from_fields, - get_model_name_map, -) -from pydantic.utils import lenient_issubclass from starlette.responses import JSONResponse from starlette.routing import BaseRoute from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY +from typing_extensions import Literal validation_error_definition = { "title": "ValidationError", @@ -88,7 +90,11 @@ def get_openapi_security_definitions( def get_openapi_operation_parameters( *, all_route_params: Sequence[ModelField], - model_name_map: Dict[Union[Type[BaseModel], Type[Enum]], str], + schema_generator: GenerateJsonSchema, + model_name_map: ModelNameMap, + field_mapping: Dict[ + Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue + ], ) -> List[Dict[str, Any]]: parameters = [] for param in all_route_params: @@ -96,13 +102,17 @@ def get_openapi_operation_parameters( field_info = cast(Param, field_info) if not field_info.include_in_schema: continue + param_schema = get_schema_from_model_field( + field=param, + schema_generator=schema_generator, + model_name_map=model_name_map, + field_mapping=field_mapping, + ) parameter = { "name": param.alias, "in": field_info.in_.value, "required": param.required, - "schema": field_schema( - param, model_name_map=model_name_map, ref_prefix=REF_PREFIX - )[0], + "schema": param_schema, } if field_info.description: parameter["description"] = field_info.description @@ -117,13 +127,20 @@ def get_openapi_operation_parameters( def get_openapi_operation_request_body( *, body_field: Optional[ModelField], - model_name_map: Dict[Union[Type[BaseModel], Type[Enum]], str], + schema_generator: GenerateJsonSchema, + model_name_map: ModelNameMap, + field_mapping: Dict[ + Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue + ], ) -> Optional[Dict[str, Any]]: if not body_field: return None assert isinstance(body_field, ModelField) - body_schema, _, _ = field_schema( - body_field, model_name_map=model_name_map, ref_prefix=REF_PREFIX + body_schema = get_schema_from_model_field( + field=body_field, + schema_generator=schema_generator, + model_name_map=model_name_map, + field_mapping=field_mapping, ) field_info = cast(Body, body_field.field_info) request_media_type = field_info.media_type @@ -186,7 +203,14 @@ def get_openapi_operation_metadata( def get_openapi_path( - *, route: routing.APIRoute, model_name_map: Dict[type, str], operation_ids: Set[str] + *, + route: routing.APIRoute, + operation_ids: Set[str], + schema_generator: GenerateJsonSchema, + model_name_map: ModelNameMap, + field_mapping: Dict[ + Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue + ], ) -> Tuple[Dict[str, Any], Dict[str, Any], Dict[str, Any]]: path = {} security_schemes: Dict[str, Any] = {} @@ -214,7 +238,10 @@ def get_openapi_path( security_schemes.update(security_definitions) all_route_params = get_flat_params(route.dependant) operation_parameters = get_openapi_operation_parameters( - all_route_params=all_route_params, model_name_map=model_name_map + all_route_params=all_route_params, + schema_generator=schema_generator, + model_name_map=model_name_map, + field_mapping=field_mapping, ) parameters.extend(operation_parameters) if parameters: @@ -232,7 +259,10 @@ def get_openapi_path( operation["parameters"] = list(all_parameters.values()) if method in METHODS_WITH_BODY: request_body_oai = get_openapi_operation_request_body( - body_field=route.body_field, model_name_map=model_name_map + body_field=route.body_field, + schema_generator=schema_generator, + model_name_map=model_name_map, + field_mapping=field_mapping, ) if request_body_oai: operation["requestBody"] = request_body_oai @@ -246,8 +276,10 @@ def get_openapi_path( cb_definitions, ) = get_openapi_path( route=callback, - model_name_map=model_name_map, operation_ids=operation_ids, + schema_generator=schema_generator, + model_name_map=model_name_map, + field_mapping=field_mapping, ) callbacks[callback.name] = {callback.path: cb_path} operation["callbacks"] = callbacks @@ -273,10 +305,11 @@ def get_openapi_path( response_schema = {"type": "string"} if lenient_issubclass(current_response_class, JSONResponse): if route.response_field: - response_schema, _, _ = field_schema( - route.response_field, + response_schema = get_schema_from_model_field( + field=route.response_field, + schema_generator=schema_generator, model_name_map=model_name_map, - ref_prefix=REF_PREFIX, + field_mapping=field_mapping, ) else: response_schema = {} @@ -305,8 +338,11 @@ def get_openapi_path( field = route.response_fields.get(additional_status_code) additional_field_schema: Optional[Dict[str, Any]] = None if field: - additional_field_schema, _, _ = field_schema( - field, model_name_map=model_name_map, ref_prefix=REF_PREFIX + additional_field_schema = get_schema_from_model_field( + field=field, + schema_generator=schema_generator, + model_name_map=model_name_map, + field_mapping=field_mapping, ) media_type = route_response_media_type or "application/json" additional_schema = ( @@ -352,13 +388,13 @@ def get_openapi_path( return path, security_schemes, definitions -def get_flat_models_from_routes( +def get_fields_from_routes( routes: Sequence[BaseRoute], -) -> Set[Union[Type[BaseModel], Type[Enum]]]: +) -> List[ModelField]: body_fields_from_routes: List[ModelField] = [] responses_from_routes: List[ModelField] = [] request_fields_from_routes: List[ModelField] = [] - callback_flat_models: Set[Union[Type[BaseModel], Type[Enum]]] = set() + callback_flat_models: List[ModelField] = [] for route in routes: if getattr(route, "include_in_schema", None) and isinstance( route, routing.APIRoute @@ -373,13 +409,12 @@ def get_flat_models_from_routes( if route.response_fields: responses_from_routes.extend(route.response_fields.values()) if route.callbacks: - callback_flat_models |= get_flat_models_from_routes(route.callbacks) + callback_flat_models.extend(get_fields_from_routes(route.callbacks)) params = get_flat_params(route.dependant) request_fields_from_routes.extend(params) - flat_models = callback_flat_models | get_flat_models_from_fields( - body_fields_from_routes + responses_from_routes + request_fields_from_routes, - known_models=set(), + flat_models = callback_flat_models + list( + body_fields_from_routes + responses_from_routes + request_fields_from_routes ) return flat_models @@ -417,15 +452,22 @@ def get_openapi( paths: Dict[str, Dict[str, Any]] = {} webhook_paths: Dict[str, Dict[str, Any]] = {} operation_ids: Set[str] = set() - flat_models = get_flat_models_from_routes(list(routes or []) + list(webhooks or [])) - model_name_map = get_model_name_map(flat_models) - definitions = get_model_definitions( - flat_models=flat_models, model_name_map=model_name_map + all_fields = get_fields_from_routes(list(routes or []) + list(webhooks or [])) + model_name_map = get_compat_model_name_map(all_fields) + schema_generator = GenerateJsonSchema(ref_template=REF_TEMPLATE) + field_mapping, definitions = get_definitions( + fields=all_fields, + schema_generator=schema_generator, + model_name_map=model_name_map, ) for route in routes or []: if isinstance(route, routing.APIRoute): result = get_openapi_path( - route=route, model_name_map=model_name_map, operation_ids=operation_ids + route=route, + operation_ids=operation_ids, + schema_generator=schema_generator, + model_name_map=model_name_map, + field_mapping=field_mapping, ) if result: path, security_schemes, path_definitions = result @@ -441,8 +483,10 @@ def get_openapi( if isinstance(webhook, routing.APIRoute): result = get_openapi_path( route=webhook, - model_name_map=model_name_map, operation_ids=operation_ids, + schema_generator=schema_generator, + model_name_map=model_name_map, + field_mapping=field_mapping, ) if result: path, security_schemes, path_definitions = result diff --git a/fastapi/param_functions.py b/fastapi/param_functions.py index 2f5818c85..a43afaf31 100644 --- a/fastapi/param_functions.py +++ b/fastapi/param_functions.py @@ -1,14 +1,22 @@ -from typing import Any, Callable, List, Optional, Sequence +from typing import Any, Callable, Dict, List, Optional, Sequence, Union from fastapi import params -from pydantic.fields import Undefined +from fastapi._compat import Undefined from typing_extensions import Annotated, deprecated +_Unset: Any = Undefined + def Path( # noqa: N802 default: Any = ..., *, + default_factory: Union[Callable[[], Any], None] = _Unset, alias: Optional[str] = None, + alias_priority: Union[int, None] = _Unset, + # TODO: update when deprecating Pydantic v1, import these types + # validation_alias: str | AliasPath | AliasChoices | None + validation_alias: Union[str, None] = None, + serialization_alias: Union[str, None] = None, title: Optional[str] = None, description: Optional[str] = None, gt: Optional[float] = None, @@ -17,7 +25,19 @@ def Path( # noqa: N802 le: Optional[float] = None, min_length: Optional[int] = None, max_length: Optional[int] = None, - regex: Optional[str] = None, + pattern: Optional[str] = None, + regex: Annotated[ + Optional[str], + deprecated( + "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." + ), + ] = None, + discriminator: Union[str, None] = None, + strict: Union[bool, None] = _Unset, + multiple_of: Union[float, None] = _Unset, + allow_inf_nan: Union[bool, None] = _Unset, + max_digits: Union[int, None] = _Unset, + decimal_places: Union[int, None] = _Unset, examples: Optional[List[Any]] = None, example: Annotated[ Optional[Any], @@ -25,14 +45,19 @@ def Path( # noqa: N802 "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " "although still supported. Use examples instead." ), - ] = Undefined, + ] = _Unset, deprecated: Optional[bool] = None, include_in_schema: bool = True, + json_schema_extra: Union[Dict[str, Any], None] = None, **extra: Any, ) -> Any: return params.Path( default=default, + default_factory=default_factory, alias=alias, + alias_priority=alias_priority, + validation_alias=validation_alias, + serialization_alias=serialization_alias, title=title, description=description, gt=gt, @@ -41,11 +66,19 @@ def Path( # noqa: N802 le=le, min_length=min_length, max_length=max_length, + pattern=pattern, regex=regex, + discriminator=discriminator, + strict=strict, + multiple_of=multiple_of, + allow_inf_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, example=example, examples=examples, deprecated=deprecated, include_in_schema=include_in_schema, + json_schema_extra=json_schema_extra, **extra, ) @@ -53,7 +86,13 @@ def Path( # noqa: N802 def Query( # noqa: N802 default: Any = Undefined, *, + default_factory: Union[Callable[[], Any], None] = _Unset, alias: Optional[str] = None, + alias_priority: Union[int, None] = _Unset, + # TODO: update when deprecating Pydantic v1, import these types + # validation_alias: str | AliasPath | AliasChoices | None + validation_alias: Union[str, None] = None, + serialization_alias: Union[str, None] = None, title: Optional[str] = None, description: Optional[str] = None, gt: Optional[float] = None, @@ -62,7 +101,19 @@ def Query( # noqa: N802 le: Optional[float] = None, min_length: Optional[int] = None, max_length: Optional[int] = None, - regex: Optional[str] = None, + pattern: Optional[str] = None, + regex: Annotated[ + Optional[str], + deprecated( + "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." + ), + ] = None, + discriminator: Union[str, None] = None, + strict: Union[bool, None] = _Unset, + multiple_of: Union[float, None] = _Unset, + allow_inf_nan: Union[bool, None] = _Unset, + max_digits: Union[int, None] = _Unset, + decimal_places: Union[int, None] = _Unset, examples: Optional[List[Any]] = None, example: Annotated[ Optional[Any], @@ -70,14 +121,19 @@ def Query( # noqa: N802 "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " "although still supported. Use examples instead." ), - ] = Undefined, + ] = _Unset, deprecated: Optional[bool] = None, include_in_schema: bool = True, + json_schema_extra: Union[Dict[str, Any], None] = None, **extra: Any, ) -> Any: return params.Query( default=default, + default_factory=default_factory, alias=alias, + alias_priority=alias_priority, + validation_alias=validation_alias, + serialization_alias=serialization_alias, title=title, description=description, gt=gt, @@ -86,11 +142,19 @@ def Query( # noqa: N802 le=le, min_length=min_length, max_length=max_length, + pattern=pattern, regex=regex, + discriminator=discriminator, + strict=strict, + multiple_of=multiple_of, + allow_inf_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, example=example, examples=examples, deprecated=deprecated, include_in_schema=include_in_schema, + json_schema_extra=json_schema_extra, **extra, ) @@ -98,7 +162,13 @@ def Query( # noqa: N802 def Header( # noqa: N802 default: Any = Undefined, *, + default_factory: Union[Callable[[], Any], None] = _Unset, alias: Optional[str] = None, + alias_priority: Union[int, None] = _Unset, + # TODO: update when deprecating Pydantic v1, import these types + # validation_alias: str | AliasPath | AliasChoices | None + validation_alias: Union[str, None] = None, + serialization_alias: Union[str, None] = None, convert_underscores: bool = True, title: Optional[str] = None, description: Optional[str] = None, @@ -108,7 +178,19 @@ def Header( # noqa: N802 le: Optional[float] = None, min_length: Optional[int] = None, max_length: Optional[int] = None, - regex: Optional[str] = None, + pattern: Optional[str] = None, + regex: Annotated[ + Optional[str], + deprecated( + "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." + ), + ] = None, + discriminator: Union[str, None] = None, + strict: Union[bool, None] = _Unset, + multiple_of: Union[float, None] = _Unset, + allow_inf_nan: Union[bool, None] = _Unset, + max_digits: Union[int, None] = _Unset, + decimal_places: Union[int, None] = _Unset, examples: Optional[List[Any]] = None, example: Annotated[ Optional[Any], @@ -116,14 +198,19 @@ def Header( # noqa: N802 "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " "although still supported. Use examples instead." ), - ] = Undefined, + ] = _Unset, deprecated: Optional[bool] = None, include_in_schema: bool = True, + json_schema_extra: Union[Dict[str, Any], None] = None, **extra: Any, ) -> Any: return params.Header( default=default, + default_factory=default_factory, alias=alias, + alias_priority=alias_priority, + validation_alias=validation_alias, + serialization_alias=serialization_alias, convert_underscores=convert_underscores, title=title, description=description, @@ -133,11 +220,19 @@ def Header( # noqa: N802 le=le, min_length=min_length, max_length=max_length, + pattern=pattern, regex=regex, + discriminator=discriminator, + strict=strict, + multiple_of=multiple_of, + allow_inf_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, example=example, examples=examples, deprecated=deprecated, include_in_schema=include_in_schema, + json_schema_extra=json_schema_extra, **extra, ) @@ -145,7 +240,13 @@ def Header( # noqa: N802 def Cookie( # noqa: N802 default: Any = Undefined, *, + default_factory: Union[Callable[[], Any], None] = _Unset, alias: Optional[str] = None, + alias_priority: Union[int, None] = _Unset, + # TODO: update when deprecating Pydantic v1, import these types + # validation_alias: str | AliasPath | AliasChoices | None + validation_alias: Union[str, None] = None, + serialization_alias: Union[str, None] = None, title: Optional[str] = None, description: Optional[str] = None, gt: Optional[float] = None, @@ -154,7 +255,19 @@ def Cookie( # noqa: N802 le: Optional[float] = None, min_length: Optional[int] = None, max_length: Optional[int] = None, - regex: Optional[str] = None, + pattern: Optional[str] = None, + regex: Annotated[ + Optional[str], + deprecated( + "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." + ), + ] = None, + discriminator: Union[str, None] = None, + strict: Union[bool, None] = _Unset, + multiple_of: Union[float, None] = _Unset, + allow_inf_nan: Union[bool, None] = _Unset, + max_digits: Union[int, None] = _Unset, + decimal_places: Union[int, None] = _Unset, examples: Optional[List[Any]] = None, example: Annotated[ Optional[Any], @@ -162,14 +275,19 @@ def Cookie( # noqa: N802 "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " "although still supported. Use examples instead." ), - ] = Undefined, + ] = _Unset, deprecated: Optional[bool] = None, include_in_schema: bool = True, + json_schema_extra: Union[Dict[str, Any], None] = None, **extra: Any, ) -> Any: return params.Cookie( default=default, + default_factory=default_factory, alias=alias, + alias_priority=alias_priority, + validation_alias=validation_alias, + serialization_alias=serialization_alias, title=title, description=description, gt=gt, @@ -178,11 +296,19 @@ def Cookie( # noqa: N802 le=le, min_length=min_length, max_length=max_length, + pattern=pattern, regex=regex, + discriminator=discriminator, + strict=strict, + multiple_of=multiple_of, + allow_inf_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, example=example, examples=examples, deprecated=deprecated, include_in_schema=include_in_schema, + json_schema_extra=json_schema_extra, **extra, ) @@ -190,9 +316,15 @@ def Cookie( # noqa: N802 def Body( # noqa: N802 default: Any = Undefined, *, + default_factory: Union[Callable[[], Any], None] = _Unset, embed: bool = False, media_type: str = "application/json", alias: Optional[str] = None, + alias_priority: Union[int, None] = _Unset, + # TODO: update when deprecating Pydantic v1, import these types + # validation_alias: str | AliasPath | AliasChoices | None + validation_alias: Union[str, None] = None, + serialization_alias: Union[str, None] = None, title: Optional[str] = None, description: Optional[str] = None, gt: Optional[float] = None, @@ -201,7 +333,19 @@ def Body( # noqa: N802 le: Optional[float] = None, min_length: Optional[int] = None, max_length: Optional[int] = None, - regex: Optional[str] = None, + pattern: Optional[str] = None, + regex: Annotated[ + Optional[str], + deprecated( + "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." + ), + ] = None, + discriminator: Union[str, None] = None, + strict: Union[bool, None] = _Unset, + multiple_of: Union[float, None] = _Unset, + allow_inf_nan: Union[bool, None] = _Unset, + max_digits: Union[int, None] = _Unset, + decimal_places: Union[int, None] = _Unset, examples: Optional[List[Any]] = None, example: Annotated[ Optional[Any], @@ -209,14 +353,21 @@ def Body( # noqa: N802 "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " "although still supported. Use examples instead." ), - ] = Undefined, + ] = _Unset, + deprecated: Optional[bool] = None, + include_in_schema: bool = True, + json_schema_extra: Union[Dict[str, Any], None] = None, **extra: Any, ) -> Any: return params.Body( default=default, + default_factory=default_factory, embed=embed, media_type=media_type, alias=alias, + alias_priority=alias_priority, + validation_alias=validation_alias, + serialization_alias=serialization_alias, title=title, description=description, gt=gt, @@ -225,9 +376,19 @@ def Body( # noqa: N802 le=le, min_length=min_length, max_length=max_length, + pattern=pattern, regex=regex, + discriminator=discriminator, + strict=strict, + multiple_of=multiple_of, + allow_inf_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, example=example, examples=examples, + deprecated=deprecated, + include_in_schema=include_in_schema, + json_schema_extra=json_schema_extra, **extra, ) @@ -235,8 +396,14 @@ def Body( # noqa: N802 def Form( # noqa: N802 default: Any = Undefined, *, + default_factory: Union[Callable[[], Any], None] = _Unset, media_type: str = "application/x-www-form-urlencoded", alias: Optional[str] = None, + alias_priority: Union[int, None] = _Unset, + # TODO: update when deprecating Pydantic v1, import these types + # validation_alias: str | AliasPath | AliasChoices | None + validation_alias: Union[str, None] = None, + serialization_alias: Union[str, None] = None, title: Optional[str] = None, description: Optional[str] = None, gt: Optional[float] = None, @@ -245,7 +412,19 @@ def Form( # noqa: N802 le: Optional[float] = None, min_length: Optional[int] = None, max_length: Optional[int] = None, - regex: Optional[str] = None, + pattern: Optional[str] = None, + regex: Annotated[ + Optional[str], + deprecated( + "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." + ), + ] = None, + discriminator: Union[str, None] = None, + strict: Union[bool, None] = _Unset, + multiple_of: Union[float, None] = _Unset, + allow_inf_nan: Union[bool, None] = _Unset, + max_digits: Union[int, None] = _Unset, + decimal_places: Union[int, None] = _Unset, examples: Optional[List[Any]] = None, example: Annotated[ Optional[Any], @@ -253,13 +432,20 @@ def Form( # noqa: N802 "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " "although still supported. Use examples instead." ), - ] = Undefined, + ] = _Unset, + deprecated: Optional[bool] = None, + include_in_schema: bool = True, + json_schema_extra: Union[Dict[str, Any], None] = None, **extra: Any, ) -> Any: return params.Form( default=default, + default_factory=default_factory, media_type=media_type, alias=alias, + alias_priority=alias_priority, + validation_alias=validation_alias, + serialization_alias=serialization_alias, title=title, description=description, gt=gt, @@ -268,9 +454,19 @@ def Form( # noqa: N802 le=le, min_length=min_length, max_length=max_length, + pattern=pattern, regex=regex, + discriminator=discriminator, + strict=strict, + multiple_of=multiple_of, + allow_inf_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, example=example, examples=examples, + deprecated=deprecated, + include_in_schema=include_in_schema, + json_schema_extra=json_schema_extra, **extra, ) @@ -278,8 +474,14 @@ def Form( # noqa: N802 def File( # noqa: N802 default: Any = Undefined, *, + default_factory: Union[Callable[[], Any], None] = _Unset, media_type: str = "multipart/form-data", alias: Optional[str] = None, + alias_priority: Union[int, None] = _Unset, + # TODO: update when deprecating Pydantic v1, import these types + # validation_alias: str | AliasPath | AliasChoices | None + validation_alias: Union[str, None] = None, + serialization_alias: Union[str, None] = None, title: Optional[str] = None, description: Optional[str] = None, gt: Optional[float] = None, @@ -288,7 +490,19 @@ def File( # noqa: N802 le: Optional[float] = None, min_length: Optional[int] = None, max_length: Optional[int] = None, - regex: Optional[str] = None, + pattern: Optional[str] = None, + regex: Annotated[ + Optional[str], + deprecated( + "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." + ), + ] = None, + discriminator: Union[str, None] = None, + strict: Union[bool, None] = _Unset, + multiple_of: Union[float, None] = _Unset, + allow_inf_nan: Union[bool, None] = _Unset, + max_digits: Union[int, None] = _Unset, + decimal_places: Union[int, None] = _Unset, examples: Optional[List[Any]] = None, example: Annotated[ Optional[Any], @@ -296,13 +510,20 @@ def File( # noqa: N802 "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " "although still supported. Use examples instead." ), - ] = Undefined, + ] = _Unset, + deprecated: Optional[bool] = None, + include_in_schema: bool = True, + json_schema_extra: Union[Dict[str, Any], None] = None, **extra: Any, ) -> Any: return params.File( default=default, + default_factory=default_factory, media_type=media_type, alias=alias, + alias_priority=alias_priority, + validation_alias=validation_alias, + serialization_alias=serialization_alias, title=title, description=description, gt=gt, @@ -311,9 +532,19 @@ def File( # noqa: N802 le=le, min_length=min_length, max_length=max_length, + pattern=pattern, regex=regex, + discriminator=discriminator, + strict=strict, + multiple_of=multiple_of, + allow_inf_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, example=example, examples=examples, + deprecated=deprecated, + include_in_schema=include_in_schema, + json_schema_extra=json_schema_extra, **extra, ) diff --git a/fastapi/params.py b/fastapi/params.py index 4069f2cda..30af5713e 100644 --- a/fastapi/params.py +++ b/fastapi/params.py @@ -1,10 +1,14 @@ import warnings from enum import Enum -from typing import Any, Callable, List, Optional, Sequence +from typing import Any, Callable, Dict, List, Optional, Sequence, Union -from pydantic.fields import FieldInfo, Undefined +from pydantic.fields import FieldInfo from typing_extensions import Annotated, deprecated +from ._compat import PYDANTIC_V2, Undefined + +_Unset: Any = Undefined + class ParamTypes(Enum): query = "query" @@ -20,7 +24,14 @@ class Param(FieldInfo): self, default: Any = Undefined, *, + default_factory: Union[Callable[[], Any], None] = _Unset, + annotation: Optional[Any] = None, alias: Optional[str] = None, + alias_priority: Union[int, None] = _Unset, + # TODO: update when deprecating Pydantic v1, import these types + # validation_alias: str | AliasPath | AliasChoices | None + validation_alias: Union[str, None] = None, + serialization_alias: Union[str, None] = None, title: Optional[str] = None, description: Optional[str] = None, gt: Optional[float] = None, @@ -29,7 +40,19 @@ class Param(FieldInfo): le: Optional[float] = None, min_length: Optional[int] = None, max_length: Optional[int] = None, - regex: Optional[str] = None, + pattern: Optional[str] = None, + regex: Annotated[ + Optional[str], + deprecated( + "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." + ), + ] = None, + discriminator: Union[str, None] = None, + strict: Union[bool, None] = _Unset, + multiple_of: Union[float, None] = _Unset, + allow_inf_nan: Union[bool, None] = _Unset, + max_digits: Union[int, None] = _Unset, + decimal_places: Union[int, None] = _Unset, examples: Optional[List[Any]] = None, example: Annotated[ Optional[Any], @@ -37,25 +60,24 @@ class Param(FieldInfo): "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " "although still supported. Use examples instead." ), - ] = Undefined, + ] = _Unset, deprecated: Optional[bool] = None, include_in_schema: bool = True, + json_schema_extra: Union[Dict[str, Any], None] = None, **extra: Any, ): self.deprecated = deprecated - if example is not Undefined: + if example is not _Unset: warnings.warn( "`example` has been depreacated, please use `examples` instead", category=DeprecationWarning, - stacklevel=1, + stacklevel=4, ) self.example = example self.include_in_schema = include_in_schema - extra_kwargs = {**extra} - if examples: - extra_kwargs["examples"] = examples - super().__init__( + kwargs = dict( default=default, + default_factory=default_factory, alias=alias, title=title, description=description, @@ -65,9 +87,40 @@ class Param(FieldInfo): le=le, min_length=min_length, max_length=max_length, - regex=regex, - **extra_kwargs, + discriminator=discriminator, + multiple_of=multiple_of, + allow_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, + **extra, ) + if examples is not None: + kwargs["examples"] = examples + if regex is not None: + warnings.warn( + "`regex` has been depreacated, please use `pattern` instead", + category=DeprecationWarning, + stacklevel=4, + ) + current_json_schema_extra = json_schema_extra or extra + if PYDANTIC_V2: + kwargs.update( + { + "annotation": annotation, + "alias_priority": alias_priority, + "validation_alias": validation_alias, + "serialization_alias": serialization_alias, + "strict": strict, + "json_schema_extra": current_json_schema_extra, + } + ) + kwargs["pattern"] = pattern or regex + else: + kwargs["regex"] = pattern or regex + kwargs.update(**current_json_schema_extra) + use_kwargs = {k: v for k, v in kwargs.items() if v is not _Unset} + + super().__init__(**use_kwargs) def __repr__(self) -> str: return f"{self.__class__.__name__}({self.default})" @@ -80,7 +133,14 @@ class Path(Param): self, default: Any = ..., *, + default_factory: Union[Callable[[], Any], None] = _Unset, + annotation: Optional[Any] = None, alias: Optional[str] = None, + alias_priority: Union[int, None] = _Unset, + # TODO: update when deprecating Pydantic v1, import these types + # validation_alias: str | AliasPath | AliasChoices | None + validation_alias: Union[str, None] = None, + serialization_alias: Union[str, None] = None, title: Optional[str] = None, description: Optional[str] = None, gt: Optional[float] = None, @@ -89,7 +149,19 @@ class Path(Param): le: Optional[float] = None, min_length: Optional[int] = None, max_length: Optional[int] = None, - regex: Optional[str] = None, + pattern: Optional[str] = None, + regex: Annotated[ + Optional[str], + deprecated( + "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." + ), + ] = None, + discriminator: Union[str, None] = None, + strict: Union[bool, None] = _Unset, + multiple_of: Union[float, None] = _Unset, + allow_inf_nan: Union[bool, None] = _Unset, + max_digits: Union[int, None] = _Unset, + decimal_places: Union[int, None] = _Unset, examples: Optional[List[Any]] = None, example: Annotated[ Optional[Any], @@ -97,16 +169,22 @@ class Path(Param): "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " "although still supported. Use examples instead." ), - ] = Undefined, + ] = _Unset, deprecated: Optional[bool] = None, include_in_schema: bool = True, + json_schema_extra: Union[Dict[str, Any], None] = None, **extra: Any, ): assert default is ..., "Path parameters cannot have a default value" self.in_ = self.in_ super().__init__( default=default, + default_factory=default_factory, + annotation=annotation, alias=alias, + alias_priority=alias_priority, + validation_alias=validation_alias, + serialization_alias=serialization_alias, title=title, description=description, gt=gt, @@ -115,11 +193,19 @@ class Path(Param): le=le, min_length=min_length, max_length=max_length, + pattern=pattern, regex=regex, + discriminator=discriminator, + strict=strict, + multiple_of=multiple_of, + allow_inf_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, deprecated=deprecated, example=example, examples=examples, include_in_schema=include_in_schema, + json_schema_extra=json_schema_extra, **extra, ) @@ -131,7 +217,14 @@ class Query(Param): self, default: Any = Undefined, *, + default_factory: Union[Callable[[], Any], None] = _Unset, + annotation: Optional[Any] = None, alias: Optional[str] = None, + alias_priority: Union[int, None] = _Unset, + # TODO: update when deprecating Pydantic v1, import these types + # validation_alias: str | AliasPath | AliasChoices | None + validation_alias: Union[str, None] = None, + serialization_alias: Union[str, None] = None, title: Optional[str] = None, description: Optional[str] = None, gt: Optional[float] = None, @@ -140,7 +233,19 @@ class Query(Param): le: Optional[float] = None, min_length: Optional[int] = None, max_length: Optional[int] = None, - regex: Optional[str] = None, + pattern: Optional[str] = None, + regex: Annotated[ + Optional[str], + deprecated( + "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." + ), + ] = None, + discriminator: Union[str, None] = None, + strict: Union[bool, None] = _Unset, + multiple_of: Union[float, None] = _Unset, + allow_inf_nan: Union[bool, None] = _Unset, + max_digits: Union[int, None] = _Unset, + decimal_places: Union[int, None] = _Unset, examples: Optional[List[Any]] = None, example: Annotated[ Optional[Any], @@ -148,14 +253,20 @@ class Query(Param): "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " "although still supported. Use examples instead." ), - ] = Undefined, + ] = _Unset, deprecated: Optional[bool] = None, include_in_schema: bool = True, + json_schema_extra: Union[Dict[str, Any], None] = None, **extra: Any, ): super().__init__( default=default, + default_factory=default_factory, + annotation=annotation, alias=alias, + alias_priority=alias_priority, + validation_alias=validation_alias, + serialization_alias=serialization_alias, title=title, description=description, gt=gt, @@ -164,11 +275,19 @@ class Query(Param): le=le, min_length=min_length, max_length=max_length, + pattern=pattern, regex=regex, + discriminator=discriminator, + strict=strict, + multiple_of=multiple_of, + allow_inf_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, deprecated=deprecated, example=example, examples=examples, include_in_schema=include_in_schema, + json_schema_extra=json_schema_extra, **extra, ) @@ -180,7 +299,14 @@ class Header(Param): self, default: Any = Undefined, *, + default_factory: Union[Callable[[], Any], None] = _Unset, + annotation: Optional[Any] = None, alias: Optional[str] = None, + alias_priority: Union[int, None] = _Unset, + # TODO: update when deprecating Pydantic v1, import these types + # validation_alias: str | AliasPath | AliasChoices | None + validation_alias: Union[str, None] = None, + serialization_alias: Union[str, None] = None, convert_underscores: bool = True, title: Optional[str] = None, description: Optional[str] = None, @@ -190,7 +316,19 @@ class Header(Param): le: Optional[float] = None, min_length: Optional[int] = None, max_length: Optional[int] = None, - regex: Optional[str] = None, + pattern: Optional[str] = None, + regex: Annotated[ + Optional[str], + deprecated( + "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." + ), + ] = None, + discriminator: Union[str, None] = None, + strict: Union[bool, None] = _Unset, + multiple_of: Union[float, None] = _Unset, + allow_inf_nan: Union[bool, None] = _Unset, + max_digits: Union[int, None] = _Unset, + decimal_places: Union[int, None] = _Unset, examples: Optional[List[Any]] = None, example: Annotated[ Optional[Any], @@ -198,15 +336,21 @@ class Header(Param): "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " "although still supported. Use examples instead." ), - ] = Undefined, + ] = _Unset, deprecated: Optional[bool] = None, include_in_schema: bool = True, + json_schema_extra: Union[Dict[str, Any], None] = None, **extra: Any, ): self.convert_underscores = convert_underscores super().__init__( default=default, + default_factory=default_factory, + annotation=annotation, alias=alias, + alias_priority=alias_priority, + validation_alias=validation_alias, + serialization_alias=serialization_alias, title=title, description=description, gt=gt, @@ -215,11 +359,19 @@ class Header(Param): le=le, min_length=min_length, max_length=max_length, + pattern=pattern, regex=regex, + discriminator=discriminator, + strict=strict, + multiple_of=multiple_of, + allow_inf_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, deprecated=deprecated, example=example, examples=examples, include_in_schema=include_in_schema, + json_schema_extra=json_schema_extra, **extra, ) @@ -231,7 +383,14 @@ class Cookie(Param): self, default: Any = Undefined, *, + default_factory: Union[Callable[[], Any], None] = _Unset, + annotation: Optional[Any] = None, alias: Optional[str] = None, + alias_priority: Union[int, None] = _Unset, + # TODO: update when deprecating Pydantic v1, import these types + # validation_alias: str | AliasPath | AliasChoices | None + validation_alias: Union[str, None] = None, + serialization_alias: Union[str, None] = None, title: Optional[str] = None, description: Optional[str] = None, gt: Optional[float] = None, @@ -240,7 +399,19 @@ class Cookie(Param): le: Optional[float] = None, min_length: Optional[int] = None, max_length: Optional[int] = None, - regex: Optional[str] = None, + pattern: Optional[str] = None, + regex: Annotated[ + Optional[str], + deprecated( + "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." + ), + ] = None, + discriminator: Union[str, None] = None, + strict: Union[bool, None] = _Unset, + multiple_of: Union[float, None] = _Unset, + allow_inf_nan: Union[bool, None] = _Unset, + max_digits: Union[int, None] = _Unset, + decimal_places: Union[int, None] = _Unset, examples: Optional[List[Any]] = None, example: Annotated[ Optional[Any], @@ -248,14 +419,20 @@ class Cookie(Param): "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " "although still supported. Use examples instead." ), - ] = Undefined, + ] = _Unset, deprecated: Optional[bool] = None, include_in_schema: bool = True, + json_schema_extra: Union[Dict[str, Any], None] = None, **extra: Any, ): super().__init__( default=default, + default_factory=default_factory, + annotation=annotation, alias=alias, + alias_priority=alias_priority, + validation_alias=validation_alias, + serialization_alias=serialization_alias, title=title, description=description, gt=gt, @@ -264,11 +441,19 @@ class Cookie(Param): le=le, min_length=min_length, max_length=max_length, + pattern=pattern, regex=regex, + discriminator=discriminator, + strict=strict, + multiple_of=multiple_of, + allow_inf_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, deprecated=deprecated, example=example, examples=examples, include_in_schema=include_in_schema, + json_schema_extra=json_schema_extra, **extra, ) @@ -278,9 +463,16 @@ class Body(FieldInfo): self, default: Any = Undefined, *, + default_factory: Union[Callable[[], Any], None] = _Unset, + annotation: Optional[Any] = None, embed: bool = False, media_type: str = "application/json", alias: Optional[str] = None, + alias_priority: Union[int, None] = _Unset, + # TODO: update when deprecating Pydantic v1, import these types + # validation_alias: str | AliasPath | AliasChoices | None + validation_alias: Union[str, None] = None, + serialization_alias: Union[str, None] = None, title: Optional[str] = None, description: Optional[str] = None, gt: Optional[float] = None, @@ -289,7 +481,19 @@ class Body(FieldInfo): le: Optional[float] = None, min_length: Optional[int] = None, max_length: Optional[int] = None, - regex: Optional[str] = None, + pattern: Optional[str] = None, + regex: Annotated[ + Optional[str], + deprecated( + "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." + ), + ] = None, + discriminator: Union[str, None] = None, + strict: Union[bool, None] = _Unset, + multiple_of: Union[float, None] = _Unset, + allow_inf_nan: Union[bool, None] = _Unset, + max_digits: Union[int, None] = _Unset, + decimal_places: Union[int, None] = _Unset, examples: Optional[List[Any]] = None, example: Annotated[ Optional[Any], @@ -297,23 +501,26 @@ class Body(FieldInfo): "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " "although still supported. Use examples instead." ), - ] = Undefined, + ] = _Unset, + deprecated: Optional[bool] = None, + include_in_schema: bool = True, + json_schema_extra: Union[Dict[str, Any], None] = None, **extra: Any, ): self.embed = embed self.media_type = media_type - if example is not Undefined: + self.deprecated = deprecated + if example is not _Unset: warnings.warn( "`example` has been depreacated, please use `examples` instead", category=DeprecationWarning, - stacklevel=1, + stacklevel=4, ) self.example = example - extra_kwargs = {**extra} - if examples is not None: - extra_kwargs["examples"] = examples - super().__init__( + self.include_in_schema = include_in_schema + kwargs = dict( default=default, + default_factory=default_factory, alias=alias, title=title, description=description, @@ -323,9 +530,41 @@ class Body(FieldInfo): le=le, min_length=min_length, max_length=max_length, - regex=regex, - **extra_kwargs, + discriminator=discriminator, + multiple_of=multiple_of, + allow_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, + **extra, ) + if examples is not None: + kwargs["examples"] = examples + if regex is not None: + warnings.warn( + "`regex` has been depreacated, please use `pattern` instead", + category=DeprecationWarning, + stacklevel=4, + ) + current_json_schema_extra = json_schema_extra or extra + if PYDANTIC_V2: + kwargs.update( + { + "annotation": annotation, + "alias_priority": alias_priority, + "validation_alias": validation_alias, + "serialization_alias": serialization_alias, + "strict": strict, + "json_schema_extra": current_json_schema_extra, + } + ) + kwargs["pattern"] = pattern or regex + else: + kwargs["regex"] = pattern or regex + kwargs.update(**current_json_schema_extra) + + use_kwargs = {k: v for k, v in kwargs.items() if v is not _Unset} + + super().__init__(**use_kwargs) def __repr__(self) -> str: return f"{self.__class__.__name__}({self.default})" @@ -336,8 +575,15 @@ class Form(Body): self, default: Any = Undefined, *, + default_factory: Union[Callable[[], Any], None] = _Unset, + annotation: Optional[Any] = None, media_type: str = "application/x-www-form-urlencoded", alias: Optional[str] = None, + alias_priority: Union[int, None] = _Unset, + # TODO: update when deprecating Pydantic v1, import these types + # validation_alias: str | AliasPath | AliasChoices | None + validation_alias: Union[str, None] = None, + serialization_alias: Union[str, None] = None, title: Optional[str] = None, description: Optional[str] = None, gt: Optional[float] = None, @@ -346,7 +592,19 @@ class Form(Body): le: Optional[float] = None, min_length: Optional[int] = None, max_length: Optional[int] = None, - regex: Optional[str] = None, + pattern: Optional[str] = None, + regex: Annotated[ + Optional[str], + deprecated( + "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." + ), + ] = None, + discriminator: Union[str, None] = None, + strict: Union[bool, None] = _Unset, + multiple_of: Union[float, None] = _Unset, + allow_inf_nan: Union[bool, None] = _Unset, + max_digits: Union[int, None] = _Unset, + decimal_places: Union[int, None] = _Unset, examples: Optional[List[Any]] = None, example: Annotated[ Optional[Any], @@ -354,14 +612,22 @@ class Form(Body): "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " "although still supported. Use examples instead." ), - ] = Undefined, + ] = _Unset, + deprecated: Optional[bool] = None, + include_in_schema: bool = True, + json_schema_extra: Union[Dict[str, Any], None] = None, **extra: Any, ): super().__init__( default=default, + default_factory=default_factory, + annotation=annotation, embed=True, media_type=media_type, alias=alias, + alias_priority=alias_priority, + validation_alias=validation_alias, + serialization_alias=serialization_alias, title=title, description=description, gt=gt, @@ -370,9 +636,19 @@ class Form(Body): le=le, min_length=min_length, max_length=max_length, + pattern=pattern, regex=regex, + discriminator=discriminator, + strict=strict, + multiple_of=multiple_of, + allow_inf_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, + deprecated=deprecated, example=example, examples=examples, + include_in_schema=include_in_schema, + json_schema_extra=json_schema_extra, **extra, ) @@ -382,8 +658,15 @@ class File(Form): self, default: Any = Undefined, *, + default_factory: Union[Callable[[], Any], None] = _Unset, + annotation: Optional[Any] = None, media_type: str = "multipart/form-data", alias: Optional[str] = None, + alias_priority: Union[int, None] = _Unset, + # TODO: update when deprecating Pydantic v1, import these types + # validation_alias: str | AliasPath | AliasChoices | None + validation_alias: Union[str, None] = None, + serialization_alias: Union[str, None] = None, title: Optional[str] = None, description: Optional[str] = None, gt: Optional[float] = None, @@ -392,7 +675,19 @@ class File(Form): le: Optional[float] = None, min_length: Optional[int] = None, max_length: Optional[int] = None, - regex: Optional[str] = None, + pattern: Optional[str] = None, + regex: Annotated[ + Optional[str], + deprecated( + "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." + ), + ] = None, + discriminator: Union[str, None] = None, + strict: Union[bool, None] = _Unset, + multiple_of: Union[float, None] = _Unset, + allow_inf_nan: Union[bool, None] = _Unset, + max_digits: Union[int, None] = _Unset, + decimal_places: Union[int, None] = _Unset, examples: Optional[List[Any]] = None, example: Annotated[ Optional[Any], @@ -400,13 +695,21 @@ class File(Form): "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " "although still supported. Use examples instead." ), - ] = Undefined, + ] = _Unset, + deprecated: Optional[bool] = None, + include_in_schema: bool = True, + json_schema_extra: Union[Dict[str, Any], None] = None, **extra: Any, ): super().__init__( default=default, + default_factory=default_factory, + annotation=annotation, media_type=media_type, alias=alias, + alias_priority=alias_priority, + validation_alias=validation_alias, + serialization_alias=serialization_alias, title=title, description=description, gt=gt, @@ -415,9 +718,19 @@ class File(Form): le=le, min_length=min_length, max_length=max_length, + pattern=pattern, regex=regex, + discriminator=discriminator, + strict=strict, + multiple_of=multiple_of, + allow_inf_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, + deprecated=deprecated, example=example, examples=examples, + include_in_schema=include_in_schema, + json_schema_extra=json_schema_extra, **extra, ) diff --git a/fastapi/routing.py b/fastapi/routing.py index ec8af99b3..d8ff0579c 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -20,6 +20,14 @@ from typing import ( ) from fastapi import params +from fastapi._compat import ( + ModelField, + Undefined, + _get_model_config, + _model_dump, + _normalize_errors, + lenient_issubclass, +) from fastapi.datastructures import Default, DefaultPlaceholder from fastapi.dependencies.models import Dependant from fastapi.dependencies.utils import ( @@ -29,13 +37,14 @@ from fastapi.dependencies.utils import ( get_typed_return_annotation, solve_dependencies, ) -from fastapi.encoders import DictIntStrAny, SetIntStr, jsonable_encoder +from fastapi.encoders import jsonable_encoder from fastapi.exceptions import ( FastAPIError, RequestValidationError, + ResponseValidationError, WebSocketRequestValidationError, ) -from fastapi.types import DecoratedCallable +from fastapi.types import DecoratedCallable, IncEx from fastapi.utils import ( create_cloned_field, create_response_field, @@ -44,9 +53,6 @@ from fastapi.utils import ( is_body_allowed_for_status_code, ) from pydantic import BaseModel -from pydantic.error_wrappers import ErrorWrapper, ValidationError -from pydantic.fields import ModelField, Undefined -from pydantic.utils import lenient_issubclass from starlette import routing from starlette.concurrency import run_in_threadpool from starlette.exceptions import HTTPException @@ -73,14 +79,15 @@ def _prepare_response_content( exclude_none: bool = False, ) -> Any: if isinstance(res, BaseModel): - read_with_orm_mode = getattr(res.__config__, "read_with_orm_mode", None) + read_with_orm_mode = getattr(_get_model_config(res), "read_with_orm_mode", None) if read_with_orm_mode: # Let from_orm extract the data from this model instead of converting # it now to a dict. # Otherwise there's no way to extract lazy data that requires attribute # access instead of dict iteration, e.g. lazy relationships. return res - return res.dict( + return _model_dump( + res, by_alias=True, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, @@ -115,8 +122,8 @@ async def serialize_response( *, field: Optional[ModelField] = None, response_content: Any, - include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, + include: Optional[IncEx] = None, + exclude: Optional[IncEx] = None, by_alias: bool = True, exclude_unset: bool = False, exclude_defaults: bool = False, @@ -125,24 +132,40 @@ async def serialize_response( ) -> Any: if field: errors = [] - response_content = _prepare_response_content( - response_content, - exclude_unset=exclude_unset, - exclude_defaults=exclude_defaults, - exclude_none=exclude_none, - ) + if not hasattr(field, "serialize"): + # pydantic v1 + response_content = _prepare_response_content( + response_content, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + ) if is_coroutine: value, errors_ = field.validate(response_content, {}, loc=("response",)) else: value, errors_ = await run_in_threadpool( field.validate, response_content, {}, loc=("response",) ) - if isinstance(errors_, ErrorWrapper): - errors.append(errors_) - elif isinstance(errors_, list): + if isinstance(errors_, list): errors.extend(errors_) + elif errors_: + errors.append(errors_) if errors: - raise ValidationError(errors, field.type_) + raise ResponseValidationError( + errors=_normalize_errors(errors), body=response_content + ) + + if hasattr(field, "serialize"): + return field.serialize( + value, + include=include, + exclude=exclude, + by_alias=by_alias, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + ) + return jsonable_encoder( value, include=include, @@ -175,8 +198,8 @@ def get_request_handler( status_code: Optional[int] = None, response_class: Union[Type[Response], DefaultPlaceholder] = Default(JSONResponse), response_field: Optional[ModelField] = None, - response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, + response_model_include: Optional[IncEx] = None, + response_model_exclude: Optional[IncEx] = None, response_model_by_alias: bool = True, response_model_exclude_unset: bool = False, response_model_exclude_defaults: bool = False, @@ -220,7 +243,16 @@ def get_request_handler( body = body_bytes except json.JSONDecodeError as e: raise RequestValidationError( - [ErrorWrapper(e, ("body", e.pos))], body=e.doc + [ + { + "type": "json_invalid", + "loc": ("body", e.pos), + "msg": "JSON decode error", + "input": {}, + "ctx": {"error": e.msg}, + } + ], + body=e.doc, ) from e except HTTPException: raise @@ -236,7 +268,7 @@ def get_request_handler( ) values, errors, background_tasks, sub_response, _ = solved_result if errors: - raise RequestValidationError(errors, body=body) + raise RequestValidationError(_normalize_errors(errors), body=body) else: raw_response = await run_endpoint_function( dependant=dependant, values=values, is_coroutine=is_coroutine @@ -287,7 +319,7 @@ def get_websocket_app( ) values, errors, _, _2, _3 = solved_result if errors: - raise WebSocketRequestValidationError(errors) + raise WebSocketRequestValidationError(_normalize_errors(errors)) assert dependant.call is not None, "dependant.call must be a function" await dependant.call(**values) @@ -348,8 +380,8 @@ class APIRoute(routing.Route): name: Optional[str] = None, methods: Optional[Union[Set[str], List[str]]] = None, operation_id: Optional[str] = None, - response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, + response_model_include: Optional[IncEx] = None, + response_model_exclude: Optional[IncEx] = None, response_model_by_alias: bool = True, response_model_exclude_unset: bool = False, response_model_exclude_defaults: bool = False, @@ -414,7 +446,11 @@ class APIRoute(routing.Route): ), f"Status code {status_code} must not have a response body" response_name = "Response_" + self.unique_id self.response_field = create_response_field( - name=response_name, type_=self.response_model + name=response_name, + type_=self.response_model, + # TODO: This should actually set mode='serialization', just, that changes the schemas + # mode="serialization", + mode="validation", ) # Create a clone of the field, so that a Pydantic submodel is not returned # as is just because it's an instance of a subclass of a more limited class @@ -423,6 +459,7 @@ class APIRoute(routing.Route): # would pass the validation and be returned as is. # By being a new field, no inheritance will be passed as is. A new model # will be always created. + # TODO: remove when deprecating Pydantic v1 self.secure_cloned_response_field: Optional[ ModelField ] = create_cloned_field(self.response_field) @@ -569,8 +606,8 @@ class APIRouter(routing.Router): deprecated: Optional[bool] = None, methods: Optional[Union[Set[str], List[str]]] = None, operation_id: Optional[str] = None, - response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, + response_model_include: Optional[IncEx] = None, + response_model_exclude: Optional[IncEx] = None, response_model_by_alias: bool = True, response_model_exclude_unset: bool = False, response_model_exclude_defaults: bool = False, @@ -650,8 +687,8 @@ class APIRouter(routing.Router): deprecated: Optional[bool] = None, methods: Optional[List[str]] = None, operation_id: Optional[str] = None, - response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, + response_model_include: Optional[IncEx] = None, + response_model_exclude: Optional[IncEx] = None, response_model_by_alias: bool = True, response_model_exclude_unset: bool = False, response_model_exclude_defaults: bool = False, @@ -877,8 +914,8 @@ class APIRouter(routing.Router): responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, deprecated: Optional[bool] = None, operation_id: Optional[str] = None, - response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, + response_model_include: Optional[IncEx] = None, + response_model_exclude: Optional[IncEx] = None, response_model_by_alias: bool = True, response_model_exclude_unset: bool = False, response_model_exclude_defaults: bool = False, @@ -933,8 +970,8 @@ class APIRouter(routing.Router): responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, deprecated: Optional[bool] = None, operation_id: Optional[str] = None, - response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, + response_model_include: Optional[IncEx] = None, + response_model_exclude: Optional[IncEx] = None, response_model_by_alias: bool = True, response_model_exclude_unset: bool = False, response_model_exclude_defaults: bool = False, @@ -989,8 +1026,8 @@ class APIRouter(routing.Router): responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, deprecated: Optional[bool] = None, operation_id: Optional[str] = None, - response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, + response_model_include: Optional[IncEx] = None, + response_model_exclude: Optional[IncEx] = None, response_model_by_alias: bool = True, response_model_exclude_unset: bool = False, response_model_exclude_defaults: bool = False, @@ -1045,8 +1082,8 @@ class APIRouter(routing.Router): responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, deprecated: Optional[bool] = None, operation_id: Optional[str] = None, - response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, + response_model_include: Optional[IncEx] = None, + response_model_exclude: Optional[IncEx] = None, response_model_by_alias: bool = True, response_model_exclude_unset: bool = False, response_model_exclude_defaults: bool = False, @@ -1101,8 +1138,8 @@ class APIRouter(routing.Router): responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, deprecated: Optional[bool] = None, operation_id: Optional[str] = None, - response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, + response_model_include: Optional[IncEx] = None, + response_model_exclude: Optional[IncEx] = None, response_model_by_alias: bool = True, response_model_exclude_unset: bool = False, response_model_exclude_defaults: bool = False, @@ -1157,8 +1194,8 @@ class APIRouter(routing.Router): responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, deprecated: Optional[bool] = None, operation_id: Optional[str] = None, - response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, + response_model_include: Optional[IncEx] = None, + response_model_exclude: Optional[IncEx] = None, response_model_by_alias: bool = True, response_model_exclude_unset: bool = False, response_model_exclude_defaults: bool = False, @@ -1213,8 +1250,8 @@ class APIRouter(routing.Router): responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, deprecated: Optional[bool] = None, operation_id: Optional[str] = None, - response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, + response_model_include: Optional[IncEx] = None, + response_model_exclude: Optional[IncEx] = None, response_model_by_alias: bool = True, response_model_exclude_unset: bool = False, response_model_exclude_defaults: bool = False, @@ -1269,8 +1306,8 @@ class APIRouter(routing.Router): responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, deprecated: Optional[bool] = None, operation_id: Optional[str] = None, - response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, + response_model_include: Optional[IncEx] = None, + response_model_exclude: Optional[IncEx] = None, response_model_by_alias: bool = True, response_model_exclude_unset: bool = False, response_model_exclude_defaults: bool = False, diff --git a/fastapi/security/oauth2.py b/fastapi/security/oauth2.py index 938dec37c..e4c4357e7 100644 --- a/fastapi/security/oauth2.py +++ b/fastapi/security/oauth2.py @@ -9,6 +9,9 @@ from fastapi.security.utils import get_authorization_scheme_param from starlette.requests import Request from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN +# TODO: import from typing when deprecating Python 3.9 +from typing_extensions import Annotated + class OAuth2PasswordRequestForm: """ @@ -45,12 +48,13 @@ class OAuth2PasswordRequestForm: def __init__( self, - grant_type: str = Form(default=None, regex="password"), - username: str = Form(), - password: str = Form(), - scope: str = Form(default=""), - client_id: Optional[str] = Form(default=None), - client_secret: Optional[str] = Form(default=None), + *, + grant_type: Annotated[Union[str, None], Form(pattern="password")] = None, + username: Annotated[str, Form()], + password: Annotated[str, Form()], + scope: Annotated[str, Form()] = "", + client_id: Annotated[Union[str, None], Form()] = None, + client_secret: Annotated[Union[str, None], Form()] = None, ): self.grant_type = grant_type self.username = username @@ -95,12 +99,12 @@ class OAuth2PasswordRequestFormStrict(OAuth2PasswordRequestForm): def __init__( self, - grant_type: str = Form(regex="password"), - username: str = Form(), - password: str = Form(), - scope: str = Form(default=""), - client_id: Optional[str] = Form(default=None), - client_secret: Optional[str] = Form(default=None), + grant_type: Annotated[str, Form(pattern="password")], + username: Annotated[str, Form()], + password: Annotated[str, Form()], + scope: Annotated[str, Form()] = "", + client_id: Annotated[Union[str, None], Form()] = None, + client_secret: Annotated[Union[str, None], Form()] = None, ): super().__init__( grant_type=grant_type, diff --git a/fastapi/types.py b/fastapi/types.py index e0bca4632..7adf565a7 100644 --- a/fastapi/types.py +++ b/fastapi/types.py @@ -1,3 +1,11 @@ -from typing import Any, Callable, TypeVar +import types +from enum import Enum +from typing import Any, Callable, Dict, Set, Type, TypeVar, Union + +from pydantic import BaseModel DecoratedCallable = TypeVar("DecoratedCallable", bound=Callable[..., Any]) +UnionType = getattr(types, "UnionType", Union) +NoneType = getattr(types, "UnionType", None) +ModelNameMap = Dict[Union[Type[BaseModel], Type[Enum]], str] +IncEx = Union[Set[int], Set[str], Dict[int, Any], Dict[str, Any]] diff --git a/fastapi/utils.py b/fastapi/utils.py index 9b9ebcb85..267d64ce8 100644 --- a/fastapi/utils.py +++ b/fastapi/utils.py @@ -1,7 +1,6 @@ import re import warnings from dataclasses import is_dataclass -from enum import Enum from typing import ( TYPE_CHECKING, Any, @@ -16,13 +15,20 @@ from typing import ( from weakref import WeakKeyDictionary import fastapi +from fastapi._compat import ( + PYDANTIC_V2, + BaseConfig, + ModelField, + PydanticSchemaGenerationError, + Undefined, + UndefinedType, + Validator, + lenient_issubclass, +) from fastapi.datastructures import DefaultPlaceholder, DefaultType -from fastapi.openapi.constants import REF_PREFIX -from pydantic import BaseConfig, BaseModel, create_model -from pydantic.class_validators import Validator -from pydantic.fields import FieldInfo, ModelField, UndefinedType -from pydantic.schema import model_process_schema -from pydantic.utils import lenient_issubclass +from pydantic import BaseModel, create_model +from pydantic.fields import FieldInfo +from typing_extensions import Literal if TYPE_CHECKING: # pragma: nocover from .routing import APIRoute @@ -50,24 +56,6 @@ def is_body_allowed_for_status_code(status_code: Union[int, str, None]) -> bool: return not (current_status_code < 200 or current_status_code in {204, 304}) -def get_model_definitions( - *, - flat_models: Set[Union[Type[BaseModel], Type[Enum]]], - model_name_map: Dict[Union[Type[BaseModel], Type[Enum]], str], -) -> Dict[str, Any]: - definitions: Dict[str, Dict[str, Any]] = {} - for model in flat_models: - m_schema, m_definitions, m_nested_models = model_process_schema( - model, model_name_map=model_name_map, ref_prefix=REF_PREFIX - ) - definitions.update(m_definitions) - model_name = model_name_map[model] - if "description" in m_schema: - m_schema["description"] = m_schema["description"].split("\f")[0] - definitions[model_name] = m_schema - return definitions - - def get_path_param_names(path: str) -> Set[str]: return set(re.findall("{(.*?)}", path)) @@ -76,30 +64,40 @@ def create_response_field( name: str, type_: Type[Any], class_validators: Optional[Dict[str, Validator]] = None, - default: Optional[Any] = None, - required: Union[bool, UndefinedType] = True, + default: Optional[Any] = Undefined, + required: Union[bool, UndefinedType] = Undefined, model_config: Type[BaseConfig] = BaseConfig, field_info: Optional[FieldInfo] = None, alias: Optional[str] = None, + mode: Literal["validation", "serialization"] = "validation", ) -> ModelField: """ Create a new response field. Raises if type_ is invalid. """ class_validators = class_validators or {} - field_info = field_info or FieldInfo() - - try: - return ModelField( - name=name, - type_=type_, - class_validators=class_validators, - default=default, - required=required, - model_config=model_config, - alias=alias, - field_info=field_info, + if PYDANTIC_V2: + field_info = field_info or FieldInfo( + annotation=type_, default=default, alias=alias + ) + else: + field_info = field_info or FieldInfo() + kwargs = {"name": name, "field_info": field_info} + if PYDANTIC_V2: + kwargs.update({"mode": mode}) + else: + kwargs.update( + { + "type_": type_, + "class_validators": class_validators, + "default": default, + "required": required, + "model_config": model_config, + "alias": alias, + } ) - except RuntimeError: + try: + return ModelField(**kwargs) # type: ignore[arg-type] + except (RuntimeError, PydanticSchemaGenerationError): raise fastapi.exceptions.FastAPIError( "Invalid args for response field! Hint: " f"check that {type_} is a valid Pydantic field type. " @@ -116,6 +114,8 @@ def create_cloned_field( *, cloned_types: Optional[MutableMapping[Type[BaseModel], Type[BaseModel]]] = None, ) -> ModelField: + if PYDANTIC_V2: + return field # cloned_types caches already cloned types to support recursive models and improve # performance by avoiding unecessary cloning if cloned_types is None: @@ -136,30 +136,30 @@ def create_cloned_field( f, cloned_types=cloned_types ) new_field = create_response_field(name=field.name, type_=use_type) - new_field.has_alias = field.has_alias - new_field.alias = field.alias - new_field.class_validators = field.class_validators - new_field.default = field.default - new_field.required = field.required - new_field.model_config = field.model_config + new_field.has_alias = field.has_alias # type: ignore[attr-defined] + new_field.alias = field.alias # type: ignore[misc] + new_field.class_validators = field.class_validators # type: ignore[attr-defined] + new_field.default = field.default # type: ignore[misc] + new_field.required = field.required # type: ignore[misc] + new_field.model_config = field.model_config # type: ignore[attr-defined] new_field.field_info = field.field_info - new_field.allow_none = field.allow_none - new_field.validate_always = field.validate_always - if field.sub_fields: - new_field.sub_fields = [ + new_field.allow_none = field.allow_none # type: ignore[attr-defined] + new_field.validate_always = field.validate_always # type: ignore[attr-defined] + if field.sub_fields: # type: ignore[attr-defined] + new_field.sub_fields = [ # type: ignore[attr-defined] create_cloned_field(sub_field, cloned_types=cloned_types) - for sub_field in field.sub_fields + for sub_field in field.sub_fields # type: ignore[attr-defined] ] - if field.key_field: - new_field.key_field = create_cloned_field( - field.key_field, cloned_types=cloned_types + if field.key_field: # type: ignore[attr-defined] + new_field.key_field = create_cloned_field( # type: ignore[attr-defined] + field.key_field, cloned_types=cloned_types # type: ignore[attr-defined] ) - new_field.validators = field.validators - new_field.pre_validators = field.pre_validators - new_field.post_validators = field.post_validators - new_field.parse_json = field.parse_json - new_field.shape = field.shape - new_field.populate_validators() + new_field.validators = field.validators # type: ignore[attr-defined] + new_field.pre_validators = field.pre_validators # type: ignore[attr-defined] + new_field.post_validators = field.post_validators # type: ignore[attr-defined] + new_field.parse_json = field.parse_json # type: ignore[attr-defined] + new_field.shape = field.shape # type: ignore[attr-defined] + new_field.populate_validators() # type: ignore[attr-defined] return new_field @@ -220,3 +220,9 @@ def get_value_or_default( if not isinstance(item, DefaultPlaceholder): return item return first_item + + +def match_pydantic_error_url(error_type: str) -> Any: + from dirty_equals import IsStr + + return IsStr(regex=rf"^https://errors\.pydantic\.dev/.*/v/{error_type}") diff --git a/pyproject.toml b/pyproject.toml index 61dbf7629..f0917578f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,8 +42,8 @@ classifiers = [ ] dependencies = [ "starlette>=0.27.0,<0.28.0", - "pydantic>=1.7.4,!=1.8,!=1.8.1,<2.0.0", - "typing-extensions>=4.5.0" + "pydantic>=1.7.4,!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,<3.0.0", + "typing-extensions>=4.5.0", ] dynamic = ["version"] @@ -61,8 +61,10 @@ all = [ "pyyaml >=5.3.1", "ujson >=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0", "orjson >=3.2.1", - "email_validator >=1.1.1", + "email_validator >=2.0.0", "uvicorn[standard] >=0.12.0", + "pydantic-settings >=2.0.0", + "pydantic-extra-types >=2.0.0", ] [tool.hatch.version] @@ -85,6 +87,7 @@ check_untyped_defs = true addopts = [ "--strict-config", "--strict-markers", + "--ignore=docs_src", ] xfail_strict = true junit_family = "xunit2" @@ -142,6 +145,7 @@ ignore = [ "docs_src/custom_response/tutorial007.py" = ["B007"] "docs_src/dataclasses/tutorial003.py" = ["I001"] "docs_src/path_operation_advanced_configuration/tutorial007.py" = ["B904"] +"docs_src/path_operation_advanced_configuration/tutorial007_pv1.py" = ["B904"] "docs_src/custom_request_and_route/tutorial002.py" = ["B904"] "docs_src/dependencies/tutorial008_an.py" = ["F821"] "docs_src/dependencies/tutorial008_an_py39.py" = ["F821"] diff --git a/requirements-tests.txt b/requirements-tests.txt index 4b34fcc2c..abefac685 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -1,11 +1,13 @@ -e . +pydantic-settings >=2.0.0 pytest >=7.1.3,<8.0.0 coverage[toml] >= 6.5.0,< 8.0 mypy ==1.4.0 ruff ==0.0.275 black == 23.3.0 httpx >=0.23.0,<0.25.0 -email_validator >=1.1.1,<2.0.0 +email_validator >=1.1.1,<3.0.0 +dirty-equals ==0.6.0 # TODO: once removing databases from tutorial, upgrade SQLAlchemy # probably when including SQLModel sqlalchemy >=1.3.18,<1.4.43 diff --git a/tests/test_additional_properties_bool.py b/tests/test_additional_properties_bool.py index e35c26342..de59e48ce 100644 --- a/tests/test_additional_properties_bool.py +++ b/tests/test_additional_properties_bool.py @@ -1,13 +1,19 @@ from typing import Union +from dirty_equals import IsDict from fastapi import FastAPI +from fastapi._compat import PYDANTIC_V2 from fastapi.testclient import TestClient -from pydantic import BaseModel +from pydantic import BaseModel, ConfigDict class FooBaseModel(BaseModel): - class Config: - extra = "forbid" + if PYDANTIC_V2: + model_config = ConfigDict(extra="forbid") + else: + + class Config: + extra = "forbid" class Foo(FooBaseModel): @@ -52,7 +58,19 @@ def test_openapi_schema(): "requestBody": { "content": { "application/json": { - "schema": {"$ref": "#/components/schemas/Foo"} + "schema": IsDict( + { + "anyOf": [ + {"$ref": "#/components/schemas/Foo"}, + {"type": "null"}, + ], + "title": "Foo", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"$ref": "#/components/schemas/Foo"} + ) } } }, diff --git a/tests/test_additional_responses_custom_model_in_callback.py b/tests/test_additional_responses_custom_model_in_callback.py index 397159142..2ad575455 100644 --- a/tests/test_additional_responses_custom_model_in_callback.py +++ b/tests/test_additional_responses_custom_model_in_callback.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi import APIRouter, FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel, HttpUrl @@ -42,13 +43,24 @@ def test_openapi_schema(): "parameters": [ { "required": True, - "schema": { - "title": "Callback Url", - "maxLength": 2083, - "minLength": 1, - "type": "string", - "format": "uri", - }, + "schema": IsDict( + { + "title": "Callback Url", + "minLength": 1, + "type": "string", + "format": "uri", + } + ) + # TODO: remove when deprecating Pydantic v1 + | IsDict( + { + "title": "Callback Url", + "maxLength": 2083, + "minLength": 1, + "type": "string", + "format": "uri", + } + ), "name": "callback_url", "in": "query", } diff --git a/tests/test_annotated.py b/tests/test_annotated.py index 5a70c4541..541f84bca 100644 --- a/tests/test_annotated.py +++ b/tests/test_annotated.py @@ -1,6 +1,8 @@ import pytest +from dirty_equals import IsDict from fastapi import APIRouter, FastAPI, Query from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from typing_extensions import Annotated app = FastAPI() @@ -30,21 +32,46 @@ client = TestClient(app) foo_is_missing = { "detail": [ - { - "loc": ["query", "foo"], - "msg": "field required", - "type": "value_error.missing", - } + IsDict( + { + "loc": ["query", "foo"], + "msg": "Field required", + "type": "missing", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ) + # TODO: remove when deprecating Pydantic v1 + | IsDict( + { + "loc": ["query", "foo"], + "msg": "field required", + "type": "value_error.missing", + } + ) ] } foo_is_short = { "detail": [ - { - "ctx": {"limit_value": 1}, - "loc": ["query", "foo"], - "msg": "ensure this value has at least 1 characters", - "type": "value_error.any_str.min_length", - } + IsDict( + { + "ctx": {"min_length": 1}, + "loc": ["query", "foo"], + "msg": "String should have at least 1 characters", + "type": "string_too_short", + "input": "", + "url": match_pydantic_error_url("string_too_short"), + } + ) + # TODO: remove when deprecating Pydantic v1 + | IsDict( + { + "ctx": {"limit_value": 1}, + "loc": ["query", "foo"], + "msg": "ensure this value has at least 1 characters", + "type": "value_error.any_str.min_length", + } + ) ] } diff --git a/tests/test_application.py b/tests/test_application.py index b036e67af..ea7a80128 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from .main import app @@ -266,10 +267,17 @@ def test_openapi_schema(): "operationId": "get_path_param_id_path_param__item_id__get", "parameters": [ { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, "name": "item_id", "in": "path", + "required": True, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Item Id", + } + ) + # TODO: remove when deprecating Pydantic v1 + | IsDict({"title": "Item Id", "type": "string"}), } ], } @@ -969,10 +977,17 @@ def test_openapi_schema(): "operationId": "get_query_type_optional_query_int_optional_get", "parameters": [ { - "required": False, - "schema": {"title": "Query", "type": "integer"}, "name": "query", "in": "query", + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "integer"}, {"type": "null"}], + "title": "Query", + } + ) + # TODO: remove when deprecating Pydantic v1 + | IsDict({"title": "Query", "type": "integer"}), } ], } diff --git a/tests/test_compat.py b/tests/test_compat.py new file mode 100644 index 000000000..47160ee76 --- /dev/null +++ b/tests/test_compat.py @@ -0,0 +1,93 @@ +from typing import List, Union + +from fastapi import FastAPI, UploadFile +from fastapi._compat import ( + ModelField, + Undefined, + _get_model_config, + is_bytes_sequence_annotation, + is_uploadfile_sequence_annotation, +) +from fastapi.testclient import TestClient +from pydantic import BaseConfig, BaseModel, ConfigDict +from pydantic.fields import FieldInfo + +from .utils import needs_pydanticv1, needs_pydanticv2 + + +@needs_pydanticv2 +def test_model_field_default_required(): + # For coverage + field_info = FieldInfo(annotation=str) + field = ModelField(name="foo", field_info=field_info) + assert field.default is Undefined + + +@needs_pydanticv1 +def test_upload_file_dummy_general_plain_validator_function(): + # For coverage + assert UploadFile.__get_pydantic_core_schema__(str, lambda x: None) == {} + + +@needs_pydanticv1 +def test_union_scalar_list(): + # For coverage + # TODO: there might not be a current valid code path that uses this, it would + # potentially enable query parameters defined as both a scalar and a list + # but that would require more refactors, also not sure it's really useful + from fastapi._compat import is_pv1_scalar_field + + field_info = FieldInfo() + field = ModelField( + name="foo", + field_info=field_info, + type_=Union[str, List[int]], + class_validators={}, + model_config=BaseConfig, + ) + assert not is_pv1_scalar_field(field) + + +@needs_pydanticv2 +def test_get_model_config(): + # For coverage in Pydantic v2 + class Foo(BaseModel): + model_config = ConfigDict(from_attributes=True) + + foo = Foo() + config = _get_model_config(foo) + assert config == {"from_attributes": True} + + +def test_complex(): + app = FastAPI() + + @app.post("/") + def foo(foo: Union[str, List[int]]): + return foo + + client = TestClient(app) + + response = client.post("/", json="bar") + assert response.status_code == 200, response.text + assert response.json() == "bar" + + response2 = client.post("/", json=[1, 2]) + assert response2.status_code == 200, response2.text + assert response2.json() == [1, 2] + + +def test_is_bytes_sequence_annotation_union(): + # For coverage + # TODO: in theory this would allow declaring types that could be lists of bytes + # to be read from files and other types, but I'm not even sure it's a good idea + # to support it as a first class "feature" + assert is_bytes_sequence_annotation(Union[List[str], List[bytes]]) + + +def test_is_uploadfile_sequence_annotation(): + # For coverage + # TODO: in theory this would allow declaring types that could be lists of UploadFile + # and other types, but I'm not even sure it's a good idea to support it as a first + # class "feature" + assert is_uploadfile_sequence_annotation(Union[List[str], List[UploadFile]]) diff --git a/tests/test_custom_schema_fields.py b/tests/test_custom_schema_fields.py index 10b02608c..ee51fc7ff 100644 --- a/tests/test_custom_schema_fields.py +++ b/tests/test_custom_schema_fields.py @@ -1,4 +1,5 @@ from fastapi import FastAPI +from fastapi._compat import PYDANTIC_V2 from fastapi.testclient import TestClient from pydantic import BaseModel @@ -8,10 +9,18 @@ app = FastAPI() class Item(BaseModel): name: str - class Config: - schema_extra = { - "x-something-internal": {"level": 4}, + if PYDANTIC_V2: + model_config = { + "json_schema_extra": { + "x-something-internal": {"level": 4}, + } } + else: + + class Config: + schema_extra = { + "x-something-internal": {"level": 4}, + } @app.get("/foo", response_model=Item) diff --git a/tests/test_datastructures.py b/tests/test_datastructures.py index 2e6217d34..b91467265 100644 --- a/tests/test_datastructures.py +++ b/tests/test_datastructures.py @@ -7,11 +7,17 @@ from fastapi.datastructures import Default from fastapi.testclient import TestClient +# TODO: remove when deprecating Pydantic v1 def test_upload_file_invalid(): with pytest.raises(ValueError): UploadFile.validate("not a Starlette UploadFile") +def test_upload_file_invalid_pydantic_v2(): + with pytest.raises(ValueError): + UploadFile._validate("not a Starlette UploadFile", {}) + + def test_default_placeholder_equals(): placeholder_1 = Default("a") placeholder_2 = Default("a") diff --git a/tests/test_datetime_custom_encoder.py b/tests/test_datetime_custom_encoder.py index 5c1833eb4..3aa77c0b1 100644 --- a/tests/test_datetime_custom_encoder.py +++ b/tests/test_datetime_custom_encoder.py @@ -4,31 +4,54 @@ from fastapi import FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel +from .utils import needs_pydanticv1, needs_pydanticv2 -class ModelWithDatetimeField(BaseModel): - dt_field: datetime - class Config: - json_encoders = { - datetime: lambda dt: dt.replace( - microsecond=0, tzinfo=timezone.utc - ).isoformat() - } +@needs_pydanticv2 +def test_pydanticv2(): + from pydantic import field_serializer + class ModelWithDatetimeField(BaseModel): + dt_field: datetime -app = FastAPI() -model = ModelWithDatetimeField(dt_field=datetime(2019, 1, 1, 8)) + @field_serializer("dt_field") + def serialize_datetime(self, dt_field: datetime): + return dt_field.replace(microsecond=0, tzinfo=timezone.utc).isoformat() + app = FastAPI() + model = ModelWithDatetimeField(dt_field=datetime(2019, 1, 1, 8)) -@app.get("/model", response_model=ModelWithDatetimeField) -def get_model(): - return model + @app.get("/model", response_model=ModelWithDatetimeField) + def get_model(): + return model + client = TestClient(app) + with client: + response = client.get("/model") + assert response.json() == {"dt_field": "2019-01-01T08:00:00+00:00"} + + +# TODO: remove when deprecating Pydantic v1 +@needs_pydanticv1 +def test_pydanticv1(): + class ModelWithDatetimeField(BaseModel): + dt_field: datetime + + class Config: + json_encoders = { + datetime: lambda dt: dt.replace( + microsecond=0, tzinfo=timezone.utc + ).isoformat() + } -client = TestClient(app) + app = FastAPI() + model = ModelWithDatetimeField(dt_field=datetime(2019, 1, 1, 8)) + @app.get("/model", response_model=ModelWithDatetimeField) + def get_model(): + return model -def test_dt(): + client = TestClient(app) with client: response = client.get("/model") assert response.json() == {"dt_field": "2019-01-01T08:00:00+00:00"} diff --git a/tests/test_dependency_duplicates.py b/tests/test_dependency_duplicates.py index 4f4f3166c..0882cc41d 100644 --- a/tests/test_dependency_duplicates.py +++ b/tests/test_dependency_duplicates.py @@ -1,7 +1,9 @@ from typing import List +from dirty_equals import IsDict from fastapi import Depends, FastAPI from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from pydantic import BaseModel app = FastAPI() @@ -47,15 +49,30 @@ async def no_duplicates_sub( def test_no_duplicates_invalid(): response = client.post("/no-duplicates", json={"item": {"data": "myitem"}}) assert response.status_code == 422, response.text - assert response.json() == { - "detail": [ - { - "loc": ["body", "item2"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "item2"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "item2"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) def test_no_duplicates(): diff --git a/tests/test_dependency_overrides.py b/tests/test_dependency_overrides.py index 8bb307971..21cff998d 100644 --- a/tests/test_dependency_overrides.py +++ b/tests/test_dependency_overrides.py @@ -1,8 +1,10 @@ from typing import Optional import pytest +from dirty_equals import IsDict from fastapi import APIRouter, Depends, FastAPI from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url app = FastAPI() @@ -50,99 +52,180 @@ async def overrider_dependency_with_sub(msg: dict = Depends(overrider_sub_depend return msg -@pytest.mark.parametrize( - "url,status_code,expected", - [ - ( - "/main-depends/", - 422, - { - "detail": [ - { - "loc": ["query", "q"], - "msg": "field required", - "type": "value_error.missing", - } - ] - }, - ), - ( - "/main-depends/?q=foo", - 200, - {"in": "main-depends", "params": {"q": "foo", "skip": 0, "limit": 100}}, - ), - ( - "/main-depends/?q=foo&skip=100&limit=200", - 200, - {"in": "main-depends", "params": {"q": "foo", "skip": 100, "limit": 200}}, - ), - ( - "/decorator-depends/", - 422, - { - "detail": [ - { - "loc": ["query", "q"], - "msg": "field required", - "type": "value_error.missing", - } - ] - }, - ), - ("/decorator-depends/?q=foo", 200, {"in": "decorator-depends"}), - ( - "/decorator-depends/?q=foo&skip=100&limit=200", - 200, - {"in": "decorator-depends"}, - ), - ( - "/router-depends/", - 422, - { - "detail": [ - { - "loc": ["query", "q"], - "msg": "field required", - "type": "value_error.missing", - } - ] - }, - ), - ( - "/router-depends/?q=foo", - 200, - {"in": "router-depends", "params": {"q": "foo", "skip": 0, "limit": 100}}, - ), - ( - "/router-depends/?q=foo&skip=100&limit=200", - 200, - {"in": "router-depends", "params": {"q": "foo", "skip": 100, "limit": 200}}, - ), - ( - "/router-decorator-depends/", - 422, - { - "detail": [ - { - "loc": ["query", "q"], - "msg": "field required", - "type": "value_error.missing", - } - ] - }, - ), - ("/router-decorator-depends/?q=foo", 200, {"in": "router-decorator-depends"}), - ( - "/router-decorator-depends/?q=foo&skip=100&limit=200", - 200, - {"in": "router-decorator-depends"}, - ), - ], -) -def test_normal_app(url, status_code, expected): - response = client.get(url) - assert response.status_code == status_code - assert response.json() == expected +def test_main_depends(): + response = client.get("/main-depends/") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "q"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "q"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_main_depends_q_foo(): + response = client.get("/main-depends/?q=foo") + assert response.status_code == 200 + assert response.json() == { + "in": "main-depends", + "params": {"q": "foo", "skip": 0, "limit": 100}, + } + + +def test_main_depends_q_foo_skip_100_limit_200(): + response = client.get("/main-depends/?q=foo&skip=100&limit=200") + assert response.status_code == 200 + assert response.json() == { + "in": "main-depends", + "params": {"q": "foo", "skip": 100, "limit": 200}, + } + + +def test_decorator_depends(): + response = client.get("/decorator-depends/") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "q"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "q"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_decorator_depends_q_foo(): + response = client.get("/decorator-depends/?q=foo") + assert response.status_code == 200 + assert response.json() == {"in": "decorator-depends"} + + +def test_decorator_depends_q_foo_skip_100_limit_200(): + response = client.get("/decorator-depends/?q=foo&skip=100&limit=200") + assert response.status_code == 200 + assert response.json() == {"in": "decorator-depends"} + + +def test_router_depends(): + response = client.get("/router-depends/") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "q"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "q"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_router_depends_q_foo(): + response = client.get("/router-depends/?q=foo") + assert response.status_code == 200 + assert response.json() == { + "in": "router-depends", + "params": {"q": "foo", "skip": 0, "limit": 100}, + } + + +def test_router_depends_q_foo_skip_100_limit_200(): + response = client.get("/router-depends/?q=foo&skip=100&limit=200") + assert response.status_code == 200 + assert response.json() == { + "in": "router-depends", + "params": {"q": "foo", "skip": 100, "limit": 200}, + } + + +def test_router_decorator_depends(): + response = client.get("/router-decorator-depends/") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "q"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "q"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_router_decorator_depends_q_foo(): + response = client.get("/router-decorator-depends/?q=foo") + assert response.status_code == 200 + assert response.json() == {"in": "router-decorator-depends"} + + +def test_router_decorator_depends_q_foo_skip_100_limit_200(): + response = client.get("/router-decorator-depends/?q=foo&skip=100&limit=200") + assert response.status_code == 200 + assert response.json() == {"in": "router-decorator-depends"} @pytest.mark.parametrize( @@ -190,126 +273,281 @@ def test_override_simple(url, status_code, expected): app.dependency_overrides = {} -@pytest.mark.parametrize( - "url,status_code,expected", - [ - ( - "/main-depends/", - 422, - { - "detail": [ - { - "loc": ["query", "k"], - "msg": "field required", - "type": "value_error.missing", - } - ] - }, - ), - ( - "/main-depends/?q=foo", - 422, - { - "detail": [ - { - "loc": ["query", "k"], - "msg": "field required", - "type": "value_error.missing", - } - ] - }, - ), - ("/main-depends/?k=bar", 200, {"in": "main-depends", "params": {"k": "bar"}}), - ( - "/decorator-depends/", - 422, - { - "detail": [ - { - "loc": ["query", "k"], - "msg": "field required", - "type": "value_error.missing", - } - ] - }, - ), - ( - "/decorator-depends/?q=foo", - 422, - { - "detail": [ - { - "loc": ["query", "k"], - "msg": "field required", - "type": "value_error.missing", - } - ] - }, - ), - ("/decorator-depends/?k=bar", 200, {"in": "decorator-depends"}), - ( - "/router-depends/", - 422, - { - "detail": [ - { - "loc": ["query", "k"], - "msg": "field required", - "type": "value_error.missing", - } - ] - }, - ), - ( - "/router-depends/?q=foo", - 422, - { - "detail": [ - { - "loc": ["query", "k"], - "msg": "field required", - "type": "value_error.missing", - } - ] - }, - ), - ( - "/router-depends/?k=bar", - 200, - {"in": "router-depends", "params": {"k": "bar"}}, - ), - ( - "/router-decorator-depends/", - 422, - { - "detail": [ - { - "loc": ["query", "k"], - "msg": "field required", - "type": "value_error.missing", - } - ] - }, - ), - ( - "/router-decorator-depends/?q=foo", - 422, - { - "detail": [ - { - "loc": ["query", "k"], - "msg": "field required", - "type": "value_error.missing", - } - ] - }, - ), - ("/router-decorator-depends/?k=bar", 200, {"in": "router-decorator-depends"}), - ], -) -def test_override_with_sub(url, status_code, expected): +def test_override_with_sub_main_depends(): app.dependency_overrides[common_parameters] = overrider_dependency_with_sub - response = client.get(url) - assert response.status_code == status_code - assert response.json() == expected + response = client.get("/main-depends/") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "k"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "k"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + app.dependency_overrides = {} + + +def test_override_with_sub__main_depends_q_foo(): + app.dependency_overrides[common_parameters] = overrider_dependency_with_sub + response = client.get("/main-depends/?q=foo") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "k"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "k"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + app.dependency_overrides = {} + + +def test_override_with_sub_main_depends_k_bar(): + app.dependency_overrides[common_parameters] = overrider_dependency_with_sub + response = client.get("/main-depends/?k=bar") + assert response.status_code == 200 + assert response.json() == {"in": "main-depends", "params": {"k": "bar"}} + app.dependency_overrides = {} + + +def test_override_with_sub_decorator_depends(): + app.dependency_overrides[common_parameters] = overrider_dependency_with_sub + response = client.get("/decorator-depends/") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "k"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "k"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + app.dependency_overrides = {} + + +def test_override_with_sub_decorator_depends_q_foo(): + app.dependency_overrides[common_parameters] = overrider_dependency_with_sub + response = client.get("/decorator-depends/?q=foo") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "k"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "k"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + app.dependency_overrides = {} + + +def test_override_with_sub_decorator_depends_k_bar(): + app.dependency_overrides[common_parameters] = overrider_dependency_with_sub + response = client.get("/decorator-depends/?k=bar") + assert response.status_code == 200 + assert response.json() == {"in": "decorator-depends"} + app.dependency_overrides = {} + + +def test_override_with_sub_router_depends(): + app.dependency_overrides[common_parameters] = overrider_dependency_with_sub + response = client.get("/router-depends/") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "k"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "k"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + app.dependency_overrides = {} + + +def test_override_with_sub_router_depends_q_foo(): + app.dependency_overrides[common_parameters] = overrider_dependency_with_sub + response = client.get("/router-depends/?q=foo") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "k"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "k"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + app.dependency_overrides = {} + + +def test_override_with_sub_router_depends_k_bar(): + app.dependency_overrides[common_parameters] = overrider_dependency_with_sub + response = client.get("/router-depends/?k=bar") + assert response.status_code == 200 + assert response.json() == {"in": "router-depends", "params": {"k": "bar"}} + app.dependency_overrides = {} + + +def test_override_with_sub_router_decorator_depends(): + app.dependency_overrides[common_parameters] = overrider_dependency_with_sub + response = client.get("/router-decorator-depends/") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "k"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "k"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + app.dependency_overrides = {} + + +def test_override_with_sub_router_decorator_depends_q_foo(): + app.dependency_overrides[common_parameters] = overrider_dependency_with_sub + response = client.get("/router-decorator-depends/?q=foo") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "k"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "k"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + app.dependency_overrides = {} + + +def test_override_with_sub_router_decorator_depends_k_bar(): + app.dependency_overrides[common_parameters] = overrider_dependency_with_sub + response = client.get("/router-decorator-depends/?k=bar") + assert response.status_code == 200 + assert response.json() == {"in": "router-decorator-depends"} app.dependency_overrides = {} diff --git a/tests/test_extra_routes.py b/tests/test_extra_routes.py index fa95d061c..bd16fe925 100644 --- a/tests/test_extra_routes.py +++ b/tests/test_extra_routes.py @@ -1,5 +1,6 @@ from typing import Optional +from dirty_equals import IsDict from fastapi import FastAPI from fastapi.responses import JSONResponse from fastapi.testclient import TestClient @@ -327,7 +328,14 @@ def test_openapi_schema(): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, + "price": IsDict( + { + "title": "Price", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + # TODO: remove when deprecating Pydantic v1 + | IsDict({"title": "Price", "type": "number"}), }, }, "ValidationError": { diff --git a/tests/test_filter_pydantic_sub_model/__init__.py b/tests/test_filter_pydantic_sub_model/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_filter_pydantic_sub_model/app_pv1.py b/tests/test_filter_pydantic_sub_model/app_pv1.py new file mode 100644 index 000000000..657e8c5d1 --- /dev/null +++ b/tests/test_filter_pydantic_sub_model/app_pv1.py @@ -0,0 +1,35 @@ +from typing import Optional + +from fastapi import Depends, FastAPI +from pydantic import BaseModel, validator + +app = FastAPI() + + +class ModelB(BaseModel): + username: str + + +class ModelC(ModelB): + password: str + + +class ModelA(BaseModel): + name: str + description: Optional[str] = None + model_b: ModelB + + @validator("name") + def lower_username(cls, name: str, values): + if not name.endswith("A"): + raise ValueError("name must end in A") + return name + + +async def get_model_c() -> ModelC: + return ModelC(username="test-user", password="test-password") + + +@app.get("/model/{name}", response_model=ModelA) +async def get_model_a(name: str, model_c=Depends(get_model_c)): + return {"name": name, "description": "model-a-desc", "model_b": model_c} diff --git a/tests/test_filter_pydantic_sub_model.py b/tests/test_filter_pydantic_sub_model/test_filter_pydantic_sub_model_pv1.py similarity index 81% rename from tests/test_filter_pydantic_sub_model.py rename to tests/test_filter_pydantic_sub_model/test_filter_pydantic_sub_model_pv1.py index 6ee928d07..48732dbf0 100644 --- a/tests/test_filter_pydantic_sub_model.py +++ b/tests/test_filter_pydantic_sub_model/test_filter_pydantic_sub_model_pv1.py @@ -1,46 +1,20 @@ -from typing import Optional - import pytest -from fastapi import Depends, FastAPI +from fastapi.exceptions import ResponseValidationError from fastapi.testclient import TestClient -from pydantic import BaseModel, ValidationError, validator - -app = FastAPI() - - -class ModelB(BaseModel): - username: str - - -class ModelC(ModelB): - password: str - - -class ModelA(BaseModel): - name: str - description: Optional[str] = None - model_b: ModelB - - @validator("name") - def lower_username(cls, name: str, values): - if not name.endswith("A"): - raise ValueError("name must end in A") - return name - - -async def get_model_c() -> ModelC: - return ModelC(username="test-user", password="test-password") +from ..utils import needs_pydanticv1 -@app.get("/model/{name}", response_model=ModelA) -async def get_model_a(name: str, model_c=Depends(get_model_c)): - return {"name": name, "description": "model-a-desc", "model_b": model_c} +@pytest.fixture(name="client") +def get_client(): + from .app_pv1 import app -client = TestClient(app) + client = TestClient(app) + return client -def test_filter_sub_model(): +@needs_pydanticv1 +def test_filter_sub_model(client: TestClient): response = client.get("/model/modelA") assert response.status_code == 200, response.text assert response.json() == { @@ -50,8 +24,9 @@ def test_filter_sub_model(): } -def test_validator_is_cloned(): - with pytest.raises(ValidationError) as err: +@needs_pydanticv1 +def test_validator_is_cloned(client: TestClient): + with pytest.raises(ResponseValidationError) as err: client.get("/model/modelX") assert err.value.errors() == [ { @@ -62,7 +37,8 @@ def test_validator_is_cloned(): ] -def test_openapi_schema(): +@needs_pydanticv1 +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { diff --git a/tests/test_filter_pydantic_sub_model_pv2.py b/tests/test_filter_pydantic_sub_model_pv2.py new file mode 100644 index 000000000..656332a01 --- /dev/null +++ b/tests/test_filter_pydantic_sub_model_pv2.py @@ -0,0 +1,182 @@ +from typing import Optional + +import pytest +from dirty_equals import IsDict +from fastapi import Depends, FastAPI +from fastapi.exceptions import ResponseValidationError +from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url + +from .utils import needs_pydanticv2 + + +@pytest.fixture(name="client") +def get_client(): + from pydantic import BaseModel, FieldValidationInfo, field_validator + + app = FastAPI() + + class ModelB(BaseModel): + username: str + + class ModelC(ModelB): + password: str + + class ModelA(BaseModel): + name: str + description: Optional[str] = None + foo: ModelB + + @field_validator("name") + def lower_username(cls, name: str, info: FieldValidationInfo): + if not name.endswith("A"): + raise ValueError("name must end in A") + return name + + async def get_model_c() -> ModelC: + return ModelC(username="test-user", password="test-password") + + @app.get("/model/{name}", response_model=ModelA) + async def get_model_a(name: str, model_c=Depends(get_model_c)): + return {"name": name, "description": "model-a-desc", "foo": model_c} + + client = TestClient(app) + return client + + +@needs_pydanticv2 +def test_filter_sub_model(client: TestClient): + response = client.get("/model/modelA") + assert response.status_code == 200, response.text + assert response.json() == { + "name": "modelA", + "description": "model-a-desc", + "foo": {"username": "test-user"}, + } + + +@needs_pydanticv2 +def test_validator_is_cloned(client: TestClient): + with pytest.raises(ResponseValidationError) as err: + client.get("/model/modelX") + assert err.value.errors() == [ + IsDict( + { + "type": "value_error", + "loc": ("response", "name"), + "msg": "Value error, name must end in A", + "input": "modelX", + "ctx": {"error": "name must end in A"}, + "url": match_pydantic_error_url("value_error"), + } + ) + | IsDict( + # TODO remove when deprecating Pydantic v1 + { + "loc": ("response", "name"), + "msg": "name must end in A", + "type": "value_error", + } + ) + ] + + +@needs_pydanticv2 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/model/{name}": { + "get": { + "summary": "Get Model A", + "operationId": "get_model_a_model__name__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Name", "type": "string"}, + "name": "name", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/ModelA"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ModelA": { + "title": "ModelA", + "required": ["name", "foo"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | + # TODO remove when deprecating Pydantic v1 + IsDict({"title": "Description", "type": "string"}), + "foo": {"$ref": "#/components/schemas/ModelB"}, + }, + }, + "ModelB": { + "title": "ModelB", + "required": ["username"], + "type": "object", + "properties": {"username": {"title": "Username", "type": "string"}}, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_infer_param_optionality.py b/tests/test_infer_param_optionality.py index 5e673d9c4..e3d57bb42 100644 --- a/tests/test_infer_param_optionality.py +++ b/tests/test_infer_param_optionality.py @@ -1,5 +1,6 @@ from typing import Optional +from dirty_equals import IsDict from fastapi import APIRouter, FastAPI from fastapi.testclient import TestClient @@ -104,35 +105,253 @@ def test_get_users_item(): assert response.json() == {"item_id": "item01", "user_id": "abc123"} -def test_schema_1(): - """Check that the user_id is a required path parameter under /users""" +def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text - r = response.json() - - d = { - "required": True, - "schema": {"title": "User Id", "type": "string"}, - "name": "user_id", - "in": "path", + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/": { + "get": { + "summary": "Get Users", + "operationId": "get_users_users__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + }, + "/users/{user_id}": { + "get": { + "summary": "Get User", + "operationId": "get_user_users__user_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "User Id", "type": "string"}, + "name": "user_id", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/items/": { + "get": { + "summary": "Get Items", + "operationId": "get_items_items__get", + "parameters": [ + { + "required": False, + "name": "user_id", + "in": "query", + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "User Id", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "User Id", "type": "string"} + ), + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/items/{item_id}": { + "get": { + "summary": "Get Item", + "operationId": "get_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + }, + { + "required": False, + "name": "user_id", + "in": "query", + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "User Id", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "User Id", "type": "string"} + ), + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/users/{user_id}/items/": { + "get": { + "summary": "Get Items", + "operationId": "get_items_users__user_id__items__get", + "parameters": [ + { + "required": True, + "name": "user_id", + "in": "path", + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "User Id", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "User Id", "type": "string"} + ), + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/users/{user_id}/items/{item_id}": { + "get": { + "summary": "Get Item", + "operationId": "get_item_users__user_id__items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + }, + { + "required": True, + "name": "user_id", + "in": "path", + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "User Id", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "User Id", "type": "string"} + ), + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, } - - assert d in r["paths"]["/users/{user_id}"]["get"]["parameters"] - assert d in r["paths"]["/users/{user_id}/items/"]["get"]["parameters"] - - -def test_schema_2(): - """Check that the user_id is an optional query parameter under /items""" - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - r = response.json() - - d = { - "required": False, - "schema": {"title": "User Id", "type": "string"}, - "name": "user_id", - "in": "query", - } - - assert d in r["paths"]["/items/{item_id}"]["get"]["parameters"] - assert d in r["paths"]["/items/"]["get"]["parameters"] diff --git a/tests/test_inherited_custom_class.py b/tests/test_inherited_custom_class.py index bac7eec1b..42b249211 100644 --- a/tests/test_inherited_custom_class.py +++ b/tests/test_inherited_custom_class.py @@ -5,7 +5,7 @@ from fastapi import FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel -app = FastAPI() +from .utils import needs_pydanticv1, needs_pydanticv2 class MyUuid: @@ -26,40 +26,78 @@ class MyUuid: raise TypeError("vars() argument must have __dict__ attribute") -@app.get("/fast_uuid") -def return_fast_uuid(): - # I don't want to import asyncpg for this test so I made my own UUID - # Import asyncpg and uncomment the two lines below for the actual bug +@needs_pydanticv2 +def test_pydanticv2(): + from pydantic import field_serializer - # from asyncpg.pgproto import pgproto - # asyncpg_uuid = pgproto.UUID("a10ff360-3b1e-4984-a26f-d3ab460bdb51") + app = FastAPI() - asyncpg_uuid = MyUuid("a10ff360-3b1e-4984-a26f-d3ab460bdb51") - assert isinstance(asyncpg_uuid, uuid.UUID) - assert type(asyncpg_uuid) != uuid.UUID - with pytest.raises(TypeError): - vars(asyncpg_uuid) - return {"fast_uuid": asyncpg_uuid} + @app.get("/fast_uuid") + def return_fast_uuid(): + asyncpg_uuid = MyUuid("a10ff360-3b1e-4984-a26f-d3ab460bdb51") + assert isinstance(asyncpg_uuid, uuid.UUID) + assert type(asyncpg_uuid) != uuid.UUID + with pytest.raises(TypeError): + vars(asyncpg_uuid) + return {"fast_uuid": asyncpg_uuid} + class SomeCustomClass(BaseModel): + model_config = {"arbitrary_types_allowed": True} -class SomeCustomClass(BaseModel): - class Config: - arbitrary_types_allowed = True - json_encoders = {uuid.UUID: str} + a_uuid: MyUuid - a_uuid: MyUuid + @field_serializer("a_uuid") + def serialize_a_uuid(self, v): + return str(v) + @app.get("/get_custom_class") + def return_some_user(): + # Test that the fix also works for custom pydantic classes + return SomeCustomClass(a_uuid=MyUuid("b8799909-f914-42de-91bc-95c819218d01")) -@app.get("/get_custom_class") -def return_some_user(): - # Test that the fix also works for custom pydantic classes - return SomeCustomClass(a_uuid=MyUuid("b8799909-f914-42de-91bc-95c819218d01")) + client = TestClient(app) + with client: + response_simple = client.get("/fast_uuid") + response_pydantic = client.get("/get_custom_class") + + assert response_simple.json() == { + "fast_uuid": "a10ff360-3b1e-4984-a26f-d3ab460bdb51" + } + + assert response_pydantic.json() == { + "a_uuid": "b8799909-f914-42de-91bc-95c819218d01" + } + + +# TODO: remove when deprecating Pydantic v1 +@needs_pydanticv1 +def test_pydanticv1(): + app = FastAPI() + + @app.get("/fast_uuid") + def return_fast_uuid(): + asyncpg_uuid = MyUuid("a10ff360-3b1e-4984-a26f-d3ab460bdb51") + assert isinstance(asyncpg_uuid, uuid.UUID) + assert type(asyncpg_uuid) != uuid.UUID + with pytest.raises(TypeError): + vars(asyncpg_uuid) + return {"fast_uuid": asyncpg_uuid} + + class SomeCustomClass(BaseModel): + class Config: + arbitrary_types_allowed = True + json_encoders = {uuid.UUID: str} + + a_uuid: MyUuid -client = TestClient(app) + @app.get("/get_custom_class") + def return_some_user(): + # Test that the fix also works for custom pydantic classes + return SomeCustomClass(a_uuid=MyUuid("b8799909-f914-42de-91bc-95c819218d01")) + client = TestClient(app) -def test_dt(): with client: response_simple = client.get("/fast_uuid") response_pydantic = client.get("/get_custom_class") diff --git a/tests/test_jsonable_encoder.py b/tests/test_jsonable_encoder.py index 1f43c33c7..ff3033ecd 100644 --- a/tests/test_jsonable_encoder.py +++ b/tests/test_jsonable_encoder.py @@ -1,13 +1,17 @@ from collections import deque from dataclasses import dataclass from datetime import datetime, timezone +from decimal import Decimal from enum import Enum from pathlib import PurePath, PurePosixPath, PureWindowsPath from typing import Optional import pytest +from fastapi._compat import PYDANTIC_V2 from fastapi.encoders import jsonable_encoder -from pydantic import BaseModel, Field, ValidationError, create_model +from pydantic import BaseModel, Field, ValidationError + +from .utils import needs_pydanticv1, needs_pydanticv2 class Person: @@ -46,22 +50,6 @@ class Unserializable: raise NotImplementedError() -class ModelWithCustomEncoder(BaseModel): - dt_field: datetime - - class Config: - json_encoders = { - datetime: lambda dt: dt.replace( - microsecond=0, tzinfo=timezone.utc - ).isoformat() - } - - -class ModelWithCustomEncoderSubclass(ModelWithCustomEncoder): - class Config: - pass - - class RoleEnum(Enum): admin = "admin" normal = "normal" @@ -70,8 +58,12 @@ class RoleEnum(Enum): class ModelWithConfig(BaseModel): role: Optional[RoleEnum] = None - class Config: - use_enum_values = True + if PYDANTIC_V2: + model_config = {"use_enum_values": True} + else: + + class Config: + use_enum_values = True class ModelWithAlias(BaseModel): @@ -84,23 +76,6 @@ class ModelWithDefault(BaseModel): bla: str = "bla" -class ModelWithRoot(BaseModel): - __root__: str - - -@pytest.fixture( - name="model_with_path", params=[PurePath, PurePosixPath, PureWindowsPath] -) -def fixture_model_with_path(request): - class Config: - arbitrary_types_allowed = True - - ModelWithPath = create_model( - "ModelWithPath", path=(request.param, ...), __config__=Config # type: ignore - ) - return ModelWithPath(path=request.param("/foo", "bar")) - - def test_encode_dict(): pet = {"name": "Firulais", "owner": {"name": "Foo"}} assert jsonable_encoder(pet) == {"name": "Firulais", "owner": {"name": "Foo"}} @@ -154,14 +129,47 @@ def test_encode_unsupported(): jsonable_encoder(unserializable) -def test_encode_custom_json_encoders_model(): +@needs_pydanticv2 +def test_encode_custom_json_encoders_model_pydanticv2(): + from pydantic import field_serializer + + class ModelWithCustomEncoder(BaseModel): + dt_field: datetime + + @field_serializer("dt_field") + def serialize_dt_field(self, dt): + return dt.replace(microsecond=0, tzinfo=timezone.utc).isoformat() + + class ModelWithCustomEncoderSubclass(ModelWithCustomEncoder): + pass + model = ModelWithCustomEncoder(dt_field=datetime(2019, 1, 1, 8)) assert jsonable_encoder(model) == {"dt_field": "2019-01-01T08:00:00+00:00"} + subclass_model = ModelWithCustomEncoderSubclass(dt_field=datetime(2019, 1, 1, 8)) + assert jsonable_encoder(subclass_model) == {"dt_field": "2019-01-01T08:00:00+00:00"} + + +# TODO: remove when deprecating Pydantic v1 +@needs_pydanticv1 +def test_encode_custom_json_encoders_model_pydanticv1(): + class ModelWithCustomEncoder(BaseModel): + dt_field: datetime + class Config: + json_encoders = { + datetime: lambda dt: dt.replace( + microsecond=0, tzinfo=timezone.utc + ).isoformat() + } -def test_encode_custom_json_encoders_model_subclass(): - model = ModelWithCustomEncoderSubclass(dt_field=datetime(2019, 1, 1, 8)) + class ModelWithCustomEncoderSubclass(ModelWithCustomEncoder): + class Config: + pass + + model = ModelWithCustomEncoder(dt_field=datetime(2019, 1, 1, 8)) assert jsonable_encoder(model) == {"dt_field": "2019-01-01T08:00:00+00:00"} + subclass_model = ModelWithCustomEncoderSubclass(dt_field=datetime(2019, 1, 1, 8)) + assert jsonable_encoder(subclass_model) == {"dt_field": "2019-01-01T08:00:00+00:00"} def test_encode_model_with_config(): @@ -197,6 +205,7 @@ def test_encode_model_with_default(): } +@needs_pydanticv1 def test_custom_encoders(): class safe_datetime(datetime): pass @@ -227,19 +236,72 @@ def test_custom_enum_encoders(): assert encoded_instance == custom_enum_encoder(instance) -def test_encode_model_with_path(model_with_path): - if isinstance(model_with_path.path, PureWindowsPath): - expected = "\\foo\\bar" - else: - expected = "/foo/bar" - assert jsonable_encoder(model_with_path) == {"path": expected} +def test_encode_model_with_pure_path(): + class ModelWithPath(BaseModel): + path: PurePath + + if PYDANTIC_V2: + model_config = {"arbitrary_types_allowed": True} + else: + + class Config: + arbitrary_types_allowed = True + + obj = ModelWithPath(path=PurePath("/foo", "bar")) + assert jsonable_encoder(obj) == {"path": "/foo/bar"} + + +def test_encode_model_with_pure_posix_path(): + class ModelWithPath(BaseModel): + path: PurePosixPath + + if PYDANTIC_V2: + model_config = {"arbitrary_types_allowed": True} + else: + + class Config: + arbitrary_types_allowed = True + + obj = ModelWithPath(path=PurePosixPath("/foo", "bar")) + assert jsonable_encoder(obj) == {"path": "/foo/bar"} +def test_encode_model_with_pure_windows_path(): + class ModelWithPath(BaseModel): + path: PureWindowsPath + + if PYDANTIC_V2: + model_config = {"arbitrary_types_allowed": True} + else: + + class Config: + arbitrary_types_allowed = True + + obj = ModelWithPath(path=PureWindowsPath("/foo", "bar")) + assert jsonable_encoder(obj) == {"path": "\\foo\\bar"} + + +@needs_pydanticv1 def test_encode_root(): + class ModelWithRoot(BaseModel): + __root__: str + model = ModelWithRoot(__root__="Foo") assert jsonable_encoder(model) == "Foo" +@needs_pydanticv2 +def test_decimal_encoder_float(): + data = {"value": Decimal(1.23)} + assert jsonable_encoder(data) == {"value": 1.23} + + +@needs_pydanticv2 +def test_decimal_encoder_int(): + data = {"value": Decimal(2)} + assert jsonable_encoder(data) == {"value": 2} + + def test_encode_deque_encodes_child_models(): class Model(BaseModel): test: str diff --git a/tests/test_multi_body_errors.py b/tests/test_multi_body_errors.py index 96043aa35..aa989c612 100644 --- a/tests/test_multi_body_errors.py +++ b/tests/test_multi_body_errors.py @@ -1,8 +1,10 @@ from decimal import Decimal from typing import List +from dirty_equals import IsDict, IsOneOf from fastapi import FastAPI from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from pydantic import BaseModel, condecimal app = FastAPI() @@ -21,59 +23,115 @@ def save_item_no_body(item: List[Item]): client = TestClient(app) -single_error = { - "detail": [ - { - "ctx": {"limit_value": 0.0}, - "loc": ["body", 0, "age"], - "msg": "ensure this value is greater than 0", - "type": "value_error.number.not_gt", - } - ] -} - -multiple_errors = { - "detail": [ - { - "loc": ["body", 0, "name"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", 0, "age"], - "msg": "value is not a valid decimal", - "type": "type_error.decimal", - }, - { - "loc": ["body", 1, "name"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", 1, "age"], - "msg": "value is not a valid decimal", - "type": "type_error.decimal", - }, - ] -} - - def test_put_correct_body(): response = client.post("/items/", json=[{"name": "Foo", "age": 5}]) assert response.status_code == 200, response.text - assert response.json() == {"item": [{"name": "Foo", "age": 5}]} + assert response.json() == { + "item": [ + { + "name": "Foo", + "age": IsOneOf( + 5, + # TODO: remove when deprecating Pydantic v1 + "5", + ), + } + ] + } def test_jsonable_encoder_requiring_error(): response = client.post("/items/", json=[{"name": "Foo", "age": -1.0}]) assert response.status_code == 422, response.text - assert response.json() == single_error + assert response.json() == IsDict( + { + "detail": [ + { + "type": "greater_than", + "loc": ["body", 0, "age"], + "msg": "Input should be greater than 0", + "input": -1.0, + "ctx": {"gt": 0.0}, + "url": match_pydantic_error_url("greater_than"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "ctx": {"limit_value": 0.0}, + "loc": ["body", 0, "age"], + "msg": "ensure this value is greater than 0", + "type": "value_error.number.not_gt", + } + ] + } + ) def test_put_incorrect_body_multiple(): response = client.post("/items/", json=[{"age": "five"}, {"age": "six"}]) assert response.status_code == 422, response.text - assert response.json() == multiple_errors + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", 0, "name"], + "msg": "Field required", + "input": {"age": "five"}, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "decimal_parsing", + "loc": ["body", 0, "age"], + "msg": "Input should be a valid decimal", + "input": "five", + }, + { + "type": "missing", + "loc": ["body", 1, "name"], + "msg": "Field required", + "input": {"age": "six"}, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "decimal_parsing", + "loc": ["body", 1, "age"], + "msg": "Input should be a valid decimal", + "input": "six", + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", 0, "name"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", 0, "age"], + "msg": "value is not a valid decimal", + "type": "type_error.decimal", + }, + { + "loc": ["body", 1, "name"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", 1, "age"], + "msg": "value is not a valid decimal", + "type": "type_error.decimal", + }, + ] + } + ) def test_openapi_schema(): @@ -126,11 +184,23 @@ def test_openapi_schema(): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, - "age": { - "title": "Age", - "exclusiveMinimum": 0.0, - "type": "number", - }, + "age": IsDict( + { + "title": "Age", + "anyOf": [ + {"exclusiveMinimum": 0.0, "type": "number"}, + {"type": "string"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Age", + "exclusiveMinimum": 0.0, + "type": "number", + } + ), }, }, "ValidationError": { diff --git a/tests/test_multi_query_errors.py b/tests/test_multi_query_errors.py index c1f0678d0..470a35808 100644 --- a/tests/test_multi_query_errors.py +++ b/tests/test_multi_query_errors.py @@ -1,7 +1,9 @@ from typing import List +from dirty_equals import IsDict from fastapi import FastAPI, Query from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url app = FastAPI() @@ -14,22 +16,6 @@ def read_items(q: List[int] = Query(default=None)): client = TestClient(app) -multiple_errors = { - "detail": [ - { - "loc": ["query", "q", 0], - "msg": "value is not a valid integer", - "type": "type_error.integer", - }, - { - "loc": ["query", "q", 1], - "msg": "value is not a valid integer", - "type": "type_error.integer", - }, - ] -} - - def test_multi_query(): response = client.get("/items/?q=5&q=6") assert response.status_code == 200, response.text @@ -39,7 +25,42 @@ def test_multi_query(): def test_multi_query_incorrect(): response = client.get("/items/?q=five&q=six") assert response.status_code == 422, response.text - assert response.json() == multiple_errors + assert response.json() == IsDict( + { + "detail": [ + { + "type": "int_parsing", + "loc": ["query", "q", 0], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "five", + "url": match_pydantic_error_url("int_parsing"), + }, + { + "type": "int_parsing", + "loc": ["query", "q", 1], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "six", + "url": match_pydantic_error_url("int_parsing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "q", 0], + "msg": "value is not a valid integer", + "type": "type_error.integer", + }, + { + "loc": ["query", "q", 1], + "msg": "value is not a valid integer", + "type": "type_error.integer", + }, + ] + } + ) def test_openapi_schema(): diff --git a/tests/test_openapi_query_parameter_extension.py b/tests/test_openapi_query_parameter_extension.py index 6f62e6726..dc7147c71 100644 --- a/tests/test_openapi_query_parameter_extension.py +++ b/tests/test_openapi_query_parameter_extension.py @@ -1,5 +1,6 @@ from typing import Optional +from dirty_equals import IsDict from fastapi import FastAPI from fastapi.testclient import TestClient @@ -52,11 +53,21 @@ def test_openapi(): "parameters": [ { "required": False, - "schema": { - "title": "Standard Query Param", - "type": "integer", - "default": 50, - }, + "schema": IsDict( + { + "anyOf": [{"type": "integer"}, {"type": "null"}], + "default": 50, + "title": "Standard Query Param", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Standard Query Param", + "type": "integer", + "default": 50, + } + ), "name": "standard_query_param", "in": "query", }, diff --git a/tests/test_openapi_servers.py b/tests/test_openapi_servers.py index 11cd795ac..8697c8438 100644 --- a/tests/test_openapi_servers.py +++ b/tests/test_openapi_servers.py @@ -1,3 +1,4 @@ +from dirty_equals import IsOneOf from fastapi import FastAPI from fastapi.testclient import TestClient @@ -35,10 +36,20 @@ def test_openapi_schema(): "servers": [ {"url": "/", "description": "Default, relative server"}, { - "url": "http://staging.localhost.tiangolo.com:8000", + "url": IsOneOf( + "http://staging.localhost.tiangolo.com:8000/", + # TODO: remove when deprecating Pydantic v1 + "http://staging.localhost.tiangolo.com:8000", + ), "description": "Staging but actually localhost still", }, - {"url": "https://prod.example.com"}, + { + "url": IsOneOf( + "https://prod.example.com/", + # TODO: remove when deprecating Pydantic v1 + "https://prod.example.com", + ) + }, ], "paths": { "/foo": { diff --git a/tests/test_params_repr.py b/tests/test_params_repr.py index d8dca1ea4..bfc7bed09 100644 --- a/tests/test_params_repr.py +++ b/tests/test_params_repr.py @@ -1,6 +1,6 @@ from typing import Any, List -import pytest +from dirty_equals import IsOneOf from fastapi.params import Body, Cookie, Depends, Header, Param, Path, Query test_data: List[Any] = ["teststr", None, ..., 1, []] @@ -10,34 +10,137 @@ def get_user(): return {} # pragma: no cover -@pytest.fixture(scope="function", params=test_data) -def params(request): - return request.param +def test_param_repr_str(): + assert repr(Param("teststr")) == "Param(teststr)" -def test_param_repr(params): - assert repr(Param(params)) == "Param(" + str(params) + ")" +def test_param_repr_none(): + assert repr(Param(None)) == "Param(None)" + + +def test_param_repr_ellipsis(): + assert repr(Param(...)) == IsOneOf( + "Param(PydanticUndefined)", + # TODO: remove when deprecating Pydantic v1 + "Param(Ellipsis)", + ) + + +def test_param_repr_number(): + assert repr(Param(1)) == "Param(1)" + + +def test_param_repr_list(): + assert repr(Param([])) == "Param([])" def test_path_repr(): - assert repr(Path()) == "Path(Ellipsis)" - assert repr(Path(...)) == "Path(Ellipsis)" + assert repr(Path()) == IsOneOf( + "Path(PydanticUndefined)", + # TODO: remove when deprecating Pydantic v1 + "Path(Ellipsis)", + ) + assert repr(Path(...)) == IsOneOf( + "Path(PydanticUndefined)", + # TODO: remove when deprecating Pydantic v1 + "Path(Ellipsis)", + ) -def test_query_repr(params): - assert repr(Query(params)) == "Query(" + str(params) + ")" +def test_query_repr_str(): + assert repr(Query("teststr")) == "Query(teststr)" -def test_header_repr(params): - assert repr(Header(params)) == "Header(" + str(params) + ")" +def test_query_repr_none(): + assert repr(Query(None)) == "Query(None)" + + +def test_query_repr_ellipsis(): + assert repr(Query(...)) == IsOneOf( + "Query(PydanticUndefined)", + # TODO: remove when deprecating Pydantic v1 + "Query(Ellipsis)", + ) + + +def test_query_repr_number(): + assert repr(Query(1)) == "Query(1)" + + +def test_query_repr_list(): + assert repr(Query([])) == "Query([])" + + +def test_header_repr_str(): + assert repr(Header("teststr")) == "Header(teststr)" + + +def test_header_repr_none(): + assert repr(Header(None)) == "Header(None)" + + +def test_header_repr_ellipsis(): + assert repr(Header(...)) == IsOneOf( + "Header(PydanticUndefined)", + # TODO: remove when deprecating Pydantic v1 + "Header(Ellipsis)", + ) + + +def test_header_repr_number(): + assert repr(Header(1)) == "Header(1)" + + +def test_header_repr_list(): + assert repr(Header([])) == "Header([])" + + +def test_cookie_repr_str(): + assert repr(Cookie("teststr")) == "Cookie(teststr)" + + +def test_cookie_repr_none(): + assert repr(Cookie(None)) == "Cookie(None)" + + +def test_cookie_repr_ellipsis(): + assert repr(Cookie(...)) == IsOneOf( + "Cookie(PydanticUndefined)", + # TODO: remove when deprecating Pydantic v1 + "Cookie(Ellipsis)", + ) + + +def test_cookie_repr_number(): + assert repr(Cookie(1)) == "Cookie(1)" + + +def test_cookie_repr_list(): + assert repr(Cookie([])) == "Cookie([])" + + +def test_body_repr_str(): + assert repr(Body("teststr")) == "Body(teststr)" + + +def test_body_repr_none(): + assert repr(Body(None)) == "Body(None)" + + +def test_body_repr_ellipsis(): + assert repr(Body(...)) == IsOneOf( + "Body(PydanticUndefined)", + # TODO: remove when deprecating Pydantic v1 + "Body(Ellipsis)", + ) -def test_cookie_repr(params): - assert repr(Cookie(params)) == "Cookie(" + str(params) + ")" +def test_body_repr_number(): + assert repr(Body(1)) == "Body(1)" -def test_body_repr(params): - assert repr(Body(params)) == "Body(" + str(params) + ")" +def test_body_repr_list(): + assert repr(Body([])) == "Body([])" def test_depends_repr(): diff --git a/tests/test_path.py b/tests/test_path.py index 03b93623a..848b245e2 100644 --- a/tests/test_path.py +++ b/tests/test_path.py @@ -1,5 +1,6 @@ -import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from .main import app @@ -18,235 +19,1259 @@ def test_nonexistent(): assert response.json() == {"detail": "Not Found"} -response_not_valid_bool = { - "detail": [ +def test_path_foobar(): + response = client.get("/path/foobar") + assert response.status_code == 200 + assert response.json() == "foobar" + + +def test_path_str_foobar(): + response = client.get("/path/str/foobar") + assert response.status_code == 200 + assert response.json() == "foobar" + + +def test_path_str_42(): + response = client.get("/path/str/42") + assert response.status_code == 200 + assert response.json() == "42" + + +def test_path_str_True(): + response = client.get("/path/str/True") + assert response.status_code == 200 + assert response.json() == "True" + + +def test_path_int_foobar(): + response = client.get("/path/int/foobar") + assert response.status_code == 422 + assert response.json() == IsDict( { - "loc": ["path", "item_id"], - "msg": "value could not be parsed to a boolean", - "type": "type_error.bool", + "detail": [ + { + "type": "int_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "foobar", + "url": match_pydantic_error_url("int_parsing"), + } + ] } - ] -} + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] + } + ) -response_not_valid_int = { - "detail": [ + +def test_path_int_True(): + response = client.get("/path/int/True") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "int_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "True", + "url": match_pydantic_error_url("int_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 { - "loc": ["path", "item_id"], - "msg": "value is not a valid integer", - "type": "type_error.integer", + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] } - ] -} + ) + + +def test_path_int_42(): + response = client.get("/path/int/42") + assert response.status_code == 200 + assert response.json() == 42 + -response_not_valid_float = { - "detail": [ +def test_path_int_42_5(): + response = client.get("/path/int/42.5") + assert response.status_code == 422 + assert response.json() == IsDict( { - "loc": ["path", "item_id"], - "msg": "value is not a valid float", - "type": "type_error.float", + "detail": [ + { + "type": "int_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "42.5", + "url": match_pydantic_error_url("int_parsing"), + } + ] } - ] -} + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] + } + ) -response_at_least_3 = { - "detail": [ + +def test_path_float_foobar(): + response = client.get("/path/float/foobar") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "float_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid number, unable to parse string as a number", + "input": "foobar", + "url": match_pydantic_error_url("float_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 { - "loc": ["path", "item_id"], - "msg": "ensure this value has at least 3 characters", - "type": "value_error.any_str.min_length", - "ctx": {"limit_value": 3}, + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "value is not a valid float", + "type": "type_error.float", + } + ] } - ] -} + ) -response_at_least_2 = { - "detail": [ +def test_path_float_True(): + response = client.get("/path/float/True") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "float_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid number, unable to parse string as a number", + "input": "True", + "url": match_pydantic_error_url("float_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 { - "loc": ["path", "item_id"], - "msg": "ensure this value has at least 2 characters", - "type": "value_error.any_str.min_length", - "ctx": {"limit_value": 2}, + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "value is not a valid float", + "type": "type_error.float", + } + ] } - ] -} + ) + +def test_path_float_42(): + response = client.get("/path/float/42") + assert response.status_code == 200 + assert response.json() == 42 -response_maximum_3 = { - "detail": [ + +def test_path_float_42_5(): + response = client.get("/path/float/42.5") + assert response.status_code == 200 + assert response.json() == 42.5 + + +def test_path_bool_foobar(): + response = client.get("/path/bool/foobar") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "bool_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid boolean, unable to interpret input", + "input": "foobar", + "url": match_pydantic_error_url("bool_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 { - "loc": ["path", "item_id"], - "msg": "ensure this value has at most 3 characters", - "type": "value_error.any_str.max_length", - "ctx": {"limit_value": 3}, + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "value could not be parsed to a boolean", + "type": "type_error.bool", + } + ] } - ] -} + ) + +def test_path_bool_True(): + response = client.get("/path/bool/True") + assert response.status_code == 200 + assert response.json() is True -response_greater_than_3 = { - "detail": [ + +def test_path_bool_42(): + response = client.get("/path/bool/42") + assert response.status_code == 422 + assert response.json() == IsDict( { - "loc": ["path", "item_id"], - "msg": "ensure this value is greater than 3", - "type": "value_error.number.not_gt", - "ctx": {"limit_value": 3}, + "detail": [ + { + "type": "bool_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid boolean, unable to interpret input", + "input": "42", + "url": match_pydantic_error_url("bool_parsing"), + } + ] } - ] -} + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "value could not be parsed to a boolean", + "type": "type_error.bool", + } + ] + } + ) + + +def test_path_bool_42_5(): + response = client.get("/path/bool/42.5") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "bool_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid boolean, unable to interpret input", + "input": "42.5", + "url": match_pydantic_error_url("bool_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "value could not be parsed to a boolean", + "type": "type_error.bool", + } + ] + } + ) + + +def test_path_bool_1(): + response = client.get("/path/bool/1") + assert response.status_code == 200 + assert response.json() is True + + +def test_path_bool_0(): + response = client.get("/path/bool/0") + assert response.status_code == 200 + assert response.json() is False + + +def test_path_bool_true(): + response = client.get("/path/bool/true") + assert response.status_code == 200 + assert response.json() is True + + +def test_path_bool_False(): + response = client.get("/path/bool/False") + assert response.status_code == 200 + assert response.json() is False + +def test_path_bool_false(): + response = client.get("/path/bool/false") + assert response.status_code == 200 + assert response.json() is False -response_greater_than_0 = { - "detail": [ + +def test_path_param_foo(): + response = client.get("/path/param/foo") + assert response.status_code == 200 + assert response.json() == "foo" + + +def test_path_param_minlength_foo(): + response = client.get("/path/param-minlength/foo") + assert response.status_code == 200 + assert response.json() == "foo" + + +def test_path_param_minlength_fo(): + response = client.get("/path/param-minlength/fo") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "string_too_short", + "loc": ["path", "item_id"], + "msg": "String should have at least 3 characters", + "input": "fo", + "ctx": {"min_length": 3}, + "url": match_pydantic_error_url("string_too_short"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "ensure this value has at least 3 characters", + "type": "value_error.any_str.min_length", + "ctx": {"limit_value": 3}, + } + ] + } + ) + + +def test_path_param_maxlength_foo(): + response = client.get("/path/param-maxlength/foo") + assert response.status_code == 200 + assert response.json() == "foo" + + +def test_path_param_maxlength_foobar(): + response = client.get("/path/param-maxlength/foobar") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "string_too_long", + "loc": ["path", "item_id"], + "msg": "String should have at most 3 characters", + "input": "foobar", + "ctx": {"max_length": 3}, + "url": match_pydantic_error_url("string_too_long"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "ensure this value has at most 3 characters", + "type": "value_error.any_str.max_length", + "ctx": {"limit_value": 3}, + } + ] + } + ) + + +def test_path_param_min_maxlength_foo(): + response = client.get("/path/param-min_maxlength/foo") + assert response.status_code == 200 + assert response.json() == "foo" + + +def test_path_param_min_maxlength_foobar(): + response = client.get("/path/param-min_maxlength/foobar") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "string_too_long", + "loc": ["path", "item_id"], + "msg": "String should have at most 3 characters", + "input": "foobar", + "ctx": {"max_length": 3}, + "url": match_pydantic_error_url("string_too_long"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "ensure this value has at most 3 characters", + "type": "value_error.any_str.max_length", + "ctx": {"limit_value": 3}, + } + ] + } + ) + + +def test_path_param_min_maxlength_f(): + response = client.get("/path/param-min_maxlength/f") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "string_too_short", + "loc": ["path", "item_id"], + "msg": "String should have at least 2 characters", + "input": "f", + "ctx": {"min_length": 2}, + "url": match_pydantic_error_url("string_too_short"), + } + ] + } + ) | IsDict( + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "ensure this value has at least 2 characters", + "type": "value_error.any_str.min_length", + "ctx": {"limit_value": 2}, + } + ] + } + ) + + +def test_path_param_gt_42(): + response = client.get("/path/param-gt/42") + assert response.status_code == 200 + assert response.json() == 42 + + +def test_path_param_gt_2(): + response = client.get("/path/param-gt/2") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "greater_than", + "loc": ["path", "item_id"], + "msg": "Input should be greater than 3", + "input": "2", + "ctx": {"gt": 3.0}, + "url": match_pydantic_error_url("greater_than"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "ensure this value is greater than 3", + "type": "value_error.number.not_gt", + "ctx": {"limit_value": 3}, + } + ] + } + ) + + +def test_path_param_gt0_0_05(): + response = client.get("/path/param-gt0/0.05") + assert response.status_code == 200 + assert response.json() == 0.05 + + +def test_path_param_gt0_0(): + response = client.get("/path/param-gt0/0") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "greater_than", + "loc": ["path", "item_id"], + "msg": "Input should be greater than 0", + "input": "0", + "ctx": {"gt": 0.0}, + "url": match_pydantic_error_url("greater_than"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "ensure this value is greater than 0", + "type": "value_error.number.not_gt", + "ctx": {"limit_value": 0}, + } + ] + } + ) + + +def test_path_param_ge_42(): + response = client.get("/path/param-ge/42") + assert response.status_code == 200 + assert response.json() == 42 + + +def test_path_param_ge_3(): + response = client.get("/path/param-ge/3") + assert response.status_code == 200 + assert response.json() == 3 + + +def test_path_param_ge_2(): + response = client.get("/path/param-ge/2") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "greater_than_equal", + "loc": ["path", "item_id"], + "msg": "Input should be greater than or equal to 3", + "input": "2", + "ctx": {"ge": 3.0}, + "url": match_pydantic_error_url("greater_than_equal"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "ensure this value is greater than or equal to 3", + "type": "value_error.number.not_ge", + "ctx": {"limit_value": 3}, + } + ] + } + ) + + +def test_path_param_lt_42(): + response = client.get("/path/param-lt/42") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "less_than", + "loc": ["path", "item_id"], + "msg": "Input should be less than 3", + "input": "42", + "ctx": {"lt": 3.0}, + "url": match_pydantic_error_url("less_than"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "ensure this value is less than 3", + "type": "value_error.number.not_lt", + "ctx": {"limit_value": 3}, + } + ] + } + ) + + +def test_path_param_lt_2(): + response = client.get("/path/param-lt/2") + assert response.status_code == 200 + assert response.json() == 2 + + +def test_path_param_lt0__1(): + response = client.get("/path/param-lt0/-1") + assert response.status_code == 200 + assert response.json() == -1 + + +def test_path_param_lt0_0(): + response = client.get("/path/param-lt0/0") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "less_than", + "loc": ["path", "item_id"], + "msg": "Input should be less than 0", + "input": "0", + "ctx": {"lt": 0.0}, + "url": match_pydantic_error_url("less_than"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "ensure this value is less than 0", + "type": "value_error.number.not_lt", + "ctx": {"limit_value": 0}, + } + ] + } + ) + + +def test_path_param_le_42(): + response = client.get("/path/param-le/42") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "less_than_equal", + "loc": ["path", "item_id"], + "msg": "Input should be less than or equal to 3", + "input": "42", + "ctx": {"le": 3.0}, + "url": match_pydantic_error_url("less_than_equal"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 { - "loc": ["path", "item_id"], - "msg": "ensure this value is greater than 0", - "type": "value_error.number.not_gt", - "ctx": {"limit_value": 0}, + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "ensure this value is less than or equal to 3", + "type": "value_error.number.not_le", + "ctx": {"limit_value": 3}, + } + ] } - ] -} + ) -response_greater_than_1 = { - "detail": [ +def test_path_param_le_3(): + response = client.get("/path/param-le/3") + assert response.status_code == 200 + assert response.json() == 3 + + +def test_path_param_le_2(): + response = client.get("/path/param-le/2") + assert response.status_code == 200 + assert response.json() == 2 + + +def test_path_param_lt_gt_2(): + response = client.get("/path/param-lt-gt/2") + assert response.status_code == 200 + assert response.json() == 2 + + +def test_path_param_lt_gt_4(): + response = client.get("/path/param-lt-gt/4") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "less_than", + "loc": ["path", "item_id"], + "msg": "Input should be less than 3", + "input": "4", + "ctx": {"lt": 3.0}, + "url": match_pydantic_error_url("less_than"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "ensure this value is less than 3", + "type": "value_error.number.not_lt", + "ctx": {"limit_value": 3}, + } + ] + } + ) + + +def test_path_param_lt_gt_0(): + response = client.get("/path/param-lt-gt/0") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "greater_than", + "loc": ["path", "item_id"], + "msg": "Input should be greater than 1", + "input": "0", + "ctx": {"gt": 1.0}, + "url": match_pydantic_error_url("greater_than"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "ensure this value is greater than 1", + "type": "value_error.number.not_gt", + "ctx": {"limit_value": 1}, + } + ] + } + ) + + +def test_path_param_le_ge_2(): + response = client.get("/path/param-le-ge/2") + assert response.status_code == 200 + assert response.json() == 2 + + +def test_path_param_le_ge_1(): + response = client.get("/path/param-le-ge/1") + assert response.status_code == 200 + + +def test_path_param_le_ge_3(): + response = client.get("/path/param-le-ge/3") + assert response.status_code == 200 + assert response.json() == 3 + + +def test_path_param_le_ge_4(): + response = client.get("/path/param-le-ge/4") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "less_than_equal", + "loc": ["path", "item_id"], + "msg": "Input should be less than or equal to 3", + "input": "4", + "ctx": {"le": 3.0}, + "url": match_pydantic_error_url("less_than_equal"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "ensure this value is less than or equal to 3", + "type": "value_error.number.not_le", + "ctx": {"limit_value": 3}, + } + ] + } + ) + + +def test_path_param_lt_int_2(): + response = client.get("/path/param-lt-int/2") + assert response.status_code == 200 + assert response.json() == 2 + + +def test_path_param_lt_int_42(): + response = client.get("/path/param-lt-int/42") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "less_than", + "loc": ["path", "item_id"], + "msg": "Input should be less than 3", + "input": "42", + "ctx": {"lt": 3}, + "url": match_pydantic_error_url("less_than"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "ensure this value is less than 3", + "type": "value_error.number.not_lt", + "ctx": {"limit_value": 3}, + } + ] + } + ) + + +def test_path_param_lt_int_2_7(): + response = client.get("/path/param-lt-int/2.7") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "int_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "2.7", + "url": match_pydantic_error_url("int_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] + } + ) + + +def test_path_param_gt_int_42(): + response = client.get("/path/param-gt-int/42") + assert response.status_code == 200 + assert response.json() == 42 + + +def test_path_param_gt_int_2(): + response = client.get("/path/param-gt-int/2") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "greater_than", + "loc": ["path", "item_id"], + "msg": "Input should be greater than 3", + "input": "2", + "ctx": {"gt": 3}, + "url": match_pydantic_error_url("greater_than"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "ensure this value is greater than 3", + "type": "value_error.number.not_gt", + "ctx": {"limit_value": 3}, + } + ] + } + ) + + +def test_path_param_gt_int_2_7(): + response = client.get("/path/param-gt-int/2.7") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "int_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "2.7", + "url": match_pydantic_error_url("int_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] + } + ) + + +def test_path_param_le_int_42(): + response = client.get("/path/param-le-int/42") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "less_than_equal", + "loc": ["path", "item_id"], + "msg": "Input should be less than or equal to 3", + "input": "42", + "ctx": {"le": 3}, + "url": match_pydantic_error_url("less_than_equal"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "ensure this value is less than or equal to 3", + "type": "value_error.number.not_le", + "ctx": {"limit_value": 3}, + } + ] + } + ) + + +def test_path_param_le_int_3(): + response = client.get("/path/param-le-int/3") + assert response.status_code == 200 + assert response.json() == 3 + + +def test_path_param_le_int_2(): + response = client.get("/path/param-le-int/2") + assert response.status_code == 200 + assert response.json() == 2 + + +def test_path_param_le_int_2_7(): + response = client.get("/path/param-le-int/2.7") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "int_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "2.7", + "url": match_pydantic_error_url("int_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] + } + ) + + +def test_path_param_ge_int_42(): + response = client.get("/path/param-ge-int/42") + assert response.status_code == 200 + assert response.json() == 42 + + +def test_path_param_ge_int_3(): + response = client.get("/path/param-ge-int/3") + assert response.status_code == 200 + assert response.json() == 3 + + +def test_path_param_ge_int_2(): + response = client.get("/path/param-ge-int/2") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "greater_than_equal", + "loc": ["path", "item_id"], + "msg": "Input should be greater than or equal to 3", + "input": "2", + "ctx": {"ge": 3}, + "url": match_pydantic_error_url("greater_than_equal"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "ensure this value is greater than or equal to 3", + "type": "value_error.number.not_ge", + "ctx": {"limit_value": 3}, + } + ] + } + ) + + +def test_path_param_ge_int_2_7(): + response = client.get("/path/param-ge-int/2.7") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "int_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "2.7", + "url": match_pydantic_error_url("int_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] + } + ) + + +def test_path_param_lt_gt_int_2(): + response = client.get("/path/param-lt-gt-int/2") + assert response.status_code == 200 + assert response.json() == 2 + + +def test_path_param_lt_gt_int_4(): + response = client.get("/path/param-lt-gt-int/4") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "less_than", + "loc": ["path", "item_id"], + "msg": "Input should be less than 3", + "input": "4", + "ctx": {"lt": 3}, + "url": match_pydantic_error_url("less_than"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "ensure this value is less than 3", + "type": "value_error.number.not_lt", + "ctx": {"limit_value": 3}, + } + ] + } + ) + + +def test_path_param_lt_gt_int_0(): + response = client.get("/path/param-lt-gt-int/0") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "greater_than", + "loc": ["path", "item_id"], + "msg": "Input should be greater than 1", + "input": "0", + "ctx": {"gt": 1}, + "url": match_pydantic_error_url("greater_than"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "ensure this value is greater than 1", + "type": "value_error.number.not_gt", + "ctx": {"limit_value": 1}, + } + ] + } + ) + + +def test_path_param_lt_gt_int_2_7(): + response = client.get("/path/param-lt-gt-int/2.7") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "int_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "2.7", + "url": match_pydantic_error_url("int_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] + } + ) + + +def test_path_param_le_ge_int_2(): + response = client.get("/path/param-le-ge-int/2") + assert response.status_code == 200 + assert response.json() == 2 + + +def test_path_param_le_ge_int_1(): + response = client.get("/path/param-le-ge-int/1") + assert response.status_code == 200 + assert response.json() == 1 + + +def test_path_param_le_ge_int_3(): + response = client.get("/path/param-le-ge-int/3") + assert response.status_code == 200 + assert response.json() == 3 + + +def test_path_param_le_ge_int_4(): + response = client.get("/path/param-le-ge-int/4") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "less_than_equal", + "loc": ["path", "item_id"], + "msg": "Input should be less than or equal to 3", + "input": "4", + "ctx": {"le": 3}, + "url": match_pydantic_error_url("less_than_equal"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "ensure this value is less than or equal to 3", + "type": "value_error.number.not_le", + "ctx": {"limit_value": 3}, + } + ] + } + ) + + +def test_path_param_le_ge_int_2_7(): + response = client.get("/path/param-le-ge-int/2.7") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "int_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "2.7", + "url": match_pydantic_error_url("int_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 { - "loc": ["path", "item_id"], - "msg": "ensure this value is greater than 1", - "type": "value_error.number.not_gt", - "ctx": {"limit_value": 1}, + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] } - ] -} - - -response_greater_than_equal_3 = { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "ensure this value is greater than or equal to 3", - "type": "value_error.number.not_ge", - "ctx": {"limit_value": 3}, - } - ] -} - - -response_less_than_3 = { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "ensure this value is less than 3", - "type": "value_error.number.not_lt", - "ctx": {"limit_value": 3}, - } - ] -} - - -response_less_than_0 = { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "ensure this value is less than 0", - "type": "value_error.number.not_lt", - "ctx": {"limit_value": 0}, - } - ] -} - - -response_less_than_equal_3 = { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "ensure this value is less than or equal to 3", - "type": "value_error.number.not_le", - "ctx": {"limit_value": 3}, - } - ] -} - - -@pytest.mark.parametrize( - "path,expected_status,expected_response", - [ - ("/path/foobar", 200, "foobar"), - ("/path/str/foobar", 200, "foobar"), - ("/path/str/42", 200, "42"), - ("/path/str/True", 200, "True"), - ("/path/int/foobar", 422, response_not_valid_int), - ("/path/int/True", 422, response_not_valid_int), - ("/path/int/42", 200, 42), - ("/path/int/42.5", 422, response_not_valid_int), - ("/path/float/foobar", 422, response_not_valid_float), - ("/path/float/True", 422, response_not_valid_float), - ("/path/float/42", 200, 42), - ("/path/float/42.5", 200, 42.5), - ("/path/bool/foobar", 422, response_not_valid_bool), - ("/path/bool/True", 200, True), - ("/path/bool/42", 422, response_not_valid_bool), - ("/path/bool/42.5", 422, response_not_valid_bool), - ("/path/bool/1", 200, True), - ("/path/bool/0", 200, False), - ("/path/bool/true", 200, True), - ("/path/bool/False", 200, False), - ("/path/bool/false", 200, False), - ("/path/param/foo", 200, "foo"), - ("/path/param-minlength/foo", 200, "foo"), - ("/path/param-minlength/fo", 422, response_at_least_3), - ("/path/param-maxlength/foo", 200, "foo"), - ("/path/param-maxlength/foobar", 422, response_maximum_3), - ("/path/param-min_maxlength/foo", 200, "foo"), - ("/path/param-min_maxlength/foobar", 422, response_maximum_3), - ("/path/param-min_maxlength/f", 422, response_at_least_2), - ("/path/param-gt/42", 200, 42), - ("/path/param-gt/2", 422, response_greater_than_3), - ("/path/param-gt0/0.05", 200, 0.05), - ("/path/param-gt0/0", 422, response_greater_than_0), - ("/path/param-ge/42", 200, 42), - ("/path/param-ge/3", 200, 3), - ("/path/param-ge/2", 422, response_greater_than_equal_3), - ("/path/param-lt/42", 422, response_less_than_3), - ("/path/param-lt/2", 200, 2), - ("/path/param-lt0/-1", 200, -1), - ("/path/param-lt0/0", 422, response_less_than_0), - ("/path/param-le/42", 422, response_less_than_equal_3), - ("/path/param-le/3", 200, 3), - ("/path/param-le/2", 200, 2), - ("/path/param-lt-gt/2", 200, 2), - ("/path/param-lt-gt/4", 422, response_less_than_3), - ("/path/param-lt-gt/0", 422, response_greater_than_1), - ("/path/param-le-ge/2", 200, 2), - ("/path/param-le-ge/1", 200, 1), - ("/path/param-le-ge/3", 200, 3), - ("/path/param-le-ge/4", 422, response_less_than_equal_3), - ("/path/param-lt-int/2", 200, 2), - ("/path/param-lt-int/42", 422, response_less_than_3), - ("/path/param-lt-int/2.7", 422, response_not_valid_int), - ("/path/param-gt-int/42", 200, 42), - ("/path/param-gt-int/2", 422, response_greater_than_3), - ("/path/param-gt-int/2.7", 422, response_not_valid_int), - ("/path/param-le-int/42", 422, response_less_than_equal_3), - ("/path/param-le-int/3", 200, 3), - ("/path/param-le-int/2", 200, 2), - ("/path/param-le-int/2.7", 422, response_not_valid_int), - ("/path/param-ge-int/42", 200, 42), - ("/path/param-ge-int/3", 200, 3), - ("/path/param-ge-int/2", 422, response_greater_than_equal_3), - ("/path/param-ge-int/2.7", 422, response_not_valid_int), - ("/path/param-lt-gt-int/2", 200, 2), - ("/path/param-lt-gt-int/4", 422, response_less_than_3), - ("/path/param-lt-gt-int/0", 422, response_greater_than_1), - ("/path/param-lt-gt-int/2.7", 422, response_not_valid_int), - ("/path/param-le-ge-int/2", 200, 2), - ("/path/param-le-ge-int/1", 200, 1), - ("/path/param-le-ge-int/3", 200, 3), - ("/path/param-le-ge-int/4", 422, response_less_than_equal_3), - ("/path/param-le-ge-int/2.7", 422, response_not_valid_int), - ], -) -def test_get_path(path, expected_status, expected_response): - response = client.get(path) - assert response.status_code == expected_status - assert response.json() == expected_response + ) diff --git a/tests/test_query.py b/tests/test_query.py index 0c73eb665..5bb9995d6 100644 --- a/tests/test_query.py +++ b/tests/test_query.py @@ -1,62 +1,410 @@ -import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from .main import app client = TestClient(app) -response_missing = { - "detail": [ - { - "loc": ["query", "query"], - "msg": "field required", - "type": "value_error.missing", - } - ] -} - -response_not_valid_int = { - "detail": [ - { - "loc": ["query", "query"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] -} - - -@pytest.mark.parametrize( - "path,expected_status,expected_response", - [ - ("/query", 422, response_missing), - ("/query?query=baz", 200, "foo bar baz"), - ("/query?not_declared=baz", 422, response_missing), - ("/query/optional", 200, "foo bar"), - ("/query/optional?query=baz", 200, "foo bar baz"), - ("/query/optional?not_declared=baz", 200, "foo bar"), - ("/query/int", 422, response_missing), - ("/query/int?query=42", 200, "foo bar 42"), - ("/query/int?query=42.5", 422, response_not_valid_int), - ("/query/int?query=baz", 422, response_not_valid_int), - ("/query/int?not_declared=baz", 422, response_missing), - ("/query/int/optional", 200, "foo bar"), - ("/query/int/optional?query=50", 200, "foo bar 50"), - ("/query/int/optional?query=foo", 422, response_not_valid_int), - ("/query/int/default", 200, "foo bar 10"), - ("/query/int/default?query=50", 200, "foo bar 50"), - ("/query/int/default?query=foo", 422, response_not_valid_int), - ("/query/param", 200, "foo bar"), - ("/query/param?query=50", 200, "foo bar 50"), - ("/query/param-required", 422, response_missing), - ("/query/param-required?query=50", 200, "foo bar 50"), - ("/query/param-required/int", 422, response_missing), - ("/query/param-required/int?query=50", 200, "foo bar 50"), - ("/query/param-required/int?query=foo", 422, response_not_valid_int), - ("/query/frozenset/?query=1&query=1&query=2", 200, "1,2"), - ], -) -def test_get_path(path, expected_status, expected_response): - response = client.get(path) - assert response.status_code == expected_status - assert response.json() == expected_response + +def test_query(): + response = client.get("/query") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "query"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "query"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_query_query_baz(): + response = client.get("/query?query=baz") + assert response.status_code == 200 + assert response.json() == "foo bar baz" + + +def test_query_not_declared_baz(): + response = client.get("/query?not_declared=baz") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "query"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "query"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_query_optional(): + response = client.get("/query/optional") + assert response.status_code == 200 + assert response.json() == "foo bar" + + +def test_query_optional_query_baz(): + response = client.get("/query/optional?query=baz") + assert response.status_code == 200 + assert response.json() == "foo bar baz" + + +def test_query_optional_not_declared_baz(): + response = client.get("/query/optional?not_declared=baz") + assert response.status_code == 200 + assert response.json() == "foo bar" + + +def test_query_int(): + response = client.get("/query/int") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "query"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "query"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_query_int_query_42(): + response = client.get("/query/int?query=42") + assert response.status_code == 200 + assert response.json() == "foo bar 42" + + +def test_query_int_query_42_5(): + response = client.get("/query/int?query=42.5") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "int_parsing", + "loc": ["query", "query"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "42.5", + "url": match_pydantic_error_url("int_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "query"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] + } + ) + + +def test_query_int_query_baz(): + response = client.get("/query/int?query=baz") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "int_parsing", + "loc": ["query", "query"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "baz", + "url": match_pydantic_error_url("int_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "query"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] + } + ) + + +def test_query_int_not_declared_baz(): + response = client.get("/query/int?not_declared=baz") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "query"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "query"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_query_int_optional(): + response = client.get("/query/int/optional") + assert response.status_code == 200 + assert response.json() == "foo bar" + + +def test_query_int_optional_query_50(): + response = client.get("/query/int/optional?query=50") + assert response.status_code == 200 + assert response.json() == "foo bar 50" + + +def test_query_int_optional_query_foo(): + response = client.get("/query/int/optional?query=foo") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "int_parsing", + "loc": ["query", "query"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "foo", + "url": match_pydantic_error_url("int_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "query"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] + } + ) + + +def test_query_int_default(): + response = client.get("/query/int/default") + assert response.status_code == 200 + assert response.json() == "foo bar 10" + + +def test_query_int_default_query_50(): + response = client.get("/query/int/default?query=50") + assert response.status_code == 200 + assert response.json() == "foo bar 50" + + +def test_query_int_default_query_foo(): + response = client.get("/query/int/default?query=foo") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "int_parsing", + "loc": ["query", "query"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "foo", + "url": match_pydantic_error_url("int_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "query"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] + } + ) + + +def test_query_param(): + response = client.get("/query/param") + assert response.status_code == 200 + assert response.json() == "foo bar" + + +def test_query_param_query_50(): + response = client.get("/query/param?query=50") + assert response.status_code == 200 + assert response.json() == "foo bar 50" + + +def test_query_param_required(): + response = client.get("/query/param-required") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "query"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "query"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_query_param_required_query_50(): + response = client.get("/query/param-required?query=50") + assert response.status_code == 200 + assert response.json() == "foo bar 50" + + +def test_query_param_required_int(): + response = client.get("/query/param-required/int") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "query"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "query"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_query_param_required_int_query_50(): + response = client.get("/query/param-required/int?query=50") + assert response.status_code == 200 + assert response.json() == "foo bar 50" + + +def test_query_param_required_int_query_foo(): + response = client.get("/query/param-required/int?query=foo") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "int_parsing", + "loc": ["query", "query"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "foo", + "url": match_pydantic_error_url("int_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "query"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] + } + ) + + +def test_query_frozenset_query_1_query_1_query_2(): + response = client.get("/query/frozenset/?query=1&query=1&query=2") + assert response.status_code == 200 + assert response.json() == "1,2" diff --git a/tests/test_read_with_orm_mode.py b/tests/test_read_with_orm_mode.py index 360ad2503..b35987443 100644 --- a/tests/test_read_with_orm_mode.py +++ b/tests/test_read_with_orm_mode.py @@ -2,48 +2,83 @@ from typing import Any from fastapi import FastAPI from fastapi.testclient import TestClient -from pydantic import BaseModel +from pydantic import BaseModel, ConfigDict +from .utils import needs_pydanticv1, needs_pydanticv2 -class PersonBase(BaseModel): - name: str - lastname: str +@needs_pydanticv2 +def test_read_with_orm_mode() -> None: + class PersonBase(BaseModel): + name: str + lastname: str + + class Person(PersonBase): + @property + def full_name(self) -> str: + return f"{self.name} {self.lastname}" + + model_config = ConfigDict(from_attributes=True) + + class PersonCreate(PersonBase): + pass -class Person(PersonBase): - @property - def full_name(self) -> str: - return f"{self.name} {self.lastname}" + class PersonRead(PersonBase): + full_name: str - class Config: - orm_mode = True - read_with_orm_mode = True + model_config = {"from_attributes": True} + app = FastAPI() -class PersonCreate(PersonBase): - pass + @app.post("/people/", response_model=PersonRead) + def create_person(person: PersonCreate) -> Any: + db_person = Person.model_validate(person) + return db_person + + client = TestClient(app) + + person_data = {"name": "Dive", "lastname": "Wilson"} + response = client.post("/people/", json=person_data) + data = response.json() + assert response.status_code == 200, response.text + assert data["name"] == person_data["name"] + assert data["lastname"] == person_data["lastname"] + assert data["full_name"] == person_data["name"] + " " + person_data["lastname"] -class PersonRead(PersonBase): - full_name: str +@needs_pydanticv1 +def test_read_with_orm_mode_pv1() -> None: + class PersonBase(BaseModel): + name: str + lastname: str - class Config: - orm_mode = True + class Person(PersonBase): + @property + def full_name(self) -> str: + return f"{self.name} {self.lastname}" + class Config: + orm_mode = True + read_with_orm_mode = True -app = FastAPI() + class PersonCreate(PersonBase): + pass + class PersonRead(PersonBase): + full_name: str -@app.post("/people/", response_model=PersonRead) -def create_person(person: PersonCreate) -> Any: - db_person = Person.from_orm(person) - return db_person + class Config: + orm_mode = True + app = FastAPI() -client = TestClient(app) + @app.post("/people/", response_model=PersonRead) + def create_person(person: PersonCreate) -> Any: + db_person = Person.from_orm(person) + return db_person + client = TestClient(app) -def test_read_with_orm_mode() -> None: person_data = {"name": "Dive", "lastname": "Wilson"} response = client.post("/people/", json=person_data) data = response.json() diff --git a/tests/test_regex_deprecated_body.py b/tests/test_regex_deprecated_body.py new file mode 100644 index 000000000..ca1ab514c --- /dev/null +++ b/tests/test_regex_deprecated_body.py @@ -0,0 +1,182 @@ +import pytest +from dirty_equals import IsDict +from fastapi import FastAPI, Form +from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url +from typing_extensions import Annotated + +from .utils import needs_py310 + + +def get_client(): + app = FastAPI() + with pytest.warns(DeprecationWarning): + + @app.post("/items/") + async def read_items( + q: Annotated[str | None, Form(regex="^fixedquery$")] = None + ): + if q: + return f"Hello {q}" + else: + return "Hello World" + + client = TestClient(app) + return client + + +@needs_py310 +def test_no_query(): + client = get_client() + response = client.post("/items/") + assert response.status_code == 200 + assert response.json() == "Hello World" + + +@needs_py310 +def test_q_fixedquery(): + client = get_client() + response = client.post("/items/", data={"q": "fixedquery"}) + assert response.status_code == 200 + assert response.json() == "Hello fixedquery" + + +@needs_py310 +def test_query_nonregexquery(): + client = get_client() + response = client.post("/items/", data={"q": "nonregexquery"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "string_pattern_mismatch", + "loc": ["body", "q"], + "msg": "String should match pattern '^fixedquery$'", + "input": "nonregexquery", + "ctx": {"pattern": "^fixedquery$"}, + "url": match_pydantic_error_url("string_pattern_mismatch"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "ctx": {"pattern": "^fixedquery$"}, + "loc": ["body", "q"], + "msg": 'string does not match regex "^fixedquery$"', + "type": "value_error.str.regex", + } + ] + } + ) + + +@needs_py310 +def test_openapi_schema(): + client = get_client() + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + # insert_assert(response.json()) + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "post": { + "summary": "Read Items", + "operationId": "read_items_items__post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": IsDict( + { + "allOf": [ + { + "$ref": "#/components/schemas/Body_read_items_items__post" + } + ], + "title": "Body", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "$ref": "#/components/schemas/Body_read_items_items__post" + } + ) + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "Body_read_items_items__post": { + "properties": { + "q": IsDict( + { + "anyOf": [ + {"type": "string", "pattern": "^fixedquery$"}, + {"type": "null"}, + ], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"type": "string", "pattern": "^fixedquery$", "title": "Q"} + ) + }, + "type": "object", + "title": "Body_read_items_items__post", + }, + "HTTPValidationError": { + "properties": { + "detail": { + "items": {"$ref": "#/components/schemas/ValidationError"}, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } diff --git a/tests/test_regex_deprecated_params.py b/tests/test_regex_deprecated_params.py new file mode 100644 index 000000000..79a653353 --- /dev/null +++ b/tests/test_regex_deprecated_params.py @@ -0,0 +1,165 @@ +import pytest +from dirty_equals import IsDict +from fastapi import FastAPI, Query +from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url +from typing_extensions import Annotated + +from .utils import needs_py310 + + +def get_client(): + app = FastAPI() + with pytest.warns(DeprecationWarning): + + @app.get("/items/") + async def read_items( + q: Annotated[str | None, Query(regex="^fixedquery$")] = None + ): + if q: + return f"Hello {q}" + else: + return "Hello World" + + client = TestClient(app) + return client + + +@needs_py310 +def test_query_params_str_validations_no_query(): + client = get_client() + response = client.get("/items/") + assert response.status_code == 200 + assert response.json() == "Hello World" + + +@needs_py310 +def test_query_params_str_validations_q_fixedquery(): + client = get_client() + response = client.get("/items/", params={"q": "fixedquery"}) + assert response.status_code == 200 + assert response.json() == "Hello fixedquery" + + +@needs_py310 +def test_query_params_str_validations_item_query_nonregexquery(): + client = get_client() + response = client.get("/items/", params={"q": "nonregexquery"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "string_pattern_mismatch", + "loc": ["query", "q"], + "msg": "String should match pattern '^fixedquery$'", + "input": "nonregexquery", + "ctx": {"pattern": "^fixedquery$"}, + "url": match_pydantic_error_url("string_pattern_mismatch"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "ctx": {"pattern": "^fixedquery$"}, + "loc": ["query", "q"], + "msg": 'string does not match regex "^fixedquery$"', + "type": "value_error.str.regex", + } + ] + } + ) + + +@needs_py310 +def test_openapi_schema(): + client = get_client() + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + # insert_assert(response.json()) + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "name": "q", + "in": "query", + "required": False, + "schema": IsDict( + { + "anyOf": [ + {"type": "string", "pattern": "^fixedquery$"}, + {"type": "null"}, + ], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "type": "string", + "pattern": "^fixedquery$", + "title": "Q", + } + ), + } + ], + "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", + }, + } + }, + } diff --git a/tests/test_request_body_parameters_media_type.py b/tests/test_request_body_parameters_media_type.py index 8424bf551..8c72fee54 100644 --- a/tests/test_request_body_parameters_media_type.py +++ b/tests/test_request_body_parameters_media_type.py @@ -39,7 +39,6 @@ client = TestClient(app) def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text - # insert_assert(response.json()) assert response.json() == { "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, diff --git a/tests/test_response_by_alias.py b/tests/test_response_by_alias.py index c3ff5b1d2..e162cd39b 100644 --- a/tests/test_response_by_alias.py +++ b/tests/test_response_by_alias.py @@ -1,8 +1,9 @@ from typing import List from fastapi import FastAPI +from fastapi._compat import PYDANTIC_V2 from fastapi.testclient import TestClient -from pydantic import BaseModel, Field +from pydantic import BaseModel, ConfigDict, Field app = FastAPI() @@ -14,13 +15,24 @@ class Model(BaseModel): class ModelNoAlias(BaseModel): name: str - class Config: - schema_extra = { - "description": ( - "response_model_by_alias=False is basically a quick hack, to support " - "proper OpenAPI use another model with the correct field names" - ) - } + if PYDANTIC_V2: + model_config = ConfigDict( + json_schema_extra={ + "description": ( + "response_model_by_alias=False is basically a quick hack, to support " + "proper OpenAPI use another model with the correct field names" + ) + } + ) + else: + + class Config: + schema_extra = { + "description": ( + "response_model_by_alias=False is basically a quick hack, to support " + "proper OpenAPI use another model with the correct field names" + ) + } @app.get("/dict", response_model=Model, response_model_by_alias=False) diff --git a/tests/test_response_model_as_return_annotation.py b/tests/test_response_model_as_return_annotation.py index 7a0cf47ec..85dd450eb 100644 --- a/tests/test_response_model_as_return_annotation.py +++ b/tests/test_response_model_as_return_annotation.py @@ -2,10 +2,10 @@ from typing import List, Union import pytest from fastapi import FastAPI -from fastapi.exceptions import FastAPIError +from fastapi.exceptions import FastAPIError, ResponseValidationError from fastapi.responses import JSONResponse, Response from fastapi.testclient import TestClient -from pydantic import BaseModel, ValidationError +from pydantic import BaseModel class BaseUser(BaseModel): @@ -277,12 +277,12 @@ def test_response_model_no_annotation_return_exact_dict(): def test_response_model_no_annotation_return_invalid_dict(): - with pytest.raises(ValidationError): + with pytest.raises(ResponseValidationError): client.get("/response_model-no_annotation-return_invalid_dict") def test_response_model_no_annotation_return_invalid_model(): - with pytest.raises(ValidationError): + with pytest.raises(ResponseValidationError): client.get("/response_model-no_annotation-return_invalid_model") @@ -313,12 +313,12 @@ def test_no_response_model_annotation_return_exact_dict(): def test_no_response_model_annotation_return_invalid_dict(): - with pytest.raises(ValidationError): + with pytest.raises(ResponseValidationError): client.get("/no_response_model-annotation-return_invalid_dict") def test_no_response_model_annotation_return_invalid_model(): - with pytest.raises(ValidationError): + with pytest.raises(ResponseValidationError): client.get("/no_response_model-annotation-return_invalid_model") @@ -395,12 +395,12 @@ def test_response_model_model1_annotation_model2_return_exact_dict(): def test_response_model_model1_annotation_model2_return_invalid_dict(): - with pytest.raises(ValidationError): + with pytest.raises(ResponseValidationError): client.get("/response_model_model1-annotation_model2-return_invalid_dict") def test_response_model_model1_annotation_model2_return_invalid_model(): - with pytest.raises(ValidationError): + with pytest.raises(ResponseValidationError): client.get("/response_model_model1-annotation_model2-return_invalid_model") diff --git a/tests/test_response_model_data_filter.py b/tests/test_response_model_data_filter.py new file mode 100644 index 000000000..a3e0f95f0 --- /dev/null +++ b/tests/test_response_model_data_filter.py @@ -0,0 +1,81 @@ +from typing import List + +from fastapi import FastAPI +from fastapi.testclient import TestClient +from pydantic import BaseModel + +app = FastAPI() + + +class UserBase(BaseModel): + email: str + + +class UserCreate(UserBase): + password: str + + +class UserDB(UserBase): + hashed_password: str + + +class PetDB(BaseModel): + name: str + owner: UserDB + + +class PetOut(BaseModel): + name: str + owner: UserBase + + +@app.post("/users/", response_model=UserBase) +async def create_user(user: UserCreate): + return user + + +@app.get("/pets/{pet_id}", response_model=PetOut) +async def read_pet(pet_id: int): + user = UserDB( + email="johndoe@example.com", + hashed_password="secrethashed", + ) + pet = PetDB(name="Nibbler", owner=user) + return pet + + +@app.get("/pets/", response_model=List[PetOut]) +async def read_pets(): + user = UserDB( + email="johndoe@example.com", + hashed_password="secrethashed", + ) + pet1 = PetDB(name="Nibbler", owner=user) + pet2 = PetDB(name="Zoidberg", owner=user) + return [pet1, pet2] + + +client = TestClient(app) + + +def test_filter_top_level_model(): + response = client.post( + "/users", json={"email": "johndoe@example.com", "password": "secret"} + ) + assert response.json() == {"email": "johndoe@example.com"} + + +def test_filter_second_level_model(): + response = client.get("/pets/1") + assert response.json() == { + "name": "Nibbler", + "owner": {"email": "johndoe@example.com"}, + } + + +def test_list_of_models(): + response = client.get("/pets/") + assert response.json() == [ + {"name": "Nibbler", "owner": {"email": "johndoe@example.com"}}, + {"name": "Zoidberg", "owner": {"email": "johndoe@example.com"}}, + ] diff --git a/tests/test_response_model_data_filter_no_inheritance.py b/tests/test_response_model_data_filter_no_inheritance.py new file mode 100644 index 000000000..64003a841 --- /dev/null +++ b/tests/test_response_model_data_filter_no_inheritance.py @@ -0,0 +1,83 @@ +from typing import List + +from fastapi import FastAPI +from fastapi.testclient import TestClient +from pydantic import BaseModel + +app = FastAPI() + + +class UserCreate(BaseModel): + email: str + password: str + + +class UserDB(BaseModel): + email: str + hashed_password: str + + +class User(BaseModel): + email: str + + +class PetDB(BaseModel): + name: str + owner: UserDB + + +class PetOut(BaseModel): + name: str + owner: User + + +@app.post("/users/", response_model=User) +async def create_user(user: UserCreate): + return user + + +@app.get("/pets/{pet_id}", response_model=PetOut) +async def read_pet(pet_id: int): + user = UserDB( + email="johndoe@example.com", + hashed_password="secrethashed", + ) + pet = PetDB(name="Nibbler", owner=user) + return pet + + +@app.get("/pets/", response_model=List[PetOut]) +async def read_pets(): + user = UserDB( + email="johndoe@example.com", + hashed_password="secrethashed", + ) + pet1 = PetDB(name="Nibbler", owner=user) + pet2 = PetDB(name="Zoidberg", owner=user) + return [pet1, pet2] + + +client = TestClient(app) + + +def test_filter_top_level_model(): + response = client.post( + "/users", json={"email": "johndoe@example.com", "password": "secret"} + ) + assert response.json() == {"email": "johndoe@example.com"} + + +def test_filter_second_level_model(): + response = client.get("/pets/1") + assert response.json() == { + "name": "Nibbler", + "owner": {"email": "johndoe@example.com"}, + } + + +def test_list_of_models(): + response = client.get("/pets/") + assert response.json() == [ + {"name": "Nibbler", "owner": {"email": "johndoe@example.com"}}, + {"name": "Zoidberg", "owner": {"email": "johndoe@example.com"}}, + ] diff --git a/tests/test_schema_extra_examples.py b/tests/test_schema_extra_examples.py index 45caa1615..a1505afe2 100644 --- a/tests/test_schema_extra_examples.py +++ b/tests/test_schema_extra_examples.py @@ -1,9 +1,11 @@ from typing import Union import pytest +from dirty_equals import IsDict from fastapi import Body, Cookie, FastAPI, Header, Path, Query +from fastapi._compat import PYDANTIC_V2 from fastapi.testclient import TestClient -from pydantic import BaseModel +from pydantic import BaseModel, ConfigDict def create_app(): @@ -12,8 +14,14 @@ def create_app(): class Item(BaseModel): data: str - class Config: - schema_extra = {"example": {"data": "Data in schema_extra"}} + if PYDANTIC_V2: + model_config = ConfigDict( + json_schema_extra={"example": {"data": "Data in schema_extra"}} + ) + else: + + class Config: + schema_extra = {"example": {"data": "Data in schema_extra"}} @app.post("/schema_extra/") def schema_extra(item: Item): @@ -333,14 +341,28 @@ def test_openapi_schema(): "requestBody": { "content": { "application/json": { - "schema": { - "allOf": [{"$ref": "#/components/schemas/Item"}], - "title": "Item", - "examples": [ - {"data": "Data in Body examples, example1"}, - {"data": "Data in Body examples, example2"}, - ], - } + "schema": IsDict( + { + "$ref": "#/components/schemas/Item", + "examples": [ + {"data": "Data in Body examples, example1"}, + {"data": "Data in Body examples, example2"}, + ], + } + ) + | IsDict( + # TODO: remove this when deprecating Pydantic v1 + { + "allOf": [ + {"$ref": "#/components/schemas/Item"} + ], + "title": "Item", + "examples": [ + {"data": "Data in Body examples, example1"}, + {"data": "Data in Body examples, example2"}, + ], + } + ) } }, "required": True, @@ -370,14 +392,28 @@ def test_openapi_schema(): "requestBody": { "content": { "application/json": { - "schema": { - "allOf": [{"$ref": "#/components/schemas/Item"}], - "title": "Item", - "examples": [ - {"data": "examples example_examples 1"}, - {"data": "examples example_examples 2"}, - ], - }, + "schema": IsDict( + { + "$ref": "#/components/schemas/Item", + "examples": [ + {"data": "examples example_examples 1"}, + {"data": "examples example_examples 2"}, + ], + } + ) + | IsDict( + # TODO: remove this when deprecating Pydantic v1 + { + "allOf": [ + {"$ref": "#/components/schemas/Item"} + ], + "title": "Item", + "examples": [ + {"data": "examples example_examples 1"}, + {"data": "examples example_examples 2"}, + ], + }, + ), "example": {"data": "Overridden example"}, } }, @@ -508,7 +544,16 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": {"title": "Data", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Data", + } + ) + | IsDict( + # TODO: Remove this when deprecating Pydantic v1 + {"title": "Data", "type": "string"} + ), "example": "query1", "name": "data", "in": "query", @@ -539,11 +584,21 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": { - "type": "string", - "title": "Data", - "examples": ["query1", "query2"], - }, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Data", + "examples": ["query1", "query2"], + } + ) + | IsDict( + # TODO: Remove this when deprecating Pydantic v1 + { + "type": "string", + "title": "Data", + "examples": ["query1", "query2"], + } + ), "name": "data", "in": "query", } @@ -573,11 +628,21 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": { - "type": "string", - "title": "Data", - "examples": ["query1", "query2"], - }, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Data", + "examples": ["query1", "query2"], + } + ) + | IsDict( + # TODO: Remove this when deprecating Pydantic v1 + { + "type": "string", + "title": "Data", + "examples": ["query1", "query2"], + } + ), "example": "query_overridden", "name": "data", "in": "query", @@ -608,7 +673,16 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": {"type": "string", "title": "Data"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Data", + } + ) + | IsDict( + # TODO: Remove this when deprecating Pydantic v1 + {"title": "Data", "type": "string"} + ), "example": "header1", "name": "data", "in": "header", @@ -639,11 +713,21 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": { - "type": "string", - "title": "Data", - "examples": ["header1", "header2"], - }, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Data", + "examples": ["header1", "header2"], + } + ) + | IsDict( + # TODO: Remove this when deprecating Pydantic v1 + { + "type": "string", + "title": "Data", + "examples": ["header1", "header2"], + } + ), "name": "data", "in": "header", } @@ -673,11 +757,21 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": { - "type": "string", - "title": "Data", - "examples": ["header1", "header2"], - }, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Data", + "examples": ["header1", "header2"], + } + ) + | IsDict( + # TODO: Remove this when deprecating Pydantic v1 + { + "title": "Data", + "type": "string", + "examples": ["header1", "header2"], + } + ), "example": "header_overridden", "name": "data", "in": "header", @@ -708,7 +802,16 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": {"type": "string", "title": "Data"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Data", + } + ) + | IsDict( + # TODO: Remove this when deprecating Pydantic v1 + {"title": "Data", "type": "string"} + ), "example": "cookie1", "name": "data", "in": "cookie", @@ -739,11 +842,21 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": { - "type": "string", - "title": "Data", - "examples": ["cookie1", "cookie2"], - }, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Data", + "examples": ["cookie1", "cookie2"], + } + ) + | IsDict( + # TODO: Remove this when deprecating Pydantic v1 + { + "title": "Data", + "type": "string", + "examples": ["cookie1", "cookie2"], + } + ), "name": "data", "in": "cookie", } @@ -773,11 +886,21 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": { - "type": "string", - "title": "Data", - "examples": ["cookie1", "cookie2"], - }, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Data", + "examples": ["cookie1", "cookie2"], + } + ) + | IsDict( + # TODO: Remove this when deprecating Pydantic v1 + { + "title": "Data", + "type": "string", + "examples": ["cookie1", "cookie2"], + } + ), "example": "cookie_overridden", "name": "data", "in": "cookie", diff --git a/tests/test_security_oauth2.py b/tests/test_security_oauth2.py index 73d1b7d94..e98f80ebf 100644 --- a/tests/test_security_oauth2.py +++ b/tests/test_security_oauth2.py @@ -1,7 +1,8 @@ -import pytest +from dirty_equals import IsDict from fastapi import Depends, FastAPI, Security from fastapi.security import OAuth2, OAuth2PasswordRequestFormStrict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from pydantic import BaseModel app = FastAPI() @@ -59,76 +60,136 @@ def test_security_oauth2_password_bearer_no_header(): assert response.json() == {"detail": "Not authenticated"} -required_params = { - "detail": [ +def test_strict_login_no_data(): + response = client.post("/login") + assert response.status_code == 422 + assert response.json() == IsDict( { - "loc": ["body", "grant_type"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - }, + "detail": [ + { + "type": "missing", + "loc": ["body", "grant_type"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - }, - ] -} + "detail": [ + { + "loc": ["body", "grant_type"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) -grant_type_required = { - "detail": [ + +def test_strict_login_no_grant_type(): + response = client.post("/login", data={"username": "johndoe", "password": "secret"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "grant_type"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 { - "loc": ["body", "grant_type"], - "msg": "field required", - "type": "value_error.missing", + "detail": [ + { + "loc": ["body", "grant_type"], + "msg": "field required", + "type": "value_error.missing", + } + ] } - ] -} + ) -grant_type_incorrect = { - "detail": [ + +def test_strict_login_incorrect_grant_type(): + response = client.post( + "/login", + data={"username": "johndoe", "password": "secret", "grant_type": "incorrect"}, + ) + assert response.status_code == 422 + assert response.json() == IsDict( { - "loc": ["body", "grant_type"], - "msg": 'string does not match regex "password"', - "type": "value_error.str.regex", - "ctx": {"pattern": "password"}, + "detail": [ + { + "type": "string_pattern_mismatch", + "loc": ["body", "grant_type"], + "msg": "String should match pattern 'password'", + "input": "incorrect", + "ctx": {"pattern": "password"}, + "url": match_pydantic_error_url("string_pattern_mismatch"), + } + ] } - ] -} - - -@pytest.mark.parametrize( - "data,expected_status,expected_response", - [ - (None, 422, required_params), - ({"username": "johndoe", "password": "secret"}, 422, grant_type_required), - ( - {"username": "johndoe", "password": "secret", "grant_type": "incorrect"}, - 422, - grant_type_incorrect, - ), - ( - {"username": "johndoe", "password": "secret", "grant_type": "password"}, - 200, - { - "grant_type": "password", - "username": "johndoe", - "password": "secret", - "scopes": [], - "client_id": None, - "client_secret": None, - }, - ), - ], -) -def test_strict_login(data, expected_status, expected_response): - response = client.post("/login", data=data) - assert response.status_code == expected_status - assert response.json() == expected_response + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "grant_type"], + "msg": 'string does not match regex "password"', + "type": "value_error.str.regex", + "ctx": {"pattern": "password"}, + } + ] + } + ) + + +def test_strict_login_correct_grant_type(): + response = client.post( + "/login", + data={"username": "johndoe", "password": "secret", "grant_type": "password"}, + ) + assert response.status_code == 200 + assert response.json() == { + "grant_type": "password", + "username": "johndoe", + "password": "secret", + "scopes": [], + "client_id": None, + "client_secret": None, + } def test_openapi_schema(): @@ -199,8 +260,26 @@ def test_openapi_schema(): "username": {"title": "Username", "type": "string"}, "password": {"title": "Password", "type": "string"}, "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": {"title": "Client Id", "type": "string"}, - "client_secret": {"title": "Client Secret", "type": "string"}, + "client_id": IsDict( + { + "title": "Client Id", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Id", "type": "string"} + ), + "client_secret": IsDict( + { + "title": "Client Secret", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Secret", "type": "string"} + ), }, }, "ValidationError": { diff --git a/tests/test_security_oauth2_optional.py b/tests/test_security_oauth2_optional.py index b24c1b58f..d06c01bba 100644 --- a/tests/test_security_oauth2_optional.py +++ b/tests/test_security_oauth2_optional.py @@ -1,9 +1,10 @@ from typing import Optional -import pytest +from dirty_equals import IsDict from fastapi import Depends, FastAPI, Security from fastapi.security import OAuth2, OAuth2PasswordRequestFormStrict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from pydantic import BaseModel app = FastAPI() @@ -63,76 +64,136 @@ def test_security_oauth2_password_bearer_no_header(): assert response.json() == {"msg": "Create an account first"} -required_params = { - "detail": [ +def test_strict_login_no_data(): + response = client.post("/login") + assert response.status_code == 422 + assert response.json() == IsDict( { - "loc": ["body", "grant_type"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - }, + "detail": [ + { + "type": "missing", + "loc": ["body", "grant_type"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - }, - ] -} + "detail": [ + { + "loc": ["body", "grant_type"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) -grant_type_required = { - "detail": [ + +def test_strict_login_no_grant_type(): + response = client.post("/login", data={"username": "johndoe", "password": "secret"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "grant_type"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 { - "loc": ["body", "grant_type"], - "msg": "field required", - "type": "value_error.missing", + "detail": [ + { + "loc": ["body", "grant_type"], + "msg": "field required", + "type": "value_error.missing", + } + ] } - ] -} + ) -grant_type_incorrect = { - "detail": [ + +def test_strict_login_incorrect_grant_type(): + response = client.post( + "/login", + data={"username": "johndoe", "password": "secret", "grant_type": "incorrect"}, + ) + assert response.status_code == 422 + assert response.json() == IsDict( { - "loc": ["body", "grant_type"], - "msg": 'string does not match regex "password"', - "type": "value_error.str.regex", - "ctx": {"pattern": "password"}, + "detail": [ + { + "type": "string_pattern_mismatch", + "loc": ["body", "grant_type"], + "msg": "String should match pattern 'password'", + "input": "incorrect", + "ctx": {"pattern": "password"}, + "url": match_pydantic_error_url("string_pattern_mismatch"), + } + ] } - ] -} - - -@pytest.mark.parametrize( - "data,expected_status,expected_response", - [ - (None, 422, required_params), - ({"username": "johndoe", "password": "secret"}, 422, grant_type_required), - ( - {"username": "johndoe", "password": "secret", "grant_type": "incorrect"}, - 422, - grant_type_incorrect, - ), - ( - {"username": "johndoe", "password": "secret", "grant_type": "password"}, - 200, - { - "grant_type": "password", - "username": "johndoe", - "password": "secret", - "scopes": [], - "client_id": None, - "client_secret": None, - }, - ), - ], -) -def test_strict_login(data, expected_status, expected_response): - response = client.post("/login", data=data) - assert response.status_code == expected_status - assert response.json() == expected_response + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "grant_type"], + "msg": 'string does not match regex "password"', + "type": "value_error.str.regex", + "ctx": {"pattern": "password"}, + } + ] + } + ) + + +def test_strict_login_correct_data(): + response = client.post( + "/login", + data={"username": "johndoe", "password": "secret", "grant_type": "password"}, + ) + assert response.status_code == 200 + assert response.json() == { + "grant_type": "password", + "username": "johndoe", + "password": "secret", + "scopes": [], + "client_id": None, + "client_secret": None, + } def test_openapi_schema(): @@ -203,8 +264,26 @@ def test_openapi_schema(): "username": {"title": "Username", "type": "string"}, "password": {"title": "Password", "type": "string"}, "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": {"title": "Client Id", "type": "string"}, - "client_secret": {"title": "Client Secret", "type": "string"}, + "client_id": IsDict( + { + "title": "Client Id", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Id", "type": "string"} + ), + "client_secret": IsDict( + { + "title": "Client Secret", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Secret", "type": "string"} + ), }, }, "ValidationError": { diff --git a/tests/test_security_oauth2_optional_description.py b/tests/test_security_oauth2_optional_description.py index cda635151..9287e4366 100644 --- a/tests/test_security_oauth2_optional_description.py +++ b/tests/test_security_oauth2_optional_description.py @@ -1,9 +1,10 @@ from typing import Optional -import pytest +from dirty_equals import IsDict from fastapi import Depends, FastAPI, Security from fastapi.security import OAuth2, OAuth2PasswordRequestFormStrict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from pydantic import BaseModel app = FastAPI() @@ -64,76 +65,136 @@ def test_security_oauth2_password_bearer_no_header(): assert response.json() == {"msg": "Create an account first"} -required_params = { - "detail": [ +def test_strict_login_None(): + response = client.post("/login", data=None) + assert response.status_code == 422 + assert response.json() == IsDict( { - "loc": ["body", "grant_type"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - }, + "detail": [ + { + "type": "missing", + "loc": ["body", "grant_type"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - }, - ] -} + "detail": [ + { + "loc": ["body", "grant_type"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) -grant_type_required = { - "detail": [ + +def test_strict_login_no_grant_type(): + response = client.post("/login", data={"username": "johndoe", "password": "secret"}) + assert response.status_code == 422 + assert response.json() == IsDict( { - "loc": ["body", "grant_type"], - "msg": "field required", - "type": "value_error.missing", + "detail": [ + { + "type": "missing", + "loc": ["body", "grant_type"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] } - ] -} + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "grant_type"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + -grant_type_incorrect = { - "detail": [ +def test_strict_login_incorrect_grant_type(): + response = client.post( + "/login", + data={"username": "johndoe", "password": "secret", "grant_type": "incorrect"}, + ) + assert response.status_code == 422 + assert response.json() == IsDict( { - "loc": ["body", "grant_type"], - "msg": 'string does not match regex "password"', - "type": "value_error.str.regex", - "ctx": {"pattern": "password"}, + "detail": [ + { + "type": "string_pattern_mismatch", + "loc": ["body", "grant_type"], + "msg": "String should match pattern 'password'", + "input": "incorrect", + "ctx": {"pattern": "password"}, + "url": match_pydantic_error_url("string_pattern_mismatch"), + } + ] } - ] -} - - -@pytest.mark.parametrize( - "data,expected_status,expected_response", - [ - (None, 422, required_params), - ({"username": "johndoe", "password": "secret"}, 422, grant_type_required), - ( - {"username": "johndoe", "password": "secret", "grant_type": "incorrect"}, - 422, - grant_type_incorrect, - ), - ( - {"username": "johndoe", "password": "secret", "grant_type": "password"}, - 200, - { - "grant_type": "password", - "username": "johndoe", - "password": "secret", - "scopes": [], - "client_id": None, - "client_secret": None, - }, - ), - ], -) -def test_strict_login(data, expected_status, expected_response): - response = client.post("/login", data=data) - assert response.status_code == expected_status - assert response.json() == expected_response + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "grant_type"], + "msg": 'string does not match regex "password"', + "type": "value_error.str.regex", + "ctx": {"pattern": "password"}, + } + ] + } + ) + + +def test_strict_login_correct_correct_grant_type(): + response = client.post( + "/login", + data={"username": "johndoe", "password": "secret", "grant_type": "password"}, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "grant_type": "password", + "username": "johndoe", + "password": "secret", + "scopes": [], + "client_id": None, + "client_secret": None, + } def test_openapi_schema(): @@ -204,8 +265,26 @@ def test_openapi_schema(): "username": {"title": "Username", "type": "string"}, "password": {"title": "Password", "type": "string"}, "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": {"title": "Client Id", "type": "string"}, - "client_secret": {"title": "Client Secret", "type": "string"}, + "client_id": IsDict( + { + "title": "Client Id", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Id", "type": "string"} + ), + "client_secret": IsDict( + { + "title": "Client Secret", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Secret", "type": "string"} + ), }, }, "ValidationError": { diff --git a/tests/test_skip_defaults.py b/tests/test_skip_defaults.py index 181fff612..02765291c 100644 --- a/tests/test_skip_defaults.py +++ b/tests/test_skip_defaults.py @@ -12,7 +12,7 @@ class SubModel(BaseModel): class Model(BaseModel): - x: Optional[int] + x: Optional[int] = None sub: SubModel diff --git a/tests/test_sub_callbacks.py b/tests/test_sub_callbacks.py index dce3ea5e2..ed7f4efe8 100644 --- a/tests/test_sub_callbacks.py +++ b/tests/test_sub_callbacks.py @@ -1,5 +1,6 @@ from typing import Optional +from dirty_equals import IsDict from fastapi import APIRouter, FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel, HttpUrl @@ -98,13 +99,30 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": { - "title": "Callback Url", - "maxLength": 2083, - "minLength": 1, - "type": "string", - "format": "uri", - }, + "schema": IsDict( + { + "title": "Callback Url", + "anyOf": [ + { + "type": "string", + "format": "uri", + "minLength": 1, + "maxLength": 2083, + }, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Callback Url", + "maxLength": 2083, + "minLength": 1, + "type": "string", + "format": "uri", + } + ), "name": "callback_url", "in": "query", } @@ -244,7 +262,16 @@ def test_openapi_schema(): "type": "object", "properties": { "id": {"title": "Id", "type": "string"}, - "title": {"title": "Title", "type": "string"}, + "title": IsDict( + { + "title": "Title", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Title", "type": "string"} + ), "customer": {"title": "Customer", "type": "string"}, "total": {"title": "Total", "type": "number"}, }, diff --git a/tests/test_tuples.py b/tests/test_tuples.py index c37a25ca6..ca33d2580 100644 --- a/tests/test_tuples.py +++ b/tests/test_tuples.py @@ -1,5 +1,6 @@ from typing import List, Tuple +from dirty_equals import IsDict from fastapi import FastAPI, Form from fastapi.testclient import TestClient from pydantic import BaseModel @@ -126,16 +127,31 @@ def test_openapi_schema(): "requestBody": { "content": { "application/json": { - "schema": { - "title": "Square", - "maxItems": 2, - "minItems": 2, - "type": "array", - "items": [ - {"$ref": "#/components/schemas/Coordinate"}, - {"$ref": "#/components/schemas/Coordinate"}, - ], - } + "schema": IsDict( + { + "title": "Square", + "maxItems": 2, + "minItems": 2, + "type": "array", + "prefixItems": [ + {"$ref": "#/components/schemas/Coordinate"}, + {"$ref": "#/components/schemas/Coordinate"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Square", + "maxItems": 2, + "minItems": 2, + "type": "array", + "items": [ + {"$ref": "#/components/schemas/Coordinate"}, + {"$ref": "#/components/schemas/Coordinate"}, + ], + } + ) } }, "required": True, @@ -198,13 +214,28 @@ def test_openapi_schema(): "required": ["values"], "type": "object", "properties": { - "values": { - "title": "Values", - "maxItems": 2, - "minItems": 2, - "type": "array", - "items": [{"type": "integer"}, {"type": "integer"}], - } + "values": IsDict( + { + "title": "Values", + "maxItems": 2, + "minItems": 2, + "type": "array", + "prefixItems": [ + {"type": "integer"}, + {"type": "integer"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Values", + "maxItems": 2, + "minItems": 2, + "type": "array", + "items": [{"type": "integer"}, {"type": "integer"}], + } + ) }, }, "Coordinate": { @@ -235,12 +266,26 @@ def test_openapi_schema(): "items": { "title": "Items", "type": "array", - "items": { - "maxItems": 2, - "minItems": 2, - "type": "array", - "items": [{"type": "string"}, {"type": "string"}], - }, + "items": IsDict( + { + "maxItems": 2, + "minItems": 2, + "type": "array", + "prefixItems": [ + {"type": "string"}, + {"type": "string"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "maxItems": 2, + "minItems": 2, + "type": "array", + "items": [{"type": "string"}, {"type": "string"}], + } + ), } }, }, diff --git a/tests/test_tutorial/test_additional_responses/test_tutorial002.py b/tests/test_tutorial/test_additional_responses/test_tutorial002.py index 8e084e152..588a3160a 100644 --- a/tests/test_tutorial/test_additional_responses/test_tutorial002.py +++ b/tests/test_tutorial/test_additional_responses/test_tutorial002.py @@ -1,6 +1,7 @@ import os import shutil +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.additional_responses.tutorial002 import app @@ -64,7 +65,16 @@ def test_openapi_schema(): }, { "required": False, - "schema": {"title": "Img", "type": "boolean"}, + "schema": IsDict( + { + "anyOf": [{"type": "boolean"}, {"type": "null"}], + "title": "Img", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Img", "type": "boolean"} + ), "name": "img", "in": "query", }, diff --git a/tests/test_tutorial/test_additional_responses/test_tutorial004.py b/tests/test_tutorial/test_additional_responses/test_tutorial004.py index 5fc8b81ca..55b556d8e 100644 --- a/tests/test_tutorial/test_additional_responses/test_tutorial004.py +++ b/tests/test_tutorial/test_additional_responses/test_tutorial004.py @@ -1,6 +1,7 @@ import os import shutil +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.additional_responses.tutorial004 import app @@ -67,7 +68,16 @@ def test_openapi_schema(): }, { "required": False, - "schema": {"title": "Img", "type": "boolean"}, + "schema": IsDict( + { + "anyOf": [{"type": "boolean"}, {"type": "null"}], + "title": "Img", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Img", "type": "boolean"} + ), "name": "img", "in": "query", }, diff --git a/tests/test_tutorial/test_async_sql_databases/test_tutorial001.py b/tests/test_tutorial/test_async_sql_databases/test_tutorial001.py index 8126cdcc6..25d6df3e9 100644 --- a/tests/test_tutorial/test_async_sql_databases/test_tutorial001.py +++ b/tests/test_tutorial/test_async_sql_databases/test_tutorial001.py @@ -2,7 +2,11 @@ from fastapi.testclient import TestClient from docs_src.async_sql_databases.tutorial001 import app +from ...utils import needs_pydanticv1 + +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_create_read(): with TestClient(app) as client: note = {"text": "Foo bar", "completed": False} diff --git a/tests/test_tutorial/test_behind_a_proxy/test_tutorial003.py b/tests/test_tutorial/test_behind_a_proxy/test_tutorial003.py index 0ae9f4f93..ec17b4179 100644 --- a/tests/test_tutorial/test_behind_a_proxy/test_tutorial003.py +++ b/tests/test_tutorial/test_behind_a_proxy/test_tutorial003.py @@ -1,3 +1,4 @@ +from dirty_equals import IsOneOf from fastapi.testclient import TestClient from docs_src.behind_a_proxy.tutorial003 import app @@ -11,7 +12,7 @@ def test_main(): assert response.json() == {"message": "Hello World", "root_path": "/api/v1"} -def test_openapi(): +def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { @@ -19,9 +20,20 @@ def test_openapi(): "info": {"title": "FastAPI", "version": "0.1.0"}, "servers": [ {"url": "/api/v1"}, - {"url": "https://stag.example.com", "description": "Staging environment"}, { - "url": "https://prod.example.com", + "url": IsOneOf( + "https://stag.example.com/", + # TODO: remove when deprecating Pydantic v1 + "https://stag.example.com", + ), + "description": "Staging environment", + }, + { + "url": IsOneOf( + "https://prod.example.com/", + # TODO: remove when deprecating Pydantic v1 + "https://prod.example.com", + ), "description": "Production environment", }, ], diff --git a/tests/test_tutorial/test_behind_a_proxy/test_tutorial004.py b/tests/test_tutorial/test_behind_a_proxy/test_tutorial004.py index 576a411a4..2f8eb4699 100644 --- a/tests/test_tutorial/test_behind_a_proxy/test_tutorial004.py +++ b/tests/test_tutorial/test_behind_a_proxy/test_tutorial004.py @@ -1,3 +1,4 @@ +from dirty_equals import IsOneOf from fastapi.testclient import TestClient from docs_src.behind_a_proxy.tutorial004 import app @@ -11,16 +12,27 @@ def test_main(): assert response.json() == {"message": "Hello World", "root_path": "/api/v1"} -def test_openapi(): +def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "servers": [ - {"url": "https://stag.example.com", "description": "Staging environment"}, { - "url": "https://prod.example.com", + "url": IsOneOf( + "https://stag.example.com/", + # TODO: remove when deprecating Pydantic v1 + "https://stag.example.com", + ), + "description": "Staging environment", + }, + { + "url": IsOneOf( + "https://prod.example.com/", + # TODO: remove when deprecating Pydantic v1 + "https://prod.example.com", + ), "description": "Production environment", }, ], diff --git a/tests/test_tutorial/test_bigger_applications/test_main.py b/tests/test_tutorial/test_bigger_applications/test_main.py index 7da663435..526e265a6 100644 --- a/tests/test_tutorial/test_bigger_applications/test_main.py +++ b/tests/test_tutorial/test_bigger_applications/test_main.py @@ -1,138 +1,368 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url -from docs_src.bigger_applications.app.main import app -client = TestClient(app) +@pytest.fixture(name="client") +def get_client(): + from docs_src.bigger_applications.app.main import app + client = TestClient(app) + return client -no_jessica = { - "detail": [ + +def test_users_token_jessica(client: TestClient): + response = client.get("/users?token=jessica") + assert response.status_code == 200 + assert response.json() == [{"username": "Rick"}, {"username": "Morty"}] + + +def test_users_with_no_token(client: TestClient): + response = client.get("/users") + assert response.status_code == 422 + assert response.json() == IsDict( { - "loc": ["query", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] -} - - -@pytest.mark.parametrize( - "path,expected_status,expected_response,headers", - [ - ( - "/users?token=jessica", - 200, - [{"username": "Rick"}, {"username": "Morty"}], - {}, - ), - ("/users", 422, no_jessica, {}), - ("/users/foo?token=jessica", 200, {"username": "foo"}, {}), - ("/users/foo", 422, no_jessica, {}), - ("/users/me?token=jessica", 200, {"username": "fakecurrentuser"}, {}), - ("/users/me", 422, no_jessica, {}), - ( - "/users?token=monica", - 400, - {"detail": "No Jessica token provided"}, - {}, - ), - ( - "/items?token=jessica", - 200, - {"plumbus": {"name": "Plumbus"}, "gun": {"name": "Portal Gun"}}, - {"X-Token": "fake-super-secret-token"}, - ), - ("/items", 422, no_jessica, {"X-Token": "fake-super-secret-token"}), - ( - "/items/plumbus?token=jessica", - 200, - {"name": "Plumbus", "item_id": "plumbus"}, - {"X-Token": "fake-super-secret-token"}, - ), - ( - "/items/bar?token=jessica", - 404, - {"detail": "Item not found"}, - {"X-Token": "fake-super-secret-token"}, - ), - ("/items/plumbus", 422, no_jessica, {"X-Token": "fake-super-secret-token"}), - ( - "/items?token=jessica", - 400, - {"detail": "X-Token header invalid"}, - {"X-Token": "invalid"}, - ), - ( - "/items/bar?token=jessica", - 400, - {"detail": "X-Token header invalid"}, - {"X-Token": "invalid"}, - ), - ( - "/items?token=jessica", - 422, - { - "detail": [ - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - } - ] - }, - {}, - ), - ( - "/items/plumbus?token=jessica", - 422, - { - "detail": [ - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - } - ] - }, - {}, - ), - ("/?token=jessica", 200, {"message": "Hello Bigger Applications!"}, {}), - ("/", 422, no_jessica, {}), - ], -) -def test_get_path(path, expected_status, expected_response, headers): - response = client.get(path, headers=headers) - assert response.status_code == expected_status - assert response.json() == expected_response - - -def test_put_no_header(): - response = client.put("/items/foo") - assert response.status_code == 422, response.text + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_users_foo_token_jessica(client: TestClient): + response = client.get("/users/foo?token=jessica") + assert response.status_code == 200 + assert response.json() == {"username": "foo"} + + +def test_users_foo_with_no_token(client: TestClient): + response = client.get("/users/foo") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_users_me_token_jessica(client: TestClient): + response = client.get("/users/me?token=jessica") + assert response.status_code == 200 + assert response.json() == {"username": "fakecurrentuser"} + + +def test_users_me_with_no_token(client: TestClient): + response = client.get("/users/me") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_users_token_monica_with_no_jessica(client: TestClient): + response = client.get("/users?token=monica") + assert response.status_code == 400 + assert response.json() == {"detail": "No Jessica token provided"} + + +def test_items_token_jessica(client: TestClient): + response = client.get( + "/items?token=jessica", headers={"X-Token": "fake-super-secret-token"} + ) + assert response.status_code == 200 assert response.json() == { - "detail": [ - { - "loc": ["query", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] + "plumbus": {"name": "Plumbus"}, + "gun": {"name": "Portal Gun"}, } -def test_put_invalid_header(): +def test_items_with_no_token_jessica(client: TestClient): + response = client.get("/items", headers={"X-Token": "fake-super-secret-token"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_items_plumbus_token_jessica(client: TestClient): + response = client.get( + "/items/plumbus?token=jessica", headers={"X-Token": "fake-super-secret-token"} + ) + assert response.status_code == 200 + assert response.json() == {"name": "Plumbus", "item_id": "plumbus"} + + +def test_items_bar_token_jessica(client: TestClient): + response = client.get( + "/items/bar?token=jessica", headers={"X-Token": "fake-super-secret-token"} + ) + assert response.status_code == 404 + assert response.json() == {"detail": "Item not found"} + + +def test_items_plumbus_with_no_token(client: TestClient): + response = client.get( + "/items/plumbus", headers={"X-Token": "fake-super-secret-token"} + ) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_items_with_invalid_token(client: TestClient): + response = client.get("/items?token=jessica", headers={"X-Token": "invalid"}) + assert response.status_code == 400 + assert response.json() == {"detail": "X-Token header invalid"} + + +def test_items_bar_with_invalid_token(client: TestClient): + response = client.get("/items/bar?token=jessica", headers={"X-Token": "invalid"}) + assert response.status_code == 400 + assert response.json() == {"detail": "X-Token header invalid"} + + +def test_items_with_missing_x_token_header(client: TestClient): + response = client.get("/items?token=jessica") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_items_plumbus_with_missing_x_token_header(client: TestClient): + response = client.get("/items/plumbus?token=jessica") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_root_token_jessica(client: TestClient): + response = client.get("/?token=jessica") + assert response.status_code == 200 + assert response.json() == {"message": "Hello Bigger Applications!"} + + +def test_root_with_no_token(client: TestClient): + response = client.get("/") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_put_no_header(client: TestClient): + response = client.put("/items/foo") + assert response.status_code == 422, response.text + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_put_invalid_header(client: TestClient): response = client.put("/items/foo", headers={"X-Token": "invalid"}) assert response.status_code == 400, response.text assert response.json() == {"detail": "X-Token header invalid"} -def test_put(): +def test_put(client: TestClient): response = client.put( "/items/plumbus?token=jessica", headers={"X-Token": "fake-super-secret-token"} ) @@ -140,7 +370,7 @@ def test_put(): assert response.json() == {"item_id": "plumbus", "name": "The great Plumbus"} -def test_put_forbidden(): +def test_put_forbidden(client: TestClient): response = client.put( "/items/bar?token=jessica", headers={"X-Token": "fake-super-secret-token"} ) @@ -148,7 +378,7 @@ def test_put_forbidden(): assert response.json() == {"detail": "You can only update the item: plumbus"} -def test_admin(): +def test_admin(client: TestClient): response = client.post( "/admin/?token=jessica", headers={"X-Token": "fake-super-secret-token"} ) @@ -156,13 +386,13 @@ def test_admin(): assert response.json() == {"message": "Admin getting schwifty"} -def test_admin_invalid_header(): +def test_admin_invalid_header(client: TestClient): response = client.post("/admin/", headers={"X-Token": "invalid"}) assert response.status_code == 400, response.text assert response.json() == {"detail": "X-Token header invalid"} -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { diff --git a/tests/test_tutorial/test_bigger_applications/test_main_an.py b/tests/test_tutorial/test_bigger_applications/test_main_an.py index 8f42d9dd1..c0b77d4a7 100644 --- a/tests/test_tutorial/test_bigger_applications/test_main_an.py +++ b/tests/test_tutorial/test_bigger_applications/test_main_an.py @@ -1,138 +1,368 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url -from docs_src.bigger_applications.app_an.main import app -client = TestClient(app) +@pytest.fixture(name="client") +def get_client(): + from docs_src.bigger_applications.app_an.main import app + client = TestClient(app) + return client -no_jessica = { - "detail": [ + +def test_users_token_jessica(client: TestClient): + response = client.get("/users?token=jessica") + assert response.status_code == 200 + assert response.json() == [{"username": "Rick"}, {"username": "Morty"}] + + +def test_users_with_no_token(client: TestClient): + response = client.get("/users") + assert response.status_code == 422 + assert response.json() == IsDict( { - "loc": ["query", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] -} - - -@pytest.mark.parametrize( - "path,expected_status,expected_response,headers", - [ - ( - "/users?token=jessica", - 200, - [{"username": "Rick"}, {"username": "Morty"}], - {}, - ), - ("/users", 422, no_jessica, {}), - ("/users/foo?token=jessica", 200, {"username": "foo"}, {}), - ("/users/foo", 422, no_jessica, {}), - ("/users/me?token=jessica", 200, {"username": "fakecurrentuser"}, {}), - ("/users/me", 422, no_jessica, {}), - ( - "/users?token=monica", - 400, - {"detail": "No Jessica token provided"}, - {}, - ), - ( - "/items?token=jessica", - 200, - {"plumbus": {"name": "Plumbus"}, "gun": {"name": "Portal Gun"}}, - {"X-Token": "fake-super-secret-token"}, - ), - ("/items", 422, no_jessica, {"X-Token": "fake-super-secret-token"}), - ( - "/items/plumbus?token=jessica", - 200, - {"name": "Plumbus", "item_id": "plumbus"}, - {"X-Token": "fake-super-secret-token"}, - ), - ( - "/items/bar?token=jessica", - 404, - {"detail": "Item not found"}, - {"X-Token": "fake-super-secret-token"}, - ), - ("/items/plumbus", 422, no_jessica, {"X-Token": "fake-super-secret-token"}), - ( - "/items?token=jessica", - 400, - {"detail": "X-Token header invalid"}, - {"X-Token": "invalid"}, - ), - ( - "/items/bar?token=jessica", - 400, - {"detail": "X-Token header invalid"}, - {"X-Token": "invalid"}, - ), - ( - "/items?token=jessica", - 422, - { - "detail": [ - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - } - ] - }, - {}, - ), - ( - "/items/plumbus?token=jessica", - 422, - { - "detail": [ - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - } - ] - }, - {}, - ), - ("/?token=jessica", 200, {"message": "Hello Bigger Applications!"}, {}), - ("/", 422, no_jessica, {}), - ], -) -def test_get_path(path, expected_status, expected_response, headers): - response = client.get(path, headers=headers) - assert response.status_code == expected_status - assert response.json() == expected_response - - -def test_put_no_header(): - response = client.put("/items/foo") - assert response.status_code == 422, response.text + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_users_foo_token_jessica(client: TestClient): + response = client.get("/users/foo?token=jessica") + assert response.status_code == 200 + assert response.json() == {"username": "foo"} + + +def test_users_foo_with_no_token(client: TestClient): + response = client.get("/users/foo") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_users_me_token_jessica(client: TestClient): + response = client.get("/users/me?token=jessica") + assert response.status_code == 200 + assert response.json() == {"username": "fakecurrentuser"} + + +def test_users_me_with_no_token(client: TestClient): + response = client.get("/users/me") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_users_token_monica_with_no_jessica(client: TestClient): + response = client.get("/users?token=monica") + assert response.status_code == 400 + assert response.json() == {"detail": "No Jessica token provided"} + + +def test_items_token_jessica(client: TestClient): + response = client.get( + "/items?token=jessica", headers={"X-Token": "fake-super-secret-token"} + ) + assert response.status_code == 200 assert response.json() == { - "detail": [ - { - "loc": ["query", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] + "plumbus": {"name": "Plumbus"}, + "gun": {"name": "Portal Gun"}, } -def test_put_invalid_header(): +def test_items_with_no_token_jessica(client: TestClient): + response = client.get("/items", headers={"X-Token": "fake-super-secret-token"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_items_plumbus_token_jessica(client: TestClient): + response = client.get( + "/items/plumbus?token=jessica", headers={"X-Token": "fake-super-secret-token"} + ) + assert response.status_code == 200 + assert response.json() == {"name": "Plumbus", "item_id": "plumbus"} + + +def test_items_bar_token_jessica(client: TestClient): + response = client.get( + "/items/bar?token=jessica", headers={"X-Token": "fake-super-secret-token"} + ) + assert response.status_code == 404 + assert response.json() == {"detail": "Item not found"} + + +def test_items_plumbus_with_no_token(client: TestClient): + response = client.get( + "/items/plumbus", headers={"X-Token": "fake-super-secret-token"} + ) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_items_with_invalid_token(client: TestClient): + response = client.get("/items?token=jessica", headers={"X-Token": "invalid"}) + assert response.status_code == 400 + assert response.json() == {"detail": "X-Token header invalid"} + + +def test_items_bar_with_invalid_token(client: TestClient): + response = client.get("/items/bar?token=jessica", headers={"X-Token": "invalid"}) + assert response.status_code == 400 + assert response.json() == {"detail": "X-Token header invalid"} + + +def test_items_with_missing_x_token_header(client: TestClient): + response = client.get("/items?token=jessica") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_items_plumbus_with_missing_x_token_header(client: TestClient): + response = client.get("/items/plumbus?token=jessica") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_root_token_jessica(client: TestClient): + response = client.get("/?token=jessica") + assert response.status_code == 200 + assert response.json() == {"message": "Hello Bigger Applications!"} + + +def test_root_with_no_token(client: TestClient): + response = client.get("/") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_put_no_header(client: TestClient): + response = client.put("/items/foo") + assert response.status_code == 422, response.text + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_put_invalid_header(client: TestClient): response = client.put("/items/foo", headers={"X-Token": "invalid"}) assert response.status_code == 400, response.text assert response.json() == {"detail": "X-Token header invalid"} -def test_put(): +def test_put(client: TestClient): response = client.put( "/items/plumbus?token=jessica", headers={"X-Token": "fake-super-secret-token"} ) @@ -140,7 +370,7 @@ def test_put(): assert response.json() == {"item_id": "plumbus", "name": "The great Plumbus"} -def test_put_forbidden(): +def test_put_forbidden(client: TestClient): response = client.put( "/items/bar?token=jessica", headers={"X-Token": "fake-super-secret-token"} ) @@ -148,7 +378,7 @@ def test_put_forbidden(): assert response.json() == {"detail": "You can only update the item: plumbus"} -def test_admin(): +def test_admin(client: TestClient): response = client.post( "/admin/?token=jessica", headers={"X-Token": "fake-super-secret-token"} ) @@ -156,13 +386,13 @@ def test_admin(): assert response.json() == {"message": "Admin getting schwifty"} -def test_admin_invalid_header(): +def test_admin_invalid_header(client: TestClient): response = client.post("/admin/", headers={"X-Token": "invalid"}) assert response.status_code == 400, response.text assert response.json() == {"detail": "X-Token header invalid"} -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { diff --git a/tests/test_tutorial/test_bigger_applications/test_main_an_py39.py b/tests/test_tutorial/test_bigger_applications/test_main_an_py39.py index 44694e371..948331b5d 100644 --- a/tests/test_tutorial/test_bigger_applications/test_main_an_py39.py +++ b/tests/test_tutorial/test_bigger_applications/test_main_an_py39.py @@ -1,18 +1,10 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from ...utils import needs_py39 -no_jessica = { - "detail": [ - { - "loc": ["query", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] -} - @pytest.fixture(name="client") def get_client(): @@ -23,116 +15,366 @@ def get_client(): @needs_py39 -@pytest.mark.parametrize( - "path,expected_status,expected_response,headers", - [ - ( - "/users?token=jessica", - 200, - [{"username": "Rick"}, {"username": "Morty"}], - {}, - ), - ("/users", 422, no_jessica, {}), - ("/users/foo?token=jessica", 200, {"username": "foo"}, {}), - ("/users/foo", 422, no_jessica, {}), - ("/users/me?token=jessica", 200, {"username": "fakecurrentuser"}, {}), - ("/users/me", 422, no_jessica, {}), - ( - "/users?token=monica", - 400, - {"detail": "No Jessica token provided"}, - {}, - ), - ( - "/items?token=jessica", - 200, - {"plumbus": {"name": "Plumbus"}, "gun": {"name": "Portal Gun"}}, - {"X-Token": "fake-super-secret-token"}, - ), - ("/items", 422, no_jessica, {"X-Token": "fake-super-secret-token"}), - ( - "/items/plumbus?token=jessica", - 200, - {"name": "Plumbus", "item_id": "plumbus"}, - {"X-Token": "fake-super-secret-token"}, - ), - ( - "/items/bar?token=jessica", - 404, - {"detail": "Item not found"}, - {"X-Token": "fake-super-secret-token"}, - ), - ("/items/plumbus", 422, no_jessica, {"X-Token": "fake-super-secret-token"}), - ( - "/items?token=jessica", - 400, - {"detail": "X-Token header invalid"}, - {"X-Token": "invalid"}, - ), - ( - "/items/bar?token=jessica", - 400, - {"detail": "X-Token header invalid"}, - {"X-Token": "invalid"}, - ), - ( - "/items?token=jessica", - 422, - { - "detail": [ - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - } - ] - }, - {}, - ), - ( - "/items/plumbus?token=jessica", - 422, - { - "detail": [ - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - } - ] - }, - {}, - ), - ("/?token=jessica", 200, {"message": "Hello Bigger Applications!"}, {}), - ("/", 422, no_jessica, {}), - ], -) -def test_get_path( - path, expected_status, expected_response, headers, client: TestClient -): - response = client.get(path, headers=headers) - assert response.status_code == expected_status - assert response.json() == expected_response +def test_users_token_jessica(client: TestClient): + response = client.get("/users?token=jessica") + assert response.status_code == 200 + assert response.json() == [{"username": "Rick"}, {"username": "Morty"}] + + +@needs_py39 +def test_users_with_no_token(client: TestClient): + response = client.get("/users") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +@needs_py39 +def test_users_foo_token_jessica(client: TestClient): + response = client.get("/users/foo?token=jessica") + assert response.status_code == 200 + assert response.json() == {"username": "foo"} + + +@needs_py39 +def test_users_foo_with_no_token(client: TestClient): + response = client.get("/users/foo") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +@needs_py39 +def test_users_me_token_jessica(client: TestClient): + response = client.get("/users/me?token=jessica") + assert response.status_code == 200 + assert response.json() == {"username": "fakecurrentuser"} + + +@needs_py39 +def test_users_me_with_no_token(client: TestClient): + response = client.get("/users/me") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +@needs_py39 +def test_users_token_monica_with_no_jessica(client: TestClient): + response = client.get("/users?token=monica") + assert response.status_code == 400 + assert response.json() == {"detail": "No Jessica token provided"} + + +@needs_py39 +def test_items_token_jessica(client: TestClient): + response = client.get( + "/items?token=jessica", headers={"X-Token": "fake-super-secret-token"} + ) + assert response.status_code == 200 + assert response.json() == { + "plumbus": {"name": "Plumbus"}, + "gun": {"name": "Portal Gun"}, + } + + +@needs_py39 +def test_items_with_no_token_jessica(client: TestClient): + response = client.get("/items", headers={"X-Token": "fake-super-secret-token"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +@needs_py39 +def test_items_plumbus_token_jessica(client: TestClient): + response = client.get( + "/items/plumbus?token=jessica", headers={"X-Token": "fake-super-secret-token"} + ) + assert response.status_code == 200 + assert response.json() == {"name": "Plumbus", "item_id": "plumbus"} + + +@needs_py39 +def test_items_bar_token_jessica(client: TestClient): + response = client.get( + "/items/bar?token=jessica", headers={"X-Token": "fake-super-secret-token"} + ) + assert response.status_code == 404 + assert response.json() == {"detail": "Item not found"} + + +@needs_py39 +def test_items_plumbus_with_no_token(client: TestClient): + response = client.get( + "/items/plumbus", headers={"X-Token": "fake-super-secret-token"} + ) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +@needs_py39 +def test_items_with_invalid_token(client: TestClient): + response = client.get("/items?token=jessica", headers={"X-Token": "invalid"}) + assert response.status_code == 400 + assert response.json() == {"detail": "X-Token header invalid"} + + +@needs_py39 +def test_items_bar_with_invalid_token(client: TestClient): + response = client.get("/items/bar?token=jessica", headers={"X-Token": "invalid"}) + assert response.status_code == 400 + assert response.json() == {"detail": "X-Token header invalid"} + + +@needs_py39 +def test_items_with_missing_x_token_header(client: TestClient): + response = client.get("/items?token=jessica") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +@needs_py39 +def test_items_plumbus_with_missing_x_token_header(client: TestClient): + response = client.get("/items/plumbus?token=jessica") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +@needs_py39 +def test_root_token_jessica(client: TestClient): + response = client.get("/?token=jessica") + assert response.status_code == 200 + assert response.json() == {"message": "Hello Bigger Applications!"} + + +@needs_py39 +def test_root_with_no_token(client: TestClient): + response = client.get("/") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) @needs_py39 def test_put_no_header(client: TestClient): response = client.put("/items/foo") assert response.status_code == 422, response.text - assert response.json() == { - "detail": [ - { - "loc": ["query", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) @needs_py39 diff --git a/tests/test_tutorial/test_body/test_tutorial001.py b/tests/test_tutorial/test_body/test_tutorial001.py index 469198e0f..2476b773f 100644 --- a/tests/test_tutorial/test_body/test_tutorial001.py +++ b/tests/test_tutorial/test_body/test_tutorial001.py @@ -1,134 +1,268 @@ from unittest.mock import patch import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url -from docs_src.body.tutorial001 import app -client = TestClient(app) +@pytest.fixture +def client(): + from docs_src.body.tutorial001 import app + client = TestClient(app) + return client -price_missing = { - "detail": [ + +def test_body_float(client: TestClient): + response = client.post("/items/", json={"name": "Foo", "price": 50.5}) + assert response.status_code == 200 + assert response.json() == { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + } + + +def test_post_with_str_float(client: TestClient): + response = client.post("/items/", json={"name": "Foo", "price": "50.5"}) + assert response.status_code == 200 + assert response.json() == { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + } + + +def test_post_with_str_float_description(client: TestClient): + response = client.post( + "/items/", json={"name": "Foo", "price": "50.5", "description": "Some Foo"} + ) + assert response.status_code == 200 + assert response.json() == { + "name": "Foo", + "price": 50.5, + "description": "Some Foo", + "tax": None, + } + + +def test_post_with_str_float_description_tax(client: TestClient): + response = client.post( + "/items/", + json={"name": "Foo", "price": "50.5", "description": "Some Foo", "tax": 0.3}, + ) + assert response.status_code == 200 + assert response.json() == { + "name": "Foo", + "price": 50.5, + "description": "Some Foo", + "tax": 0.3, + } + + +def test_post_with_only_name(client: TestClient): + response = client.post("/items/", json={"name": "Foo"}) + assert response.status_code == 422 + assert response.json() == IsDict( { - "loc": ["body", "price"], - "msg": "field required", - "type": "value_error.missing", + "detail": [ + { + "type": "missing", + "loc": ["body", "price"], + "msg": "Field required", + "input": {"name": "Foo"}, + "url": match_pydantic_error_url("missing"), + } + ] } - ] -} + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "price"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + -price_not_float = { - "detail": [ +def test_post_with_only_name_price(client: TestClient): + response = client.post("/items/", json={"name": "Foo", "price": "twenty"}) + assert response.status_code == 422 + assert response.json() == IsDict( { - "loc": ["body", "price"], - "msg": "value is not a valid float", - "type": "type_error.float", + "detail": [ + { + "type": "float_parsing", + "loc": ["body", "price"], + "msg": "Input should be a valid number, unable to parse string as a number", + "input": "twenty", + "url": match_pydantic_error_url("float_parsing"), + } + ] } - ] -} + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "price"], + "msg": "value is not a valid float", + "type": "type_error.float", + } + ] + } + ) -name_price_missing = { - "detail": [ + +def test_post_with_no_data(client: TestClient): + response = client.post("/items/", json={}) + assert response.status_code == 422 + assert response.json() == IsDict( { - "loc": ["body", "name"], - "msg": "field required", - "type": "value_error.missing", - }, + "detail": [ + { + "type": "missing", + "loc": ["body", "name"], + "msg": "Field required", + "input": {}, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "price"], + "msg": "Field required", + "input": {}, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 { - "loc": ["body", "price"], - "msg": "field required", - "type": "value_error.missing", - }, - ] -} - -body_missing = { - "detail": [ - {"loc": ["body"], "msg": "field required", "type": "value_error.missing"} - ] -} - - -@pytest.mark.parametrize( - "path,body,expected_status,expected_response", - [ - ( - "/items/", - {"name": "Foo", "price": 50.5}, - 200, - {"name": "Foo", "price": 50.5, "description": None, "tax": None}, - ), - ( - "/items/", - {"name": "Foo", "price": "50.5"}, - 200, - {"name": "Foo", "price": 50.5, "description": None, "tax": None}, - ), - ( - "/items/", - {"name": "Foo", "price": "50.5", "description": "Some Foo"}, - 200, - {"name": "Foo", "price": 50.5, "description": "Some Foo", "tax": None}, - ), - ( - "/items/", - {"name": "Foo", "price": "50.5", "description": "Some Foo", "tax": 0.3}, - 200, - {"name": "Foo", "price": 50.5, "description": "Some Foo", "tax": 0.3}, - ), - ("/items/", {"name": "Foo"}, 422, price_missing), - ("/items/", {"name": "Foo", "price": "twenty"}, 422, price_not_float), - ("/items/", {}, 422, name_price_missing), - ("/items/", None, 422, body_missing), - ], -) -def test_post_body(path, body, expected_status, expected_response): - response = client.post(path, json=body) - assert response.status_code == expected_status - assert response.json() == expected_response - - -def test_post_broken_body(): + "detail": [ + { + "loc": ["body", "name"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "price"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_post_with_none(client: TestClient): + response = client.post("/items/", json=None) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_post_broken_body(client: TestClient): response = client.post( "/items/", headers={"content-type": "application/json"}, content="{some broken json}", ) assert response.status_code == 422, response.text - assert response.json() == { - "detail": [ - { - "loc": ["body", 1], - "msg": "Expecting property name enclosed in double quotes: line 1 column 2 (char 1)", - "type": "value_error.jsondecode", - "ctx": { - "msg": "Expecting property name enclosed in double quotes", - "doc": "{some broken json}", - "pos": 1, - "lineno": 1, - "colno": 2, - }, - } - ] - } + assert response.json() == IsDict( + { + "detail": [ + { + "type": "json_invalid", + "loc": ["body", 1], + "msg": "JSON decode error", + "input": {}, + "ctx": { + "error": "Expecting property name enclosed in double quotes" + }, + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", 1], + "msg": "Expecting property name enclosed in double quotes: line 1 column 2 (char 1)", + "type": "value_error.jsondecode", + "ctx": { + "msg": "Expecting property name enclosed in double quotes", + "doc": "{some broken json}", + "pos": 1, + "lineno": 1, + "colno": 2, + }, + } + ] + } + ) -def test_post_form_for_json(): +def test_post_form_for_json(client: TestClient): response = client.post("/items/", data={"name": "Foo", "price": 50.5}) assert response.status_code == 422, response.text - assert response.json() == { - "detail": [ - { - "loc": ["body"], - "msg": "value is not a valid dict", - "type": "type_error.dict", - } - ] - } + assert response.json() == IsDict( + { + "detail": [ + { + "type": "model_attributes_type", + "loc": ["body"], + "msg": "Input should be a valid dictionary or object to extract fields from", + "input": "name=Foo&price=50.5", + "url": match_pydantic_error_url("model_attributes_type"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body"], + "msg": "value is not a valid dict", + "type": "type_error.dict", + } + ] + } + ) -def test_explicit_content_type(): +def test_explicit_content_type(client: TestClient): response = client.post( "/items/", content='{"name": "Foo", "price": 50.5}', @@ -137,7 +271,7 @@ def test_explicit_content_type(): assert response.status_code == 200, response.text -def test_geo_json(): +def test_geo_json(client: TestClient): response = client.post( "/items/", content='{"name": "Foo", "price": 50.5}', @@ -146,7 +280,7 @@ def test_geo_json(): assert response.status_code == 200, response.text -def test_no_content_type_is_json(): +def test_no_content_type_is_json(client: TestClient): response = client.post( "/items/", content='{"name": "Foo", "price": 50.5}', @@ -160,43 +294,104 @@ def test_no_content_type_is_json(): } -def test_wrong_headers(): +def test_wrong_headers(client: TestClient): data = '{"name": "Foo", "price": 50.5}' - invalid_dict = { - "detail": [ - { - "loc": ["body"], - "msg": "value is not a valid dict", - "type": "type_error.dict", - } - ] - } - response = client.post( "/items/", content=data, headers={"Content-Type": "text/plain"} ) assert response.status_code == 422, response.text - assert response.json() == invalid_dict + assert response.json() == IsDict( + { + "detail": [ + { + "type": "model_attributes_type", + "loc": ["body"], + "msg": "Input should be a valid dictionary or object to extract fields from", + "input": '{"name": "Foo", "price": 50.5}', + "url": match_pydantic_error_url( + "model_attributes_type" + ), # "https://errors.pydantic.dev/0.38.0/v/dict_attributes_type", + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body"], + "msg": "value is not a valid dict", + "type": "type_error.dict", + } + ] + } + ) response = client.post( "/items/", content=data, headers={"Content-Type": "application/geo+json-seq"} ) assert response.status_code == 422, response.text - assert response.json() == invalid_dict + assert response.json() == IsDict( + { + "detail": [ + { + "type": "model_attributes_type", + "loc": ["body"], + "msg": "Input should be a valid dictionary or object to extract fields from", + "input": '{"name": "Foo", "price": 50.5}', + "url": match_pydantic_error_url("model_attributes_type"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body"], + "msg": "value is not a valid dict", + "type": "type_error.dict", + } + ] + } + ) response = client.post( "/items/", content=data, headers={"Content-Type": "application/not-really-json"} ) assert response.status_code == 422, response.text - assert response.json() == invalid_dict + assert response.json() == IsDict( + { + "detail": [ + { + "type": "model_attributes_type", + "loc": ["body"], + "msg": "Input should be a valid dictionary or object to extract fields from", + "input": '{"name": "Foo", "price": 50.5}', + "url": match_pydantic_error_url("model_attributes_type"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body"], + "msg": "value is not a valid dict", + "type": "type_error.dict", + } + ] + } + ) -def test_other_exceptions(): +def test_other_exceptions(client: TestClient): with patch("json.loads", side_effect=Exception): response = client.post("/items/", json={"test": "test2"}) assert response.status_code == 400, response.text -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { @@ -243,8 +438,26 @@ def test_openapi_schema(): "properties": { "name": {"title": "Name", "type": "string"}, "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_body/test_tutorial001_py310.py b/tests/test_tutorial/test_body/test_tutorial001_py310.py index a68b4e044..b64d86005 100644 --- a/tests/test_tutorial/test_body/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_body/test_tutorial001_py310.py @@ -1,7 +1,9 @@ from unittest.mock import patch import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from ...utils import needs_py310 @@ -14,86 +16,189 @@ def client(): return client -price_missing = { - "detail": [ +@needs_py310 +def test_body_float(client: TestClient): + response = client.post("/items/", json={"name": "Foo", "price": 50.5}) + assert response.status_code == 200 + assert response.json() == { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + } + + +@needs_py310 +def test_post_with_str_float(client: TestClient): + response = client.post("/items/", json={"name": "Foo", "price": "50.5"}) + assert response.status_code == 200 + assert response.json() == { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + } + + +@needs_py310 +def test_post_with_str_float_description(client: TestClient): + response = client.post( + "/items/", json={"name": "Foo", "price": "50.5", "description": "Some Foo"} + ) + assert response.status_code == 200 + assert response.json() == { + "name": "Foo", + "price": 50.5, + "description": "Some Foo", + "tax": None, + } + + +@needs_py310 +def test_post_with_str_float_description_tax(client: TestClient): + response = client.post( + "/items/", + json={"name": "Foo", "price": "50.5", "description": "Some Foo", "tax": 0.3}, + ) + assert response.status_code == 200 + assert response.json() == { + "name": "Foo", + "price": 50.5, + "description": "Some Foo", + "tax": 0.3, + } + + +@needs_py310 +def test_post_with_only_name(client: TestClient): + response = client.post("/items/", json={"name": "Foo"}) + assert response.status_code == 422 + assert response.json() == IsDict( { - "loc": ["body", "price"], - "msg": "field required", - "type": "value_error.missing", + "detail": [ + { + "type": "missing", + "loc": ["body", "price"], + "msg": "Field required", + "input": {"name": "Foo"}, + "url": match_pydantic_error_url("missing"), + } + ] } - ] -} - -price_not_float = { - "detail": [ + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 { - "loc": ["body", "price"], - "msg": "value is not a valid float", - "type": "type_error.float", + "detail": [ + { + "loc": ["body", "price"], + "msg": "field required", + "type": "value_error.missing", + } + ] } - ] -} + ) + -name_price_missing = { - "detail": [ +@needs_py310 +def test_post_with_only_name_price(client: TestClient): + response = client.post("/items/", json={"name": "Foo", "price": "twenty"}) + assert response.status_code == 422 + assert response.json() == IsDict( { - "loc": ["body", "name"], - "msg": "field required", - "type": "value_error.missing", - }, + "detail": [ + { + "type": "float_parsing", + "loc": ["body", "price"], + "msg": "Input should be a valid number, unable to parse string as a number", + "input": "twenty", + "url": match_pydantic_error_url("float_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 { - "loc": ["body", "price"], - "msg": "field required", - "type": "value_error.missing", - }, - ] -} + "detail": [ + { + "loc": ["body", "price"], + "msg": "value is not a valid float", + "type": "type_error.float", + } + ] + } + ) -body_missing = { - "detail": [ - {"loc": ["body"], "msg": "field required", "type": "value_error.missing"} - ] -} + +@needs_py310 +def test_post_with_no_data(client: TestClient): + response = client.post("/items/", json={}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "name"], + "msg": "Field required", + "input": {}, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "price"], + "msg": "Field required", + "input": {}, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "name"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "price"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) @needs_py310 -@pytest.mark.parametrize( - "path,body,expected_status,expected_response", - [ - ( - "/items/", - {"name": "Foo", "price": 50.5}, - 200, - {"name": "Foo", "price": 50.5, "description": None, "tax": None}, - ), - ( - "/items/", - {"name": "Foo", "price": "50.5"}, - 200, - {"name": "Foo", "price": 50.5, "description": None, "tax": None}, - ), - ( - "/items/", - {"name": "Foo", "price": "50.5", "description": "Some Foo"}, - 200, - {"name": "Foo", "price": 50.5, "description": "Some Foo", "tax": None}, - ), - ( - "/items/", - {"name": "Foo", "price": "50.5", "description": "Some Foo", "tax": 0.3}, - 200, - {"name": "Foo", "price": 50.5, "description": "Some Foo", "tax": 0.3}, - ), - ("/items/", {"name": "Foo"}, 422, price_missing), - ("/items/", {"name": "Foo", "price": "twenty"}, 422, price_not_float), - ("/items/", {}, 422, name_price_missing), - ("/items/", None, 422, body_missing), - ], -) -def test_post_body(path, body, expected_status, expected_response, client: TestClient): - response = client.post(path, json=body) - assert response.status_code == expected_status - assert response.json() == expected_response +def test_post_with_none(client: TestClient): + response = client.post("/items/", json=None) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) @needs_py310 @@ -104,37 +209,69 @@ def test_post_broken_body(client: TestClient): content="{some broken json}", ) assert response.status_code == 422, response.text - assert response.json() == { - "detail": [ - { - "loc": ["body", 1], - "msg": "Expecting property name enclosed in double quotes: line 1 column 2 (char 1)", - "type": "value_error.jsondecode", - "ctx": { - "msg": "Expecting property name enclosed in double quotes", - "doc": "{some broken json}", - "pos": 1, - "lineno": 1, - "colno": 2, - }, - } - ] - } + assert response.json() == IsDict( + { + "detail": [ + { + "type": "json_invalid", + "loc": ["body", 1], + "msg": "JSON decode error", + "input": {}, + "ctx": { + "error": "Expecting property name enclosed in double quotes" + }, + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", 1], + "msg": "Expecting property name enclosed in double quotes: line 1 column 2 (char 1)", + "type": "value_error.jsondecode", + "ctx": { + "msg": "Expecting property name enclosed in double quotes", + "doc": "{some broken json}", + "pos": 1, + "lineno": 1, + "colno": 2, + }, + } + ] + } + ) @needs_py310 def test_post_form_for_json(client: TestClient): response = client.post("/items/", data={"name": "Foo", "price": 50.5}) assert response.status_code == 422, response.text - assert response.json() == { - "detail": [ - { - "loc": ["body"], - "msg": "value is not a valid dict", - "type": "type_error.dict", - } - ] - } + assert response.json() == IsDict( + { + "detail": [ + { + "type": "model_attributes_type", + "loc": ["body"], + "msg": "Input should be a valid dictionary or object to extract fields from", + "input": "name=Foo&price=50.5", + "url": match_pydantic_error_url("model_attributes_type"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body"], + "msg": "value is not a valid dict", + "type": "type_error.dict", + } + ] + } + ) @needs_py310 @@ -175,32 +312,91 @@ def test_no_content_type_is_json(client: TestClient): @needs_py310 def test_wrong_headers(client: TestClient): data = '{"name": "Foo", "price": 50.5}' - invalid_dict = { - "detail": [ - { - "loc": ["body"], - "msg": "value is not a valid dict", - "type": "type_error.dict", - } - ] - } - response = client.post( "/items/", content=data, headers={"Content-Type": "text/plain"} ) assert response.status_code == 422, response.text - assert response.json() == invalid_dict + assert response.json() == IsDict( + { + "detail": [ + { + "type": "model_attributes_type", + "loc": ["body"], + "msg": "Input should be a valid dictionary or object to extract fields from", + "input": '{"name": "Foo", "price": 50.5}', + "url": match_pydantic_error_url("model_attributes_type"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body"], + "msg": "value is not a valid dict", + "type": "type_error.dict", + } + ] + } + ) response = client.post( "/items/", content=data, headers={"Content-Type": "application/geo+json-seq"} ) assert response.status_code == 422, response.text - assert response.json() == invalid_dict + assert response.json() == IsDict( + { + "detail": [ + { + "type": "model_attributes_type", + "loc": ["body"], + "msg": "Input should be a valid dictionary or object to extract fields from", + "input": '{"name": "Foo", "price": 50.5}', + "url": match_pydantic_error_url("model_attributes_type"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body"], + "msg": "value is not a valid dict", + "type": "type_error.dict", + } + ] + } + ) response = client.post( "/items/", content=data, headers={"Content-Type": "application/not-really-json"} ) assert response.status_code == 422, response.text - assert response.json() == invalid_dict + assert response.json() == IsDict( + { + "detail": [ + { + "type": "model_attributes_type", + "loc": ["body"], + "msg": "Input should be a valid dictionary or object to extract fields from", + "input": '{"name": "Foo", "price": 50.5}', + "url": match_pydantic_error_url("model_attributes_type"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body"], + "msg": "value is not a valid dict", + "type": "type_error.dict", + } + ] + } + ) @needs_py310 @@ -258,8 +454,26 @@ def test_openapi_schema(client: TestClient): "properties": { "name": {"title": "Name", "type": "string"}, "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_body_fields/test_tutorial001.py b/tests/test_tutorial/test_body_fields/test_tutorial001.py index 4999cbf6b..1ff2d9576 100644 --- a/tests/test_tutorial/test_body_fields/test_tutorial001.py +++ b/tests/test_tutorial/test_body_fields/test_tutorial001.py @@ -1,66 +1,82 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url -from docs_src.body_fields.tutorial001 import app -client = TestClient(app) +@pytest.fixture(name="client") +def get_client(): + from docs_src.body_fields.tutorial001 import app + client = TestClient(app) + return client -price_not_greater = { - "detail": [ - { - "ctx": {"limit_value": 0}, - "loc": ["body", "item", "price"], - "msg": "ensure this value is greater than 0", - "type": "value_error.number.not_gt", - } - ] -} + +def test_items_5(client: TestClient): + response = client.put("/items/5", json={"item": {"name": "Foo", "price": 3.0}}) + assert response.status_code == 200 + assert response.json() == { + "item_id": 5, + "item": {"name": "Foo", "price": 3.0, "description": None, "tax": None}, + } + + +def test_items_6(client: TestClient): + response = client.put( + "/items/6", + json={ + "item": { + "name": "Bar", + "price": 0.2, + "description": "Some bar", + "tax": "5.4", + } + }, + ) + assert response.status_code == 200 + assert response.json() == { + "item_id": 6, + "item": { + "name": "Bar", + "price": 0.2, + "description": "Some bar", + "tax": 5.4, + }, + } -@pytest.mark.parametrize( - "path,body,expected_status,expected_response", - [ - ( - "/items/5", - {"item": {"name": "Foo", "price": 3.0}}, - 200, - { - "item_id": 5, - "item": {"name": "Foo", "price": 3.0, "description": None, "tax": None}, - }, - ), - ( - "/items/6", - { - "item": { - "name": "Bar", - "price": 0.2, - "description": "Some bar", - "tax": "5.4", +def test_invalid_price(client: TestClient): + response = client.put("/items/5", json={"item": {"name": "Foo", "price": -3.0}}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "greater_than", + "loc": ["body", "item", "price"], + "msg": "Input should be greater than 0", + "input": -3.0, + "ctx": {"gt": 0.0}, + "url": match_pydantic_error_url("greater_than"), } - }, - 200, - { - "item_id": 6, - "item": { - "name": "Bar", - "price": 0.2, - "description": "Some bar", - "tax": 5.4, - }, - }, - ), - ("/items/5", {"item": {"name": "Foo", "price": -3.0}}, 422, price_not_greater), - ], -) -def test(path, body, expected_status, expected_response): - response = client.put(path, json=body) - assert response.status_code == expected_status - assert response.json() == expected_response + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "ctx": {"limit_value": 0}, + "loc": ["body", "item", "price"], + "msg": "ensure this value is greater than 0", + "type": "value_error.number.not_gt", + } + ] + } + ) -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { @@ -116,18 +132,39 @@ def test_openapi_schema(): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, - "description": { - "title": "The description of the item", - "maxLength": 300, - "type": "string", - }, + "description": IsDict( + { + "title": "The description of the item", + "anyOf": [ + {"maxLength": 300, "type": "string"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "The description of the item", + "maxLength": 300, + "type": "string", + } + ), "price": { "title": "Price", "exclusiveMinimum": 0.0, "type": "number", "description": "The price must be greater than zero", }, - "tax": {"title": "Tax", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), }, }, "Body_update_item_items__item_id__put": { diff --git a/tests/test_tutorial/test_body_fields/test_tutorial001_an.py b/tests/test_tutorial/test_body_fields/test_tutorial001_an.py index 011946d07..907d6842a 100644 --- a/tests/test_tutorial/test_body_fields/test_tutorial001_an.py +++ b/tests/test_tutorial/test_body_fields/test_tutorial001_an.py @@ -1,66 +1,82 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url -from docs_src.body_fields.tutorial001_an import app -client = TestClient(app) +@pytest.fixture(name="client") +def get_client(): + from docs_src.body_fields.tutorial001_an import app + client = TestClient(app) + return client -price_not_greater = { - "detail": [ - { - "ctx": {"limit_value": 0}, - "loc": ["body", "item", "price"], - "msg": "ensure this value is greater than 0", - "type": "value_error.number.not_gt", - } - ] -} + +def test_items_5(client: TestClient): + response = client.put("/items/5", json={"item": {"name": "Foo", "price": 3.0}}) + assert response.status_code == 200 + assert response.json() == { + "item_id": 5, + "item": {"name": "Foo", "price": 3.0, "description": None, "tax": None}, + } + + +def test_items_6(client: TestClient): + response = client.put( + "/items/6", + json={ + "item": { + "name": "Bar", + "price": 0.2, + "description": "Some bar", + "tax": "5.4", + } + }, + ) + assert response.status_code == 200 + assert response.json() == { + "item_id": 6, + "item": { + "name": "Bar", + "price": 0.2, + "description": "Some bar", + "tax": 5.4, + }, + } -@pytest.mark.parametrize( - "path,body,expected_status,expected_response", - [ - ( - "/items/5", - {"item": {"name": "Foo", "price": 3.0}}, - 200, - { - "item_id": 5, - "item": {"name": "Foo", "price": 3.0, "description": None, "tax": None}, - }, - ), - ( - "/items/6", - { - "item": { - "name": "Bar", - "price": 0.2, - "description": "Some bar", - "tax": "5.4", +def test_invalid_price(client: TestClient): + response = client.put("/items/5", json={"item": {"name": "Foo", "price": -3.0}}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "greater_than", + "loc": ["body", "item", "price"], + "msg": "Input should be greater than 0", + "input": -3.0, + "ctx": {"gt": 0.0}, + "url": match_pydantic_error_url("greater_than"), } - }, - 200, - { - "item_id": 6, - "item": { - "name": "Bar", - "price": 0.2, - "description": "Some bar", - "tax": 5.4, - }, - }, - ), - ("/items/5", {"item": {"name": "Foo", "price": -3.0}}, 422, price_not_greater), - ], -) -def test(path, body, expected_status, expected_response): - response = client.put(path, json=body) - assert response.status_code == expected_status - assert response.json() == expected_response + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "ctx": {"limit_value": 0}, + "loc": ["body", "item", "price"], + "msg": "ensure this value is greater than 0", + "type": "value_error.number.not_gt", + } + ] + } + ) -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { @@ -116,18 +132,39 @@ def test_openapi_schema(): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, - "description": { - "title": "The description of the item", - "maxLength": 300, - "type": "string", - }, + "description": IsDict( + { + "title": "The description of the item", + "anyOf": [ + {"maxLength": 300, "type": "string"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "The description of the item", + "maxLength": 300, + "type": "string", + } + ), "price": { "title": "Price", "exclusiveMinimum": 0.0, "type": "number", "description": "The price must be greater than zero", }, - "tax": {"title": "Tax", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), }, }, "Body_update_item_items__item_id__put": { diff --git a/tests/test_tutorial/test_body_fields/test_tutorial001_an_py310.py b/tests/test_tutorial/test_body_fields/test_tutorial001_an_py310.py index e7dcb54e9..431d2d181 100644 --- a/tests/test_tutorial/test_body_fields/test_tutorial001_an_py310.py +++ b/tests/test_tutorial/test_body_fields/test_tutorial001_an_py310.py @@ -1,5 +1,7 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from ...utils import needs_py310 @@ -12,59 +14,71 @@ def get_client(): return client -price_not_greater = { - "detail": [ - { - "ctx": {"limit_value": 0}, - "loc": ["body", "item", "price"], - "msg": "ensure this value is greater than 0", - "type": "value_error.number.not_gt", - } - ] -} +@needs_py310 +def test_items_5(client: TestClient): + response = client.put("/items/5", json={"item": {"name": "Foo", "price": 3.0}}) + assert response.status_code == 200 + assert response.json() == { + "item_id": 5, + "item": {"name": "Foo", "price": 3.0, "description": None, "tax": None}, + } @needs_py310 -@pytest.mark.parametrize( - "path,body,expected_status,expected_response", - [ - ( - "/items/5", - {"item": {"name": "Foo", "price": 3.0}}, - 200, - { - "item_id": 5, - "item": {"name": "Foo", "price": 3.0, "description": None, "tax": None}, - }, - ), - ( - "/items/6", - { - "item": { - "name": "Bar", - "price": 0.2, - "description": "Some bar", - "tax": "5.4", +def test_items_6(client: TestClient): + response = client.put( + "/items/6", + json={ + "item": { + "name": "Bar", + "price": 0.2, + "description": "Some bar", + "tax": "5.4", + } + }, + ) + assert response.status_code == 200 + assert response.json() == { + "item_id": 6, + "item": { + "name": "Bar", + "price": 0.2, + "description": "Some bar", + "tax": 5.4, + }, + } + + +@needs_py310 +def test_invalid_price(client: TestClient): + response = client.put("/items/5", json={"item": {"name": "Foo", "price": -3.0}}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "greater_than", + "loc": ["body", "item", "price"], + "msg": "Input should be greater than 0", + "input": -3.0, + "ctx": {"gt": 0.0}, + "url": match_pydantic_error_url("greater_than"), } - }, - 200, - { - "item_id": 6, - "item": { - "name": "Bar", - "price": 0.2, - "description": "Some bar", - "tax": 5.4, - }, - }, - ), - ("/items/5", {"item": {"name": "Foo", "price": -3.0}}, 422, price_not_greater), - ], -) -def test(path, body, expected_status, expected_response, client: TestClient): - response = client.put(path, json=body) - assert response.status_code == expected_status - assert response.json() == expected_response + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "ctx": {"limit_value": 0}, + "loc": ["body", "item", "price"], + "msg": "ensure this value is greater than 0", + "type": "value_error.number.not_gt", + } + ] + } + ) @needs_py310 @@ -124,18 +138,39 @@ def test_openapi_schema(client: TestClient): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, - "description": { - "title": "The description of the item", - "maxLength": 300, - "type": "string", - }, + "description": IsDict( + { + "title": "The description of the item", + "anyOf": [ + {"maxLength": 300, "type": "string"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "The description of the item", + "maxLength": 300, + "type": "string", + } + ), "price": { "title": "Price", "exclusiveMinimum": 0.0, "type": "number", "description": "The price must be greater than zero", }, - "tax": {"title": "Tax", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), }, }, "Body_update_item_items__item_id__put": { diff --git a/tests/test_tutorial/test_body_fields/test_tutorial001_an_py39.py b/tests/test_tutorial/test_body_fields/test_tutorial001_an_py39.py index f1015a03b..8cef6c154 100644 --- a/tests/test_tutorial/test_body_fields/test_tutorial001_an_py39.py +++ b/tests/test_tutorial/test_body_fields/test_tutorial001_an_py39.py @@ -1,5 +1,7 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from ...utils import needs_py39 @@ -12,59 +14,71 @@ def get_client(): return client -price_not_greater = { - "detail": [ - { - "ctx": {"limit_value": 0}, - "loc": ["body", "item", "price"], - "msg": "ensure this value is greater than 0", - "type": "value_error.number.not_gt", - } - ] -} +@needs_py39 +def test_items_5(client: TestClient): + response = client.put("/items/5", json={"item": {"name": "Foo", "price": 3.0}}) + assert response.status_code == 200 + assert response.json() == { + "item_id": 5, + "item": {"name": "Foo", "price": 3.0, "description": None, "tax": None}, + } @needs_py39 -@pytest.mark.parametrize( - "path,body,expected_status,expected_response", - [ - ( - "/items/5", - {"item": {"name": "Foo", "price": 3.0}}, - 200, - { - "item_id": 5, - "item": {"name": "Foo", "price": 3.0, "description": None, "tax": None}, - }, - ), - ( - "/items/6", - { - "item": { - "name": "Bar", - "price": 0.2, - "description": "Some bar", - "tax": "5.4", +def test_items_6(client: TestClient): + response = client.put( + "/items/6", + json={ + "item": { + "name": "Bar", + "price": 0.2, + "description": "Some bar", + "tax": "5.4", + } + }, + ) + assert response.status_code == 200 + assert response.json() == { + "item_id": 6, + "item": { + "name": "Bar", + "price": 0.2, + "description": "Some bar", + "tax": 5.4, + }, + } + + +@needs_py39 +def test_invalid_price(client: TestClient): + response = client.put("/items/5", json={"item": {"name": "Foo", "price": -3.0}}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "greater_than", + "loc": ["body", "item", "price"], + "msg": "Input should be greater than 0", + "input": -3.0, + "ctx": {"gt": 0.0}, + "url": match_pydantic_error_url("greater_than"), } - }, - 200, - { - "item_id": 6, - "item": { - "name": "Bar", - "price": 0.2, - "description": "Some bar", - "tax": 5.4, - }, - }, - ), - ("/items/5", {"item": {"name": "Foo", "price": -3.0}}, 422, price_not_greater), - ], -) -def test(path, body, expected_status, expected_response, client: TestClient): - response = client.put(path, json=body) - assert response.status_code == expected_status - assert response.json() == expected_response + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "ctx": {"limit_value": 0}, + "loc": ["body", "item", "price"], + "msg": "ensure this value is greater than 0", + "type": "value_error.number.not_gt", + } + ] + } + ) @needs_py39 @@ -124,18 +138,39 @@ def test_openapi_schema(client: TestClient): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, - "description": { - "title": "The description of the item", - "maxLength": 300, - "type": "string", - }, + "description": IsDict( + { + "title": "The description of the item", + "anyOf": [ + {"maxLength": 300, "type": "string"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "The description of the item", + "maxLength": 300, + "type": "string", + } + ), "price": { "title": "Price", "exclusiveMinimum": 0.0, "type": "number", "description": "The price must be greater than zero", }, - "tax": {"title": "Tax", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), }, }, "Body_update_item_items__item_id__put": { diff --git a/tests/test_tutorial/test_body_fields/test_tutorial001_py310.py b/tests/test_tutorial/test_body_fields/test_tutorial001_py310.py index 29c8ef4e9..b48cd9ec2 100644 --- a/tests/test_tutorial/test_body_fields/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_body_fields/test_tutorial001_py310.py @@ -1,5 +1,7 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from ...utils import needs_py310 @@ -12,59 +14,71 @@ def get_client(): return client -price_not_greater = { - "detail": [ - { - "ctx": {"limit_value": 0}, - "loc": ["body", "item", "price"], - "msg": "ensure this value is greater than 0", - "type": "value_error.number.not_gt", - } - ] -} +@needs_py310 +def test_items_5(client: TestClient): + response = client.put("/items/5", json={"item": {"name": "Foo", "price": 3.0}}) + assert response.status_code == 200 + assert response.json() == { + "item_id": 5, + "item": {"name": "Foo", "price": 3.0, "description": None, "tax": None}, + } @needs_py310 -@pytest.mark.parametrize( - "path,body,expected_status,expected_response", - [ - ( - "/items/5", - {"item": {"name": "Foo", "price": 3.0}}, - 200, - { - "item_id": 5, - "item": {"name": "Foo", "price": 3.0, "description": None, "tax": None}, - }, - ), - ( - "/items/6", - { - "item": { - "name": "Bar", - "price": 0.2, - "description": "Some bar", - "tax": "5.4", +def test_items_6(client: TestClient): + response = client.put( + "/items/6", + json={ + "item": { + "name": "Bar", + "price": 0.2, + "description": "Some bar", + "tax": "5.4", + } + }, + ) + assert response.status_code == 200 + assert response.json() == { + "item_id": 6, + "item": { + "name": "Bar", + "price": 0.2, + "description": "Some bar", + "tax": 5.4, + }, + } + + +@needs_py310 +def test_invalid_price(client: TestClient): + response = client.put("/items/5", json={"item": {"name": "Foo", "price": -3.0}}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "greater_than", + "loc": ["body", "item", "price"], + "msg": "Input should be greater than 0", + "input": -3.0, + "ctx": {"gt": 0.0}, + "url": match_pydantic_error_url("greater_than"), } - }, - 200, - { - "item_id": 6, - "item": { - "name": "Bar", - "price": 0.2, - "description": "Some bar", - "tax": 5.4, - }, - }, - ), - ("/items/5", {"item": {"name": "Foo", "price": -3.0}}, 422, price_not_greater), - ], -) -def test(path, body, expected_status, expected_response, client: TestClient): - response = client.put(path, json=body) - assert response.status_code == expected_status - assert response.json() == expected_response + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "ctx": {"limit_value": 0}, + "loc": ["body", "item", "price"], + "msg": "ensure this value is greater than 0", + "type": "value_error.number.not_gt", + } + ] + } + ) @needs_py310 @@ -124,18 +138,39 @@ def test_openapi_schema(client: TestClient): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, - "description": { - "title": "The description of the item", - "maxLength": 300, - "type": "string", - }, + "description": IsDict( + { + "title": "The description of the item", + "anyOf": [ + {"maxLength": 300, "type": "string"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "The description of the item", + "maxLength": 300, + "type": "string", + } + ), "price": { "title": "Price", "exclusiveMinimum": 0.0, "type": "number", "description": "The price must be greater than zero", }, - "tax": {"title": "Tax", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), }, }, "Body_update_item_items__item_id__put": { diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial001.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial001.py index ce41a4283..e5dc13b26 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial001.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial001.py @@ -1,52 +1,74 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url -from docs_src.body_multiple_params.tutorial001 import app -client = TestClient(app) +@pytest.fixture(name="client") +def get_client(): + from docs_src.body_multiple_params.tutorial001 import app + client = TestClient(app) + return client -item_id_not_int = { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] -} +def test_post_body_q_bar_content(client: TestClient): + response = client.put("/items/5?q=bar", json={"name": "Foo", "price": 50.5}) + assert response.status_code == 200 + assert response.json() == { + "item_id": 5, + "item": { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + }, + "q": "bar", + } -@pytest.mark.parametrize( - "path,body,expected_status,expected_response", - [ - ( - "/items/5?q=bar", - {"name": "Foo", "price": 50.5}, - 200, - { - "item_id": 5, - "item": { - "name": "Foo", - "price": 50.5, - "description": None, - "tax": None, - }, - "q": "bar", - }, - ), - ("/items/5?q=bar", None, 200, {"item_id": 5, "q": "bar"}), - ("/items/5", None, 200, {"item_id": 5}), - ("/items/foo", None, 422, item_id_not_int), - ], -) -def test_post_body(path, body, expected_status, expected_response): - response = client.put(path, json=body) - assert response.status_code == expected_status - assert response.json() == expected_response +def test_post_no_body_q_bar(client: TestClient): + response = client.put("/items/5?q=bar", json=None) + assert response.status_code == 200 + assert response.json() == {"item_id": 5, "q": "bar"} -def test_openapi_schema(): + +def test_post_no_body(client: TestClient): + response = client.put("/items/5", json=None) + assert response.status_code == 200 + assert response.json() == {"item_id": 5} + + +def test_post_id_foo(client: TestClient): + response = client.put("/items/foo", json=None) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "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": match_pydantic_error_url("int_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] + } + ) + + +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { @@ -87,7 +109,16 @@ def test_openapi_schema(): }, { "required": False, - "schema": {"title": "Q", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Q", "type": "string"} + ), "name": "q", "in": "query", }, @@ -95,7 +126,19 @@ def test_openapi_schema(): "requestBody": { "content": { "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} + "schema": IsDict( + { + "anyOf": [ + {"$ref": "#/components/schemas/Item"}, + {"type": "null"}, + ], + "title": "Item", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"$ref": "#/components/schemas/Item"} + ) } } }, @@ -110,9 +153,27 @@ def test_openapi_schema(): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an.py index acc4cfadc..51e8e3a4e 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an.py @@ -1,52 +1,74 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url -from docs_src.body_multiple_params.tutorial001_an import app -client = TestClient(app) +@pytest.fixture(name="client") +def get_client(): + from docs_src.body_multiple_params.tutorial001_an import app + client = TestClient(app) + return client -item_id_not_int = { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] -} +def test_post_body_q_bar_content(client: TestClient): + response = client.put("/items/5?q=bar", json={"name": "Foo", "price": 50.5}) + assert response.status_code == 200 + assert response.json() == { + "item_id": 5, + "item": { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + }, + "q": "bar", + } -@pytest.mark.parametrize( - "path,body,expected_status,expected_response", - [ - ( - "/items/5?q=bar", - {"name": "Foo", "price": 50.5}, - 200, - { - "item_id": 5, - "item": { - "name": "Foo", - "price": 50.5, - "description": None, - "tax": None, - }, - "q": "bar", - }, - ), - ("/items/5?q=bar", None, 200, {"item_id": 5, "q": "bar"}), - ("/items/5", None, 200, {"item_id": 5}), - ("/items/foo", None, 422, item_id_not_int), - ], -) -def test_post_body(path, body, expected_status, expected_response): - response = client.put(path, json=body) - assert response.status_code == expected_status - assert response.json() == expected_response +def test_post_no_body_q_bar(client: TestClient): + response = client.put("/items/5?q=bar", json=None) + assert response.status_code == 200 + assert response.json() == {"item_id": 5, "q": "bar"} -def test_openapi_schema(): + +def test_post_no_body(client: TestClient): + response = client.put("/items/5", json=None) + assert response.status_code == 200 + assert response.json() == {"item_id": 5} + + +def test_post_id_foo(client: TestClient): + response = client.put("/items/foo", json=None) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "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": match_pydantic_error_url("int_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] + } + ) + + +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { @@ -87,7 +109,16 @@ def test_openapi_schema(): }, { "required": False, - "schema": {"title": "Q", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Q", "type": "string"} + ), "name": "q", "in": "query", }, @@ -95,7 +126,19 @@ def test_openapi_schema(): "requestBody": { "content": { "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} + "schema": IsDict( + { + "anyOf": [ + {"$ref": "#/components/schemas/Item"}, + {"type": "null"}, + ], + "title": "Item", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"$ref": "#/components/schemas/Item"} + ) } } }, @@ -110,9 +153,27 @@ def test_openapi_schema(): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py310.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py310.py index a8dc02a6c..8ac1f7261 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py310.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py310.py @@ -1,5 +1,7 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from ...utils import needs_py310 @@ -12,45 +14,64 @@ def get_client(): return client -item_id_not_int = { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] -} +@needs_py310 +def test_post_body_q_bar_content(client: TestClient): + response = client.put("/items/5?q=bar", json={"name": "Foo", "price": 50.5}) + assert response.status_code == 200 + assert response.json() == { + "item_id": 5, + "item": { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + }, + "q": "bar", + } @needs_py310 -@pytest.mark.parametrize( - "path,body,expected_status,expected_response", - [ - ( - "/items/5?q=bar", - {"name": "Foo", "price": 50.5}, - 200, - { - "item_id": 5, - "item": { - "name": "Foo", - "price": 50.5, - "description": None, - "tax": None, - }, - "q": "bar", - }, - ), - ("/items/5?q=bar", None, 200, {"item_id": 5, "q": "bar"}), - ("/items/5", None, 200, {"item_id": 5}), - ("/items/foo", None, 422, item_id_not_int), - ], -) -def test_post_body(path, body, expected_status, expected_response, client: TestClient): - response = client.put(path, json=body) - assert response.status_code == expected_status - assert response.json() == expected_response +def test_post_no_body_q_bar(client: TestClient): + response = client.put("/items/5?q=bar", json=None) + assert response.status_code == 200 + assert response.json() == {"item_id": 5, "q": "bar"} + + +@needs_py310 +def test_post_no_body(client: TestClient): + response = client.put("/items/5", json=None) + assert response.status_code == 200 + assert response.json() == {"item_id": 5} + + +@needs_py310 +def test_post_id_foo(client: TestClient): + response = client.put("/items/foo", json=None) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "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": match_pydantic_error_url("int_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] + } + ) @needs_py310 @@ -95,7 +116,16 @@ def test_openapi_schema(client: TestClient): }, { "required": False, - "schema": {"title": "Q", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Q", "type": "string"} + ), "name": "q", "in": "query", }, @@ -103,7 +133,19 @@ def test_openapi_schema(client: TestClient): "requestBody": { "content": { "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} + "schema": IsDict( + { + "anyOf": [ + {"$ref": "#/components/schemas/Item"}, + {"type": "null"}, + ], + "title": "Item", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"$ref": "#/components/schemas/Item"} + ) } } }, @@ -118,9 +160,27 @@ def test_openapi_schema(client: TestClient): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py39.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py39.py index f31fee78e..7ada42c52 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py39.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py39.py @@ -1,5 +1,7 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from ...utils import needs_py39 @@ -12,45 +14,64 @@ def get_client(): return client -item_id_not_int = { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] -} +@needs_py39 +def test_post_body_q_bar_content(client: TestClient): + response = client.put("/items/5?q=bar", json={"name": "Foo", "price": 50.5}) + assert response.status_code == 200 + assert response.json() == { + "item_id": 5, + "item": { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + }, + "q": "bar", + } @needs_py39 -@pytest.mark.parametrize( - "path,body,expected_status,expected_response", - [ - ( - "/items/5?q=bar", - {"name": "Foo", "price": 50.5}, - 200, - { - "item_id": 5, - "item": { - "name": "Foo", - "price": 50.5, - "description": None, - "tax": None, - }, - "q": "bar", - }, - ), - ("/items/5?q=bar", None, 200, {"item_id": 5, "q": "bar"}), - ("/items/5", None, 200, {"item_id": 5}), - ("/items/foo", None, 422, item_id_not_int), - ], -) -def test_post_body(path, body, expected_status, expected_response, client: TestClient): - response = client.put(path, json=body) - assert response.status_code == expected_status - assert response.json() == expected_response +def test_post_no_body_q_bar(client: TestClient): + response = client.put("/items/5?q=bar", json=None) + assert response.status_code == 200 + assert response.json() == {"item_id": 5, "q": "bar"} + + +@needs_py39 +def test_post_no_body(client: TestClient): + response = client.put("/items/5", json=None) + assert response.status_code == 200 + assert response.json() == {"item_id": 5} + + +@needs_py39 +def test_post_id_foo(client: TestClient): + response = client.put("/items/foo", json=None) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "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": match_pydantic_error_url("int_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] + } + ) @needs_py39 @@ -95,7 +116,16 @@ def test_openapi_schema(client: TestClient): }, { "required": False, - "schema": {"title": "Q", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Q", "type": "string"} + ), "name": "q", "in": "query", }, @@ -103,7 +133,19 @@ def test_openapi_schema(client: TestClient): "requestBody": { "content": { "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} + "schema": IsDict( + { + "anyOf": [ + {"$ref": "#/components/schemas/Item"}, + {"type": "null"}, + ], + "title": "Item", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"$ref": "#/components/schemas/Item"} + ) } } }, @@ -118,9 +160,27 @@ def test_openapi_schema(client: TestClient): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial001_py310.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_py310.py index 0e46df253..0a832eaf6 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_py310.py @@ -1,5 +1,7 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from ...utils import needs_py310 @@ -12,45 +14,64 @@ def get_client(): return client -item_id_not_int = { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] -} +@needs_py310 +def test_post_body_q_bar_content(client: TestClient): + response = client.put("/items/5?q=bar", json={"name": "Foo", "price": 50.5}) + assert response.status_code == 200 + assert response.json() == { + "item_id": 5, + "item": { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + }, + "q": "bar", + } @needs_py310 -@pytest.mark.parametrize( - "path,body,expected_status,expected_response", - [ - ( - "/items/5?q=bar", - {"name": "Foo", "price": 50.5}, - 200, - { - "item_id": 5, - "item": { - "name": "Foo", - "price": 50.5, - "description": None, - "tax": None, - }, - "q": "bar", - }, - ), - ("/items/5?q=bar", None, 200, {"item_id": 5, "q": "bar"}), - ("/items/5", None, 200, {"item_id": 5}), - ("/items/foo", None, 422, item_id_not_int), - ], -) -def test_post_body(path, body, expected_status, expected_response, client: TestClient): - response = client.put(path, json=body) - assert response.status_code == expected_status - assert response.json() == expected_response +def test_post_no_body_q_bar(client: TestClient): + response = client.put("/items/5?q=bar", json=None) + assert response.status_code == 200 + assert response.json() == {"item_id": 5, "q": "bar"} + + +@needs_py310 +def test_post_no_body(client: TestClient): + response = client.put("/items/5", json=None) + assert response.status_code == 200 + assert response.json() == {"item_id": 5} + + +@needs_py310 +def test_post_id_foo(client: TestClient): + response = client.put("/items/foo", json=None) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "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": match_pydantic_error_url("int_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] + } + ) @needs_py310 @@ -95,7 +116,16 @@ def test_openapi_schema(client: TestClient): }, { "required": False, - "schema": {"title": "Q", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Q", "type": "string"} + ), "name": "q", "in": "query", }, @@ -103,7 +133,19 @@ def test_openapi_schema(client: TestClient): "requestBody": { "content": { "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} + "schema": IsDict( + { + "anyOf": [ + {"$ref": "#/components/schemas/Item"}, + {"type": "null"}, + ], + "title": "Item", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"$ref": "#/components/schemas/Item"} + ) } } }, @@ -118,9 +160,27 @@ def test_openapi_schema(client: TestClient): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial003.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial003.py index 8555cf88c..2046579a9 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial003.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial003.py @@ -1,92 +1,147 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url -from docs_src.body_multiple_params.tutorial003 import app -client = TestClient(app) +@pytest.fixture(name="client") +def get_client(): + from docs_src.body_multiple_params.tutorial003 import app + client = TestClient(app) + return client -# Test required and embedded body parameters with no bodies sent -@pytest.mark.parametrize( - "path,body,expected_status,expected_response", - [ - ( - "/items/5", - { - "importance": 2, - "item": {"name": "Foo", "price": 50.5}, - "user": {"username": "Dave"}, - }, - 200, - { - "item_id": 5, - "importance": 2, - "item": { - "name": "Foo", - "price": 50.5, - "description": None, - "tax": None, - }, - "user": {"username": "Dave", "full_name": None}, - }, - ), - ( - "/items/5", - None, - 422, - { - "detail": [ - { - "loc": ["body", "item"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "user"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "importance"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - }, - ), - ( - "/items/5", - [], - 422, - { - "detail": [ - { - "loc": ["body", "item"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "user"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "importance"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - }, - ), - ], -) -def test_post_body(path, body, expected_status, expected_response): - response = client.put(path, json=body) - assert response.status_code == expected_status - assert response.json() == expected_response + +def test_post_body_valid(client: TestClient): + response = client.put( + "/items/5", + json={ + "importance": 2, + "item": {"name": "Foo", "price": 50.5}, + "user": {"username": "Dave"}, + }, + ) + assert response.status_code == 200 + assert response.json() == { + "item_id": 5, + "importance": 2, + "item": { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + }, + "user": {"username": "Dave", "full_name": None}, + } + + +def test_post_body_no_data(client: TestClient): + response = client.put("/items/5", json=None) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "item"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "user"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "importance"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "item"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "user"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "importance"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) -def test_openapi_schema(): +def test_post_body_empty_list(client: TestClient): + response = client.put("/items/5", json=[]) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "item"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "user"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "importance"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "item"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "user"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "importance"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { @@ -142,9 +197,27 @@ def test_openapi_schema(): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), }, }, "User": { @@ -153,7 +226,16 @@ def test_openapi_schema(): "type": "object", "properties": { "username": {"title": "Username", "type": "string"}, - "full_name": {"title": "Full Name", "type": "string"}, + "full_name": IsDict( + { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Full Name", "type": "string"} + ), }, }, "Body_update_item_items__item_id__put": { diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an.py index f4d300cc5..1282483e0 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an.py @@ -1,92 +1,147 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url -from docs_src.body_multiple_params.tutorial003_an import app -client = TestClient(app) +@pytest.fixture(name="client") +def get_client(): + from docs_src.body_multiple_params.tutorial003_an import app + client = TestClient(app) + return client -# Test required and embedded body parameters with no bodies sent -@pytest.mark.parametrize( - "path,body,expected_status,expected_response", - [ - ( - "/items/5", - { - "importance": 2, - "item": {"name": "Foo", "price": 50.5}, - "user": {"username": "Dave"}, - }, - 200, - { - "item_id": 5, - "importance": 2, - "item": { - "name": "Foo", - "price": 50.5, - "description": None, - "tax": None, - }, - "user": {"username": "Dave", "full_name": None}, - }, - ), - ( - "/items/5", - None, - 422, - { - "detail": [ - { - "loc": ["body", "item"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "user"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "importance"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - }, - ), - ( - "/items/5", - [], - 422, - { - "detail": [ - { - "loc": ["body", "item"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "user"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "importance"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - }, - ), - ], -) -def test_post_body(path, body, expected_status, expected_response): - response = client.put(path, json=body) - assert response.status_code == expected_status - assert response.json() == expected_response + +def test_post_body_valid(client: TestClient): + response = client.put( + "/items/5", + json={ + "importance": 2, + "item": {"name": "Foo", "price": 50.5}, + "user": {"username": "Dave"}, + }, + ) + assert response.status_code == 200 + assert response.json() == { + "item_id": 5, + "importance": 2, + "item": { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + }, + "user": {"username": "Dave", "full_name": None}, + } + + +def test_post_body_no_data(client: TestClient): + response = client.put("/items/5", json=None) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "item"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "user"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "importance"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "item"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "user"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "importance"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) -def test_openapi_schema(): +def test_post_body_empty_list(client: TestClient): + response = client.put("/items/5", json=[]) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "item"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "user"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "importance"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "item"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "user"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "importance"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { @@ -142,9 +197,27 @@ def test_openapi_schema(): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), }, }, "User": { @@ -153,7 +226,16 @@ def test_openapi_schema(): "type": "object", "properties": { "username": {"title": "Username", "type": "string"}, - "full_name": {"title": "Full Name", "type": "string"}, + "full_name": IsDict( + { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Full Name", "type": "string"} + ), }, }, "Body_update_item_items__item_id__put": { diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py310.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py310.py index afe2b2c20..577c079d0 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py310.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py310.py @@ -1,5 +1,7 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from ...utils import needs_py310 @@ -12,85 +14,136 @@ def get_client(): return client -# Test required and embedded body parameters with no bodies sent @needs_py310 -@pytest.mark.parametrize( - "path,body,expected_status,expected_response", - [ - ( - "/items/5", - { - "importance": 2, - "item": {"name": "Foo", "price": 50.5}, - "user": {"username": "Dave"}, - }, - 200, - { - "item_id": 5, - "importance": 2, - "item": { - "name": "Foo", - "price": 50.5, - "description": None, - "tax": None, - }, - "user": {"username": "Dave", "full_name": None}, - }, - ), - ( - "/items/5", - None, - 422, - { - "detail": [ - { - "loc": ["body", "item"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "user"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "importance"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - }, - ), - ( - "/items/5", - [], - 422, - { - "detail": [ - { - "loc": ["body", "item"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "user"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "importance"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - }, - ), - ], -) -def test_post_body(path, body, expected_status, expected_response, client: TestClient): - response = client.put(path, json=body) - assert response.status_code == expected_status - assert response.json() == expected_response +def test_post_body_valid(client: TestClient): + response = client.put( + "/items/5", + json={ + "importance": 2, + "item": {"name": "Foo", "price": 50.5}, + "user": {"username": "Dave"}, + }, + ) + assert response.status_code == 200 + assert response.json() == { + "item_id": 5, + "importance": 2, + "item": { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + }, + "user": {"username": "Dave", "full_name": None}, + } + + +@needs_py310 +def test_post_body_no_data(client: TestClient): + response = client.put("/items/5", json=None) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "item"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "user"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "importance"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "item"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "user"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "importance"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +@needs_py310 +def test_post_body_empty_list(client: TestClient): + response = client.put("/items/5", json=[]) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "item"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "user"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "importance"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "item"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "user"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "importance"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) @needs_py310 @@ -150,9 +203,27 @@ def test_openapi_schema(client: TestClient): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), }, }, "User": { @@ -161,7 +232,16 @@ def test_openapi_schema(client: TestClient): "type": "object", "properties": { "username": {"title": "Username", "type": "string"}, - "full_name": {"title": "Full Name", "type": "string"}, + "full_name": IsDict( + { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Full Name", "type": "string"} + ), }, }, "Body_update_item_items__item_id__put": { diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py39.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py39.py index 033d5892e..0ec04151c 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py39.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py39.py @@ -1,5 +1,7 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from ...utils import needs_py39 @@ -12,85 +14,136 @@ def get_client(): return client -# Test required and embedded body parameters with no bodies sent @needs_py39 -@pytest.mark.parametrize( - "path,body,expected_status,expected_response", - [ - ( - "/items/5", - { - "importance": 2, - "item": {"name": "Foo", "price": 50.5}, - "user": {"username": "Dave"}, - }, - 200, - { - "item_id": 5, - "importance": 2, - "item": { - "name": "Foo", - "price": 50.5, - "description": None, - "tax": None, - }, - "user": {"username": "Dave", "full_name": None}, - }, - ), - ( - "/items/5", - None, - 422, - { - "detail": [ - { - "loc": ["body", "item"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "user"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "importance"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - }, - ), - ( - "/items/5", - [], - 422, - { - "detail": [ - { - "loc": ["body", "item"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "user"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "importance"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - }, - ), - ], -) -def test_post_body(path, body, expected_status, expected_response, client: TestClient): - response = client.put(path, json=body) - assert response.status_code == expected_status - assert response.json() == expected_response +def test_post_body_valid(client: TestClient): + response = client.put( + "/items/5", + json={ + "importance": 2, + "item": {"name": "Foo", "price": 50.5}, + "user": {"username": "Dave"}, + }, + ) + assert response.status_code == 200 + assert response.json() == { + "item_id": 5, + "importance": 2, + "item": { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + }, + "user": {"username": "Dave", "full_name": None}, + } + + +@needs_py39 +def test_post_body_no_data(client: TestClient): + response = client.put("/items/5", json=None) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "item"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "user"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "importance"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "item"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "user"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "importance"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +@needs_py39 +def test_post_body_empty_list(client: TestClient): + response = client.put("/items/5", json=[]) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "item"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "user"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "importance"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "item"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "user"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "importance"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) @needs_py39 @@ -150,9 +203,27 @@ def test_openapi_schema(client: TestClient): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), }, }, "User": { @@ -161,7 +232,16 @@ def test_openapi_schema(client: TestClient): "type": "object", "properties": { "username": {"title": "Username", "type": "string"}, - "full_name": {"title": "Full Name", "type": "string"}, + "full_name": IsDict( + { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Full Name", "type": "string"} + ), }, }, "Body_update_item_items__item_id__put": { diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_py310.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_py310.py index 8fcc00013..9caf5fe6c 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_py310.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_py310.py @@ -1,5 +1,7 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from ...utils import needs_py310 @@ -12,85 +14,136 @@ def get_client(): return client -# Test required and embedded body parameters with no bodies sent @needs_py310 -@pytest.mark.parametrize( - "path,body,expected_status,expected_response", - [ - ( - "/items/5", - { - "importance": 2, - "item": {"name": "Foo", "price": 50.5}, - "user": {"username": "Dave"}, - }, - 200, - { - "item_id": 5, - "importance": 2, - "item": { - "name": "Foo", - "price": 50.5, - "description": None, - "tax": None, - }, - "user": {"username": "Dave", "full_name": None}, - }, - ), - ( - "/items/5", - None, - 422, - { - "detail": [ - { - "loc": ["body", "item"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "user"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "importance"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - }, - ), - ( - "/items/5", - [], - 422, - { - "detail": [ - { - "loc": ["body", "item"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "user"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "importance"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - }, - ), - ], -) -def test_post_body(path, body, expected_status, expected_response, client: TestClient): - response = client.put(path, json=body) - assert response.status_code == expected_status - assert response.json() == expected_response +def test_post_body_valid(client: TestClient): + response = client.put( + "/items/5", + json={ + "importance": 2, + "item": {"name": "Foo", "price": 50.5}, + "user": {"username": "Dave"}, + }, + ) + assert response.status_code == 200 + assert response.json() == { + "item_id": 5, + "importance": 2, + "item": { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + }, + "user": {"username": "Dave", "full_name": None}, + } + + +@needs_py310 +def test_post_body_no_data(client: TestClient): + response = client.put("/items/5", json=None) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "item"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "user"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "importance"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "item"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "user"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "importance"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +@needs_py310 +def test_post_body_empty_list(client: TestClient): + response = client.put("/items/5", json=[]) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "item"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "user"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "importance"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "item"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "user"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "importance"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) @needs_py310 @@ -150,9 +203,27 @@ def test_openapi_schema(client: TestClient): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), }, }, "User": { @@ -161,7 +232,16 @@ def test_openapi_schema(client: TestClient): "type": "object", "properties": { "username": {"title": "Username", "type": "string"}, - "full_name": {"title": "Full Name", "type": "string"}, + "full_name": IsDict( + { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Full Name", "type": "string"} + ), }, }, "Body_update_item_items__item_id__put": { diff --git a/tests/test_tutorial/test_body_nested_models/test_tutorial009.py b/tests/test_tutorial/test_body_nested_models/test_tutorial009.py index ac39cd93f..f4a76be44 100644 --- a/tests/test_tutorial/test_body_nested_models/test_tutorial009.py +++ b/tests/test_tutorial/test_body_nested_models/test_tutorial009.py @@ -1,33 +1,55 @@ +import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url -from docs_src.body_nested_models.tutorial009 import app -client = TestClient(app) +@pytest.fixture(name="client") +def get_client(): + from docs_src.body_nested_models.tutorial009 import app + client = TestClient(app) + return client -def test_post_body(): + +def test_post_body(client: TestClient): data = {"2": 2.2, "3": 3.3} response = client.post("/index-weights/", json=data) assert response.status_code == 200, response.text assert response.json() == data -def test_post_invalid_body(): +def test_post_invalid_body(client: TestClient): data = {"foo": 2.2, "3": 3.3} response = client.post("/index-weights/", json=data) assert response.status_code == 422, response.text - assert response.json() == { - "detail": [ - { - "loc": ["body", "__key__"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } + assert response.json() == IsDict( + { + "detail": [ + { + "type": "int_parsing", + "loc": ["body", "foo", "[key]"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "foo", + "url": match_pydantic_error_url("int_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "__key__"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] + } + ) -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { diff --git a/tests/test_tutorial/test_body_nested_models/test_tutorial009_py39.py b/tests/test_tutorial/test_body_nested_models/test_tutorial009_py39.py index 0800abe29..8ab9bcac8 100644 --- a/tests/test_tutorial/test_body_nested_models/test_tutorial009_py39.py +++ b/tests/test_tutorial/test_body_nested_models/test_tutorial009_py39.py @@ -1,5 +1,7 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from ...utils import needs_py39 @@ -25,15 +27,30 @@ def test_post_invalid_body(client: TestClient): data = {"foo": 2.2, "3": 3.3} response = client.post("/index-weights/", json=data) assert response.status_code == 422, response.text - assert response.json() == { - "detail": [ - { - "loc": ["body", "__key__"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } + assert response.json() == IsDict( + { + "detail": [ + { + "type": "int_parsing", + "loc": ["body", "foo", "[key]"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "foo", + "url": match_pydantic_error_url("int_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "__key__"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] + } + ) @needs_py39 diff --git a/tests/test_tutorial/test_body_updates/test_tutorial001.py b/tests/test_tutorial/test_body_updates/test_tutorial001.py index 151b4b917..b02f7c81c 100644 --- a/tests/test_tutorial/test_body_updates/test_tutorial001.py +++ b/tests/test_tutorial/test_body_updates/test_tutorial001.py @@ -1,11 +1,17 @@ +import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient -from docs_src.body_updates.tutorial001 import app -client = TestClient(app) +@pytest.fixture(name="client") +def get_client(): + from docs_src.body_updates.tutorial001 import app + client = TestClient(app) + return client -def test_get(): + +def test_get(client: TestClient): response = client.get("/items/baz") assert response.status_code == 200, response.text assert response.json() == { @@ -17,7 +23,7 @@ def test_get(): } -def test_put(): +def test_put(client: TestClient): response = client.put( "/items/bar", json={"name": "Barz", "price": 3, "description": None} ) @@ -30,7 +36,7 @@ def test_put(): } -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { @@ -118,9 +124,36 @@ def test_openapi_schema(): "title": "Item", "type": "object", "properties": { - "name": {"title": "Name", "type": "string"}, - "description": {"title": "Description", "type": "string"}, - "price": {"title": "Price", "type": "number"}, + "name": IsDict( + { + "title": "Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Name", "type": "string"} + ), + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), + "price": IsDict( + { + "title": "Price", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Price", "type": "number"} + ), "tax": {"title": "Tax", "type": "number", "default": 10.5}, "tags": { "title": "Tags", diff --git a/tests/test_tutorial/test_body_updates/test_tutorial001_py310.py b/tests/test_tutorial/test_body_updates/test_tutorial001_py310.py index c4b4b9df3..4af2652a7 100644 --- a/tests/test_tutorial/test_body_updates/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_body_updates/test_tutorial001_py310.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -128,9 +129,36 @@ def test_openapi_schema(client: TestClient): "title": "Item", "type": "object", "properties": { - "name": {"title": "Name", "type": "string"}, - "description": {"title": "Description", "type": "string"}, - "price": {"title": "Price", "type": "number"}, + "name": IsDict( + { + "title": "Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Name", "type": "string"} + ), + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), + "price": IsDict( + { + "title": "Price", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Price", "type": "number"} + ), "tax": {"title": "Tax", "type": "number", "default": 10.5}, "tags": { "title": "Tags", diff --git a/tests/test_tutorial/test_body_updates/test_tutorial001_py39.py b/tests/test_tutorial/test_body_updates/test_tutorial001_py39.py index 940b4b3b8..832f45388 100644 --- a/tests/test_tutorial/test_body_updates/test_tutorial001_py39.py +++ b/tests/test_tutorial/test_body_updates/test_tutorial001_py39.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py39 @@ -128,9 +129,36 @@ def test_openapi_schema(client: TestClient): "title": "Item", "type": "object", "properties": { - "name": {"title": "Name", "type": "string"}, - "description": {"title": "Description", "type": "string"}, - "price": {"title": "Price", "type": "number"}, + "name": IsDict( + { + "title": "Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Name", "type": "string"} + ), + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), + "price": IsDict( + { + "title": "Price", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Price", "type": "number"} + ), "tax": {"title": "Tax", "type": "number", "default": 10.5}, "tags": { "title": "Tags", diff --git a/tests/test_tutorial/test_conditional_openapi/test_tutorial001.py b/tests/test_tutorial/test_conditional_openapi/test_tutorial001.py index a43394ab1..b098f259c 100644 --- a/tests/test_tutorial/test_conditional_openapi/test_tutorial001.py +++ b/tests/test_tutorial/test_conditional_openapi/test_tutorial001.py @@ -2,13 +2,23 @@ import importlib from fastapi.testclient import TestClient -from docs_src.conditional_openapi import tutorial001 +from ...utils import needs_pydanticv2 -def test_disable_openapi(monkeypatch): - monkeypatch.setenv("OPENAPI_URL", "") +def get_client() -> TestClient: + from docs_src.conditional_openapi import tutorial001 + importlib.reload(tutorial001) + client = TestClient(tutorial001.app) + return client + + +@needs_pydanticv2 +def test_disable_openapi(monkeypatch): + monkeypatch.setenv("OPENAPI_URL", "") + # Load the client after setting the env var + client = get_client() response = client.get("/openapi.json") assert response.status_code == 404, response.text response = client.get("/docs") @@ -17,16 +27,17 @@ def test_disable_openapi(monkeypatch): assert response.status_code == 404, response.text +@needs_pydanticv2 def test_root(): - client = TestClient(tutorial001.app) + client = get_client() response = client.get("/") assert response.status_code == 200 assert response.json() == {"message": "Hello World"} +@needs_pydanticv2 def test_default_openapi(): - importlib.reload(tutorial001) - client = TestClient(tutorial001.app) + client = get_client() response = client.get("/docs") assert response.status_code == 200, response.text response = client.get("/redoc") diff --git a/tests/test_tutorial/test_cookie_params/test_tutorial001.py b/tests/test_tutorial/test_cookie_params/test_tutorial001.py index 902bed843..7d0e669ab 100644 --- a/tests/test_tutorial/test_cookie_params/test_tutorial001.py +++ b/tests/test_tutorial/test_cookie_params/test_tutorial001.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.cookie_params.tutorial001 import app @@ -56,7 +57,16 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": {"title": "Ads Id", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Ads Id", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Ads Id", "type": "string"} + ), "name": "ads_id", "in": "cookie", } diff --git a/tests/test_tutorial/test_cookie_params/test_tutorial001_an.py b/tests/test_tutorial/test_cookie_params/test_tutorial001_an.py index aa5807844..2505876c8 100644 --- a/tests/test_tutorial/test_cookie_params/test_tutorial001_an.py +++ b/tests/test_tutorial/test_cookie_params/test_tutorial001_an.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.cookie_params.tutorial001_an import app @@ -56,7 +57,16 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": {"title": "Ads Id", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Ads Id", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Ads Id", "type": "string"} + ), "name": "ads_id", "in": "cookie", } diff --git a/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py310.py b/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py310.py index ffb55d4e1..108f78b9c 100644 --- a/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py310.py +++ b/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py310.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -62,7 +63,16 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": {"title": "Ads Id", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Ads Id", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Ads Id", "type": "string"} + ), "name": "ads_id", "in": "cookie", } diff --git a/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py39.py b/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py39.py index 9bc38effd..8126a1052 100644 --- a/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py39.py +++ b/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py39.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py39 @@ -62,7 +63,16 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": {"title": "Ads Id", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Ads Id", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Ads Id", "type": "string"} + ), "name": "ads_id", "in": "cookie", } diff --git a/tests/test_tutorial/test_cookie_params/test_tutorial001_py310.py b/tests/test_tutorial/test_cookie_params/test_tutorial001_py310.py index bb2953ef6..6711fa581 100644 --- a/tests/test_tutorial/test_cookie_params/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_cookie_params/test_tutorial001_py310.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -62,7 +63,16 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": {"title": "Ads Id", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Ads Id", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Ads Id", "type": "string"} + ), "name": "ads_id", "in": "cookie", } diff --git a/tests/test_tutorial/test_custom_request_and_route/test_tutorial002.py b/tests/test_tutorial/test_custom_request_and_route/test_tutorial002.py index d2d27f8a2..ad142ec88 100644 --- a/tests/test_tutorial/test_custom_request_and_route/test_tutorial002.py +++ b/tests/test_tutorial/test_custom_request_and_route/test_tutorial002.py @@ -1,4 +1,6 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from docs_src.custom_request_and_route.tutorial002 import app @@ -12,16 +14,33 @@ def test_endpoint_works(): def test_exception_handler_body_access(): response = client.post("/", json={"numbers": [1, 2, 3]}) - - assert response.json() == { - "detail": { - "body": '{"numbers": [1, 2, 3]}', - "errors": [ - { - "loc": ["body"], - "msg": "value is not a valid list", - "type": "type_error.list", - } - ], + assert response.json() == IsDict( + { + "detail": { + "errors": [ + { + "type": "list_type", + "loc": ["body"], + "msg": "Input should be a valid list", + "input": {"numbers": [1, 2, 3]}, + "url": match_pydantic_error_url("list_type"), + } + ], + "body": '{"numbers": [1, 2, 3]}', + } + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": { + "body": '{"numbers": [1, 2, 3]}', + "errors": [ + { + "loc": ["body"], + "msg": "value is not a valid list", + "type": "type_error.list", + } + ], + } } - } + ) diff --git a/tests/test_tutorial/test_dataclasses/test_tutorial001.py b/tests/test_tutorial/test_dataclasses/test_tutorial001.py index e20c0efe9..9f1200f37 100644 --- a/tests/test_tutorial/test_dataclasses/test_tutorial001.py +++ b/tests/test_tutorial/test_dataclasses/test_tutorial001.py @@ -1,4 +1,6 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from docs_src.dataclasses.tutorial001 import app @@ -19,15 +21,30 @@ def test_post_item(): def test_post_invalid_item(): response = client.post("/items/", json={"name": "Foo", "price": "invalid price"}) assert response.status_code == 422 - assert response.json() == { - "detail": [ - { - "loc": ["body", "price"], - "msg": "value is not a valid float", - "type": "type_error.float", - } - ] - } + assert response.json() == IsDict( + { + "detail": [ + { + "type": "float_parsing", + "loc": ["body", "price"], + "msg": "Input should be a valid number, unable to parse string as a number", + "input": "invalid price", + "url": match_pydantic_error_url("float_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "price"], + "msg": "value is not a valid float", + "type": "type_error.float", + } + ] + } + ) def test_openapi_schema(): @@ -88,8 +105,26 @@ def test_openapi_schema(): "properties": { "name": {"title": "Name", "type": "string"}, "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_dataclasses/test_tutorial002.py b/tests/test_tutorial/test_dataclasses/test_tutorial002.py index e122239d8..7d88e2861 100644 --- a/tests/test_tutorial/test_dataclasses/test_tutorial002.py +++ b/tests/test_tutorial/test_dataclasses/test_tutorial002.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.dataclasses.tutorial002 import app @@ -51,13 +52,42 @@ def test_openapi_schema(): "properties": { "name": {"title": "Name", "type": "string"}, "price": {"title": "Price", "type": "number"}, - "tags": { - "title": "Tags", - "type": "array", - "items": {"type": "string"}, - }, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number"}, + "tags": IsDict( + { + "title": "Tags", + "type": "array", + "items": {"type": "string"}, + "default": [], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Tags", + "type": "array", + "items": {"type": "string"}, + } + ), + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), }, } } diff --git a/tests/test_tutorial/test_dataclasses/test_tutorial003.py b/tests/test_tutorial/test_dataclasses/test_tutorial003.py index 204426e8b..597757e09 100644 --- a/tests/test_tutorial/test_dataclasses/test_tutorial003.py +++ b/tests/test_tutorial/test_dataclasses/test_tutorial003.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.dataclasses.tutorial003 import app @@ -135,11 +136,22 @@ def test_openapi_schema(): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, - "items": { - "title": "Items", - "type": "array", - "items": {"$ref": "#/components/schemas/Item"}, - }, + "items": IsDict( + { + "title": "Items", + "type": "array", + "items": {"$ref": "#/components/schemas/Item"}, + "default": [], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Items", + "type": "array", + "items": {"$ref": "#/components/schemas/Item"}, + } + ), }, }, "HTTPValidationError": { @@ -159,7 +171,16 @@ def test_openapi_schema(): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, - "description": {"title": "Description", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_dependencies/test_tutorial001.py b/tests/test_tutorial/test_dependencies/test_tutorial001.py index a8e564ebe..d1324a641 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial001.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial001.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.dependencies.tutorial001 import app @@ -52,7 +53,16 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": {"title": "Q", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Q", "type": "string"} + ), "name": "q", "in": "query", }, @@ -102,7 +112,16 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": {"title": "Q", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Q", "type": "string"} + ), "name": "q", "in": "query", }, diff --git a/tests/test_tutorial/test_dependencies/test_tutorial001_an.py b/tests/test_tutorial/test_dependencies/test_tutorial001_an.py index 4e6a329f4..79c2a1e10 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial001_an.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial001_an.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.dependencies.tutorial001_an import app @@ -52,7 +53,16 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": {"title": "Q", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Q", "type": "string"} + ), "name": "q", "in": "query", }, @@ -102,7 +112,16 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": {"title": "Q", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Q", "type": "string"} + ), "name": "q", "in": "query", }, diff --git a/tests/test_tutorial/test_dependencies/test_tutorial001_an_py310.py b/tests/test_tutorial/test_dependencies/test_tutorial001_an_py310.py index 205aee908..7db55a1c5 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial001_an_py310.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial001_an_py310.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -60,7 +61,16 @@ def test_openapi_schema(client: TestClient): "parameters": [ { "required": False, - "schema": {"title": "Q", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Q", "type": "string"} + ), "name": "q", "in": "query", }, @@ -110,7 +120,16 @@ def test_openapi_schema(client: TestClient): "parameters": [ { "required": False, - "schema": {"title": "Q", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Q", "type": "string"} + ), "name": "q", "in": "query", }, diff --git a/tests/test_tutorial/test_dependencies/test_tutorial001_an_py39.py b/tests/test_tutorial/test_dependencies/test_tutorial001_an_py39.py index 73593ea55..68c2dedb1 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial001_an_py39.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial001_an_py39.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py39 @@ -60,7 +61,16 @@ def test_openapi_schema(client: TestClient): "parameters": [ { "required": False, - "schema": {"title": "Q", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Q", "type": "string"} + ), "name": "q", "in": "query", }, @@ -110,7 +120,16 @@ def test_openapi_schema(client: TestClient): "parameters": [ { "required": False, - "schema": {"title": "Q", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Q", "type": "string"} + ), "name": "q", "in": "query", }, diff --git a/tests/test_tutorial/test_dependencies/test_tutorial001_py310.py b/tests/test_tutorial/test_dependencies/test_tutorial001_py310.py index 10bf84fb5..381eecb63 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial001_py310.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -60,7 +61,16 @@ def test_openapi_schema(client: TestClient): "parameters": [ { "required": False, - "schema": {"title": "Q", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Q", "type": "string"} + ), "name": "q", "in": "query", }, @@ -110,7 +120,16 @@ def test_openapi_schema(client: TestClient): "parameters": [ { "required": False, - "schema": {"title": "Q", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Q", "type": "string"} + ), "name": "q", "in": "query", }, diff --git a/tests/test_tutorial/test_dependencies/test_tutorial004.py b/tests/test_tutorial/test_dependencies/test_tutorial004.py index d16fd9ef7..5c5d34cfc 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial004.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial004.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.dependencies.tutorial004 import app @@ -90,7 +91,16 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": {"title": "Q", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Q", "type": "string"} + ), "name": "q", "in": "query", }, diff --git a/tests/test_tutorial/test_dependencies/test_tutorial004_an.py b/tests/test_tutorial/test_dependencies/test_tutorial004_an.py index 46fe97fb2..c5c1a1fb8 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial004_an.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial004_an.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.dependencies.tutorial004_an import app @@ -90,7 +91,16 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": {"title": "Q", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Q", "type": "string"} + ), "name": "q", "in": "query", }, diff --git a/tests/test_tutorial/test_dependencies/test_tutorial004_an_py310.py b/tests/test_tutorial/test_dependencies/test_tutorial004_an_py310.py index c6a0fc665..6fd093ddb 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial004_an_py310.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial004_an_py310.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -98,7 +99,16 @@ def test_openapi_schema(client: TestClient): "parameters": [ { "required": False, - "schema": {"title": "Q", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Q", "type": "string"} + ), "name": "q", "in": "query", }, diff --git a/tests/test_tutorial/test_dependencies/test_tutorial004_an_py39.py b/tests/test_tutorial/test_dependencies/test_tutorial004_an_py39.py index 30431cd29..fbbe84cc9 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial004_an_py39.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial004_an_py39.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py39 @@ -98,7 +99,16 @@ def test_openapi_schema(client: TestClient): "parameters": [ { "required": False, - "schema": {"title": "Q", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Q", "type": "string"} + ), "name": "q", "in": "query", }, diff --git a/tests/test_tutorial/test_dependencies/test_tutorial004_py310.py b/tests/test_tutorial/test_dependencies/test_tutorial004_py310.py index 9793c8c33..845b098e7 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial004_py310.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial004_py310.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -98,7 +99,16 @@ def test_openapi_schema(client: TestClient): "parameters": [ { "required": False, - "schema": {"title": "Q", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Q", "type": "string"} + ), "name": "q", "in": "query", }, diff --git a/tests/test_tutorial/test_dependencies/test_tutorial006.py b/tests/test_tutorial/test_dependencies/test_tutorial006.py index 6fac9f8eb..704e389a5 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial006.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial006.py @@ -1,4 +1,6 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from docs_src.dependencies.tutorial006 import app @@ -8,20 +10,42 @@ client = TestClient(app) def test_get_no_headers(): response = client.get("/items/") assert response.status_code == 422, response.text - assert response.json() == { - "detail": [ - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["header", "x-key"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["header", "x-key"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["header", "x-key"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) def test_get_invalid_one_header(): diff --git a/tests/test_tutorial/test_dependencies/test_tutorial006_an.py b/tests/test_tutorial/test_dependencies/test_tutorial006_an.py index 810537e48..5034fceba 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial006_an.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial006_an.py @@ -1,4 +1,6 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from docs_src.dependencies.tutorial006_an import app @@ -8,20 +10,42 @@ client = TestClient(app) def test_get_no_headers(): response = client.get("/items/") assert response.status_code == 422, response.text - assert response.json() == { - "detail": [ - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["header", "x-key"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["header", "x-key"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["header", "x-key"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) def test_get_invalid_one_header(): diff --git a/tests/test_tutorial/test_dependencies/test_tutorial006_an_py39.py b/tests/test_tutorial/test_dependencies/test_tutorial006_an_py39.py index f17cbcfc7..3fc22dd3c 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial006_an_py39.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial006_an_py39.py @@ -1,5 +1,7 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from ...utils import needs_py39 @@ -16,20 +18,42 @@ def get_client(): def test_get_no_headers(client: TestClient): response = client.get("/items/") assert response.status_code == 422, response.text - assert response.json() == { - "detail": [ - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["header", "x-key"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["header", "x-key"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["header", "x-key"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) @needs_py39 diff --git a/tests/test_tutorial/test_dependencies/test_tutorial012.py b/tests/test_tutorial/test_dependencies/test_tutorial012.py index af1fcde55..753e62e43 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial012.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial012.py @@ -1,4 +1,6 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from docs_src.dependencies.tutorial012 import app @@ -8,39 +10,83 @@ client = TestClient(app) def test_get_no_headers_items(): response = client.get("/items/") assert response.status_code == 422, response.text - assert response.json() == { - "detail": [ - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["header", "x-key"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["header", "x-key"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["header", "x-key"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) def test_get_no_headers_users(): response = client.get("/users/") assert response.status_code == 422, response.text - assert response.json() == { - "detail": [ - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["header", "x-key"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["header", "x-key"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["header", "x-key"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) def test_get_invalid_one_header_items(): diff --git a/tests/test_tutorial/test_dependencies/test_tutorial012_an.py b/tests/test_tutorial/test_dependencies/test_tutorial012_an.py index c33d51d87..4157d4612 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial012_an.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial012_an.py @@ -1,4 +1,6 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from docs_src.dependencies.tutorial012_an import app @@ -8,39 +10,83 @@ client = TestClient(app) def test_get_no_headers_items(): response = client.get("/items/") assert response.status_code == 422, response.text - assert response.json() == { - "detail": [ - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["header", "x-key"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["header", "x-key"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["header", "x-key"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) def test_get_no_headers_users(): response = client.get("/users/") assert response.status_code == 422, response.text - assert response.json() == { - "detail": [ - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["header", "x-key"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["header", "x-key"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["header", "x-key"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) def test_get_invalid_one_header_items(): diff --git a/tests/test_tutorial/test_dependencies/test_tutorial012_an_py39.py b/tests/test_tutorial/test_dependencies/test_tutorial012_an_py39.py index d7bd756b5..9e46758cb 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial012_an_py39.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial012_an_py39.py @@ -1,5 +1,7 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from ...utils import needs_py39 @@ -16,40 +18,84 @@ def get_client(): def test_get_no_headers_items(client: TestClient): response = client.get("/items/") assert response.status_code == 422, response.text - assert response.json() == { - "detail": [ - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["header", "x-key"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["header", "x-key"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["header", "x-key"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) @needs_py39 def test_get_no_headers_users(client: TestClient): response = client.get("/users/") assert response.status_code == 422, response.text - assert response.json() == { - "detail": [ - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["header", "x-key"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["header", "x-key"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["header", "x-key"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) @needs_py39 diff --git a/tests/test_tutorial/test_extra_data_types/test_tutorial001.py b/tests/test_tutorial/test_extra_data_types/test_tutorial001.py index 39d2005ab..7710446ce 100644 --- a/tests/test_tutorial/test_extra_data_types/test_tutorial001.py +++ b/tests/test_tutorial/test_extra_data_types/test_tutorial001.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.extra_data_types.tutorial001 import app @@ -68,9 +69,22 @@ def test_openapi_schema(): "requestBody": { "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_read_items_items__item_id__put" - } + "schema": IsDict( + { + "allOf": [ + { + "$ref": "#/components/schemas/Body_read_items_items__item_id__put" + } + ], + "title": "Body", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "$ref": "#/components/schemas/Body_read_items_items__item_id__put" + } + ) } } }, @@ -83,26 +97,74 @@ def test_openapi_schema(): "title": "Body_read_items_items__item_id__put", "type": "object", "properties": { - "start_datetime": { - "title": "Start Datetime", - "type": "string", - "format": "date-time", - }, - "end_datetime": { - "title": "End Datetime", - "type": "string", - "format": "date-time", - }, - "repeat_at": { - "title": "Repeat At", - "type": "string", - "format": "time", - }, - "process_after": { - "title": "Process After", - "type": "number", - "format": "time-delta", - }, + "start_datetime": IsDict( + { + "title": "Start Datetime", + "anyOf": [ + {"type": "string", "format": "date-time"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Start Datetime", + "type": "string", + "format": "date-time", + } + ), + "end_datetime": IsDict( + { + "title": "End Datetime", + "anyOf": [ + {"type": "string", "format": "date-time"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "End Datetime", + "type": "string", + "format": "date-time", + } + ), + "repeat_at": IsDict( + { + "title": "Repeat At", + "anyOf": [ + {"type": "string", "format": "time"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Repeat At", + "type": "string", + "format": "time", + } + ), + "process_after": IsDict( + { + "title": "Process After", + "anyOf": [ + {"type": "string", "format": "duration"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Process After", + "type": "number", + "format": "time-delta", + } + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_extra_data_types/test_tutorial001_an.py b/tests/test_tutorial/test_extra_data_types/test_tutorial001_an.py index 3e497a291..9951b3b51 100644 --- a/tests/test_tutorial/test_extra_data_types/test_tutorial001_an.py +++ b/tests/test_tutorial/test_extra_data_types/test_tutorial001_an.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.extra_data_types.tutorial001_an import app @@ -68,9 +69,22 @@ def test_openapi_schema(): "requestBody": { "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_read_items_items__item_id__put" - } + "schema": IsDict( + { + "allOf": [ + { + "$ref": "#/components/schemas/Body_read_items_items__item_id__put" + } + ], + "title": "Body", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "$ref": "#/components/schemas/Body_read_items_items__item_id__put" + } + ) } } }, @@ -83,26 +97,74 @@ def test_openapi_schema(): "title": "Body_read_items_items__item_id__put", "type": "object", "properties": { - "start_datetime": { - "title": "Start Datetime", - "type": "string", - "format": "date-time", - }, - "end_datetime": { - "title": "End Datetime", - "type": "string", - "format": "date-time", - }, - "repeat_at": { - "title": "Repeat At", - "type": "string", - "format": "time", - }, - "process_after": { - "title": "Process After", - "type": "number", - "format": "time-delta", - }, + "start_datetime": IsDict( + { + "title": "Start Datetime", + "anyOf": [ + {"type": "string", "format": "date-time"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Start Datetime", + "type": "string", + "format": "date-time", + } + ), + "end_datetime": IsDict( + { + "title": "End Datetime", + "anyOf": [ + {"type": "string", "format": "date-time"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "End Datetime", + "type": "string", + "format": "date-time", + } + ), + "repeat_at": IsDict( + { + "title": "Repeat At", + "anyOf": [ + {"type": "string", "format": "time"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Repeat At", + "type": "string", + "format": "time", + } + ), + "process_after": IsDict( + { + "title": "Process After", + "anyOf": [ + {"type": "string", "format": "duration"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Process After", + "type": "number", + "format": "time-delta", + } + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py310.py b/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py310.py index b539cf3d6..7c482b8cb 100644 --- a/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py310.py +++ b/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py310.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -77,9 +78,22 @@ def test_openapi_schema(client: TestClient): "requestBody": { "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_read_items_items__item_id__put" - } + "schema": IsDict( + { + "allOf": [ + { + "$ref": "#/components/schemas/Body_read_items_items__item_id__put" + } + ], + "title": "Body", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "$ref": "#/components/schemas/Body_read_items_items__item_id__put" + } + ) } } }, @@ -92,26 +106,74 @@ def test_openapi_schema(client: TestClient): "title": "Body_read_items_items__item_id__put", "type": "object", "properties": { - "start_datetime": { - "title": "Start Datetime", - "type": "string", - "format": "date-time", - }, - "end_datetime": { - "title": "End Datetime", - "type": "string", - "format": "date-time", - }, - "repeat_at": { - "title": "Repeat At", - "type": "string", - "format": "time", - }, - "process_after": { - "title": "Process After", - "type": "number", - "format": "time-delta", - }, + "start_datetime": IsDict( + { + "title": "Start Datetime", + "anyOf": [ + {"type": "string", "format": "date-time"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Start Datetime", + "type": "string", + "format": "date-time", + } + ), + "end_datetime": IsDict( + { + "title": "End Datetime", + "anyOf": [ + {"type": "string", "format": "date-time"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "End Datetime", + "type": "string", + "format": "date-time", + } + ), + "repeat_at": IsDict( + { + "title": "Repeat At", + "anyOf": [ + {"type": "string", "format": "time"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Repeat At", + "type": "string", + "format": "time", + } + ), + "process_after": IsDict( + { + "title": "Process After", + "anyOf": [ + {"type": "string", "format": "duration"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Process After", + "type": "number", + "format": "time-delta", + } + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py39.py b/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py39.py index efd31e63d..87473867b 100644 --- a/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py39.py +++ b/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py39.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py39 @@ -77,9 +78,22 @@ def test_openapi_schema(client: TestClient): "requestBody": { "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_read_items_items__item_id__put" - } + "schema": IsDict( + { + "allOf": [ + { + "$ref": "#/components/schemas/Body_read_items_items__item_id__put" + } + ], + "title": "Body", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "$ref": "#/components/schemas/Body_read_items_items__item_id__put" + } + ) } } }, @@ -92,26 +106,74 @@ def test_openapi_schema(client: TestClient): "title": "Body_read_items_items__item_id__put", "type": "object", "properties": { - "start_datetime": { - "title": "Start Datetime", - "type": "string", - "format": "date-time", - }, - "end_datetime": { - "title": "End Datetime", - "type": "string", - "format": "date-time", - }, - "repeat_at": { - "title": "Repeat At", - "type": "string", - "format": "time", - }, - "process_after": { - "title": "Process After", - "type": "number", - "format": "time-delta", - }, + "start_datetime": IsDict( + { + "title": "Start Datetime", + "anyOf": [ + {"type": "string", "format": "date-time"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Start Datetime", + "type": "string", + "format": "date-time", + } + ), + "end_datetime": IsDict( + { + "title": "End Datetime", + "anyOf": [ + {"type": "string", "format": "date-time"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "End Datetime", + "type": "string", + "format": "date-time", + } + ), + "repeat_at": IsDict( + { + "title": "Repeat At", + "anyOf": [ + {"type": "string", "format": "time"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Repeat At", + "type": "string", + "format": "time", + } + ), + "process_after": IsDict( + { + "title": "Process After", + "anyOf": [ + {"type": "string", "format": "duration"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Process After", + "type": "number", + "format": "time-delta", + } + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_extra_data_types/test_tutorial001_py310.py b/tests/test_tutorial/test_extra_data_types/test_tutorial001_py310.py index 733d9f406..0b71d9177 100644 --- a/tests/test_tutorial/test_extra_data_types/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_extra_data_types/test_tutorial001_py310.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -77,9 +78,22 @@ def test_openapi_schema(client: TestClient): "requestBody": { "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_read_items_items__item_id__put" - } + "schema": IsDict( + { + "allOf": [ + { + "$ref": "#/components/schemas/Body_read_items_items__item_id__put" + } + ], + "title": "Body", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "$ref": "#/components/schemas/Body_read_items_items__item_id__put" + } + ) } } }, @@ -92,26 +106,74 @@ def test_openapi_schema(client: TestClient): "title": "Body_read_items_items__item_id__put", "type": "object", "properties": { - "start_datetime": { - "title": "Start Datetime", - "type": "string", - "format": "date-time", - }, - "end_datetime": { - "title": "End Datetime", - "type": "string", - "format": "date-time", - }, - "repeat_at": { - "title": "Repeat At", - "type": "string", - "format": "time", - }, - "process_after": { - "title": "Process After", - "type": "number", - "format": "time-delta", - }, + "start_datetime": IsDict( + { + "title": "Start Datetime", + "anyOf": [ + {"type": "string", "format": "date-time"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Start Datetime", + "type": "string", + "format": "date-time", + } + ), + "end_datetime": IsDict( + { + "title": "End Datetime", + "anyOf": [ + {"type": "string", "format": "date-time"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "End Datetime", + "type": "string", + "format": "date-time", + } + ), + "repeat_at": IsDict( + { + "title": "Repeat At", + "anyOf": [ + {"type": "string", "format": "time"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Repeat At", + "type": "string", + "format": "time", + } + ), + "process_after": IsDict( + { + "title": "Process After", + "anyOf": [ + {"type": "string", "format": "duration"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Process After", + "type": "number", + "format": "time-delta", + } + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_handling_errors/test_tutorial004.py b/tests/test_tutorial/test_handling_errors/test_tutorial004.py index 0c0988c64..217159a59 100644 --- a/tests/test_tutorial/test_handling_errors/test_tutorial004.py +++ b/tests/test_tutorial/test_handling_errors/test_tutorial004.py @@ -8,12 +8,18 @@ client = TestClient(app) def test_get_validation_error(): response = client.get("/items/foo") assert response.status_code == 400, response.text - validation_error_str_lines = [ - b"1 validation error for Request", - b"path -> item_id", - b" value is not a valid integer (type=type_error.integer)", - ] - assert response.content == b"\n".join(validation_error_str_lines) + # TODO: remove when deprecating Pydantic v1 + assert ( + # TODO: remove when deprecating Pydantic v1 + "path -> item_id" in response.text + or "'loc': ('path', 'item_id')" in response.text + ) + assert ( + # TODO: remove when deprecating Pydantic v1 + "value is not a valid integer" in response.text + or "Input should be a valid integer, unable to parse string as an integer" + in response.text + ) def test_get_http_error(): diff --git a/tests/test_tutorial/test_handling_errors/test_tutorial005.py b/tests/test_tutorial/test_handling_errors/test_tutorial005.py index f356178ac..494c317ca 100644 --- a/tests/test_tutorial/test_handling_errors/test_tutorial005.py +++ b/tests/test_tutorial/test_handling_errors/test_tutorial005.py @@ -1,4 +1,6 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from docs_src.handling_errors.tutorial005 import app @@ -8,16 +10,32 @@ client = TestClient(app) def test_post_validation_error(): response = client.post("/items/", json={"title": "towel", "size": "XL"}) assert response.status_code == 422, response.text - assert response.json() == { - "detail": [ - { - "loc": ["body", "size"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ], - "body": {"title": "towel", "size": "XL"}, - } + assert response.json() == IsDict( + { + "detail": [ + { + "type": "int_parsing", + "loc": ["body", "size"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "XL", + "url": match_pydantic_error_url("int_parsing"), + } + ], + "body": {"title": "towel", "size": "XL"}, + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "size"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ], + "body": {"title": "towel", "size": "XL"}, + } + ) def test_post(): diff --git a/tests/test_tutorial/test_handling_errors/test_tutorial006.py b/tests/test_tutorial/test_handling_errors/test_tutorial006.py index 4dd1adf43..cc2b496a8 100644 --- a/tests/test_tutorial/test_handling_errors/test_tutorial006.py +++ b/tests/test_tutorial/test_handling_errors/test_tutorial006.py @@ -1,4 +1,6 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from docs_src.handling_errors.tutorial006 import app @@ -8,15 +10,30 @@ client = TestClient(app) def test_get_validation_error(): response = client.get("/items/foo") assert response.status_code == 422, response.text - assert response.json() == { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } + assert response.json() == IsDict( + { + "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": match_pydantic_error_url("int_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] + } + ) def test_get_http_error(): diff --git a/tests/test_tutorial/test_header_params/test_tutorial001.py b/tests/test_tutorial/test_header_params/test_tutorial001.py index 030159dcf..746fc0502 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial001.py +++ b/tests/test_tutorial/test_header_params/test_tutorial001.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.header_params.tutorial001 import app @@ -20,7 +21,7 @@ def test(path, headers, expected_status, expected_response): assert response.json() == expected_response -def test_openapi(): +def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { @@ -50,7 +51,16 @@ def test_openapi(): "parameters": [ { "required": False, - "schema": {"title": "User-Agent", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "User-Agent", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "User-Agent", "type": "string"} + ), "name": "user-agent", "in": "header", } diff --git a/tests/test_tutorial/test_header_params/test_tutorial001_an.py b/tests/test_tutorial/test_header_params/test_tutorial001_an.py index 3755ab758..a715228aa 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial001_an.py +++ b/tests/test_tutorial/test_header_params/test_tutorial001_an.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.header_params.tutorial001_an import app @@ -50,7 +51,16 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": {"title": "User-Agent", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "User-Agent", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "User-Agent", "type": "string"} + ), "name": "user-agent", "in": "header", } diff --git a/tests/test_tutorial/test_header_params/test_tutorial001_an_py310.py b/tests/test_tutorial/test_header_params/test_tutorial001_an_py310.py index 207b3b02b..caf85bc6c 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial001_an_py310.py +++ b/tests/test_tutorial/test_header_params/test_tutorial001_an_py310.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -58,7 +59,16 @@ def test_openapi_schema(client: TestClient): "parameters": [ { "required": False, - "schema": {"title": "User-Agent", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "User-Agent", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "User-Agent", "type": "string"} + ), "name": "user-agent", "in": "header", } diff --git a/tests/test_tutorial/test_header_params/test_tutorial001_py310.py b/tests/test_tutorial/test_header_params/test_tutorial001_py310.py index bf51982b7..57e0a296a 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_header_params/test_tutorial001_py310.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -58,7 +59,16 @@ def test_openapi_schema(client: TestClient): "parameters": [ { "required": False, - "schema": {"title": "User-Agent", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "User-Agent", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "User-Agent", "type": "string"} + ), "name": "user-agent", "in": "header", } diff --git a/tests/test_tutorial/test_header_params/test_tutorial002.py b/tests/test_tutorial/test_header_params/test_tutorial002.py index 545fc836b..78bac838c 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial002.py +++ b/tests/test_tutorial/test_header_params/test_tutorial002.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.header_params.tutorial002 import app @@ -61,7 +62,16 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": {"title": "Strange Header", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Strange Header", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Strange Header", "type": "string"} + ), "name": "strange_header", "in": "header", } diff --git a/tests/test_tutorial/test_header_params/test_tutorial002_an.py b/tests/test_tutorial/test_header_params/test_tutorial002_an.py index cfd581e33..ffda8158f 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial002_an.py +++ b/tests/test_tutorial/test_header_params/test_tutorial002_an.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.header_params.tutorial002_an import app @@ -61,7 +62,16 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": {"title": "Strange Header", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Strange Header", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Strange Header", "type": "string"} + ), "name": "strange_header", "in": "header", } diff --git a/tests/test_tutorial/test_header_params/test_tutorial002_an_py310.py b/tests/test_tutorial/test_header_params/test_tutorial002_an_py310.py index c8d61e42e..6f332f3ba 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial002_an_py310.py +++ b/tests/test_tutorial/test_header_params/test_tutorial002_an_py310.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -69,7 +70,16 @@ def test_openapi_schema(client: TestClient): "parameters": [ { "required": False, - "schema": {"title": "Strange Header", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Strange Header", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Strange Header", "type": "string"} + ), "name": "strange_header", "in": "header", } diff --git a/tests/test_tutorial/test_header_params/test_tutorial002_an_py39.py b/tests/test_tutorial/test_header_params/test_tutorial002_an_py39.py index 85150d4a9..8202bc671 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial002_an_py39.py +++ b/tests/test_tutorial/test_header_params/test_tutorial002_an_py39.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py39 @@ -72,7 +73,16 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": {"title": "Strange Header", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Strange Header", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Strange Header", "type": "string"} + ), "name": "strange_header", "in": "header", } diff --git a/tests/test_tutorial/test_header_params/test_tutorial002_py310.py b/tests/test_tutorial/test_header_params/test_tutorial002_py310.py index f189d85b5..c113ed23e 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial002_py310.py +++ b/tests/test_tutorial/test_header_params/test_tutorial002_py310.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -72,7 +73,16 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": {"title": "Strange Header", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Strange Header", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Strange Header", "type": "string"} + ), "name": "strange_header", "in": "header", } diff --git a/tests/test_tutorial/test_header_params/test_tutorial003.py b/tests/test_tutorial/test_header_params/test_tutorial003.py index b2fc17b8f..268df7a3e 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial003.py +++ b/tests/test_tutorial/test_header_params/test_tutorial003.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.header_params.tutorial003 import app @@ -24,7 +25,6 @@ def test(path, headers, expected_status, expected_response): def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200 - # insert_assert(response.json()) assert response.json() == { "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, @@ -36,11 +36,23 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": { - "title": "X-Token", - "type": "array", - "items": {"type": "string"}, - }, + "schema": IsDict( + { + "title": "X-Token", + "anyOf": [ + {"type": "array", "items": {"type": "string"}}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "X-Token", + "type": "array", + "items": {"type": "string"}, + } + ), "name": "x-token", "in": "header", } diff --git a/tests/test_tutorial/test_header_params/test_tutorial003_an.py b/tests/test_tutorial/test_header_params/test_tutorial003_an.py index 87fa839e2..742ed41f4 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial003_an.py +++ b/tests/test_tutorial/test_header_params/test_tutorial003_an.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.header_params.tutorial003_an import app @@ -24,7 +25,6 @@ def test(path, headers, expected_status, expected_response): def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200 - # insert_assert(response.json()) assert response.json() == { "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, @@ -36,11 +36,23 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": { - "title": "X-Token", - "type": "array", - "items": {"type": "string"}, - }, + "schema": IsDict( + { + "title": "X-Token", + "anyOf": [ + {"type": "array", "items": {"type": "string"}}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "X-Token", + "type": "array", + "items": {"type": "string"}, + } + ), "name": "x-token", "in": "header", } diff --git a/tests/test_tutorial/test_header_params/test_tutorial003_an_py310.py b/tests/test_tutorial/test_header_params/test_tutorial003_an_py310.py index ef6c268c5..fdac4a416 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial003_an_py310.py +++ b/tests/test_tutorial/test_header_params/test_tutorial003_an_py310.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -32,7 +33,6 @@ def test(path, headers, expected_status, expected_response, client: TestClient): def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200 - # insert_assert(response.json()) assert response.json() == { "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, @@ -44,11 +44,23 @@ def test_openapi_schema(client: TestClient): "parameters": [ { "required": False, - "schema": { - "title": "X-Token", - "type": "array", - "items": {"type": "string"}, - }, + "schema": IsDict( + { + "title": "X-Token", + "anyOf": [ + {"type": "array", "items": {"type": "string"}}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "X-Token", + "type": "array", + "items": {"type": "string"}, + } + ), "name": "x-token", "in": "header", } diff --git a/tests/test_tutorial/test_header_params/test_tutorial003_an_py39.py b/tests/test_tutorial/test_header_params/test_tutorial003_an_py39.py index 6525fd50c..c50543cc8 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial003_an_py39.py +++ b/tests/test_tutorial/test_header_params/test_tutorial003_an_py39.py @@ -1,7 +1,8 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient -from ...utils import needs_py310 +from ...utils import needs_py39 @pytest.fixture(name="client") @@ -12,7 +13,7 @@ def get_client(): return client -@needs_py310 +@needs_py39 @pytest.mark.parametrize( "path,headers,expected_status,expected_response", [ @@ -28,11 +29,10 @@ def test(path, headers, expected_status, expected_response, client: TestClient): assert response.json() == expected_response -@needs_py310 +@needs_py39 def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200 - # insert_assert(response.json()) assert response.json() == { "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, @@ -44,11 +44,23 @@ def test_openapi_schema(client: TestClient): "parameters": [ { "required": False, - "schema": { - "title": "X-Token", - "type": "array", - "items": {"type": "string"}, - }, + "schema": IsDict( + { + "title": "X-Token", + "anyOf": [ + {"type": "array", "items": {"type": "string"}}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "X-Token", + "type": "array", + "items": {"type": "string"}, + } + ), "name": "x-token", "in": "header", } diff --git a/tests/test_tutorial/test_header_params/test_tutorial003_py310.py b/tests/test_tutorial/test_header_params/test_tutorial003_py310.py index b404ce5d8..3afb355e9 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial003_py310.py +++ b/tests/test_tutorial/test_header_params/test_tutorial003_py310.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -32,7 +33,6 @@ def test(path, headers, expected_status, expected_response, client: TestClient): def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200 - # insert_assert(response.json()) assert response.json() == { "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, @@ -44,11 +44,23 @@ def test_openapi_schema(client: TestClient): "parameters": [ { "required": False, - "schema": { - "title": "X-Token", - "type": "array", - "items": {"type": "string"}, - }, + "schema": IsDict( + { + "title": "X-Token", + "anyOf": [ + {"type": "array", "items": {"type": "string"}}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "X-Token", + "type": "array", + "items": {"type": "string"}, + } + ), "name": "x-token", "in": "header", } diff --git a/tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py b/tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py index a6e898c49..73af420ae 100644 --- a/tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py +++ b/tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.openapi_callbacks.tutorial001 import app, invoice_notification @@ -33,13 +34,30 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": { - "title": "Callback Url", - "maxLength": 2083, - "minLength": 1, - "type": "string", - "format": "uri", - }, + "schema": IsDict( + { + "anyOf": [ + { + "type": "string", + "format": "uri", + "minLength": 1, + "maxLength": 2083, + }, + {"type": "null"}, + ], + "title": "Callback Url", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Callback Url", + "maxLength": 2083, + "minLength": 1, + "type": "string", + "format": "uri", + } + ), "name": "callback_url", "in": "query", } @@ -132,7 +150,16 @@ def test_openapi_schema(): "type": "object", "properties": { "id": {"title": "Id", "type": "string"}, - "title": {"title": "Title", "type": "string"}, + "title": IsDict( + { + "title": "Title", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Title", "type": "string"} + ), "customer": {"title": "Customer", "type": "string"}, "total": {"title": "Total", "type": "number"}, }, diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py index cd9fc520e..dd123f48d 100644 --- a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py +++ b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.path_operation_advanced_configuration.tutorial004 import app @@ -68,9 +69,27 @@ def test_openapi_schema(): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), "tags": { "title": "Tags", "uniqueItems": True, diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007.py index 3b88a38c2..2d2802269 100644 --- a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007.py +++ b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007.py @@ -1,11 +1,20 @@ +import pytest from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url -from docs_src.path_operation_advanced_configuration.tutorial007 import app +from ...utils import needs_pydanticv2 -client = TestClient(app) +@pytest.fixture(name="client") +def get_client(): + from docs_src.path_operation_advanced_configuration.tutorial007 import app -def test_post(): + client = TestClient(app) + return client + + +@needs_pydanticv2 +def test_post(client: TestClient): yaml_data = """ name: Deadpoolio tags: @@ -21,7 +30,8 @@ def test_post(): } -def test_post_broken_yaml(): +@needs_pydanticv2 +def test_post_broken_yaml(client: TestClient): yaml_data = """ name: Deadpoolio tags: @@ -34,7 +44,8 @@ def test_post_broken_yaml(): assert response.json() == {"detail": "Invalid YAML"} -def test_post_invalid(): +@needs_pydanticv2 +def test_post_invalid(client: TestClient): yaml_data = """ name: Deadpoolio tags: @@ -45,14 +56,22 @@ def test_post_invalid(): """ response = client.post("/items/", content=yaml_data) assert response.status_code == 422, response.text + # insert_assert(response.json()) assert response.json() == { "detail": [ - {"loc": ["tags", 3], "msg": "str type expected", "type": "type_error.str"} + { + "type": "string_type", + "loc": ["tags", 3], + "msg": "Input should be a valid string", + "input": {"sneaky": "object"}, + "url": match_pydantic_error_url("string_type"), + } ] } -def test_openapi_schema(): +@needs_pydanticv2 +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007_pv1.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007_pv1.py new file mode 100644 index 000000000..ef012f8a6 --- /dev/null +++ b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007_pv1.py @@ -0,0 +1,106 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_pydanticv1 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.path_operation_advanced_configuration.tutorial007_pv1 import app + + client = TestClient(app) + return client + + +@needs_pydanticv1 +def test_post(client: TestClient): + yaml_data = """ + name: Deadpoolio + tags: + - x-force + - x-men + - x-avengers + """ + response = client.post("/items/", content=yaml_data) + assert response.status_code == 200, response.text + assert response.json() == { + "name": "Deadpoolio", + "tags": ["x-force", "x-men", "x-avengers"], + } + + +@needs_pydanticv1 +def test_post_broken_yaml(client: TestClient): + yaml_data = """ + name: Deadpoolio + tags: + x - x-force + x - x-men + x - x-avengers + """ + response = client.post("/items/", content=yaml_data) + assert response.status_code == 422, response.text + assert response.json() == {"detail": "Invalid YAML"} + + +@needs_pydanticv1 +def test_post_invalid(client: TestClient): + yaml_data = """ + name: Deadpoolio + tags: + - x-force + - x-men + - x-avengers + - sneaky: object + """ + response = client.post("/items/", content=yaml_data) + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + {"loc": ["tags", 3], "msg": "str type expected", "type": "type_error.str"} + ] + } + + +@needs_pydanticv1 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "post": { + "summary": "Create Item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/x-yaml": { + "schema": { + "title": "Item", + "required": ["name", "tags"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "tags": { + "title": "Tags", + "type": "array", + "items": {"type": "string"}, + }, + }, + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + } + }, + } diff --git a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005.py b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005.py index 30278caf8..e7e9a982e 100644 --- a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005.py +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.path_operation_configuration.tutorial005 import app @@ -68,9 +69,27 @@ def test_openapi_schema(): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), "tags": { "title": "Tags", "uniqueItems": True, diff --git a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py310.py b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py310.py index cf59d354c..ebfeb809c 100644 --- a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py310.py +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py310.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -77,9 +78,27 @@ def test_openapi_schema(client: TestClient): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), "tags": { "title": "Tags", "uniqueItems": True, diff --git a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py39.py b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py39.py index a93ea8807..8e79afe96 100644 --- a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py39.py +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py39.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py39 @@ -77,9 +78,27 @@ def test_openapi_schema(client: TestClient): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), "tags": { "title": "Tags", "uniqueItems": True, diff --git a/tests/test_tutorial/test_path_params/test_tutorial005.py b/tests/test_tutorial/test_path_params/test_tutorial005.py index b9b58c961..90fa6adaf 100644 --- a/tests/test_tutorial/test_path_params/test_tutorial005.py +++ b/tests/test_tutorial/test_path_params/test_tutorial005.py @@ -1,4 +1,4 @@ -import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.path_params.tutorial005 import app @@ -6,47 +6,55 @@ from docs_src.path_params.tutorial005 import app client = TestClient(app) -@pytest.mark.parametrize( - "url,status_code,expected", - [ - ( - "/models/alexnet", - 200, - {"model_name": "alexnet", "message": "Deep Learning FTW!"}, - ), - ( - "/models/lenet", - 200, - {"model_name": "lenet", "message": "LeCNN all the images"}, - ), - ( - "/models/resnet", - 200, - {"model_name": "resnet", "message": "Have some residuals"}, - ), - ( - "/models/foo", - 422, - { - "detail": [ - { - "ctx": {"enum_values": ["alexnet", "resnet", "lenet"]}, - "loc": ["path", "model_name"], - "msg": "value is not a valid enumeration member; permitted: 'alexnet', 'resnet', 'lenet'", - "type": "type_error.enum", - } - ] - }, - ), - ], -) -def test_get_enums(url, status_code, expected): - response = client.get(url) - assert response.status_code == status_code - assert response.json() == expected +def test_get_enums_alexnet(): + response = client.get("/models/alexnet") + assert response.status_code == 200 + assert response.json() == {"model_name": "alexnet", "message": "Deep Learning FTW!"} + + +def test_get_enums_lenet(): + response = client.get("/models/lenet") + assert response.status_code == 200 + assert response.json() == {"model_name": "lenet", "message": "LeCNN all the images"} -def test_openapi(): +def test_get_enums_resnet(): + response = client.get("/models/resnet") + assert response.status_code == 200 + assert response.json() == {"model_name": "resnet", "message": "Have some residuals"} + + +def test_get_enums_invalid(): + response = client.get("/models/foo") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "enum", + "loc": ["path", "model_name"], + "msg": "Input should be 'alexnet','resnet' or 'lenet'", + "input": "foo", + "ctx": {"expected": "'alexnet','resnet' or 'lenet'"}, + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "ctx": {"enum_values": ["alexnet", "resnet", "lenet"]}, + "loc": ["path", "model_name"], + "msg": "value is not a valid enumeration member; permitted: 'alexnet', 'resnet', 'lenet'", + "type": "type_error.enum", + } + ] + } + ) + + +def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text data = response.json() @@ -98,12 +106,22 @@ def test_openapi(): } }, }, - "ModelName": { - "title": "ModelName", - "enum": ["alexnet", "resnet", "lenet"], - "type": "string", - "description": "An enumeration.", - }, + "ModelName": IsDict( + { + "title": "ModelName", + "enum": ["alexnet", "resnet", "lenet"], + "type": "string", + } + ) + | IsDict( + { + # TODO: remove when deprecating Pydantic v1 + "title": "ModelName", + "enum": ["alexnet", "resnet", "lenet"], + "type": "string", + "description": "An enumeration.", + } + ), "ValidationError": { "title": "ValidationError", "required": ["loc", "msg", "type"], diff --git a/tests/test_tutorial/test_query_params/test_tutorial005.py b/tests/test_tutorial/test_query_params/test_tutorial005.py index 6c2cba7e1..921586357 100644 --- a/tests/test_tutorial/test_query_params/test_tutorial005.py +++ b/tests/test_tutorial/test_query_params/test_tutorial005.py @@ -1,34 +1,45 @@ -import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from docs_src.query_params.tutorial005 import app client = TestClient(app) -query_required = { - "detail": [ - { - "loc": ["query", "needy"], - "msg": "field required", - "type": "value_error.missing", - } - ] -} +def test_foo_needy_very(): + response = client.get("/items/foo?needy=very") + assert response.status_code == 200 + assert response.json() == {"item_id": "foo", "needy": "very"} -@pytest.mark.parametrize( - "path,expected_status,expected_response", - [ - ("/items/foo?needy=very", 200, {"item_id": "foo", "needy": "very"}), - ("/items/foo", 422, query_required), - ("/items/foo", 422, query_required), - ], -) -def test(path, expected_status, expected_response): - response = client.get(path) - assert response.status_code == expected_status - assert response.json() == expected_response +def test_foo_no_needy(): + response = client.get("/items/foo") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "needy"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "needy"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) def test_openapi_schema(): diff --git a/tests/test_tutorial/test_query_params/test_tutorial006.py b/tests/test_tutorial/test_query_params/test_tutorial006.py index 626637903..e07803d6c 100644 --- a/tests/test_tutorial/test_query_params/test_tutorial006.py +++ b/tests/test_tutorial/test_query_params/test_tutorial006.py @@ -1,62 +1,82 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url -from docs_src.query_params.tutorial006 import app -client = TestClient(app) +@pytest.fixture(name="client") +def get_client(): + from docs_src.query_params.tutorial006 import app + c = TestClient(app) + return c -query_required = { - "detail": [ - { - "loc": ["query", "needy"], - "msg": "field required", - "type": "value_error.missing", - } - ] -} + +def test_foo_needy_very(client: TestClient): + response = client.get("/items/foo?needy=very") + assert response.status_code == 200 + assert response.json() == { + "item_id": "foo", + "needy": "very", + "skip": 0, + "limit": None, + } -@pytest.mark.parametrize( - "path,expected_status,expected_response", - [ - ( - "/items/foo?needy=very", - 200, - {"item_id": "foo", "needy": "very", "skip": 0, "limit": None}, - ), - ( - "/items/foo?skip=a&limit=b", - 422, - { - "detail": [ - { - "loc": ["query", "needy"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["query", "skip"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - }, - { - "loc": ["query", "limit"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - }, - ] - }, - ), - ], -) -def test(path, expected_status, expected_response): - response = client.get(path) - assert response.status_code == expected_status - assert response.json() == expected_response +def test_foo_no_needy(client: TestClient): + response = client.get("/items/foo?skip=a&limit=b") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "needy"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "int_parsing", + "loc": ["query", "skip"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "a", + "url": match_pydantic_error_url("int_parsing"), + }, + { + "type": "int_parsing", + "loc": ["query", "limit"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "b", + "url": match_pydantic_error_url("int_parsing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "needy"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["query", "skip"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + }, + { + "loc": ["query", "limit"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + }, + ] + } + ) -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { @@ -108,7 +128,16 @@ def test_openapi_schema(): }, { "required": False, - "schema": {"title": "Limit", "type": "integer"}, + "schema": IsDict( + { + "anyOf": [{"type": "integer"}, {"type": "null"}], + "title": "Limit", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Limit", "type": "integer"} + ), "name": "limit", "in": "query", }, diff --git a/tests/test_tutorial/test_query_params/test_tutorial006_py310.py b/tests/test_tutorial/test_query_params/test_tutorial006_py310.py index b6fb2f39e..6c4c0b4dc 100644 --- a/tests/test_tutorial/test_query_params/test_tutorial006_py310.py +++ b/tests/test_tutorial/test_query_params/test_tutorial006_py310.py @@ -1,18 +1,10 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from ...utils import needs_py310 -query_required = { - "detail": [ - { - "loc": ["query", "needy"], - "msg": "field required", - "type": "value_error.missing", - } - ] -} - @pytest.fixture(name="client") def get_client(): @@ -23,43 +15,69 @@ def get_client(): @needs_py310 -@pytest.mark.parametrize( - "path,expected_status,expected_response", - [ - ( - "/items/foo?needy=very", - 200, - {"item_id": "foo", "needy": "very", "skip": 0, "limit": None}, - ), - ( - "/items/foo?skip=a&limit=b", - 422, - { - "detail": [ - { - "loc": ["query", "needy"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["query", "skip"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - }, - { - "loc": ["query", "limit"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - }, - ] - }, - ), - ], -) -def test(path, expected_status, expected_response, client: TestClient): - response = client.get(path) - assert response.status_code == expected_status - assert response.json() == expected_response +def test_foo_needy_very(client: TestClient): + response = client.get("/items/foo?needy=very") + assert response.status_code == 200 + assert response.json() == { + "item_id": "foo", + "needy": "very", + "skip": 0, + "limit": None, + } + + +@needs_py310 +def test_foo_no_needy(client: TestClient): + response = client.get("/items/foo?skip=a&limit=b") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "needy"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "int_parsing", + "loc": ["query", "skip"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "a", + "url": match_pydantic_error_url("int_parsing"), + }, + { + "type": "int_parsing", + "loc": ["query", "limit"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "b", + "url": match_pydantic_error_url("int_parsing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "needy"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["query", "skip"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + }, + { + "loc": ["query", "limit"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + }, + ] + } + ) @needs_py310 @@ -115,7 +133,16 @@ def test_openapi_schema(client: TestClient): }, { "required": False, - "schema": {"title": "Limit", "type": "integer"}, + "schema": IsDict( + { + "anyOf": [{"type": "integer"}, {"type": "null"}], + "title": "Limit", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Limit", "type": "integer"} + ), "name": "limit", "in": "query", }, diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010.py index 370ae0ff0..287c2e8f8 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010.py @@ -1,47 +1,70 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url -from docs_src.query_params_str_validations.tutorial010 import app -client = TestClient(app) +@pytest.fixture(name="client") +def get_client(): + from docs_src.query_params_str_validations.tutorial010 import app + client = TestClient(app) + return client -regex_error = { - "detail": [ - { - "ctx": {"pattern": "^fixedquery$"}, - "loc": ["query", "item-query"], - "msg": 'string does not match regex "^fixedquery$"', - "type": "value_error.str.regex", - } - ] -} + +def test_query_params_str_validations_no_query(client: TestClient): + response = client.get("/items/") + assert response.status_code == 200 + assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + + +def test_query_params_str_validations_item_query_fixedquery(client: TestClient): + response = client.get("/items/", params={"item-query": "fixedquery"}) + assert response.status_code == 200 + assert response.json() == { + "items": [{"item_id": "Foo"}, {"item_id": "Bar"}], + "q": "fixedquery", + } + + +def test_query_params_str_validations_q_fixedquery(client: TestClient): + response = client.get("/items/", params={"q": "fixedquery"}) + assert response.status_code == 200 + assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} -@pytest.mark.parametrize( - "q_name,q,expected_status,expected_response", - [ - (None, None, 200, {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}), - ( - "item-query", - "fixedquery", - 200, - {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}], "q": "fixedquery"}, - ), - ("q", "fixedquery", 200, {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}), - ("item-query", "nonregexquery", 422, regex_error), - ], -) -def test_query_params_str_validations(q_name, q, expected_status, expected_response): - url = "/items/" - if q_name and q: - url = f"{url}?{q_name}={q}" - response = client.get(url) - assert response.status_code == expected_status - assert response.json() == expected_response +def test_query_params_str_validations_item_query_nonregexquery(client: TestClient): + response = client.get("/items/", params={"item-query": "nonregexquery"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "string_pattern_mismatch", + "loc": ["query", "item-query"], + "msg": "String should match pattern '^fixedquery$'", + "input": "nonregexquery", + "ctx": {"pattern": "^fixedquery$"}, + "url": match_pydantic_error_url("string_pattern_mismatch"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "ctx": {"pattern": "^fixedquery$"}, + "loc": ["query", "item-query"], + "msg": 'string does not match regex "^fixedquery$"', + "type": "value_error.str.regex", + } + ] + } + ) -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { @@ -73,14 +96,32 @@ def test_openapi_schema(): "description": "Query string for the items to search in the database that have a good match", "required": False, "deprecated": True, - "schema": { - "title": "Query string", - "maxLength": 50, - "minLength": 3, - "pattern": "^fixedquery$", - "type": "string", - "description": "Query string for the items to search in the database that have a good match", - }, + "schema": IsDict( + { + "anyOf": [ + { + "type": "string", + "minLength": 3, + "maxLength": 50, + "pattern": "^fixedquery$", + }, + {"type": "null"}, + ], + "title": "Query string", + "description": "Query string for the items to search in the database that have a good match", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Query string", + "maxLength": 50, + "minLength": 3, + "pattern": "^fixedquery$", + "type": "string", + "description": "Query string for the items to search in the database that have a good match", + } + ), "name": "item-query", "in": "query", } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an.py index 1f76ef314..5b0515070 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an.py @@ -1,47 +1,70 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url -from docs_src.query_params_str_validations.tutorial010_an import app -client = TestClient(app) +@pytest.fixture(name="client") +def get_client(): + from docs_src.query_params_str_validations.tutorial010_an import app + client = TestClient(app) + return client -regex_error = { - "detail": [ - { - "ctx": {"pattern": "^fixedquery$"}, - "loc": ["query", "item-query"], - "msg": 'string does not match regex "^fixedquery$"', - "type": "value_error.str.regex", - } - ] -} + +def test_query_params_str_validations_no_query(client: TestClient): + response = client.get("/items/") + assert response.status_code == 200 + assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + + +def test_query_params_str_validations_item_query_fixedquery(client: TestClient): + response = client.get("/items/", params={"item-query": "fixedquery"}) + assert response.status_code == 200 + assert response.json() == { + "items": [{"item_id": "Foo"}, {"item_id": "Bar"}], + "q": "fixedquery", + } + + +def test_query_params_str_validations_q_fixedquery(client: TestClient): + response = client.get("/items/", params={"q": "fixedquery"}) + assert response.status_code == 200 + assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} -@pytest.mark.parametrize( - "q_name,q,expected_status,expected_response", - [ - (None, None, 200, {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}), - ( - "item-query", - "fixedquery", - 200, - {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}], "q": "fixedquery"}, - ), - ("q", "fixedquery", 200, {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}), - ("item-query", "nonregexquery", 422, regex_error), - ], -) -def test_query_params_str_validations(q_name, q, expected_status, expected_response): - url = "/items/" - if q_name and q: - url = f"{url}?{q_name}={q}" - response = client.get(url) - assert response.status_code == expected_status - assert response.json() == expected_response +def test_query_params_str_validations_item_query_nonregexquery(client: TestClient): + response = client.get("/items/", params={"item-query": "nonregexquery"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "string_pattern_mismatch", + "loc": ["query", "item-query"], + "msg": "String should match pattern '^fixedquery$'", + "input": "nonregexquery", + "ctx": {"pattern": "^fixedquery$"}, + "url": match_pydantic_error_url("string_pattern_mismatch"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "ctx": {"pattern": "^fixedquery$"}, + "loc": ["query", "item-query"], + "msg": 'string does not match regex "^fixedquery$"', + "type": "value_error.str.regex", + } + ] + } + ) -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { @@ -73,14 +96,32 @@ def test_openapi_schema(): "description": "Query string for the items to search in the database that have a good match", "required": False, "deprecated": True, - "schema": { - "title": "Query string", - "maxLength": 50, - "minLength": 3, - "pattern": "^fixedquery$", - "type": "string", - "description": "Query string for the items to search in the database that have a good match", - }, + "schema": IsDict( + { + "anyOf": [ + { + "type": "string", + "minLength": 3, + "maxLength": 50, + "pattern": "^fixedquery$", + }, + {"type": "null"}, + ], + "title": "Query string", + "description": "Query string for the items to search in the database that have a good match", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Query string", + "maxLength": 50, + "minLength": 3, + "pattern": "^fixedquery$", + "type": "string", + "description": "Query string for the items to search in the database that have a good match", + } + ), "name": "item-query", "in": "query", } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py310.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py310.py index 3a06b4bc7..d22b1ce20 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py310.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py310.py @@ -1,5 +1,7 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from ...utils import needs_py310 @@ -12,42 +14,60 @@ def get_client(): return client -regex_error = { - "detail": [ - { - "ctx": {"pattern": "^fixedquery$"}, - "loc": ["query", "item-query"], - "msg": 'string does not match regex "^fixedquery$"', - "type": "value_error.str.regex", - } - ] -} +@needs_py310 +def test_query_params_str_validations_no_query(client: TestClient): + response = client.get("/items/") + assert response.status_code == 200 + assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + + +@needs_py310 +def test_query_params_str_validations_item_query_fixedquery(client: TestClient): + response = client.get("/items/", params={"item-query": "fixedquery"}) + assert response.status_code == 200 + assert response.json() == { + "items": [{"item_id": "Foo"}, {"item_id": "Bar"}], + "q": "fixedquery", + } + + +@needs_py310 +def test_query_params_str_validations_q_fixedquery(client: TestClient): + response = client.get("/items/", params={"q": "fixedquery"}) + assert response.status_code == 200 + assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} @needs_py310 -@pytest.mark.parametrize( - "q_name,q,expected_status,expected_response", - [ - (None, None, 200, {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}), - ( - "item-query", - "fixedquery", - 200, - {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}], "q": "fixedquery"}, - ), - ("q", "fixedquery", 200, {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}), - ("item-query", "nonregexquery", 422, regex_error), - ], -) -def test_query_params_str_validations( - q_name, q, expected_status, expected_response, client: TestClient -): - url = "/items/" - if q_name and q: - url = f"{url}?{q_name}={q}" - response = client.get(url) - assert response.status_code == expected_status - assert response.json() == expected_response +def test_query_params_str_validations_item_query_nonregexquery(client: TestClient): + response = client.get("/items/", params={"item-query": "nonregexquery"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "string_pattern_mismatch", + "loc": ["query", "item-query"], + "msg": "String should match pattern '^fixedquery$'", + "input": "nonregexquery", + "ctx": {"pattern": "^fixedquery$"}, + "url": match_pydantic_error_url("string_pattern_mismatch"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "ctx": {"pattern": "^fixedquery$"}, + "loc": ["query", "item-query"], + "msg": 'string does not match regex "^fixedquery$"', + "type": "value_error.str.regex", + } + ] + } + ) @needs_py310 @@ -83,14 +103,32 @@ def test_openapi_schema(client: TestClient): "description": "Query string for the items to search in the database that have a good match", "required": False, "deprecated": True, - "schema": { - "title": "Query string", - "maxLength": 50, - "minLength": 3, - "pattern": "^fixedquery$", - "type": "string", - "description": "Query string for the items to search in the database that have a good match", - }, + "schema": IsDict( + { + "anyOf": [ + { + "type": "string", + "minLength": 3, + "maxLength": 50, + "pattern": "^fixedquery$", + }, + {"type": "null"}, + ], + "title": "Query string", + "description": "Query string for the items to search in the database that have a good match", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Query string", + "maxLength": 50, + "minLength": 3, + "pattern": "^fixedquery$", + "type": "string", + "description": "Query string for the items to search in the database that have a good match", + } + ), "name": "item-query", "in": "query", } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py39.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py39.py index 1e6f93093..3e7d5d3ad 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py39.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py39.py @@ -1,5 +1,7 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from ...utils import needs_py39 @@ -12,42 +14,60 @@ def get_client(): return client -regex_error = { - "detail": [ - { - "ctx": {"pattern": "^fixedquery$"}, - "loc": ["query", "item-query"], - "msg": 'string does not match regex "^fixedquery$"', - "type": "value_error.str.regex", - } - ] -} +@needs_py39 +def test_query_params_str_validations_no_query(client: TestClient): + response = client.get("/items/") + assert response.status_code == 200 + assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + + +@needs_py39 +def test_query_params_str_validations_item_query_fixedquery(client: TestClient): + response = client.get("/items/", params={"item-query": "fixedquery"}) + assert response.status_code == 200 + assert response.json() == { + "items": [{"item_id": "Foo"}, {"item_id": "Bar"}], + "q": "fixedquery", + } + + +@needs_py39 +def test_query_params_str_validations_q_fixedquery(client: TestClient): + response = client.get("/items/", params={"q": "fixedquery"}) + assert response.status_code == 200 + assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} @needs_py39 -@pytest.mark.parametrize( - "q_name,q,expected_status,expected_response", - [ - (None, None, 200, {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}), - ( - "item-query", - "fixedquery", - 200, - {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}], "q": "fixedquery"}, - ), - ("q", "fixedquery", 200, {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}), - ("item-query", "nonregexquery", 422, regex_error), - ], -) -def test_query_params_str_validations( - q_name, q, expected_status, expected_response, client: TestClient -): - url = "/items/" - if q_name and q: - url = f"{url}?{q_name}={q}" - response = client.get(url) - assert response.status_code == expected_status - assert response.json() == expected_response +def test_query_params_str_validations_item_query_nonregexquery(client: TestClient): + response = client.get("/items/", params={"item-query": "nonregexquery"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "string_pattern_mismatch", + "loc": ["query", "item-query"], + "msg": "String should match pattern '^fixedquery$'", + "input": "nonregexquery", + "ctx": {"pattern": "^fixedquery$"}, + "url": match_pydantic_error_url("string_pattern_mismatch"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "ctx": {"pattern": "^fixedquery$"}, + "loc": ["query", "item-query"], + "msg": 'string does not match regex "^fixedquery$"', + "type": "value_error.str.regex", + } + ] + } + ) @needs_py39 @@ -83,14 +103,32 @@ def test_openapi_schema(client: TestClient): "description": "Query string for the items to search in the database that have a good match", "required": False, "deprecated": True, - "schema": { - "title": "Query string", - "maxLength": 50, - "minLength": 3, - "pattern": "^fixedquery$", - "type": "string", - "description": "Query string for the items to search in the database that have a good match", - }, + "schema": IsDict( + { + "anyOf": [ + { + "type": "string", + "minLength": 3, + "maxLength": 50, + "pattern": "^fixedquery$", + }, + {"type": "null"}, + ], + "title": "Query string", + "description": "Query string for the items to search in the database that have a good match", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Query string", + "maxLength": 50, + "minLength": 3, + "pattern": "^fixedquery$", + "type": "string", + "description": "Query string for the items to search in the database that have a good match", + } + ), "name": "item-query", "in": "query", } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_py310.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_py310.py index 63524d291..1c3a09d39 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_py310.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_py310.py @@ -1,5 +1,7 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from ...utils import needs_py310 @@ -12,42 +14,60 @@ def get_client(): return client -regex_error = { - "detail": [ - { - "ctx": {"pattern": "^fixedquery$"}, - "loc": ["query", "item-query"], - "msg": 'string does not match regex "^fixedquery$"', - "type": "value_error.str.regex", - } - ] -} +@needs_py310 +def test_query_params_str_validations_no_query(client: TestClient): + response = client.get("/items/") + assert response.status_code == 200 + assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + + +@needs_py310 +def test_query_params_str_validations_item_query_fixedquery(client: TestClient): + response = client.get("/items/", params={"item-query": "fixedquery"}) + assert response.status_code == 200 + assert response.json() == { + "items": [{"item_id": "Foo"}, {"item_id": "Bar"}], + "q": "fixedquery", + } + + +@needs_py310 +def test_query_params_str_validations_q_fixedquery(client: TestClient): + response = client.get("/items/", params={"q": "fixedquery"}) + assert response.status_code == 200 + assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} @needs_py310 -@pytest.mark.parametrize( - "q_name,q,expected_status,expected_response", - [ - (None, None, 200, {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}), - ( - "item-query", - "fixedquery", - 200, - {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}], "q": "fixedquery"}, - ), - ("q", "fixedquery", 200, {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}), - ("item-query", "nonregexquery", 422, regex_error), - ], -) -def test_query_params_str_validations( - q_name, q, expected_status, expected_response, client: TestClient -): - url = "/items/" - if q_name and q: - url = f"{url}?{q_name}={q}" - response = client.get(url) - assert response.status_code == expected_status - assert response.json() == expected_response +def test_query_params_str_validations_item_query_nonregexquery(client: TestClient): + response = client.get("/items/", params={"item-query": "nonregexquery"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "string_pattern_mismatch", + "loc": ["query", "item-query"], + "msg": "String should match pattern '^fixedquery$'", + "input": "nonregexquery", + "ctx": {"pattern": "^fixedquery$"}, + "url": match_pydantic_error_url("string_pattern_mismatch"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "ctx": {"pattern": "^fixedquery$"}, + "loc": ["query", "item-query"], + "msg": 'string does not match regex "^fixedquery$"', + "type": "value_error.str.regex", + } + ] + } + ) @needs_py310 @@ -83,14 +103,32 @@ def test_openapi_schema(client: TestClient): "description": "Query string for the items to search in the database that have a good match", "required": False, "deprecated": True, - "schema": { - "title": "Query string", - "maxLength": 50, - "minLength": 3, - "pattern": "^fixedquery$", - "type": "string", - "description": "Query string for the items to search in the database that have a good match", - }, + "schema": IsDict( + { + "anyOf": [ + { + "type": "string", + "minLength": 3, + "maxLength": 50, + "pattern": "^fixedquery$", + }, + {"type": "null"}, + ], + "title": "Query string", + "description": "Query string for the items to search in the database that have a good match", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Query string", + "maxLength": 50, + "minLength": 3, + "pattern": "^fixedquery$", + "type": "string", + "description": "Query string for the items to search in the database that have a good match", + } + ), "name": "item-query", "in": "query", } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011.py index 164ec1193..5ba39b05d 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.query_params_str_validations.tutorial011 import app @@ -49,11 +50,23 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": { - "title": "Q", - "type": "array", - "items": {"type": "string"}, - }, + "schema": IsDict( + { + "anyOf": [ + {"type": "array", "items": {"type": "string"}}, + {"type": "null"}, + ], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Q", + "type": "array", + "items": {"type": "string"}, + } + ), "name": "q", "in": "query", } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an.py index 2afaafd92..3942ea77a 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.query_params_str_validations.tutorial011_an import app @@ -49,11 +50,23 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": { - "title": "Q", - "type": "array", - "items": {"type": "string"}, - }, + "schema": IsDict( + { + "anyOf": [ + {"type": "array", "items": {"type": "string"}}, + {"type": "null"}, + ], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Q", + "type": "array", + "items": {"type": "string"}, + } + ), "name": "q", "in": "query", } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py310.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py310.py index fafd38337..f2ec38c95 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py310.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py310.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -59,11 +60,23 @@ def test_openapi_schema(client: TestClient): "parameters": [ { "required": False, - "schema": { - "title": "Q", - "type": "array", - "items": {"type": "string"}, - }, + "schema": IsDict( + { + "anyOf": [ + {"type": "array", "items": {"type": "string"}}, + {"type": "null"}, + ], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Q", + "type": "array", + "items": {"type": "string"}, + } + ), "name": "q", "in": "query", } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py39.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py39.py index f3fb47528..cd7b15679 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py39.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py39.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py39 @@ -59,11 +60,23 @@ def test_openapi_schema(client: TestClient): "parameters": [ { "required": False, - "schema": { - "title": "Q", - "type": "array", - "items": {"type": "string"}, - }, + "schema": IsDict( + { + "anyOf": [ + {"type": "array", "items": {"type": "string"}}, + {"type": "null"}, + ], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Q", + "type": "array", + "items": {"type": "string"}, + } + ), "name": "q", "in": "query", } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py310.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py310.py index 21f348f2b..bdc729516 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py310.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py310.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -59,11 +60,23 @@ def test_openapi_schema(client: TestClient): "parameters": [ { "required": False, - "schema": { - "title": "Q", - "type": "array", - "items": {"type": "string"}, - }, + "schema": IsDict( + { + "anyOf": [ + {"type": "array", "items": {"type": "string"}}, + {"type": "null"}, + ], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Q", + "type": "array", + "items": {"type": "string"}, + } + ), "name": "q", "in": "query", } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py39.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py39.py index f2c2a5a33..26ac56b2f 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py39.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py39.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py39 @@ -59,11 +60,23 @@ def test_openapi_schema(client: TestClient): "parameters": [ { "required": False, - "schema": { - "title": "Q", - "type": "array", - "items": {"type": "string"}, - }, + "schema": IsDict( + { + "anyOf": [ + {"type": "array", "items": {"type": "string"}}, + {"type": "null"}, + ], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Q", + "type": "array", + "items": {"type": "string"}, + } + ), "name": "q", "in": "query", } diff --git a/tests/test_tutorial/test_request_files/test_tutorial001.py b/tests/test_tutorial/test_request_files/test_tutorial001.py index 84c736180..91cc2b636 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001.py @@ -1,4 +1,6 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from docs_src.request_files.tutorial001 import app @@ -19,13 +21,59 @@ file_required = { def test_post_form_no_body(): response = client.post("/files/") assert response.status_code == 422, response.text - assert response.json() == file_required + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "file"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "file"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) def test_post_body_json(): response = client.post("/files/", json={"file": "Foo"}) assert response.status_code == 422, response.text - assert response.json() == file_required + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "file"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "file"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) def test_post_file(tmp_path): diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_02.py b/tests/test_tutorial/test_request_files/test_tutorial001_02.py index 8ebe4eafd..42f75442a 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001_02.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001_02.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.request_files.tutorial001_02 import app @@ -53,9 +54,22 @@ def test_openapi_schema(): "requestBody": { "content": { "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_file_files__post" - } + "schema": IsDict( + { + "allOf": [ + { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + ], + "title": "Body", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + ) } } }, @@ -84,9 +98,22 @@ def test_openapi_schema(): "requestBody": { "content": { "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" - } + "schema": IsDict( + { + "allOf": [ + { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + ], + "title": "Body", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + ) } } }, @@ -115,14 +142,38 @@ def test_openapi_schema(): "title": "Body_create_file_files__post", "type": "object", "properties": { - "file": {"title": "File", "type": "string", "format": "binary"} + "file": IsDict( + { + "title": "File", + "anyOf": [ + {"type": "string", "format": "binary"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "File", "type": "string", "format": "binary"} + ) }, }, "Body_create_upload_file_uploadfile__post": { "title": "Body_create_upload_file_uploadfile__post", "type": "object", "properties": { - "file": {"title": "File", "type": "string", "format": "binary"} + "file": IsDict( + { + "title": "File", + "anyOf": [ + {"type": "string", "format": "binary"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "File", "type": "string", "format": "binary"} + ) }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_02_an.py b/tests/test_tutorial/test_request_files/test_tutorial001_02_an.py index 5da8b320b..f63eb339c 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001_02_an.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001_02_an.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.request_files.tutorial001_02_an import app @@ -53,9 +54,22 @@ def test_openapi_schema(): "requestBody": { "content": { "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_file_files__post" - } + "schema": IsDict( + { + "allOf": [ + { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + ], + "title": "Body", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + ) } } }, @@ -84,9 +98,22 @@ def test_openapi_schema(): "requestBody": { "content": { "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" - } + "schema": IsDict( + { + "allOf": [ + { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + ], + "title": "Body", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + ) } } }, @@ -115,14 +142,38 @@ def test_openapi_schema(): "title": "Body_create_file_files__post", "type": "object", "properties": { - "file": {"title": "File", "type": "string", "format": "binary"} + "file": IsDict( + { + "title": "File", + "anyOf": [ + {"type": "string", "format": "binary"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "File", "type": "string", "format": "binary"} + ) }, }, "Body_create_upload_file_uploadfile__post": { "title": "Body_create_upload_file_uploadfile__post", "type": "object", "properties": { - "file": {"title": "File", "type": "string", "format": "binary"} + "file": IsDict( + { + "title": "File", + "anyOf": [ + {"type": "string", "format": "binary"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "File", "type": "string", "format": "binary"} + ) }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py310.py b/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py310.py index 166f59b1a..94b6ac67e 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py310.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py310.py @@ -1,6 +1,7 @@ from pathlib import Path import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -65,9 +66,22 @@ def test_openapi_schema(client: TestClient): "requestBody": { "content": { "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_file_files__post" - } + "schema": IsDict( + { + "allOf": [ + { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + ], + "title": "Body", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + ) } } }, @@ -96,9 +110,22 @@ def test_openapi_schema(client: TestClient): "requestBody": { "content": { "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" - } + "schema": IsDict( + { + "allOf": [ + { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + ], + "title": "Body", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + ) } } }, @@ -127,14 +154,38 @@ def test_openapi_schema(client: TestClient): "title": "Body_create_file_files__post", "type": "object", "properties": { - "file": {"title": "File", "type": "string", "format": "binary"} + "file": IsDict( + { + "title": "File", + "anyOf": [ + {"type": "string", "format": "binary"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "File", "type": "string", "format": "binary"} + ) }, }, "Body_create_upload_file_uploadfile__post": { "title": "Body_create_upload_file_uploadfile__post", "type": "object", "properties": { - "file": {"title": "File", "type": "string", "format": "binary"} + "file": IsDict( + { + "title": "File", + "anyOf": [ + {"type": "string", "format": "binary"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "File", "type": "string", "format": "binary"} + ) }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py39.py b/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py39.py index 02ea604b2..fcb39f8f1 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py39.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py39.py @@ -1,6 +1,7 @@ from pathlib import Path import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py39 @@ -65,9 +66,22 @@ def test_openapi_schema(client: TestClient): "requestBody": { "content": { "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_file_files__post" - } + "schema": IsDict( + { + "allOf": [ + { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + ], + "title": "Body", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + ) } } }, @@ -96,9 +110,22 @@ def test_openapi_schema(client: TestClient): "requestBody": { "content": { "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" - } + "schema": IsDict( + { + "allOf": [ + { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + ], + "title": "Body", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + ) } } }, @@ -127,14 +154,38 @@ def test_openapi_schema(client: TestClient): "title": "Body_create_file_files__post", "type": "object", "properties": { - "file": {"title": "File", "type": "string", "format": "binary"} + "file": IsDict( + { + "title": "File", + "anyOf": [ + {"type": "string", "format": "binary"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "File", "type": "string", "format": "binary"} + ) }, }, "Body_create_upload_file_uploadfile__post": { "title": "Body_create_upload_file_uploadfile__post", "type": "object", "properties": { - "file": {"title": "File", "type": "string", "format": "binary"} + "file": IsDict( + { + "title": "File", + "anyOf": [ + {"type": "string", "format": "binary"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "File", "type": "string", "format": "binary"} + ) }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_02_py310.py b/tests/test_tutorial/test_request_files/test_tutorial001_02_py310.py index c753e14d1..a700752a3 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001_02_py310.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001_02_py310.py @@ -1,6 +1,7 @@ from pathlib import Path import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -65,9 +66,22 @@ def test_openapi_schema(client: TestClient): "requestBody": { "content": { "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_file_files__post" - } + "schema": IsDict( + { + "allOf": [ + { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + ], + "title": "Body", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + ) } } }, @@ -96,9 +110,22 @@ def test_openapi_schema(client: TestClient): "requestBody": { "content": { "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" - } + "schema": IsDict( + { + "allOf": [ + { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + ], + "title": "Body", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + ) } } }, @@ -127,14 +154,38 @@ def test_openapi_schema(client: TestClient): "title": "Body_create_file_files__post", "type": "object", "properties": { - "file": {"title": "File", "type": "string", "format": "binary"} + "file": IsDict( + { + "title": "File", + "anyOf": [ + {"type": "string", "format": "binary"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "File", "type": "string", "format": "binary"} + ) }, }, "Body_create_upload_file_uploadfile__post": { "title": "Body_create_upload_file_uploadfile__post", "type": "object", "properties": { - "file": {"title": "File", "type": "string", "format": "binary"} + "file": IsDict( + { + "title": "File", + "anyOf": [ + {"type": "string", "format": "binary"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "File", "type": "string", "format": "binary"} + ) }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_an.py b/tests/test_tutorial/test_request_files/test_tutorial001_an.py index 6eb2d55dc..3021eb3c3 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001_an.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001_an.py @@ -1,31 +1,68 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from docs_src.request_files.tutorial001_an import app client = TestClient(app) -file_required = { - "detail": [ - { - "loc": ["body", "file"], - "msg": "field required", - "type": "value_error.missing", - } - ] -} - - def test_post_form_no_body(): response = client.post("/files/") assert response.status_code == 422, response.text - assert response.json() == file_required + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "file"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "file"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) def test_post_body_json(): response = client.post("/files/", json={"file": "Foo"}) assert response.status_code == 422, response.text - assert response.json() == file_required + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "file"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "file"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) def test_post_file(tmp_path): diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_an_py39.py b/tests/test_tutorial/test_request_files/test_tutorial001_an_py39.py index 4e3ef6869..04f3a4693 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001_an_py39.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001_an_py39.py @@ -1,5 +1,7 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from ...utils import needs_py39 @@ -12,29 +14,64 @@ def get_client(): return client -file_required = { - "detail": [ - { - "loc": ["body", "file"], - "msg": "field required", - "type": "value_error.missing", - } - ] -} - - @needs_py39 def test_post_form_no_body(client: TestClient): response = client.post("/files/") assert response.status_code == 422, response.text - assert response.json() == file_required + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "file"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "file"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) @needs_py39 def test_post_body_json(client: TestClient): response = client.post("/files/", json={"file": "Foo"}) assert response.status_code == 422, response.text - assert response.json() == file_required + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "file"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "file"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) @needs_py39 diff --git a/tests/test_tutorial/test_request_files/test_tutorial002.py b/tests/test_tutorial/test_request_files/test_tutorial002.py index 65a8a9e61..ed9680b62 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial002.py +++ b/tests/test_tutorial/test_request_files/test_tutorial002.py @@ -1,31 +1,68 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from docs_src.request_files.tutorial002 import app client = TestClient(app) -file_required = { - "detail": [ - { - "loc": ["body", "files"], - "msg": "field required", - "type": "value_error.missing", - } - ] -} - - def test_post_form_no_body(): response = client.post("/files/") assert response.status_code == 422, response.text - assert response.json() == file_required + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "files"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "files"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) def test_post_body_json(): response = client.post("/files/", json={"file": "Foo"}) assert response.status_code == 422, response.text - assert response.json() == file_required + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "files"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "files"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) def test_post_files(tmp_path): diff --git a/tests/test_tutorial/test_request_files/test_tutorial002_an.py b/tests/test_tutorial/test_request_files/test_tutorial002_an.py index 52a8e1964..ea8c1216c 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial002_an.py +++ b/tests/test_tutorial/test_request_files/test_tutorial002_an.py @@ -1,31 +1,68 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from docs_src.request_files.tutorial002_an import app client = TestClient(app) -file_required = { - "detail": [ - { - "loc": ["body", "files"], - "msg": "field required", - "type": "value_error.missing", - } - ] -} - - def test_post_form_no_body(): response = client.post("/files/") assert response.status_code == 422, response.text - assert response.json() == file_required + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "files"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "files"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) def test_post_body_json(): response = client.post("/files/", json={"file": "Foo"}) assert response.status_code == 422, response.text - assert response.json() == file_required + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "files"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "files"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) def test_post_files(tmp_path): diff --git a/tests/test_tutorial/test_request_files/test_tutorial002_an_py39.py b/tests/test_tutorial/test_request_files/test_tutorial002_an_py39.py index 6594e0116..6d5877836 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial002_an_py39.py +++ b/tests/test_tutorial/test_request_files/test_tutorial002_an_py39.py @@ -1,6 +1,8 @@ import pytest +from dirty_equals import IsDict from fastapi import FastAPI from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from ...utils import needs_py39 @@ -18,29 +20,64 @@ def get_client(app: FastAPI): return client -file_required = { - "detail": [ - { - "loc": ["body", "files"], - "msg": "field required", - "type": "value_error.missing", - } - ] -} - - @needs_py39 def test_post_form_no_body(client: TestClient): response = client.post("/files/") assert response.status_code == 422, response.text - assert response.json() == file_required + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "files"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "files"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) @needs_py39 def test_post_body_json(client: TestClient): response = client.post("/files/", json={"file": "Foo"}) assert response.status_code == 422, response.text - assert response.json() == file_required + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "files"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "files"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) @needs_py39 diff --git a/tests/test_tutorial/test_request_files/test_tutorial002_py39.py b/tests/test_tutorial/test_request_files/test_tutorial002_py39.py index bfe964604..2d0445421 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial002_py39.py +++ b/tests/test_tutorial/test_request_files/test_tutorial002_py39.py @@ -1,6 +1,8 @@ import pytest +from dirty_equals import IsDict from fastapi import FastAPI from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from ...utils import needs_py39 @@ -33,14 +35,60 @@ file_required = { def test_post_form_no_body(client: TestClient): response = client.post("/files/") assert response.status_code == 422, response.text - assert response.json() == file_required + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "files"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "files"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) @needs_py39 def test_post_body_json(client: TestClient): response = client.post("/files/", json={"file": "Foo"}) assert response.status_code == 422, response.text - assert response.json() == file_required + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "files"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "files"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) @needs_py39 diff --git a/tests/test_tutorial/test_request_forms/test_tutorial001.py b/tests/test_tutorial/test_request_forms/test_tutorial001.py index 4a2a7abe9..805daeb10 100644 --- a/tests/test_tutorial/test_request_forms/test_tutorial001.py +++ b/tests/test_tutorial/test_request_forms/test_tutorial001.py @@ -1,72 +1,164 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url -from docs_src.request_forms.tutorial001 import app -client = TestClient(app) +@pytest.fixture(name="client") +def get_client(): + from docs_src.request_forms.tutorial001 import app + client = TestClient(app) + return client -password_required = { - "detail": [ + +def test_post_body_form(client: TestClient): + response = client.post("/login/", data={"username": "Foo", "password": "secret"}) + assert response.status_code == 200 + assert response.json() == {"username": "Foo"} + + +def test_post_body_form_no_password(client: TestClient): + response = client.post("/login/", data={"username": "Foo"}) + assert response.status_code == 422 + assert response.json() == IsDict( { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", + "detail": [ + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] } - ] -} -username_required = { - "detail": [ + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", + "detail": [ + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + } + ] } - ] -} -username_and_password_required = { - "detail": [ + ) + + +def test_post_body_form_no_username(client: TestClient): + response = client.post("/login/", data={"password": "secret"}) + assert response.status_code == 422 + assert response.json() == IsDict( { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - }, + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - }, - ] -} + "detail": [ + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) -@pytest.mark.parametrize( - "path,body,expected_status,expected_response", - [ - ( - "/login/", - {"username": "Foo", "password": "secret"}, - 200, - {"username": "Foo"}, - ), - ("/login/", {"username": "Foo"}, 422, password_required), - ("/login/", {"password": "secret"}, 422, username_required), - ("/login/", None, 422, username_and_password_required), - ], -) -def test_post_body_form(path, body, expected_status, expected_response): - response = client.post(path, data=body) - assert response.status_code == expected_status - assert response.json() == expected_response +def test_post_body_form_no_data(client: TestClient): + response = client.post("/login/") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) -def test_post_body_json(): +def test_post_body_json(client: TestClient): response = client.post("/login/", json={"username": "Foo", "password": "secret"}) assert response.status_code == 422, response.text - assert response.json() == username_and_password_required + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { diff --git a/tests/test_tutorial/test_request_forms/test_tutorial001_an.py b/tests/test_tutorial/test_request_forms/test_tutorial001_an.py index 347361344..c43a0b695 100644 --- a/tests/test_tutorial/test_request_forms/test_tutorial001_an.py +++ b/tests/test_tutorial/test_request_forms/test_tutorial001_an.py @@ -1,72 +1,164 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url -from docs_src.request_forms.tutorial001_an import app -client = TestClient(app) +@pytest.fixture(name="client") +def get_client(): + from docs_src.request_forms.tutorial001_an import app + client = TestClient(app) + return client -password_required = { - "detail": [ + +def test_post_body_form(client: TestClient): + response = client.post("/login/", data={"username": "Foo", "password": "secret"}) + assert response.status_code == 200 + assert response.json() == {"username": "Foo"} + + +def test_post_body_form_no_password(client: TestClient): + response = client.post("/login/", data={"username": "Foo"}) + assert response.status_code == 422 + assert response.json() == IsDict( { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", + "detail": [ + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] } - ] -} -username_required = { - "detail": [ + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", + "detail": [ + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + } + ] } - ] -} -username_and_password_required = { - "detail": [ + ) + + +def test_post_body_form_no_username(client: TestClient): + response = client.post("/login/", data={"password": "secret"}) + assert response.status_code == 422 + assert response.json() == IsDict( { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - }, + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - }, - ] -} + "detail": [ + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) -@pytest.mark.parametrize( - "path,body,expected_status,expected_response", - [ - ( - "/login/", - {"username": "Foo", "password": "secret"}, - 200, - {"username": "Foo"}, - ), - ("/login/", {"username": "Foo"}, 422, password_required), - ("/login/", {"password": "secret"}, 422, username_required), - ("/login/", None, 422, username_and_password_required), - ], -) -def test_post_body_form(path, body, expected_status, expected_response): - response = client.post(path, data=body) - assert response.status_code == expected_status - assert response.json() == expected_response +def test_post_body_form_no_data(client: TestClient): + response = client.post("/login/") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) -def test_post_body_json(): +def test_post_body_json(client: TestClient): response = client.post("/login/", json={"username": "Foo", "password": "secret"}) assert response.status_code == 422, response.text - assert response.json() == username_and_password_required + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { diff --git a/tests/test_tutorial/test_request_forms/test_tutorial001_an_py39.py b/tests/test_tutorial/test_request_forms/test_tutorial001_an_py39.py index e65a8823e..078b812aa 100644 --- a/tests/test_tutorial/test_request_forms/test_tutorial001_an_py39.py +++ b/tests/test_tutorial/test_request_forms/test_tutorial001_an_py39.py @@ -1,5 +1,7 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from ...utils import needs_py39 @@ -12,68 +14,155 @@ def get_client(): return client -password_required = { - "detail": [ +@needs_py39 +def test_post_body_form(client: TestClient): + response = client.post("/login/", data={"username": "Foo", "password": "secret"}) + assert response.status_code == 200 + assert response.json() == {"username": "Foo"} + + +@needs_py39 +def test_post_body_form_no_password(client: TestClient): + response = client.post("/login/", data={"username": "Foo"}) + assert response.status_code == 422 + assert response.json() == IsDict( { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", + "detail": [ + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] } - ] -} -username_required = { - "detail": [ + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", + "detail": [ + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + } + ] } - ] -} -username_and_password_required = { - "detail": [ + ) + + +@needs_py39 +def test_post_body_form_no_username(client: TestClient): + response = client.post("/login/", data={"password": "secret"}) + assert response.status_code == 422 + assert response.json() == IsDict( { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - }, + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - }, - ] -} + "detail": [ + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) @needs_py39 -@pytest.mark.parametrize( - "path,body,expected_status,expected_response", - [ - ( - "/login/", - {"username": "Foo", "password": "secret"}, - 200, - {"username": "Foo"}, - ), - ("/login/", {"username": "Foo"}, 422, password_required), - ("/login/", {"password": "secret"}, 422, username_required), - ("/login/", None, 422, username_and_password_required), - ], -) -def test_post_body_form( - path, body, expected_status, expected_response, client: TestClient -): - response = client.post(path, data=body) - assert response.status_code == expected_status - assert response.json() == expected_response +def test_post_body_form_no_data(client: TestClient): + response = client.post("/login/") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) @needs_py39 def test_post_body_json(client: TestClient): response = client.post("/login/", json={"username": "Foo", "password": "secret"}) assert response.status_code == 422, response.text - assert response.json() == username_and_password_required + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) @needs_py39 diff --git a/tests/test_tutorial/test_request_forms_and_files/test_tutorial001.py b/tests/test_tutorial/test_request_forms_and_files/test_tutorial001.py index be12656d2..cac58639f 100644 --- a/tests/test_tutorial/test_request_forms_and_files/test_tutorial001.py +++ b/tests/test_tutorial/test_request_forms_and_files/test_tutorial001.py @@ -1,82 +1,171 @@ +import pytest +from dirty_equals import IsDict +from fastapi import FastAPI from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url -from docs_src.request_forms_and_files.tutorial001 import app -client = TestClient(app) +@pytest.fixture(name="app") +def get_app(): + from docs_src.request_forms_and_files.tutorial001 import app + return app -file_required = { - "detail": [ - { - "loc": ["body", "file"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "fileb"], - "msg": "field required", - "type": "value_error.missing", - }, - ] -} - -token_required = { - "detail": [ - { - "loc": ["body", "fileb"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] -} -# {'detail': [, {'loc': ['body', 'token'], 'msg': 'field required', 'type': 'value_error.missing'}]} - -file_and_token_required = { - "detail": [ - { - "loc": ["body", "file"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "fileb"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] -} +@pytest.fixture(name="client") +def get_client(app: FastAPI): + client = TestClient(app) + return client -def test_post_form_no_body(): +def test_post_form_no_body(client: TestClient): response = client.post("/files/") assert response.status_code == 422, response.text - assert response.json() == file_and_token_required + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "file"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "fileb"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "file"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "fileb"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) -def test_post_form_no_file(): +def test_post_form_no_file(client: TestClient): response = client.post("/files/", data={"token": "foo"}) assert response.status_code == 422, response.text - assert response.json() == file_required + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "file"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "fileb"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "file"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "fileb"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) -def test_post_body_json(): +def test_post_body_json(client: TestClient): response = client.post("/files/", json={"file": "Foo", "token": "Bar"}) assert response.status_code == 422, response.text - assert response.json() == file_and_token_required + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "file"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "fileb"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "file"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "fileb"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) -def test_post_file_no_token(tmp_path): +def test_post_file_no_token(tmp_path, app: FastAPI): path = tmp_path / "test.txt" path.write_bytes(b"") @@ -84,10 +173,45 @@ def test_post_file_no_token(tmp_path): with path.open("rb") as file: response = client.post("/files/", files={"file": file}) assert response.status_code == 422, response.text - assert response.json() == token_required + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "fileb"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "fileb"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) -def test_post_files_and_token(tmp_path): +def test_post_files_and_token(tmp_path, app: FastAPI): patha = tmp_path / "test.txt" pathb = tmp_path / "testb.txt" patha.write_text("") @@ -108,7 +232,7 @@ def test_post_files_and_token(tmp_path): } -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { diff --git a/tests/test_tutorial/test_request_forms_and_files/test_tutorial001_an.py b/tests/test_tutorial/test_request_forms_and_files/test_tutorial001_an.py index a5fcb3a94..009568048 100644 --- a/tests/test_tutorial/test_request_forms_and_files/test_tutorial001_an.py +++ b/tests/test_tutorial/test_request_forms_and_files/test_tutorial001_an.py @@ -1,82 +1,171 @@ +import pytest +from dirty_equals import IsDict +from fastapi import FastAPI from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url -from docs_src.request_forms_and_files.tutorial001_an import app -client = TestClient(app) +@pytest.fixture(name="app") +def get_app(): + from docs_src.request_forms_and_files.tutorial001_an import app + return app -file_required = { - "detail": [ - { - "loc": ["body", "file"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "fileb"], - "msg": "field required", - "type": "value_error.missing", - }, - ] -} - -token_required = { - "detail": [ - { - "loc": ["body", "fileb"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] -} -# {'detail': [, {'loc': ['body', 'token'], 'msg': 'field required', 'type': 'value_error.missing'}]} - -file_and_token_required = { - "detail": [ - { - "loc": ["body", "file"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "fileb"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] -} +@pytest.fixture(name="client") +def get_client(app: FastAPI): + client = TestClient(app) + return client -def test_post_form_no_body(): +def test_post_form_no_body(client: TestClient): response = client.post("/files/") assert response.status_code == 422, response.text - assert response.json() == file_and_token_required + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "file"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "fileb"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "file"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "fileb"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) -def test_post_form_no_file(): +def test_post_form_no_file(client: TestClient): response = client.post("/files/", data={"token": "foo"}) assert response.status_code == 422, response.text - assert response.json() == file_required + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "file"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "fileb"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "file"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "fileb"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) -def test_post_body_json(): +def test_post_body_json(client: TestClient): response = client.post("/files/", json={"file": "Foo", "token": "Bar"}) assert response.status_code == 422, response.text - assert response.json() == file_and_token_required + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "file"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "fileb"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "file"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "fileb"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) -def test_post_file_no_token(tmp_path): +def test_post_file_no_token(tmp_path, app: FastAPI): path = tmp_path / "test.txt" path.write_bytes(b"") @@ -84,10 +173,45 @@ def test_post_file_no_token(tmp_path): with path.open("rb") as file: response = client.post("/files/", files={"file": file}) assert response.status_code == 422, response.text - assert response.json() == token_required + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "fileb"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "fileb"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) -def test_post_files_and_token(tmp_path): +def test_post_files_and_token(tmp_path, app: FastAPI): patha = tmp_path / "test.txt" pathb = tmp_path / "testb.txt" patha.write_text("") @@ -108,7 +232,7 @@ def test_post_files_and_token(tmp_path): } -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { diff --git a/tests/test_tutorial/test_request_forms_and_files/test_tutorial001_an_py39.py b/tests/test_tutorial/test_request_forms_and_files/test_tutorial001_an_py39.py index 6eacb2fcf..3d007e90b 100644 --- a/tests/test_tutorial/test_request_forms_and_files/test_tutorial001_an_py39.py +++ b/tests/test_tutorial/test_request_forms_and_files/test_tutorial001_an_py39.py @@ -1,6 +1,8 @@ import pytest +from dirty_equals import IsDict from fastapi import FastAPI from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from ...utils import needs_py39 @@ -18,78 +20,154 @@ def get_client(app: FastAPI): return client -file_required = { - "detail": [ - { - "loc": ["body", "file"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "fileb"], - "msg": "field required", - "type": "value_error.missing", - }, - ] -} - -token_required = { - "detail": [ - { - "loc": ["body", "fileb"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] -} - -# {'detail': [, {'loc': ['body', 'token'], 'msg': 'field required', 'type': 'value_error.missing'}]} - -file_and_token_required = { - "detail": [ - { - "loc": ["body", "file"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "fileb"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] -} - - @needs_py39 def test_post_form_no_body(client: TestClient): response = client.post("/files/") assert response.status_code == 422, response.text - assert response.json() == file_and_token_required + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "file"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "fileb"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "file"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "fileb"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) @needs_py39 def test_post_form_no_file(client: TestClient): response = client.post("/files/", data={"token": "foo"}) assert response.status_code == 422, response.text - assert response.json() == file_required + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "file"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "fileb"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "file"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "fileb"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) @needs_py39 def test_post_body_json(client: TestClient): response = client.post("/files/", json={"file": "Foo", "token": "Bar"}) assert response.status_code == 422, response.text - assert response.json() == file_and_token_required + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "file"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "fileb"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "file"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "fileb"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) @needs_py39 @@ -101,7 +179,42 @@ def test_post_file_no_token(tmp_path, app: FastAPI): with path.open("rb") as file: response = client.post("/files/", files={"file": file}) assert response.status_code == 422, response.text - assert response.json() == token_required + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "fileb"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "fileb"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) @needs_py39 diff --git a/tests/test_tutorial/test_response_model/test_tutorial003.py b/tests/test_tutorial/test_response_model/test_tutorial003.py index 9cb0419a3..20221399b 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial003.py +++ b/tests/test_tutorial/test_response_model/test_tutorial003.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.response_model.tutorial003 import app @@ -78,7 +79,16 @@ def test_openapi_schema(): "type": "string", "format": "email", }, - "full_name": {"title": "Full Name", "type": "string"}, + "full_name": IsDict( + { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Full Name", "type": "string"} + ), }, }, "UserIn": { @@ -93,7 +103,16 @@ def test_openapi_schema(): "type": "string", "format": "email", }, - "full_name": {"title": "Full Name", "type": "string"}, + "full_name": IsDict( + { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Full Name", "type": "string"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_01.py b/tests/test_tutorial/test_response_model/test_tutorial003_01.py index 8b8fe514a..e8f0658f4 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial003_01.py +++ b/tests/test_tutorial/test_response_model/test_tutorial003_01.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.response_model.tutorial003_01 import app @@ -78,7 +79,16 @@ def test_openapi_schema(): "type": "string", "format": "email", }, - "full_name": {"title": "Full Name", "type": "string"}, + "full_name": IsDict( + { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Full Name", "type": "string"} + ), }, }, "HTTPValidationError": { @@ -103,7 +113,16 @@ def test_openapi_schema(): "type": "string", "format": "email", }, - "full_name": {"title": "Full Name", "type": "string"}, + "full_name": IsDict( + { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Full Name", "type": "string"} + ), "password": {"title": "Password", "type": "string"}, }, }, diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_01_py310.py b/tests/test_tutorial/test_response_model/test_tutorial003_01_py310.py index 01dc8e71c..a69f8cc8d 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial003_01_py310.py +++ b/tests/test_tutorial/test_response_model/test_tutorial003_01_py310.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -87,7 +88,16 @@ def test_openapi_schema(client: TestClient): "type": "string", "format": "email", }, - "full_name": {"title": "Full Name", "type": "string"}, + "full_name": IsDict( + { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Full Name", "type": "string"} + ), }, }, "HTTPValidationError": { @@ -112,7 +122,16 @@ def test_openapi_schema(client: TestClient): "type": "string", "format": "email", }, - "full_name": {"title": "Full Name", "type": "string"}, + "full_name": IsDict( + { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Full Name", "type": "string"} + ), "password": {"title": "Password", "type": "string"}, }, }, diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_py310.py b/tests/test_tutorial/test_response_model/test_tutorial003_py310.py index 602147b13..64dcd6cbd 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial003_py310.py +++ b/tests/test_tutorial/test_response_model/test_tutorial003_py310.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -87,7 +88,16 @@ def test_openapi_schema(client: TestClient): "type": "string", "format": "email", }, - "full_name": {"title": "Full Name", "type": "string"}, + "full_name": IsDict( + { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Full Name", "type": "string"} + ), }, }, "UserIn": { @@ -102,7 +112,16 @@ def test_openapi_schema(client: TestClient): "type": "string", "format": "email", }, - "full_name": {"title": "Full Name", "type": "string"}, + "full_name": IsDict( + { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Full Name", "type": "string"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_response_model/test_tutorial004.py b/tests/test_tutorial/test_response_model/test_tutorial004.py index 07af29207..8beb847d1 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial004.py +++ b/tests/test_tutorial/test_response_model/test_tutorial004.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.response_model.tutorial004 import app @@ -83,7 +84,16 @@ def test_openapi_schema(): "properties": { "name": {"title": "Name", "type": "string"}, "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), "tax": {"title": "Tax", "type": "number", "default": 10.5}, "tags": { "title": "Tags", diff --git a/tests/test_tutorial/test_response_model/test_tutorial004_py310.py b/tests/test_tutorial/test_response_model/test_tutorial004_py310.py index 90147fbdd..28eb88c34 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial004_py310.py +++ b/tests/test_tutorial/test_response_model/test_tutorial004_py310.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -91,7 +92,16 @@ def test_openapi_schema(client: TestClient): "properties": { "name": {"title": "Name", "type": "string"}, "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), "tax": {"title": "Tax", "type": "number", "default": 10.5}, "tags": { "title": "Tags", diff --git a/tests/test_tutorial/test_response_model/test_tutorial004_py39.py b/tests/test_tutorial/test_response_model/test_tutorial004_py39.py index 740a49590..9e1a21f8d 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial004_py39.py +++ b/tests/test_tutorial/test_response_model/test_tutorial004_py39.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py39 @@ -91,7 +92,16 @@ def test_openapi_schema(client: TestClient): "properties": { "name": {"title": "Name", "type": "string"}, "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), "tax": {"title": "Tax", "type": "number", "default": 10.5}, "tags": { "title": "Tags", diff --git a/tests/test_tutorial/test_response_model/test_tutorial005.py b/tests/test_tutorial/test_response_model/test_tutorial005.py index e8c8946c5..06e5d0fd1 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial005.py +++ b/tests/test_tutorial/test_response_model/test_tutorial005.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.response_model.tutorial005 import app @@ -106,7 +107,16 @@ def test_openapi_schema(): "properties": { "name": {"title": "Name", "type": "string"}, "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), "tax": {"title": "Tax", "type": "number", "default": 10.5}, }, }, diff --git a/tests/test_tutorial/test_response_model/test_tutorial005_py310.py b/tests/test_tutorial/test_response_model/test_tutorial005_py310.py index 388e030bd..0f1566243 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial005_py310.py +++ b/tests/test_tutorial/test_response_model/test_tutorial005_py310.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -116,7 +117,16 @@ def test_openapi_schema(client: TestClient): "properties": { "name": {"title": "Name", "type": "string"}, "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), "tax": {"title": "Tax", "type": "number", "default": 10.5}, }, }, diff --git a/tests/test_tutorial/test_response_model/test_tutorial006.py b/tests/test_tutorial/test_response_model/test_tutorial006.py index 548a3dbd8..6e6152b9f 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial006.py +++ b/tests/test_tutorial/test_response_model/test_tutorial006.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.response_model.tutorial006 import app @@ -106,7 +107,16 @@ def test_openapi_schema(): "properties": { "name": {"title": "Name", "type": "string"}, "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), "tax": {"title": "Tax", "type": "number", "default": 10.5}, }, }, diff --git a/tests/test_tutorial/test_response_model/test_tutorial006_py310.py b/tests/test_tutorial/test_response_model/test_tutorial006_py310.py index 075bb8079..9a980ab5b 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial006_py310.py +++ b/tests/test_tutorial/test_response_model/test_tutorial006_py310.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -116,7 +117,16 @@ def test_openapi_schema(client: TestClient): "properties": { "name": {"title": "Name", "type": "string"}, "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), "tax": {"title": "Tax", "type": "number", "default": 10.5}, }, }, diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial001.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial001.py new file mode 100644 index 000000000..98b187355 --- /dev/null +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial001.py @@ -0,0 +1,133 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_pydanticv2 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.schema_extra_example.tutorial001 import app + + client = TestClient(app) + return client + + +@needs_pydanticv2 +def test_post_body_example(client: TestClient): + response = client.put( + "/items/5", + json={ + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + ) + assert response.status_code == 200 + + +@needs_pydanticv2 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + # insert_assert(response.json()) + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "name": "item_id", + "in": "path", + "required": True, + "schema": {"type": "integer", "title": "Item Id"}, + } + ], + "requestBody": { + "required": True, + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "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", + }, + "Item": { + "properties": { + "name": {"type": "string", "title": "Name"}, + "description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Description", + }, + "price": {"type": "number", "title": "Price"}, + "tax": { + "anyOf": [{"type": "number"}, {"type": "null"}], + "title": "Tax", + }, + }, + "type": "object", + "required": ["name", "price"], + "title": "Item", + "examples": [ + { + "description": "A very nice Item", + "name": "Foo", + "price": 35.4, + "tax": 3.2, + } + ], + }, + "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", + }, + } + }, + } diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial001_pv1.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial001_pv1.py new file mode 100644 index 000000000..3520ef61d --- /dev/null +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial001_pv1.py @@ -0,0 +1,127 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_pydanticv1 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.schema_extra_example.tutorial001_pv1 import app + + client = TestClient(app) + return client + + +@needs_pydanticv1 +def test_post_body_example(client: TestClient): + response = client.put( + "/items/5", + json={ + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + ) + assert response.status_code == 200 + + +@needs_pydanticv1 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + # insert_assert(response.json()) + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"type": "integer", "title": "Item Id"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": {"$ref": "#/components/schemas/ValidationError"}, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "Item": { + "properties": { + "name": {"type": "string", "title": "Name"}, + "description": {"type": "string", "title": "Description"}, + "price": {"type": "number", "title": "Price"}, + "tax": {"type": "number", "title": "Tax"}, + }, + "type": "object", + "required": ["name", "price"], + "title": "Item", + "examples": [ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + } + ], + }, + "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", + }, + } + }, + } diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial001_py310.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial001_py310.py new file mode 100644 index 000000000..e63e33cda --- /dev/null +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial001_py310.py @@ -0,0 +1,135 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310, needs_pydanticv2 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.schema_extra_example.tutorial001_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +@needs_pydanticv2 +def test_post_body_example(client: TestClient): + response = client.put( + "/items/5", + json={ + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + ) + assert response.status_code == 200 + + +@needs_py310 +@needs_pydanticv2 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + # insert_assert(response.json()) + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "name": "item_id", + "in": "path", + "required": True, + "schema": {"type": "integer", "title": "Item Id"}, + } + ], + "requestBody": { + "required": True, + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "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", + }, + "Item": { + "properties": { + "name": {"type": "string", "title": "Name"}, + "description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Description", + }, + "price": {"type": "number", "title": "Price"}, + "tax": { + "anyOf": [{"type": "number"}, {"type": "null"}], + "title": "Tax", + }, + }, + "type": "object", + "required": ["name", "price"], + "title": "Item", + "examples": [ + { + "description": "A very nice Item", + "name": "Foo", + "price": 35.4, + "tax": 3.2, + } + ], + }, + "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", + }, + } + }, + } diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial001_py310_pv1.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial001_py310_pv1.py new file mode 100644 index 000000000..e036d6b68 --- /dev/null +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial001_py310_pv1.py @@ -0,0 +1,129 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310, needs_pydanticv1 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.schema_extra_example.tutorial001_py310_pv1 import app + + client = TestClient(app) + return client + + +@needs_py310 +@needs_pydanticv1 +def test_post_body_example(client: TestClient): + response = client.put( + "/items/5", + json={ + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + ) + assert response.status_code == 200 + + +@needs_py310 +@needs_pydanticv1 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + # insert_assert(response.json()) + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"type": "integer", "title": "Item Id"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": {"$ref": "#/components/schemas/ValidationError"}, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "Item": { + "properties": { + "name": {"type": "string", "title": "Name"}, + "description": {"type": "string", "title": "Description"}, + "price": {"type": "number", "title": "Price"}, + "tax": {"type": "number", "title": "Tax"}, + }, + "type": "object", + "required": ["name", "price"], + "title": "Item", + "examples": [ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + } + ], + }, + "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", + }, + } + }, + } diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial004.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial004.py index 313cd51d6..eac0d1e29 100644 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial004.py +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial004.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.schema_extra_example.tutorial004 import app @@ -41,23 +42,46 @@ def test_openapi_schema(): "requestBody": { "content": { "application/json": { - "schema": { - "allOf": [{"$ref": "#/components/schemas/Item"}], - "title": "Item", - "examples": [ - { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, - {"name": "Bar", "price": "35.4"}, - { - "name": "Baz", - "price": "thirty five point four", - }, - ], - } + "schema": IsDict( + { + "$ref": "#/components/schemas/Item", + "examples": [ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + {"name": "Bar", "price": "35.4"}, + { + "name": "Baz", + "price": "thirty five point four", + }, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "allOf": [ + {"$ref": "#/components/schemas/Item"} + ], + "title": "Item", + "examples": [ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + {"name": "Bar", "price": "35.4"}, + { + "name": "Baz", + "price": "thirty five point four", + }, + ], + } + ) } }, "required": True, @@ -100,9 +124,27 @@ def test_openapi_schema(): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, - "description": {"title": "Description", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), "price": {"title": "Price", "type": "number"}, - "tax": {"title": "Tax", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an.py index 353401b78..a9cecd098 100644 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an.py +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.schema_extra_example.tutorial004_an import app @@ -41,23 +42,46 @@ def test_openapi_schema(): "requestBody": { "content": { "application/json": { - "schema": { - "allOf": [{"$ref": "#/components/schemas/Item"}], - "title": "Item", - "examples": [ - { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, - {"name": "Bar", "price": "35.4"}, - { - "name": "Baz", - "price": "thirty five point four", - }, - ], - } + "schema": IsDict( + { + "$ref": "#/components/schemas/Item", + "examples": [ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + {"name": "Bar", "price": "35.4"}, + { + "name": "Baz", + "price": "thirty five point four", + }, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "allOf": [ + {"$ref": "#/components/schemas/Item"} + ], + "title": "Item", + "examples": [ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + {"name": "Bar", "price": "35.4"}, + { + "name": "Baz", + "price": "thirty five point four", + }, + ], + } + ) } }, "required": True, @@ -100,9 +124,27 @@ def test_openapi_schema(): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, - "description": {"title": "Description", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), "price": {"title": "Price", "type": "number"}, - "tax": {"title": "Tax", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py310.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py310.py index 79f4e1e1e..b6a735599 100644 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py310.py +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py310.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -50,23 +51,46 @@ def test_openapi_schema(client: TestClient): "requestBody": { "content": { "application/json": { - "schema": { - "allOf": [{"$ref": "#/components/schemas/Item"}], - "title": "Item", - "examples": [ - { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, - {"name": "Bar", "price": "35.4"}, - { - "name": "Baz", - "price": "thirty five point four", - }, - ], - } + "schema": IsDict( + { + "$ref": "#/components/schemas/Item", + "examples": [ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + {"name": "Bar", "price": "35.4"}, + { + "name": "Baz", + "price": "thirty five point four", + }, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "allOf": [ + {"$ref": "#/components/schemas/Item"} + ], + "title": "Item", + "examples": [ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + {"name": "Bar", "price": "35.4"}, + { + "name": "Baz", + "price": "thirty five point four", + }, + ], + } + ) } }, "required": True, @@ -109,9 +133,27 @@ def test_openapi_schema(client: TestClient): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, - "description": {"title": "Description", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), "price": {"title": "Price", "type": "number"}, - "tax": {"title": "Tax", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py39.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py39.py index 1ee120705..2493194a0 100644 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py39.py +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py39.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py39 @@ -50,23 +51,46 @@ def test_openapi_schema(client: TestClient): "requestBody": { "content": { "application/json": { - "schema": { - "allOf": [{"$ref": "#/components/schemas/Item"}], - "title": "Item", - "examples": [ - { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, - {"name": "Bar", "price": "35.4"}, - { - "name": "Baz", - "price": "thirty five point four", - }, - ], - } + "schema": IsDict( + { + "$ref": "#/components/schemas/Item", + "examples": [ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + {"name": "Bar", "price": "35.4"}, + { + "name": "Baz", + "price": "thirty five point four", + }, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "allOf": [ + {"$ref": "#/components/schemas/Item"} + ], + "title": "Item", + "examples": [ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + {"name": "Bar", "price": "35.4"}, + { + "name": "Baz", + "price": "thirty five point four", + }, + ], + } + ) } }, "required": True, @@ -109,9 +133,27 @@ def test_openapi_schema(client: TestClient): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, - "description": {"title": "Description", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), "price": {"title": "Price", "type": "number"}, - "tax": {"title": "Tax", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_py310.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_py310.py index b77368400..15f54bd5a 100644 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_py310.py +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_py310.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -50,23 +51,46 @@ def test_openapi_schema(client: TestClient): "requestBody": { "content": { "application/json": { - "schema": { - "allOf": [{"$ref": "#/components/schemas/Item"}], - "title": "Item", - "examples": [ - { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, - {"name": "Bar", "price": "35.4"}, - { - "name": "Baz", - "price": "thirty five point four", - }, - ], - } + "schema": IsDict( + { + "$ref": "#/components/schemas/Item", + "examples": [ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + {"name": "Bar", "price": "35.4"}, + { + "name": "Baz", + "price": "thirty five point four", + }, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "allOf": [ + {"$ref": "#/components/schemas/Item"} + ], + "title": "Item", + "examples": [ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + {"name": "Bar", "price": "35.4"}, + { + "name": "Baz", + "price": "thirty five point four", + }, + ], + } + ) } }, "required": True, @@ -109,9 +133,27 @@ def test_openapi_schema(client: TestClient): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, - "description": {"title": "Description", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), "price": {"title": "Price", "type": "number"}, - "tax": {"title": "Tax", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_security/test_tutorial003.py b/tests/test_tutorial/test_security/test_tutorial003.py index cb5cdaa04..18d4680f6 100644 --- a/tests/test_tutorial/test_security/test_tutorial003.py +++ b/tests/test_tutorial/test_security/test_tutorial003.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.security.tutorial003 import app @@ -126,16 +127,46 @@ def test_openapi_schema(): "required": ["username", "password"], "type": "object", "properties": { - "grant_type": { - "title": "Grant Type", - "pattern": "password", - "type": "string", - }, + "grant_type": IsDict( + { + "title": "Grant Type", + "anyOf": [ + {"pattern": "password", "type": "string"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Grant Type", + "pattern": "password", + "type": "string", + } + ), "username": {"title": "Username", "type": "string"}, "password": {"title": "Password", "type": "string"}, "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": {"title": "Client Id", "type": "string"}, - "client_secret": {"title": "Client Secret", "type": "string"}, + "client_id": IsDict( + { + "title": "Client Id", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Id", "type": "string"} + ), + "client_secret": IsDict( + { + "title": "Client Secret", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Secret", "type": "string"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_security/test_tutorial003_an.py b/tests/test_tutorial/test_security/test_tutorial003_an.py index 26e68a029..a8f64d0c6 100644 --- a/tests/test_tutorial/test_security/test_tutorial003_an.py +++ b/tests/test_tutorial/test_security/test_tutorial003_an.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.security.tutorial003_an import app @@ -126,16 +127,46 @@ def test_openapi_schema(): "required": ["username", "password"], "type": "object", "properties": { - "grant_type": { - "title": "Grant Type", - "pattern": "password", - "type": "string", - }, + "grant_type": IsDict( + { + "title": "Grant Type", + "anyOf": [ + {"pattern": "password", "type": "string"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Grant Type", + "pattern": "password", + "type": "string", + } + ), "username": {"title": "Username", "type": "string"}, "password": {"title": "Password", "type": "string"}, "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": {"title": "Client Id", "type": "string"}, - "client_secret": {"title": "Client Secret", "type": "string"}, + "client_id": IsDict( + { + "title": "Client Id", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Id", "type": "string"} + ), + "client_secret": IsDict( + { + "title": "Client Secret", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Secret", "type": "string"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_security/test_tutorial003_an_py310.py b/tests/test_tutorial/test_security/test_tutorial003_an_py310.py index 1250d4afb..7cbbcee2f 100644 --- a/tests/test_tutorial/test_security/test_tutorial003_an_py310.py +++ b/tests/test_tutorial/test_security/test_tutorial003_an_py310.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -142,16 +143,46 @@ def test_openapi_schema(client: TestClient): "required": ["username", "password"], "type": "object", "properties": { - "grant_type": { - "title": "Grant Type", - "pattern": "password", - "type": "string", - }, + "grant_type": IsDict( + { + "title": "Grant Type", + "anyOf": [ + {"pattern": "password", "type": "string"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Grant Type", + "pattern": "password", + "type": "string", + } + ), "username": {"title": "Username", "type": "string"}, "password": {"title": "Password", "type": "string"}, "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": {"title": "Client Id", "type": "string"}, - "client_secret": {"title": "Client Secret", "type": "string"}, + "client_id": IsDict( + { + "title": "Client Id", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Id", "type": "string"} + ), + "client_secret": IsDict( + { + "title": "Client Secret", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Secret", "type": "string"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_security/test_tutorial003_an_py39.py b/tests/test_tutorial/test_security/test_tutorial003_an_py39.py index b74cfdc54..7b21fbcc9 100644 --- a/tests/test_tutorial/test_security/test_tutorial003_an_py39.py +++ b/tests/test_tutorial/test_security/test_tutorial003_an_py39.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py39 @@ -142,16 +143,46 @@ def test_openapi_schema(client: TestClient): "required": ["username", "password"], "type": "object", "properties": { - "grant_type": { - "title": "Grant Type", - "pattern": "password", - "type": "string", - }, + "grant_type": IsDict( + { + "title": "Grant Type", + "anyOf": [ + {"pattern": "password", "type": "string"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Grant Type", + "pattern": "password", + "type": "string", + } + ), "username": {"title": "Username", "type": "string"}, "password": {"title": "Password", "type": "string"}, "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": {"title": "Client Id", "type": "string"}, - "client_secret": {"title": "Client Secret", "type": "string"}, + "client_id": IsDict( + { + "title": "Client Id", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Id", "type": "string"} + ), + "client_secret": IsDict( + { + "title": "Client Secret", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Secret", "type": "string"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_security/test_tutorial003_py310.py b/tests/test_tutorial/test_security/test_tutorial003_py310.py index 8a75d2321..512504534 100644 --- a/tests/test_tutorial/test_security/test_tutorial003_py310.py +++ b/tests/test_tutorial/test_security/test_tutorial003_py310.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -142,16 +143,46 @@ def test_openapi_schema(client: TestClient): "required": ["username", "password"], "type": "object", "properties": { - "grant_type": { - "title": "Grant Type", - "pattern": "password", - "type": "string", - }, + "grant_type": IsDict( + { + "title": "Grant Type", + "anyOf": [ + {"pattern": "password", "type": "string"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Grant Type", + "pattern": "password", + "type": "string", + } + ), "username": {"title": "Username", "type": "string"}, "password": {"title": "Password", "type": "string"}, "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": {"title": "Client Id", "type": "string"}, - "client_secret": {"title": "Client Secret", "type": "string"}, + "client_id": IsDict( + { + "title": "Client Id", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Id", "type": "string"} + ), + "client_secret": IsDict( + { + "title": "Client Secret", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Secret", "type": "string"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_security/test_tutorial005.py b/tests/test_tutorial/test_security/test_tutorial005.py index 4e4b6afe8..22ae76f42 100644 --- a/tests/test_tutorial/test_security/test_tutorial005.py +++ b/tests/test_tutorial/test_security/test_tutorial005.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.security.tutorial005 import ( @@ -270,9 +271,36 @@ def test_openapi_schema(): "type": "object", "properties": { "username": {"title": "Username", "type": "string"}, - "email": {"title": "Email", "type": "string"}, - "full_name": {"title": "Full Name", "type": "string"}, - "disabled": {"title": "Disabled", "type": "boolean"}, + "email": IsDict( + { + "title": "Email", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Email", "type": "string"} + ), + "full_name": IsDict( + { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Full Name", "type": "string"} + ), + "disabled": IsDict( + { + "title": "Disabled", + "anyOf": [{"type": "boolean"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Disabled", "type": "boolean"} + ), }, }, "Token": { @@ -289,16 +317,46 @@ def test_openapi_schema(): "required": ["username", "password"], "type": "object", "properties": { - "grant_type": { - "title": "Grant Type", - "pattern": "password", - "type": "string", - }, + "grant_type": IsDict( + { + "title": "Grant Type", + "anyOf": [ + {"pattern": "password", "type": "string"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Grant Type", + "pattern": "password", + "type": "string", + } + ), "username": {"title": "Username", "type": "string"}, "password": {"title": "Password", "type": "string"}, "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": {"title": "Client Id", "type": "string"}, - "client_secret": {"title": "Client Secret", "type": "string"}, + "client_id": IsDict( + { + "title": "Client Id", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Id", "type": "string"} + ), + "client_secret": IsDict( + { + "title": "Client Secret", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Secret", "type": "string"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_security/test_tutorial005_an.py b/tests/test_tutorial/test_security/test_tutorial005_an.py index 51cc8329a..07239cc89 100644 --- a/tests/test_tutorial/test_security/test_tutorial005_an.py +++ b/tests/test_tutorial/test_security/test_tutorial005_an.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.security.tutorial005_an import ( @@ -270,9 +271,36 @@ def test_openapi_schema(): "type": "object", "properties": { "username": {"title": "Username", "type": "string"}, - "email": {"title": "Email", "type": "string"}, - "full_name": {"title": "Full Name", "type": "string"}, - "disabled": {"title": "Disabled", "type": "boolean"}, + "email": IsDict( + { + "title": "Email", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Email", "type": "string"} + ), + "full_name": IsDict( + { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Full Name", "type": "string"} + ), + "disabled": IsDict( + { + "title": "Disabled", + "anyOf": [{"type": "boolean"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Disabled", "type": "boolean"} + ), }, }, "Token": { @@ -289,16 +317,46 @@ def test_openapi_schema(): "required": ["username", "password"], "type": "object", "properties": { - "grant_type": { - "title": "Grant Type", - "pattern": "password", - "type": "string", - }, + "grant_type": IsDict( + { + "title": "Grant Type", + "anyOf": [ + {"pattern": "password", "type": "string"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Grant Type", + "pattern": "password", + "type": "string", + } + ), "username": {"title": "Username", "type": "string"}, "password": {"title": "Password", "type": "string"}, "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": {"title": "Client Id", "type": "string"}, - "client_secret": {"title": "Client Secret", "type": "string"}, + "client_id": IsDict( + { + "title": "Client Id", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Id", "type": "string"} + ), + "client_secret": IsDict( + { + "title": "Client Secret", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Secret", "type": "string"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_security/test_tutorial005_an_py310.py b/tests/test_tutorial/test_security/test_tutorial005_an_py310.py index b0d0fed12..1ab836639 100644 --- a/tests/test_tutorial/test_security/test_tutorial005_an_py310.py +++ b/tests/test_tutorial/test_security/test_tutorial005_an_py310.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -298,9 +299,36 @@ def test_openapi_schema(client: TestClient): "type": "object", "properties": { "username": {"title": "Username", "type": "string"}, - "email": {"title": "Email", "type": "string"}, - "full_name": {"title": "Full Name", "type": "string"}, - "disabled": {"title": "Disabled", "type": "boolean"}, + "email": IsDict( + { + "title": "Email", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Email", "type": "string"} + ), + "full_name": IsDict( + { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Full Name", "type": "string"} + ), + "disabled": IsDict( + { + "title": "Disabled", + "anyOf": [{"type": "boolean"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Disabled", "type": "boolean"} + ), }, }, "Token": { @@ -317,16 +345,46 @@ def test_openapi_schema(client: TestClient): "required": ["username", "password"], "type": "object", "properties": { - "grant_type": { - "title": "Grant Type", - "pattern": "password", - "type": "string", - }, + "grant_type": IsDict( + { + "title": "Grant Type", + "anyOf": [ + {"pattern": "password", "type": "string"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Grant Type", + "pattern": "password", + "type": "string", + } + ), "username": {"title": "Username", "type": "string"}, "password": {"title": "Password", "type": "string"}, "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": {"title": "Client Id", "type": "string"}, - "client_secret": {"title": "Client Secret", "type": "string"}, + "client_id": IsDict( + { + "title": "Client Id", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Id", "type": "string"} + ), + "client_secret": IsDict( + { + "title": "Client Secret", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Secret", "type": "string"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_security/test_tutorial005_an_py39.py b/tests/test_tutorial/test_security/test_tutorial005_an_py39.py index 26deaaf3c..6aabbe04a 100644 --- a/tests/test_tutorial/test_security/test_tutorial005_an_py39.py +++ b/tests/test_tutorial/test_security/test_tutorial005_an_py39.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py39 @@ -298,9 +299,36 @@ def test_openapi_schema(client: TestClient): "type": "object", "properties": { "username": {"title": "Username", "type": "string"}, - "email": {"title": "Email", "type": "string"}, - "full_name": {"title": "Full Name", "type": "string"}, - "disabled": {"title": "Disabled", "type": "boolean"}, + "email": IsDict( + { + "title": "Email", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Email", "type": "string"} + ), + "full_name": IsDict( + { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Full Name", "type": "string"} + ), + "disabled": IsDict( + { + "title": "Disabled", + "anyOf": [{"type": "boolean"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Disabled", "type": "boolean"} + ), }, }, "Token": { @@ -317,16 +345,46 @@ def test_openapi_schema(client: TestClient): "required": ["username", "password"], "type": "object", "properties": { - "grant_type": { - "title": "Grant Type", - "pattern": "password", - "type": "string", - }, + "grant_type": IsDict( + { + "title": "Grant Type", + "anyOf": [ + {"pattern": "password", "type": "string"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Grant Type", + "pattern": "password", + "type": "string", + } + ), "username": {"title": "Username", "type": "string"}, "password": {"title": "Password", "type": "string"}, "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": {"title": "Client Id", "type": "string"}, - "client_secret": {"title": "Client Secret", "type": "string"}, + "client_id": IsDict( + { + "title": "Client Id", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Id", "type": "string"} + ), + "client_secret": IsDict( + { + "title": "Client Secret", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Secret", "type": "string"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_security/test_tutorial005_py310.py b/tests/test_tutorial/test_security/test_tutorial005_py310.py index e93f34c3b..c21884df8 100644 --- a/tests/test_tutorial/test_security/test_tutorial005_py310.py +++ b/tests/test_tutorial/test_security/test_tutorial005_py310.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -298,9 +299,36 @@ def test_openapi_schema(client: TestClient): "type": "object", "properties": { "username": {"title": "Username", "type": "string"}, - "email": {"title": "Email", "type": "string"}, - "full_name": {"title": "Full Name", "type": "string"}, - "disabled": {"title": "Disabled", "type": "boolean"}, + "email": IsDict( + { + "title": "Email", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Email", "type": "string"} + ), + "full_name": IsDict( + { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Full Name", "type": "string"} + ), + "disabled": IsDict( + { + "title": "Disabled", + "anyOf": [{"type": "boolean"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Disabled", "type": "boolean"} + ), }, }, "Token": { @@ -317,16 +345,46 @@ def test_openapi_schema(client: TestClient): "required": ["username", "password"], "type": "object", "properties": { - "grant_type": { - "title": "Grant Type", - "pattern": "password", - "type": "string", - }, + "grant_type": IsDict( + { + "title": "Grant Type", + "anyOf": [ + {"pattern": "password", "type": "string"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Grant Type", + "pattern": "password", + "type": "string", + } + ), "username": {"title": "Username", "type": "string"}, "password": {"title": "Password", "type": "string"}, "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": {"title": "Client Id", "type": "string"}, - "client_secret": {"title": "Client Secret", "type": "string"}, + "client_id": IsDict( + { + "title": "Client Id", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Id", "type": "string"} + ), + "client_secret": IsDict( + { + "title": "Client Secret", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Secret", "type": "string"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_security/test_tutorial005_py39.py b/tests/test_tutorial/test_security/test_tutorial005_py39.py index 737a8548f..170c5d60b 100644 --- a/tests/test_tutorial/test_security/test_tutorial005_py39.py +++ b/tests/test_tutorial/test_security/test_tutorial005_py39.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py39 @@ -298,9 +299,36 @@ def test_openapi_schema(client: TestClient): "type": "object", "properties": { "username": {"title": "Username", "type": "string"}, - "email": {"title": "Email", "type": "string"}, - "full_name": {"title": "Full Name", "type": "string"}, - "disabled": {"title": "Disabled", "type": "boolean"}, + "email": IsDict( + { + "title": "Email", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Email", "type": "string"} + ), + "full_name": IsDict( + { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Full Name", "type": "string"} + ), + "disabled": IsDict( + { + "title": "Disabled", + "anyOf": [{"type": "boolean"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Disabled", "type": "boolean"} + ), }, }, "Token": { @@ -317,16 +345,46 @@ def test_openapi_schema(client: TestClient): "required": ["username", "password"], "type": "object", "properties": { - "grant_type": { - "title": "Grant Type", - "pattern": "password", - "type": "string", - }, + "grant_type": IsDict( + { + "title": "Grant Type", + "anyOf": [ + {"pattern": "password", "type": "string"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Grant Type", + "pattern": "password", + "type": "string", + } + ), "username": {"title": "Username", "type": "string"}, "password": {"title": "Password", "type": "string"}, "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": {"title": "Client Id", "type": "string"}, - "client_secret": {"title": "Client Secret", "type": "string"}, + "client_id": IsDict( + { + "title": "Client Id", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Id", "type": "string"} + ), + "client_secret": IsDict( + { + "title": "Client Secret", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Secret", "type": "string"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_settings/test_app02.py b/tests/test_tutorial/test_settings/test_app02.py index fd32b8766..eced88c04 100644 --- a/tests/test_tutorial/test_settings/test_app02.py +++ b/tests/test_tutorial/test_settings/test_app02.py @@ -1,17 +1,20 @@ -from fastapi.testclient import TestClient from pytest import MonkeyPatch -from docs_src.settings.app02 import main, test_main - -client = TestClient(main.app) +from ...utils import needs_pydanticv2 +@needs_pydanticv2 def test_settings(monkeypatch: MonkeyPatch): + from docs_src.settings.app02 import main + monkeypatch.setenv("ADMIN_EMAIL", "admin@example.com") settings = main.get_settings() assert settings.app_name == "Awesome API" assert settings.items_per_user == 50 +@needs_pydanticv2 def test_override_settings(): + from docs_src.settings.app02 import test_main + test_main.test_app() diff --git a/tests/test_tutorial/test_settings/test_tutorial001.py b/tests/test_tutorial/test_settings/test_tutorial001.py new file mode 100644 index 000000000..eb30dbcee --- /dev/null +++ b/tests/test_tutorial/test_settings/test_tutorial001.py @@ -0,0 +1,19 @@ +from fastapi.testclient import TestClient +from pytest import MonkeyPatch + +from ...utils import needs_pydanticv2 + + +@needs_pydanticv2 +def test_settings(monkeypatch: MonkeyPatch): + monkeypatch.setenv("ADMIN_EMAIL", "admin@example.com") + from docs_src.settings.tutorial001 import app + + client = TestClient(app) + response = client.get("/info") + assert response.status_code == 200, response.text + assert response.json() == { + "app_name": "Awesome API", + "admin_email": "admin@example.com", + "items_per_user": 50, + } diff --git a/tests/test_tutorial/test_settings/test_tutorial001_pv1.py b/tests/test_tutorial/test_settings/test_tutorial001_pv1.py new file mode 100644 index 000000000..e4659de66 --- /dev/null +++ b/tests/test_tutorial/test_settings/test_tutorial001_pv1.py @@ -0,0 +1,19 @@ +from fastapi.testclient import TestClient +from pytest import MonkeyPatch + +from ...utils import needs_pydanticv1 + + +@needs_pydanticv1 +def test_settings(monkeypatch: MonkeyPatch): + monkeypatch.setenv("ADMIN_EMAIL", "admin@example.com") + from docs_src.settings.tutorial001_pv1 import app + + client = TestClient(app) + response = client.get("/info") + assert response.status_code == 200, response.text + assert response.json() == { + "app_name": "Awesome API", + "admin_email": "admin@example.com", + "items_per_user": 50, + } diff --git a/tests/test_tutorial/test_sql_databases/test_sql_databases.py b/tests/test_tutorial/test_sql_databases/test_sql_databases.py index d927940da..03e747433 100644 --- a/tests/test_tutorial/test_sql_databases/test_sql_databases.py +++ b/tests/test_tutorial/test_sql_databases/test_sql_databases.py @@ -3,8 +3,11 @@ import os from pathlib import Path import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from ...utils import needs_pydanticv1 + @pytest.fixture(scope="module") def client(tmp_path_factory: pytest.TempPathFactory): @@ -26,6 +29,8 @@ def client(tmp_path_factory: pytest.TempPathFactory): os.chdir(cwd) +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_create_user(client): test_user = {"email": "johndoe@example.com", "password": "secret"} response = client.post("/users/", json=test_user) @@ -37,6 +42,8 @@ def test_create_user(client): assert response.status_code == 400, response.text +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_get_user(client): response = client.get("/users/1") assert response.status_code == 200, response.text @@ -45,11 +52,15 @@ def test_get_user(client): assert "id" in data +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_inexistent_user(client): response = client.get("/users/999") assert response.status_code == 404, response.text +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_get_users(client): response = client.get("/users/") assert response.status_code == 200, response.text @@ -58,6 +69,8 @@ def test_get_users(client): assert "id" in data[0] +# TODO: pv2 add Pydantic v2 version +@needs_pydanticv1 def test_create_item(client): item = {"title": "Foo", "description": "Something that fights"} response = client.post("/users/1/items/", json=item) @@ -75,6 +88,8 @@ def test_create_item(client): assert item_to_check["description"] == item["description"] +# TODO: pv2 add Pydantic v2 version +@needs_pydanticv1 def test_read_items(client): response = client.get("/items/") assert response.status_code == 200, response.text @@ -85,7 +100,9 @@ def test_read_items(client): assert "description" in first_item -def test_openapi_schema(client): +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { @@ -313,7 +330,16 @@ def test_openapi_schema(client): "type": "object", "properties": { "title": {"title": "Title", "type": "string"}, - "description": {"title": "Description", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), }, }, "Item": { @@ -322,7 +348,16 @@ def test_openapi_schema(client): "type": "object", "properties": { "title": {"title": "Title", "type": "string"}, - "description": {"title": "Description", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"}, + ), "id": {"title": "Id", "type": "integer"}, "owner_id": {"title": "Owner Id", "type": "integer"}, }, diff --git a/tests/test_tutorial/test_sql_databases/test_sql_databases_middleware.py b/tests/test_tutorial/test_sql_databases/test_sql_databases_middleware.py index 08d7b3533..a503ef2a6 100644 --- a/tests/test_tutorial/test_sql_databases/test_sql_databases_middleware.py +++ b/tests/test_tutorial/test_sql_databases/test_sql_databases_middleware.py @@ -2,8 +2,11 @@ import importlib from pathlib import Path import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from ...utils import needs_pydanticv1 + @pytest.fixture(scope="module") def client(): @@ -22,6 +25,8 @@ def client(): test_db.unlink() +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_create_user(client): test_user = {"email": "johndoe@example.com", "password": "secret"} response = client.post("/users/", json=test_user) @@ -33,6 +38,8 @@ def test_create_user(client): assert response.status_code == 400, response.text +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_get_user(client): response = client.get("/users/1") assert response.status_code == 200, response.text @@ -41,11 +48,15 @@ def test_get_user(client): assert "id" in data +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_inexistent_user(client): response = client.get("/users/999") assert response.status_code == 404, response.text +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_get_users(client): response = client.get("/users/") assert response.status_code == 200, response.text @@ -54,6 +65,8 @@ def test_get_users(client): assert "id" in data[0] +# TODO: pv2 add Pydantic v2 version +@needs_pydanticv1 def test_create_item(client): item = {"title": "Foo", "description": "Something that fights"} response = client.post("/users/1/items/", json=item) @@ -77,6 +90,8 @@ def test_create_item(client): assert item_to_check["description"] == item["description"] +# TODO: pv2 add Pydantic v2 version +@needs_pydanticv1 def test_read_items(client): response = client.get("/items/") assert response.status_code == 200, response.text @@ -87,7 +102,9 @@ def test_read_items(client): assert "description" in first_item -def test_openapi_schema(client): +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { @@ -315,7 +332,16 @@ def test_openapi_schema(client): "type": "object", "properties": { "title": {"title": "Title", "type": "string"}, - "description": {"title": "Description", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), }, }, "Item": { @@ -324,7 +350,16 @@ def test_openapi_schema(client): "type": "object", "properties": { "title": {"title": "Title", "type": "string"}, - "description": {"title": "Description", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"}, + ), "id": {"title": "Id", "type": "integer"}, "owner_id": {"title": "Owner Id", "type": "integer"}, }, diff --git a/tests/test_tutorial/test_sql_databases/test_sql_databases_middleware_py310.py b/tests/test_tutorial/test_sql_databases/test_sql_databases_middleware_py310.py index 493fb3b6b..d54cc6552 100644 --- a/tests/test_tutorial/test_sql_databases/test_sql_databases_middleware_py310.py +++ b/tests/test_tutorial/test_sql_databases/test_sql_databases_middleware_py310.py @@ -3,9 +3,10 @@ import os from pathlib import Path import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient -from ...utils import needs_py310 +from ...utils import needs_py310, needs_pydanticv1 @pytest.fixture(scope="module") @@ -30,6 +31,8 @@ def client(tmp_path_factory: pytest.TempPathFactory): @needs_py310 +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_create_user(client): test_user = {"email": "johndoe@example.com", "password": "secret"} response = client.post("/users/", json=test_user) @@ -42,6 +45,8 @@ def test_create_user(client): @needs_py310 +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_get_user(client): response = client.get("/users/1") assert response.status_code == 200, response.text @@ -51,12 +56,16 @@ def test_get_user(client): @needs_py310 +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_inexistent_user(client): response = client.get("/users/999") assert response.status_code == 404, response.text @needs_py310 +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_get_users(client): response = client.get("/users/") assert response.status_code == 200, response.text @@ -66,6 +75,8 @@ def test_get_users(client): @needs_py310 +# TODO: pv2 add Pydantic v2 version +@needs_pydanticv1 def test_create_item(client): item = {"title": "Foo", "description": "Something that fights"} response = client.post("/users/1/items/", json=item) @@ -90,6 +101,8 @@ def test_create_item(client): @needs_py310 +# TODO: pv2 add Pydantic v2 version +@needs_pydanticv1 def test_read_items(client): response = client.get("/items/") assert response.status_code == 200, response.text @@ -101,7 +114,9 @@ def test_read_items(client): @needs_py310 -def test_openapi_schema(client): +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { @@ -329,7 +344,16 @@ def test_openapi_schema(client): "type": "object", "properties": { "title": {"title": "Title", "type": "string"}, - "description": {"title": "Description", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), }, }, "Item": { @@ -338,7 +362,16 @@ def test_openapi_schema(client): "type": "object", "properties": { "title": {"title": "Title", "type": "string"}, - "description": {"title": "Description", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"}, + ), "id": {"title": "Id", "type": "integer"}, "owner_id": {"title": "Owner Id", "type": "integer"}, }, diff --git a/tests/test_tutorial/test_sql_databases/test_sql_databases_middleware_py39.py b/tests/test_tutorial/test_sql_databases/test_sql_databases_middleware_py39.py index 7b56685bc..4e43995e6 100644 --- a/tests/test_tutorial/test_sql_databases/test_sql_databases_middleware_py39.py +++ b/tests/test_tutorial/test_sql_databases/test_sql_databases_middleware_py39.py @@ -3,9 +3,10 @@ import os from pathlib import Path import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient -from ...utils import needs_py39 +from ...utils import needs_py39, needs_pydanticv1 @pytest.fixture(scope="module") @@ -30,6 +31,8 @@ def client(tmp_path_factory: pytest.TempPathFactory): @needs_py39 +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_create_user(client): test_user = {"email": "johndoe@example.com", "password": "secret"} response = client.post("/users/", json=test_user) @@ -42,6 +45,8 @@ def test_create_user(client): @needs_py39 +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_get_user(client): response = client.get("/users/1") assert response.status_code == 200, response.text @@ -51,12 +56,16 @@ def test_get_user(client): @needs_py39 +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_inexistent_user(client): response = client.get("/users/999") assert response.status_code == 404, response.text @needs_py39 +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_get_users(client): response = client.get("/users/") assert response.status_code == 200, response.text @@ -66,6 +75,8 @@ def test_get_users(client): @needs_py39 +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_create_item(client): item = {"title": "Foo", "description": "Something that fights"} response = client.post("/users/1/items/", json=item) @@ -90,6 +101,8 @@ def test_create_item(client): @needs_py39 +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_read_items(client): response = client.get("/items/") assert response.status_code == 200, response.text @@ -101,7 +114,9 @@ def test_read_items(client): @needs_py39 -def test_openapi_schema(client): +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { @@ -329,7 +344,16 @@ def test_openapi_schema(client): "type": "object", "properties": { "title": {"title": "Title", "type": "string"}, - "description": {"title": "Description", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), }, }, "Item": { @@ -338,7 +362,16 @@ def test_openapi_schema(client): "type": "object", "properties": { "title": {"title": "Title", "type": "string"}, - "description": {"title": "Description", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"}, + ), "id": {"title": "Id", "type": "integer"}, "owner_id": {"title": "Owner Id", "type": "integer"}, }, diff --git a/tests/test_tutorial/test_sql_databases/test_sql_databases_py310.py b/tests/test_tutorial/test_sql_databases/test_sql_databases_py310.py index 43c2b272f..b89b8b031 100644 --- a/tests/test_tutorial/test_sql_databases/test_sql_databases_py310.py +++ b/tests/test_tutorial/test_sql_databases/test_sql_databases_py310.py @@ -3,9 +3,10 @@ import os from pathlib import Path import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient -from ...utils import needs_py310 +from ...utils import needs_py310, needs_pydanticv1 @pytest.fixture(scope="module", name="client") @@ -29,6 +30,8 @@ def get_client(tmp_path_factory: pytest.TempPathFactory): @needs_py310 +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_create_user(client): test_user = {"email": "johndoe@example.com", "password": "secret"} response = client.post("/users/", json=test_user) @@ -41,6 +44,8 @@ def test_create_user(client): @needs_py310 +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_get_user(client): response = client.get("/users/1") assert response.status_code == 200, response.text @@ -50,12 +55,16 @@ def test_get_user(client): @needs_py310 +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_inexistent_user(client): response = client.get("/users/999") assert response.status_code == 404, response.text @needs_py310 +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_get_users(client): response = client.get("/users/") assert response.status_code == 200, response.text @@ -65,6 +74,8 @@ def test_get_users(client): @needs_py310 +# TODO: pv2 add Pydantic v2 version +@needs_pydanticv1 def test_create_item(client): item = {"title": "Foo", "description": "Something that fights"} response = client.post("/users/1/items/", json=item) @@ -89,6 +100,8 @@ def test_create_item(client): @needs_py310 +# TODO: pv2 add Pydantic v2 version +@needs_pydanticv1 def test_read_items(client): response = client.get("/items/") assert response.status_code == 200, response.text @@ -100,7 +113,9 @@ def test_read_items(client): @needs_py310 -def test_openapi_schema(client): +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { @@ -328,7 +343,16 @@ def test_openapi_schema(client): "type": "object", "properties": { "title": {"title": "Title", "type": "string"}, - "description": {"title": "Description", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), }, }, "Item": { @@ -337,7 +361,16 @@ def test_openapi_schema(client): "type": "object", "properties": { "title": {"title": "Title", "type": "string"}, - "description": {"title": "Description", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"}, + ), "id": {"title": "Id", "type": "integer"}, "owner_id": {"title": "Owner Id", "type": "integer"}, }, diff --git a/tests/test_tutorial/test_sql_databases/test_sql_databases_py39.py b/tests/test_tutorial/test_sql_databases/test_sql_databases_py39.py index fd33517db..13351bc81 100644 --- a/tests/test_tutorial/test_sql_databases/test_sql_databases_py39.py +++ b/tests/test_tutorial/test_sql_databases/test_sql_databases_py39.py @@ -3,9 +3,10 @@ import os from pathlib import Path import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient -from ...utils import needs_py39 +from ...utils import needs_py39, needs_pydanticv1 @pytest.fixture(scope="module", name="client") @@ -29,6 +30,8 @@ def get_client(tmp_path_factory: pytest.TempPathFactory): @needs_py39 +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_create_user(client): test_user = {"email": "johndoe@example.com", "password": "secret"} response = client.post("/users/", json=test_user) @@ -41,6 +44,8 @@ def test_create_user(client): @needs_py39 +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_get_user(client): response = client.get("/users/1") assert response.status_code == 200, response.text @@ -50,12 +55,16 @@ def test_get_user(client): @needs_py39 +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_inexistent_user(client): response = client.get("/users/999") assert response.status_code == 404, response.text @needs_py39 +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_get_users(client): response = client.get("/users/") assert response.status_code == 200, response.text @@ -65,6 +74,8 @@ def test_get_users(client): @needs_py39 +# TODO: pv2 add Pydantic v2 version +@needs_pydanticv1 def test_create_item(client): item = {"title": "Foo", "description": "Something that fights"} response = client.post("/users/1/items/", json=item) @@ -89,6 +100,8 @@ def test_create_item(client): @needs_py39 +# TODO: pv2 add Pydantic v2 version +@needs_pydanticv1 def test_read_items(client): response = client.get("/items/") assert response.status_code == 200, response.text @@ -100,7 +113,9 @@ def test_read_items(client): @needs_py39 -def test_openapi_schema(client): +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { @@ -328,7 +343,16 @@ def test_openapi_schema(client): "type": "object", "properties": { "title": {"title": "Title", "type": "string"}, - "description": {"title": "Description", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), }, }, "Item": { @@ -337,7 +361,16 @@ def test_openapi_schema(client): "type": "object", "properties": { "title": {"title": "Title", "type": "string"}, - "description": {"title": "Description", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"}, + ), "id": {"title": "Id", "type": "integer"}, "owner_id": {"title": "Owner Id", "type": "integer"}, }, diff --git a/tests/test_tutorial/test_sql_databases/test_testing_databases.py b/tests/test_tutorial/test_sql_databases/test_testing_databases.py index 6f667dea0..ce6ce230c 100644 --- a/tests/test_tutorial/test_sql_databases/test_testing_databases.py +++ b/tests/test_tutorial/test_sql_databases/test_testing_databases.py @@ -4,7 +4,11 @@ from pathlib import Path import pytest +from ...utils import needs_pydanticv1 + +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_testing_dbs(tmp_path_factory: pytest.TempPathFactory): tmp_path = tmp_path_factory.mktemp("data") cwd = os.getcwd() diff --git a/tests/test_tutorial/test_sql_databases/test_testing_databases_py310.py b/tests/test_tutorial/test_sql_databases/test_testing_databases_py310.py index 9e6b3f3e2..545d63c2a 100644 --- a/tests/test_tutorial/test_sql_databases/test_testing_databases_py310.py +++ b/tests/test_tutorial/test_sql_databases/test_testing_databases_py310.py @@ -4,10 +4,12 @@ from pathlib import Path import pytest -from ...utils import needs_py310 +from ...utils import needs_py310, needs_pydanticv1 @needs_py310 +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_testing_dbs_py39(tmp_path_factory: pytest.TempPathFactory): tmp_path = tmp_path_factory.mktemp("data") cwd = os.getcwd() diff --git a/tests/test_tutorial/test_sql_databases/test_testing_databases_py39.py b/tests/test_tutorial/test_sql_databases/test_testing_databases_py39.py index 0b27adf44..99bfd3fa8 100644 --- a/tests/test_tutorial/test_sql_databases/test_testing_databases_py39.py +++ b/tests/test_tutorial/test_sql_databases/test_testing_databases_py39.py @@ -4,10 +4,12 @@ from pathlib import Path import pytest -from ...utils import needs_py39 +from ...utils import needs_py39, needs_pydanticv1 @needs_py39 +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_testing_dbs_py39(tmp_path_factory: pytest.TempPathFactory): tmp_path = tmp_path_factory.mktemp("data") cwd = os.getcwd() diff --git a/tests/test_tutorial/test_sql_databases_peewee/test_sql_databases_peewee.py b/tests/test_tutorial/test_sql_databases_peewee/test_sql_databases_peewee.py index ac6c427ca..4350567d1 100644 --- a/tests/test_tutorial/test_sql_databases_peewee/test_sql_databases_peewee.py +++ b/tests/test_tutorial/test_sql_databases_peewee/test_sql_databases_peewee.py @@ -5,6 +5,8 @@ from unittest.mock import MagicMock import pytest from fastapi.testclient import TestClient +from ...utils import needs_pydanticv1 + @pytest.fixture(scope="module") def client(): @@ -17,6 +19,7 @@ def client(): test_db.unlink() +@needs_pydanticv1 def test_create_user(client): test_user = {"email": "johndoe@example.com", "password": "secret"} response = client.post("/users/", json=test_user) @@ -28,6 +31,7 @@ def test_create_user(client): assert response.status_code == 400, response.text +@needs_pydanticv1 def test_get_user(client): response = client.get("/users/1") assert response.status_code == 200, response.text @@ -36,11 +40,13 @@ def test_get_user(client): assert "id" in data +@needs_pydanticv1 def test_inexistent_user(client): response = client.get("/users/999") assert response.status_code == 404, response.text +@needs_pydanticv1 def test_get_users(client): response = client.get("/users/") assert response.status_code == 200, response.text @@ -52,6 +58,7 @@ def test_get_users(client): time.sleep = MagicMock() +@needs_pydanticv1 def test_get_slowusers(client): response = client.get("/slowusers/") assert response.status_code == 200, response.text @@ -60,6 +67,7 @@ def test_get_slowusers(client): assert "id" in data[0] +@needs_pydanticv1 def test_create_item(client): item = {"title": "Foo", "description": "Something that fights"} response = client.post("/users/1/items/", json=item) @@ -83,6 +91,7 @@ def test_create_item(client): assert item_to_check["description"] == item["description"] +@needs_pydanticv1 def test_read_items(client): response = client.get("/items/") assert response.status_code == 200, response.text @@ -93,6 +102,7 @@ def test_read_items(client): assert "description" in first_item +@needs_pydanticv1 def test_openapi_schema(client): response = client.get("/openapi.json") assert response.status_code == 200, response.text diff --git a/tests/test_union_body.py b/tests/test_union_body.py index 57a14b574..c15acacd1 100644 --- a/tests/test_union_body.py +++ b/tests/test_union_body.py @@ -1,5 +1,6 @@ from typing import Optional, Union +from dirty_equals import IsDict from fastapi import FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel @@ -90,7 +91,18 @@ def test_openapi_schema(): "Item": { "title": "Item", "type": "object", - "properties": {"name": {"title": "Name", "type": "string"}}, + "properties": IsDict( + { + "name": { + "title": "Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"name": {"title": "Name", "type": "string"}} + ), }, "ValidationError": { "title": "ValidationError", diff --git a/tests/test_union_inherited_body.py b/tests/test_union_inherited_body.py index c2a37d3dd..ef75d459e 100644 --- a/tests/test_union_inherited_body.py +++ b/tests/test_union_inherited_body.py @@ -1,5 +1,6 @@ from typing import Optional, Union +from dirty_equals import IsDict from fastapi import FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel @@ -84,14 +85,34 @@ def test_openapi_schema(): "Item": { "title": "Item", "type": "object", - "properties": {"name": {"title": "Name", "type": "string"}}, + "properties": { + "name": IsDict( + { + "title": "Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Name", "type": "string"} + ) + }, }, "ExtendedItem": { "title": "ExtendedItem", "required": ["age"], "type": "object", "properties": { - "name": {"title": "Name", "type": "string"}, + "name": IsDict( + { + "title": "Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Name", "type": "string"} + ), "age": {"title": "Age", "type": "integer"}, }, }, diff --git a/tests/test_validate_response.py b/tests/test_validate_response.py index 62f51c960..cd97007a4 100644 --- a/tests/test_validate_response.py +++ b/tests/test_validate_response.py @@ -2,8 +2,9 @@ from typing import List, Optional, Union import pytest from fastapi import FastAPI +from fastapi.exceptions import ResponseValidationError from fastapi.testclient import TestClient -from pydantic import BaseModel, ValidationError +from pydantic import BaseModel app = FastAPI() @@ -50,12 +51,12 @@ client = TestClient(app) def test_invalid(): - with pytest.raises(ValidationError): + with pytest.raises(ResponseValidationError): client.get("/items/invalid") def test_invalid_none(): - with pytest.raises(ValidationError): + with pytest.raises(ResponseValidationError): client.get("/items/invalidnone") @@ -74,10 +75,10 @@ def test_valid_none_none(): def test_double_invalid(): - with pytest.raises(ValidationError): + with pytest.raises(ResponseValidationError): client.get("/items/innerinvalid") def test_invalid_list(): - with pytest.raises(ValidationError): + with pytest.raises(ResponseValidationError): client.get("/items/invalidlist") diff --git a/tests/test_validate_response_dataclass.py b/tests/test_validate_response_dataclass.py index f2cfa7a11..0415988a0 100644 --- a/tests/test_validate_response_dataclass.py +++ b/tests/test_validate_response_dataclass.py @@ -2,8 +2,8 @@ from typing import List, Optional import pytest from fastapi import FastAPI +from fastapi.exceptions import ResponseValidationError from fastapi.testclient import TestClient -from pydantic import ValidationError from pydantic.dataclasses import dataclass app = FastAPI() @@ -39,15 +39,15 @@ client = TestClient(app) def test_invalid(): - with pytest.raises(ValidationError): + with pytest.raises(ResponseValidationError): client.get("/items/invalid") def test_double_invalid(): - with pytest.raises(ValidationError): + with pytest.raises(ResponseValidationError): client.get("/items/innerinvalid") def test_invalid_list(): - with pytest.raises(ValidationError): + with pytest.raises(ResponseValidationError): client.get("/items/invalidlist") diff --git a/tests/test_validate_response_recursive/__init__.py b/tests/test_validate_response_recursive/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_validate_response_recursive.py b/tests/test_validate_response_recursive/app_pv1.py similarity index 58% rename from tests/test_validate_response_recursive.py rename to tests/test_validate_response_recursive/app_pv1.py index 3a4b10e0c..4cfc4b3ee 100644 --- a/tests/test_validate_response_recursive.py +++ b/tests/test_validate_response_recursive/app_pv1.py @@ -1,7 +1,6 @@ from typing import List from fastapi import FastAPI -from fastapi.testclient import TestClient from pydantic import BaseModel app = FastAPI() @@ -49,32 +48,3 @@ def get_recursive_submodel(): } ], } - - -client = TestClient(app) - - -def test_recursive(): - response = client.get("/items/recursive") - assert response.status_code == 200, response.text - assert response.json() == { - "sub_items": [{"name": "subitem", "sub_items": []}], - "name": "item", - } - - response = client.get("/items/recursive-submodel") - assert response.status_code == 200, response.text - assert response.json() == { - "name": "item", - "sub_items1": [ - { - "name": "subitem", - "sub_items2": [ - { - "name": "subsubitem", - "sub_items1": [{"name": "subsubsubitem", "sub_items2": []}], - } - ], - } - ], - } diff --git a/tests/test_validate_response_recursive/app_pv2.py b/tests/test_validate_response_recursive/app_pv2.py new file mode 100644 index 000000000..8c93a8349 --- /dev/null +++ b/tests/test_validate_response_recursive/app_pv2.py @@ -0,0 +1,51 @@ +from typing import List + +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class RecursiveItem(BaseModel): + sub_items: List["RecursiveItem"] = [] + name: str + + +RecursiveItem.model_rebuild() + + +class RecursiveSubitemInSubmodel(BaseModel): + sub_items2: List["RecursiveItemViaSubmodel"] = [] + name: str + + +class RecursiveItemViaSubmodel(BaseModel): + sub_items1: List[RecursiveSubitemInSubmodel] = [] + name: str + + +RecursiveSubitemInSubmodel.model_rebuild() +RecursiveItemViaSubmodel.model_rebuild() + + +@app.get("/items/recursive", response_model=RecursiveItem) +def get_recursive(): + return {"name": "item", "sub_items": [{"name": "subitem", "sub_items": []}]} + + +@app.get("/items/recursive-submodel", response_model=RecursiveItemViaSubmodel) +def get_recursive_submodel(): + return { + "name": "item", + "sub_items1": [ + { + "name": "subitem", + "sub_items2": [ + { + "name": "subsubitem", + "sub_items1": [{"name": "subsubsubitem", "sub_items2": []}], + } + ], + } + ], + } diff --git a/tests/test_validate_response_recursive/test_validate_response_recursive_pv1.py b/tests/test_validate_response_recursive/test_validate_response_recursive_pv1.py new file mode 100644 index 000000000..de578ae03 --- /dev/null +++ b/tests/test_validate_response_recursive/test_validate_response_recursive_pv1.py @@ -0,0 +1,33 @@ +from fastapi.testclient import TestClient + +from ..utils import needs_pydanticv1 + + +@needs_pydanticv1 +def test_recursive(): + from .app_pv1 import app + + client = TestClient(app) + response = client.get("/items/recursive") + assert response.status_code == 200, response.text + assert response.json() == { + "sub_items": [{"name": "subitem", "sub_items": []}], + "name": "item", + } + + response = client.get("/items/recursive-submodel") + assert response.status_code == 200, response.text + assert response.json() == { + "name": "item", + "sub_items1": [ + { + "name": "subitem", + "sub_items2": [ + { + "name": "subsubitem", + "sub_items1": [{"name": "subsubsubitem", "sub_items2": []}], + } + ], + } + ], + } diff --git a/tests/test_validate_response_recursive/test_validate_response_recursive_pv2.py b/tests/test_validate_response_recursive/test_validate_response_recursive_pv2.py new file mode 100644 index 000000000..7d45e7fe4 --- /dev/null +++ b/tests/test_validate_response_recursive/test_validate_response_recursive_pv2.py @@ -0,0 +1,33 @@ +from fastapi.testclient import TestClient + +from ..utils import needs_pydanticv2 + + +@needs_pydanticv2 +def test_recursive(): + from .app_pv2 import app + + client = TestClient(app) + response = client.get("/items/recursive") + assert response.status_code == 200, response.text + assert response.json() == { + "sub_items": [{"name": "subitem", "sub_items": []}], + "name": "item", + } + + response = client.get("/items/recursive-submodel") + assert response.status_code == 200, response.text + assert response.json() == { + "name": "item", + "sub_items1": [ + { + "name": "subitem", + "sub_items2": [ + { + "name": "subsubitem", + "sub_items1": [{"name": "subsubsubitem", "sub_items2": []}], + } + ], + } + ], + } diff --git a/tests/utils.py b/tests/utils.py index 5305424c4..460c028f7 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,8 +1,11 @@ import sys import pytest +from fastapi._compat import PYDANTIC_V2 needs_py39 = pytest.mark.skipif(sys.version_info < (3, 9), reason="requires python3.9+") needs_py310 = pytest.mark.skipif( sys.version_info < (3, 10), reason="requires python3.10+" ) +needs_pydanticv2 = pytest.mark.skipif(not PYDANTIC_V2, reason="requires Pydantic v2") +needs_pydanticv1 = pytest.mark.skipif(PYDANTIC_V2, reason="requires Pydantic v1") From bb7e5b7261b7aebaf47fdc0ee9e9e8763d69fd23 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 7 Jul 2023 17:12:58 +0000 Subject: [PATCH 46/58] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index f4ce74404..57595e3d1 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✨ Add support for Pydantic v2. PR [#9816](https://github.com/tiangolo/fastapi/pull/9816) by [@tiangolo](https://github.com/tiangolo). ✨ Support for **Pydantic v2** ✨ Pydantic version 2 has the **core** re-written in **Rust** and includes a lot of improvements and features, for example: From 179e409159c4b3683cb133d2e4bd0179f8f624d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 7 Jul 2023 19:14:54 +0200 Subject: [PATCH 47/58] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 57595e3d1..946c38e10 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,7 +2,6 @@ ## Latest Changes -* ✨ Add support for Pydantic v2. PR [#9816](https://github.com/tiangolo/fastapi/pull/9816) by [@tiangolo](https://github.com/tiangolo). ✨ Support for **Pydantic v2** ✨ Pydantic version 2 has the **core** re-written in **Rust** and includes a lot of improvements and features, for example: @@ -77,6 +76,8 @@ There are **tests for both Pydantic v1 and v2**, and test **coverage** is kept a * Now Pydantic Settings is an additional optional package (included in `"fastapi[all]"`). To use settings you should now import `from pydantic_settings import BaseSettings` instead of importing from `pydantic` directly. * You can read more about it in the docs for [Settings and Environment Variables](https://fastapi.tiangolo.com/advanced/settings/). +* PR [#9816](https://github.com/tiangolo/fastapi/pull/9816) by [@tiangolo](https://github.com/tiangolo), included all the work done (in multiple PRs) on the beta branch (`main-pv2`). + ## 0.99.1 ### Fixes From f8356d9fffcc728062cc4dbfc905882969bd5123 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 7 Jul 2023 19:25:59 +0200 Subject: [PATCH 48/58] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.100.?= =?UTF-8?q?0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 3 +++ fastapi/__init__.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 946c38e10..3f5b076ff 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,9 @@ ## Latest Changes + +## 0.100.0 + ✨ Support for **Pydantic v2** ✨ Pydantic version 2 has the **core** re-written in **Rust** and includes a lot of improvements and features, for example: diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 5eb3c4de2..e9c3abe01 100644 --- a/fastapi/__init__.py +++ b/fastapi/__init__.py @@ -1,6 +1,6 @@ """FastAPI framework, high performance, easy to learn, fast to code, ready for production""" -__version__ = "0.100.0-beta3" +__version__ = "0.100.0" from starlette import status as status From 5f85e2cf58f4b145141718a32ce613e3fbee2862 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 7 Jul 2023 20:15:08 +0200 Subject: [PATCH 49/58] =?UTF-8?q?=F0=9F=93=9D=20Update=20links=20for=20sel?= =?UTF-8?q?f-hosted=20Swagger=20UI,=20point=20to=20v5,=20for=20OpenAPI=203?= =?UTF-8?q?1.0=20(#9834)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 📝 Update links for self-hosted Swagger UI, point to v5, for OpenAPI 3.1.0 --- docs/en/docs/advanced/extending-openapi.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/en/docs/advanced/extending-openapi.md b/docs/en/docs/advanced/extending-openapi.md index c47f939af..bec184dee 100644 --- a/docs/en/docs/advanced/extending-openapi.md +++ b/docs/en/docs/advanced/extending-openapi.md @@ -136,8 +136,8 @@ You can probably right-click each link and select an option similar to `Save lin **Swagger UI** uses the files: -* `swagger-ui-bundle.js` -* `swagger-ui.css` +* `swagger-ui-bundle.js` +* `swagger-ui.css` And **ReDoc** uses the file: From c165be380f87b7b079f98c5134cccef0c9d67e4d Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 7 Jul 2023 18:15:42 +0000 Subject: [PATCH 50/58] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 3f5b076ff..4e176d366 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Update links for self-hosted Swagger UI, point to v5, for OpenAPI 31.0. PR [#9834](https://github.com/tiangolo/fastapi/pull/9834) by [@tiangolo](https://github.com/tiangolo). ## 0.100.0 From 69df2fa1e591f21acad8ab22c8a4389d2c50175c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 9 Jul 2023 16:34:45 +0200 Subject: [PATCH 51/58] =?UTF-8?q?=F0=9F=91=B7=20Update=20token=20for=20lat?= =?UTF-8?q?est=20changes=20(#9842)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/latest-changes.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/latest-changes.yml b/.github/workflows/latest-changes.yml index f11a63848..1f7ac7b28 100644 --- a/.github/workflows/latest-changes.yml +++ b/.github/workflows/latest-changes.yml @@ -23,7 +23,7 @@ jobs: - uses: actions/checkout@v3 with: # To allow latest-changes to commit to master - token: ${{ secrets.ACTIONS_TOKEN }} + token: ${{ secrets.FASTAPI_LATEST_CHANGES }} # Allow debugging with tmate - name: Setup tmate session uses: mxschmitt/action-tmate@v3 @@ -32,7 +32,7 @@ jobs: limit-access-to-actor: true - uses: docker://tiangolo/latest-changes:0.0.3 with: - token: ${{ secrets.FASTAPI_LATEST_CHANGES }} + token: ${{ secrets.GITHUB_TOKEN }} latest_changes_file: docs/en/docs/release-notes.md latest_changes_header: '## Latest Changes\n\n' debug_logs: true From eaa14e18d3179ca4e60012aeec84bc67ee992352 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 9 Jul 2023 14:37:16 +0000 Subject: [PATCH 52/58] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 4e176d366..105a0b75a 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 👷 Update token for latest changes. PR [#9842](https://github.com/tiangolo/fastapi/pull/9842) by [@tiangolo](https://github.com/tiangolo). * 📝 Update links for self-hosted Swagger UI, point to v5, for OpenAPI 31.0. PR [#9834](https://github.com/tiangolo/fastapi/pull/9834) by [@tiangolo](https://github.com/tiangolo). ## 0.100.0 From 9213b72115870b25f86fedbdb7126b72831be077 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 9 Jul 2023 17:39:42 +0200 Subject: [PATCH 53/58] =?UTF-8?q?=F0=9F=91=B7=20Update=20MkDocs=20Material?= =?UTF-8?q?=20token=20(#9843)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build-docs.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index a155ecfec..19009447b 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -51,7 +51,7 @@ jobs: # Install MkDocs Material Insiders here just to put it in the cache for the rest of the steps - 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 + run: pip install git+https://${{ secrets.FASTAPI_MKDOCS_MATERIAL_INSIDERS }}@github.com/squidfunk/mkdocs-material-insiders.git - name: Export Language Codes id: show-langs run: | @@ -86,7 +86,7 @@ jobs: run: pip install -r requirements-docs.txt - 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 + run: pip install git+https://${{ secrets.FASTAPI_MKDOCS_MATERIAL_INSIDERS }}@github.com/squidfunk/mkdocs-material-insiders.git - name: Update Languages run: python ./scripts/docs.py update-languages - uses: actions/cache@v3 From ea92dcaa01dc85ea8a87ddde466727e26b731c01 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 9 Jul 2023 15:40:19 +0000 Subject: [PATCH 54/58] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 105a0b75a..33a95adc1 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 👷 Update MkDocs Material token. PR [#9843](https://github.com/tiangolo/fastapi/pull/9843) by [@tiangolo](https://github.com/tiangolo). * 👷 Update token for latest changes. PR [#9842](https://github.com/tiangolo/fastapi/pull/9842) by [@tiangolo](https://github.com/tiangolo). * 📝 Update links for self-hosted Swagger UI, point to v5, for OpenAPI 31.0. PR [#9834](https://github.com/tiangolo/fastapi/pull/9834) by [@tiangolo](https://github.com/tiangolo). From 73c39745d8c08158bb8d84343d1a8009d44eef09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 9 Jul 2023 17:44:21 +0200 Subject: [PATCH 55/58] =?UTF-8?q?=F0=9F=91=A5=20Update=20FastAPI=20People?= =?UTF-8?q?=20(#9775)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions --- docs/en/data/github_sponsors.yml | 81 +++++++++------ docs/en/data/people.yml | 166 +++++++++++++++++-------------- 2 files changed, 139 insertions(+), 108 deletions(-) diff --git a/docs/en/data/github_sponsors.yml b/docs/en/data/github_sponsors.yml index 71afb66b1..56a886c68 100644 --- a/docs/en/data/github_sponsors.yml +++ b/docs/en/data/github_sponsors.yml @@ -2,6 +2,9 @@ sponsors: - - login: cryptapi avatarUrl: https://avatars.githubusercontent.com/u/44925437?u=61369138589bc7fee6c417f3fbd50fbd38286cc4&v=4 url: https://github.com/cryptapi + - login: nanram22 + avatarUrl: https://avatars.githubusercontent.com/u/116367316?v=4 + url: https://github.com/nanram22 - - login: nihpo avatarUrl: https://avatars.githubusercontent.com/u/1841030?u=0264956d7580f7e46687a762a7baa629f84cf97c&v=4 url: https://github.com/nihpo @@ -41,24 +44,30 @@ sponsors: - login: marvin-robot avatarUrl: https://avatars.githubusercontent.com/u/41086007?u=091c5cb75af363123d66f58194805a97220ee1a7&v=4 url: https://github.com/marvin-robot + - login: Flint-company + avatarUrl: https://avatars.githubusercontent.com/u/48908872?u=355cd3d8992d4be8173058e7000728757c55ad49&v=4 + url: https://github.com/Flint-company - login: BoostryJP avatarUrl: https://avatars.githubusercontent.com/u/57932412?v=4 url: https://github.com/BoostryJP - - login: HiredScore avatarUrl: https://avatars.githubusercontent.com/u/3908850?v=4 url: https://github.com/HiredScore + - login: petebachant + avatarUrl: https://avatars.githubusercontent.com/u/4604869?u=b17a5a4ac82f77b7efff864d439e8068d2a36593&v=4 + url: https://github.com/petebachant - login: Trivie avatarUrl: https://avatars.githubusercontent.com/u/8161763?v=4 url: https://github.com/Trivie -- - login: JonasKs - avatarUrl: https://avatars.githubusercontent.com/u/5310116?u=98a049f3e1491bffb91e1feb7e93def6881a9389&v=4 - url: https://github.com/JonasKs - - login: moellenbeck avatarUrl: https://avatars.githubusercontent.com/u/169372?v=4 url: https://github.com/moellenbeck - login: birkjernstrom avatarUrl: https://avatars.githubusercontent.com/u/281715?u=4be14b43f76b4bd497b1941309bb390250b405e6&v=4 url: https://github.com/birkjernstrom + - login: yasyf + avatarUrl: https://avatars.githubusercontent.com/u/709645?u=f36736b3c6a85f578886ecc42a740e7b436e7a01&v=4 + url: https://github.com/yasyf - login: AccentDesign avatarUrl: https://avatars.githubusercontent.com/u/2429332?v=4 url: https://github.com/AccentDesign @@ -92,9 +101,9 @@ sponsors: - login: jefftriplett avatarUrl: https://avatars.githubusercontent.com/u/50527?u=af1ddfd50f6afd6d99f333ba2ac8d0a5b245ea74&v=4 url: https://github.com/jefftriplett - - login: medecau - avatarUrl: https://avatars.githubusercontent.com/u/59870?u=f9341c95adaba780828162fd4c7442357ecfcefa&v=4 - url: https://github.com/medecau + - login: jstanden + avatarUrl: https://avatars.githubusercontent.com/u/63288?u=c3658d57d2862c607a0e19c2101c3c51876e36ad&v=4 + url: https://github.com/jstanden - login: kamalgill avatarUrl: https://avatars.githubusercontent.com/u/133923?u=0df9181d97436ce330e9acf90ab8a54b7022efe7&v=4 url: https://github.com/kamalgill @@ -119,9 +128,6 @@ sponsors: - login: jqueguiner avatarUrl: https://avatars.githubusercontent.com/u/690878?u=bd65cc1f228ce6455e56dfaca3ef47c33bc7c3b0&v=4 url: https://github.com/jqueguiner - - login: iobruno - avatarUrl: https://avatars.githubusercontent.com/u/901651?u=460bc34ac298dca9870aafe3a1560a2ae789bc4a&v=4 - url: https://github.com/iobruno - login: tcsmith avatarUrl: https://avatars.githubusercontent.com/u/989034?u=7d8d741552b3279e8f4d3878679823a705a46f8f&v=4 url: https://github.com/tcsmith @@ -173,6 +179,9 @@ sponsors: - login: iwpnd avatarUrl: https://avatars.githubusercontent.com/u/6152183?u=c485eefca5c6329600cae63dd35e4f5682ce6924&v=4 url: https://github.com/iwpnd + - login: FernandoCelmer + avatarUrl: https://avatars.githubusercontent.com/u/6262214?u=ab6108a843a2fb9df0934f482375d2907609f3ff&v=4 + url: https://github.com/FernandoCelmer - login: simw avatarUrl: https://avatars.githubusercontent.com/u/6322526?v=4 url: https://github.com/simw @@ -191,6 +200,9 @@ sponsors: - login: wdwinslow avatarUrl: https://avatars.githubusercontent.com/u/11562137?u=dc01daafb354135603a263729e3d26d939c0c452&v=4 url: https://github.com/wdwinslow + - login: joeds13 + avatarUrl: https://avatars.githubusercontent.com/u/13631604?u=628eb122e08bef43767b3738752b883e8e7f6259&v=4 + url: https://github.com/joeds13 - login: dannywade avatarUrl: https://avatars.githubusercontent.com/u/13680237?u=418ee985bd41577b20fde81417fb2d901e875e8a&v=4 url: https://github.com/dannywade @@ -209,12 +221,6 @@ sponsors: - login: Filimoa avatarUrl: https://avatars.githubusercontent.com/u/21352040?u=0be845711495bbd7b756e13fcaeb8efc1ebd78ba&v=4 url: https://github.com/Filimoa - - login: shuheng-liu - avatarUrl: https://avatars.githubusercontent.com/u/22414322?u=813c45f30786c6b511b21a661def025d8f7b609e&v=4 - url: https://github.com/shuheng-liu - - login: SebTota - avatarUrl: https://avatars.githubusercontent.com/u/25122511?v=4 - url: https://github.com/SebTota - login: LarryGF avatarUrl: https://avatars.githubusercontent.com/u/26148349?u=431bb34d36d41c172466252242175281ae132152&v=4 url: https://github.com/LarryGF @@ -245,6 +251,9 @@ sponsors: - login: thenickben avatarUrl: https://avatars.githubusercontent.com/u/40610922?u=1e907d904041b7c91213951a3cb344cd37c14aaf&v=4 url: https://github.com/thenickben + - login: adtalos + avatarUrl: https://avatars.githubusercontent.com/u/40748353?v=4 + url: https://github.com/adtalos - login: ybressler avatarUrl: https://avatars.githubusercontent.com/u/40807730?u=41e2c00f1eebe3c402635f0325e41b4e6511462c&v=4 url: https://github.com/ybressler @@ -257,9 +266,6 @@ sponsors: - login: thisistheplace avatarUrl: https://avatars.githubusercontent.com/u/57633545?u=a3f3a7f8ace8511c6c067753f6eb6aee0db11ac6&v=4 url: https://github.com/thisistheplace - - login: A-Edge - avatarUrl: https://avatars.githubusercontent.com/u/59514131?v=4 - url: https://github.com/A-Edge - login: yakkonaut avatarUrl: https://avatars.githubusercontent.com/u/60633704?u=90a71fd631aa998ba4a96480788f017c9904e07b&v=4 url: https://github.com/yakkonaut @@ -275,6 +281,9 @@ sponsors: - login: DelfinaCare avatarUrl: https://avatars.githubusercontent.com/u/83734439?v=4 url: https://github.com/DelfinaCare + - login: khoadaniel + avatarUrl: https://avatars.githubusercontent.com/u/84840546?v=4 + url: https://github.com/khoadaniel - login: osawa-koki avatarUrl: https://avatars.githubusercontent.com/u/94336223?u=59c6fe6945bcbbaff87b2a794238671b060620d2&v=4 url: https://github.com/osawa-koki @@ -284,9 +293,9 @@ sponsors: - login: Dagmaara avatarUrl: https://avatars.githubusercontent.com/u/115501964?v=4 url: https://github.com/Dagmaara -- - login: Yarden-zamir - avatarUrl: https://avatars.githubusercontent.com/u/8178413?u=ee177a8b0f87ea56747f4d96f34cd4e9604a8217&v=4 - url: https://github.com/Yarden-zamir +- - login: SebTota + avatarUrl: https://avatars.githubusercontent.com/u/25122511?v=4 + url: https://github.com/SebTota - - login: pawamoy avatarUrl: https://avatars.githubusercontent.com/u/3999221?u=b030e4c89df2f3a36bc4710b925bdeb6745c9856&v=4 url: https://github.com/pawamoy @@ -323,9 +332,15 @@ sponsors: - login: WillHogan avatarUrl: https://avatars.githubusercontent.com/u/1661551?u=7036c064cf29781470573865264ec8e60b6b809f&v=4 url: https://github.com/WillHogan + - login: NateShoffner + avatarUrl: https://avatars.githubusercontent.com/u/1712163?u=b43cc2fa3fd8bec54b7706e4b98b72543c7bfea8&v=4 + url: https://github.com/NateShoffner - login: my3 avatarUrl: https://avatars.githubusercontent.com/u/1825270?v=4 url: https://github.com/my3 + - login: leobiscassi + avatarUrl: https://avatars.githubusercontent.com/u/1977418?u=f9f82445a847ab479bd7223debd677fcac6c49a0&v=4 + url: https://github.com/leobiscassi - login: cbonoz avatarUrl: https://avatars.githubusercontent.com/u/2351087?u=fd3e8030b2cc9fbfbb54a65e9890c548a016f58b&v=4 url: https://github.com/cbonoz @@ -338,9 +353,6 @@ sponsors: - login: anthonycorletti avatarUrl: https://avatars.githubusercontent.com/u/3477132?v=4 url: https://github.com/anthonycorletti - - login: jonathanhle - avatarUrl: https://avatars.githubusercontent.com/u/3851599?u=76b9c5d2fecd6c3a16e7645231878c4507380d4d&v=4 - url: https://github.com/jonathanhle - login: nikeee avatarUrl: https://avatars.githubusercontent.com/u/4068864?u=bbe73151f2b409c120160d032dc9aa6875ef0c4b&v=4 url: https://github.com/nikeee @@ -413,15 +425,18 @@ sponsors: - login: jangia avatarUrl: https://avatars.githubusercontent.com/u/17927101?u=9261b9bb0c3e3bb1ecba43e8915dc58d8c9a077e&v=4 url: https://github.com/jangia + - login: shuheng-liu + avatarUrl: https://avatars.githubusercontent.com/u/22414322?u=813c45f30786c6b511b21a661def025d8f7b609e&v=4 + url: https://github.com/shuheng-liu - login: ghandic avatarUrl: https://avatars.githubusercontent.com/u/23500353?u=e2e1d736f924d9be81e8bfc565b6d8836ba99773&v=4 url: https://github.com/ghandic - login: pers0n4 avatarUrl: https://avatars.githubusercontent.com/u/24864600?u=f211a13a7b572cbbd7779b9c8d8cb428cc7ba07e&v=4 url: https://github.com/pers0n4 - - login: kadekillary + - login: kxzk avatarUrl: https://avatars.githubusercontent.com/u/25046261?u=e185e58080090f9e678192cd214a14b14a2b232b&v=4 - url: https://github.com/kadekillary + url: https://github.com/kxzk - login: hoenie-ams avatarUrl: https://avatars.githubusercontent.com/u/25708487?u=cda07434f0509ac728d9edf5e681117c0f6b818b&v=4 url: https://github.com/hoenie-ams @@ -441,8 +456,11 @@ sponsors: avatarUrl: https://avatars.githubusercontent.com/u/33275230?u=eb223cad27017bb1e936ee9b429b450d092d0236&v=4 url: https://github.com/engineerjoe440 - login: bnkc - avatarUrl: https://avatars.githubusercontent.com/u/34930566?u=9fbf76b9bf7786275e2900efa51d1394bcf1f06a&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/34930566?u=527044d90b5ebb7f8dad517db5da1f45253b774b&v=4 url: https://github.com/bnkc + - login: devbruce + avatarUrl: https://avatars.githubusercontent.com/u/35563380?u=ca4e811ac7f7b3eb1600fa63285119fcdee01188&v=4 + url: https://github.com/devbruce - login: declon avatarUrl: https://avatars.githubusercontent.com/u/36180226?v=4 url: https://github.com/declon @@ -470,15 +488,15 @@ sponsors: - login: leo-jp-edwards avatarUrl: https://avatars.githubusercontent.com/u/58213433?u=2c128e8b0794b7a66211cd7d8ebe05db20b7e9c0&v=4 url: https://github.com/leo-jp-edwards - - login: tamtam-fitness - avatarUrl: https://avatars.githubusercontent.com/u/62091034?u=8da19a6bd3d02f5d6ba30c7247d5b46c98dd1403&v=4 - url: https://github.com/tamtam-fitness - login: 0417taehyun avatarUrl: https://avatars.githubusercontent.com/u/63915557?u=47debaa860fd52c9b98c97ef357ddcec3b3fb399&v=4 url: https://github.com/0417taehyun - - login: ssbarnea avatarUrl: https://avatars.githubusercontent.com/u/102495?u=b4bf6818deefe59952ac22fec6ed8c76de1b8f7c&v=4 url: https://github.com/ssbarnea + - login: tomast1337 + avatarUrl: https://avatars.githubusercontent.com/u/15125899?u=2c2f2907012d820499e2c43632389184923513fe&v=4 + url: https://github.com/tomast1337 - login: sadikkuzu avatarUrl: https://avatars.githubusercontent.com/u/23168063?u=d179c06bb9f65c4167fcab118526819f8e0dac17&v=4 url: https://github.com/sadikkuzu @@ -491,6 +509,3 @@ sponsors: - login: rwxd avatarUrl: https://avatars.githubusercontent.com/u/40308458?u=cd04a39e3655923be4f25c2ba8a5a07b3da3230a&v=4 url: https://github.com/rwxd - - login: xNykram - avatarUrl: https://avatars.githubusercontent.com/u/55030025?u=2c1ba313fd79d29273b5ff7c9c5cf4edfb271b29&v=4 - url: https://github.com/xNykram diff --git a/docs/en/data/people.yml b/docs/en/data/people.yml index 2da1c968b..dd2dbe5ea 100644 --- a/docs/en/data/people.yml +++ b/docs/en/data/people.yml @@ -1,17 +1,17 @@ maintainers: - login: tiangolo - answers: 1839 - prs: 398 + answers: 1844 + prs: 430 avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=740f11212a731f56798f558ceddb0bd07642afa7&v=4 url: https://github.com/tiangolo experts: - login: Kludex - count: 410 + count: 434 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex - login: dmontagu count: 237 - avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=58ed2a45798a4339700e2f62b2e12e6e54bf0396&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=540f30c937a6450812628b9592a1dfe91bbe148e&v=4 url: https://github.com/dmontagu - login: Mause count: 220 @@ -29,14 +29,14 @@ experts: count: 152 avatarUrl: https://avatars.githubusercontent.com/u/1104190?u=321a2e953e6645a7d09b732786c7a8061e0f8a8b&v=4 url: https://github.com/euri10 -- login: phy25 - count: 126 - avatarUrl: https://avatars.githubusercontent.com/u/331403?v=4 - url: https://github.com/phy25 - login: jgould22 - count: 124 + count: 139 avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 url: https://github.com/jgould22 +- login: phy25 + count: 126 + avatarUrl: https://avatars.githubusercontent.com/u/331403?u=191cd73f0c936497c8d1931a217bb3039d050265&v=4 + url: https://github.com/phy25 - login: iudeen count: 118 avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=2843b3303282bff8b212dcd4d9d6689452e4470c&v=4 @@ -61,34 +61,34 @@ experts: count: 49 avatarUrl: https://avatars.githubusercontent.com/u/516999?u=437c0c5038558c67e887ccd863c1ba0f846c03da&v=4 url: https://github.com/sm-Fifteen +- login: adriangb + count: 45 + avatarUrl: https://avatars.githubusercontent.com/u/1755071?u=612704256e38d6ac9cbed24f10e4b6ac2da74ecb&v=4 + url: https://github.com/adriangb - login: yinziyan1206 count: 45 avatarUrl: https://avatars.githubusercontent.com/u/37829370?u=da44ca53aefd5c23f346fab8e9fd2e108294c179&v=4 url: https://github.com/yinziyan1206 -- login: insomnes - count: 45 - avatarUrl: https://avatars.githubusercontent.com/u/16958893?u=f8be7088d5076d963984a21f95f44e559192d912&v=4 - url: https://github.com/insomnes - login: acidjunk count: 45 avatarUrl: https://avatars.githubusercontent.com/u/685002?u=b5094ab4527fc84b006c0ac9ff54367bdebb2267&v=4 url: https://github.com/acidjunk +- login: insomnes + count: 45 + avatarUrl: https://avatars.githubusercontent.com/u/16958893?u=f8be7088d5076d963984a21f95f44e559192d912&v=4 + url: https://github.com/insomnes - login: Dustyposa count: 45 avatarUrl: https://avatars.githubusercontent.com/u/27180793?u=5cf2877f50b3eb2bc55086089a78a36f07042889&v=4 url: https://github.com/Dustyposa -- login: adriangb +- login: odiseo0 count: 43 - avatarUrl: https://avatars.githubusercontent.com/u/1755071?u=1e2c2c9b39f5c9b780fb933d8995cf08ec235a47&v=4 - url: https://github.com/adriangb + avatarUrl: https://avatars.githubusercontent.com/u/87550035?u=2da05dab6cc8e1ade557801634760a56e4101796&v=4 + url: https://github.com/odiseo0 - login: frankie567 count: 43 avatarUrl: https://avatars.githubusercontent.com/u/1144727?u=85c025e3fcc7bd79a5665c63ee87cdf8aae13374&v=4 url: https://github.com/frankie567 -- login: odiseo0 - count: 42 - avatarUrl: https://avatars.githubusercontent.com/u/87550035?u=2da05dab6cc8e1ade557801634760a56e4101796&v=4 - url: https://github.com/odiseo0 - login: includeamin count: 40 avatarUrl: https://avatars.githubusercontent.com/u/11836741?u=8bd5ef7e62fe6a82055e33c4c0e0a7879ff8cfb6&v=4 @@ -97,14 +97,14 @@ experts: count: 37 avatarUrl: https://avatars.githubusercontent.com/u/5167622?u=de8f597c81d6336fcebc37b32dfd61a3f877160c&v=4 url: https://github.com/STeveShary -- login: krishnardt - count: 35 - avatarUrl: https://avatars.githubusercontent.com/u/31960541?u=47f4829c77f4962ab437ffb7995951e41eeebe9b&v=4 - url: https://github.com/krishnardt - login: chbndrhnns count: 35 avatarUrl: https://avatars.githubusercontent.com/u/7534547?v=4 url: https://github.com/chbndrhnns +- login: krishnardt + count: 35 + avatarUrl: https://avatars.githubusercontent.com/u/31960541?u=47f4829c77f4962ab437ffb7995951e41eeebe9b&v=4 + url: https://github.com/krishnardt - login: panla count: 32 avatarUrl: https://avatars.githubusercontent.com/u/41326348?u=ba2fda6b30110411ecbf406d187907e2b420ac19&v=4 @@ -133,30 +133,30 @@ experts: count: 21 avatarUrl: https://avatars.githubusercontent.com/u/51059348?u=f8f0d6d6e90fac39fa786228158ba7f013c74271&v=4 url: https://github.com/rafsaf -- login: nsidnev - count: 20 - avatarUrl: https://avatars.githubusercontent.com/u/22559461?u=a9cc3238217e21dc8796a1a500f01b722adb082c&v=4 - url: https://github.com/nsidnev - login: chris-allnutt count: 20 avatarUrl: https://avatars.githubusercontent.com/u/565544?v=4 url: https://github.com/chris-allnutt -- login: zoliknemet +- login: nsidnev + count: 20 + avatarUrl: https://avatars.githubusercontent.com/u/22559461?u=a9cc3238217e21dc8796a1a500f01b722adb082c&v=4 + url: https://github.com/nsidnev +- login: n8sty count: 18 - avatarUrl: https://avatars.githubusercontent.com/u/22326718?u=31ba446ac290e23e56eea8e4f0c558aaf0b40779&v=4 - url: https://github.com/zoliknemet + avatarUrl: https://avatars.githubusercontent.com/u/2964996?v=4 + url: https://github.com/n8sty - login: retnikt count: 18 avatarUrl: https://avatars.githubusercontent.com/u/24581770?v=4 url: https://github.com/retnikt +- login: zoliknemet + count: 18 + avatarUrl: https://avatars.githubusercontent.com/u/22326718?u=31ba446ac290e23e56eea8e4f0c558aaf0b40779&v=4 + url: https://github.com/zoliknemet - login: Hultner count: 17 avatarUrl: https://avatars.githubusercontent.com/u/2669034?u=115e53df959309898ad8dc9443fbb35fee71df07&v=4 url: https://github.com/Hultner -- login: n8sty - count: 17 - avatarUrl: https://avatars.githubusercontent.com/u/2964996?v=4 - url: https://github.com/n8sty - login: harunyasar count: 17 avatarUrl: https://avatars.githubusercontent.com/u/1765494?u=5b1ab7c582db4b4016fa31affe977d10af108ad4&v=4 @@ -177,6 +177,14 @@ experts: count: 16 avatarUrl: https://avatars.githubusercontent.com/u/41964673?u=9f2174f9d61c15c6e3a4c9e3aeee66f711ce311f&v=4 url: https://github.com/dstlny +- login: abhint + count: 15 + avatarUrl: https://avatars.githubusercontent.com/u/25699289?u=b5d219277b4d001ac26fb8be357fddd88c29d51b&v=4 + url: https://github.com/abhint +- login: nymous + count: 15 + avatarUrl: https://avatars.githubusercontent.com/u/4216559?u=360a36fb602cded27273cbfc0afc296eece90662&v=4 + url: https://github.com/nymous - login: ghost count: 15 avatarUrl: https://avatars.githubusercontent.com/u/10137?u=b1951d34a583cf12ec0d3b0781ba19be97726318&v=4 @@ -189,39 +197,39 @@ experts: count: 15 avatarUrl: https://avatars.githubusercontent.com/u/12537771?u=7444d20019198e34911082780cc7ad73f2b97cb3&v=4 url: https://github.com/jorgerpo -- login: ebottos94 - count: 14 - avatarUrl: https://avatars.githubusercontent.com/u/100039558?u=e2c672da5a7977fd24d87ce6ab35f8bf5b1ed9fa&v=4 - url: https://github.com/ebottos94 -- login: hellocoldworld - count: 14 - avatarUrl: https://avatars.githubusercontent.com/u/47581948?u=3d2186796434c507a6cb6de35189ab0ad27c356f&v=4 - url: https://github.com/hellocoldworld last_month_active: - login: jgould22 - count: 13 + count: 15 avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 url: https://github.com/jgould22 - login: Kludex - count: 7 + count: 13 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex - login: abhint count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/25699289?u=5b9f9f6192c83ca86a411eafd4be46d9e5828585&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/25699289?u=b5d219277b4d001ac26fb8be357fddd88c29d51b&v=4 url: https://github.com/abhint - login: chrisK824 count: 4 avatarUrl: https://avatars.githubusercontent.com/u/79946379?u=03d85b22d696a58a9603e55fbbbe2de6b0f4face&v=4 url: https://github.com/chrisK824 -- login: djimontyp - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/53098395?u=583bade70950b277c322d35f1be2b75c7b0f189c&v=4 - url: https://github.com/djimontyp -- login: JavierSanchezCastro +- login: arjwilliams + count: 3 + avatarUrl: https://avatars.githubusercontent.com/u/22227620?v=4 + url: https://github.com/arjwilliams +- login: wu-clan count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/72013291?u=ae5679e6bd971d9d98cd5e76e8683f83642ba950&v=4 - url: https://github.com/JavierSanchezCastro + avatarUrl: https://avatars.githubusercontent.com/u/52145145?u=f8c9e5c8c259d248e1683fedf5027b4ee08a0967&v=4 + url: https://github.com/wu-clan +- login: Ahmed-Abdou14 + count: 3 + avatarUrl: https://avatars.githubusercontent.com/u/104530599?u=d1e1c064d57c3ad5b6481716928da840f6d5a492&v=4 + url: https://github.com/Ahmed-Abdou14 +- login: esrefzeki + count: 3 + avatarUrl: https://avatars.githubusercontent.com/u/54935247?u=193cf5a169ca05fc54995a4dceabc82c7dc6e5ea&v=4 + url: https://github.com/esrefzeki top_contributors: - login: waynerv count: 25 @@ -232,7 +240,7 @@ top_contributors: avatarUrl: https://avatars.githubusercontent.com/u/41147016?u=55010621aece725aa702270b54fed829b6a1fe60&v=4 url: https://github.com/tokusumi - login: Kludex - count: 17 + count: 20 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex - login: jaystone776 @@ -241,20 +249,20 @@ top_contributors: url: https://github.com/jaystone776 - login: dmontagu count: 16 - avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=58ed2a45798a4339700e2f62b2e12e6e54bf0396&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=540f30c937a6450812628b9592a1dfe91bbe148e&v=4 url: https://github.com/dmontagu - login: euri10 count: 13 avatarUrl: https://avatars.githubusercontent.com/u/1104190?u=321a2e953e6645a7d09b732786c7a8061e0f8a8b&v=4 url: https://github.com/euri10 +- login: Xewus + count: 13 + avatarUrl: https://avatars.githubusercontent.com/u/85196001?u=f8e2dc7e5104f109cef944af79050ea8d1b8f914&v=4 + url: https://github.com/Xewus - login: mariacamilagl count: 12 avatarUrl: https://avatars.githubusercontent.com/u/11489395?u=4adb6986bf3debfc2b8216ae701f2bd47d73da7d&v=4 url: https://github.com/mariacamilagl -- login: Xewus - count: 12 - avatarUrl: https://avatars.githubusercontent.com/u/85196001?u=f8e2dc7e5104f109cef944af79050ea8d1b8f914&v=4 - url: https://github.com/Xewus - login: Smlep count: 10 avatarUrl: https://avatars.githubusercontent.com/u/16785985?v=4 @@ -275,6 +283,10 @@ top_contributors: count: 7 avatarUrl: https://avatars.githubusercontent.com/u/9651103?u=95db33927bbff1ed1c07efddeb97ac2ff33068ed&v=4 url: https://github.com/hard-coders +- login: Alexandrhub + count: 7 + avatarUrl: https://avatars.githubusercontent.com/u/119126536?u=9fc0d48f3307817bafecc5861eb2168401a6cb04&v=4 + url: https://github.com/Alexandrhub - login: batlopes count: 6 avatarUrl: https://avatars.githubusercontent.com/u/33462923?u=0fb3d7acb316764616f11e4947faf080e49ad8d9&v=4 @@ -331,17 +343,21 @@ top_contributors: count: 4 avatarUrl: https://avatars.githubusercontent.com/u/1334088?u=9667041f5b15dc002b6f9665fda8c0412933ac04&v=4 url: https://github.com/axel584 +- login: ivan-abc + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/36765187?u=c6e0ba571c1ccb6db9d94e62e4b8b5eda811a870&v=4 + url: https://github.com/ivan-abc top_reviewers: - login: Kludex - count: 117 + count: 122 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex - login: BilalAlpaslan - count: 75 + count: 79 avatarUrl: https://avatars.githubusercontent.com/u/47563997?u=63ed66e304fe8d765762c70587d61d9196e5c82d&v=4 url: https://github.com/BilalAlpaslan - login: yezz123 - count: 74 + count: 77 avatarUrl: https://avatars.githubusercontent.com/u/52716203?u=d7062cbc6eb7671d5dc9cc0e32a24ae335e0f225&v=4 url: https://github.com/yezz123 - login: tokusumi @@ -368,6 +384,10 @@ top_reviewers: count: 41 avatarUrl: https://avatars.githubusercontent.com/u/24587499?u=e772190a051ab0eaa9c8542fcff1892471638f2b&v=4 url: https://github.com/cikay +- login: Xewus + count: 35 + avatarUrl: https://avatars.githubusercontent.com/u/85196001?u=f8e2dc7e5104f109cef944af79050ea8d1b8f914&v=4 + url: https://github.com/Xewus - login: JarroVGIT count: 34 avatarUrl: https://avatars.githubusercontent.com/u/13659033?u=e8bea32d07a5ef72f7dde3b2079ceb714923ca05&v=4 @@ -376,10 +396,6 @@ top_reviewers: count: 33 avatarUrl: https://avatars.githubusercontent.com/u/1024932?u=b2ea249c6b41ddf98679c8d110d0f67d4a3ebf93&v=4 url: https://github.com/AdrianDeAnda -- login: Xewus - count: 32 - avatarUrl: https://avatars.githubusercontent.com/u/85196001?u=f8e2dc7e5104f109cef944af79050ea8d1b8f914&v=4 - url: https://github.com/Xewus - login: ArcLightSlavik count: 31 avatarUrl: https://avatars.githubusercontent.com/u/31127044?u=b0f2c37142f4b762e41ad65dc49581813422bd71&v=4 @@ -402,7 +418,7 @@ top_reviewers: url: https://github.com/Ryandaydev - login: dmontagu count: 23 - avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=58ed2a45798a4339700e2f62b2e12e6e54bf0396&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=540f30c937a6450812628b9592a1dfe91bbe148e&v=4 url: https://github.com/dmontagu - login: LorhanSohaky count: 23 @@ -452,6 +468,10 @@ top_reviewers: count: 15 avatarUrl: https://avatars.githubusercontent.com/u/63476957?u=6c86e59b48e0394d4db230f37fc9ad4d7e2c27c7&v=4 url: https://github.com/delhi09 +- login: Alexandrhub + count: 15 + avatarUrl: https://avatars.githubusercontent.com/u/119126536?u=9fc0d48f3307817bafecc5861eb2168401a6cb04&v=4 + url: https://github.com/Alexandrhub - login: sh0nk count: 13 avatarUrl: https://avatars.githubusercontent.com/u/6478810?u=af15d724875cec682ed8088a86d36b2798f981c0&v=4 @@ -472,6 +492,10 @@ top_reviewers: count: 12 avatarUrl: https://avatars.githubusercontent.com/u/1334088?u=9667041f5b15dc002b6f9665fda8c0412933ac04&v=4 url: https://github.com/axel584 +- login: ivan-abc + count: 12 + avatarUrl: https://avatars.githubusercontent.com/u/36765187?u=c6e0ba571c1ccb6db9d94e62e4b8b5eda811a870&v=4 + url: https://github.com/ivan-abc - login: solomein-sv count: 11 avatarUrl: https://avatars.githubusercontent.com/u/46193920?u=789927ee09cfabd752d3bd554fa6baf4850d2777&v=4 @@ -496,10 +520,6 @@ top_reviewers: count: 10 avatarUrl: https://avatars.githubusercontent.com/u/43503750?u=f440bc9062afb3c43b9b9c6cdfdcfe31d58699ef&v=4 url: https://github.com/ComicShrimp -- login: Alexandrhub - count: 10 - avatarUrl: https://avatars.githubusercontent.com/u/119126536?u=9fc0d48f3307817bafecc5861eb2168401a6cb04&v=4 - url: https://github.com/Alexandrhub - login: izaguerreiro count: 9 avatarUrl: https://avatars.githubusercontent.com/u/2241504?v=4 @@ -524,7 +544,3 @@ top_reviewers: count: 9 avatarUrl: https://avatars.githubusercontent.com/u/83456692?v=4 url: https://github.com/oandersonmagalhaes -- login: NinaHwang - count: 9 - avatarUrl: https://avatars.githubusercontent.com/u/79563565?u=eee6bfe9224c71193025ab7477f4f96ceaa05c62&v=4 - url: https://github.com/NinaHwang From fe91def5153683bffcf58ab9513e952d75d842be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 9 Jul 2023 17:44:40 +0200 Subject: [PATCH 56/58] =?UTF-8?q?=F0=9F=91=B7=20Update=20FastAPI=20People?= =?UTF-8?q?=20token=20(#9844)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/actions/people/action.yml | 5 +---- .github/actions/people/app/main.py | 3 +-- .github/workflows/people.yml | 3 +-- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/.github/actions/people/action.yml b/.github/actions/people/action.yml index 16bc8cdcb..71745b874 100644 --- a/.github/actions/people/action.yml +++ b/.github/actions/people/action.yml @@ -3,10 +3,7 @@ description: "Generate the data for the FastAPI People page" author: "Sebastián Ramírez " inputs: token: - description: 'User token, to read the GitHub API. Can be passed in using {{ secrets.ACTION_TOKEN }}' - required: true - standard_token: - description: 'Default GitHub Action token, used for the PR. Can be passed in using {{ secrets.GITHUB_TOKEN }}' + description: 'User token, to read the GitHub API. Can be passed in using {{ secrets.FASTAPI_PEOPLE }}' required: true runs: using: 'docker' diff --git a/.github/actions/people/app/main.py b/.github/actions/people/app/main.py index 2bf59f25e..b11e3456d 100644 --- a/.github/actions/people/app/main.py +++ b/.github/actions/people/app/main.py @@ -352,7 +352,6 @@ class SponsorsResponse(BaseModel): class Settings(BaseSettings): input_token: SecretStr - input_standard_token: SecretStr github_repository: str httpx_timeout: int = 30 @@ -609,7 +608,7 @@ if __name__ == "__main__": logging.basicConfig(level=logging.INFO) settings = Settings() logging.info(f"Using config: {settings.json()}") - g = Github(settings.input_standard_token.get_secret_value()) + g = Github(settings.input_token.get_secret_value()) repo = g.get_repo(settings.github_repository) question_commentors, question_last_month_commentors, question_authors = get_experts( settings=settings diff --git a/.github/workflows/people.yml b/.github/workflows/people.yml index 15ea464a1..dac526a6c 100644 --- a/.github/workflows/people.yml +++ b/.github/workflows/people.yml @@ -27,5 +27,4 @@ jobs: limit-access-to-actor: true - uses: ./.github/actions/people with: - token: ${{ secrets.ACTIONS_TOKEN }} - standard_token: ${{ secrets.FASTAPI_PEOPLE }} + token: ${{ secrets.FASTAPI_PEOPLE }} From 2d69531509fbcf875d804c3e1bed75326d86279c Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 9 Jul 2023 15:44:58 +0000 Subject: [PATCH 57/58] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 33a95adc1..836d056a7 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 👥 Update FastAPI People. PR [#9775](https://github.com/tiangolo/fastapi/pull/9775) by [@tiangolo](https://github.com/tiangolo). * 👷 Update MkDocs Material token. PR [#9843](https://github.com/tiangolo/fastapi/pull/9843) by [@tiangolo](https://github.com/tiangolo). * 👷 Update token for latest changes. PR [#9842](https://github.com/tiangolo/fastapi/pull/9842) by [@tiangolo](https://github.com/tiangolo). * 📝 Update links for self-hosted Swagger UI, point to v5, for OpenAPI 31.0. PR [#9834](https://github.com/tiangolo/fastapi/pull/9834) by [@tiangolo](https://github.com/tiangolo). From f7e3559bd5997f831fb9b02bef9c767a50facbc3 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 9 Jul 2023 15:45:55 +0000 Subject: [PATCH 58/58] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 836d056a7..72fa346d1 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 👷 Update FastAPI People token. PR [#9844](https://github.com/tiangolo/fastapi/pull/9844) by [@tiangolo](https://github.com/tiangolo). * 👥 Update FastAPI People. PR [#9775](https://github.com/tiangolo/fastapi/pull/9775) by [@tiangolo](https://github.com/tiangolo). * 👷 Update MkDocs Material token. PR [#9843](https://github.com/tiangolo/fastapi/pull/9843) by [@tiangolo](https://github.com/tiangolo). * 👷 Update token for latest changes. PR [#9842](https://github.com/tiangolo/fastapi/pull/9842) by [@tiangolo](https://github.com/tiangolo).