From eb79441a7f23c0df42f13547af67159d037f43c5 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 9 Dec 2021 10:40:14 +0000 Subject: [PATCH 01/59] =?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 ca7553855..053244434 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add portuguese translation for `docs/tutorial/query-params-str-validations.md`. PR [#3965](https://github.com/tiangolo/fastapi/pull/3965) by [@leandrodesouzadev](https://github.com/leandrodesouzadev). * 🌐 Add Korean translation for `docs/tutorial/response-status-code.md`. PR [#3742](https://github.com/tiangolo/fastapi/pull/3742) by [@NinaHwang](https://github.com/NinaHwang). * 🌐 Add Korean translation for Tutorial - JSON Compatible Encoder. PR [#3152](https://github.com/tiangolo/fastapi/pull/3152) by [@NEONKID](https://github.com/NEONKID). * 🌐 Add Korean translation for Tutorial - Path Parameters and Numeric Validations. PR [#2432](https://github.com/tiangolo/fastapi/pull/2432) by [@hard-coders](https://github.com/hard-coders). From aa4a69f790fb9a78ba7ed7767273f8734a5d028d Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 9 Dec 2021 10:45:00 +0000 Subject: [PATCH 02/59] =?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 053244434..b96e81c76 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Korean translation for `docs/tutorial/request-files.md`. PR [#3743](https://github.com/tiangolo/fastapi/pull/3743) by [@NinaHwang](https://github.com/NinaHwang). * 🌐 Add portuguese translation for `docs/tutorial/query-params-str-validations.md`. PR [#3965](https://github.com/tiangolo/fastapi/pull/3965) by [@leandrodesouzadev](https://github.com/leandrodesouzadev). * 🌐 Add Korean translation for `docs/tutorial/response-status-code.md`. PR [#3742](https://github.com/tiangolo/fastapi/pull/3742) by [@NinaHwang](https://github.com/NinaHwang). * 🌐 Add Korean translation for Tutorial - JSON Compatible Encoder. PR [#3152](https://github.com/tiangolo/fastapi/pull/3152) by [@NEONKID](https://github.com/NEONKID). From 062efcbb5d19efb075ed1b50cb3f01d813c04460 Mon Sep 17 00:00:00 2001 From: Nina Hwang <79563565+NinaHwang@users.noreply.github.com> Date: Thu, 9 Dec 2021 19:47:26 +0900 Subject: [PATCH 03/59] =?UTF-8?q?=F0=9F=8C=90=20Add=20Korean=20translation?= =?UTF-8?q?=20for=20`docs/tutorial/request-forms-and-files.md`=20(#3744)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: weekwith.me <63915557+0417taehyun@users.noreply.github.com> --- .../docs/tutorial/request-forms-and-files.md | 35 +++++++++++++++++++ docs/ko/mkdocs.yml | 1 + 2 files changed, 36 insertions(+) create mode 100644 docs/ko/docs/tutorial/request-forms-and-files.md diff --git a/docs/ko/docs/tutorial/request-forms-and-files.md b/docs/ko/docs/tutorial/request-forms-and-files.md new file mode 100644 index 000000000..6750c7b23 --- /dev/null +++ b/docs/ko/docs/tutorial/request-forms-and-files.md @@ -0,0 +1,35 @@ +# 폼 및 파일 요청 + +`File` 과 `Form` 을 사용하여 파일과 폼을 함께 정의할 수 있습니다. + +!!! info "정보" + 파일과 폼 데이터를 함께, 또는 각각 업로드하기 위해 먼저 `python-multipart`를 설치해야합니다. + + 예 ) `pip install python-multipart`. + +## `File` 및 `Form` 업로드 + +```Python hl_lines="1" +{!../../../docs_src/request_forms_and_files/tutorial001.py!} +``` + +## `File` 및 `Form` 매개변수 정의 + +`Body` 및 `Query`와 동일한 방식으로 파일과 폼의 매개변수를 생성합니다: + +```Python hl_lines="8" +{!../../../docs_src/request_forms_and_files/tutorial001.py!} +``` + +파일과 폼 필드는 폼 데이터 형식으로 업로드되어 파일과 폼 필드로 전달됩니다. + +어떤 파일들은 `bytes`로, 또 어떤 파일들은 `UploadFile`로 선언할 수 있습니다. + +!!! warning "주의" + 다수의 `File`과 `Form` 매개변수를 한 *경로 작동*에 선언하는 것이 가능하지만, 요청의 본문이 `application/json`가 아닌 `multipart/form-data`로 인코딩 되기 때문에 JSON으로 받아야하는 `Body` 필드를 함께 선언할 수는 없습니다. + + 이는 **FastAPI**의 한계가 아니라, HTTP 프로토콜에 의한 것입니다. + +## 요약 + +하나의 요청으로 데이터와 파일들을 받아야 할 경우 `File`과 `Form`을 함께 사용하기 바랍니다. diff --git a/docs/ko/mkdocs.yml b/docs/ko/mkdocs.yml index 124e7b1d3..7a75940a3 100644 --- a/docs/ko/mkdocs.yml +++ b/docs/ko/mkdocs.yml @@ -64,6 +64,7 @@ nav: - tutorial/path-params-numeric-validations.md - tutorial/response-status-code.md - tutorial/request-files.md + - tutorial/request-forms-and-files.md markdown_extensions: - toc: permalink: true From 0a87bc88b85445018d3ced9d07ac6a37f9e6d8f1 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 9 Dec 2021 10:48:10 +0000 Subject: [PATCH 04/59] =?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 b96e81c76..271aab5d1 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Korean translation for `docs/tutorial/request-forms-and-files.md`. PR [#3744](https://github.com/tiangolo/fastapi/pull/3744) by [@NinaHwang](https://github.com/NinaHwang). * 🌐 Add Korean translation for `docs/tutorial/request-files.md`. PR [#3743](https://github.com/tiangolo/fastapi/pull/3743) by [@NinaHwang](https://github.com/NinaHwang). * 🌐 Add portuguese translation for `docs/tutorial/query-params-str-validations.md`. PR [#3965](https://github.com/tiangolo/fastapi/pull/3965) by [@leandrodesouzadev](https://github.com/leandrodesouzadev). * 🌐 Add Korean translation for `docs/tutorial/response-status-code.md`. PR [#3742](https://github.com/tiangolo/fastapi/pull/3742) by [@NinaHwang](https://github.com/NinaHwang). From b0cd4d7e7ebdbab130d663d73d5474361116ce63 Mon Sep 17 00:00:00 2001 From: Eric Jolibois Date: Sun, 12 Dec 2021 12:28:35 +0100 Subject: [PATCH 05/59] =?UTF-8?q?=F0=9F=90=9B=20Fix=20JSON=20Schema=20for?= =?UTF-8?q?=20dataclasses,=20supporting=20the=20fixes=20in=20Pydantic=201.?= =?UTF-8?q?9=20(#4272)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- .../test_dataclasses/test_tutorial002.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/tests/test_tutorial/test_dataclasses/test_tutorial002.py b/tests/test_tutorial/test_dataclasses/test_tutorial002.py index 10d8d227d..34aeb0be5 100644 --- a/tests/test_tutorial/test_dataclasses/test_tutorial002.py +++ b/tests/test_tutorial/test_dataclasses/test_tutorial002.py @@ -1,3 +1,5 @@ +from copy import deepcopy + from fastapi.testclient import TestClient from docs_src.dataclasses.tutorial002 import app @@ -29,7 +31,7 @@ openapi_schema = { "schemas": { "Item": { "title": "Item", - "required": ["name", "price", "tags"], + "required": ["name", "price"], "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, @@ -51,7 +53,18 @@ openapi_schema = { def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200 - assert response.json() == openapi_schema + # TODO: remove this once Pydantic 1.9 is released + # Ref: https://github.com/samuelcolvin/pydantic/pull/2557 + data = response.json() + alternative_data1 = deepcopy(data) + alternative_data2 = deepcopy(data) + alternative_data1["components"]["schemas"]["Item"]["required"] = ["name", "price"] + alternative_data2["components"]["schemas"]["Item"]["required"] = [ + "name", + "price", + "tags", + ] + assert alternative_data1 == openapi_schema or alternative_data2 == openapi_schema def test_get_item(): From 6d642ef5fb60eb48d178de2846657cf4d90e882a Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 12 Dec 2021 11:29:36 +0000 Subject: [PATCH 06/59] =?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 271aab5d1..2a0d27360 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🐛 Fix JSON Schema for dataclasses, supporting the fixes in Pydantic 1.9. PR [#4272](https://github.com/tiangolo/fastapi/pull/4272) by [@PrettyWood](https://github.com/PrettyWood). * 🌐 Add Korean translation for `docs/tutorial/request-forms-and-files.md`. PR [#3744](https://github.com/tiangolo/fastapi/pull/3744) by [@NinaHwang](https://github.com/NinaHwang). * 🌐 Add Korean translation for `docs/tutorial/request-files.md`. PR [#3743](https://github.com/tiangolo/fastapi/pull/3743) by [@NinaHwang](https://github.com/NinaHwang). * 🌐 Add portuguese translation for `docs/tutorial/query-params-str-validations.md`. PR [#3965](https://github.com/tiangolo/fastapi/pull/3965) by [@leandrodesouzadev](https://github.com/leandrodesouzadev). From e7158bc59212ddc5ecd6a45a83e6d99f8c6bb3d4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 12 Dec 2021 12:34:18 +0100 Subject: [PATCH 07/59] =?UTF-8?q?=F0=9F=91=A5=20Update=20FastAPI=20People?= =?UTF-8?q?=20(#4274)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions --- docs/en/data/people.yml | 258 ++++++++++++++++++++-------------------- 1 file changed, 127 insertions(+), 131 deletions(-) diff --git a/docs/en/data/people.yml b/docs/en/data/people.yml index 8f1710d33..89c6f8dc3 100644 --- a/docs/en/data/people.yml +++ b/docs/en/data/people.yml @@ -1,12 +1,12 @@ maintainers: - login: tiangolo answers: 1230 - prs: 260 + prs: 269 avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=5cad72c846b7aba2e960546af490edc7375dafc4&v=4 url: https://github.com/tiangolo experts: - login: Kludex - count: 299 + count: 315 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=cf8455cb899806b774a3a71073f88583adec99f6&v=4 url: https://github.com/Kludex - login: dmontagu @@ -30,41 +30,45 @@ experts: avatarUrl: https://avatars.githubusercontent.com/u/331403?v=4 url: https://github.com/phy25 - login: ArcLightSlavik - count: 68 + count: 71 avatarUrl: https://avatars.githubusercontent.com/u/31127044?u=81a84af39c89b898b0fbc5a04e8834f60f23e55a&v=4 url: https://github.com/ArcLightSlavik +- login: raphaelauv + count: 63 + avatarUrl: https://avatars.githubusercontent.com/u/10202690?u=e6f86f5c0c3026a15d6b51792fa3e532b12f1371&v=4 + url: https://github.com/raphaelauv - login: falkben - count: 57 + count: 58 avatarUrl: https://avatars.githubusercontent.com/u/653031?u=0c8d8f33d87f1aa1a6488d3f02105e9abc838105&v=4 url: https://github.com/falkben - login: sm-Fifteen count: 48 avatarUrl: https://avatars.githubusercontent.com/u/516999?u=437c0c5038558c67e887ccd863c1ba0f846c03da&v=4 url: https://github.com/sm-Fifteen -- login: raphaelauv - count: 48 - avatarUrl: https://avatars.githubusercontent.com/u/10202690?u=e6f86f5c0c3026a15d6b51792fa3e532b12f1371&v=4 - url: https://github.com/raphaelauv +- login: Dustyposa + count: 42 + avatarUrl: https://avatars.githubusercontent.com/u/27180793?u=5cf2877f50b3eb2bc55086089a78a36f07042889&v=4 + url: https://github.com/Dustyposa - login: includeamin count: 38 avatarUrl: https://avatars.githubusercontent.com/u/11836741?u=8bd5ef7e62fe6a82055e33c4c0e0a7879ff8cfb6&v=4 url: https://github.com/includeamin -- login: Dustyposa - count: 38 - avatarUrl: https://avatars.githubusercontent.com/u/27180793?u=5cf2877f50b3eb2bc55086089a78a36f07042889&v=4 - url: https://github.com/Dustyposa +- login: insomnes + count: 37 + avatarUrl: https://avatars.githubusercontent.com/u/16958893?u=f8be7088d5076d963984a21f95f44e559192d912&v=4 + url: https://github.com/insomnes - login: prostomarkeloff count: 33 avatarUrl: https://avatars.githubusercontent.com/u/28061158?u=72309cc1f2e04e40fa38b29969cb4e9d3f722e7b&v=4 url: https://github.com/prostomarkeloff +- login: STeveShary + count: 32 + avatarUrl: https://avatars.githubusercontent.com/u/5167622?u=de8f597c81d6336fcebc37b32dfd61a3f877160c&v=4 + url: https://github.com/STeveShary - login: krishnardt count: 31 avatarUrl: https://avatars.githubusercontent.com/u/31960541?u=47f4829c77f4962ab437ffb7995951e41eeebe9b&v=4 url: https://github.com/krishnardt -- login: insomnes - count: 30 - avatarUrl: https://avatars.githubusercontent.com/u/16958893?u=f8be7088d5076d963984a21f95f44e559192d912&v=4 - url: https://github.com/insomnes - login: wshayes count: 29 avatarUrl: https://avatars.githubusercontent.com/u/365303?u=07ca03c5ee811eb0920e633cc3c3db73dbec1aa5&v=4 @@ -73,6 +77,14 @@ experts: count: 29 avatarUrl: https://avatars.githubusercontent.com/u/1144727?u=85c025e3fcc7bd79a5665c63ee87cdf8aae13374&v=4 url: https://github.com/frankie567 +- login: adriangb + count: 26 + avatarUrl: https://avatars.githubusercontent.com/u/1755071?u=81f0262df34e1460ca546fbd0c211169c2478532&v=4 + url: https://github.com/adriangb +- login: ghandic + count: 25 + avatarUrl: https://avatars.githubusercontent.com/u/23500353?u=e2e1d736f924d9be81e8bfc565b6d8836ba99773&v=4 + url: https://github.com/ghandic - login: dbanty count: 25 avatarUrl: https://avatars.githubusercontent.com/u/43723790?u=9bcce836bbce55835291c5b2ac93a4e311f4b3c3&v=4 @@ -85,13 +97,9 @@ experts: count: 24 avatarUrl: https://avatars.githubusercontent.com/u/9435877?u=719327b7d2c4c62212456d771bfa7c6b8dbb9eac&v=4 url: https://github.com/SirTelemak -- login: STeveShary - count: 24 - avatarUrl: https://avatars.githubusercontent.com/u/5167622?u=de8f597c81d6336fcebc37b32dfd61a3f877160c&v=4 - url: https://github.com/STeveShary - login: acnebs count: 22 - avatarUrl: https://avatars.githubusercontent.com/u/9054108?u=bfd127b3e6200f4d00afd714f0fc95c2512df19b&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/9054108?u=c27e50269f1ef8ea950cc6f0268c8ec5cebbe9c9&v=4 url: https://github.com/acnebs - login: nsidnev count: 22 @@ -101,22 +109,18 @@ experts: count: 21 avatarUrl: https://avatars.githubusercontent.com/u/565544?v=4 url: https://github.com/chris-allnutt +- login: panla + count: 21 + avatarUrl: https://avatars.githubusercontent.com/u/41326348?u=ba2fda6b30110411ecbf406d187907e2b420ac19&v=4 + url: https://github.com/panla - login: retnikt count: 19 avatarUrl: https://avatars.githubusercontent.com/u/24581770?v=4 url: https://github.com/retnikt -- login: ghandic - count: 18 - avatarUrl: https://avatars.githubusercontent.com/u/23500353?u=e2e1d736f924d9be81e8bfc565b6d8836ba99773&v=4 - url: https://github.com/ghandic - login: Hultner count: 18 avatarUrl: https://avatars.githubusercontent.com/u/2669034?u=115e53df959309898ad8dc9443fbb35fee71df07&v=4 url: https://github.com/Hultner -- login: panla - count: 18 - avatarUrl: https://avatars.githubusercontent.com/u/41326348?u=ba2fda6b30110411ecbf406d187907e2b420ac19&v=4 - url: https://github.com/panla - login: jorgerpo count: 17 avatarUrl: https://avatars.githubusercontent.com/u/12537771?u=7444d20019198e34911082780cc7ad73f2b97cb3&v=4 @@ -125,26 +129,30 @@ experts: count: 17 avatarUrl: https://avatars.githubusercontent.com/u/28262306?u=66ee21316275ef356081c2efc4ed7a4572e690dc&v=4 url: https://github.com/nkhitrov -- login: adriangb - count: 17 - avatarUrl: https://avatars.githubusercontent.com/u/1755071?u=81f0262df34e1460ca546fbd0c211169c2478532&v=4 - url: https://github.com/adriangb - login: waynerv count: 16 avatarUrl: https://avatars.githubusercontent.com/u/39515546?u=ec35139777597cdbbbddda29bf8b9d4396b429a9&v=4 url: https://github.com/waynerv +- login: acidjunk + count: 15 + avatarUrl: https://avatars.githubusercontent.com/u/685002?u=b5094ab4527fc84b006c0ac9ff54367bdebb2267&v=4 + url: https://github.com/acidjunk - login: haizaar count: 13 avatarUrl: https://avatars.githubusercontent.com/u/58201?u=4f1f9843d69433ca0d380d95146cfe119e5fdac4&v=4 url: https://github.com/haizaar -- login: acidjunk +- login: dstlny count: 13 - avatarUrl: https://avatars.githubusercontent.com/u/685002?u=b5094ab4527fc84b006c0ac9ff54367bdebb2267&v=4 - url: https://github.com/acidjunk + avatarUrl: https://avatars.githubusercontent.com/u/41964673?u=9f2174f9d61c15c6e3a4c9e3aeee66f711ce311f&v=4 + url: https://github.com/dstlny - login: David-Lor count: 12 avatarUrl: https://avatars.githubusercontent.com/u/17401854?u=474680c02b94cba810cb9032fb7eb787d9cc9d22&v=4 url: https://github.com/David-Lor +- login: lowercase00 + count: 11 + avatarUrl: https://avatars.githubusercontent.com/u/21188280?v=4 + url: https://github.com/lowercase00 - login: zamiramir count: 11 avatarUrl: https://avatars.githubusercontent.com/u/40475662?u=e58ef61034e8d0d6a312cc956fb09b9c3332b449&v=4 @@ -153,10 +161,6 @@ experts: count: 11 avatarUrl: https://avatars.githubusercontent.com/u/8134632?v=4 url: https://github.com/juntatalor -- login: dstlny - count: 11 - avatarUrl: https://avatars.githubusercontent.com/u/41964673?u=9f2174f9d61c15c6e3a4c9e3aeee66f711ce311f&v=4 - url: https://github.com/dstlny - login: valentin994 count: 11 avatarUrl: https://avatars.githubusercontent.com/u/42819267?u=fdeeaa9242a59b243f8603496b00994f6951d5a2&v=4 @@ -173,43 +177,27 @@ experts: count: 10 avatarUrl: https://avatars.githubusercontent.com/u/47581948?v=4 url: https://github.com/hellocoldworld +- login: oligond + count: 10 + avatarUrl: https://avatars.githubusercontent.com/u/2858306?u=1bb1182a5944e93624b7fb26585f22c8f7a9d76e&v=4 + url: https://github.com/oligond last_month_active: +- login: raphaelauv + count: 6 + avatarUrl: https://avatars.githubusercontent.com/u/10202690?u=e6f86f5c0c3026a15d6b51792fa3e532b12f1371&v=4 + url: https://github.com/raphaelauv +- login: oligond + count: 5 + avatarUrl: https://avatars.githubusercontent.com/u/2858306?u=1bb1182a5944e93624b7fb26585f22c8f7a9d76e&v=4 + url: https://github.com/oligond - login: Kludex - count: 13 + count: 5 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=cf8455cb899806b774a3a71073f88583adec99f6&v=4 url: https://github.com/Kludex -- login: ghandic - count: 12 - avatarUrl: https://avatars.githubusercontent.com/u/23500353?u=e2e1d736f924d9be81e8bfc565b6d8836ba99773&v=4 - url: https://github.com/ghandic -- login: STeveShary - count: 8 - avatarUrl: https://avatars.githubusercontent.com/u/5167622?u=de8f597c81d6336fcebc37b32dfd61a3f877160c&v=4 - url: https://github.com/STeveShary -- login: Honda-a +- login: Dustyposa count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/22759187?u=f45bd5fb17b4dca331529b8e9e5eab6122b84b8b&v=4 - url: https://github.com/Honda-a -- login: frankie567 - count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/1144727?u=85c025e3fcc7bd79a5665c63ee87cdf8aae13374&v=4 - url: https://github.com/frankie567 -- login: panla - count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/41326348?u=ba2fda6b30110411ecbf406d187907e2b420ac19&v=4 - url: https://github.com/panla -- login: dstlny - count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/41964673?u=9f2174f9d61c15c6e3a4c9e3aeee66f711ce311f&v=4 - url: https://github.com/dstlny -- login: trevorwang - count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/121966?v=4 - url: https://github.com/trevorwang -- login: klaa97 - count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/39653693?v=4 - url: https://github.com/klaa97 + avatarUrl: https://avatars.githubusercontent.com/u/27180793?u=5cf2877f50b3eb2bc55086089a78a36f07042889&v=4 + url: https://github.com/Dustyposa top_contributors: - login: waynerv count: 25 @@ -227,14 +215,18 @@ top_contributors: count: 13 avatarUrl: https://avatars.githubusercontent.com/u/1104190?u=321a2e953e6645a7d09b732786c7a8061e0f8a8b&v=4 url: https://github.com/euri10 +- login: jaystone776 + count: 13 + avatarUrl: https://avatars.githubusercontent.com/u/11191137?u=299205a95e9b6817a43144a48b643346a5aac5cc&v=4 + url: https://github.com/jaystone776 - login: mariacamilagl count: 12 avatarUrl: https://avatars.githubusercontent.com/u/11489395?u=4adb6986bf3debfc2b8216ae701f2bd47d73da7d&v=4 url: https://github.com/mariacamilagl -- login: jaystone776 - count: 11 - avatarUrl: https://avatars.githubusercontent.com/u/11191137?u=299205a95e9b6817a43144a48b643346a5aac5cc&v=4 - url: https://github.com/jaystone776 +- login: Smlep + count: 9 + avatarUrl: https://avatars.githubusercontent.com/u/16785985?v=4 + url: https://github.com/Smlep - login: Serrones count: 8 avatarUrl: https://avatars.githubusercontent.com/u/22691749?u=4795b880e13ca33a73e52fc0ef7dc9c60c8fce47&v=4 @@ -248,8 +240,8 @@ top_contributors: avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=cf8455cb899806b774a3a71073f88583adec99f6&v=4 url: https://github.com/Kludex - login: hard-coders - count: 6 - avatarUrl: https://avatars.githubusercontent.com/u/9651103?u=f2d3d2038c55d86d7f9348f4e6c5e30191e4ee8b&v=4 + count: 7 + avatarUrl: https://avatars.githubusercontent.com/u/9651103?u=95db33927bbff1ed1c07efddeb97ac2ff33068ed&v=4 url: https://github.com/hard-coders - login: wshayes count: 5 @@ -263,10 +255,6 @@ top_contributors: count: 5 avatarUrl: https://avatars.githubusercontent.com/u/1175560?v=4 url: https://github.com/Attsun1031 -- login: Smlep - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/16785985?v=4 - url: https://github.com/Smlep - login: jekirl count: 4 avatarUrl: https://avatars.githubusercontent.com/u/2546697?u=a027452387d85bd4a14834e19d716c99255fb3b7&v=4 @@ -275,13 +263,21 @@ top_contributors: count: 4 avatarUrl: https://avatars.githubusercontent.com/u/805749?v=4 url: https://github.com/jfunez +- login: ycd + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/62724709?u=826f228edf0bab0d19ad1d5c4ba4df1047ccffef&v=4 + url: https://github.com/ycd - login: komtaki count: 4 avatarUrl: https://avatars.githubusercontent.com/u/39375566?u=260ad6b1a4b34c07dbfa728da5e586f16f6d1824&v=4 url: https://github.com/komtaki +- login: NinaHwang + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/79563565?u=36ca24be1fc3daa967b0f409bab0e17132d8c80c&v=4 + url: https://github.com/NinaHwang top_reviewers: - login: Kludex - count: 90 + count: 91 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=cf8455cb899806b774a3a71073f88583adec99f6&v=4 url: https://github.com/Kludex - login: waynerv @@ -297,29 +293,37 @@ top_reviewers: avatarUrl: https://avatars.githubusercontent.com/u/41147016?u=55010621aece725aa702270b54fed829b6a1fe60&v=4 url: https://github.com/tokusumi - login: ycd - count: 43 + count: 44 avatarUrl: https://avatars.githubusercontent.com/u/62724709?u=826f228edf0bab0d19ad1d5c4ba4df1047ccffef&v=4 url: https://github.com/ycd - login: AdrianDeAnda - count: 32 + count: 33 avatarUrl: https://avatars.githubusercontent.com/u/1024932?u=bb7f8a0d6c9de4e9d0320a9f271210206e202250&v=4 url: https://github.com/AdrianDeAnda - login: ArcLightSlavik - count: 27 + count: 31 avatarUrl: https://avatars.githubusercontent.com/u/31127044?u=81a84af39c89b898b0fbc5a04e8834f60f23e55a&v=4 url: https://github.com/ArcLightSlavik - login: dmontagu count: 23 avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=58ed2a45798a4339700e2f62b2e12e6e54bf0396&v=4 url: https://github.com/dmontagu +- login: cassiobotaro + count: 22 + avatarUrl: https://avatars.githubusercontent.com/u/3127847?u=b0a652331da17efeb85cd6e3a4969182e5004804&v=4 + url: https://github.com/cassiobotaro - login: komtaki count: 21 avatarUrl: https://avatars.githubusercontent.com/u/39375566?u=260ad6b1a4b34c07dbfa728da5e586f16f6d1824&v=4 url: https://github.com/komtaki -- login: cassiobotaro +- login: hard-coders count: 19 - avatarUrl: https://avatars.githubusercontent.com/u/3127847?u=b0a652331da17efeb85cd6e3a4969182e5004804&v=4 - url: https://github.com/cassiobotaro + avatarUrl: https://avatars.githubusercontent.com/u/9651103?u=95db33927bbff1ed1c07efddeb97ac2ff33068ed&v=4 + url: https://github.com/hard-coders +- login: 0417taehyun + count: 19 + avatarUrl: https://avatars.githubusercontent.com/u/63915557?u=47debaa860fd52c9b98c97ef357ddcec3b3fb399&v=4 + url: https://github.com/0417taehyun - login: yanever count: 16 avatarUrl: https://avatars.githubusercontent.com/u/21978760?v=4 @@ -328,10 +332,14 @@ top_reviewers: count: 16 avatarUrl: https://avatars.githubusercontent.com/u/52768429?u=6a3aa15277406520ad37f6236e89466ed44bc5b8&v=4 url: https://github.com/SwftAlpc -- login: hard-coders +- login: Smlep count: 16 - avatarUrl: https://avatars.githubusercontent.com/u/9651103?u=f2d3d2038c55d86d7f9348f4e6c5e30191e4ee8b&v=4 - url: https://github.com/hard-coders + avatarUrl: https://avatars.githubusercontent.com/u/16785985?v=4 + url: https://github.com/Smlep +- login: DevDae + count: 16 + avatarUrl: https://avatars.githubusercontent.com/u/87962045?u=08e10fa516e844934f4b3fc7c38b33c61697e4a1&v=4 + url: https://github.com/DevDae - login: pedabraham count: 15 avatarUrl: https://avatars.githubusercontent.com/u/16860088?u=abf922a7b920bf8fdb7867d8b43e091f1e796178&v=4 @@ -340,10 +348,6 @@ top_reviewers: count: 15 avatarUrl: https://avatars.githubusercontent.com/u/63476957?u=6c86e59b48e0394d4db230f37fc9ad4d7e2c27c7&v=4 url: https://github.com/delhi09 -- login: Smlep - count: 15 - avatarUrl: https://avatars.githubusercontent.com/u/16785985?v=4 - url: https://github.com/Smlep - login: lsglucas count: 14 avatarUrl: https://avatars.githubusercontent.com/u/61513630?u=320e43fe4dc7bc6efc64e9b8f325f8075634fd20&v=4 @@ -372,18 +376,38 @@ top_reviewers: count: 10 avatarUrl: https://avatars.githubusercontent.com/u/7887703?v=4 url: https://github.com/maoyibo +- login: yezz123 + count: 9 + avatarUrl: https://avatars.githubusercontent.com/u/52716203?u=636b4f79645176df4527dd45c12d5dbb5a4193cf&v=4 + url: https://github.com/yezz123 +- login: graingert + count: 9 + avatarUrl: https://avatars.githubusercontent.com/u/413772?v=4 + url: https://github.com/graingert - login: PandaHun count: 9 avatarUrl: https://avatars.githubusercontent.com/u/13096845?u=646eba44db720e37d0dbe8e98e77ab534ea78a20&v=4 url: https://github.com/PandaHun +- login: kty4119 + count: 9 + avatarUrl: https://avatars.githubusercontent.com/u/49435654?v=4 + url: https://github.com/kty4119 - login: bezaca count: 9 avatarUrl: https://avatars.githubusercontent.com/u/69092910?u=4ac58eab99bd37d663f3d23551df96d4fbdbf760&v=4 url: https://github.com/bezaca +- login: solomein-sv + count: 9 + avatarUrl: https://avatars.githubusercontent.com/u/46193920?u=46acfb4aeefb1d7b9fdc5a8cbd9eb8744683c47a&v=4 + url: https://github.com/solomein-sv - login: blt232018 count: 8 avatarUrl: https://avatars.githubusercontent.com/u/43393471?u=172b0e0391db1aa6c1706498d6dfcb003c8a4857&v=4 url: https://github.com/blt232018 +- login: ComicShrimp + count: 8 + avatarUrl: https://avatars.githubusercontent.com/u/43503750?u=b3e4d9a14d9a65d429ce62c566aef73178b7111d&v=4 + url: https://github.com/ComicShrimp - login: Serrones count: 7 avatarUrl: https://avatars.githubusercontent.com/u/22691749?u=4795b880e13ca33a73e52fc0ef7dc9c60c8fce47&v=4 @@ -396,10 +420,6 @@ top_reviewers: count: 7 avatarUrl: https://avatars.githubusercontent.com/u/10202690?u=e6f86f5c0c3026a15d6b51792fa3e532b12f1371&v=4 url: https://github.com/raphaelauv -- login: graingert - count: 7 - avatarUrl: https://avatars.githubusercontent.com/u/413772?v=4 - url: https://github.com/graingert - login: NastasiaSaby count: 7 avatarUrl: https://avatars.githubusercontent.com/u/8245071?u=b3afd005f9e4bf080c219ef61a592b3a8004b764&v=4 @@ -408,14 +428,10 @@ top_reviewers: count: 7 avatarUrl: https://avatars.githubusercontent.com/u/1405026?v=4 url: https://github.com/Mause -- login: solomein-sv +- login: krocdort count: 7 - avatarUrl: https://avatars.githubusercontent.com/u/46193920?u=46acfb4aeefb1d7b9fdc5a8cbd9eb8744683c47a&v=4 - url: https://github.com/solomein-sv -- login: ComicShrimp - count: 7 - avatarUrl: https://avatars.githubusercontent.com/u/43503750?u=b3e4d9a14d9a65d429ce62c566aef73178b7111d&v=4 - url: https://github.com/ComicShrimp + avatarUrl: https://avatars.githubusercontent.com/u/34248814?v=4 + url: https://github.com/krocdort - login: jovicon count: 6 avatarUrl: https://avatars.githubusercontent.com/u/21287303?u=b049eac3e51a4c0473c2efe66b4d28a7d8f2b572&v=4 @@ -424,6 +440,10 @@ top_reviewers: count: 6 avatarUrl: https://avatars.githubusercontent.com/u/47563997?u=63ed66e304fe8d765762c70587d61d9196e5c82d&v=4 url: https://github.com/BilalAlpaslan +- login: diogoduartec + count: 5 + avatarUrl: https://avatars.githubusercontent.com/u/31852339?u=b50fc11c531e9b77922e19edfc9e7233d4d7b92e&v=4 + url: https://github.com/diogoduartec - login: nimctl count: 5 avatarUrl: https://avatars.githubusercontent.com/u/49960770?u=e39b11d47188744ee07b2a1c7ce1a1bdf3c80760&v=4 @@ -452,27 +472,3 @@ top_reviewers: count: 4 avatarUrl: https://avatars.githubusercontent.com/u/23391143?u=56ab6bff50be950fa8cae5cf736f2ae66e319ff3&v=4 url: https://github.com/rkbeatss -- login: izaguerreiro - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/2241504?v=4 - url: https://github.com/izaguerreiro -- login: aviramha - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/41201924?u=6883cc4fc13a7b2e60d4deddd4be06f9c5287880&v=4 - url: https://github.com/aviramha -- login: Cajuteq - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/26676532?u=8ee0422981810e51480855de1c0d67b6b79cd3f2&v=4 - url: https://github.com/Cajuteq -- login: Zxilly - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/31370133?v=4 - url: https://github.com/Zxilly -- login: dukkee - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/36825394?u=ccfd86e6a4f2d093dad6f7544cc875af67fa2df8&v=4 - url: https://github.com/dukkee -- login: Bluenix2 - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/38372706?u=c9d28aff15958d6ebf1971148bfb3154ff943c4f&v=4 - url: https://github.com/Bluenix2 From 9c25f9615c037cdfdeccb06b002e6ed8fd50d903 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 12 Dec 2021 11:34:58 +0000 Subject: [PATCH 08/59] =?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 2a0d27360..2d3a0f4e4 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 [#4274](https://github.com/tiangolo/fastapi/pull/4274) by [@github-actions[bot]](https://github.com/apps/github-actions). * 🐛 Fix JSON Schema for dataclasses, supporting the fixes in Pydantic 1.9. PR [#4272](https://github.com/tiangolo/fastapi/pull/4272) by [@PrettyWood](https://github.com/PrettyWood). * 🌐 Add Korean translation for `docs/tutorial/request-forms-and-files.md`. PR [#3744](https://github.com/tiangolo/fastapi/pull/3744) by [@NinaHwang](https://github.com/NinaHwang). * 🌐 Add Korean translation for `docs/tutorial/request-files.md`. PR [#3743](https://github.com/tiangolo/fastapi/pull/3743) by [@NinaHwang](https://github.com/NinaHwang). From 3efb4f7edff99fdc12802a85ae9d140ec4772497 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 12 Dec 2021 12:39:32 +0100 Subject: [PATCH 09/59] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.70.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 14 +++++++++++++- fastapi/__init__.py | 2 +- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 2d3a0f4e4..5995ecca6 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,8 +2,16 @@ ## Latest Changes -* 👥 Update FastAPI People. PR [#4274](https://github.com/tiangolo/fastapi/pull/4274) by [@github-actions[bot]](https://github.com/apps/github-actions). +## 0.70.1 + +There's nothing interesting in this particular FastAPI release. It is mainly to enable/unblock the release of the next version of Pydantic that comes packed with features and improvements. 🤩 + +### Fixes + * 🐛 Fix JSON Schema for dataclasses, supporting the fixes in Pydantic 1.9. PR [#4272](https://github.com/tiangolo/fastapi/pull/4272) by [@PrettyWood](https://github.com/PrettyWood). + +### Translations + * 🌐 Add Korean translation for `docs/tutorial/request-forms-and-files.md`. PR [#3744](https://github.com/tiangolo/fastapi/pull/3744) by [@NinaHwang](https://github.com/NinaHwang). * 🌐 Add Korean translation for `docs/tutorial/request-files.md`. PR [#3743](https://github.com/tiangolo/fastapi/pull/3743) by [@NinaHwang](https://github.com/NinaHwang). * 🌐 Add portuguese translation for `docs/tutorial/query-params-str-validations.md`. PR [#3965](https://github.com/tiangolo/fastapi/pull/3965) by [@leandrodesouzadev](https://github.com/leandrodesouzadev). @@ -19,6 +27,10 @@ * 🌐 Add French translation for `docs/tutorial/query-params.md`. PR [#3556](https://github.com/tiangolo/fastapi/pull/3556) by [@Smlep](https://github.com/Smlep). * 🌐 Add Turkish translation for `docs/python-types.md`. PR [#3926](https://github.com/tiangolo/fastapi/pull/3926) by [@BilalAlpaslan](https://github.com/BilalAlpaslan). +### Internal + +* 👥 Update FastAPI People. PR [#4274](https://github.com/tiangolo/fastapi/pull/4274) by [@github-actions[bot]](https://github.com/apps/github-actions). + ## 0.70.0 This release just upgrades Starlette to the latest version, `0.16.0`, which includes several bug fixes and some small breaking changes. diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 4d4d4333d..8bb6ce15b 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.70.0" +__version__ = "0.70.1" from starlette import status as status From 2b10ca1cc47146b07b6598ac9473cbe8cac50cc0 Mon Sep 17 00:00:00 2001 From: simondale00 <33907262+simondale00@users.noreply.github.com> Date: Fri, 7 Jan 2022 04:34:28 -0500 Subject: [PATCH 10/59] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Upgrade=20Starlette?= =?UTF-8?q?=20to=200.17.1=20(#4145)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Dima Tisnek Co-authored-by: simond Co-authored-by: Samuel Colvin Co-authored-by: Samuel Colvin --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 50e0afe87..4feae5524 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,7 @@ classifiers = [ "Topic :: Internet :: WWW/HTTP", ] requires = [ - "starlette ==0.16.0", + "starlette ==0.17.1", "pydantic >=1.6.2,!=1.7,!=1.7.1,!=1.7.2,!=1.7.3,!=1.8,!=1.8.1,<2.0.0", ] description-file = "README.md" From 4e9f75912f75f09816328d5f8a28060c9d1ddd69 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 7 Jan 2022 09:35:10 +0000 Subject: [PATCH 11/59] =?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 5995ecca6..add51ca59 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆️ Upgrade Starlette to 0.17.1. PR [#4145](https://github.com/tiangolo/fastapi/pull/4145) by [@simondale00](https://github.com/simondale00). ## 0.70.1 There's nothing interesting in this particular FastAPI release. It is mainly to enable/unblock the release of the next version of Pydantic that comes packed with features and improvements. 🤩 From 764ecae2d477c48ba2e61dda44ee3d663d54b84d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 7 Jan 2022 11:24:00 +0100 Subject: [PATCH 12/59] =?UTF-8?q?=E2=AC=86=20Upgrade=20MkDocs=20Material?= =?UTF-8?q?=20and=20configs=20(#4385)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/az/mkdocs.yml | 9 +++++---- docs/de/mkdocs.yml | 9 +++++---- docs/en/mkdocs.yml | 9 +++++---- docs/es/mkdocs.yml | 9 +++++---- docs/fr/mkdocs.yml | 9 +++++---- docs/id/mkdocs.yml | 9 +++++---- docs/it/mkdocs.yml | 9 +++++---- docs/ja/mkdocs.yml | 9 +++++---- docs/ko/mkdocs.yml | 9 +++++---- docs/pl/mkdocs.yml | 9 +++++---- docs/pt/mkdocs.yml | 9 +++++---- docs/ru/mkdocs.yml | 9 +++++---- docs/sq/mkdocs.yml | 9 +++++---- docs/tr/mkdocs.yml | 9 +++++---- docs/uk/mkdocs.yml | 9 +++++---- docs/zh/mkdocs.yml | 9 +++++---- pyproject.toml | 2 +- 17 files changed, 81 insertions(+), 65 deletions(-) diff --git a/docs/az/mkdocs.yml b/docs/az/mkdocs.yml index 72ca92a96..66220f63e 100644 --- a/docs/az/mkdocs.yml +++ b/docs/az/mkdocs.yml @@ -29,9 +29,6 @@ theme: repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' -google_analytics: -- UA-133183413-1 -- auto plugins: - search - markdownextradata: @@ -70,8 +67,12 @@ markdown_extensions: - name: mermaid class: mermaid format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed +- pymdownx.tabbed: + alternate_style: true extra: + analytics: + provider: google + property: UA-133183413-1 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi diff --git a/docs/de/mkdocs.yml b/docs/de/mkdocs.yml index 621305d20..360fa8c4a 100644 --- a/docs/de/mkdocs.yml +++ b/docs/de/mkdocs.yml @@ -29,9 +29,6 @@ theme: repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' -google_analytics: -- UA-133183413-1 -- auto plugins: - search - markdownextradata: @@ -71,8 +68,12 @@ markdown_extensions: - name: mermaid class: mermaid format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed +- pymdownx.tabbed: + alternate_style: true extra: + analytics: + provider: google + property: UA-133183413-1 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index aa2d0451d..5bdd2c54a 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -29,9 +29,6 @@ theme: repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' -google_analytics: -- UA-133183413-1 -- auto plugins: - search - markdownextradata: @@ -176,8 +173,12 @@ markdown_extensions: - name: mermaid class: mermaid format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed +- pymdownx.tabbed: + alternate_style: true extra: + analytics: + provider: google + property: UA-133183413-1 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi diff --git a/docs/es/mkdocs.yml b/docs/es/mkdocs.yml index ef722715b..a4bc41154 100644 --- a/docs/es/mkdocs.yml +++ b/docs/es/mkdocs.yml @@ -29,9 +29,6 @@ theme: repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' -google_analytics: -- UA-133183413-1 -- auto plugins: - search - markdownextradata: @@ -80,8 +77,12 @@ markdown_extensions: - name: mermaid class: mermaid format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed +- pymdownx.tabbed: + alternate_style: true extra: + analytics: + provider: google + property: UA-133183413-1 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi diff --git a/docs/fr/mkdocs.yml b/docs/fr/mkdocs.yml index 6242a88fd..ff16e1d78 100644 --- a/docs/fr/mkdocs.yml +++ b/docs/fr/mkdocs.yml @@ -29,9 +29,6 @@ theme: repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' -google_analytics: -- UA-133183413-1 -- auto plugins: - search - markdownextradata: @@ -85,8 +82,12 @@ markdown_extensions: - name: mermaid class: mermaid format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed +- pymdownx.tabbed: + alternate_style: true extra: + analytics: + provider: google + property: UA-133183413-1 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi diff --git a/docs/id/mkdocs.yml b/docs/id/mkdocs.yml index f9cae22cc..d70d2b3c3 100644 --- a/docs/id/mkdocs.yml +++ b/docs/id/mkdocs.yml @@ -29,9 +29,6 @@ theme: repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' -google_analytics: -- UA-133183413-1 -- auto plugins: - search - markdownextradata: @@ -70,8 +67,12 @@ markdown_extensions: - name: mermaid class: mermaid format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed +- pymdownx.tabbed: + alternate_style: true extra: + analytics: + provider: google + property: UA-133183413-1 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi diff --git a/docs/it/mkdocs.yml b/docs/it/mkdocs.yml index f16210608..e6d01fbde 100644 --- a/docs/it/mkdocs.yml +++ b/docs/it/mkdocs.yml @@ -29,9 +29,6 @@ theme: repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' -google_analytics: -- UA-133183413-1 -- auto plugins: - search - markdownextradata: @@ -70,8 +67,12 @@ markdown_extensions: - name: mermaid class: mermaid format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed +- pymdownx.tabbed: + alternate_style: true extra: + analytics: + provider: google + property: UA-133183413-1 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi diff --git a/docs/ja/mkdocs.yml b/docs/ja/mkdocs.yml index 237bc9654..39fd8a211 100644 --- a/docs/ja/mkdocs.yml +++ b/docs/ja/mkdocs.yml @@ -29,9 +29,6 @@ theme: repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' -google_analytics: -- UA-133183413-1 -- auto plugins: - search - markdownextradata: @@ -110,8 +107,12 @@ markdown_extensions: - name: mermaid class: mermaid format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed +- pymdownx.tabbed: + alternate_style: true extra: + analytics: + provider: google + property: UA-133183413-1 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi diff --git a/docs/ko/mkdocs.yml b/docs/ko/mkdocs.yml index 7a75940a3..1d4d30913 100644 --- a/docs/ko/mkdocs.yml +++ b/docs/ko/mkdocs.yml @@ -29,9 +29,6 @@ theme: repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' -google_analytics: -- UA-133183413-1 -- auto plugins: - search - markdownextradata: @@ -80,8 +77,12 @@ markdown_extensions: - name: mermaid class: mermaid format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed +- pymdownx.tabbed: + alternate_style: true extra: + analytics: + provider: google + property: UA-133183413-1 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi diff --git a/docs/pl/mkdocs.yml b/docs/pl/mkdocs.yml index 220008607..3c1351a12 100644 --- a/docs/pl/mkdocs.yml +++ b/docs/pl/mkdocs.yml @@ -29,9 +29,6 @@ theme: repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' -google_analytics: -- UA-133183413-1 -- auto plugins: - search - markdownextradata: @@ -70,8 +67,12 @@ markdown_extensions: - name: mermaid class: mermaid format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed +- pymdownx.tabbed: + alternate_style: true extra: + analytics: + provider: google + property: UA-133183413-1 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi diff --git a/docs/pt/mkdocs.yml b/docs/pt/mkdocs.yml index ea4af852e..f202f306d 100644 --- a/docs/pt/mkdocs.yml +++ b/docs/pt/mkdocs.yml @@ -29,9 +29,6 @@ theme: repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' -google_analytics: -- UA-133183413-1 -- auto plugins: - search - markdownextradata: @@ -90,8 +87,12 @@ markdown_extensions: - name: mermaid class: mermaid format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed +- pymdownx.tabbed: + alternate_style: true extra: + analytics: + provider: google + property: UA-133183413-1 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi diff --git a/docs/ru/mkdocs.yml b/docs/ru/mkdocs.yml index e15a36bf2..6e17c287e 100644 --- a/docs/ru/mkdocs.yml +++ b/docs/ru/mkdocs.yml @@ -29,9 +29,6 @@ theme: repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' -google_analytics: -- UA-133183413-1 -- auto plugins: - search - markdownextradata: @@ -70,8 +67,12 @@ markdown_extensions: - name: mermaid class: mermaid format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed +- pymdownx.tabbed: + alternate_style: true extra: + analytics: + provider: google + property: UA-133183413-1 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi diff --git a/docs/sq/mkdocs.yml b/docs/sq/mkdocs.yml index a4879521e..d9c3dad4c 100644 --- a/docs/sq/mkdocs.yml +++ b/docs/sq/mkdocs.yml @@ -29,9 +29,6 @@ theme: repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' -google_analytics: -- UA-133183413-1 -- auto plugins: - search - markdownextradata: @@ -70,8 +67,12 @@ markdown_extensions: - name: mermaid class: mermaid format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed +- pymdownx.tabbed: + alternate_style: true extra: + analytics: + provider: google + property: UA-133183413-1 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi diff --git a/docs/tr/mkdocs.yml b/docs/tr/mkdocs.yml index 5ef8fd6a7..f6ed7f5b9 100644 --- a/docs/tr/mkdocs.yml +++ b/docs/tr/mkdocs.yml @@ -29,9 +29,6 @@ theme: repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' -google_analytics: -- UA-133183413-1 -- auto plugins: - search - markdownextradata: @@ -73,8 +70,12 @@ markdown_extensions: - name: mermaid class: mermaid format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed +- pymdownx.tabbed: + alternate_style: true extra: + analytics: + provider: google + property: UA-133183413-1 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi diff --git a/docs/uk/mkdocs.yml b/docs/uk/mkdocs.yml index 7f5785e3e..d0de8cc0e 100644 --- a/docs/uk/mkdocs.yml +++ b/docs/uk/mkdocs.yml @@ -29,9 +29,6 @@ theme: repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' -google_analytics: -- UA-133183413-1 -- auto plugins: - search - markdownextradata: @@ -70,8 +67,12 @@ markdown_extensions: - name: mermaid class: mermaid format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed +- pymdownx.tabbed: + alternate_style: true extra: + analytics: + provider: google + property: UA-133183413-1 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi diff --git a/docs/zh/mkdocs.yml b/docs/zh/mkdocs.yml index 07ffa367f..a929e3388 100644 --- a/docs/zh/mkdocs.yml +++ b/docs/zh/mkdocs.yml @@ -29,9 +29,6 @@ theme: repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' -google_analytics: -- UA-133183413-1 -- auto plugins: - search - markdownextradata: @@ -120,8 +117,12 @@ markdown_extensions: - name: mermaid class: mermaid format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed +- pymdownx.tabbed: + alternate_style: true extra: + analytics: + provider: google + property: UA-133183413-1 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi diff --git a/pyproject.toml b/pyproject.toml index 4feae5524..e6148831b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,7 +69,7 @@ test = [ ] doc = [ "mkdocs >=1.1.2,<2.0.0", - "mkdocs-material >=7.1.9,<8.0.0", + "mkdocs-material >=8.1.4,<9.0.0", "mdx-include >=1.4.1,<2.0.0", "mkdocs-markdownextradata-plugin >=0.1.7,<0.3.0", "typer-cli >=0.0.12,<0.0.13", From 83f67810371f48c22b7955de926d86e42ac30416 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 7 Jan 2022 10:24:43 +0000 Subject: [PATCH 13/59] =?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 add51ca59..ce1d3144d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Upgrade MkDocs Material and configs. PR [#4385](https://github.com/tiangolo/fastapi/pull/4385) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Upgrade Starlette to 0.17.1. PR [#4145](https://github.com/tiangolo/fastapi/pull/4145) by [@simondale00](https://github.com/simondale00). ## 0.70.1 From d08a062ee2121f446537f06c8425cdeb1209e4ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 7 Jan 2022 15:11:31 +0100 Subject: [PATCH 14/59] =?UTF-8?q?=E2=9C=A8=20Add=20docs=20and=20tests=20fo?= =?UTF-8?q?r=20Python=203.9=20and=20Python=203.10=20(#3712)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Thomas Grainger --- .github/workflows/test.yml | 2 +- docs/az/mkdocs.yml | 1 - docs/de/mkdocs.yml | 1 - docs/en/docs/python-types.md | 208 ++++++++-- docs/en/docs/tutorial/background-tasks.md | 14 +- docs/en/docs/tutorial/body-fields.md | 28 +- docs/en/docs/tutorial/body-multiple-params.md | 76 +++- docs/en/docs/tutorial/body-nested-models.md | 208 ++++++++-- docs/en/docs/tutorial/body-updates.md | 80 +++- docs/en/docs/tutorial/body.md | 84 +++- docs/en/docs/tutorial/cookie-params.md | 28 +- .../dependencies/classes-as-dependencies.md | 98 ++++- docs/en/docs/tutorial/dependencies/index.md | 42 +- .../tutorial/dependencies/sub-dependencies.md | 48 ++- docs/en/docs/tutorial/encoder.md | 14 +- docs/en/docs/tutorial/extra-data-types.md | 28 +- docs/en/docs/tutorial/extra-models.md | 86 +++- docs/en/docs/tutorial/header-params.md | 63 ++- .../tutorial/path-operation-configuration.md | 100 ++++- .../path-params-numeric-validations.md | 30 +- .../tutorial/query-params-str-validations.md | 182 +++++++-- docs/en/docs/tutorial/query-params.md | 61 ++- docs/en/docs/tutorial/request-files.md | 16 +- docs/en/docs/tutorial/response-model.md | 158 +++++-- docs/en/docs/tutorial/schema-extra-example.md | 56 ++- .../tutorial/security/get-current-user.md | 72 +++- docs/en/docs/tutorial/security/oauth2-jwt.md | 56 ++- .../docs/tutorial/security/simple-oauth2.md | 70 +++- docs/en/docs/tutorial/sql-databases.md | 164 ++++++-- docs/en/docs/tutorial/testing.md | 20 +- docs/en/mkdocs.yml | 1 - docs/es/mkdocs.yml | 1 - docs/fr/mkdocs.yml | 1 - docs/id/mkdocs.yml | 1 - docs/it/mkdocs.yml | 1 - docs/ja/mkdocs.yml | 1 - docs/ko/mkdocs.yml | 1 - docs/pl/mkdocs.yml | 1 - docs/pt/mkdocs.yml | 1 - docs/ru/mkdocs.yml | 1 - docs/sq/mkdocs.yml | 1 - docs/tr/mkdocs.yml | 1 - docs/uk/mkdocs.yml | 1 - docs/zh/mkdocs.yml | 1 - docs_src/app_testing/app_b/__init__.py | 0 .../app_testing/{main_b.py => app_b/main.py} | 0 .../{test_main_b.py => app_b/test_main.py} | 2 +- docs_src/app_testing/app_b_py310/__init__.py | 0 docs_src/app_testing/app_b_py310/main.py | 36 ++ docs_src/app_testing/app_b_py310/test_main.py | 65 +++ .../background_tasks/tutorial002_py310.py | 24 ++ docs_src/body/tutorial001_py310.py | 17 + docs_src/body/tutorial002_py310.py | 21 + docs_src/body/tutorial003_py310.py | 17 + docs_src/body/tutorial004_py310.py | 20 + docs_src/body_fields/tutorial001_py310.py | 19 + .../body_multiple_params/tutorial001_py310.py | 26 ++ .../body_multiple_params/tutorial002_py310.py | 22 + .../body_multiple_params/tutorial003_py310.py | 24 ++ .../body_multiple_params/tutorial004_py310.py | 31 ++ .../body_multiple_params/tutorial005_py310.py | 17 + .../body_nested_models/tutorial001_py310.py | 18 + .../body_nested_models/tutorial002_py310.py | 18 + .../body_nested_models/tutorial002_py39.py | 20 + .../body_nested_models/tutorial003_py310.py | 18 + .../body_nested_models/tutorial003_py39.py | 20 + .../body_nested_models/tutorial004_py310.py | 24 ++ .../body_nested_models/tutorial004_py39.py | 26 ++ .../body_nested_models/tutorial005_py310.py | 24 ++ .../body_nested_models/tutorial005_py39.py | 26 ++ .../body_nested_models/tutorial006_py310.py | 24 ++ .../body_nested_models/tutorial006_py39.py | 26 ++ .../body_nested_models/tutorial007_py310.py | 30 ++ .../body_nested_models/tutorial007_py39.py | 32 ++ .../body_nested_models/tutorial008_py39.py | 14 + .../body_nested_models/tutorial009_py39.py | 8 + docs_src/body_updates/tutorial001_py310.py | 32 ++ docs_src/body_updates/tutorial001_py39.py | 34 ++ docs_src/body_updates/tutorial002_py310.py | 35 ++ docs_src/body_updates/tutorial002_py39.py | 37 ++ docs_src/cookie_params/tutorial001_py310.py | 8 + docs_src/dependencies/tutorial001_py310.py | 17 + docs_src/dependencies/tutorial002_py310.py | 23 ++ docs_src/dependencies/tutorial003_py310.py | 23 ++ docs_src/dependencies/tutorial004_py310.py | 23 ++ docs_src/dependencies/tutorial005_py310.py | 20 + docs_src/encoder/tutorial001_py310.py | 22 + .../extra_data_types/tutorial001_py310.py | 27 ++ docs_src/extra_models/tutorial001_py310.py | 41 ++ docs_src/extra_models/tutorial002_py310.py | 39 ++ docs_src/extra_models/tutorial003_py310.py | 35 ++ docs_src/extra_models/tutorial004_py39.py | 20 + docs_src/extra_models/tutorial005_py39.py | 8 + docs_src/header_params/tutorial001_py310.py | 8 + docs_src/header_params/tutorial002_py310.py | 10 + docs_src/header_params/tutorial003_py310.py | 8 + docs_src/header_params/tutorial003_py39.py | 10 + .../tutorial001.py | 2 +- .../tutorial001_py310.py | 17 + .../tutorial001_py39.py | 19 + .../tutorial002.py | 2 +- .../tutorial002_py310.py | 27 ++ .../tutorial002_py39.py | 29 ++ .../tutorial003.py | 2 +- .../tutorial003_py310.py | 22 + .../tutorial003_py39.py | 24 ++ .../tutorial004.py | 2 +- .../tutorial004_py310.py | 26 ++ .../tutorial004_py39.py | 28 ++ .../tutorial005.py | 2 +- .../tutorial005_py310.py | 31 ++ .../tutorial005_py39.py | 33 ++ .../tutorial001_py310.py | 14 + docs_src/python_types/tutorial006_py39.py | 3 + docs_src/python_types/tutorial007_py39.py | 2 + docs_src/python_types/tutorial008.py | 5 +- docs_src/python_types/tutorial008b.py | 5 + docs_src/python_types/tutorial008b_py310.py | 2 + docs_src/python_types/tutorial009_py310.py | 5 + docs_src/python_types/tutorial009b.py | 8 + docs_src/python_types/tutorial011_py310.py | 22 + docs_src/python_types/tutorial011_py39.py | 23 ++ docs_src/query_params/tutorial002_py310.py | 10 + docs_src/query_params/tutorial003_py310.py | 15 + docs_src/query_params/tutorial004_py310.py | 17 + docs_src/query_params/tutorial006_py310.py | 11 + docs_src/query_params/tutorial006b.py | 13 + .../tutorial001_py310.py | 11 + .../tutorial002_py310.py | 11 + .../tutorial003_py310.py | 11 + .../tutorial004_py310.py | 13 + .../tutorial007_py310.py | 11 + .../tutorial008_py310.py | 19 + .../tutorial009_py310.py | 11 + .../tutorial010_py310.py | 23 ++ .../tutorial011_py310.py | 9 + .../tutorial011_py39.py | 11 + .../tutorial012_py39.py | 9 + docs_src/request_files/tutorial002_py39.py | 31 ++ docs_src/response_model/tutorial001_py310.py | 17 + docs_src/response_model/tutorial001_py39.py | 19 + docs_src/response_model/tutorial002_py310.py | 17 + docs_src/response_model/tutorial003_py310.py | 22 + docs_src/response_model/tutorial004_py310.py | 24 ++ docs_src/response_model/tutorial004_py39.py | 26 ++ docs_src/response_model/tutorial005_py310.py | 37 ++ docs_src/response_model/tutorial006_py310.py | 37 ++ .../schema_extra_example/tutorial001_py310.py | 27 ++ .../schema_extra_example/tutorial002_py310.py | 17 + .../schema_extra_example/tutorial003_py310.py | 28 ++ .../schema_extra_example/tutorial004_py310.py | 50 +++ docs_src/security/tutorial002_py310.py | 30 ++ docs_src/security/tutorial003_py310.py | 88 ++++ docs_src/security/tutorial004_py310.py | 137 +++++++ docs_src/security/tutorial005_py310.py | 172 ++++++++ docs_src/security/tutorial005_py39.py | 173 ++++++++ .../sql_databases/sql_app_py310/__init__.py | 0 .../sql_databases/sql_app_py310/alt_main.py | 60 +++ docs_src/sql_databases/sql_app_py310/crud.py | 36 ++ .../sql_databases/sql_app_py310/database.py | 13 + docs_src/sql_databases/sql_app_py310/main.py | 53 +++ .../sql_databases/sql_app_py310/models.py | 26 ++ .../sql_databases/sql_app_py310/schemas.py | 35 ++ .../sql_app_py310/tests/__init__.py | 0 .../sql_app_py310/tests/test_sql_app.py | 47 +++ .../sql_databases/sql_app_py39/__init__.py | 0 .../sql_databases/sql_app_py39/alt_main.py | 60 +++ docs_src/sql_databases/sql_app_py39/crud.py | 36 ++ .../sql_databases/sql_app_py39/database.py | 13 + docs_src/sql_databases/sql_app_py39/main.py | 53 +++ docs_src/sql_databases/sql_app_py39/models.py | 26 ++ .../sql_databases/sql_app_py39/schemas.py | 37 ++ .../sql_app_py39/tests/__init__.py | 0 .../sql_app_py39/tests/test_sql_app.py | 47 +++ pyproject.toml | 1 + .../test_tutorial002_py310.py | 21 + .../test_body/test_tutorial001_py310.py | 292 +++++++++++++ .../test_tutorial001_py310.py | 176 ++++++++ .../test_tutorial001_py310.py | 155 +++++++ .../test_tutorial003_py310.py | 206 ++++++++++ .../test_tutorial009_py39.py | 113 ++++++ .../test_tutorial001_py310.py | 172 ++++++++ .../test_tutorial001_py39.py | 172 ++++++++ .../test_tutorial001_py310.py | 100 +++++ .../test_tutorial001_py310.py | 157 +++++++ .../test_tutorial004_py310.py | 152 +++++++ .../test_tutorial001_py310.py | 146 +++++++ .../test_tutorial003_py310.py | 135 ++++++ .../test_tutorial004_py39.py | 69 ++++ .../test_tutorial005_py39.py | 53 +++ .../test_tutorial001_py310.py | 94 +++++ .../test_tutorial005_py310.py | 121 ++++++ .../test_tutorial005_py39.py | 121 ++++++ .../test_tutorial006_py310.py | 148 +++++++ .../test_tutorial001_py310.py | 132 ++++++ .../test_tutorial011_py310.py | 105 +++++ .../test_tutorial011_py39.py | 105 +++++ .../test_tutorial012_py39.py | 106 +++++ .../test_tutorial002_py39.py | 235 +++++++++++ .../test_tutorial003_py310.py | 129 ++++++ .../test_tutorial004_py310.py | 133 ++++++ .../test_tutorial004_py39.py | 133 ++++++ .../test_tutorial005_py310.py | 152 +++++++ .../test_tutorial006_py310.py | 152 +++++++ .../test_tutorial004_py310.py | 143 +++++++ .../test_security/test_tutorial003_py310.py | 192 +++++++++ .../test_security/test_tutorial005_py310.py | 375 +++++++++++++++++ .../test_security/test_tutorial005_py39.py | 375 +++++++++++++++++ .../test_sql_databases/test_sql_databases.py | 7 +- .../test_sql_databases_middleware_py310.py | 384 ++++++++++++++++++ .../test_sql_databases_middleware_py39.py | 384 ++++++++++++++++++ .../test_sql_databases_py310.py | 383 +++++++++++++++++ .../test_sql_databases_py39.py | 383 +++++++++++++++++ .../test_testing_databases.py | 9 +- .../test_testing_databases_py310.py | 26 ++ .../test_testing_databases_py39.py | 26 ++ .../test_tutorial/test_testing/test_main_b.py | 14 +- .../test_testing/test_main_b_py310.py | 13 + tests/utils.py | 3 + 219 files changed, 11562 insertions(+), 452 deletions(-) create mode 100644 docs_src/app_testing/app_b/__init__.py rename docs_src/app_testing/{main_b.py => app_b/main.py} (100%) rename docs_src/app_testing/{test_main_b.py => app_b/test_main.py} (98%) create mode 100644 docs_src/app_testing/app_b_py310/__init__.py create mode 100644 docs_src/app_testing/app_b_py310/main.py create mode 100644 docs_src/app_testing/app_b_py310/test_main.py create mode 100644 docs_src/background_tasks/tutorial002_py310.py create mode 100644 docs_src/body/tutorial001_py310.py create mode 100644 docs_src/body/tutorial002_py310.py create mode 100644 docs_src/body/tutorial003_py310.py create mode 100644 docs_src/body/tutorial004_py310.py create mode 100644 docs_src/body_fields/tutorial001_py310.py create mode 100644 docs_src/body_multiple_params/tutorial001_py310.py create mode 100644 docs_src/body_multiple_params/tutorial002_py310.py create mode 100644 docs_src/body_multiple_params/tutorial003_py310.py create mode 100644 docs_src/body_multiple_params/tutorial004_py310.py create mode 100644 docs_src/body_multiple_params/tutorial005_py310.py create mode 100644 docs_src/body_nested_models/tutorial001_py310.py create mode 100644 docs_src/body_nested_models/tutorial002_py310.py create mode 100644 docs_src/body_nested_models/tutorial002_py39.py create mode 100644 docs_src/body_nested_models/tutorial003_py310.py create mode 100644 docs_src/body_nested_models/tutorial003_py39.py create mode 100644 docs_src/body_nested_models/tutorial004_py310.py create mode 100644 docs_src/body_nested_models/tutorial004_py39.py create mode 100644 docs_src/body_nested_models/tutorial005_py310.py create mode 100644 docs_src/body_nested_models/tutorial005_py39.py create mode 100644 docs_src/body_nested_models/tutorial006_py310.py create mode 100644 docs_src/body_nested_models/tutorial006_py39.py create mode 100644 docs_src/body_nested_models/tutorial007_py310.py create mode 100644 docs_src/body_nested_models/tutorial007_py39.py create mode 100644 docs_src/body_nested_models/tutorial008_py39.py create mode 100644 docs_src/body_nested_models/tutorial009_py39.py create mode 100644 docs_src/body_updates/tutorial001_py310.py create mode 100644 docs_src/body_updates/tutorial001_py39.py create mode 100644 docs_src/body_updates/tutorial002_py310.py create mode 100644 docs_src/body_updates/tutorial002_py39.py create mode 100644 docs_src/cookie_params/tutorial001_py310.py create mode 100644 docs_src/dependencies/tutorial001_py310.py create mode 100644 docs_src/dependencies/tutorial002_py310.py create mode 100644 docs_src/dependencies/tutorial003_py310.py create mode 100644 docs_src/dependencies/tutorial004_py310.py create mode 100644 docs_src/dependencies/tutorial005_py310.py create mode 100644 docs_src/encoder/tutorial001_py310.py create mode 100644 docs_src/extra_data_types/tutorial001_py310.py create mode 100644 docs_src/extra_models/tutorial001_py310.py create mode 100644 docs_src/extra_models/tutorial002_py310.py create mode 100644 docs_src/extra_models/tutorial003_py310.py create mode 100644 docs_src/extra_models/tutorial004_py39.py create mode 100644 docs_src/extra_models/tutorial005_py39.py create mode 100644 docs_src/header_params/tutorial001_py310.py create mode 100644 docs_src/header_params/tutorial002_py310.py create mode 100644 docs_src/header_params/tutorial003_py310.py create mode 100644 docs_src/header_params/tutorial003_py39.py create mode 100644 docs_src/path_operation_configuration/tutorial001_py310.py create mode 100644 docs_src/path_operation_configuration/tutorial001_py39.py create mode 100644 docs_src/path_operation_configuration/tutorial002_py310.py create mode 100644 docs_src/path_operation_configuration/tutorial002_py39.py create mode 100644 docs_src/path_operation_configuration/tutorial003_py310.py create mode 100644 docs_src/path_operation_configuration/tutorial003_py39.py create mode 100644 docs_src/path_operation_configuration/tutorial004_py310.py create mode 100644 docs_src/path_operation_configuration/tutorial004_py39.py create mode 100644 docs_src/path_operation_configuration/tutorial005_py310.py create mode 100644 docs_src/path_operation_configuration/tutorial005_py39.py create mode 100644 docs_src/path_params_numeric_validations/tutorial001_py310.py create mode 100644 docs_src/python_types/tutorial006_py39.py create mode 100644 docs_src/python_types/tutorial007_py39.py create mode 100644 docs_src/python_types/tutorial008b.py create mode 100644 docs_src/python_types/tutorial008b_py310.py create mode 100644 docs_src/python_types/tutorial009_py310.py create mode 100644 docs_src/python_types/tutorial009b.py create mode 100644 docs_src/python_types/tutorial011_py310.py create mode 100644 docs_src/python_types/tutorial011_py39.py create mode 100644 docs_src/query_params/tutorial002_py310.py create mode 100644 docs_src/query_params/tutorial003_py310.py create mode 100644 docs_src/query_params/tutorial004_py310.py create mode 100644 docs_src/query_params/tutorial006_py310.py create mode 100644 docs_src/query_params/tutorial006b.py create mode 100644 docs_src/query_params_str_validations/tutorial001_py310.py create mode 100644 docs_src/query_params_str_validations/tutorial002_py310.py create mode 100644 docs_src/query_params_str_validations/tutorial003_py310.py create mode 100644 docs_src/query_params_str_validations/tutorial004_py310.py create mode 100644 docs_src/query_params_str_validations/tutorial007_py310.py create mode 100644 docs_src/query_params_str_validations/tutorial008_py310.py create mode 100644 docs_src/query_params_str_validations/tutorial009_py310.py create mode 100644 docs_src/query_params_str_validations/tutorial010_py310.py create mode 100644 docs_src/query_params_str_validations/tutorial011_py310.py create mode 100644 docs_src/query_params_str_validations/tutorial011_py39.py create mode 100644 docs_src/query_params_str_validations/tutorial012_py39.py create mode 100644 docs_src/request_files/tutorial002_py39.py create mode 100644 docs_src/response_model/tutorial001_py310.py create mode 100644 docs_src/response_model/tutorial001_py39.py create mode 100644 docs_src/response_model/tutorial002_py310.py create mode 100644 docs_src/response_model/tutorial003_py310.py create mode 100644 docs_src/response_model/tutorial004_py310.py create mode 100644 docs_src/response_model/tutorial004_py39.py create mode 100644 docs_src/response_model/tutorial005_py310.py create mode 100644 docs_src/response_model/tutorial006_py310.py create mode 100644 docs_src/schema_extra_example/tutorial001_py310.py create mode 100644 docs_src/schema_extra_example/tutorial002_py310.py create mode 100644 docs_src/schema_extra_example/tutorial003_py310.py create mode 100644 docs_src/schema_extra_example/tutorial004_py310.py create mode 100644 docs_src/security/tutorial002_py310.py create mode 100644 docs_src/security/tutorial003_py310.py create mode 100644 docs_src/security/tutorial004_py310.py create mode 100644 docs_src/security/tutorial005_py310.py create mode 100644 docs_src/security/tutorial005_py39.py create mode 100644 docs_src/sql_databases/sql_app_py310/__init__.py create mode 100644 docs_src/sql_databases/sql_app_py310/alt_main.py create mode 100644 docs_src/sql_databases/sql_app_py310/crud.py create mode 100644 docs_src/sql_databases/sql_app_py310/database.py create mode 100644 docs_src/sql_databases/sql_app_py310/main.py create mode 100644 docs_src/sql_databases/sql_app_py310/models.py create mode 100644 docs_src/sql_databases/sql_app_py310/schemas.py create mode 100644 docs_src/sql_databases/sql_app_py310/tests/__init__.py create mode 100644 docs_src/sql_databases/sql_app_py310/tests/test_sql_app.py create mode 100644 docs_src/sql_databases/sql_app_py39/__init__.py create mode 100644 docs_src/sql_databases/sql_app_py39/alt_main.py create mode 100644 docs_src/sql_databases/sql_app_py39/crud.py create mode 100644 docs_src/sql_databases/sql_app_py39/database.py create mode 100644 docs_src/sql_databases/sql_app_py39/main.py create mode 100644 docs_src/sql_databases/sql_app_py39/models.py create mode 100644 docs_src/sql_databases/sql_app_py39/schemas.py create mode 100644 docs_src/sql_databases/sql_app_py39/tests/__init__.py create mode 100644 docs_src/sql_databases/sql_app_py39/tests/test_sql_app.py create mode 100644 tests/test_tutorial/test_background_tasks/test_tutorial002_py310.py create mode 100644 tests/test_tutorial/test_body/test_tutorial001_py310.py create mode 100644 tests/test_tutorial/test_body_fields/test_tutorial001_py310.py create mode 100644 tests/test_tutorial/test_body_multiple_params/test_tutorial001_py310.py create mode 100644 tests/test_tutorial/test_body_multiple_params/test_tutorial003_py310.py create mode 100644 tests/test_tutorial/test_body_nested_models/test_tutorial009_py39.py create mode 100644 tests/test_tutorial/test_body_updates/test_tutorial001_py310.py create mode 100644 tests/test_tutorial/test_body_updates/test_tutorial001_py39.py create mode 100644 tests/test_tutorial/test_cookie_params/test_tutorial001_py310.py create mode 100644 tests/test_tutorial/test_dependencies/test_tutorial001_py310.py create mode 100644 tests/test_tutorial/test_dependencies/test_tutorial004_py310.py create mode 100644 tests/test_tutorial/test_extra_data_types/test_tutorial001_py310.py create mode 100644 tests/test_tutorial/test_extra_models/test_tutorial003_py310.py create mode 100644 tests/test_tutorial/test_extra_models/test_tutorial004_py39.py create mode 100644 tests/test_tutorial/test_extra_models/test_tutorial005_py39.py create mode 100644 tests/test_tutorial/test_header_params/test_tutorial001_py310.py create mode 100644 tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py310.py create mode 100644 tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py39.py create mode 100644 tests/test_tutorial/test_query_params/test_tutorial006_py310.py create mode 100644 tests/test_tutorial/test_query_params_str_validations/test_tutorial001_py310.py create mode 100644 tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py310.py create mode 100644 tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py39.py create mode 100644 tests/test_tutorial/test_query_params_str_validations/test_tutorial012_py39.py create mode 100644 tests/test_tutorial/test_request_files/test_tutorial002_py39.py create mode 100644 tests/test_tutorial/test_response_model/test_tutorial003_py310.py create mode 100644 tests/test_tutorial/test_response_model/test_tutorial004_py310.py create mode 100644 tests/test_tutorial/test_response_model/test_tutorial004_py39.py create mode 100644 tests/test_tutorial/test_response_model/test_tutorial005_py310.py create mode 100644 tests/test_tutorial/test_response_model/test_tutorial006_py310.py create mode 100644 tests/test_tutorial/test_schema_extra_example/test_tutorial004_py310.py create mode 100644 tests/test_tutorial/test_security/test_tutorial003_py310.py create mode 100644 tests/test_tutorial/test_security/test_tutorial005_py310.py create mode 100644 tests/test_tutorial/test_security/test_tutorial005_py39.py create mode 100644 tests/test_tutorial/test_sql_databases/test_sql_databases_middleware_py310.py create mode 100644 tests/test_tutorial/test_sql_databases/test_sql_databases_middleware_py39.py create mode 100644 tests/test_tutorial/test_sql_databases/test_sql_databases_py310.py create mode 100644 tests/test_tutorial/test_sql_databases/test_sql_databases_py39.py create mode 100644 tests/test_tutorial/test_sql_databases/test_testing_databases_py310.py create mode 100644 tests/test_tutorial/test_sql_databases/test_testing_databases_py39.py create mode 100644 tests/test_tutorial/test_testing/test_main_b_py310.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7ab0f1230..21ea7c1a8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.6, 3.7, 3.8, 3.9] + python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"] fail-fast: false steps: diff --git a/docs/az/mkdocs.yml b/docs/az/mkdocs.yml index 66220f63e..dbff7b26a 100644 --- a/docs/az/mkdocs.yml +++ b/docs/az/mkdocs.yml @@ -20,7 +20,6 @@ theme: features: - search.suggest - search.highlight - - content.tabs.link icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg diff --git a/docs/de/mkdocs.yml b/docs/de/mkdocs.yml index 360fa8c4a..8f29ef316 100644 --- a/docs/de/mkdocs.yml +++ b/docs/de/mkdocs.yml @@ -20,7 +20,6 @@ theme: features: - search.suggest - search.highlight - - content.tabs.link icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg diff --git a/docs/en/docs/python-types.md b/docs/en/docs/python-types.md index 9bbf955b9..76d442855 100644 --- a/docs/en/docs/python-types.md +++ b/docs/en/docs/python-types.md @@ -148,37 +148,62 @@ You can use, for example: There are some data structures that can contain other values, like `dict`, `list`, `set` and `tuple`. And the internal values can have their own type too. -To declare those types and the internal types, you can use the standard Python module `typing`. +These types that have internal types are called "**generic**" types. And it's possible to declare them, even with their internal types. -It exists specifically to support these type hints. +To declare those types and the internal types, you can use the standard Python module `typing`. It exists specifically to support these type hints. -#### `List` +#### Newer versions of Python + +The syntax using `typing` is **compatible** with all versions, from Python 3.6 to the latest ones, including Python 3.9, Python 3.10, etc. + +As Python advances, **newer versions** come with improved support for these type annotations and in many cases you won't even need to import and use the `typing` module to declare the type annotations. + +If you can chose a more recent version of Python for your project, you will be able to take advantage of that extra simplicity. See some examples below. + +#### List For example, let's define a variable to be a `list` of `str`. -From `typing`, import `List` (with a capital `L`): +=== "Python 3.6 and above" -```Python hl_lines="1" -{!../../../docs_src/python_types/tutorial006.py!} -``` + From `typing`, import `List` (with a capital `L`): -Declare the variable, with the same colon (`:`) syntax. + ``` Python hl_lines="1" + {!> ../../../docs_src/python_types/tutorial006.py!} + ``` -As the type, put the `List`. + Declare the variable, with the same colon (`:`) syntax. -As the list is a type that contains some internal types, you put them in square brackets: + As the type, put the `List` that you imported from `typing`. -```Python hl_lines="4" -{!../../../docs_src/python_types/tutorial006.py!} -``` + As the list is a type that contains some internal types, you put them in square brackets: -!!! tip + ```Python hl_lines="4" + {!> ../../../docs_src/python_types/tutorial006.py!} + ``` + +=== "Python 3.9 and above" + + Declare the variable, with the same colon (`:`) syntax. + + As the type, put `list`. + + As the list is a type that contains some internal types, you put them in square brackets: + + ```Python hl_lines="1" + {!> ../../../docs_src/python_types/tutorial006_py39.py!} + ``` + +!!! info Those internal types in the square brackets are called "type parameters". - In this case, `str` is the type parameter passed to `List`. + In this case, `str` is the type parameter passed to `List` (or `list` in Python 3.9 and above). That means: "the variable `items` is a `list`, and each of the items in this list is a `str`". +!!! tip + If you use Python 3.9 or above, you don't have to import `List` from `typing`, you can use the same regular `list` type instead. + By doing that, your editor can provide support even while processing items from the list: @@ -189,20 +214,28 @@ Notice that the variable `item` is one of the elements in the list `items`. And still, the editor knows it is a `str`, and provides support for that. -#### `Tuple` and `Set` +#### Tuple and Set You would do the same to declare `tuple`s and `set`s: -```Python hl_lines="1 4" -{!../../../docs_src/python_types/tutorial007.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="1 4" + {!> ../../../docs_src/python_types/tutorial007.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="1" + {!> ../../../docs_src/python_types/tutorial007_py39.py!} + ``` This means: * The variable `items_t` is a `tuple` with 3 items, an `int`, another `int`, and a `str`. * The variable `items_s` is a `set`, and each of its items is of type `bytes`. -#### `Dict` +#### Dict To define a `dict`, you pass 2 type parameters, separated by commas. @@ -210,9 +243,17 @@ The first type parameter is for the keys of the `dict`. The second type parameter is for the values of the `dict`: -```Python hl_lines="1 4" -{!../../../docs_src/python_types/tutorial008.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="1 4" + {!> ../../../docs_src/python_types/tutorial008.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="1" + {!> ../../../docs_src/python_types/tutorial008.py!} + ``` This means: @@ -220,9 +261,33 @@ This means: * The keys of this `dict` are of type `str` (let's say, the name of each item). * The values of this `dict` are of type `float` (let's say, the price of each item). -#### `Optional` +#### Union -You can also use `Optional` to declare that a variable has a type, like `str`, but that it is "optional", which means that it could also be `None`: +You can declare that a variable can be any of **several types**, for example, an `int` or a `str`. + +In Python 3.6 and above (including Python 3.10) you can use the `Union` type from `typing` and put inside the square brackets the possible types to accept. + +In Python 3.10 there's also an **alternative syntax** were you can put the possible types separated by a vertical bar (`|`). + +=== "Python 3.6 and above" + + ```Python hl_lines="1 4" + {!> ../../../docs_src/python_types/tutorial008b.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="1" + {!> ../../../docs_src/python_types/tutorial008b_py310.py!} + ``` + +In both cases this means that `item` could be an `int` or a `str`. + +#### Possibly `None` + +You can declare that a value could have a type, like `str`, but that it could also be `None`. + +In Python 3.6 and above (including Python 3.10) you can declare it by importing and using `Optional` from the `typing` module. ```Python hl_lines="1 4" {!../../../docs_src/python_types/tutorial009.py!} @@ -230,18 +295,73 @@ You can also use `Optional` to declare that a variable has a type, like `str`, b Using `Optional[str]` instead of just `str` will let the editor help you detecting errors where you could be assuming that a value is always a `str`, when it could actually be `None` too. +`Optional[Something]` is actually a shortcut for `Union[Something, None]`, they are equivalent. + +This also means that in Python 3.10, you can use `Something | None`: + +=== "Python 3.6 and above" + + ```Python hl_lines="1 4" + {!> ../../../docs_src/python_types/tutorial009.py!} + ``` + +=== "Python 3.6 and above - alternative" + + ```Python hl_lines="1 4" + {!> ../../../docs_src/python_types/tutorial009b.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="1" + {!> ../../../docs_src/python_types/tutorial009_py310.py!} + ``` + #### Generic types -These types that take type parameters in square brackets, like: +These types that take type parameters in square brackets are called **Generic types** or **Generics**, for example: -* `List` -* `Tuple` -* `Set` -* `Dict` -* `Optional` -* ...and others. +=== "Python 3.6 and above" -are called **Generic types** or **Generics**. + * `List` + * `Tuple` + * `Set` + * `Dict` + * `Union` + * `Optional` + * ...and others. + +=== "Python 3.9 and above" + + You can use the same builtin types as generics (with square brakets and types inside): + + * `list` + * `tuple` + * `set` + * `dict` + + And the same as with Python 3.6, from the `typing` module: + + * `Union` + * `Optional` + * ...and others. + +=== "Python 3.10 and above" + + You can use the same builtin types as generics (with square brakets and types inside): + + * `list` + * `tuple` + * `set` + * `dict` + + And the same as with Python 3.6, from the `typing` module: + + * `Union` + * `Optional` (the same as with Python 3.6) + * ...and others. + + In Python 3.10, as an alternative to using the generics `Union` and `Optional`, you can use the vertical bar (`|`) to declare unions of types. ### Classes as types @@ -275,11 +395,25 @@ Then you create an instance of that class with some values and it will validate And you get all the editor support with that resulting object. -Taken from the official Pydantic docs: +An example from the official Pydantic docs: -```Python -{!../../../docs_src/python_types/tutorial011.py!} -``` +=== "Python 3.6 and above" + + ```Python + {!> ../../../docs_src/python_types/tutorial011.py!} + ``` + +=== "Python 3.9 and above" + + ```Python + {!> ../../../docs_src/python_types/tutorial011_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python + {!> ../../../docs_src/python_types/tutorial011_py310.py!} + ``` !!! info To learn more about Pydantic, check its docs. diff --git a/docs/en/docs/tutorial/background-tasks.md b/docs/en/docs/tutorial/background-tasks.md index 6002ce075..69aeb6712 100644 --- a/docs/en/docs/tutorial/background-tasks.md +++ b/docs/en/docs/tutorial/background-tasks.md @@ -57,9 +57,17 @@ Using `BackgroundTasks` also works with the dependency injection system, you can **FastAPI** knows what to do in each case and how to re-use the same object, so that all the background tasks are merged together and are run in the background afterwards: -```Python hl_lines="13 15 22 25" -{!../../../docs_src/background_tasks/tutorial002.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="13 15 22 25" + {!> ../../../docs_src/background_tasks/tutorial002.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="11 13 20 23" + {!> ../../../docs_src/background_tasks/tutorial002_py310.py!} + ``` In this example, the messages will be written to the `log.txt` file *after* the response is sent. diff --git a/docs/en/docs/tutorial/body-fields.md b/docs/en/docs/tutorial/body-fields.md index 6b3fd8fb5..1f38a0c5c 100644 --- a/docs/en/docs/tutorial/body-fields.md +++ b/docs/en/docs/tutorial/body-fields.md @@ -6,9 +6,17 @@ The same way you can declare additional validation and metadata in *path operati First, you have to import it: -```Python hl_lines="4" -{!../../../docs_src/body_fields/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="4" + {!> ../../../docs_src/body_fields/tutorial001.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="2" + {!> ../../../docs_src/body_fields/tutorial001_py310.py!} + ``` !!! warning Notice that `Field` is imported directly from `pydantic`, not from `fastapi` as are all the rest (`Query`, `Path`, `Body`, etc). @@ -17,9 +25,17 @@ First, you have to import it: You can then use `Field` with model attributes: -```Python hl_lines="11-14" -{!../../../docs_src/body_fields/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="11-14" + {!> ../../../docs_src/body_fields/tutorial001.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="9-12" + {!> ../../../docs_src/body_fields/tutorial001_py310.py!} + ``` `Field` works the same way as `Query`, `Path` and `Body`, it has all the same parameters, etc. diff --git a/docs/en/docs/tutorial/body-multiple-params.md b/docs/en/docs/tutorial/body-multiple-params.md index a4484147f..13de4c8ea 100644 --- a/docs/en/docs/tutorial/body-multiple-params.md +++ b/docs/en/docs/tutorial/body-multiple-params.md @@ -8,9 +8,17 @@ First, of course, you can mix `Path`, `Query` and request body parameter declara And you can also declare body parameters as optional, by setting the default to `None`: -```Python hl_lines="19-21" -{!../../../docs_src/body_multiple_params/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="19-21" + {!> ../../../docs_src/body_multiple_params/tutorial001.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="17-19" + {!> ../../../docs_src/body_multiple_params/tutorial001_py310.py!} + ``` !!! note Notice that, in this case, the `item` that would be taken from the body is optional. As it has a `None` default value. @@ -30,9 +38,17 @@ In the previous example, the *path operations* would expect a JSON body with the But you can also declare multiple body parameters, e.g. `item` and `user`: -```Python hl_lines="22" -{!../../../docs_src/body_multiple_params/tutorial002.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="22" + {!> ../../../docs_src/body_multiple_params/tutorial002.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="20" + {!> ../../../docs_src/body_multiple_params/tutorial002_py310.py!} + ``` In this case, **FastAPI** will notice that there are more than one body parameters in the function (two parameters that are Pydantic models). @@ -71,14 +87,20 @@ If you declare it as is, because it is a singular value, **FastAPI** will assume But you can instruct **FastAPI** to treat it as another body key using `Body`: +=== "Python 3.6 and above" -```Python hl_lines="23" -{!../../../docs_src/body_multiple_params/tutorial003.py!} -``` + ```Python hl_lines="23" + {!> ../../../docs_src/body_multiple_params/tutorial003.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="21" + {!> ../../../docs_src/body_multiple_params/tutorial003_py310.py!} + ``` In this case, **FastAPI** will expect a body like: - ```JSON { "item": { @@ -107,12 +129,26 @@ As, by default, singular values are interpreted as query parameters, you don't h q: Optional[str] = None ``` -as in: +Or in Python 3.10 and above: -```Python hl_lines="28" -{!../../../docs_src/body_multiple_params/tutorial004.py!} +```Python +q: str | None = None ``` +For example: + +=== "Python 3.6 and above" + + ```Python hl_lines="28" + {!> ../../../docs_src/body_multiple_params/tutorial004.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="26" + {!> ../../../docs_src/body_multiple_params/tutorial004_py310.py!} + ``` + !!! info `Body` also has all the same extra validation and metadata parameters as `Query`,`Path` and others you will see later. @@ -131,9 +167,17 @@ item: Item = Body(..., embed=True) as in: -```Python hl_lines="17" -{!../../../docs_src/body_multiple_params/tutorial005.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="17" + {!> ../../../docs_src/body_multiple_params/tutorial005.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="15" + {!> ../../../docs_src/body_multiple_params/tutorial005_py310.py!} + ``` In this case **FastAPI** will expect a body like: diff --git a/docs/en/docs/tutorial/body-nested-models.md b/docs/en/docs/tutorial/body-nested-models.md index 5bacf1666..fa38cfc48 100644 --- a/docs/en/docs/tutorial/body-nested-models.md +++ b/docs/en/docs/tutorial/body-nested-models.md @@ -6,9 +6,17 @@ With **FastAPI**, you can define, validate, document, and use arbitrarily deeply You can define an attribute to be a subtype. For example, a Python `list`: -```Python hl_lines="14" -{!../../../docs_src/body_nested_models/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="14" + {!> ../../../docs_src/body_nested_models/tutorial001.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="12" + {!> ../../../docs_src/body_nested_models/tutorial001_py310.py!} + ``` This will make `tags` be a list of items. Although it doesn't declare the type of each of the items. @@ -18,19 +26,29 @@ But Python has a specific way to declare lists with internal types, or "type par ### Import typing's `List` -First, import `List` from standard Python's `typing` module: +In Python 3.9 and above you can use the standard `list` to declare these type annotations as we'll see below. 💡 + +But in Python versions before 3.9 (3.6 and above), you first need to import `List` from standard Python's `typing` module: ```Python hl_lines="1" -{!../../../docs_src/body_nested_models/tutorial002.py!} +{!> ../../../docs_src/body_nested_models/tutorial002.py!} ``` -### Declare a `List` with a type parameter +### Declare a `list` with a type parameter To declare types that have type parameters (internal types), like `list`, `dict`, `tuple`: -* Import them from the `typing` module +* If you are in a Python version lower than 3.9, import their equivalent version from the `typing` module * Pass the internal type(s) as "type parameters" using square brackets: `[` and `]` +In Python 3.9 it would be: + +```Python +my_list: list[str] +``` + +In versions of Python before 3.9, it would be: + ```Python from typing import List @@ -43,9 +61,23 @@ Use that same standard syntax for model attributes with internal types. So, in our example, we can make `tags` be specifically a "list of strings": -```Python hl_lines="14" -{!../../../docs_src/body_nested_models/tutorial002.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="14" + {!> ../../../docs_src/body_nested_models/tutorial002.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="14" + {!> ../../../docs_src/body_nested_models/tutorial002_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="12" + {!> ../../../docs_src/body_nested_models/tutorial002_py310.py!} + ``` ## Set types @@ -53,11 +85,25 @@ But then we think about it, and realize that tags shouldn't repeat, they would p And Python has a special data type for sets of unique items, the `set`. -Then we can import `Set` and declare `tags` as a `set` of `str`: +Then we can declare `tags` as a set of strings: -```Python hl_lines="1 14" -{!../../../docs_src/body_nested_models/tutorial003.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="1 14" + {!> ../../../docs_src/body_nested_models/tutorial003.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="14" + {!> ../../../docs_src/body_nested_models/tutorial003_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="12" + {!> ../../../docs_src/body_nested_models/tutorial003_py310.py!} + ``` With this, even if you receive a request with duplicate data, it will be converted to a set of unique items. @@ -79,17 +125,45 @@ All that, arbitrarily nested. For example, we can define an `Image` model: -```Python hl_lines="9-11" -{!../../../docs_src/body_nested_models/tutorial004.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="9-11" + {!> ../../../docs_src/body_nested_models/tutorial004.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="9-11" + {!> ../../../docs_src/body_nested_models/tutorial004_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="7-9" + {!> ../../../docs_src/body_nested_models/tutorial004_py310.py!} + ``` ### Use the submodel as a type And then we can use it as the type of an attribute: -```Python hl_lines="20" -{!../../../docs_src/body_nested_models/tutorial004.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="20" + {!> ../../../docs_src/body_nested_models/tutorial004.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="20" + {!> ../../../docs_src/body_nested_models/tutorial004_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="18" + {!> ../../../docs_src/body_nested_models/tutorial004_py310.py!} + ``` This would mean that **FastAPI** would expect a body similar to: @@ -122,9 +196,23 @@ To see all the options you have, checkout the docs for ../../../docs_src/body_nested_models/tutorial005.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="4 10" + {!> ../../../docs_src/body_nested_models/tutorial005_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="2 8" + {!> ../../../docs_src/body_nested_models/tutorial005_py310.py!} + ``` The string will be checked to be a valid URL, and documented in JSON Schema / OpenAPI as such. @@ -132,9 +220,23 @@ The string will be checked to be a valid URL, and documented in JSON Schema / Op You can also use Pydantic models as subtypes of `list`, `set`, etc: -```Python hl_lines="20" -{!../../../docs_src/body_nested_models/tutorial006.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="20" + {!> ../../../docs_src/body_nested_models/tutorial006.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="20" + {!> ../../../docs_src/body_nested_models/tutorial006_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="18" + {!> ../../../docs_src/body_nested_models/tutorial006_py310.py!} + ``` This will expect (convert, validate, document, etc) a JSON body like: @@ -169,9 +271,23 @@ This will expect (convert, validate, document, etc) a JSON body like: You can define arbitrarily deeply nested models: -```Python hl_lines="9 14 20 23 27" -{!../../../docs_src/body_nested_models/tutorial007.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="9 14 20 23 27" + {!> ../../../docs_src/body_nested_models/tutorial007.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="9 14 20 23 27" + {!> ../../../docs_src/body_nested_models/tutorial007_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="7 12 18 21 25" + {!> ../../../docs_src/body_nested_models/tutorial007_py310.py!} + ``` !!! info Notice how `Offer` has a list of `Item`s, which in turn have an optional list of `Image`s @@ -184,11 +300,25 @@ If the top level value of the JSON body you expect is a JSON `array` (a Python ` images: List[Image] ``` +or in Python 3.9 and above: + +```Python +images: list[Image] +``` + as in: -```Python hl_lines="15" -{!../../../docs_src/body_nested_models/tutorial008.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="15" + {!> ../../../docs_src/body_nested_models/tutorial008.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="13" + {!> ../../../docs_src/body_nested_models/tutorial008_py39.py!} + ``` ## Editor support everywhere @@ -218,9 +348,17 @@ That's what we are going to see here. In this case, you would accept any `dict` as long as it has `int` keys with `float` values: -```Python hl_lines="9" -{!../../../docs_src/body_nested_models/tutorial009.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/body_nested_models/tutorial009.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="7" + {!> ../../../docs_src/body_nested_models/tutorial009_py39.py!} + ``` !!! tip Have in mind that JSON only supports `str` as keys. diff --git a/docs/en/docs/tutorial/body-updates.md b/docs/en/docs/tutorial/body-updates.md index 757d7bdbc..7d8675060 100644 --- a/docs/en/docs/tutorial/body-updates.md +++ b/docs/en/docs/tutorial/body-updates.md @@ -6,9 +6,23 @@ To update an item you can use the ../../../docs_src/body_updates/tutorial001.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="30-35" + {!> ../../../docs_src/body_updates/tutorial001_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="28-33" + {!> ../../../docs_src/body_updates/tutorial001_py310.py!} + ``` `PUT` is used to receive data that should replace the existing data. @@ -53,9 +67,23 @@ That would generate a `dict` with only the data that was set when creating the ` Then you can use this to generate a `dict` with only the data that was set (sent in the request), omitting default values: -```Python hl_lines="34" -{!../../../docs_src/body_updates/tutorial002.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="34" + {!> ../../../docs_src/body_updates/tutorial002.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="34" + {!> ../../../docs_src/body_updates/tutorial002_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="32" + {!> ../../../docs_src/body_updates/tutorial002_py310.py!} + ``` ### Using Pydantic's `update` parameter @@ -63,9 +91,23 @@ Now, you can create a copy of the existing model using `.copy()`, and pass the ` Like `stored_item_model.copy(update=update_data)`: -```Python hl_lines="35" -{!../../../docs_src/body_updates/tutorial002.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="35" + {!> ../../../docs_src/body_updates/tutorial002.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="35" + {!> ../../../docs_src/body_updates/tutorial002_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="33" + {!> ../../../docs_src/body_updates/tutorial002_py310.py!} + ``` ### Partial updates recap @@ -82,9 +124,23 @@ In summary, to apply partial updates you would: * Save the data to your DB. * Return the updated model. -```Python hl_lines="30-37" -{!../../../docs_src/body_updates/tutorial002.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="30-37" + {!> ../../../docs_src/body_updates/tutorial002.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="30-37" + {!> ../../../docs_src/body_updates/tutorial002_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="28-35" + {!> ../../../docs_src/body_updates/tutorial002_py310.py!} + ``` !!! tip You can actually use this same technique with an HTTP `PUT` operation. diff --git a/docs/en/docs/tutorial/body.md b/docs/en/docs/tutorial/body.md index f18afaea6..81441b41e 100644 --- a/docs/en/docs/tutorial/body.md +++ b/docs/en/docs/tutorial/body.md @@ -19,9 +19,17 @@ To declare a **request** body, you use ../../../docs_src/body/tutorial001.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="2" + {!> ../../../docs_src/body/tutorial001_py310.py!} + ``` ## Create your data model @@ -29,9 +37,17 @@ Then you declare your data model as a class that inherits from `BaseModel`. Use standard Python types for all the attributes: -```Python hl_lines="7-11" -{!../../../docs_src/body/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="7-11" + {!> ../../../docs_src/body/tutorial001.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="5-9" + {!> ../../../docs_src/body/tutorial001_py310.py!} + ``` The same as when declaring query parameters, when a model attribute has a default value, it is not required. Otherwise, it is required. Use `None` to make it just optional. @@ -59,9 +75,17 @@ For example, this model above declares a JSON "`object`" (or Python `dict`) like To add it to your *path operation*, declare it the same way you declared path and query parameters: -```Python hl_lines="18" -{!../../../docs_src/body/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="18" + {!> ../../../docs_src/body/tutorial001.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="16" + {!> ../../../docs_src/body/tutorial001_py310.py!} + ``` ...and declare its type as the model you created, `Item`. @@ -125,9 +149,17 @@ But you would get the same editor support with ../../../docs_src/body/tutorial002.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="19" + {!> ../../../docs_src/body/tutorial002_py310.py!} + ``` ## Request body + path parameters @@ -135,9 +167,17 @@ You can declare path parameters and request body at the same time. **FastAPI** will recognize that the function parameters that match path parameters should be **taken from the path**, and that function parameters that are declared to be Pydantic models should be **taken from the request body**. -```Python hl_lines="17-18" -{!../../../docs_src/body/tutorial003.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="17-18" + {!> ../../../docs_src/body/tutorial003.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="15-16" + {!> ../../../docs_src/body/tutorial003_py310.py!} + ``` ## Request body + path + query parameters @@ -145,9 +185,17 @@ You can also declare **body**, **path** and **query** parameters, all at the sam **FastAPI** will recognize each of them and take the data from the correct place. -```Python hl_lines="18" -{!../../../docs_src/body/tutorial004.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="18" + {!> ../../../docs_src/body/tutorial004.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="16" + {!> ../../../docs_src/body/tutorial004_py310.py!} + ``` The function parameters will be recognized as follows: diff --git a/docs/en/docs/tutorial/cookie-params.md b/docs/en/docs/tutorial/cookie-params.md index 9aa2300c4..221cdfffb 100644 --- a/docs/en/docs/tutorial/cookie-params.md +++ b/docs/en/docs/tutorial/cookie-params.md @@ -6,9 +6,17 @@ You can define Cookie parameters the same way you define `Query` and `Path` para First import `Cookie`: -```Python hl_lines="3" -{!../../../docs_src/cookie_params/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="3" + {!> ../../../docs_src/cookie_params/tutorial001.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="1" + {!> ../../../docs_src/cookie_params/tutorial001_py310.py!} + ``` ## Declare `Cookie` parameters @@ -16,9 +24,17 @@ Then declare the cookie parameters using the same structure as with `Path` and ` The first value is the default value, you can pass all the extra validation or annotation parameters: -```Python hl_lines="9" -{!../../../docs_src/cookie_params/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/cookie_params/tutorial001.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="7" + {!> ../../../docs_src/cookie_params/tutorial001_py310.py!} + ``` !!! note "Technical Details" `Cookie` is a "sister" class of `Path` and `Query`. It also inherits from the same common `Param` class. diff --git a/docs/en/docs/tutorial/dependencies/classes-as-dependencies.md b/docs/en/docs/tutorial/dependencies/classes-as-dependencies.md index 7747e3e1b..663fff15b 100644 --- a/docs/en/docs/tutorial/dependencies/classes-as-dependencies.md +++ b/docs/en/docs/tutorial/dependencies/classes-as-dependencies.md @@ -6,9 +6,17 @@ Before diving deeper into the **Dependency Injection** system, let's upgrade the In the previous example, we were returning a `dict` from our dependency ("dependable"): -```Python hl_lines="9" -{!../../../docs_src/dependencies/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/dependencies/tutorial001.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="7" + {!> ../../../docs_src/dependencies/tutorial001_py310.py!} + ``` But then we get a `dict` in the parameter `commons` of the *path operation function*. @@ -71,21 +79,45 @@ That also applies to callables with no parameters at all. The same as it would b Then, we can change the dependency "dependable" `common_parameters` from above to the class `CommonQueryParams`: -```Python hl_lines="11-15" -{!../../../docs_src/dependencies/tutorial002.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="11-15" + {!> ../../../docs_src/dependencies/tutorial002.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="9-13" + {!> ../../../docs_src/dependencies/tutorial002_py310.py!} + ``` Pay attention to the `__init__` method used to create the instance of the class: -```Python hl_lines="12" -{!../../../docs_src/dependencies/tutorial002.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="12" + {!> ../../../docs_src/dependencies/tutorial002.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="10" + {!> ../../../docs_src/dependencies/tutorial002_py310.py!} + ``` ...it has the same parameters as our previous `common_parameters`: -```Python hl_lines="8" -{!../../../docs_src/dependencies/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="8" + {!> ../../../docs_src/dependencies/tutorial001.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="6" + {!> ../../../docs_src/dependencies/tutorial001_py310.py!} + ``` Those parameters are what **FastAPI** will use to "solve" the dependency. @@ -101,9 +133,17 @@ In both cases the data will be converted, validated, documented on the OpenAPI s Now you can declare your dependency using this class. -```Python hl_lines="19" -{!../../../docs_src/dependencies/tutorial002.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial002.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="17" + {!> ../../../docs_src/dependencies/tutorial002_py310.py!} + ``` **FastAPI** calls the `CommonQueryParams` class. This creates an "instance" of that class and the instance will be passed as the parameter `commons` to your function. @@ -143,9 +183,17 @@ commons = Depends(CommonQueryParams) ..as in: -```Python hl_lines="19" -{!../../../docs_src/dependencies/tutorial003.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial003.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="17" + {!> ../../../docs_src/dependencies/tutorial003_py310.py!} + ``` But declaring the type is encouraged as that way your editor will know what will be passed as the parameter `commons`, and then it can help you with code completion, type checks, etc: @@ -179,9 +227,17 @@ You declare the dependency as the type of the parameter, and you use `Depends()` The same example would then look like: -```Python hl_lines="19" -{!../../../docs_src/dependencies/tutorial004.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial004.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="17" + {!> ../../../docs_src/dependencies/tutorial004_py310.py!} + ``` ...and **FastAPI** will know what to do. diff --git a/docs/en/docs/tutorial/dependencies/index.md b/docs/en/docs/tutorial/dependencies/index.md index cf0b5a92f..fe10facfb 100644 --- a/docs/en/docs/tutorial/dependencies/index.md +++ b/docs/en/docs/tutorial/dependencies/index.md @@ -31,9 +31,17 @@ Let's first focus on the dependency. It is just a function that can take all the same parameters that a *path operation function* can take: -```Python hl_lines="8-9" -{!../../../docs_src/dependencies/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="8-9" + {!> ../../../docs_src/dependencies/tutorial001.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="6-7" + {!> ../../../docs_src/dependencies/tutorial001_py310.py!} + ``` That's it. @@ -55,17 +63,33 @@ And then it just returns a `dict` containing those values. ### Import `Depends` -```Python hl_lines="3" -{!../../../docs_src/dependencies/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="3" + {!> ../../../docs_src/dependencies/tutorial001.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="1" + {!> ../../../docs_src/dependencies/tutorial001_py310.py!} + ``` ### Declare the dependency, in the "dependant" The same way you use `Body`, `Query`, etc. with your *path operation function* parameters, use `Depends` with a new parameter: -```Python hl_lines="13 18" -{!../../../docs_src/dependencies/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="13 18" + {!> ../../../docs_src/dependencies/tutorial001.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="11 16" + {!> ../../../docs_src/dependencies/tutorial001_py310.py!} + ``` Although you use `Depends` in the parameters of your function the same way you use `Body`, `Query`, etc, `Depends` works a bit differently. diff --git a/docs/en/docs/tutorial/dependencies/sub-dependencies.md b/docs/en/docs/tutorial/dependencies/sub-dependencies.md index 097edb68b..51531228d 100644 --- a/docs/en/docs/tutorial/dependencies/sub-dependencies.md +++ b/docs/en/docs/tutorial/dependencies/sub-dependencies.md @@ -6,25 +6,41 @@ They can be as **deep** as you need them to be. **FastAPI** will take care of solving them. -### First dependency "dependable" +## First dependency "dependable" You could create a first dependency ("dependable") like: -```Python hl_lines="8-9" -{!../../../docs_src/dependencies/tutorial005.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="8-9" + {!> ../../../docs_src/dependencies/tutorial005.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="6-7" + {!> ../../../docs_src/dependencies/tutorial005_py310.py!} + ``` It declares an optional query parameter `q` as a `str`, and then it just returns it. This is quite simple (not very useful), but will help us focus on how the sub-dependencies work. -### Second dependency, "dependable" and "dependant" +## Second dependency, "dependable" and "dependant" Then you can create another dependency function (a "dependable") that at the same time declares a dependency of its own (so it is a "dependant" too): -```Python hl_lines="13" -{!../../../docs_src/dependencies/tutorial005.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="13" + {!> ../../../docs_src/dependencies/tutorial005.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="11" + {!> ../../../docs_src/dependencies/tutorial005_py310.py!} + ``` Let's focus on the parameters declared: @@ -33,13 +49,21 @@ Let's focus on the parameters declared: * It also declares an optional `last_query` cookie, as a `str`. * If the user didn't provide any query `q`, we use the last query used, which we saved to a cookie before. -### Use the dependency +## Use the dependency Then we can use the dependency with: -```Python hl_lines="21" -{!../../../docs_src/dependencies/tutorial005.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="21" + {!> ../../../docs_src/dependencies/tutorial005.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial005_py310.py!} + ``` !!! info Notice that we are only declaring one dependency in the *path operation function*, the `query_or_cookie_extractor`. diff --git a/docs/en/docs/tutorial/encoder.md b/docs/en/docs/tutorial/encoder.md index a2cbe45d5..7a69c5474 100644 --- a/docs/en/docs/tutorial/encoder.md +++ b/docs/en/docs/tutorial/encoder.md @@ -20,9 +20,17 @@ You can use `jsonable_encoder` for that. It receives an object, like a Pydantic model, and returns a JSON compatible version: -```Python hl_lines="5 22" -{!../../../docs_src/encoder/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="5 22" + {!> ../../../docs_src/encoder/tutorial001.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="4 21" + {!> ../../../docs_src/encoder/tutorial001_py310.py!} + ``` In this example, it would convert the Pydantic model to a `dict`, and the `datetime` to a `str`. diff --git a/docs/en/docs/tutorial/extra-data-types.md b/docs/en/docs/tutorial/extra-data-types.md index 995f0b972..a00bd3212 100644 --- a/docs/en/docs/tutorial/extra-data-types.md +++ b/docs/en/docs/tutorial/extra-data-types.md @@ -55,12 +55,28 @@ Here are some of the additional data types you can use: Here's an example *path operation* with parameters using some of the above types. -```Python hl_lines="1 3 12-16" -{!../../../docs_src/extra_data_types/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="1 3 12-16" + {!> ../../../docs_src/extra_data_types/tutorial001.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="1 2 11-15" + {!> ../../../docs_src/extra_data_types/tutorial001_py310.py!} + ``` Note that the parameters inside the function have their natural data type, and you can, for example, perform normal date manipulations, like: -```Python hl_lines="18-19" -{!../../../docs_src/extra_data_types/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="18-19" + {!> ../../../docs_src/extra_data_types/tutorial001.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="17-18" + {!> ../../../docs_src/extra_data_types/tutorial001_py310.py!} + ``` diff --git a/docs/en/docs/tutorial/extra-models.md b/docs/en/docs/tutorial/extra-models.md index 4eced04ca..72fc74894 100644 --- a/docs/en/docs/tutorial/extra-models.md +++ b/docs/en/docs/tutorial/extra-models.md @@ -17,9 +17,17 @@ This is especially the case for user models, because: Here's a general idea of how the models could look like with their password fields and the places where they are used: -```Python hl_lines="9 11 16 22 24 29-30 33-35 40-41" -{!../../../docs_src/extra_models/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="9 11 16 22 24 29-30 33-35 40-41" + {!> ../../../docs_src/extra_models/tutorial001.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="7 9 14 20 22 27-28 31-33 38-39" + {!> ../../../docs_src/extra_models/tutorial001_py310.py!} + ``` ### About `**user_in.dict()` @@ -150,9 +158,17 @@ All the data conversion, validation, documentation, etc. will still work as norm That way, we can declare just the differences between the models (with plaintext `password`, with `hashed_password` and without password): -```Python hl_lines="9 15-16 19-20 23-24" -{!../../../docs_src/extra_models/tutorial002.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="9 15-16 19-20 23-24" + {!> ../../../docs_src/extra_models/tutorial002.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="7 13-14 17-18 21-22" + {!> ../../../docs_src/extra_models/tutorial002_py310.py!} + ``` ## `Union` or `anyOf` @@ -165,19 +181,49 @@ To do that, use the standard Python type hint `Union`, include the most specific type first, followed by the less specific type. In the example below, the more specific `PlaneItem` comes before `CarItem` in `Union[PlaneItem, CarItem]`. -```Python hl_lines="1 14-15 18-20 33" -{!../../../docs_src/extra_models/tutorial003.py!} +=== "Python 3.6 and above" + + ```Python hl_lines="1 14-15 18-20 33" + {!> ../../../docs_src/extra_models/tutorial003.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="1 14-15 18-20 33" + {!> ../../../docs_src/extra_models/tutorial003_py310.py!} + ``` + +### `Union` in Python 3.10 + +In this example we pass `Union[PlaneItem, CarItem]` as the value of the argument `response_model`. + +Because we are passing it as a **value to an argument** instead of putting it in a **type annotation**, we have to use `Union` even in Python 3.10. + +If it was in a type annotation we could have used the vertical bar, as: + +```Python +some_variable: PlaneItem | CarItem ``` +But if we put that in `response_model=PlaneItem | CarItem` we would get an error, because Python would try to perform an **invalid operation** between `PlaneItem` and `CarItem` instead of interpreting that as a type annotation. + ## List of models The same way, you can declare responses of lists of objects. -For that, use the standard Python `typing.List`: +For that, use the standard Python `typing.List` (or just `list` in Python 3.9 and above): -```Python hl_lines="1 20" -{!../../../docs_src/extra_models/tutorial004.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="1 20" + {!> ../../../docs_src/extra_models/tutorial004.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="18" + {!> ../../../docs_src/extra_models/tutorial004_py39.py!} + ``` ## Response with arbitrary `dict` @@ -185,11 +231,19 @@ You can also declare a response using a plain arbitrary `dict`, declaring just t This is useful if you don't know the valid field/attribute names (that would be needed for a Pydantic model) beforehand. -In this case, you can use `typing.Dict`: +In this case, you can use `typing.Dict` (or just `dict` in Python 3.9 and above): -```Python hl_lines="1 8" -{!../../../docs_src/extra_models/tutorial005.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="1 8" + {!> ../../../docs_src/extra_models/tutorial005.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="6" + {!> ../../../docs_src/extra_models/tutorial005_py39.py!} + ``` ## Recap diff --git a/docs/en/docs/tutorial/header-params.md b/docs/en/docs/tutorial/header-params.md index 57570153f..8e294416c 100644 --- a/docs/en/docs/tutorial/header-params.md +++ b/docs/en/docs/tutorial/header-params.md @@ -6,9 +6,17 @@ You can define Header parameters the same way you define `Query`, `Path` and `Co First import `Header`: -```Python hl_lines="3" -{!../../../docs_src/header_params/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="3" + {!> ../../../docs_src/header_params/tutorial001.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="1" + {!> ../../../docs_src/header_params/tutorial001_py310.py!} + ``` ## Declare `Header` parameters @@ -16,9 +24,17 @@ Then declare the header parameters using the same structure as with `Path`, `Que The first value is the default value, you can pass all the extra validation or annotation parameters: -```Python hl_lines="9" -{!../../../docs_src/header_params/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/header_params/tutorial001.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="7" + {!> ../../../docs_src/header_params/tutorial001_py310.py!} + ``` !!! note "Technical Details" `Header` is a "sister" class of `Path`, `Query` and `Cookie`. It also inherits from the same common `Param` class. @@ -44,14 +60,21 @@ So, you can use `user_agent` as you normally would in Python code, instead of ne If for some reason you need to disable automatic conversion of underscores to hyphens, set the parameter `convert_underscores` of `Header` to `False`: -```Python hl_lines="10" -{!../../../docs_src/header_params/tutorial002.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="10" + {!> ../../../docs_src/header_params/tutorial002.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="8" + {!> ../../../docs_src/header_params/tutorial002_py310.py!} + ``` !!! warning Before setting `convert_underscores` to `False`, bear in mind that some HTTP proxies and servers disallow the usage of headers with underscores. - ## Duplicate headers It is possible to receive duplicate headers. That means, the same header with multiple values. @@ -62,9 +85,23 @@ You will receive all the values from the duplicate header as a Python `list`. For example, to declare a header of `X-Token` that can appear more than once, you can write: -```Python hl_lines="9" -{!../../../docs_src/header_params/tutorial003.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/header_params/tutorial003.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/header_params/tutorial003_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="7" + {!> ../../../docs_src/header_params/tutorial003_py310.py!} + ``` If you communicate with that *path operation* sending two HTTP headers like: diff --git a/docs/en/docs/tutorial/path-operation-configuration.md b/docs/en/docs/tutorial/path-operation-configuration.md index 0d606331d..1ff448e76 100644 --- a/docs/en/docs/tutorial/path-operation-configuration.md +++ b/docs/en/docs/tutorial/path-operation-configuration.md @@ -13,9 +13,23 @@ You can pass directly the `int` code, like `404`. But if you don't remember what each number code is for, you can use the shortcut constants in `status`: -```Python hl_lines="3 17" -{!../../../docs_src/path_operation_configuration/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="3 17" + {!> ../../../docs_src/path_operation_configuration/tutorial001.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="3 17" + {!> ../../../docs_src/path_operation_configuration/tutorial001_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="1 15" + {!> ../../../docs_src/path_operation_configuration/tutorial001_py310.py!} + ``` That status code will be used in the response and will be added to the OpenAPI schema. @@ -28,9 +42,23 @@ That status code will be used in the response and will be added to the OpenAPI s You can add tags to your *path operation*, pass the parameter `tags` with a `list` of `str` (commonly just one `str`): -```Python hl_lines="17 22 27" -{!../../../docs_src/path_operation_configuration/tutorial002.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="17 22 27" + {!> ../../../docs_src/path_operation_configuration/tutorial002.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="17 22 27" + {!> ../../../docs_src/path_operation_configuration/tutorial002_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="15 20 25" + {!> ../../../docs_src/path_operation_configuration/tutorial002_py310.py!} + ``` They will be added to the OpenAPI schema and used by the automatic documentation interfaces: @@ -40,9 +68,23 @@ They will be added to the OpenAPI schema and used by the automatic documentation You can add a `summary` and `description`: -```Python hl_lines="20-21" -{!../../../docs_src/path_operation_configuration/tutorial003.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="20-21" + {!> ../../../docs_src/path_operation_configuration/tutorial003.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="20-21" + {!> ../../../docs_src/path_operation_configuration/tutorial003_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="18-19" + {!> ../../../docs_src/path_operation_configuration/tutorial003_py310.py!} + ``` ## Description from docstring @@ -50,9 +92,23 @@ As descriptions tend to be long and cover multiple lines, you can declare the *p You can write Markdown in the docstring, it will be interpreted and displayed correctly (taking into account docstring indentation). -```Python hl_lines="19-27" -{!../../../docs_src/path_operation_configuration/tutorial004.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="19-27" + {!> ../../../docs_src/path_operation_configuration/tutorial004.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="19-27" + {!> ../../../docs_src/path_operation_configuration/tutorial004_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="17-25" + {!> ../../../docs_src/path_operation_configuration/tutorial004_py310.py!} + ``` It will be used in the interactive docs: @@ -62,9 +118,23 @@ It will be used in the interactive docs: You can specify the response description with the parameter `response_description`: -```Python hl_lines="21" -{!../../../docs_src/path_operation_configuration/tutorial005.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="21" + {!> ../../../docs_src/path_operation_configuration/tutorial005.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="21" + {!> ../../../docs_src/path_operation_configuration/tutorial005_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="19" + {!> ../../../docs_src/path_operation_configuration/tutorial005_py310.py!} + ``` !!! info Notice that `response_description` refers specifically to the response, the `description` refers to the *path operation* in general. diff --git a/docs/en/docs/tutorial/path-params-numeric-validations.md b/docs/en/docs/tutorial/path-params-numeric-validations.md index 5da69a21b..31bf91a0e 100644 --- a/docs/en/docs/tutorial/path-params-numeric-validations.md +++ b/docs/en/docs/tutorial/path-params-numeric-validations.md @@ -6,9 +6,17 @@ The same way you can declare more validations and metadata for query parameters First, import `Path` from `fastapi`: -```Python hl_lines="3" -{!../../../docs_src/path_params_numeric_validations/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="3" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="1" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_py310.py!} + ``` ## Declare metadata @@ -16,13 +24,21 @@ You can declare all the same parameters as for `Query`. For example, to declare a `title` metadata value for the path parameter `item_id` you can type: -```Python hl_lines="10" -{!../../../docs_src/path_params_numeric_validations/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="10" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="8" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_py310.py!} + ``` !!! note A path parameter is always required as it has to be part of the path. - + So, you should declare it with `...` to mark it as required. Nevertheless, even if you declared it with `None` or set a default value, it would not affect anything, it would still be always required. diff --git a/docs/en/docs/tutorial/query-params-str-validations.md b/docs/en/docs/tutorial/query-params-str-validations.md index 8deccda0b..fcac1a4e0 100644 --- a/docs/en/docs/tutorial/query-params-str-validations.md +++ b/docs/en/docs/tutorial/query-params-str-validations.md @@ -4,11 +4,19 @@ Let's take this application as example: -```Python hl_lines="9" -{!../../../docs_src/query_params_str_validations/tutorial001.py!} -``` +=== "Python 3.6 and above" -The query parameter `q` is of type `Optional[str]`, that means that it's of type `str` but could also be `None`, and indeed, the default value is `None`, so FastAPI will know it's not required. + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial001.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial001_py310.py!} + ``` + +The query parameter `q` is of type `Optional[str]` (or `str | None` in Python 3.10), that means that it's of type `str` but could also be `None`, and indeed, the default value is `None`, so FastAPI will know it's not required. !!! note FastAPI will know that the value of `q` is not required because of the default value `= None`. @@ -23,17 +31,33 @@ We are going to enforce that even though `q` is optional, whenever it is provide To achieve that, first import `Query` from `fastapi`: -```Python hl_lines="3" -{!../../../docs_src/query_params_str_validations/tutorial002.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="3" + {!> ../../../docs_src/query_params_str_validations/tutorial002.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="1" + {!> ../../../docs_src/query_params_str_validations/tutorial002_py310.py!} + ``` ## Use `Query` as the default value And now use it as the default value of your parameter, setting the parameter `max_length` to 50: -```Python hl_lines="9" -{!../../../docs_src/query_params_str_validations/tutorial002.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial002.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial002_py310.py!} + ``` As we have to replace the default value `None` with `Query(None)`, the first parameter to `Query` serves the same purpose of defining that default value. @@ -49,10 +73,22 @@ q: Optional[str] = Query(None) q: Optional[str] = None ``` +And in Python 3.10 and above: + +```Python +q: str | None = Query(None) +``` + +...makes the parameter optional, the same as: + +```Python +q: str | None = None +``` + But it declares it explicitly as being a query parameter. !!! info - Have in mind that FastAPI cares about the part: + Have in mind that the most important part to make a parameter optional is the part: ```Python = None @@ -64,9 +100,9 @@ But it declares it explicitly as being a query parameter. = Query(None) ``` - and will use that `None` to detect that the query parameter is not required. + as it will use that `None` as the default value, and that way make the parameter **not required**. - The `Optional` part is only to allow your editor to provide better support. + The `Optional` part allows your editor to provide better support, but it is not what tells FastAPI that this parameter is not required. Then, we can pass more parameters to `Query`. In this case, the `max_length` parameter that applies to strings: @@ -80,17 +116,33 @@ This will validate the data, show a clear error when the data is not valid, and You can also add a parameter `min_length`: -```Python hl_lines="9" -{!../../../docs_src/query_params_str_validations/tutorial003.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial003.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial003_py310.py!} + ``` ## Add regular expressions You can define a regular expression that the parameter should match: -```Python hl_lines="10" -{!../../../docs_src/query_params_str_validations/tutorial004.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial004.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="8" + {!> ../../../docs_src/query_params_str_validations/tutorial004_py310.py!} + ``` This specific regular expression checks that the received parameter value: @@ -152,9 +204,23 @@ When you define a query parameter explicitly with `Query` you can also declare i For example, to declare a query parameter `q` that can appear multiple times in the URL, you can write: -```Python hl_lines="9" -{!../../../docs_src/query_params_str_validations/tutorial011.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial011.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial011_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial011_py310.py!} + ``` Then, with a URL like: @@ -186,9 +252,17 @@ The interactive API docs will update accordingly, to allow multiple values: And you can also define a default `list` of values if none are provided: -```Python hl_lines="9" -{!../../../docs_src/query_params_str_validations/tutorial012.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial012.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial012_py39.py!} + ``` If you go to: @@ -209,7 +283,7 @@ the default of `q` will be: `["foo", "bar"]` and your response will be: #### Using `list` -You can also use `list` directly instead of `List[str]`: +You can also use `list` directly instead of `List[str]` (or `list[str]` in Python 3.9+): ```Python hl_lines="7" {!../../../docs_src/query_params_str_validations/tutorial013.py!} @@ -233,15 +307,31 @@ That information will be included in the generated OpenAPI and used by the docum You can add a `title`: -```Python hl_lines="10" -{!../../../docs_src/query_params_str_validations/tutorial007.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial007.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial007_py310.py!} + ``` And a `description`: -```Python hl_lines="13" -{!../../../docs_src/query_params_str_validations/tutorial008.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="13" + {!> ../../../docs_src/query_params_str_validations/tutorial008.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="12" + {!> ../../../docs_src/query_params_str_validations/tutorial008_py310.py!} + ``` ## Alias parameters @@ -261,9 +351,17 @@ But you still need it to be exactly `item-query`... Then you can declare an `alias`, and that alias is what will be used to find the parameter value: -```Python hl_lines="9" -{!../../../docs_src/query_params_str_validations/tutorial009.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial009.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial009_py310.py!} + ``` ## Deprecating parameters @@ -273,9 +371,17 @@ You have to leave it there a while because there are clients using it, but you w Then pass the parameter `deprecated=True` to `Query`: -```Python hl_lines="18" -{!../../../docs_src/query_params_str_validations/tutorial010.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="18" + {!> ../../../docs_src/query_params_str_validations/tutorial010.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="17" + {!> ../../../docs_src/query_params_str_validations/tutorial010_py310.py!} + ``` The docs will show it like this: diff --git a/docs/en/docs/tutorial/query-params.md b/docs/en/docs/tutorial/query-params.md index 4401745ac..eec55502a 100644 --- a/docs/en/docs/tutorial/query-params.md +++ b/docs/en/docs/tutorial/query-params.md @@ -63,27 +63,38 @@ The parameter values in your function will be: The same way, you can declare optional query parameters, by setting their default to `None`: -```Python hl_lines="9" -{!../../../docs_src/query_params/tutorial002.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params/tutorial002.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params/tutorial002_py310.py!} + ``` In this case, the function parameter `q` will be optional, and will be `None` by default. !!! check Also notice that **FastAPI** is smart enough to notice that the path parameter `item_id` is a path parameter and `q` is not, so, it's a query parameter. -!!! note - FastAPI will know that `q` is optional because of the `= None`. - - The `Optional` in `Optional[str]` is not used by FastAPI (FastAPI will only use the `str` part), but the `Optional[str]` will let your editor help you finding errors in your code. - ## Query parameter type conversion You can also declare `bool` types, and they will be converted: -```Python hl_lines="9" -{!../../../docs_src/query_params/tutorial003.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params/tutorial003.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params/tutorial003_py310.py!} + ``` In this case, if you go to: @@ -126,9 +137,17 @@ And you don't have to declare them in any specific order. They will be detected by name: -```Python hl_lines="8 10" -{!../../../docs_src/query_params/tutorial004.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="8 10" + {!> ../../../docs_src/query_params/tutorial004.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="6 8" + {!> ../../../docs_src/query_params/tutorial004_py310.py!} + ``` ## Required query parameters @@ -184,9 +203,17 @@ http://127.0.0.1:8000/items/foo-item?needy=sooooneedy And of course, you can define some parameters as required, some as having a default value, and some entirely optional: -```Python hl_lines="10" -{!../../../docs_src/query_params/tutorial006.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params/tutorial006.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="8" + {!> ../../../docs_src/query_params/tutorial006_py310.py!} + ``` In this case, there are 3 query parameters: diff --git a/docs/en/docs/tutorial/request-files.md b/docs/en/docs/tutorial/request-files.md index 68ea654c2..b7257c7eb 100644 --- a/docs/en/docs/tutorial/request-files.md +++ b/docs/en/docs/tutorial/request-files.md @@ -119,11 +119,19 @@ It's possible to upload several files at the same time. They would be associated to the same "form field" sent using "form data". -To use that, declare a `List` of `bytes` or `UploadFile`: +To use that, declare a list of `bytes` or `UploadFile`: -```Python hl_lines="10 15" -{!../../../docs_src/request_files/tutorial002.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="10 15" + {!> ../../../docs_src/request_files/tutorial002.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="8 13" + {!> ../../../docs_src/request_files/tutorial002_py39.py!} + ``` You will receive, as declared, a `list` of `bytes` or `UploadFile`s. diff --git a/docs/en/docs/tutorial/response-model.md b/docs/en/docs/tutorial/response-model.md index e2b89ca9e..c751a9256 100644 --- a/docs/en/docs/tutorial/response-model.md +++ b/docs/en/docs/tutorial/response-model.md @@ -8,9 +8,23 @@ You can declare the model used for the response with the parameter `response_mod * `@app.delete()` * etc. -```Python hl_lines="17" -{!../../../docs_src/response_model/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="17" + {!> ../../../docs_src/response_model/tutorial001.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="17" + {!> ../../../docs_src/response_model/tutorial001_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="15" + {!> ../../../docs_src/response_model/tutorial001_py310.py!} + ``` !!! note Notice that `response_model` is a parameter of the "decorator" method (`get`, `post`, etc). Not of your *path operation function*, like all the parameters and body. @@ -35,15 +49,31 @@ But most importantly: Here we are declaring a `UserIn` model, it will contain a plaintext password: -```Python hl_lines="9 11" -{!../../../docs_src/response_model/tutorial002.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="9 11" + {!> ../../../docs_src/response_model/tutorial002.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="7 9" + {!> ../../../docs_src/response_model/tutorial002_py310.py!} + ``` And we are using this model to declare our input and the same model to declare our output: -```Python hl_lines="17-18" -{!../../../docs_src/response_model/tutorial002.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="17-18" + {!> ../../../docs_src/response_model/tutorial002.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="15-16" + {!> ../../../docs_src/response_model/tutorial002_py310.py!} + ``` Now, whenever a browser is creating a user with a password, the API will return the same password in the response. @@ -58,21 +88,45 @@ But if we use the same model for another *path operation*, we could be sending o We can instead create an input model with the plaintext password and an output model without it: -```Python hl_lines="9 11 16" -{!../../../docs_src/response_model/tutorial003.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="9 11 16" + {!> ../../../docs_src/response_model/tutorial003.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="7 9 14" + {!> ../../../docs_src/response_model/tutorial003_py310.py!} + ``` Here, even though our *path operation function* is returning the same input user that contains the password: -```Python hl_lines="24" -{!../../../docs_src/response_model/tutorial003.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="24" + {!> ../../../docs_src/response_model/tutorial003.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="22" + {!> ../../../docs_src/response_model/tutorial003_py310.py!} + ``` ...we declared the `response_model` to be our model `UserOut`, that doesn't include the password: -```Python hl_lines="22" -{!../../../docs_src/response_model/tutorial003.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="22" + {!> ../../../docs_src/response_model/tutorial003.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="20" + {!> ../../../docs_src/response_model/tutorial003_py310.py!} + ``` So, **FastAPI** will take care of filtering out all the data that is not declared in the output model (using Pydantic). @@ -90,9 +144,23 @@ And both models will be used for the interactive API documentation: Your response model could have default values, like: -```Python hl_lines="11 13-14" -{!../../../docs_src/response_model/tutorial004.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="11 13-14" + {!> ../../../docs_src/response_model/tutorial004.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="11 13-14" + {!> ../../../docs_src/response_model/tutorial004_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="9 11-12" + {!> ../../../docs_src/response_model/tutorial004_py310.py!} + ``` * `description: Optional[str] = None` has a default of `None`. * `tax: float = 10.5` has a default of `10.5`. @@ -106,9 +174,23 @@ For example, if you have models with many optional attributes in a NoSQL databas You can set the *path operation decorator* parameter `response_model_exclude_unset=True`: -```Python hl_lines="24" -{!../../../docs_src/response_model/tutorial004.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="24" + {!> ../../../docs_src/response_model/tutorial004.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="24" + {!> ../../../docs_src/response_model/tutorial004_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="22" + {!> ../../../docs_src/response_model/tutorial004_py310.py!} + ``` and those default values won't be included in the response, only the values actually set. @@ -185,9 +267,17 @@ This can be used as a quick shortcut if you have only one Pydantic model and wan This also applies to `response_model_by_alias` that works similarly. -```Python hl_lines="31 37" -{!../../../docs_src/response_model/tutorial005.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="31 37" + {!> ../../../docs_src/response_model/tutorial005.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="29 35" + {!> ../../../docs_src/response_model/tutorial005_py310.py!} + ``` !!! tip The syntax `{"name", "description"}` creates a `set` with those two values. @@ -198,9 +288,17 @@ This can be used as a quick shortcut if you have only one Pydantic model and wan If you forget to use a `set` and use a `list` or `tuple` instead, FastAPI will still convert it to a `set` and it will work correctly: -```Python hl_lines="31 37" -{!../../../docs_src/response_model/tutorial006.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="31 37" + {!> ../../../docs_src/response_model/tutorial006.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="29 35" + {!> ../../../docs_src/response_model/tutorial006_py310.py!} + ``` ## Recap diff --git a/docs/en/docs/tutorial/schema-extra-example.md b/docs/en/docs/tutorial/schema-extra-example.md index 71f9a3918..c69df51dc 100644 --- a/docs/en/docs/tutorial/schema-extra-example.md +++ b/docs/en/docs/tutorial/schema-extra-example.md @@ -8,9 +8,17 @@ Here are several ways to do it. You can declare an `example` for a Pydantic model using `Config` and `schema_extra`, as described in Pydantic's docs: Schema customization: -```Python hl_lines="15-23" -{!../../../docs_src/schema_extra_example/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="15-23" + {!> ../../../docs_src/schema_extra_example/tutorial001.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="13-21" + {!> ../../../docs_src/schema_extra_example/tutorial001_py310.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. @@ -25,9 +33,17 @@ When using `Field()` with Pydantic models, you can also declare extra info for t You can use this to add `example` for each field: -```Python hl_lines="4 10-13" -{!../../../docs_src/schema_extra_example/tutorial002.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="4 10-13" + {!> ../../../docs_src/schema_extra_example/tutorial002.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="2 8-11" + {!> ../../../docs_src/schema_extra_example/tutorial002_py310.py!} + ``` !!! warning Keep in mind that those extra arguments passed won't add any validation, only extra information, for documentation purposes. @@ -50,9 +66,17 @@ you can also declare a data `example` or a group of `examples` with additional i Here we pass an `example` of the data expected in `Body()`: -```Python hl_lines="21-26" -{!../../../docs_src/schema_extra_example/tutorial003.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="21-26" + {!> ../../../docs_src/schema_extra_example/tutorial003.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="19-24" + {!> ../../../docs_src/schema_extra_example/tutorial003_py310.py!} + ``` ### Example in the docs UI @@ -73,9 +97,17 @@ Each specific example `dict` in the `examples` can contain: * `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`. -```Python hl_lines="22-48" -{!../../../docs_src/schema_extra_example/tutorial004.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="22-48" + {!> ../../../docs_src/schema_extra_example/tutorial004.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="20-46" + {!> ../../../docs_src/schema_extra_example/tutorial004_py310.py!} + ``` ### Examples in the docs UI diff --git a/docs/en/docs/tutorial/security/get-current-user.md b/docs/en/docs/tutorial/security/get-current-user.md index a41db2b67..13e867216 100644 --- a/docs/en/docs/tutorial/security/get-current-user.md +++ b/docs/en/docs/tutorial/security/get-current-user.md @@ -16,9 +16,17 @@ First, let's create a Pydantic user model. The same way we use Pydantic to declare bodies, we can use it anywhere else: -```Python hl_lines="5 12-16" -{!../../../docs_src/security/tutorial002.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="5 12-16" + {!> ../../../docs_src/security/tutorial002.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="3 10-14" + {!> ../../../docs_src/security/tutorial002_py310.py!} + ``` ## Create a `get_current_user` dependency @@ -30,25 +38,49 @@ Remember that dependencies can have sub-dependencies? The same as we were doing before in the *path operation* directly, our new dependency `get_current_user` will receive a `token` as a `str` from the sub-dependency `oauth2_scheme`: -```Python hl_lines="25" -{!../../../docs_src/security/tutorial002.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="25" + {!> ../../../docs_src/security/tutorial002.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="23" + {!> ../../../docs_src/security/tutorial002_py310.py!} + ``` ## Get the user `get_current_user` will use a (fake) utility function we created, that takes a token as a `str` and returns our Pydantic `User` model: -```Python hl_lines="19-22 26-27" -{!../../../docs_src/security/tutorial002.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="19-22 26-27" + {!> ../../../docs_src/security/tutorial002.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="17-20 24-25" + {!> ../../../docs_src/security/tutorial002_py310.py!} + ``` ## Inject the current user So now we can use the same `Depends` with our `get_current_user` in the *path operation*: -```Python hl_lines="31" -{!../../../docs_src/security/tutorial002.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="31" + {!> ../../../docs_src/security/tutorial002.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="29" + {!> ../../../docs_src/security/tutorial002_py310.py!} + ``` Notice that we declare the type of `current_user` as the Pydantic model `User`. @@ -64,7 +96,6 @@ This will help us inside of the function with all the completion and type checks We are not restricted to having only one dependency that can return that type of data. - ## Other models You can now get the current user directly in the *path operation functions* and deal with the security mechanisms at the **Dependency Injection** level, using `Depends`. @@ -81,7 +112,6 @@ You actually don't have users that log in to your application but robots, bots, Just use any kind of model, any kind of class, any kind of database that you need for your application. **FastAPI** has you covered with the dependency injection system. - ## Code size This example might seem verbose. Have in mind that we are mixing security, data models, utility functions and *path operations* in the same file. @@ -98,9 +128,17 @@ And all of them (or any portion of them that you want) can take the advantage of And all these thousands of *path operations* can be as small as 3 lines: -```Python hl_lines="30-32" -{!../../../docs_src/security/tutorial002.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="30-32" + {!> ../../../docs_src/security/tutorial002.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="28-30" + {!> ../../../docs_src/security/tutorial002_py310.py!} + ``` ## Recap diff --git a/docs/en/docs/tutorial/security/oauth2-jwt.md b/docs/en/docs/tutorial/security/oauth2-jwt.md index f88cf23c0..09557f1ac 100644 --- a/docs/en/docs/tutorial/security/oauth2-jwt.md +++ b/docs/en/docs/tutorial/security/oauth2-jwt.md @@ -109,9 +109,17 @@ And another utility to verify if a received password matches the hash stored. And another one to authenticate and return a user. -```Python hl_lines="7 48 55-56 59-60 69-75" -{!../../../docs_src/security/tutorial004.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="7 48 55-56 59-60 69-75" + {!> ../../../docs_src/security/tutorial004.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="6 47 54-55 58-59 68-74" + {!> ../../../docs_src/security/tutorial004_py310.py!} + ``` !!! note If you check the new (fake) database `fake_users_db`, you will see how the hashed password looks like now: `"$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW"`. @@ -144,9 +152,17 @@ Define a Pydantic Model that will be used in the token endpoint for the response Create a utility function to generate a new access token. -```Python hl_lines="6 12-14 28-30 78-86" -{!../../../docs_src/security/tutorial004.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="6 12-14 28-30 78-86" + {!> ../../../docs_src/security/tutorial004.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="5 11-13 27-29 77-85" + {!> ../../../docs_src/security/tutorial004_py310.py!} + ``` ## Update the dependencies @@ -156,9 +172,17 @@ Decode the received token, verify it, and return the current user. If the token is invalid, return an HTTP error right away. -```Python hl_lines="89-106" -{!../../../docs_src/security/tutorial004.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="89-106" + {!> ../../../docs_src/security/tutorial004.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="88-105" + {!> ../../../docs_src/security/tutorial004_py310.py!} + ``` ## Update the `/token` *path operation* @@ -166,9 +190,17 @@ Create a `timedelta` with the expiration time of the token. Create a real JWT access token and return it. -```Python hl_lines="115-128" -{!../../../docs_src/security/tutorial004.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="115-128" + {!> ../../../docs_src/security/tutorial004.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="114-127" + {!> ../../../docs_src/security/tutorial004_py310.py!} + ``` ### Technical details about the JWT "subject" `sub` diff --git a/docs/en/docs/tutorial/security/simple-oauth2.md b/docs/en/docs/tutorial/security/simple-oauth2.md index 2aa747847..505b223b1 100644 --- a/docs/en/docs/tutorial/security/simple-oauth2.md +++ b/docs/en/docs/tutorial/security/simple-oauth2.md @@ -49,9 +49,17 @@ Now let's use the utilities provided by **FastAPI** to handle this. First, import `OAuth2PasswordRequestForm`, and use it as a dependency with `Depends` in the *path operation* for `/token`: -```Python hl_lines="4 76" -{!../../../docs_src/security/tutorial003.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="4 76" + {!> ../../../docs_src/security/tutorial003.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="2 74" + {!> ../../../docs_src/security/tutorial003_py310.py!} + ``` `OAuth2PasswordRequestForm` is a class dependency that declares a form body with: @@ -90,9 +98,17 @@ If there is no such user, we return an error saying "incorrect username or passw For the error, we use the exception `HTTPException`: -```Python hl_lines="3 77-79" -{!../../../docs_src/security/tutorial003.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="3 77-79" + {!> ../../../docs_src/security/tutorial003.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="1 75-77" + {!> ../../../docs_src/security/tutorial003_py310.py!} + ``` ### Check the password @@ -118,9 +134,17 @@ If your database is stolen, the thief won't have your users' plaintext passwords So, the thief won't be able to try to use those same passwords in another system (as many users use the same password everywhere, this would be dangerous). -```Python hl_lines="80-83" -{!../../../docs_src/security/tutorial003.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="80-83" + {!> ../../../docs_src/security/tutorial003.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="78-81" + {!> ../../../docs_src/security/tutorial003_py310.py!} + ``` #### About `**user_dict` @@ -156,9 +180,17 @@ For this simple example, we are going to just be completely insecure and return But for now, let's focus on the specific details we need. -```Python hl_lines="85" -{!../../../docs_src/security/tutorial003.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="85" + {!> ../../../docs_src/security/tutorial003.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="83" + {!> ../../../docs_src/security/tutorial003_py310.py!} + ``` !!! tip By the spec, you should return a JSON with an `access_token` and a `token_type`, the same as in this example. @@ -181,9 +213,17 @@ Both of these dependencies will just return an HTTP error if the user doesn't ex So, in our endpoint, we will only get a user if the user exists, was correctly authenticated, and is active: -```Python hl_lines="58-67 69-72 90" -{!../../../docs_src/security/tutorial003.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="58-66 69-72 90" + {!> ../../../docs_src/security/tutorial003.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="55-64 67-70 88" + {!> ../../../docs_src/security/tutorial003_py310.py!} + ``` !!! info The additional header `WWW-Authenticate` with value `Bearer` we are returning here is also part of the spec. diff --git a/docs/en/docs/tutorial/sql-databases.md b/docs/en/docs/tutorial/sql-databases.md index e8ebb29c8..9dc2f64c6 100644 --- a/docs/en/docs/tutorial/sql-databases.md +++ b/docs/en/docs/tutorial/sql-databases.md @@ -248,9 +248,23 @@ So, the user will also have a `password` when creating it. But for security, the `password` won't be in other Pydantic *models*, for example, it won't be sent from the API when reading a user. -```Python hl_lines="3 6-8 11-12 23-24 27-28" -{!../../../docs_src/sql_databases/sql_app/schemas.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="3 6-8 11-12 23-24 27-28" + {!> ../../../docs_src/sql_databases/sql_app/schemas.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="3 6-8 11-12 23-24 27-28" + {!> ../../../docs_src/sql_databases/sql_app_py39/schemas.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="1 4-6 9-10 21-22 25-26" + {!> ../../../docs_src/sql_databases/sql_app_py310/schemas.py!} + ``` #### SQLAlchemy style and Pydantic style @@ -278,9 +292,23 @@ The same way, when reading a user, we can now declare that `items` will contain Not only the IDs of those items, but all the data that we defined in the Pydantic *model* for reading items: `Item`. -```Python hl_lines="15-17 31-34" -{!../../../docs_src/sql_databases/sql_app/schemas.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="15-17 31-34" + {!> ../../../docs_src/sql_databases/sql_app/schemas.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="15-17 31-34" + {!> ../../../docs_src/sql_databases/sql_app_py39/schemas.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="13-15 29-32" + {!> ../../../docs_src/sql_databases/sql_app_py310/schemas.py!} + ``` !!! tip Notice that the `User`, the Pydantic *model* that will be used when reading a user (returning it from the API) doesn't include the `password`. @@ -293,9 +321,23 @@ This ../../../docs_src/sql_databases/sql_app/schemas.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="15 19-20 31 36-37" + {!> ../../../docs_src/sql_databases/sql_app_py39/schemas.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="13 17-18 29 34-35" + {!> ../../../docs_src/sql_databases/sql_app_py310/schemas.py!} + ``` !!! tip Notice it's assigning a value with `=`, like: @@ -425,9 +467,17 @@ And now in the file `sql_app/main.py` let's integrate and use all the other part In a very simplistic way create the database tables: -```Python hl_lines="9" -{!../../../docs_src/sql_databases/sql_app/main.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/sql_databases/sql_app/main.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="7" + {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} + ``` #### Alembic Note @@ -451,9 +501,17 @@ For that, we will create a new dependency with `yield`, as explained before in t Our dependency will create a new SQLAlchemy `SessionLocal` that will be used in a single request, and then close it once the request is finished. -```Python hl_lines="15-20" -{!../../../docs_src/sql_databases/sql_app/main.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="15-20" + {!> ../../../docs_src/sql_databases/sql_app/main.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="13-18" + {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} + ``` !!! info We put the creation of the `SessionLocal()` and handling of the requests in a `try` block. @@ -468,9 +526,17 @@ And then, when using the dependency in a *path operation function*, we declare i This will then give us better editor support inside the *path operation function*, because the editor will know that the `db` parameter is of type `Session`: -```Python hl_lines="24 32 38 47 53" -{!../../../docs_src/sql_databases/sql_app/main.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="24 32 38 47 53" + {!> ../../../docs_src/sql_databases/sql_app/main.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="22 30 36 45 51" + {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} + ``` !!! info "Technical Details" The parameter `db` is actually of type `SessionLocal`, but this class (created with `sessionmaker()`) is a "proxy" of a SQLAlchemy `Session`, so, the editor doesn't really know what methods are provided. @@ -481,9 +547,17 @@ This will then give us better editor support inside the *path operation function Now, finally, here's the standard **FastAPI** *path operations* code. -```Python hl_lines="23-28 31-34 37-42 45-49 52-55" -{!../../../docs_src/sql_databases/sql_app/main.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="23-28 31-34 37-42 45-49 52-55" + {!> ../../../docs_src/sql_databases/sql_app/main.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="21-26 29-32 35-40 43-47 50-53" + {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} + ``` We are creating the database session before each request in the dependency with `yield`, and then closing it afterwards. @@ -566,9 +640,23 @@ For example, in a background task worker with ../../../docs_src/sql_databases/sql_app/schemas.py!} + ``` + +=== "Python 3.9 and above" + + ```Python + {!> ../../../docs_src/sql_databases/sql_app_py39/schemas.py!} + ``` + +=== "Python 3.10 and above" + + ```Python + {!> ../../../docs_src/sql_databases/sql_app_py310/schemas.py!} + ``` * `sql_app/crud.py`: @@ -578,9 +666,17 @@ For example, in a background task worker with ../../../docs_src/sql_databases/sql_app/main.py!} + ``` + +=== "Python 3.9 and above" + + ```Python + {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} + ``` ## Check it @@ -629,9 +725,17 @@ A "middleware" is basically a function that is always executed for each request, The middleware we'll add (just a function) will create a new SQLAlchemy `SessionLocal` for each request, add it to the request and then close it once the request is finished. -```Python hl_lines="14-22" -{!../../../docs_src/sql_databases/sql_app/alt_main.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="14-22" + {!> ../../../docs_src/sql_databases/sql_app/alt_main.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="12-20" + {!> ../../../docs_src/sql_databases/sql_app_py39/alt_main.py!} + ``` !!! info We put the creation of the `SessionLocal()` and handling of the requests in a `try` block. diff --git a/docs/en/docs/tutorial/testing.md b/docs/en/docs/tutorial/testing.md index 9c9a8270c..7e2ae84d0 100644 --- a/docs/en/docs/tutorial/testing.md +++ b/docs/en/docs/tutorial/testing.md @@ -65,7 +65,7 @@ Now let's extend this example and add more details to see how to test different ### Extended **FastAPI** app file -Let's say you have a file `main_b.py` with your **FastAPI** app. +Let's say that now the file `main.py` with your **FastAPI** app has some other **path operations**. It has a `GET` operation that could return an error. @@ -73,16 +73,24 @@ It has a `POST` operation that could return several errors. Both *path operations* require an `X-Token` header. -```Python -{!../../../docs_src/app_testing/main_b.py!} -``` +=== "Python 3.6 and above" + + ```Python + {!> ../../../docs_src/app_testing/app_b/main.py!} + ``` + +=== "Python 3.10 and above" + + ```Python + {!> ../../../docs_src/app_testing/app_b_py310/main.py!} + ``` ### Extended testing file -You could then have a `test_main_b.py`, the same as before, with the extended tests: +You could then update `test_main.py` with the extended tests: ```Python -{!../../../docs_src/app_testing/test_main_b.py!} +{!> ../../../docs_src/app_testing/app_b/test_main.py!} ``` Whenever you need the client to pass information in the request and you don't know how to, you can search (Google) how to do it in `requests`. diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index 5bdd2c54a..b3716f508 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -20,7 +20,6 @@ theme: features: - search.suggest - search.highlight - - content.tabs.link icon: repo: fontawesome/brands/github-alt logo: img/icon-white.svg diff --git a/docs/es/mkdocs.yml b/docs/es/mkdocs.yml index a4bc41154..06b015a1d 100644 --- a/docs/es/mkdocs.yml +++ b/docs/es/mkdocs.yml @@ -20,7 +20,6 @@ theme: features: - search.suggest - search.highlight - - content.tabs.link icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg diff --git a/docs/fr/mkdocs.yml b/docs/fr/mkdocs.yml index ff16e1d78..c42f92b8a 100644 --- a/docs/fr/mkdocs.yml +++ b/docs/fr/mkdocs.yml @@ -20,7 +20,6 @@ theme: features: - search.suggest - search.highlight - - content.tabs.link icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg diff --git a/docs/id/mkdocs.yml b/docs/id/mkdocs.yml index d70d2b3c3..0dc34a14f 100644 --- a/docs/id/mkdocs.yml +++ b/docs/id/mkdocs.yml @@ -20,7 +20,6 @@ theme: features: - search.suggest - search.highlight - - content.tabs.link icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg diff --git a/docs/it/mkdocs.yml b/docs/it/mkdocs.yml index e6d01fbde..18afbd547 100644 --- a/docs/it/mkdocs.yml +++ b/docs/it/mkdocs.yml @@ -20,7 +20,6 @@ theme: features: - search.suggest - search.highlight - - content.tabs.link icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg diff --git a/docs/ja/mkdocs.yml b/docs/ja/mkdocs.yml index 39fd8a211..327fe862a 100644 --- a/docs/ja/mkdocs.yml +++ b/docs/ja/mkdocs.yml @@ -20,7 +20,6 @@ theme: features: - search.suggest - search.highlight - - content.tabs.link icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg diff --git a/docs/ko/mkdocs.yml b/docs/ko/mkdocs.yml index 1d4d30913..5ffb71877 100644 --- a/docs/ko/mkdocs.yml +++ b/docs/ko/mkdocs.yml @@ -20,7 +20,6 @@ theme: features: - search.suggest - search.highlight - - content.tabs.link icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg diff --git a/docs/pl/mkdocs.yml b/docs/pl/mkdocs.yml index 3c1351a12..724dc2725 100644 --- a/docs/pl/mkdocs.yml +++ b/docs/pl/mkdocs.yml @@ -20,7 +20,6 @@ theme: features: - search.suggest - search.highlight - - content.tabs.link icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg diff --git a/docs/pt/mkdocs.yml b/docs/pt/mkdocs.yml index f202f306d..f36ecf49d 100644 --- a/docs/pt/mkdocs.yml +++ b/docs/pt/mkdocs.yml @@ -20,7 +20,6 @@ theme: features: - search.suggest - search.highlight - - content.tabs.link icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg diff --git a/docs/ru/mkdocs.yml b/docs/ru/mkdocs.yml index 6e17c287e..eb2597de7 100644 --- a/docs/ru/mkdocs.yml +++ b/docs/ru/mkdocs.yml @@ -20,7 +20,6 @@ theme: features: - search.suggest - search.highlight - - content.tabs.link icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg diff --git a/docs/sq/mkdocs.yml b/docs/sq/mkdocs.yml index d9c3dad4c..5afe0a665 100644 --- a/docs/sq/mkdocs.yml +++ b/docs/sq/mkdocs.yml @@ -20,7 +20,6 @@ theme: features: - search.suggest - search.highlight - - content.tabs.link icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg diff --git a/docs/tr/mkdocs.yml b/docs/tr/mkdocs.yml index f6ed7f5b9..40ba6d522 100644 --- a/docs/tr/mkdocs.yml +++ b/docs/tr/mkdocs.yml @@ -20,7 +20,6 @@ theme: features: - search.suggest - search.highlight - - content.tabs.link icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg diff --git a/docs/uk/mkdocs.yml b/docs/uk/mkdocs.yml index d0de8cc0e..a116d90a3 100644 --- a/docs/uk/mkdocs.yml +++ b/docs/uk/mkdocs.yml @@ -20,7 +20,6 @@ theme: features: - search.suggest - search.highlight - - content.tabs.link icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg diff --git a/docs/zh/mkdocs.yml b/docs/zh/mkdocs.yml index a929e3388..33d40129a 100644 --- a/docs/zh/mkdocs.yml +++ b/docs/zh/mkdocs.yml @@ -20,7 +20,6 @@ theme: features: - search.suggest - search.highlight - - content.tabs.link icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg diff --git a/docs_src/app_testing/app_b/__init__.py b/docs_src/app_testing/app_b/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/app_testing/main_b.py b/docs_src/app_testing/app_b/main.py similarity index 100% rename from docs_src/app_testing/main_b.py rename to docs_src/app_testing/app_b/main.py diff --git a/docs_src/app_testing/test_main_b.py b/docs_src/app_testing/app_b/test_main.py similarity index 98% rename from docs_src/app_testing/test_main_b.py rename to docs_src/app_testing/app_b/test_main.py index 83cc7d255..d186b8ecb 100644 --- a/docs_src/app_testing/test_main_b.py +++ b/docs_src/app_testing/app_b/test_main.py @@ -1,6 +1,6 @@ from fastapi.testclient import TestClient -from .main_b import app +from .main import app client = TestClient(app) diff --git a/docs_src/app_testing/app_b_py310/__init__.py b/docs_src/app_testing/app_b_py310/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/app_testing/app_b_py310/main.py b/docs_src/app_testing/app_b_py310/main.py new file mode 100644 index 000000000..d44ab9e7c --- /dev/null +++ b/docs_src/app_testing/app_b_py310/main.py @@ -0,0 +1,36 @@ +from fastapi import FastAPI, Header, HTTPException +from pydantic import BaseModel + +fake_secret_token = "coneofsilence" + +fake_db = { + "foo": {"id": "foo", "title": "Foo", "description": "There goes my hero"}, + "bar": {"id": "bar", "title": "Bar", "description": "The bartenders"}, +} + +app = FastAPI() + + +class Item(BaseModel): + id: str + title: str + description: str | None = None + + +@app.get("/items/{item_id}", response_model=Item) +async def read_main(item_id: str, x_token: str = Header(...)): + if x_token != fake_secret_token: + raise HTTPException(status_code=400, detail="Invalid X-Token header") + if item_id not in fake_db: + raise HTTPException(status_code=404, detail="Item not found") + return fake_db[item_id] + + +@app.post("/items/", response_model=Item) +async def create_item(item: Item, x_token: str = Header(...)): + if x_token != fake_secret_token: + raise HTTPException(status_code=400, detail="Invalid X-Token header") + if item.id in fake_db: + raise HTTPException(status_code=400, detail="Item already exists") + fake_db[item.id] = item + return item diff --git a/docs_src/app_testing/app_b_py310/test_main.py b/docs_src/app_testing/app_b_py310/test_main.py new file mode 100644 index 000000000..d186b8ecb --- /dev/null +++ b/docs_src/app_testing/app_b_py310/test_main.py @@ -0,0 +1,65 @@ +from fastapi.testclient import TestClient + +from .main import app + +client = TestClient(app) + + +def test_read_item(): + response = client.get("/items/foo", headers={"X-Token": "coneofsilence"}) + assert response.status_code == 200 + assert response.json() == { + "id": "foo", + "title": "Foo", + "description": "There goes my hero", + } + + +def test_read_item_bad_token(): + response = client.get("/items/foo", headers={"X-Token": "hailhydra"}) + assert response.status_code == 400 + assert response.json() == {"detail": "Invalid X-Token header"} + + +def test_read_inexistent_item(): + response = client.get("/items/baz", headers={"X-Token": "coneofsilence"}) + assert response.status_code == 404 + assert response.json() == {"detail": "Item not found"} + + +def test_create_item(): + response = client.post( + "/items/", + headers={"X-Token": "coneofsilence"}, + json={"id": "foobar", "title": "Foo Bar", "description": "The Foo Barters"}, + ) + assert response.status_code == 200 + assert response.json() == { + "id": "foobar", + "title": "Foo Bar", + "description": "The Foo Barters", + } + + +def test_create_item_bad_token(): + response = client.post( + "/items/", + headers={"X-Token": "hailhydra"}, + json={"id": "bazz", "title": "Bazz", "description": "Drop the bazz"}, + ) + assert response.status_code == 400 + assert response.json() == {"detail": "Invalid X-Token header"} + + +def test_create_existing_item(): + response = client.post( + "/items/", + headers={"X-Token": "coneofsilence"}, + json={ + "id": "foo", + "title": "The Foo ID Stealers", + "description": "There goes my stealer", + }, + ) + assert response.status_code == 400 + assert response.json() == {"detail": "Item already exists"} diff --git a/docs_src/background_tasks/tutorial002_py310.py b/docs_src/background_tasks/tutorial002_py310.py new file mode 100644 index 000000000..626af1358 --- /dev/null +++ b/docs_src/background_tasks/tutorial002_py310.py @@ -0,0 +1,24 @@ +from fastapi import BackgroundTasks, Depends, FastAPI + +app = FastAPI() + + +def write_log(message: str): + with open("log.txt", mode="a") as log: + log.write(message) + + +def get_query(background_tasks: BackgroundTasks, q: str | None = None): + if q: + message = f"found query: {q}\n" + background_tasks.add_task(write_log, message) + return q + + +@app.post("/send-notification/{email}") +async def send_notification( + email: str, background_tasks: BackgroundTasks, q: str = Depends(get_query) +): + message = f"message to {email}\n" + background_tasks.add_task(write_log, message) + return {"message": "Message sent"} diff --git a/docs_src/body/tutorial001_py310.py b/docs_src/body/tutorial001_py310.py new file mode 100644 index 000000000..b71a52b62 --- /dev/null +++ b/docs_src/body/tutorial001_py310.py @@ -0,0 +1,17 @@ +from fastapi import FastAPI +from pydantic import BaseModel + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + + +app = FastAPI() + + +@app.post("/items/") +async def create_item(item: Item): + return item diff --git a/docs_src/body/tutorial002_py310.py b/docs_src/body/tutorial002_py310.py new file mode 100644 index 000000000..8928b72b8 --- /dev/null +++ b/docs_src/body/tutorial002_py310.py @@ -0,0 +1,21 @@ +from fastapi import FastAPI +from pydantic import BaseModel + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + + +app = FastAPI() + + +@app.post("/items/") +async def create_item(item: Item): + item_dict = item.dict() + if item.tax: + price_with_tax = item.price + item.tax + item_dict.update({"price_with_tax": price_with_tax}) + return item_dict diff --git a/docs_src/body/tutorial003_py310.py b/docs_src/body/tutorial003_py310.py new file mode 100644 index 000000000..a936f28fd --- /dev/null +++ b/docs_src/body/tutorial003_py310.py @@ -0,0 +1,17 @@ +from fastapi import FastAPI +from pydantic import BaseModel + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + + +app = FastAPI() + + +@app.put("/items/{item_id}") +async def create_item(item_id: int, item: Item): + return {"item_id": item_id, **item.dict()} diff --git a/docs_src/body/tutorial004_py310.py b/docs_src/body/tutorial004_py310.py new file mode 100644 index 000000000..60cfd9610 --- /dev/null +++ b/docs_src/body/tutorial004_py310.py @@ -0,0 +1,20 @@ +from fastapi import FastAPI +from pydantic import BaseModel + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + + +app = FastAPI() + + +@app.put("/items/{item_id}") +async def create_item(item_id: int, item: Item, q: str | None = None): + result = {"item_id": item_id, **item.dict()} + if q: + result.update({"q": q}) + return result diff --git a/docs_src/body_fields/tutorial001_py310.py b/docs_src/body_fields/tutorial001_py310.py new file mode 100644 index 000000000..01e02a050 --- /dev/null +++ b/docs_src/body_fields/tutorial001_py310.py @@ -0,0 +1,19 @@ +from fastapi import Body, FastAPI +from pydantic import BaseModel, Field + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = Field( + None, title="The description of the item", max_length=300 + ) + price: float = Field(..., gt=0, description="The price must be greater than zero") + tax: float | None = None + + +@app.put("/items/{item_id}") +async def update_item(item_id: int, item: Item = Body(..., embed=True)): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/body_multiple_params/tutorial001_py310.py b/docs_src/body_multiple_params/tutorial001_py310.py new file mode 100644 index 000000000..b08d397b3 --- /dev/null +++ b/docs_src/body_multiple_params/tutorial001_py310.py @@ -0,0 +1,26 @@ +from fastapi import FastAPI, Path +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + + +@app.put("/items/{item_id}") +async def update_item( + *, + item_id: int = Path(..., title="The ID of the item to get", ge=0, le=1000), + q: str | None = None, + item: Item | None = None, +): + results = {"item_id": item_id} + if q: + results.update({"q": q}) + if item: + results.update({"item": item}) + return results diff --git a/docs_src/body_multiple_params/tutorial002_py310.py b/docs_src/body_multiple_params/tutorial002_py310.py new file mode 100644 index 000000000..2c4eb824c --- /dev/null +++ b/docs_src/body_multiple_params/tutorial002_py310.py @@ -0,0 +1,22 @@ +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 User(BaseModel): + username: str + full_name: str | None = None + + +@app.put("/items/{item_id}") +async def update_item(item_id: int, item: Item, user: User): + results = {"item_id": item_id, "item": item, "user": user} + return results diff --git a/docs_src/body_multiple_params/tutorial003_py310.py b/docs_src/body_multiple_params/tutorial003_py310.py new file mode 100644 index 000000000..9ddbda3f7 --- /dev/null +++ b/docs_src/body_multiple_params/tutorial003_py310.py @@ -0,0 +1,24 @@ +from fastapi import Body, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + + +class User(BaseModel): + username: str + full_name: str | None = None + + +@app.put("/items/{item_id}") +async def update_item( + item_id: int, item: Item, user: User, importance: int = Body(...) +): + results = {"item_id": item_id, "item": item, "user": user, "importance": importance} + return results diff --git a/docs_src/body_multiple_params/tutorial004_py310.py b/docs_src/body_multiple_params/tutorial004_py310.py new file mode 100644 index 000000000..77321300e --- /dev/null +++ b/docs_src/body_multiple_params/tutorial004_py310.py @@ -0,0 +1,31 @@ +from fastapi import Body, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + + +class User(BaseModel): + username: str + full_name: str | None = None + + +@app.put("/items/{item_id}") +async def update_item( + *, + item_id: int, + item: Item, + user: User, + importance: int = Body(..., gt=0), + q: str | None = None +): + results = {"item_id": item_id, "item": item, "user": user, "importance": importance} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/body_multiple_params/tutorial005_py310.py b/docs_src/body_multiple_params/tutorial005_py310.py new file mode 100644 index 000000000..97b213b16 --- /dev/null +++ b/docs_src/body_multiple_params/tutorial005_py310.py @@ -0,0 +1,17 @@ +from fastapi import Body, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + + +@app.put("/items/{item_id}") +async def update_item(item_id: int, item: Item = Body(..., embed=True)): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/body_nested_models/tutorial001_py310.py b/docs_src/body_nested_models/tutorial001_py310.py new file mode 100644 index 000000000..d89be35d4 --- /dev/null +++ b/docs_src/body_nested_models/tutorial001_py310.py @@ -0,0 +1,18 @@ +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 + tags: list = [] + + +@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/body_nested_models/tutorial002_py310.py b/docs_src/body_nested_models/tutorial002_py310.py new file mode 100644 index 000000000..71340e9fd --- /dev/null +++ b/docs_src/body_nested_models/tutorial002_py310.py @@ -0,0 +1,18 @@ +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 + tags: list[str] = [] + + +@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/body_nested_models/tutorial002_py39.py b/docs_src/body_nested_models/tutorial002_py39.py new file mode 100644 index 000000000..af523a74e --- /dev/null +++ b/docs_src/body_nested_models/tutorial002_py39.py @@ -0,0 +1,20 @@ +from typing import Optional + +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Optional[str] = None + price: float + tax: Optional[float] = None + tags: list[str] = [] + + +@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/body_nested_models/tutorial003_py310.py b/docs_src/body_nested_models/tutorial003_py310.py new file mode 100644 index 000000000..194f2dc23 --- /dev/null +++ b/docs_src/body_nested_models/tutorial003_py310.py @@ -0,0 +1,18 @@ +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 + tags: set[str] = set() + + +@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/body_nested_models/tutorial003_py39.py b/docs_src/body_nested_models/tutorial003_py39.py new file mode 100644 index 000000000..931d92f88 --- /dev/null +++ b/docs_src/body_nested_models/tutorial003_py39.py @@ -0,0 +1,20 @@ +from typing import Optional + +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Optional[str] = None + price: float + tax: Optional[float] = None + tags: set[str] = set() + + +@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/body_nested_models/tutorial004_py310.py b/docs_src/body_nested_models/tutorial004_py310.py new file mode 100644 index 000000000..ab0b63db1 --- /dev/null +++ b/docs_src/body_nested_models/tutorial004_py310.py @@ -0,0 +1,24 @@ +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Image(BaseModel): + url: str + name: str + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + tags: set[str] = [] + image: Image | None = None + + +@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/body_nested_models/tutorial004_py39.py b/docs_src/body_nested_models/tutorial004_py39.py new file mode 100644 index 000000000..19985ec7a --- /dev/null +++ b/docs_src/body_nested_models/tutorial004_py39.py @@ -0,0 +1,26 @@ +from typing import Optional + +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Image(BaseModel): + url: str + name: str + + +class Item(BaseModel): + name: str + description: Optional[str] = None + price: float + tax: Optional[float] = None + tags: set[str] = [] + image: Optional[Image] = None + + +@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/body_nested_models/tutorial005_py310.py b/docs_src/body_nested_models/tutorial005_py310.py new file mode 100644 index 000000000..009628453 --- /dev/null +++ b/docs_src/body_nested_models/tutorial005_py310.py @@ -0,0 +1,24 @@ +from fastapi import FastAPI +from pydantic import BaseModel, HttpUrl + +app = FastAPI() + + +class Image(BaseModel): + url: HttpUrl + name: str + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + tags: set[str] = set() + image: Image | None = None + + +@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/body_nested_models/tutorial005_py39.py b/docs_src/body_nested_models/tutorial005_py39.py new file mode 100644 index 000000000..504551883 --- /dev/null +++ b/docs_src/body_nested_models/tutorial005_py39.py @@ -0,0 +1,26 @@ +from typing import Optional + +from fastapi import FastAPI +from pydantic import BaseModel, HttpUrl + +app = FastAPI() + + +class Image(BaseModel): + url: HttpUrl + name: str + + +class Item(BaseModel): + name: str + description: Optional[str] = None + price: float + tax: Optional[float] = None + tags: set[str] = set() + image: Optional[Image] = None + + +@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/body_nested_models/tutorial006_py310.py b/docs_src/body_nested_models/tutorial006_py310.py new file mode 100644 index 000000000..c87cda3c8 --- /dev/null +++ b/docs_src/body_nested_models/tutorial006_py310.py @@ -0,0 +1,24 @@ +from fastapi import FastAPI +from pydantic import BaseModel, HttpUrl + +app = FastAPI() + + +class Image(BaseModel): + url: HttpUrl + name: str + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + tags: set[str] = set() + images: list[Image] | None = None + + +@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/body_nested_models/tutorial006_py39.py b/docs_src/body_nested_models/tutorial006_py39.py new file mode 100644 index 000000000..61898178e --- /dev/null +++ b/docs_src/body_nested_models/tutorial006_py39.py @@ -0,0 +1,26 @@ +from typing import Optional + +from fastapi import FastAPI +from pydantic import BaseModel, HttpUrl + +app = FastAPI() + + +class Image(BaseModel): + url: HttpUrl + name: str + + +class Item(BaseModel): + name: str + description: Optional[str] = None + price: float + tax: Optional[float] = None + tags: set[str] = set() + images: Optional[list[Image]] = None + + +@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/body_nested_models/tutorial007_py310.py b/docs_src/body_nested_models/tutorial007_py310.py new file mode 100644 index 000000000..665ac56e5 --- /dev/null +++ b/docs_src/body_nested_models/tutorial007_py310.py @@ -0,0 +1,30 @@ +from fastapi import FastAPI +from pydantic import BaseModel, HttpUrl + +app = FastAPI() + + +class Image(BaseModel): + url: HttpUrl + name: str + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + tags: set[str] = set() + images: list[Image] | None = None + + +class Offer(BaseModel): + name: str + description: str | None = None + price: float + items: list[Item] + + +@app.post("/offers/") +async def create_offer(offer: Offer): + return offer diff --git a/docs_src/body_nested_models/tutorial007_py39.py b/docs_src/body_nested_models/tutorial007_py39.py new file mode 100644 index 000000000..0c7d32fbb --- /dev/null +++ b/docs_src/body_nested_models/tutorial007_py39.py @@ -0,0 +1,32 @@ +from typing import Optional + +from fastapi import FastAPI +from pydantic import BaseModel, HttpUrl + +app = FastAPI() + + +class Image(BaseModel): + url: HttpUrl + name: str + + +class Item(BaseModel): + name: str + description: Optional[str] = None + price: float + tax: Optional[float] = None + tags: set[str] = set() + images: Optional[list[Image]] = None + + +class Offer(BaseModel): + name: str + description: Optional[str] = None + price: float + items: list[Item] + + +@app.post("/offers/") +async def create_offer(offer: Offer): + return offer diff --git a/docs_src/body_nested_models/tutorial008_py39.py b/docs_src/body_nested_models/tutorial008_py39.py new file mode 100644 index 000000000..854a7a5a4 --- /dev/null +++ b/docs_src/body_nested_models/tutorial008_py39.py @@ -0,0 +1,14 @@ +from fastapi import FastAPI +from pydantic import BaseModel, HttpUrl + +app = FastAPI() + + +class Image(BaseModel): + url: HttpUrl + name: str + + +@app.post("/images/multiple/") +async def create_multiple_images(images: list[Image]): + return images diff --git a/docs_src/body_nested_models/tutorial009_py39.py b/docs_src/body_nested_models/tutorial009_py39.py new file mode 100644 index 000000000..59c1e5082 --- /dev/null +++ b/docs_src/body_nested_models/tutorial009_py39.py @@ -0,0 +1,8 @@ +from fastapi import FastAPI + +app = FastAPI() + + +@app.post("/index-weights/") +async def create_index_weights(weights: dict[int, float]): + return weights diff --git a/docs_src/body_updates/tutorial001_py310.py b/docs_src/body_updates/tutorial001_py310.py new file mode 100644 index 000000000..d275d7f84 --- /dev/null +++ b/docs_src/body_updates/tutorial001_py310.py @@ -0,0 +1,32 @@ +from fastapi import FastAPI +from fastapi.encoders import jsonable_encoder +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str | None = None + description: str | None = None + price: float | None = None + tax: float = 10.5 + tags: list[str] = [] + + +items = { + "foo": {"name": "Foo", "price": 50.2}, + "bar": {"name": "Bar", "description": "The bartenders", "price": 62, "tax": 20.2}, + "baz": {"name": "Baz", "description": None, "price": 50.2, "tax": 10.5, "tags": []}, +} + + +@app.get("/items/{item_id}", response_model=Item) +async def read_item(item_id: str): + return items[item_id] + + +@app.put("/items/{item_id}", response_model=Item) +async def update_item(item_id: str, item: Item): + update_item_encoded = jsonable_encoder(item) + items[item_id] = update_item_encoded + return update_item_encoded diff --git a/docs_src/body_updates/tutorial001_py39.py b/docs_src/body_updates/tutorial001_py39.py new file mode 100644 index 000000000..5d5388b56 --- /dev/null +++ b/docs_src/body_updates/tutorial001_py39.py @@ -0,0 +1,34 @@ +from typing import Optional + +from fastapi import FastAPI +from fastapi.encoders import jsonable_encoder +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: Optional[str] = None + description: Optional[str] = None + price: Optional[float] = None + tax: float = 10.5 + tags: list[str] = [] + + +items = { + "foo": {"name": "Foo", "price": 50.2}, + "bar": {"name": "Bar", "description": "The bartenders", "price": 62, "tax": 20.2}, + "baz": {"name": "Baz", "description": None, "price": 50.2, "tax": 10.5, "tags": []}, +} + + +@app.get("/items/{item_id}", response_model=Item) +async def read_item(item_id: str): + return items[item_id] + + +@app.put("/items/{item_id}", response_model=Item) +async def update_item(item_id: str, item: Item): + update_item_encoded = jsonable_encoder(item) + items[item_id] = update_item_encoded + return update_item_encoded diff --git a/docs_src/body_updates/tutorial002_py310.py b/docs_src/body_updates/tutorial002_py310.py new file mode 100644 index 000000000..349841496 --- /dev/null +++ b/docs_src/body_updates/tutorial002_py310.py @@ -0,0 +1,35 @@ +from fastapi import FastAPI +from fastapi.encoders import jsonable_encoder +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str | None = None + description: str | None = None + price: float | None = None + tax: float = 10.5 + tags: list[str] = [] + + +items = { + "foo": {"name": "Foo", "price": 50.2}, + "bar": {"name": "Bar", "description": "The bartenders", "price": 62, "tax": 20.2}, + "baz": {"name": "Baz", "description": None, "price": 50.2, "tax": 10.5, "tags": []}, +} + + +@app.get("/items/{item_id}", response_model=Item) +async def read_item(item_id: str): + return items[item_id] + + +@app.patch("/items/{item_id}", response_model=Item) +async def update_item(item_id: str, item: Item): + stored_item_data = items[item_id] + stored_item_model = Item(**stored_item_data) + update_data = item.dict(exclude_unset=True) + updated_item = stored_item_model.copy(update=update_data) + items[item_id] = jsonable_encoder(updated_item) + return updated_item diff --git a/docs_src/body_updates/tutorial002_py39.py b/docs_src/body_updates/tutorial002_py39.py new file mode 100644 index 000000000..ab85bd5ae --- /dev/null +++ b/docs_src/body_updates/tutorial002_py39.py @@ -0,0 +1,37 @@ +from typing import Optional + +from fastapi import FastAPI +from fastapi.encoders import jsonable_encoder +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: Optional[str] = None + description: Optional[str] = None + price: Optional[float] = None + tax: float = 10.5 + tags: list[str] = [] + + +items = { + "foo": {"name": "Foo", "price": 50.2}, + "bar": {"name": "Bar", "description": "The bartenders", "price": 62, "tax": 20.2}, + "baz": {"name": "Baz", "description": None, "price": 50.2, "tax": 10.5, "tags": []}, +} + + +@app.get("/items/{item_id}", response_model=Item) +async def read_item(item_id: str): + return items[item_id] + + +@app.patch("/items/{item_id}", response_model=Item) +async def update_item(item_id: str, item: Item): + stored_item_data = items[item_id] + stored_item_model = Item(**stored_item_data) + update_data = item.dict(exclude_unset=True) + updated_item = stored_item_model.copy(update=update_data) + items[item_id] = jsonable_encoder(updated_item) + return updated_item diff --git a/docs_src/cookie_params/tutorial001_py310.py b/docs_src/cookie_params/tutorial001_py310.py new file mode 100644 index 000000000..d0b004631 --- /dev/null +++ b/docs_src/cookie_params/tutorial001_py310.py @@ -0,0 +1,8 @@ +from fastapi import Cookie, FastAPI + +app = FastAPI() + + +@app.get("/items/") +async def read_items(ads_id: str | None = Cookie(None)): + return {"ads_id": ads_id} diff --git a/docs_src/dependencies/tutorial001_py310.py b/docs_src/dependencies/tutorial001_py310.py new file mode 100644 index 000000000..662af9b1b --- /dev/null +++ b/docs_src/dependencies/tutorial001_py310.py @@ -0,0 +1,17 @@ +from fastapi import Depends, FastAPI + +app = FastAPI() + + +async def common_parameters(q: str | None = None, skip: int = 0, limit: int = 100): + return {"q": q, "skip": skip, "limit": limit} + + +@app.get("/items/") +async def read_items(commons: dict = Depends(common_parameters)): + return commons + + +@app.get("/users/") +async def read_users(commons: dict = Depends(common_parameters)): + return commons diff --git a/docs_src/dependencies/tutorial002_py310.py b/docs_src/dependencies/tutorial002_py310.py new file mode 100644 index 000000000..23c048ab9 --- /dev/null +++ b/docs_src/dependencies/tutorial002_py310.py @@ -0,0 +1,23 @@ +from fastapi import Depends, FastAPI + +app = FastAPI() + + +fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}] + + +class CommonQueryParams: + def __init__(self, q: str | None = None, skip: int = 0, limit: int = 100): + self.q = q + self.skip = skip + self.limit = limit + + +@app.get("/items/") +async def read_items(commons: CommonQueryParams = Depends(CommonQueryParams)): + response = {} + if commons.q: + response.update({"q": commons.q}) + items = fake_items_db[commons.skip : commons.skip + commons.limit] + response.update({"items": items}) + return response diff --git a/docs_src/dependencies/tutorial003_py310.py b/docs_src/dependencies/tutorial003_py310.py new file mode 100644 index 000000000..10d3771c1 --- /dev/null +++ b/docs_src/dependencies/tutorial003_py310.py @@ -0,0 +1,23 @@ +from fastapi import Depends, FastAPI + +app = FastAPI() + + +fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}] + + +class CommonQueryParams: + def __init__(self, q: str | None = None, skip: int = 0, limit: int = 100): + self.q = q + self.skip = skip + self.limit = limit + + +@app.get("/items/") +async def read_items(commons=Depends(CommonQueryParams)): + response = {} + if commons.q: + response.update({"q": commons.q}) + items = fake_items_db[commons.skip : commons.skip + commons.limit] + response.update({"items": items}) + return response diff --git a/docs_src/dependencies/tutorial004_py310.py b/docs_src/dependencies/tutorial004_py310.py new file mode 100644 index 000000000..5245bb0f6 --- /dev/null +++ b/docs_src/dependencies/tutorial004_py310.py @@ -0,0 +1,23 @@ +from fastapi import Depends, FastAPI + +app = FastAPI() + + +fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}] + + +class CommonQueryParams: + def __init__(self, q: str | None = None, skip: int = 0, limit: int = 100): + self.q = q + self.skip = skip + self.limit = limit + + +@app.get("/items/") +async def read_items(commons: CommonQueryParams = Depends()): + response = {} + if commons.q: + response.update({"q": commons.q}) + items = fake_items_db[commons.skip : commons.skip + commons.limit] + response.update({"items": items}) + return response diff --git a/docs_src/dependencies/tutorial005_py310.py b/docs_src/dependencies/tutorial005_py310.py new file mode 100644 index 000000000..5e1d7e0ef --- /dev/null +++ b/docs_src/dependencies/tutorial005_py310.py @@ -0,0 +1,20 @@ +from fastapi import Cookie, Depends, FastAPI + +app = FastAPI() + + +def query_extractor(q: str | None = None): + return q + + +def query_or_cookie_extractor( + q: str = Depends(query_extractor), last_query: str | None = Cookie(None) +): + if not q: + return last_query + return q + + +@app.get("/items/") +async def read_query(query_or_default: str = Depends(query_or_cookie_extractor)): + return {"q_or_cookie": query_or_default} diff --git a/docs_src/encoder/tutorial001_py310.py b/docs_src/encoder/tutorial001_py310.py new file mode 100644 index 000000000..5baf13628 --- /dev/null +++ b/docs_src/encoder/tutorial001_py310.py @@ -0,0 +1,22 @@ +from datetime import datetime + +from fastapi import FastAPI +from fastapi.encoders import jsonable_encoder +from pydantic import BaseModel + +fake_db = {} + + +class Item(BaseModel): + title: str + timestamp: datetime + description: str | None = None + + +app = FastAPI() + + +@app.put("/items/{id}") +def update_item(id: str, item: Item): + json_compatible_item_data = jsonable_encoder(item) + fake_db[id] = json_compatible_item_data diff --git a/docs_src/extra_data_types/tutorial001_py310.py b/docs_src/extra_data_types/tutorial001_py310.py new file mode 100644 index 000000000..4a33481b7 --- /dev/null +++ b/docs_src/extra_data_types/tutorial001_py310.py @@ -0,0 +1,27 @@ +from datetime import datetime, time, timedelta +from uuid import UUID + +from fastapi import Body, FastAPI + +app = FastAPI() + + +@app.put("/items/{item_id}") +async def read_items( + item_id: UUID, + start_datetime: datetime | None = Body(None), + end_datetime: datetime | None = Body(None), + repeat_at: time | None = Body(None), + process_after: timedelta | None = Body(None), +): + start_process = start_datetime + process_after + duration = end_datetime - start_process + return { + "item_id": item_id, + "start_datetime": start_datetime, + "end_datetime": end_datetime, + "repeat_at": repeat_at, + "process_after": process_after, + "start_process": start_process, + "duration": duration, + } diff --git a/docs_src/extra_models/tutorial001_py310.py b/docs_src/extra_models/tutorial001_py310.py new file mode 100644 index 000000000..669386ae6 --- /dev/null +++ b/docs_src/extra_models/tutorial001_py310.py @@ -0,0 +1,41 @@ +from fastapi import FastAPI +from pydantic import BaseModel, EmailStr + +app = FastAPI() + + +class UserIn(BaseModel): + username: str + password: str + email: EmailStr + full_name: str | None = None + + +class UserOut(BaseModel): + username: str + email: EmailStr + full_name: str | None = None + + +class UserInDB(BaseModel): + username: str + hashed_password: str + email: EmailStr + full_name: str | None = None + + +def fake_password_hasher(raw_password: str): + return "supersecret" + raw_password + + +def fake_save_user(user_in: UserIn): + hashed_password = fake_password_hasher(user_in.password) + user_in_db = UserInDB(**user_in.dict(), hashed_password=hashed_password) + print("User saved! ..not really") + return user_in_db + + +@app.post("/user/", response_model=UserOut) +async def create_user(user_in: UserIn): + user_saved = fake_save_user(user_in) + return user_saved diff --git a/docs_src/extra_models/tutorial002_py310.py b/docs_src/extra_models/tutorial002_py310.py new file mode 100644 index 000000000..5b8ed7de3 --- /dev/null +++ b/docs_src/extra_models/tutorial002_py310.py @@ -0,0 +1,39 @@ +from fastapi import FastAPI +from pydantic import BaseModel, EmailStr + +app = FastAPI() + + +class UserBase(BaseModel): + username: str + email: EmailStr + full_name: str | None = None + + +class UserIn(UserBase): + password: str + + +class UserOut(UserBase): + pass + + +class UserInDB(UserBase): + hashed_password: str + + +def fake_password_hasher(raw_password: str): + return "supersecret" + raw_password + + +def fake_save_user(user_in: UserIn): + hashed_password = fake_password_hasher(user_in.password) + user_in_db = UserInDB(**user_in.dict(), hashed_password=hashed_password) + print("User saved! ..not really") + return user_in_db + + +@app.post("/user/", response_model=UserOut) +async def create_user(user_in: UserIn): + user_saved = fake_save_user(user_in) + return user_saved diff --git a/docs_src/extra_models/tutorial003_py310.py b/docs_src/extra_models/tutorial003_py310.py new file mode 100644 index 000000000..065439acc --- /dev/null +++ b/docs_src/extra_models/tutorial003_py310.py @@ -0,0 +1,35 @@ +from typing import Union + +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class BaseItem(BaseModel): + description: str + type: str + + +class CarItem(BaseItem): + type = "car" + + +class PlaneItem(BaseItem): + type = "plane" + size: int + + +items = { + "item1": {"description": "All my friends drive a low rider", "type": "car"}, + "item2": { + "description": "Music is my aeroplane, it's my aeroplane", + "type": "plane", + "size": 5, + }, +} + + +@app.get("/items/{item_id}", response_model=Union[PlaneItem, CarItem]) +async def read_item(item_id: str): + return items[item_id] diff --git a/docs_src/extra_models/tutorial004_py39.py b/docs_src/extra_models/tutorial004_py39.py new file mode 100644 index 000000000..28cacde4d --- /dev/null +++ b/docs_src/extra_models/tutorial004_py39.py @@ -0,0 +1,20 @@ +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str + + +items = [ + {"name": "Foo", "description": "There comes my hero"}, + {"name": "Red", "description": "It's my aeroplane"}, +] + + +@app.get("/items/", response_model=list[Item]) +async def read_items(): + return items diff --git a/docs_src/extra_models/tutorial005_py39.py b/docs_src/extra_models/tutorial005_py39.py new file mode 100644 index 000000000..9da2a0a0f --- /dev/null +++ b/docs_src/extra_models/tutorial005_py39.py @@ -0,0 +1,8 @@ +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/keyword-weights/", response_model=dict[str, float]) +async def read_keyword_weights(): + return {"foo": 2.3, "bar": 3.4} diff --git a/docs_src/header_params/tutorial001_py310.py b/docs_src/header_params/tutorial001_py310.py new file mode 100644 index 000000000..b28463346 --- /dev/null +++ b/docs_src/header_params/tutorial001_py310.py @@ -0,0 +1,8 @@ +from fastapi import FastAPI, Header + +app = FastAPI() + + +@app.get("/items/") +async def read_items(user_agent: str | None = Header(None)): + return {"User-Agent": user_agent} diff --git a/docs_src/header_params/tutorial002_py310.py b/docs_src/header_params/tutorial002_py310.py new file mode 100644 index 000000000..98ab5a807 --- /dev/null +++ b/docs_src/header_params/tutorial002_py310.py @@ -0,0 +1,10 @@ +from fastapi import FastAPI, Header + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + strange_header: str | None = Header(None, convert_underscores=False) +): + return {"strange_header": strange_header} diff --git a/docs_src/header_params/tutorial003_py310.py b/docs_src/header_params/tutorial003_py310.py new file mode 100644 index 000000000..2dac2c13c --- /dev/null +++ b/docs_src/header_params/tutorial003_py310.py @@ -0,0 +1,8 @@ +from fastapi import FastAPI, Header + +app = FastAPI() + + +@app.get("/items/") +async def read_items(x_token: list[str] | None = Header(None)): + return {"X-Token values": x_token} diff --git a/docs_src/header_params/tutorial003_py39.py b/docs_src/header_params/tutorial003_py39.py new file mode 100644 index 000000000..359766527 --- /dev/null +++ b/docs_src/header_params/tutorial003_py39.py @@ -0,0 +1,10 @@ +from typing import Optional + +from fastapi import FastAPI, Header + +app = FastAPI() + + +@app.get("/items/") +async def read_items(x_token: Optional[list[str]] = Header(None)): + return {"X-Token values": x_token} diff --git a/docs_src/path_operation_configuration/tutorial001.py b/docs_src/path_operation_configuration/tutorial001.py index 66ea442ea..1316d9237 100644 --- a/docs_src/path_operation_configuration/tutorial001.py +++ b/docs_src/path_operation_configuration/tutorial001.py @@ -11,7 +11,7 @@ class Item(BaseModel): description: Optional[str] = None price: float tax: Optional[float] = None - tags: Set[str] = [] + tags: Set[str] = set() @app.post("/items/", response_model=Item, status_code=status.HTTP_201_CREATED) diff --git a/docs_src/path_operation_configuration/tutorial001_py310.py b/docs_src/path_operation_configuration/tutorial001_py310.py new file mode 100644 index 000000000..da078fdf5 --- /dev/null +++ b/docs_src/path_operation_configuration/tutorial001_py310.py @@ -0,0 +1,17 @@ +from fastapi import FastAPI, status +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + tags: set[str] = set() + + +@app.post("/items/", response_model=Item, status_code=status.HTTP_201_CREATED) +async def create_item(item: Item): + return item diff --git a/docs_src/path_operation_configuration/tutorial001_py39.py b/docs_src/path_operation_configuration/tutorial001_py39.py new file mode 100644 index 000000000..5c04d8bac --- /dev/null +++ b/docs_src/path_operation_configuration/tutorial001_py39.py @@ -0,0 +1,19 @@ +from typing import Optional + +from fastapi import FastAPI, status +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Optional[str] = None + price: float + tax: Optional[float] = None + tags: set[str] = set() + + +@app.post("/items/", response_model=Item, status_code=status.HTTP_201_CREATED) +async def create_item(item: Item): + return item diff --git a/docs_src/path_operation_configuration/tutorial002.py b/docs_src/path_operation_configuration/tutorial002.py index 01df041b8..2df537d86 100644 --- a/docs_src/path_operation_configuration/tutorial002.py +++ b/docs_src/path_operation_configuration/tutorial002.py @@ -11,7 +11,7 @@ class Item(BaseModel): description: Optional[str] = None price: float tax: Optional[float] = None - tags: Set[str] = [] + tags: Set[str] = set() @app.post("/items/", response_model=Item, tags=["items"]) diff --git a/docs_src/path_operation_configuration/tutorial002_py310.py b/docs_src/path_operation_configuration/tutorial002_py310.py new file mode 100644 index 000000000..9a8af5432 --- /dev/null +++ b/docs_src/path_operation_configuration/tutorial002_py310.py @@ -0,0 +1,27 @@ +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 + tags: set[str] = set() + + +@app.post("/items/", response_model=Item, tags=["items"]) +async def create_item(item: Item): + return item + + +@app.get("/items/", tags=["items"]) +async def read_items(): + return [{"name": "Foo", "price": 42}] + + +@app.get("/users/", tags=["users"]) +async def read_users(): + return [{"username": "johndoe"}] diff --git a/docs_src/path_operation_configuration/tutorial002_py39.py b/docs_src/path_operation_configuration/tutorial002_py39.py new file mode 100644 index 000000000..766d9fb0b --- /dev/null +++ b/docs_src/path_operation_configuration/tutorial002_py39.py @@ -0,0 +1,29 @@ +from typing import Optional + +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Optional[str] = None + price: float + tax: Optional[float] = None + tags: set[str] = set() + + +@app.post("/items/", response_model=Item, tags=["items"]) +async def create_item(item: Item): + return item + + +@app.get("/items/", tags=["items"]) +async def read_items(): + return [{"name": "Foo", "price": 42}] + + +@app.get("/users/", tags=["users"]) +async def read_users(): + return [{"username": "johndoe"}] diff --git a/docs_src/path_operation_configuration/tutorial003.py b/docs_src/path_operation_configuration/tutorial003.py index 29bcb4a22..269a1a253 100644 --- a/docs_src/path_operation_configuration/tutorial003.py +++ b/docs_src/path_operation_configuration/tutorial003.py @@ -11,7 +11,7 @@ class Item(BaseModel): description: Optional[str] = None price: float tax: Optional[float] = None - tags: Set[str] = [] + tags: Set[str] = set() @app.post( diff --git a/docs_src/path_operation_configuration/tutorial003_py310.py b/docs_src/path_operation_configuration/tutorial003_py310.py new file mode 100644 index 000000000..3d94afe2c --- /dev/null +++ b/docs_src/path_operation_configuration/tutorial003_py310.py @@ -0,0 +1,22 @@ +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 + tags: set[str] = set() + + +@app.post( + "/items/", + response_model=Item, + summary="Create an item", + description="Create an item with all the information, name, description, price, tax and a set of unique tags", +) +async def create_item(item: Item): + return item diff --git a/docs_src/path_operation_configuration/tutorial003_py39.py b/docs_src/path_operation_configuration/tutorial003_py39.py new file mode 100644 index 000000000..446198b5c --- /dev/null +++ b/docs_src/path_operation_configuration/tutorial003_py39.py @@ -0,0 +1,24 @@ +from typing import Optional + +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Optional[str] = None + price: float + tax: Optional[float] = None + tags: set[str] = set() + + +@app.post( + "/items/", + response_model=Item, + summary="Create an item", + description="Create an item with all the information, name, description, price, tax and a set of unique tags", +) +async def create_item(item: Item): + return item diff --git a/docs_src/path_operation_configuration/tutorial004.py b/docs_src/path_operation_configuration/tutorial004.py index ac95cc4bb..de83be836 100644 --- a/docs_src/path_operation_configuration/tutorial004.py +++ b/docs_src/path_operation_configuration/tutorial004.py @@ -11,7 +11,7 @@ class Item(BaseModel): description: Optional[str] = None price: float tax: Optional[float] = None - tags: Set[str] = [] + tags: Set[str] = set() @app.post("/items/", response_model=Item, summary="Create an item") diff --git a/docs_src/path_operation_configuration/tutorial004_py310.py b/docs_src/path_operation_configuration/tutorial004_py310.py new file mode 100644 index 000000000..4cb8bdd43 --- /dev/null +++ b/docs_src/path_operation_configuration/tutorial004_py310.py @@ -0,0 +1,26 @@ +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 + tags: set[str] = set() + + +@app.post("/items/", response_model=Item, summary="Create an item") +async def create_item(item: Item): + """ + Create an item with all the information: + + - **name**: each item must have a name + - **description**: a long description + - **price**: required + - **tax**: if the item doesn't have tax, you can omit this + - **tags**: a set of unique tag strings for this item + """ + return item diff --git a/docs_src/path_operation_configuration/tutorial004_py39.py b/docs_src/path_operation_configuration/tutorial004_py39.py new file mode 100644 index 000000000..bf6005b95 --- /dev/null +++ b/docs_src/path_operation_configuration/tutorial004_py39.py @@ -0,0 +1,28 @@ +from typing import Optional + +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Optional[str] = None + price: float + tax: Optional[float] = None + tags: set[str] = set() + + +@app.post("/items/", response_model=Item, summary="Create an item") +async def create_item(item: Item): + """ + Create an item with all the information: + + - **name**: each item must have a name + - **description**: a long description + - **price**: required + - **tax**: if the item doesn't have tax, you can omit this + - **tags**: a set of unique tag strings for this item + """ + return item diff --git a/docs_src/path_operation_configuration/tutorial005.py b/docs_src/path_operation_configuration/tutorial005.py index c1b809887..0f62c3814 100644 --- a/docs_src/path_operation_configuration/tutorial005.py +++ b/docs_src/path_operation_configuration/tutorial005.py @@ -11,7 +11,7 @@ class Item(BaseModel): description: Optional[str] = None price: float tax: Optional[float] = None - tags: Set[str] = [] + tags: Set[str] = set() @app.post( diff --git a/docs_src/path_operation_configuration/tutorial005_py310.py b/docs_src/path_operation_configuration/tutorial005_py310.py new file mode 100644 index 000000000..b176631d8 --- /dev/null +++ b/docs_src/path_operation_configuration/tutorial005_py310.py @@ -0,0 +1,31 @@ +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 + tags: set[str] = set() + + +@app.post( + "/items/", + response_model=Item, + summary="Create an item", + response_description="The created item", +) +async def create_item(item: Item): + """ + Create an item with all the information: + + - **name**: each item must have a name + - **description**: a long description + - **price**: required + - **tax**: if the item doesn't have tax, you can omit this + - **tags**: a set of unique tag strings for this item + """ + return item diff --git a/docs_src/path_operation_configuration/tutorial005_py39.py b/docs_src/path_operation_configuration/tutorial005_py39.py new file mode 100644 index 000000000..5ef320405 --- /dev/null +++ b/docs_src/path_operation_configuration/tutorial005_py39.py @@ -0,0 +1,33 @@ +from typing import Optional + +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Optional[str] = None + price: float + tax: Optional[float] = None + tags: set[str] = set() + + +@app.post( + "/items/", + response_model=Item, + summary="Create an item", + response_description="The created item", +) +async def create_item(item: Item): + """ + Create an item with all the information: + + - **name**: each item must have a name + - **description**: a long description + - **price**: required + - **tax**: if the item doesn't have tax, you can omit this + - **tags**: a set of unique tag strings for this item + """ + return item diff --git a/docs_src/path_params_numeric_validations/tutorial001_py310.py b/docs_src/path_params_numeric_validations/tutorial001_py310.py new file mode 100644 index 000000000..b940a0949 --- /dev/null +++ b/docs_src/path_params_numeric_validations/tutorial001_py310.py @@ -0,0 +1,14 @@ +from fastapi import FastAPI, Path, Query + +app = FastAPI() + + +@app.get("/items/{item_id}") +async def read_items( + item_id: int = Path(..., title="The ID of the item to get"), + q: str | None = Query(None, alias="item-query"), +): + results = {"item_id": item_id} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/python_types/tutorial006_py39.py b/docs_src/python_types/tutorial006_py39.py new file mode 100644 index 000000000..486b67caf --- /dev/null +++ b/docs_src/python_types/tutorial006_py39.py @@ -0,0 +1,3 @@ +def process_items(items: list[str]): + for item in items: + print(item) diff --git a/docs_src/python_types/tutorial007_py39.py b/docs_src/python_types/tutorial007_py39.py new file mode 100644 index 000000000..ea96c7964 --- /dev/null +++ b/docs_src/python_types/tutorial007_py39.py @@ -0,0 +1,2 @@ +def process_items(items_t: tuple[int, int, str], items_s: set[bytes]): + return items_t, items_s diff --git a/docs_src/python_types/tutorial008.py b/docs_src/python_types/tutorial008.py index 9fb1043bb..a393385b0 100644 --- a/docs_src/python_types/tutorial008.py +++ b/docs_src/python_types/tutorial008.py @@ -1,7 +1,4 @@ -from typing import Dict - - -def process_items(prices: Dict[str, float]): +def process_items(prices: dict[str, float]): for item_name, item_price in prices.items(): print(item_name) print(item_price) diff --git a/docs_src/python_types/tutorial008b.py b/docs_src/python_types/tutorial008b.py new file mode 100644 index 000000000..e52539ead --- /dev/null +++ b/docs_src/python_types/tutorial008b.py @@ -0,0 +1,5 @@ +from typing import Union + + +def process_item(item: Union[int, str]): + print(item) diff --git a/docs_src/python_types/tutorial008b_py310.py b/docs_src/python_types/tutorial008b_py310.py new file mode 100644 index 000000000..6bb437841 --- /dev/null +++ b/docs_src/python_types/tutorial008b_py310.py @@ -0,0 +1,2 @@ +def process_item(item: int | str): + print(item) diff --git a/docs_src/python_types/tutorial009_py310.py b/docs_src/python_types/tutorial009_py310.py new file mode 100644 index 000000000..8464c6ff0 --- /dev/null +++ b/docs_src/python_types/tutorial009_py310.py @@ -0,0 +1,5 @@ +def say_hi(name: str | None = None): + if name is not None: + print(f"Hey {name}!") + else: + print("Hello World") diff --git a/docs_src/python_types/tutorial009b.py b/docs_src/python_types/tutorial009b.py new file mode 100644 index 000000000..9f1a05bc0 --- /dev/null +++ b/docs_src/python_types/tutorial009b.py @@ -0,0 +1,8 @@ +from typing import Union + + +def say_hi(name: Union[str, None] = None): + if name is not None: + print(f"Hey {name}!") + else: + print("Hello World") diff --git a/docs_src/python_types/tutorial011_py310.py b/docs_src/python_types/tutorial011_py310.py new file mode 100644 index 000000000..7f173880f --- /dev/null +++ b/docs_src/python_types/tutorial011_py310.py @@ -0,0 +1,22 @@ +from datetime import datetime + +from pydantic import BaseModel + + +class User(BaseModel): + id: int + name = "John Doe" + signup_ts: datetime | None = None + friends: list[int] = [] + + +external_data = { + "id": "123", + "signup_ts": "2017-06-01 12:22", + "friends": [1, "2", b"3"], +} +user = User(**external_data) +print(user) +# > User id=123 name='John Doe' signup_ts=datetime.datetime(2017, 6, 1, 12, 22) friends=[1, 2, 3] +print(user.id) +# > 123 diff --git a/docs_src/python_types/tutorial011_py39.py b/docs_src/python_types/tutorial011_py39.py new file mode 100644 index 000000000..af79e2df0 --- /dev/null +++ b/docs_src/python_types/tutorial011_py39.py @@ -0,0 +1,23 @@ +from datetime import datetime +from typing import Optional + +from pydantic import BaseModel + + +class User(BaseModel): + id: int + name = "John Doe" + signup_ts: Optional[datetime] = None + friends: list[int] = [] + + +external_data = { + "id": "123", + "signup_ts": "2017-06-01 12:22", + "friends": [1, "2", b"3"], +} +user = User(**external_data) +print(user) +# > User id=123 name='John Doe' signup_ts=datetime.datetime(2017, 6, 1, 12, 22) friends=[1, 2, 3] +print(user.id) +# > 123 diff --git a/docs_src/query_params/tutorial002_py310.py b/docs_src/query_params/tutorial002_py310.py new file mode 100644 index 000000000..bedb58ce8 --- /dev/null +++ b/docs_src/query_params/tutorial002_py310.py @@ -0,0 +1,10 @@ +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/items/{item_id}") +async def read_item(item_id: str, q: str | None = None): + if q: + return {"item_id": item_id, "q": q} + return {"item_id": item_id} diff --git a/docs_src/query_params/tutorial003_py310.py b/docs_src/query_params/tutorial003_py310.py new file mode 100644 index 000000000..1eec66d90 --- /dev/null +++ b/docs_src/query_params/tutorial003_py310.py @@ -0,0 +1,15 @@ +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/items/{item_id}") +async def read_item(item_id: str, q: str | None = None, short: bool = False): + item = {"item_id": item_id} + if q: + item.update({"q": q}) + if not short: + item.update( + {"description": "This is an amazing item that has a long description"} + ) + return item diff --git a/docs_src/query_params/tutorial004_py310.py b/docs_src/query_params/tutorial004_py310.py new file mode 100644 index 000000000..8ec4338df --- /dev/null +++ b/docs_src/query_params/tutorial004_py310.py @@ -0,0 +1,17 @@ +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/users/{user_id}/items/{item_id}") +async def read_user_item( + user_id: int, item_id: str, q: str | None = None, short: bool = False +): + item = {"item_id": item_id, "owner_id": user_id} + if q: + item.update({"q": q}) + if not short: + item.update( + {"description": "This is an amazing item that has a long description"} + ) + return item diff --git a/docs_src/query_params/tutorial006_py310.py b/docs_src/query_params/tutorial006_py310.py new file mode 100644 index 000000000..be2a44c31 --- /dev/null +++ b/docs_src/query_params/tutorial006_py310.py @@ -0,0 +1,11 @@ +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/items/{item_id}") +async def read_user_item( + item_id: str, needy: str, skip: int = 0, limit: int | None = None +): + item = {"item_id": item_id, "needy": needy, "skip": skip, "limit": limit} + return item diff --git a/docs_src/query_params/tutorial006b.py b/docs_src/query_params/tutorial006b.py new file mode 100644 index 000000000..f0dbfe08f --- /dev/null +++ b/docs_src/query_params/tutorial006b.py @@ -0,0 +1,13 @@ +from typing import Union + +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/items/{item_id}") +async def read_user_item( + item_id: str, needy: str, skip: int = 0, limit: Union[int, None] = None +): + item = {"item_id": item_id, "needy": needy, "skip": skip, "limit": limit} + return item diff --git a/docs_src/query_params_str_validations/tutorial001_py310.py b/docs_src/query_params_str_validations/tutorial001_py310.py new file mode 100644 index 000000000..eeefddfa7 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial001_py310.py @@ -0,0 +1,11 @@ +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: str | None = 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/tutorial002_py310.py b/docs_src/query_params_str_validations/tutorial002_py310.py new file mode 100644 index 000000000..fa3139d5a --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial002_py310.py @@ -0,0 +1,11 @@ +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: str | None = Query(None, max_length=50)): + 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/tutorial003_py310.py b/docs_src/query_params_str_validations/tutorial003_py310.py new file mode 100644 index 000000000..335858a40 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial003_py310.py @@ -0,0 +1,11 @@ +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: str | None = Query(None, min_length=3, max_length=50)): + 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_py310.py b/docs_src/query_params_str_validations/tutorial004_py310.py new file mode 100644 index 000000000..518b779f7 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial004_py310.py @@ -0,0 +1,13 @@ +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: str | None = Query(None, min_length=3, max_length=50, regex="^fixedquery$") +): + 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/tutorial007_py310.py b/docs_src/query_params_str_validations/tutorial007_py310.py new file mode 100644 index 000000000..14ef4cb69 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial007_py310.py @@ -0,0 +1,11 @@ +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: str | None = Query(None, title="Query string", min_length=3)): + 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/tutorial008_py310.py b/docs_src/query_params_str_validations/tutorial008_py310.py new file mode 100644 index 000000000..06bb02442 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial008_py310.py @@ -0,0 +1,19 @@ +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: str + | None = Query( + None, + title="Query string", + description="Query string for the items to search in the database that have a good match", + min_length=3, + ) +): + 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/tutorial009_py310.py b/docs_src/query_params_str_validations/tutorial009_py310.py new file mode 100644 index 000000000..e84c116f1 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial009_py310.py @@ -0,0 +1,11 @@ +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: str | None = Query(None, alias="item-query")): + 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/tutorial010_py310.py b/docs_src/query_params_str_validations/tutorial010_py310.py new file mode 100644 index 000000000..c35800858 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial010_py310.py @@ -0,0 +1,23 @@ +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: str + | None = Query( + None, + alias="item-query", + title="Query string", + description="Query string for the items to search in the database that have a good match", + min_length=3, + max_length=50, + regex="^fixedquery$", + deprecated=True, + ) +): + 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/tutorial011_py310.py b/docs_src/query_params_str_validations/tutorial011_py310.py new file mode 100644 index 000000000..c3d992e62 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial011_py310.py @@ -0,0 +1,9 @@ +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: list[str] | None = Query(None)): + query_items = {"q": q} + return query_items diff --git a/docs_src/query_params_str_validations/tutorial011_py39.py b/docs_src/query_params_str_validations/tutorial011_py39.py new file mode 100644 index 000000000..38ba764d6 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial011_py39.py @@ -0,0 +1,11 @@ +from typing import Optional + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Optional[list[str]] = Query(None)): + query_items = {"q": q} + return query_items diff --git a/docs_src/query_params_str_validations/tutorial012_py39.py b/docs_src/query_params_str_validations/tutorial012_py39.py new file mode 100644 index 000000000..1900133d9 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial012_py39.py @@ -0,0 +1,9 @@ +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: list[str] = Query(["foo", "bar"])): + query_items = {"q": q} + return query_items diff --git a/docs_src/request_files/tutorial002_py39.py b/docs_src/request_files/tutorial002_py39.py new file mode 100644 index 000000000..26cd56769 --- /dev/null +++ b/docs_src/request_files/tutorial002_py39.py @@ -0,0 +1,31 @@ +from fastapi import FastAPI, File, UploadFile +from fastapi.responses import HTMLResponse + +app = FastAPI() + + +@app.post("/files/") +async def create_files(files: list[bytes] = File(...)): + return {"file_sizes": [len(file) for file in files]} + + +@app.post("/uploadfiles/") +async def create_upload_files(files: list[UploadFile] = File(...)): + return {"filenames": [file.filename for file in files]} + + +@app.get("/") +async def main(): + content = """ + +
+ + +
+
+ + +
+ + """ + return HTMLResponse(content=content) diff --git a/docs_src/response_model/tutorial001_py310.py b/docs_src/response_model/tutorial001_py310.py new file mode 100644 index 000000000..59efecde4 --- /dev/null +++ b/docs_src/response_model/tutorial001_py310.py @@ -0,0 +1,17 @@ +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 + tags: list[str] = [] + + +@app.post("/items/", response_model=Item) +async def create_item(item: Item): + return item diff --git a/docs_src/response_model/tutorial001_py39.py b/docs_src/response_model/tutorial001_py39.py new file mode 100644 index 000000000..37b866864 --- /dev/null +++ b/docs_src/response_model/tutorial001_py39.py @@ -0,0 +1,19 @@ +from typing import Optional + +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Optional[str] = None + price: float + tax: Optional[float] = None + tags: list[str] = [] + + +@app.post("/items/", response_model=Item) +async def create_item(item: Item): + return item diff --git a/docs_src/response_model/tutorial002_py310.py b/docs_src/response_model/tutorial002_py310.py new file mode 100644 index 000000000..29ab9c9d2 --- /dev/null +++ b/docs_src/response_model/tutorial002_py310.py @@ -0,0 +1,17 @@ +from fastapi import FastAPI +from pydantic import BaseModel, EmailStr + +app = FastAPI() + + +class UserIn(BaseModel): + username: str + password: str + email: EmailStr + full_name: str | None = None + + +# Don't do this in production! +@app.post("/user/", response_model=UserIn) +async def create_user(user: UserIn): + return user diff --git a/docs_src/response_model/tutorial003_py310.py b/docs_src/response_model/tutorial003_py310.py new file mode 100644 index 000000000..fc9693e3c --- /dev/null +++ b/docs_src/response_model/tutorial003_py310.py @@ -0,0 +1,22 @@ +from fastapi import FastAPI +from pydantic import BaseModel, EmailStr + +app = FastAPI() + + +class UserIn(BaseModel): + username: str + password: str + email: EmailStr + full_name: str | None = None + + +class UserOut(BaseModel): + username: str + email: EmailStr + full_name: str | None = None + + +@app.post("/user/", response_model=UserOut) +async def create_user(user: UserIn): + return user diff --git a/docs_src/response_model/tutorial004_py310.py b/docs_src/response_model/tutorial004_py310.py new file mode 100644 index 000000000..a3e21b434 --- /dev/null +++ b/docs_src/response_model/tutorial004_py310.py @@ -0,0 +1,24 @@ +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float = 10.5 + tags: list[str] = [] + + +items = { + "foo": {"name": "Foo", "price": 50.2}, + "bar": {"name": "Bar", "description": "The bartenders", "price": 62, "tax": 20.2}, + "baz": {"name": "Baz", "description": None, "price": 50.2, "tax": 10.5, "tags": []}, +} + + +@app.get("/items/{item_id}", response_model=Item, response_model_exclude_unset=True) +async def read_item(item_id: str): + return items[item_id] diff --git a/docs_src/response_model/tutorial004_py39.py b/docs_src/response_model/tutorial004_py39.py new file mode 100644 index 000000000..07ccbbf41 --- /dev/null +++ b/docs_src/response_model/tutorial004_py39.py @@ -0,0 +1,26 @@ +from typing import Optional + +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Optional[str] = None + price: float + tax: float = 10.5 + tags: list[str] = [] + + +items = { + "foo": {"name": "Foo", "price": 50.2}, + "bar": {"name": "Bar", "description": "The bartenders", "price": 62, "tax": 20.2}, + "baz": {"name": "Baz", "description": None, "price": 50.2, "tax": 10.5, "tags": []}, +} + + +@app.get("/items/{item_id}", response_model=Item, response_model_exclude_unset=True) +async def read_item(item_id: str): + return items[item_id] diff --git a/docs_src/response_model/tutorial005_py310.py b/docs_src/response_model/tutorial005_py310.py new file mode 100644 index 000000000..acb3035b9 --- /dev/null +++ b/docs_src/response_model/tutorial005_py310.py @@ -0,0 +1,37 @@ +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float = 10.5 + + +items = { + "foo": {"name": "Foo", "price": 50.2}, + "bar": {"name": "Bar", "description": "The Bar fighters", "price": 62, "tax": 20.2}, + "baz": { + "name": "Baz", + "description": "There goes my baz", + "price": 50.2, + "tax": 10.5, + }, +} + + +@app.get( + "/items/{item_id}/name", + response_model=Item, + response_model_include={"name", "description"}, +) +async def read_item_name(item_id: str): + return items[item_id] + + +@app.get("/items/{item_id}/public", response_model=Item, response_model_exclude={"tax"}) +async def read_item_public_data(item_id: str): + return items[item_id] diff --git a/docs_src/response_model/tutorial006_py310.py b/docs_src/response_model/tutorial006_py310.py new file mode 100644 index 000000000..9ccec324e --- /dev/null +++ b/docs_src/response_model/tutorial006_py310.py @@ -0,0 +1,37 @@ +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float = 10.5 + + +items = { + "foo": {"name": "Foo", "price": 50.2}, + "bar": {"name": "Bar", "description": "The Bar fighters", "price": 62, "tax": 20.2}, + "baz": { + "name": "Baz", + "description": "There goes my baz", + "price": 50.2, + "tax": 10.5, + }, +} + + +@app.get( + "/items/{item_id}/name", + response_model=Item, + response_model_include=["name", "description"], +) +async def read_item_name(item_id: str): + return items[item_id] + + +@app.get("/items/{item_id}/public", response_model=Item, response_model_exclude=["tax"]) +async def read_item_public_data(item_id: str): + return items[item_id] diff --git a/docs_src/schema_extra_example/tutorial001_py310.py b/docs_src/schema_extra_example/tutorial001_py310.py new file mode 100644 index 000000000..77ceedd60 --- /dev/null +++ b/docs_src/schema_extra_example/tutorial001_py310.py @@ -0,0 +1,27 @@ +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 = { + "example": { + "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/tutorial002_py310.py b/docs_src/schema_extra_example/tutorial002_py310.py new file mode 100644 index 000000000..4f8f8304e --- /dev/null +++ b/docs_src/schema_extra_example/tutorial002_py310.py @@ -0,0 +1,17 @@ +from fastapi import FastAPI +from pydantic import BaseModel, Field + +app = FastAPI() + + +class Item(BaseModel): + name: str = Field(..., example="Foo") + description: str | None = Field(None, example="A very nice Item") + price: float = Field(..., example=35.4) + tax: float | None = Field(None, example=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/tutorial003_py310.py b/docs_src/schema_extra_example/tutorial003_py310.py new file mode 100644 index 000000000..cf4c99dc0 --- /dev/null +++ b/docs_src/schema_extra_example/tutorial003_py310.py @@ -0,0 +1,28 @@ +from fastapi import Body, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + + +@app.put("/items/{item_id}") +async def update_item( + item_id: int, + item: Item = Body( + ..., + example={ + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + ), +): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/schema_extra_example/tutorial004_py310.py b/docs_src/schema_extra_example/tutorial004_py310.py new file mode 100644 index 000000000..6f29c1a5c --- /dev/null +++ b/docs_src/schema_extra_example/tutorial004_py310.py @@ -0,0 +1,50 @@ +from fastapi import Body, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + + +@app.put("/items/{item_id}") +async def update_item( + *, + item_id: int, + item: Item = Body( + ..., + 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, + }, + }, + "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", + }, + }, + }, + ), +): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/security/tutorial002_py310.py b/docs_src/security/tutorial002_py310.py new file mode 100644 index 000000000..3c2018f54 --- /dev/null +++ b/docs_src/security/tutorial002_py310.py @@ -0,0 +1,30 @@ +from fastapi import Depends, FastAPI +from fastapi.security import OAuth2PasswordBearer +from pydantic import BaseModel + +app = FastAPI() + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + + +class User(BaseModel): + username: str + email: str | None = None + full_name: str | None = None + disabled: bool | None = None + + +def fake_decode_token(token): + return User( + username=token + "fakedecoded", email="john@example.com", full_name="John Doe" + ) + + +async def get_current_user(token: str = Depends(oauth2_scheme)): + user = fake_decode_token(token) + return user + + +@app.get("/users/me") +async def read_users_me(current_user: User = Depends(get_current_user)): + return current_user diff --git a/docs_src/security/tutorial003_py310.py b/docs_src/security/tutorial003_py310.py new file mode 100644 index 000000000..af935e997 --- /dev/null +++ b/docs_src/security/tutorial003_py310.py @@ -0,0 +1,88 @@ +from fastapi import Depends, FastAPI, HTTPException, status +from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm +from pydantic import BaseModel + +fake_users_db = { + "johndoe": { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "hashed_password": "fakehashedsecret", + "disabled": False, + }, + "alice": { + "username": "alice", + "full_name": "Alice Wonderson", + "email": "alice@example.com", + "hashed_password": "fakehashedsecret2", + "disabled": True, + }, +} + +app = FastAPI() + + +def fake_hash_password(password: str): + return "fakehashed" + password + + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + + +class User(BaseModel): + username: str + email: str | None = None + full_name: str | None = None + disabled: bool | None = None + + +class UserInDB(User): + hashed_password: str + + +def get_user(db, username: str): + if username in db: + user_dict = db[username] + return UserInDB(**user_dict) + + +def fake_decode_token(token): + # This doesn't provide any security at all + # Check the next version + user = get_user(fake_users_db, token) + return user + + +async def get_current_user(token: str = Depends(oauth2_scheme)): + user = fake_decode_token(token) + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid authentication credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + return user + + +async def get_current_active_user(current_user: User = Depends(get_current_user)): + if current_user.disabled: + raise HTTPException(status_code=400, detail="Inactive user") + return current_user + + +@app.post("/token") +async def login(form_data: OAuth2PasswordRequestForm = Depends()): + user_dict = fake_users_db.get(form_data.username) + if not user_dict: + raise HTTPException(status_code=400, detail="Incorrect username or password") + user = UserInDB(**user_dict) + hashed_password = fake_hash_password(form_data.password) + if not hashed_password == user.hashed_password: + raise HTTPException(status_code=400, detail="Incorrect username or password") + + return {"access_token": user.username, "token_type": "bearer"} + + +@app.get("/users/me") +async def read_users_me(current_user: User = Depends(get_current_active_user)): + return current_user diff --git a/docs_src/security/tutorial004_py310.py b/docs_src/security/tutorial004_py310.py new file mode 100644 index 000000000..797d56d04 --- /dev/null +++ b/docs_src/security/tutorial004_py310.py @@ -0,0 +1,137 @@ +from datetime import datetime, timedelta + +from fastapi import Depends, FastAPI, HTTPException, status +from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm +from jose import JWTError, jwt +from passlib.context import CryptContext +from pydantic import BaseModel + +# to get a string like this run: +# openssl rand -hex 32 +SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7" +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 30 + + +fake_users_db = { + "johndoe": { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW", + "disabled": False, + } +} + + +class Token(BaseModel): + access_token: str + token_type: str + + +class TokenData(BaseModel): + username: str | None = None + + +class User(BaseModel): + username: str + email: str | None = None + full_name: str | None = None + disabled: bool | None = None + + +class UserInDB(User): + hashed_password: str + + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + +app = FastAPI() + + +def verify_password(plain_password, hashed_password): + return pwd_context.verify(plain_password, hashed_password) + + +def get_password_hash(password): + return pwd_context.hash(password) + + +def get_user(db, username: str): + if username in db: + user_dict = db[username] + return UserInDB(**user_dict) + + +def authenticate_user(fake_db, username: str, password: str): + user = get_user(fake_db, username) + if not user: + return False + if not verify_password(password, user.hashed_password): + return False + return user + + +def create_access_token(data: dict, expires_delta: timedelta | None = None): + to_encode = data.copy() + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(minutes=15) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt + + +async def get_current_user(token: str = Depends(oauth2_scheme)): + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + username: str = payload.get("sub") + if username is None: + raise credentials_exception + token_data = TokenData(username=username) + except JWTError: + raise credentials_exception + user = get_user(fake_users_db, username=token_data.username) + if user is None: + raise credentials_exception + return user + + +async def get_current_active_user(current_user: User = Depends(get_current_user)): + if current_user.disabled: + raise HTTPException(status_code=400, detail="Inactive user") + return current_user + + +@app.post("/token", response_model=Token) +async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()): + user = authenticate_user(fake_users_db, form_data.username, form_data.password) + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect username or password", + headers={"WWW-Authenticate": "Bearer"}, + ) + access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + access_token = create_access_token( + data={"sub": user.username}, expires_delta=access_token_expires + ) + return {"access_token": access_token, "token_type": "bearer"} + + +@app.get("/users/me/", response_model=User) +async def read_users_me(current_user: User = Depends(get_current_active_user)): + return current_user + + +@app.get("/users/me/items/") +async def read_own_items(current_user: User = Depends(get_current_active_user)): + return [{"item_id": "Foo", "owner": current_user.username}] diff --git a/docs_src/security/tutorial005_py310.py b/docs_src/security/tutorial005_py310.py new file mode 100644 index 000000000..c6a095d2c --- /dev/null +++ b/docs_src/security/tutorial005_py310.py @@ -0,0 +1,172 @@ +from datetime import datetime, timedelta + +from fastapi import Depends, FastAPI, HTTPException, Security, status +from fastapi.security import ( + OAuth2PasswordBearer, + OAuth2PasswordRequestForm, + SecurityScopes, +) +from jose import JWTError, jwt +from passlib.context import CryptContext +from pydantic import BaseModel, ValidationError + +# to get a string like this run: +# openssl rand -hex 32 +SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7" +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 30 + + +fake_users_db = { + "johndoe": { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW", + "disabled": False, + }, + "alice": { + "username": "alice", + "full_name": "Alice Chains", + "email": "alicechains@example.com", + "hashed_password": "$2b$12$gSvqqUPvlXP2tfVFaWK1Be7DlH.PKZbv5H8KnzzVgXXbVxpva.pFm", + "disabled": True, + }, +} + + +class Token(BaseModel): + access_token: str + token_type: str + + +class TokenData(BaseModel): + username: str | None = None + scopes: list[str] = [] + + +class User(BaseModel): + username: str + email: str | None = None + full_name: str | None = None + disabled: bool | None = None + + +class UserInDB(User): + hashed_password: str + + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +oauth2_scheme = OAuth2PasswordBearer( + tokenUrl="token", + scopes={"me": "Read information about the current user.", "items": "Read items."}, +) + +app = FastAPI() + + +def verify_password(plain_password, hashed_password): + return pwd_context.verify(plain_password, hashed_password) + + +def get_password_hash(password): + return pwd_context.hash(password) + + +def get_user(db, username: str): + if username in db: + user_dict = db[username] + return UserInDB(**user_dict) + + +def authenticate_user(fake_db, username: str, password: str): + user = get_user(fake_db, username) + if not user: + return False + if not verify_password(password, user.hashed_password): + return False + return user + + +def create_access_token(data: dict, expires_delta: timedelta | None = None): + to_encode = data.copy() + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(minutes=15) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt + + +async def get_current_user( + security_scopes: SecurityScopes, token: str = Depends(oauth2_scheme) +): + if security_scopes.scopes: + authenticate_value = f'Bearer scope="{security_scopes.scope_str}"' + else: + authenticate_value = f"Bearer" + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": authenticate_value}, + ) + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + username: str = payload.get("sub") + if username is None: + raise credentials_exception + token_scopes = payload.get("scopes", []) + token_data = TokenData(scopes=token_scopes, username=username) + except (JWTError, ValidationError): + raise credentials_exception + user = get_user(fake_users_db, username=token_data.username) + if user is None: + raise credentials_exception + for scope in security_scopes.scopes: + if scope not in token_data.scopes: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Not enough permissions", + headers={"WWW-Authenticate": authenticate_value}, + ) + return user + + +async def get_current_active_user( + current_user: User = Security(get_current_user, scopes=["me"]) +): + if current_user.disabled: + raise HTTPException(status_code=400, detail="Inactive user") + return current_user + + +@app.post("/token", response_model=Token) +async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()): + user = authenticate_user(fake_users_db, form_data.username, form_data.password) + if not user: + raise HTTPException(status_code=400, detail="Incorrect username or password") + access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + access_token = create_access_token( + data={"sub": user.username, "scopes": form_data.scopes}, + expires_delta=access_token_expires, + ) + return {"access_token": access_token, "token_type": "bearer"} + + +@app.get("/users/me/", response_model=User) +async def read_users_me(current_user: User = Depends(get_current_active_user)): + return current_user + + +@app.get("/users/me/items/") +async def read_own_items( + current_user: User = Security(get_current_active_user, scopes=["items"]) +): + return [{"item_id": "Foo", "owner": current_user.username}] + + +@app.get("/status/") +async def read_system_status(current_user: User = Depends(get_current_user)): + return {"status": "ok"} diff --git a/docs_src/security/tutorial005_py39.py b/docs_src/security/tutorial005_py39.py new file mode 100644 index 000000000..d45c08ce6 --- /dev/null +++ b/docs_src/security/tutorial005_py39.py @@ -0,0 +1,173 @@ +from datetime import datetime, timedelta +from typing import Optional + +from fastapi import Depends, FastAPI, HTTPException, Security, status +from fastapi.security import ( + OAuth2PasswordBearer, + OAuth2PasswordRequestForm, + SecurityScopes, +) +from jose import JWTError, jwt +from passlib.context import CryptContext +from pydantic import BaseModel, ValidationError + +# to get a string like this run: +# openssl rand -hex 32 +SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7" +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 30 + + +fake_users_db = { + "johndoe": { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW", + "disabled": False, + }, + "alice": { + "username": "alice", + "full_name": "Alice Chains", + "email": "alicechains@example.com", + "hashed_password": "$2b$12$gSvqqUPvlXP2tfVFaWK1Be7DlH.PKZbv5H8KnzzVgXXbVxpva.pFm", + "disabled": True, + }, +} + + +class Token(BaseModel): + access_token: str + token_type: str + + +class TokenData(BaseModel): + username: Optional[str] = None + scopes: list[str] = [] + + +class User(BaseModel): + username: str + email: Optional[str] = None + full_name: Optional[str] = None + disabled: Optional[bool] = None + + +class UserInDB(User): + hashed_password: str + + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +oauth2_scheme = OAuth2PasswordBearer( + tokenUrl="token", + scopes={"me": "Read information about the current user.", "items": "Read items."}, +) + +app = FastAPI() + + +def verify_password(plain_password, hashed_password): + return pwd_context.verify(plain_password, hashed_password) + + +def get_password_hash(password): + return pwd_context.hash(password) + + +def get_user(db, username: str): + if username in db: + user_dict = db[username] + return UserInDB(**user_dict) + + +def authenticate_user(fake_db, username: str, password: str): + user = get_user(fake_db, username) + if not user: + return False + if not verify_password(password, user.hashed_password): + return False + return user + + +def create_access_token(data: dict, expires_delta: Optional[timedelta] = None): + to_encode = data.copy() + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(minutes=15) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt + + +async def get_current_user( + security_scopes: SecurityScopes, token: str = Depends(oauth2_scheme) +): + if security_scopes.scopes: + authenticate_value = f'Bearer scope="{security_scopes.scope_str}"' + else: + authenticate_value = f"Bearer" + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": authenticate_value}, + ) + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + username: str = payload.get("sub") + if username is None: + raise credentials_exception + token_scopes = payload.get("scopes", []) + token_data = TokenData(scopes=token_scopes, username=username) + except (JWTError, ValidationError): + raise credentials_exception + user = get_user(fake_users_db, username=token_data.username) + if user is None: + raise credentials_exception + for scope in security_scopes.scopes: + if scope not in token_data.scopes: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Not enough permissions", + headers={"WWW-Authenticate": authenticate_value}, + ) + return user + + +async def get_current_active_user( + current_user: User = Security(get_current_user, scopes=["me"]) +): + if current_user.disabled: + raise HTTPException(status_code=400, detail="Inactive user") + return current_user + + +@app.post("/token", response_model=Token) +async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()): + user = authenticate_user(fake_users_db, form_data.username, form_data.password) + if not user: + raise HTTPException(status_code=400, detail="Incorrect username or password") + access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + access_token = create_access_token( + data={"sub": user.username, "scopes": form_data.scopes}, + expires_delta=access_token_expires, + ) + return {"access_token": access_token, "token_type": "bearer"} + + +@app.get("/users/me/", response_model=User) +async def read_users_me(current_user: User = Depends(get_current_active_user)): + return current_user + + +@app.get("/users/me/items/") +async def read_own_items( + current_user: User = Security(get_current_active_user, scopes=["items"]) +): + return [{"item_id": "Foo", "owner": current_user.username}] + + +@app.get("/status/") +async def read_system_status(current_user: User = Depends(get_current_user)): + return {"status": "ok"} diff --git a/docs_src/sql_databases/sql_app_py310/__init__.py b/docs_src/sql_databases/sql_app_py310/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/sql_databases/sql_app_py310/alt_main.py b/docs_src/sql_databases/sql_app_py310/alt_main.py new file mode 100644 index 000000000..5de88ec3a --- /dev/null +++ b/docs_src/sql_databases/sql_app_py310/alt_main.py @@ -0,0 +1,60 @@ +from fastapi import Depends, FastAPI, HTTPException, Request, Response +from sqlalchemy.orm import Session + +from . import crud, models, schemas +from .database import SessionLocal, engine + +models.Base.metadata.create_all(bind=engine) + +app = FastAPI() + + +@app.middleware("http") +async def db_session_middleware(request: Request, call_next): + response = Response("Internal server error", status_code=500) + try: + request.state.db = SessionLocal() + response = await call_next(request) + finally: + request.state.db.close() + return response + + +# Dependency +def get_db(request: Request): + return request.state.db + + +@app.post("/users/", response_model=schemas.User) +def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)): + db_user = crud.get_user_by_email(db, email=user.email) + if db_user: + raise HTTPException(status_code=400, detail="Email already registered") + return crud.create_user(db=db, user=user) + + +@app.get("/users/", response_model=list[schemas.User]) +def read_users(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): + users = crud.get_users(db, skip=skip, limit=limit) + return users + + +@app.get("/users/{user_id}", response_model=schemas.User) +def read_user(user_id: int, db: Session = Depends(get_db)): + db_user = crud.get_user(db, user_id=user_id) + if db_user is None: + raise HTTPException(status_code=404, detail="User not found") + return db_user + + +@app.post("/users/{user_id}/items/", response_model=schemas.Item) +def create_item_for_user( + user_id: int, item: schemas.ItemCreate, db: Session = Depends(get_db) +): + return crud.create_user_item(db=db, item=item, user_id=user_id) + + +@app.get("/items/", response_model=list[schemas.Item]) +def read_items(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): + items = crud.get_items(db, skip=skip, limit=limit) + return items diff --git a/docs_src/sql_databases/sql_app_py310/crud.py b/docs_src/sql_databases/sql_app_py310/crud.py new file mode 100644 index 000000000..679acdb5c --- /dev/null +++ b/docs_src/sql_databases/sql_app_py310/crud.py @@ -0,0 +1,36 @@ +from sqlalchemy.orm import Session + +from . import models, schemas + + +def get_user(db: Session, user_id: int): + return db.query(models.User).filter(models.User.id == user_id).first() + + +def get_user_by_email(db: Session, email: str): + return db.query(models.User).filter(models.User.email == email).first() + + +def get_users(db: Session, skip: int = 0, limit: int = 100): + return db.query(models.User).offset(skip).limit(limit).all() + + +def create_user(db: Session, user: schemas.UserCreate): + fake_hashed_password = user.password + "notreallyhashed" + db_user = models.User(email=user.email, hashed_password=fake_hashed_password) + db.add(db_user) + db.commit() + db.refresh(db_user) + return db_user + + +def get_items(db: Session, skip: int = 0, limit: int = 100): + return db.query(models.Item).offset(skip).limit(limit).all() + + +def create_user_item(db: Session, item: schemas.ItemCreate, user_id: int): + db_item = models.Item(**item.dict(), owner_id=user_id) + db.add(db_item) + db.commit() + db.refresh(db_item) + return db_item diff --git a/docs_src/sql_databases/sql_app_py310/database.py b/docs_src/sql_databases/sql_app_py310/database.py new file mode 100644 index 000000000..45a8b9f69 --- /dev/null +++ b/docs_src/sql_databases/sql_app_py310/database.py @@ -0,0 +1,13 @@ +from sqlalchemy import create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker + +SQLALCHEMY_DATABASE_URL = "sqlite:///./sql_app.db" +# SQLALCHEMY_DATABASE_URL = "postgresql://user:password@postgresserver/db" + +engine = create_engine( + SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False} +) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +Base = declarative_base() diff --git a/docs_src/sql_databases/sql_app_py310/main.py b/docs_src/sql_databases/sql_app_py310/main.py new file mode 100644 index 000000000..a9856d0b6 --- /dev/null +++ b/docs_src/sql_databases/sql_app_py310/main.py @@ -0,0 +1,53 @@ +from fastapi import Depends, FastAPI, HTTPException +from sqlalchemy.orm import Session + +from . import crud, models, schemas +from .database import SessionLocal, engine + +models.Base.metadata.create_all(bind=engine) + +app = FastAPI() + + +# Dependency +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() + + +@app.post("/users/", response_model=schemas.User) +def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)): + db_user = crud.get_user_by_email(db, email=user.email) + if db_user: + raise HTTPException(status_code=400, detail="Email already registered") + return crud.create_user(db=db, user=user) + + +@app.get("/users/", response_model=list[schemas.User]) +def read_users(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): + users = crud.get_users(db, skip=skip, limit=limit) + return users + + +@app.get("/users/{user_id}", response_model=schemas.User) +def read_user(user_id: int, db: Session = Depends(get_db)): + db_user = crud.get_user(db, user_id=user_id) + if db_user is None: + raise HTTPException(status_code=404, detail="User not found") + return db_user + + +@app.post("/users/{user_id}/items/", response_model=schemas.Item) +def create_item_for_user( + user_id: int, item: schemas.ItemCreate, db: Session = Depends(get_db) +): + return crud.create_user_item(db=db, item=item, user_id=user_id) + + +@app.get("/items/", response_model=list[schemas.Item]) +def read_items(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): + items = crud.get_items(db, skip=skip, limit=limit) + return items diff --git a/docs_src/sql_databases/sql_app_py310/models.py b/docs_src/sql_databases/sql_app_py310/models.py new file mode 100644 index 000000000..62d8ab4aa --- /dev/null +++ b/docs_src/sql_databases/sql_app_py310/models.py @@ -0,0 +1,26 @@ +from sqlalchemy import Boolean, Column, ForeignKey, Integer, String +from sqlalchemy.orm import relationship + +from .database import Base + + +class User(Base): + __tablename__ = "users" + + id = Column(Integer, primary_key=True, index=True) + email = Column(String, unique=True, index=True) + hashed_password = Column(String) + is_active = Column(Boolean, default=True) + + items = relationship("Item", back_populates="owner") + + +class Item(Base): + __tablename__ = "items" + + id = Column(Integer, primary_key=True, index=True) + title = Column(String, index=True) + description = Column(String, index=True) + owner_id = Column(Integer, ForeignKey("users.id")) + + owner = relationship("User", back_populates="items") diff --git a/docs_src/sql_databases/sql_app_py310/schemas.py b/docs_src/sql_databases/sql_app_py310/schemas.py new file mode 100644 index 000000000..aea2e3f10 --- /dev/null +++ b/docs_src/sql_databases/sql_app_py310/schemas.py @@ -0,0 +1,35 @@ +from pydantic import BaseModel + + +class ItemBase(BaseModel): + title: str + description: str | None = None + + +class ItemCreate(ItemBase): + pass + + +class Item(ItemBase): + id: int + owner_id: int + + class Config: + orm_mode = True + + +class UserBase(BaseModel): + email: str + + +class UserCreate(UserBase): + password: str + + +class User(UserBase): + id: int + is_active: bool + items: list[Item] = [] + + class Config: + orm_mode = True diff --git a/docs_src/sql_databases/sql_app_py310/tests/__init__.py b/docs_src/sql_databases/sql_app_py310/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/sql_databases/sql_app_py310/tests/test_sql_app.py b/docs_src/sql_databases/sql_app_py310/tests/test_sql_app.py new file mode 100644 index 000000000..c60c3356f --- /dev/null +++ b/docs_src/sql_databases/sql_app_py310/tests/test_sql_app.py @@ -0,0 +1,47 @@ +from fastapi.testclient import TestClient +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker + +from ..database import Base +from ..main import app, get_db + +SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db" + +engine = create_engine( + SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False} +) +TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + + +Base.metadata.create_all(bind=engine) + + +def override_get_db(): + try: + db = TestingSessionLocal() + yield db + finally: + db.close() + + +app.dependency_overrides[get_db] = override_get_db + +client = TestClient(app) + + +def test_create_user(): + response = client.post( + "/users/", + json={"email": "deadpool@example.com", "password": "chimichangas4life"}, + ) + assert response.status_code == 200, response.text + data = response.json() + assert data["email"] == "deadpool@example.com" + assert "id" in data + user_id = data["id"] + + response = client.get(f"/users/{user_id}") + assert response.status_code == 200, response.text + data = response.json() + assert data["email"] == "deadpool@example.com" + assert data["id"] == user_id diff --git a/docs_src/sql_databases/sql_app_py39/__init__.py b/docs_src/sql_databases/sql_app_py39/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/sql_databases/sql_app_py39/alt_main.py b/docs_src/sql_databases/sql_app_py39/alt_main.py new file mode 100644 index 000000000..5de88ec3a --- /dev/null +++ b/docs_src/sql_databases/sql_app_py39/alt_main.py @@ -0,0 +1,60 @@ +from fastapi import Depends, FastAPI, HTTPException, Request, Response +from sqlalchemy.orm import Session + +from . import crud, models, schemas +from .database import SessionLocal, engine + +models.Base.metadata.create_all(bind=engine) + +app = FastAPI() + + +@app.middleware("http") +async def db_session_middleware(request: Request, call_next): + response = Response("Internal server error", status_code=500) + try: + request.state.db = SessionLocal() + response = await call_next(request) + finally: + request.state.db.close() + return response + + +# Dependency +def get_db(request: Request): + return request.state.db + + +@app.post("/users/", response_model=schemas.User) +def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)): + db_user = crud.get_user_by_email(db, email=user.email) + if db_user: + raise HTTPException(status_code=400, detail="Email already registered") + return crud.create_user(db=db, user=user) + + +@app.get("/users/", response_model=list[schemas.User]) +def read_users(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): + users = crud.get_users(db, skip=skip, limit=limit) + return users + + +@app.get("/users/{user_id}", response_model=schemas.User) +def read_user(user_id: int, db: Session = Depends(get_db)): + db_user = crud.get_user(db, user_id=user_id) + if db_user is None: + raise HTTPException(status_code=404, detail="User not found") + return db_user + + +@app.post("/users/{user_id}/items/", response_model=schemas.Item) +def create_item_for_user( + user_id: int, item: schemas.ItemCreate, db: Session = Depends(get_db) +): + return crud.create_user_item(db=db, item=item, user_id=user_id) + + +@app.get("/items/", response_model=list[schemas.Item]) +def read_items(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): + items = crud.get_items(db, skip=skip, limit=limit) + return items diff --git a/docs_src/sql_databases/sql_app_py39/crud.py b/docs_src/sql_databases/sql_app_py39/crud.py new file mode 100644 index 000000000..679acdb5c --- /dev/null +++ b/docs_src/sql_databases/sql_app_py39/crud.py @@ -0,0 +1,36 @@ +from sqlalchemy.orm import Session + +from . import models, schemas + + +def get_user(db: Session, user_id: int): + return db.query(models.User).filter(models.User.id == user_id).first() + + +def get_user_by_email(db: Session, email: str): + return db.query(models.User).filter(models.User.email == email).first() + + +def get_users(db: Session, skip: int = 0, limit: int = 100): + return db.query(models.User).offset(skip).limit(limit).all() + + +def create_user(db: Session, user: schemas.UserCreate): + fake_hashed_password = user.password + "notreallyhashed" + db_user = models.User(email=user.email, hashed_password=fake_hashed_password) + db.add(db_user) + db.commit() + db.refresh(db_user) + return db_user + + +def get_items(db: Session, skip: int = 0, limit: int = 100): + return db.query(models.Item).offset(skip).limit(limit).all() + + +def create_user_item(db: Session, item: schemas.ItemCreate, user_id: int): + db_item = models.Item(**item.dict(), owner_id=user_id) + db.add(db_item) + db.commit() + db.refresh(db_item) + return db_item diff --git a/docs_src/sql_databases/sql_app_py39/database.py b/docs_src/sql_databases/sql_app_py39/database.py new file mode 100644 index 000000000..45a8b9f69 --- /dev/null +++ b/docs_src/sql_databases/sql_app_py39/database.py @@ -0,0 +1,13 @@ +from sqlalchemy import create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker + +SQLALCHEMY_DATABASE_URL = "sqlite:///./sql_app.db" +# SQLALCHEMY_DATABASE_URL = "postgresql://user:password@postgresserver/db" + +engine = create_engine( + SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False} +) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +Base = declarative_base() diff --git a/docs_src/sql_databases/sql_app_py39/main.py b/docs_src/sql_databases/sql_app_py39/main.py new file mode 100644 index 000000000..a9856d0b6 --- /dev/null +++ b/docs_src/sql_databases/sql_app_py39/main.py @@ -0,0 +1,53 @@ +from fastapi import Depends, FastAPI, HTTPException +from sqlalchemy.orm import Session + +from . import crud, models, schemas +from .database import SessionLocal, engine + +models.Base.metadata.create_all(bind=engine) + +app = FastAPI() + + +# Dependency +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() + + +@app.post("/users/", response_model=schemas.User) +def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)): + db_user = crud.get_user_by_email(db, email=user.email) + if db_user: + raise HTTPException(status_code=400, detail="Email already registered") + return crud.create_user(db=db, user=user) + + +@app.get("/users/", response_model=list[schemas.User]) +def read_users(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): + users = crud.get_users(db, skip=skip, limit=limit) + return users + + +@app.get("/users/{user_id}", response_model=schemas.User) +def read_user(user_id: int, db: Session = Depends(get_db)): + db_user = crud.get_user(db, user_id=user_id) + if db_user is None: + raise HTTPException(status_code=404, detail="User not found") + return db_user + + +@app.post("/users/{user_id}/items/", response_model=schemas.Item) +def create_item_for_user( + user_id: int, item: schemas.ItemCreate, db: Session = Depends(get_db) +): + return crud.create_user_item(db=db, item=item, user_id=user_id) + + +@app.get("/items/", response_model=list[schemas.Item]) +def read_items(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): + items = crud.get_items(db, skip=skip, limit=limit) + return items diff --git a/docs_src/sql_databases/sql_app_py39/models.py b/docs_src/sql_databases/sql_app_py39/models.py new file mode 100644 index 000000000..62d8ab4aa --- /dev/null +++ b/docs_src/sql_databases/sql_app_py39/models.py @@ -0,0 +1,26 @@ +from sqlalchemy import Boolean, Column, ForeignKey, Integer, String +from sqlalchemy.orm import relationship + +from .database import Base + + +class User(Base): + __tablename__ = "users" + + id = Column(Integer, primary_key=True, index=True) + email = Column(String, unique=True, index=True) + hashed_password = Column(String) + is_active = Column(Boolean, default=True) + + items = relationship("Item", back_populates="owner") + + +class Item(Base): + __tablename__ = "items" + + id = Column(Integer, primary_key=True, index=True) + title = Column(String, index=True) + description = Column(String, index=True) + owner_id = Column(Integer, ForeignKey("users.id")) + + owner = relationship("User", back_populates="items") diff --git a/docs_src/sql_databases/sql_app_py39/schemas.py b/docs_src/sql_databases/sql_app_py39/schemas.py new file mode 100644 index 000000000..a19f1cdfe --- /dev/null +++ b/docs_src/sql_databases/sql_app_py39/schemas.py @@ -0,0 +1,37 @@ +from typing import Optional + +from pydantic import BaseModel + + +class ItemBase(BaseModel): + title: str + description: Optional[str] = None + + +class ItemCreate(ItemBase): + pass + + +class Item(ItemBase): + id: int + owner_id: int + + class Config: + orm_mode = True + + +class UserBase(BaseModel): + email: str + + +class UserCreate(UserBase): + password: str + + +class User(UserBase): + id: int + is_active: bool + items: list[Item] = [] + + class Config: + orm_mode = True diff --git a/docs_src/sql_databases/sql_app_py39/tests/__init__.py b/docs_src/sql_databases/sql_app_py39/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/sql_databases/sql_app_py39/tests/test_sql_app.py b/docs_src/sql_databases/sql_app_py39/tests/test_sql_app.py new file mode 100644 index 000000000..c60c3356f --- /dev/null +++ b/docs_src/sql_databases/sql_app_py39/tests/test_sql_app.py @@ -0,0 +1,47 @@ +from fastapi.testclient import TestClient +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker + +from ..database import Base +from ..main import app, get_db + +SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db" + +engine = create_engine( + SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False} +) +TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + + +Base.metadata.create_all(bind=engine) + + +def override_get_db(): + try: + db = TestingSessionLocal() + yield db + finally: + db.close() + + +app.dependency_overrides[get_db] = override_get_db + +client = TestClient(app) + + +def test_create_user(): + response = client.post( + "/users/", + json={"email": "deadpool@example.com", "password": "chimichangas4life"}, + ) + assert response.status_code == 200, response.text + data = response.json() + assert data["email"] == "deadpool@example.com" + assert "id" in data + user_id = data["id"] + + response = client.get(f"/users/{user_id}") + assert response.status_code == 200, response.text + data = response.json() + assert data["email"] == "deadpool@example.com" + assert data["id"] == user_id diff --git a/pyproject.toml b/pyproject.toml index e6148831b..fa399c492 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,7 @@ classifiers = [ "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", "Topic :: Internet :: WWW/HTTP :: HTTP Servers", "Topic :: Internet :: WWW/HTTP", ] diff --git a/tests/test_tutorial/test_background_tasks/test_tutorial002_py310.py b/tests/test_tutorial/test_background_tasks/test_tutorial002_py310.py new file mode 100644 index 000000000..6937c8fab --- /dev/null +++ b/tests/test_tutorial/test_background_tasks/test_tutorial002_py310.py @@ -0,0 +1,21 @@ +import os +from pathlib import Path + +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@needs_py310 +def test(): + from docs_src.background_tasks.tutorial002_py310 import app + + client = TestClient(app) + log = Path("log.txt") + if log.is_file(): + os.remove(log) # pragma: no cover + response = client.post("/send-notification/foo@example.com?q=some-query") + assert response.status_code == 200, response.text + assert response.json() == {"message": "Message sent"} + with open("./log.txt") as f: + assert "found query: some-query\nmessage to foo@example.com" in f.read() diff --git a/tests/test_tutorial/test_body/test_tutorial001_py310.py b/tests/test_tutorial/test_body/test_tutorial001_py310.py new file mode 100644 index 000000000..e292b5346 --- /dev/null +++ b/tests/test_tutorial/test_body/test_tutorial001_py310.py @@ -0,0 +1,292 @@ +from unittest.mock import patch + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create Item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": {"title": "Description", "type": "string"}, + "tax": {"title": "Tax", "type": "number"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture +def client(): + from docs_src.body.tutorial001_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +price_missing = { + "detail": [ + { + "loc": ["body", "price"], + "msg": "field required", + "type": "value_error.missing", + } + ] +} + +price_not_float = { + "detail": [ + { + "loc": ["body", "price"], + "msg": "value is not a valid float", + "type": "type_error.float", + } + ] +} + +name_price_missing = { + "detail": [ + { + "loc": ["body", "name"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "price"], + "msg": "field required", + "type": "value_error.missing", + }, + ] +} + +body_missing = { + "detail": [ + {"loc": ["body"], "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 + + +@needs_py310 +def test_post_broken_body(client: TestClient): + response = client.post( + "/items/", + headers={"content-type": "application/json"}, + data="{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, + }, + } + ] + } + + +@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", + } + ] + } + + +@needs_py310 +def test_explicit_content_type(client: TestClient): + response = client.post( + "/items/", + data='{"name": "Foo", "price": 50.5}', + headers={"Content-Type": "application/json"}, + ) + assert response.status_code == 200, response.text + + +@needs_py310 +def test_geo_json(client: TestClient): + response = client.post( + "/items/", + data='{"name": "Foo", "price": 50.5}', + headers={"Content-Type": "application/geo+json"}, + ) + assert response.status_code == 200, response.text + + +@needs_py310 +def test_no_content_type_is_json(client: TestClient): + response = client.post( + "/items/", + data='{"name": "Foo", "price": 50.5}', + ) + assert response.status_code == 200, response.text + assert response.json() == { + "name": "Foo", + "description": None, + "price": 50.5, + "tax": None, + } + + +@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/", data=data, headers={"Content-Type": "text/plain"}) + assert response.status_code == 422, response.text + assert response.json() == invalid_dict + + response = client.post( + "/items/", data=data, headers={"Content-Type": "application/geo+json-seq"} + ) + assert response.status_code == 422, response.text + assert response.json() == invalid_dict + response = client.post( + "/items/", data=data, headers={"Content-Type": "application/not-really-json"} + ) + assert response.status_code == 422, response.text + assert response.json() == invalid_dict + + +@needs_py310 +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 diff --git a/tests/test_tutorial/test_body_fields/test_tutorial001_py310.py b/tests/test_tutorial/test_body_fields/test_tutorial001_py310.py new file mode 100644 index 000000000..d7a525ea7 --- /dev/null +++ b/tests/test_tutorial/test_body_fields/test_tutorial001_py310.py @@ -0,0 +1,176 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_update_item_items__item_id__put" + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": { + "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"}, + }, + }, + "Body_update_item_items__item_id__put": { + "title": "Body_update_item_items__item_id__put", + "required": ["item"], + "type": "object", + "properties": {"item": {"$ref": "#/components/schemas/Item"}}, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.body_fields.tutorial001_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +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 +@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", + } + }, + 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 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 new file mode 100644 index 000000000..85ba41ce6 --- /dev/null +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_py310.py @@ -0,0 +1,155 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": { + "title": "The ID of the item to get", + "maximum": 1000.0, + "minimum": 0.0, + "type": "integer", + }, + "name": "item_id", + "in": "path", + }, + { + "required": False, + "schema": {"title": "Q", "type": "string"}, + "name": "q", + "in": "query", + }, + ], + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + } + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": {"title": "Description", "type": "string"}, + "tax": {"title": "Tax", "type": "number"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.body_multiple_params.tutorial001_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +item_id_not_int = { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] +} + + +@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 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 new file mode 100644 index 000000000..f896f7bf5 --- /dev/null +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_py310.py @@ -0,0 +1,206 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_update_item_items__item_id__put" + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": {"title": "Description", "type": "string"}, + "tax": {"title": "Tax", "type": "number"}, + }, + }, + "User": { + "title": "User", + "required": ["username"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "full_name": {"title": "Full Name", "type": "string"}, + }, + }, + "Body_update_item_items__item_id__put": { + "title": "Body_update_item_items__item_id__put", + "required": ["item", "user", "importance"], + "type": "object", + "properties": { + "item": {"$ref": "#/components/schemas/Item"}, + "user": {"$ref": "#/components/schemas/User"}, + "importance": {"title": "Importance", "type": "integer"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.body_multiple_params.tutorial003_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +# 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 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 new file mode 100644 index 000000000..17ca29ce5 --- /dev/null +++ b/tests/test_tutorial/test_body_nested_models/test_tutorial009_py39.py @@ -0,0 +1,113 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/index-weights/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create Index Weights", + "operationId": "create_index_weights_index_weights__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "title": "Weights", + "type": "object", + "additionalProperties": {"type": "number"}, + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.body_nested_models.tutorial009_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py39 +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 + + +@needs_py39 +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", + } + ] + } diff --git a/tests/test_tutorial/test_body_updates/test_tutorial001_py310.py b/tests/test_tutorial/test_body_updates/test_tutorial001_py310.py new file mode 100644 index 000000000..ca1d8c585 --- /dev/null +++ b/tests/test_tutorial/test_body_updates/test_tutorial001_py310.py @@ -0,0 +1,172 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + }, + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + }, + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": {"title": "Description", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "tax": {"title": "Tax", "type": "number", "default": 10.5}, + "tags": { + "title": "Tags", + "type": "array", + "items": {"type": "string"}, + "default": [], + }, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.body_updates.tutorial001_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py310 +def test_get(client: TestClient): + response = client.get("/items/baz") + assert response.status_code == 200, response.text + assert response.json() == { + "name": "Baz", + "description": None, + "price": 50.2, + "tax": 10.5, + "tags": [], + } + + +@needs_py310 +def test_put(client: TestClient): + response = client.put( + "/items/bar", json={"name": "Barz", "price": 3, "description": None} + ) + assert response.json() == { + "name": "Barz", + "description": None, + "price": 3, + "tax": 10.5, + "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 new file mode 100644 index 000000000..f2b184c4f --- /dev/null +++ b/tests/test_tutorial/test_body_updates/test_tutorial001_py39.py @@ -0,0 +1,172 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + }, + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + }, + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": {"title": "Description", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "tax": {"title": "Tax", "type": "number", "default": 10.5}, + "tags": { + "title": "Tags", + "type": "array", + "items": {"type": "string"}, + "default": [], + }, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.body_updates.tutorial001_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py39 +def test_get(client: TestClient): + response = client.get("/items/baz") + assert response.status_code == 200, response.text + assert response.json() == { + "name": "Baz", + "description": None, + "price": 50.2, + "tax": 10.5, + "tags": [], + } + + +@needs_py39 +def test_put(client: TestClient): + response = client.put( + "/items/bar", json={"name": "Barz", "price": 3, "description": None} + ) + assert response.json() == { + "name": "Barz", + "description": None, + "price": 3, + "tax": 10.5, + "tags": [], + } diff --git a/tests/test_tutorial/test_cookie_params/test_tutorial001_py310.py b/tests/test_tutorial/test_cookie_params/test_tutorial001_py310.py new file mode 100644 index 000000000..587a328da --- /dev/null +++ b/tests/test_tutorial/test_cookie_params/test_tutorial001_py310.py @@ -0,0 +1,100 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Ads Id", "type": "string"}, + "name": "ads_id", + "in": "cookie", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.cookie_params.tutorial001_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +@pytest.mark.parametrize( + "path,cookies,expected_status,expected_response", + [ + ("/openapi.json", None, 200, openapi_schema), + ("/items", None, 200, {"ads_id": None}), + ("/items", {"ads_id": "ads_track"}, 200, {"ads_id": "ads_track"}), + ( + "/items", + {"ads_id": "ads_track", "session": "cookiesession"}, + 200, + {"ads_id": "ads_track"}, + ), + ("/items", {"session": "cookiesession"}, 200, {"ads_id": None}), + ], +) +def test(path, cookies, expected_status, expected_response, client: TestClient): + response = client.get(path, cookies=cookies) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_dependencies/test_tutorial001_py310.py b/tests/test_tutorial/test_dependencies/test_tutorial001_py310.py new file mode 100644 index 000000000..a7991170e --- /dev/null +++ b/tests/test_tutorial/test_dependencies/test_tutorial001_py310.py @@ -0,0 +1,157 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Q", "type": "string"}, + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Skip", "type": "integer", "default": 0}, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Limit", "type": "integer", "default": 100}, + "name": "limit", + "in": "query", + }, + ], + } + }, + "/users/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Users", + "operationId": "read_users_users__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Q", "type": "string"}, + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Skip", "type": "integer", "default": 0}, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Limit", "type": "integer", "default": 100}, + "name": "limit", + "in": "query", + }, + ], + } + }, + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.dependencies.tutorial001_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py310 +@pytest.mark.parametrize( + "path,expected_status,expected_response", + [ + ("/items", 200, {"q": None, "skip": 0, "limit": 100}), + ("/items?q=foo", 200, {"q": "foo", "skip": 0, "limit": 100}), + ("/items?q=foo&skip=5", 200, {"q": "foo", "skip": 5, "limit": 100}), + ("/items?q=foo&skip=5&limit=30", 200, {"q": "foo", "skip": 5, "limit": 30}), + ("/users", 200, {"q": None, "skip": 0, "limit": 100}), + ("/openapi.json", 200, openapi_schema), + ], +) +def test_get(path, expected_status, expected_response, client: TestClient): + response = client.get(path) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_dependencies/test_tutorial004_py310.py b/tests/test_tutorial/test_dependencies/test_tutorial004_py310.py new file mode 100644 index 000000000..f66a36a99 --- /dev/null +++ b/tests/test_tutorial/test_dependencies/test_tutorial004_py310.py @@ -0,0 +1,152 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Q", "type": "string"}, + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Skip", "type": "integer", "default": 0}, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Limit", "type": "integer", "default": 100}, + "name": "limit", + "in": "query", + }, + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.dependencies.tutorial004_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py310 +@pytest.mark.parametrize( + "path,expected_status,expected_response", + [ + ( + "/items", + 200, + { + "items": [ + {"item_name": "Foo"}, + {"item_name": "Bar"}, + {"item_name": "Baz"}, + ] + }, + ), + ( + "/items?q=foo", + 200, + { + "items": [ + {"item_name": "Foo"}, + {"item_name": "Bar"}, + {"item_name": "Baz"}, + ], + "q": "foo", + }, + ), + ( + "/items?q=foo&skip=1", + 200, + {"items": [{"item_name": "Bar"}, {"item_name": "Baz"}], "q": "foo"}, + ), + ( + "/items?q=bar&limit=2", + 200, + {"items": [{"item_name": "Foo"}, {"item_name": "Bar"}], "q": "bar"}, + ), + ( + "/items?q=bar&skip=1&limit=1", + 200, + {"items": [{"item_name": "Bar"}], "q": "bar"}, + ), + ( + "/items?limit=1&q=bar&skip=1", + 200, + {"items": [{"item_name": "Bar"}], "q": "bar"}, + ), + ], +) +def test_get(path, expected_status, expected_response, client: TestClient): + response = client.get(path) + assert response.status_code == expected_status + assert response.json() == expected_response 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 new file mode 100644 index 000000000..3d4c1d07d --- /dev/null +++ b/tests/test_tutorial/test_extra_data_types/test_tutorial001_py310.py @@ -0,0 +1,146 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "type": "string", + "format": "uuid", + }, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_read_items_items__item_id__put" + } + } + } + }, + } + } + }, + "components": { + "schemas": { + "Body_read_items_items__item_id__put": { + "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", + }, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.extra_data_types.tutorial001_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py310 +def test_extra_types(client: TestClient): + item_id = "ff97dd87-a4a5-4a12-b412-cde99f33e00e" + data = { + "start_datetime": "2018-12-22T14:00:00+00:00", + "end_datetime": "2018-12-24T15:00:00+00:00", + "repeat_at": "15:30:00", + "process_after": 300, + } + expected_response = data.copy() + expected_response.update( + { + "start_process": "2018-12-22T14:05:00+00:00", + "duration": 176_100, + "item_id": item_id, + } + ) + response = client.put(f"/items/{item_id}", json=data) + assert response.status_code == 200, response.text + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_extra_models/test_tutorial003_py310.py b/tests/test_tutorial/test_extra_models/test_tutorial003_py310.py new file mode 100644 index 000000000..185bc3a37 --- /dev/null +++ b/tests/test_tutorial/test_extra_models/test_tutorial003_py310.py @@ -0,0 +1,135 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Item Items Item Id Get", + "anyOf": [ + {"$ref": "#/components/schemas/PlaneItem"}, + {"$ref": "#/components/schemas/CarItem"}, + ], + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + } + } + }, + "components": { + "schemas": { + "PlaneItem": { + "title": "PlaneItem", + "required": ["description", "size"], + "type": "object", + "properties": { + "description": {"title": "Description", "type": "string"}, + "type": {"title": "Type", "type": "string", "default": "plane"}, + "size": {"title": "Size", "type": "integer"}, + }, + }, + "CarItem": { + "title": "CarItem", + "required": ["description"], + "type": "object", + "properties": { + "description": {"title": "Description", "type": "string"}, + "type": {"title": "Type", "type": "string", "default": "car"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.extra_models.tutorial003_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py310 +def test_get_car(client: TestClient): + response = client.get("/items/item1") + assert response.status_code == 200, response.text + assert response.json() == { + "description": "All my friends drive a low rider", + "type": "car", + } + + +@needs_py310 +def test_get_plane(client: TestClient): + response = client.get("/items/item2") + assert response.status_code == 200, response.text + assert response.json() == { + "description": "Music is my aeroplane, it's my aeroplane", + "type": "plane", + "size": 5, + } diff --git a/tests/test_tutorial/test_extra_models/test_tutorial004_py39.py b/tests/test_tutorial/test_extra_models/test_tutorial004_py39.py new file mode 100644 index 000000000..7f4f5b9be --- /dev/null +++ b/tests/test_tutorial/test_extra_models/test_tutorial004_py39.py @@ -0,0 +1,69 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Items Items Get", + "type": "array", + "items": {"$ref": "#/components/schemas/Item"}, + } + } + }, + } + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "description"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": {"title": "Description", "type": "string"}, + }, + } + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.extra_models.tutorial004_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py39 +def test_get_items(client: TestClient): + response = client.get("/items/") + assert response.status_code == 200, response.text + assert response.json() == [ + {"name": "Foo", "description": "There comes my hero"}, + {"name": "Red", "description": "It's my aeroplane"}, + ] diff --git a/tests/test_tutorial/test_extra_models/test_tutorial005_py39.py b/tests/test_tutorial/test_extra_models/test_tutorial005_py39.py new file mode 100644 index 000000000..3bb5a99f1 --- /dev/null +++ b/tests/test_tutorial/test_extra_models/test_tutorial005_py39.py @@ -0,0 +1,53 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/keyword-weights/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Keyword Weights Keyword Weights Get", + "type": "object", + "additionalProperties": {"type": "number"}, + } + } + }, + } + }, + "summary": "Read Keyword Weights", + "operationId": "read_keyword_weights_keyword_weights__get", + } + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.extra_models.tutorial005_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py39 +def test_get_items(client: TestClient): + response = client.get("/keyword-weights/") + assert response.status_code == 200, response.text + assert response.json() == {"foo": 2.3, "bar": 3.4} diff --git a/tests/test_tutorial/test_header_params/test_tutorial001_py310.py b/tests/test_tutorial/test_header_params/test_tutorial001_py310.py new file mode 100644 index 000000000..f5ee17428 --- /dev/null +++ b/tests/test_tutorial/test_header_params/test_tutorial001_py310.py @@ -0,0 +1,94 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "User-Agent", "type": "string"}, + "name": "user-agent", + "in": "header", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.header_params.tutorial001_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +@pytest.mark.parametrize( + "path,headers,expected_status,expected_response", + [ + ("/openapi.json", None, 200, openapi_schema), + ("/items", None, 200, {"User-Agent": "testclient"}), + ("/items", {"X-Header": "notvalid"}, 200, {"User-Agent": "testclient"}), + ("/items", {"User-Agent": "FastAPI test"}, 200, {"User-Agent": "FastAPI test"}), + ], +) +def test(path, headers, expected_status, expected_response, client: TestClient): + response = client.get(path, headers=headers) + assert response.status_code == expected_status + assert response.json() == expected_response 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 new file mode 100644 index 000000000..1f617da70 --- /dev/null +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py310.py @@ -0,0 +1,121 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "post": { + "responses": { + "200": { + "description": "The created item", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create an item", + "description": "Create an item with all the information:\n\n- **name**: each item must have a name\n- **description**: a long description\n- **price**: required\n- **tax**: if the item doesn't have tax, you can omit this\n- **tags**: a set of unique tag strings for this item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": {"title": "Description", "type": "string"}, + "tax": {"title": "Tax", "type": "number"}, + "tags": { + "title": "Tags", + "uniqueItems": True, + "type": "array", + "items": {"type": "string"}, + "default": [], + }, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.path_operation_configuration.tutorial005_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py310 +def test_query_params_str_validations(client: TestClient): + response = client.post("/items/", json={"name": "Foo", "price": 42}) + assert response.status_code == 200, response.text + assert response.json() == { + "name": "Foo", + "price": 42, + "description": None, + "tax": None, + "tags": [], + } 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 new file mode 100644 index 000000000..ffdf05081 --- /dev/null +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py39.py @@ -0,0 +1,121 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "post": { + "responses": { + "200": { + "description": "The created item", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create an item", + "description": "Create an item with all the information:\n\n- **name**: each item must have a name\n- **description**: a long description\n- **price**: required\n- **tax**: if the item doesn't have tax, you can omit this\n- **tags**: a set of unique tag strings for this item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": {"title": "Description", "type": "string"}, + "tax": {"title": "Tax", "type": "number"}, + "tags": { + "title": "Tags", + "uniqueItems": True, + "type": "array", + "items": {"type": "string"}, + "default": [], + }, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.path_operation_configuration.tutorial005_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py39 +def test_query_params_str_validations(client: TestClient): + response = client.post("/items/", json={"name": "Foo", "price": 42}) + assert response.status_code == 200, response.text + assert response.json() == { + "name": "Foo", + "price": 42, + "description": None, + "tax": None, + "tags": [], + } diff --git a/tests/test_tutorial/test_query_params/test_tutorial006_py310.py b/tests/test_tutorial/test_query_params/test_tutorial006_py310.py new file mode 100644 index 000000000..1986d27d0 --- /dev/null +++ b/tests/test_tutorial/test_query_params/test_tutorial006_py310.py @@ -0,0 +1,148 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read User Item", + "operationId": "read_user_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + }, + { + "required": True, + "schema": {"title": "Needy", "type": "string"}, + "name": "needy", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Skip", "type": "integer", "default": 0}, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Limit", "type": "integer"}, + "name": "limit", + "in": "query", + }, + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +query_required = { + "detail": [ + { + "loc": ["query", "needy"], + "msg": "field required", + "type": "value_error.missing", + } + ] +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.query_params.tutorial006_py310 import app + + c = TestClient(app) + return c + + +@needs_py310 +@pytest.mark.parametrize( + "path,expected_status,expected_response", + [ + ("/openapi.json", 200, openapi_schema), + ( + "/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 diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial001_py310.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial001_py310.py new file mode 100644 index 000000000..66b24017e --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial001_py310.py @@ -0,0 +1,132 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "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", + }, + "name": "item-query", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.query_params_str_validations.tutorial010_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +regex_error = { + "detail": [ + { + "ctx": {"pattern": "^fixedquery$"}, + "loc": ["query", "item-query"], + "msg": 'string does not match regex "^fixedquery$"', + "type": "value_error.str.regex", + } + ] +} + + +@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 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 new file mode 100644 index 000000000..8894ee1b5 --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py310.py @@ -0,0 +1,105 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Q", + "type": "array", + "items": {"type": "string"}, + }, + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.query_params_str_validations.tutorial011_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py310 +def test_multi_query_values(client: TestClient): + url = "/items/?q=foo&q=bar" + response = client.get(url) + assert response.status_code == 200, response.text + assert response.json() == {"q": ["foo", "bar"]} + + +@needs_py310 +def test_query_no_values(client: TestClient): + url = "/items/" + response = client.get(url) + assert response.status_code == 200, response.text + assert response.json() == {"q": None} 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 new file mode 100644 index 000000000..b10e70af7 --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py39.py @@ -0,0 +1,105 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Q", + "type": "array", + "items": {"type": "string"}, + }, + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.query_params_str_validations.tutorial011_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py39 +def test_multi_query_values(client: TestClient): + url = "/items/?q=foo&q=bar" + response = client.get(url) + assert response.status_code == 200, response.text + assert response.json() == {"q": ["foo", "bar"]} + + +@needs_py39 +def test_query_no_values(client: TestClient): + url = "/items/" + response = client.get(url) + assert response.status_code == 200, response.text + assert response.json() == {"q": None} 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 new file mode 100644 index 000000000..a9cbce02a --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_py39.py @@ -0,0 +1,106 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Q", + "type": "array", + "items": {"type": "string"}, + "default": ["foo", "bar"], + }, + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.query_params_str_validations.tutorial012_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py39 +def test_default_query_values(client: TestClient): + url = "/items/" + response = client.get(url) + assert response.status_code == 200, response.text + assert response.json() == {"q": ["foo", "bar"]} + + +@needs_py39 +def test_multi_query_values(client: TestClient): + url = "/items/?q=baz&q=foobar" + response = client.get(url) + assert response.status_code == 200, response.text + assert response.json() == {"q": ["baz", "foobar"]} diff --git a/tests/test_tutorial/test_request_files/test_tutorial002_py39.py b/tests/test_tutorial/test_request_files/test_tutorial002_py39.py new file mode 100644 index 000000000..bbdf25cd9 --- /dev/null +++ b/tests/test_tutorial/test_request_files/test_tutorial002_py39.py @@ -0,0 +1,235 @@ +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create Files", + "operationId": "create_files_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_files_files__post" + } + } + }, + "required": True, + }, + } + }, + "/uploadfiles/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create Upload Files", + "operationId": "create_upload_files_uploadfiles__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_upload_files_uploadfiles__post" + } + } + }, + "required": True, + }, + } + }, + "/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Main", + "operationId": "main__get", + } + }, + }, + "components": { + "schemas": { + "Body_create_upload_files_uploadfiles__post": { + "title": "Body_create_upload_files_uploadfiles__post", + "required": ["files"], + "type": "object", + "properties": { + "files": { + "title": "Files", + "type": "array", + "items": {"type": "string", "format": "binary"}, + } + }, + }, + "Body_create_files_files__post": { + "title": "Body_create_files_files__post", + "required": ["files"], + "type": "object", + "properties": { + "files": { + "title": "Files", + "type": "array", + "items": {"type": "string", "format": "binary"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="app") +def get_app(): + from docs_src.request_files.tutorial002_py39 import app + + return app + + +@pytest.fixture(name="client") +def get_client(app: FastAPI): + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +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 + + +@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 + + +@needs_py39 +def test_post_files(tmp_path, app: FastAPI): + path = tmp_path / "test.txt" + path.write_bytes(b"") + path2 = tmp_path / "test2.txt" + path2.write_bytes(b"") + + client = TestClient(app) + with path.open("rb") as file, path2.open("rb") as file2: + response = client.post( + "/files/", + files=( + ("files", ("test.txt", file)), + ("files", ("test2.txt", file2)), + ), + ) + assert response.status_code == 200, response.text + assert response.json() == {"file_sizes": [14, 15]} + + +@needs_py39 +def test_post_upload_file(tmp_path, app: FastAPI): + path = tmp_path / "test.txt" + path.write_bytes(b"") + path2 = tmp_path / "test2.txt" + path2.write_bytes(b"") + + client = TestClient(app) + with path.open("rb") as file, path2.open("rb") as file2: + response = client.post( + "/uploadfiles/", + files=( + ("files", ("test.txt", file)), + ("files", ("test2.txt", file2)), + ), + ) + assert response.status_code == 200, response.text + assert response.json() == {"filenames": ["test.txt", "test2.txt"]} + + +@needs_py39 +def test_get_root(app: FastAPI): + client = TestClient(app) + response = client.get("/") + assert response.status_code == 200, response.text + assert b" Date: Fri, 7 Jan 2022 14:12:16 +0000 Subject: [PATCH 15/59] =?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 ce1d3144d..f6e66d805 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✨ Add docs and tests for Python 3.9 and Python 3.10. PR [#3712](https://github.com/tiangolo/fastapi/pull/3712) by [@tiangolo](https://github.com/tiangolo). * ⬆ Upgrade MkDocs Material and configs. PR [#4385](https://github.com/tiangolo/fastapi/pull/4385) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Upgrade Starlette to 0.17.1. PR [#4145](https://github.com/tiangolo/fastapi/pull/4145) by [@simondale00](https://github.com/simondale00). ## 0.70.1 From a1ede32f29366902033e701f09bfe77c29d0442e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 7 Jan 2022 15:17:13 +0100 Subject: [PATCH 16/59] =?UTF-8?q?=F0=9F=94=A7=20Add=20FastAPI=20Trove=20Cl?= =?UTF-8?q?assifier=20for=20PyPI=20(#4386)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index fa399c492..77c01322f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,6 +22,7 @@ classifiers = [ "Development Status :: 4 - Beta", "Environment :: Web Environment", "Framework :: AsyncIO", + "Framework :: FastAPI", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3 :: Only", From 4da33e303148cbabeb96bb3e17cd236ef4402d33 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 7 Jan 2022 14:17:49 +0000 Subject: [PATCH 17/59] =?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 f6e66d805..9453909be 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔧 Add FastAPI Trove Classifier for PyPI as now there's one 🤷😁. PR [#4386](https://github.com/tiangolo/fastapi/pull/4386) by [@tiangolo](https://github.com/tiangolo). * ✨ Add docs and tests for Python 3.9 and Python 3.10. PR [#3712](https://github.com/tiangolo/fastapi/pull/3712) by [@tiangolo](https://github.com/tiangolo). * ⬆ Upgrade MkDocs Material and configs. PR [#4385](https://github.com/tiangolo/fastapi/pull/4385) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Upgrade Starlette to 0.17.1. PR [#4145](https://github.com/tiangolo/fastapi/pull/4145) by [@simondale00](https://github.com/simondale00). From 44f4885c663f498c5902ba1a951d6e0e3d318fa1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 7 Jan 2022 15:18:45 +0100 Subject: [PATCH 18/59] =?UTF-8?q?=F0=9F=91=A5=20Update=20FastAPI=20People?= =?UTF-8?q?=20(#4354)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions --- docs/en/data/people.yml | 98 ++++++++++++++++++++++------------------- 1 file changed, 53 insertions(+), 45 deletions(-) diff --git a/docs/en/data/people.yml b/docs/en/data/people.yml index 89c6f8dc3..df088f39f 100644 --- a/docs/en/data/people.yml +++ b/docs/en/data/people.yml @@ -6,8 +6,8 @@ maintainers: url: https://github.com/tiangolo experts: - login: Kludex - count: 315 - avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=cf8455cb899806b774a3a71073f88583adec99f6&v=4 + count: 316 + avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=3682d9b9b93bef272f379ab623dc031c8d71432e&v=4 url: https://github.com/Kludex - login: dmontagu count: 262 @@ -34,7 +34,7 @@ experts: avatarUrl: https://avatars.githubusercontent.com/u/31127044?u=81a84af39c89b898b0fbc5a04e8834f60f23e55a&v=4 url: https://github.com/ArcLightSlavik - login: raphaelauv - count: 63 + count: 67 avatarUrl: https://avatars.githubusercontent.com/u/10202690?u=e6f86f5c0c3026a15d6b51792fa3e532b12f1371&v=4 url: https://github.com/raphaelauv - login: falkben @@ -45,6 +45,10 @@ experts: count: 48 avatarUrl: https://avatars.githubusercontent.com/u/516999?u=437c0c5038558c67e887ccd863c1ba0f846c03da&v=4 url: https://github.com/sm-Fifteen +- login: insomnes + count: 46 + avatarUrl: https://avatars.githubusercontent.com/u/16958893?u=f8be7088d5076d963984a21f95f44e559192d912&v=4 + url: https://github.com/insomnes - login: Dustyposa count: 42 avatarUrl: https://avatars.githubusercontent.com/u/27180793?u=5cf2877f50b3eb2bc55086089a78a36f07042889&v=4 @@ -53,10 +57,6 @@ experts: count: 38 avatarUrl: https://avatars.githubusercontent.com/u/11836741?u=8bd5ef7e62fe6a82055e33c4c0e0a7879ff8cfb6&v=4 url: https://github.com/includeamin -- login: insomnes - count: 37 - avatarUrl: https://avatars.githubusercontent.com/u/16958893?u=f8be7088d5076d963984a21f95f44e559192d912&v=4 - url: https://github.com/insomnes - login: prostomarkeloff count: 33 avatarUrl: https://avatars.githubusercontent.com/u/28061158?u=72309cc1f2e04e40fa38b29969cb4e9d3f722e7b&v=4 @@ -78,7 +78,7 @@ experts: avatarUrl: https://avatars.githubusercontent.com/u/1144727?u=85c025e3fcc7bd79a5665c63ee87cdf8aae13374&v=4 url: https://github.com/frankie567 - login: adriangb - count: 26 + count: 28 avatarUrl: https://avatars.githubusercontent.com/u/1755071?u=81f0262df34e1460ca546fbd0c211169c2478532&v=4 url: https://github.com/adriangb - login: ghandic @@ -97,6 +97,10 @@ experts: count: 24 avatarUrl: https://avatars.githubusercontent.com/u/9435877?u=719327b7d2c4c62212456d771bfa7c6b8dbb9eac&v=4 url: https://github.com/SirTelemak +- login: panla + count: 23 + avatarUrl: https://avatars.githubusercontent.com/u/41326348?u=ba2fda6b30110411ecbf406d187907e2b420ac19&v=4 + url: https://github.com/panla - login: acnebs count: 22 avatarUrl: https://avatars.githubusercontent.com/u/9054108?u=c27e50269f1ef8ea950cc6f0268c8ec5cebbe9c9&v=4 @@ -109,10 +113,6 @@ experts: count: 21 avatarUrl: https://avatars.githubusercontent.com/u/565544?v=4 url: https://github.com/chris-allnutt -- login: panla - count: 21 - avatarUrl: https://avatars.githubusercontent.com/u/41326348?u=ba2fda6b30110411ecbf406d187907e2b420ac19&v=4 - url: https://github.com/panla - login: retnikt count: 19 avatarUrl: https://avatars.githubusercontent.com/u/24581770?v=4 @@ -137,14 +137,14 @@ experts: count: 15 avatarUrl: https://avatars.githubusercontent.com/u/685002?u=b5094ab4527fc84b006c0ac9ff54367bdebb2267&v=4 url: https://github.com/acidjunk +- login: dstlny + count: 14 + avatarUrl: https://avatars.githubusercontent.com/u/41964673?u=9f2174f9d61c15c6e3a4c9e3aeee66f711ce311f&v=4 + url: https://github.com/dstlny - login: haizaar count: 13 avatarUrl: https://avatars.githubusercontent.com/u/58201?u=4f1f9843d69433ca0d380d95146cfe119e5fdac4&v=4 url: https://github.com/haizaar -- login: dstlny - count: 13 - avatarUrl: https://avatars.githubusercontent.com/u/41964673?u=9f2174f9d61c15c6e3a4c9e3aeee66f711ce311f&v=4 - url: https://github.com/dstlny - login: David-Lor count: 12 avatarUrl: https://avatars.githubusercontent.com/u/17401854?u=474680c02b94cba810cb9032fb7eb787d9cc9d22&v=4 @@ -182,22 +182,30 @@ experts: avatarUrl: https://avatars.githubusercontent.com/u/2858306?u=1bb1182a5944e93624b7fb26585f22c8f7a9d76e&v=4 url: https://github.com/oligond last_month_active: +- login: insomnes + count: 10 + avatarUrl: https://avatars.githubusercontent.com/u/16958893?u=f8be7088d5076d963984a21f95f44e559192d912&v=4 + url: https://github.com/insomnes - login: raphaelauv count: 6 avatarUrl: https://avatars.githubusercontent.com/u/10202690?u=e6f86f5c0c3026a15d6b51792fa3e532b12f1371&v=4 url: https://github.com/raphaelauv -- login: oligond - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/2858306?u=1bb1182a5944e93624b7fb26585f22c8f7a9d76e&v=4 - url: https://github.com/oligond +- login: jgould22 + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 + url: https://github.com/jgould22 +- login: harunyasar + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/1765494?u=5b1ab7c582db4b4016fa31affe977d10af108ad4&v=4 + url: https://github.com/harunyasar - login: Kludex - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=cf8455cb899806b774a3a71073f88583adec99f6&v=4 - url: https://github.com/Kludex -- login: Dustyposa count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/27180793?u=5cf2877f50b3eb2bc55086089a78a36f07042889&v=4 - url: https://github.com/Dustyposa + avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=3682d9b9b93bef272f379ab623dc031c8d71432e&v=4 + url: https://github.com/Kludex +- login: panla + count: 3 + avatarUrl: https://avatars.githubusercontent.com/u/41326348?u=ba2fda6b30110411ecbf406d187907e2b420ac19&v=4 + url: https://github.com/panla top_contributors: - login: waynerv count: 25 @@ -237,7 +245,7 @@ top_contributors: url: https://github.com/RunningIkkyu - login: Kludex count: 7 - avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=cf8455cb899806b774a3a71073f88583adec99f6&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=3682d9b9b93bef272f379ab623dc031c8d71432e&v=4 url: https://github.com/Kludex - login: hard-coders count: 7 @@ -273,12 +281,12 @@ top_contributors: url: https://github.com/komtaki - login: NinaHwang count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/79563565?u=36ca24be1fc3daa967b0f409bab0e17132d8c80c&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/79563565?u=1741703bd6c8f491503354b363a86e879b4c1cab&v=4 url: https://github.com/NinaHwang top_reviewers: - login: Kludex count: 91 - avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=cf8455cb899806b774a3a71073f88583adec99f6&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=3682d9b9b93bef272f379ab623dc031c8d71432e&v=4 url: https://github.com/Kludex - login: waynerv count: 47 @@ -364,6 +372,10 @@ top_reviewers: count: 12 avatarUrl: https://avatars.githubusercontent.com/u/6478810?u=af15d724875cec682ed8088a86d36b2798f981c0&v=4 url: https://github.com/sh0nk +- login: yezz123 + count: 11 + avatarUrl: https://avatars.githubusercontent.com/u/52716203?u=636b4f79645176df4527dd45c12d5dbb5a4193cf&v=4 + url: https://github.com/yezz123 - login: mariacamilagl count: 10 avatarUrl: https://avatars.githubusercontent.com/u/11489395?u=4adb6986bf3debfc2b8216ae701f2bd47d73da7d&v=4 @@ -376,10 +388,6 @@ top_reviewers: count: 10 avatarUrl: https://avatars.githubusercontent.com/u/7887703?v=4 url: https://github.com/maoyibo -- login: yezz123 - count: 9 - avatarUrl: https://avatars.githubusercontent.com/u/52716203?u=636b4f79645176df4527dd45c12d5dbb5a4193cf&v=4 - url: https://github.com/yezz123 - login: graingert count: 9 avatarUrl: https://avatars.githubusercontent.com/u/413772?v=4 @@ -420,6 +428,10 @@ top_reviewers: count: 7 avatarUrl: https://avatars.githubusercontent.com/u/10202690?u=e6f86f5c0c3026a15d6b51792fa3e532b12f1371&v=4 url: https://github.com/raphaelauv +- login: BilalAlpaslan + count: 7 + avatarUrl: https://avatars.githubusercontent.com/u/47563997?u=63ed66e304fe8d765762c70587d61d9196e5c82d&v=4 + url: https://github.com/BilalAlpaslan - login: NastasiaSaby count: 7 avatarUrl: https://avatars.githubusercontent.com/u/8245071?u=b3afd005f9e4bf080c219ef61a592b3a8004b764&v=4 @@ -436,10 +448,6 @@ top_reviewers: count: 6 avatarUrl: https://avatars.githubusercontent.com/u/21287303?u=b049eac3e51a4c0473c2efe66b4d28a7d8f2b572&v=4 url: https://github.com/jovicon -- login: BilalAlpaslan - count: 6 - avatarUrl: https://avatars.githubusercontent.com/u/47563997?u=63ed66e304fe8d765762c70587d61d9196e5c82d&v=4 - url: https://github.com/BilalAlpaslan - login: diogoduartec count: 5 avatarUrl: https://avatars.githubusercontent.com/u/31852339?u=b50fc11c531e9b77922e19edfc9e7233d4d7b92e&v=4 @@ -448,6 +456,10 @@ top_reviewers: count: 5 avatarUrl: https://avatars.githubusercontent.com/u/49960770?u=e39b11d47188744ee07b2a1c7ce1a1bdf3c80760&v=4 url: https://github.com/nimctl +- login: israteneda + count: 5 + avatarUrl: https://avatars.githubusercontent.com/u/20668624?u=d7b2961d330aca65fbce5bdb26a0800a3d23ed2d&v=4 + url: https://github.com/israteneda - login: juntatalor count: 5 avatarUrl: https://avatars.githubusercontent.com/u/8134632?v=4 @@ -464,11 +476,7 @@ top_reviewers: count: 5 avatarUrl: https://avatars.githubusercontent.com/u/83456692?v=4 url: https://github.com/oandersonmagalhaes -- login: euri10 - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/1104190?u=321a2e953e6645a7d09b732786c7a8061e0f8a8b&v=4 - url: https://github.com/euri10 -- login: rkbeatss - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/23391143?u=56ab6bff50be950fa8cae5cf736f2ae66e319ff3&v=4 - url: https://github.com/rkbeatss +- login: qysfblog + count: 5 + avatarUrl: https://avatars.githubusercontent.com/u/52229895?v=4 + url: https://github.com/qysfblog From 66c27c3e0752b63bfe6e843dcf2af28d774caaa7 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 7 Jan 2022 14:19:23 +0000 Subject: [PATCH 19/59] =?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 9453909be..02e2c8879 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 [#4354](https://github.com/tiangolo/fastapi/pull/4354) by [@github-actions[bot]](https://github.com/apps/github-actions). * 🔧 Add FastAPI Trove Classifier for PyPI as now there's one 🤷😁. PR [#4386](https://github.com/tiangolo/fastapi/pull/4386) by [@tiangolo](https://github.com/tiangolo). * ✨ Add docs and tests for Python 3.9 and Python 3.10. PR [#3712](https://github.com/tiangolo/fastapi/pull/3712) by [@tiangolo](https://github.com/tiangolo). * ⬆ Upgrade MkDocs Material and configs. PR [#4385](https://github.com/tiangolo/fastapi/pull/4385) by [@tiangolo](https://github.com/tiangolo). From 672c55ac626b20cc6219696023cdcc5871e5141d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 7 Jan 2022 18:07:59 +0100 Subject: [PATCH 20/59] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.71.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 14 ++++++++++++-- fastapi/__init__.py | 2 +- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 02e2c8879..01df31267 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,11 +2,21 @@ ## Latest Changes +## 0.71.0 + +### Features + +* ✨ Add docs and tests for Python 3.9 and Python 3.10. PR [#3712](https://github.com/tiangolo/fastapi/pull/3712) by [@tiangolo](https://github.com/tiangolo). + * You can start with [Python Types Intro](https://fastapi.tiangolo.com/python-types/), it explains what changes between different Python versions, in Python 3.9 and in Python 3.10. + * All the FastAPI docs are updated. Each code example in the docs that could use different syntax in Python 3.9 or Python 3.10 now has all the alternatives in tabs. +* ⬆️ Upgrade Starlette to 0.17.1. PR [#4145](https://github.com/tiangolo/fastapi/pull/4145) by [@simondale00](https://github.com/simondale00). + +### Internal + * 👥 Update FastAPI People. PR [#4354](https://github.com/tiangolo/fastapi/pull/4354) by [@github-actions[bot]](https://github.com/apps/github-actions). * 🔧 Add FastAPI Trove Classifier for PyPI as now there's one 🤷😁. PR [#4386](https://github.com/tiangolo/fastapi/pull/4386) by [@tiangolo](https://github.com/tiangolo). -* ✨ Add docs and tests for Python 3.9 and Python 3.10. PR [#3712](https://github.com/tiangolo/fastapi/pull/3712) by [@tiangolo](https://github.com/tiangolo). * ⬆ Upgrade MkDocs Material and configs. PR [#4385](https://github.com/tiangolo/fastapi/pull/4385) by [@tiangolo](https://github.com/tiangolo). -* ⬆️ Upgrade Starlette to 0.17.1. PR [#4145](https://github.com/tiangolo/fastapi/pull/4145) by [@simondale00](https://github.com/simondale00). + ## 0.70.1 There's nothing interesting in this particular FastAPI release. It is mainly to enable/unblock the release of the next version of Pydantic that comes packed with features and improvements. 🤩 diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 8bb6ce15b..5b735aed5 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.70.1" +__version__ = "0.71.0" from starlette import status as status From ca2b1dbb648b24ba776cf73f4dfa7026d5ff4a0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 16 Jan 2022 15:34:45 +0100 Subject: [PATCH 21/59] =?UTF-8?q?=F0=9F=94=A7=20Enable=20MkDocs=20Material?= =?UTF-8?q?=20Insiders'=20`content.tabs.link`=20(#4399)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/az/mkdocs.yml | 1 + docs/de/mkdocs.yml | 1 + docs/en/mkdocs.yml | 1 + docs/es/mkdocs.yml | 1 + docs/fr/mkdocs.yml | 1 + docs/id/mkdocs.yml | 1 + docs/it/mkdocs.yml | 1 + docs/ja/mkdocs.yml | 1 + docs/ko/mkdocs.yml | 1 + docs/pl/mkdocs.yml | 1 + docs/pt/mkdocs.yml | 1 + docs/ru/mkdocs.yml | 1 + docs/sq/mkdocs.yml | 1 + docs/tr/mkdocs.yml | 1 + docs/uk/mkdocs.yml | 1 + docs/zh/mkdocs.yml | 1 + 16 files changed, 16 insertions(+) diff --git a/docs/az/mkdocs.yml b/docs/az/mkdocs.yml index dbff7b26a..66220f63e 100644 --- a/docs/az/mkdocs.yml +++ b/docs/az/mkdocs.yml @@ -20,6 +20,7 @@ theme: features: - search.suggest - search.highlight + - content.tabs.link icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg diff --git a/docs/de/mkdocs.yml b/docs/de/mkdocs.yml index 8f29ef316..360fa8c4a 100644 --- a/docs/de/mkdocs.yml +++ b/docs/de/mkdocs.yml @@ -20,6 +20,7 @@ theme: features: - search.suggest - search.highlight + - content.tabs.link icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index b3716f508..5bdd2c54a 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -20,6 +20,7 @@ theme: features: - search.suggest - search.highlight + - content.tabs.link icon: repo: fontawesome/brands/github-alt logo: img/icon-white.svg diff --git a/docs/es/mkdocs.yml b/docs/es/mkdocs.yml index 06b015a1d..a4bc41154 100644 --- a/docs/es/mkdocs.yml +++ b/docs/es/mkdocs.yml @@ -20,6 +20,7 @@ theme: features: - search.suggest - search.highlight + - content.tabs.link icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg diff --git a/docs/fr/mkdocs.yml b/docs/fr/mkdocs.yml index c42f92b8a..ff16e1d78 100644 --- a/docs/fr/mkdocs.yml +++ b/docs/fr/mkdocs.yml @@ -20,6 +20,7 @@ theme: features: - search.suggest - search.highlight + - content.tabs.link icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg diff --git a/docs/id/mkdocs.yml b/docs/id/mkdocs.yml index 0dc34a14f..d70d2b3c3 100644 --- a/docs/id/mkdocs.yml +++ b/docs/id/mkdocs.yml @@ -20,6 +20,7 @@ theme: features: - search.suggest - search.highlight + - content.tabs.link icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg diff --git a/docs/it/mkdocs.yml b/docs/it/mkdocs.yml index 18afbd547..e6d01fbde 100644 --- a/docs/it/mkdocs.yml +++ b/docs/it/mkdocs.yml @@ -20,6 +20,7 @@ theme: features: - search.suggest - search.highlight + - content.tabs.link icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg diff --git a/docs/ja/mkdocs.yml b/docs/ja/mkdocs.yml index 327fe862a..39fd8a211 100644 --- a/docs/ja/mkdocs.yml +++ b/docs/ja/mkdocs.yml @@ -20,6 +20,7 @@ theme: features: - search.suggest - search.highlight + - content.tabs.link icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg diff --git a/docs/ko/mkdocs.yml b/docs/ko/mkdocs.yml index 5ffb71877..1d4d30913 100644 --- a/docs/ko/mkdocs.yml +++ b/docs/ko/mkdocs.yml @@ -20,6 +20,7 @@ theme: features: - search.suggest - search.highlight + - content.tabs.link icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg diff --git a/docs/pl/mkdocs.yml b/docs/pl/mkdocs.yml index 724dc2725..3c1351a12 100644 --- a/docs/pl/mkdocs.yml +++ b/docs/pl/mkdocs.yml @@ -20,6 +20,7 @@ theme: features: - search.suggest - search.highlight + - content.tabs.link icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg diff --git a/docs/pt/mkdocs.yml b/docs/pt/mkdocs.yml index f36ecf49d..f202f306d 100644 --- a/docs/pt/mkdocs.yml +++ b/docs/pt/mkdocs.yml @@ -20,6 +20,7 @@ theme: features: - search.suggest - search.highlight + - content.tabs.link icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg diff --git a/docs/ru/mkdocs.yml b/docs/ru/mkdocs.yml index eb2597de7..6e17c287e 100644 --- a/docs/ru/mkdocs.yml +++ b/docs/ru/mkdocs.yml @@ -20,6 +20,7 @@ theme: features: - search.suggest - search.highlight + - content.tabs.link icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg diff --git a/docs/sq/mkdocs.yml b/docs/sq/mkdocs.yml index 5afe0a665..d9c3dad4c 100644 --- a/docs/sq/mkdocs.yml +++ b/docs/sq/mkdocs.yml @@ -20,6 +20,7 @@ theme: features: - search.suggest - search.highlight + - content.tabs.link icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg diff --git a/docs/tr/mkdocs.yml b/docs/tr/mkdocs.yml index 40ba6d522..f6ed7f5b9 100644 --- a/docs/tr/mkdocs.yml +++ b/docs/tr/mkdocs.yml @@ -20,6 +20,7 @@ theme: features: - search.suggest - search.highlight + - content.tabs.link icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg diff --git a/docs/uk/mkdocs.yml b/docs/uk/mkdocs.yml index a116d90a3..d0de8cc0e 100644 --- a/docs/uk/mkdocs.yml +++ b/docs/uk/mkdocs.yml @@ -20,6 +20,7 @@ theme: features: - search.suggest - search.highlight + - content.tabs.link icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg diff --git a/docs/zh/mkdocs.yml b/docs/zh/mkdocs.yml index 33d40129a..a929e3388 100644 --- a/docs/zh/mkdocs.yml +++ b/docs/zh/mkdocs.yml @@ -20,6 +20,7 @@ theme: features: - search.suggest - search.highlight + - content.tabs.link icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg From b8ae84d460f331b8adc4d25b5b6f0bcb40baaac8 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 16 Jan 2022 14:35:21 +0000 Subject: [PATCH 22/59] =?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 01df31267..5bf2302b2 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔧 Enable MkDocs Material Insiders' `content.tabs.link`. PR [#4399](https://github.com/tiangolo/fastapi/pull/4399) by [@tiangolo](https://github.com/tiangolo). ## 0.71.0 ### Features From 7fe79441c1cb7cf3baecc271a3e9680006c8f2c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 16 Jan 2022 15:44:08 +0100 Subject: [PATCH 23/59] =?UTF-8?q?=F0=9F=93=9D=20Update=20Python=20Types=20?= =?UTF-8?q?docs,=20add=20missing=203.6=20/=203.9=20example=20(#4434)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/python-types.md | 2 +- docs_src/python_types/tutorial008.py | 5 ++++- docs_src/python_types/tutorial008_py39.py | 4 ++++ 3 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 docs_src/python_types/tutorial008_py39.py diff --git a/docs/en/docs/python-types.md b/docs/en/docs/python-types.md index 76d442855..fe56dadec 100644 --- a/docs/en/docs/python-types.md +++ b/docs/en/docs/python-types.md @@ -252,7 +252,7 @@ The second type parameter is for the values of the `dict`: === "Python 3.9 and above" ```Python hl_lines="1" - {!> ../../../docs_src/python_types/tutorial008.py!} + {!> ../../../docs_src/python_types/tutorial008_py39.py!} ``` This means: diff --git a/docs_src/python_types/tutorial008.py b/docs_src/python_types/tutorial008.py index a393385b0..9fb1043bb 100644 --- a/docs_src/python_types/tutorial008.py +++ b/docs_src/python_types/tutorial008.py @@ -1,4 +1,7 @@ -def process_items(prices: dict[str, float]): +from typing import Dict + + +def process_items(prices: Dict[str, float]): for item_name, item_price in prices.items(): print(item_name) print(item_price) diff --git a/docs_src/python_types/tutorial008_py39.py b/docs_src/python_types/tutorial008_py39.py new file mode 100644 index 000000000..a393385b0 --- /dev/null +++ b/docs_src/python_types/tutorial008_py39.py @@ -0,0 +1,4 @@ +def process_items(prices: dict[str, float]): + for item_name, item_price in prices.items(): + print(item_name) + print(item_price) From 9af8cc69d509cd8538bba3d481687d9f85cb52b4 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 16 Jan 2022 14:44:43 +0000 Subject: [PATCH 24/59] =?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 5bf2302b2..b33e9dcfc 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Update Python Types docs, add missing 3.6 / 3.9 example. PR [#4434](https://github.com/tiangolo/fastapi/pull/4434) by [@tiangolo](https://github.com/tiangolo). * 🔧 Enable MkDocs Material Insiders' `content.tabs.link`. PR [#4399](https://github.com/tiangolo/fastapi/pull/4399) by [@tiangolo](https://github.com/tiangolo). ## 0.71.0 From a85aa125d24acb225b7b722bb4f3e331a21ec267 Mon Sep 17 00:00:00 2001 From: John Riebold Date: Sun, 16 Jan 2022 11:26:24 -0800 Subject: [PATCH 25/59] =?UTF-8?q?=E2=9C=A8=20Enable=20configuring=20Swagge?= =?UTF-8?q?r=20UI=20parameters=20(#2568)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Artem Ivanov Co-authored-by: Sebastián Ramírez --- docs/en/docs/advanced/extending-openapi.md | 79 ++++++++++++++++++ .../tutorial/extending-openapi/image02.png | Bin 0 -> 11292 bytes .../tutorial/extending-openapi/image03.png | Bin 0 -> 11176 bytes .../tutorial/extending-openapi/image04.png | Bin 0 -> 11180 bytes docs_src/extending_openapi/tutorial003.py | 8 ++ docs_src/extending_openapi/tutorial004.py | 8 ++ docs_src/extending_openapi/tutorial005.py | 8 ++ fastapi/applications.py | 3 + fastapi/openapi/docs.py | 22 +++-- .../test_tutorial003.py | 41 +++++++++ .../test_tutorial004.py | 44 ++++++++++ .../test_tutorial005.py | 44 ++++++++++ 12 files changed, 251 insertions(+), 6 deletions(-) create mode 100644 docs/en/docs/img/tutorial/extending-openapi/image02.png create mode 100644 docs/en/docs/img/tutorial/extending-openapi/image03.png create mode 100644 docs/en/docs/img/tutorial/extending-openapi/image04.png create mode 100644 docs_src/extending_openapi/tutorial003.py create mode 100644 docs_src/extending_openapi/tutorial004.py create mode 100644 docs_src/extending_openapi/tutorial005.py create mode 100644 tests/test_tutorial/test_extending_openapi/test_tutorial003.py create mode 100644 tests/test_tutorial/test_extending_openapi/test_tutorial004.py create mode 100644 tests/test_tutorial/test_extending_openapi/test_tutorial005.py diff --git a/docs/en/docs/advanced/extending-openapi.md b/docs/en/docs/advanced/extending-openapi.md index d8d280ba6..d1b14bc00 100644 --- a/docs/en/docs/advanced/extending-openapi.md +++ b/docs/en/docs/advanced/extending-openapi.md @@ -233,3 +233,82 @@ Now, to be able to test that everything works, create a *path operation*: Now, you should be able to disconnect your WiFi, go to your docs at
http://127.0.0.1:8000/docs, and reload the page. And even without Internet, you would be able to see the docs for your API and interact with it. + +## Configuring Swagger UI + +You can configure some extra Swagger UI parameters. + +To configure them, pass the `swagger_ui_parameters` argument when creating the `FastAPI()` app object or to the `get_swagger_ui_html()` function. + +`swagger_ui_parameters` receives a dictionary with the configurations passed to Swagger UI directly. + +FastAPI converts the configurations to **JSON** to make them compatible with JavaScript, as that's what Swagger UI needs. + +### Disable Syntax Highlighting + +For example, you could disable syntax highlighting in Swagger UI. + +Without changing the settings, syntax highlighting is enabled by default: + + + +But you can disable it by setting `syntaxHighlight` to `False`: + +```Python hl_lines="3" +{!../../../docs_src/extending_openapi/tutorial003.py!} +``` + +...and then Swagger UI won't show the syntax highlighting anymore: + + + +### Change the Theme + +The same way you could set the syntax highlighting theme with the key `"syntaxHighlight.theme"` (notice that it has a dot in the middle): + +```Python hl_lines="3" +{!../../../docs_src/extending_openapi/tutorial004.py!} +``` + +That configuration would change the syntax highlighting color theme: + + + +### Change Default Swagger UI Parameters + +FastAPI includes some default configuration parameters appropriate for most of the use cases. + +It includes these default configurations: + +```Python +{!../../../fastapi/openapi/docs.py[ln:7-13]!} +``` + +You can override any of them by setting a different value in the argument `swagger_ui_parameters`. + +For example, to disable `deepLinking` you could pass these settings to `swagger_ui_parameters`: + +```Python hl_lines="3" +{!../../../docs_src/extending_openapi/tutorial005.py!} +``` + +### Other Swagger UI Parameters + +To see all the other possible configurations you can use, read the official docs for Swagger UI parameters. + +### JavaScript-only settings + +Swagger UI also allows other configurations to be **JavaScript-only** objects (for example, JavaScript functions). + +FastAPI also includes these JavaScript-only `presets` settings: + +```JavaScript +presets: [ + SwaggerUIBundle.presets.apis, + SwaggerUIBundle.SwaggerUIStandalonePreset +] +``` + +These are **JavaScript** objects, not strings, so you can't pass them from Python code directly. + +If you need to use JavaScript-only configurations like those, you can use one of the methods above. Override all the Swagger UI *path operation* and manually write any JavaScript you need. diff --git a/docs/en/docs/img/tutorial/extending-openapi/image02.png b/docs/en/docs/img/tutorial/extending-openapi/image02.png new file mode 100644 index 0000000000000000000000000000000000000000..91453fb56f687b99fc725afa0a7fa07eb1955137 GIT binary patch literal 11292 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 literal 0 HcmV?d00001 diff --git a/docs/en/docs/img/tutorial/extending-openapi/image04.png b/docs/en/docs/img/tutorial/extending-openapi/image04.png new file mode 100644 index 0000000000000000000000000000000000000000..394d2bb431ab5eadaa919087ce1618f996bd829e GIT binary patch literal 11180 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 None: self._debug: bool = debug @@ -120,6 +121,7 @@ class FastAPI(Starlette): self.redoc_url = redoc_url self.swagger_ui_oauth2_redirect_url = swagger_ui_oauth2_redirect_url self.swagger_ui_init_oauth = swagger_ui_init_oauth + self.swagger_ui_parameters = swagger_ui_parameters self.extra = extra self.dependency_overrides: Dict[Callable[..., Any], Callable[..., Any]] = {} @@ -174,6 +176,7 @@ class FastAPI(Starlette): title=self.title + " - Swagger UI", oauth2_redirect_url=oauth2_redirect_url, init_oauth=self.swagger_ui_init_oauth, + swagger_ui_parameters=self.swagger_ui_parameters, ) self.add_route(self.docs_url, swagger_ui_html, include_in_schema=False) diff --git a/fastapi/openapi/docs.py b/fastapi/openapi/docs.py index fd22e4e8c..1be90d188 100644 --- a/fastapi/openapi/docs.py +++ b/fastapi/openapi/docs.py @@ -4,6 +4,14 @@ from typing import Any, Dict, Optional from fastapi.encoders import jsonable_encoder from starlette.responses import HTMLResponse +swagger_ui_default_parameters = { + "dom_id": "#swagger-ui", + "layout": "BaseLayout", + "deepLinking": True, + "showExtensions": True, + "showCommonExtensions": True, +} + def get_swagger_ui_html( *, @@ -14,7 +22,11 @@ def get_swagger_ui_html( swagger_favicon_url: str = "https://fastapi.tiangolo.com/img/favicon.png", oauth2_redirect_url: Optional[str] = None, init_oauth: Optional[Dict[str, Any]] = None, + swagger_ui_parameters: Optional[Dict[str, Any]] = None, ) -> HTMLResponse: + current_swagger_ui_parameters = swagger_ui_default_parameters.copy() + if swagger_ui_parameters: + current_swagger_ui_parameters.update(swagger_ui_parameters) html = f""" @@ -34,19 +46,17 @@ def get_swagger_ui_html( url: '{openapi_url}', """ + for key, value in current_swagger_ui_parameters.items(): + html += f"{json.dumps(key)}: {json.dumps(jsonable_encoder(value))},\n" + if oauth2_redirect_url: html += f"oauth2RedirectUrl: window.location.origin + '{oauth2_redirect_url}'," html += """ - dom_id: '#swagger-ui', - presets: [ + presets: [ SwaggerUIBundle.presets.apis, SwaggerUIBundle.SwaggerUIStandalonePreset ], - layout: "BaseLayout", - deepLinking: true, - showExtensions: true, - showCommonExtensions: true })""" if init_oauth: diff --git a/tests/test_tutorial/test_extending_openapi/test_tutorial003.py b/tests/test_tutorial/test_extending_openapi/test_tutorial003.py new file mode 100644 index 000000000..0184dd9f8 --- /dev/null +++ b/tests/test_tutorial/test_extending_openapi/test_tutorial003.py @@ -0,0 +1,41 @@ +from fastapi.testclient import TestClient + +from docs_src.extending_openapi.tutorial003 import app + +client = TestClient(app) + + +def test_swagger_ui(): + response = client.get("/docs") + assert response.status_code == 200, response.text + assert ( + '"syntaxHighlight": false' in response.text + ), "syntaxHighlight should be included and converted to JSON" + assert ( + '"dom_id": "#swagger-ui"' in response.text + ), "default configs should be preserved" + assert "presets: [" in response.text, "default configs should be preserved" + assert ( + "SwaggerUIBundle.presets.apis," in response.text + ), "default configs should be preserved" + assert ( + "SwaggerUIBundle.SwaggerUIStandalonePreset" in response.text + ), "default configs should be preserved" + assert ( + '"layout": "BaseLayout",' in response.text + ), "default configs should be preserved" + assert ( + '"deepLinking": true,' in response.text + ), "default configs should be preserved" + assert ( + '"showExtensions": true,' in response.text + ), "default configs should be preserved" + assert ( + '"showCommonExtensions": true,' in response.text + ), "default configs should be preserved" + + +def test_get_users(): + response = client.get("/users/foo") + assert response.status_code == 200, response.text + assert response.json() == {"message": "Hello foo"} diff --git a/tests/test_tutorial/test_extending_openapi/test_tutorial004.py b/tests/test_tutorial/test_extending_openapi/test_tutorial004.py new file mode 100644 index 000000000..4f7615126 --- /dev/null +++ b/tests/test_tutorial/test_extending_openapi/test_tutorial004.py @@ -0,0 +1,44 @@ +from fastapi.testclient import TestClient + +from docs_src.extending_openapi.tutorial004 import app + +client = TestClient(app) + + +def test_swagger_ui(): + response = client.get("/docs") + assert response.status_code == 200, response.text + assert ( + '"syntaxHighlight": false' not in response.text + ), "not used parameters should not be included" + assert ( + '"syntaxHighlight.theme": "obsidian"' in response.text + ), "parameters with middle dots should be included in a JSON compatible way" + assert ( + '"dom_id": "#swagger-ui"' in response.text + ), "default configs should be preserved" + assert "presets: [" in response.text, "default configs should be preserved" + assert ( + "SwaggerUIBundle.presets.apis," in response.text + ), "default configs should be preserved" + assert ( + "SwaggerUIBundle.SwaggerUIStandalonePreset" in response.text + ), "default configs should be preserved" + assert ( + '"layout": "BaseLayout",' in response.text + ), "default configs should be preserved" + assert ( + '"deepLinking": true,' in response.text + ), "default configs should be preserved" + assert ( + '"showExtensions": true,' in response.text + ), "default configs should be preserved" + assert ( + '"showCommonExtensions": true,' in response.text + ), "default configs should be preserved" + + +def test_get_users(): + response = client.get("/users/foo") + assert response.status_code == 200, response.text + assert response.json() == {"message": "Hello foo"} diff --git a/tests/test_tutorial/test_extending_openapi/test_tutorial005.py b/tests/test_tutorial/test_extending_openapi/test_tutorial005.py new file mode 100644 index 000000000..24aeb93db --- /dev/null +++ b/tests/test_tutorial/test_extending_openapi/test_tutorial005.py @@ -0,0 +1,44 @@ +from fastapi.testclient import TestClient + +from docs_src.extending_openapi.tutorial005 import app + +client = TestClient(app) + + +def test_swagger_ui(): + response = client.get("/docs") + assert response.status_code == 200, response.text + assert ( + '"deepLinking": false,' in response.text + ), "overridden configs should be preserved" + assert ( + '"deepLinking": true' not in response.text + ), "overridden configs should not include the old value" + assert ( + '"syntaxHighlight": false' not in response.text + ), "not used parameters should not be included" + assert ( + '"dom_id": "#swagger-ui"' in response.text + ), "default configs should be preserved" + assert "presets: [" in response.text, "default configs should be preserved" + assert ( + "SwaggerUIBundle.presets.apis," in response.text + ), "default configs should be preserved" + assert ( + "SwaggerUIBundle.SwaggerUIStandalonePreset" in response.text + ), "default configs should be preserved" + assert ( + '"layout": "BaseLayout",' in response.text + ), "default configs should be preserved" + assert ( + '"showExtensions": true,' in response.text + ), "default configs should be preserved" + assert ( + '"showCommonExtensions": true,' in response.text + ), "default configs should be preserved" + + +def test_get_users(): + response = client.get("/users/foo") + assert response.status_code == 200, response.text + assert response.json() == {"message": "Hello foo"} From acbe79da371bc7e3673c86cafeb6d07ca64712f4 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 16 Jan 2022 19:27:03 +0000 Subject: [PATCH 26/59] =?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 b33e9dcfc..2ed9729d0 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✨ Enable configuring Swagger UI parameters. PR [#2568](https://github.com/tiangolo/fastapi/pull/2568) by [@jmriebold](https://github.com/jmriebold). * 📝 Update Python Types docs, add missing 3.6 / 3.9 example. PR [#4434](https://github.com/tiangolo/fastapi/pull/4434) by [@tiangolo](https://github.com/tiangolo). * 🔧 Enable MkDocs Material Insiders' `content.tabs.link`. PR [#4399](https://github.com/tiangolo/fastapi/pull/4399) by [@tiangolo](https://github.com/tiangolo). ## 0.71.0 From 5c62a59e7b99a49ce25c622747e510e211f22692 Mon Sep 17 00:00:00 2001 From: jaystone776 Date: Mon, 17 Jan 2022 03:34:28 +0800 Subject: [PATCH 27/59] =?UTF-8?q?=F0=9F=8C=90=20Add=20Chinese=20translatio?= =?UTF-8?q?n=20for=20`docs\tutorial\path-operation-configuration.md`=20(#3?= =?UTF-8?q?312)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- .../tutorial/path-operation-configuration.md | 101 ++++++++++++++++++ docs/zh/mkdocs.yml | 1 + 2 files changed, 102 insertions(+) create mode 100644 docs/zh/docs/tutorial/path-operation-configuration.md diff --git a/docs/zh/docs/tutorial/path-operation-configuration.md b/docs/zh/docs/tutorial/path-operation-configuration.md new file mode 100644 index 000000000..ad0e08d30 --- /dev/null +++ b/docs/zh/docs/tutorial/path-operation-configuration.md @@ -0,0 +1,101 @@ +# 路径操作配置 + +*路径操作装饰器*支持多种配置参数。 + +!!! warning "警告" + + 注意:以下参数应直接传递给**路径操作装饰器**,不能传递给*路径操作函数*。 + +## `status_code` 状态码 + +`status_code` 用于定义*路径操作*响应中的 HTTP 状态码。 + +可以直接传递 `int` 代码, 比如 `404`。 + +如果记不住数字码的涵义,也可以用 `status` 的快捷常量: + +```Python hl_lines="3 17" +{!../../../docs_src/path_operation_configuration/tutorial001.py!} +``` + +状态码在响应中使用,并会被添加到 OpenAPI 概图。 + +!!! note "技术细节" + + 也可以使用 `from starlette import status` 导入状态码。 + + **FastAPI** 的`fastapi.status` 和 `starlette.status` 一样,只是快捷方式。实际上,`fastapi.status` 直接继承自 Starlette。 + +## `tags` 参数 + +`tags` 参数的值是由 `str` 组成的 `list` (一般只有一个 `str` ),`tags` 用于为*路径操作*添加标签: + +```Python hl_lines="17 22 27" +{!../../../docs_src/path_operation_configuration/tutorial002.py!} +``` + +OpenAPI 概图会自动添加标签,供 API 文档接口使用: + + + +## `summary` 和 `description` 参数 + +路径装饰器还支持 `summary` 和 `description` 这两个参数: + +```Python hl_lines="20-21" +{!../../../docs_src/path_operation_configuration/tutorial003.py!} +``` + +## 文档字符串(`docstring`) + +描述内容比较长且占用多行时,可以在函数的 docstring 中声明*路径操作*的描述,**FastAPI** 支持从文档字符串中读取描述内容。 + +文档字符串支持 Markdown,能正确解析和显示 Markdown 的内容,但要注意文档字符串的缩进。 + +```Python hl_lines="19-27" +{!../../../docs_src/path_operation_configuration/tutorial004.py!} +``` + +下图为 Markdown 文本在 API 文档中的显示效果: + + + +## 响应描述 + +`response_description` 参数用于定义响应的描述说明: + +```Python hl_lines="21" +{!../../../docs_src/path_operation_configuration/tutorial005.py!} +``` + +!!! info "说明" + + 注意,`response_description` 只用于描述响应,`description` 一般则用于描述*路径操作*。 + +!!! check "检查" + + OpenAPI 规定每个*路径操作*都要有响应描述。 + + 如果没有定义响应描述,**FastAPI** 则自动生成内容为 "Successful response" 的响应描述。 + + + +## 弃用*路径操作* + +`deprecated` 参数可以把*路径操作*标记为弃用,无需直接删除: + +```Python hl_lines="16" +{!../../../docs_src/path_operation_configuration/tutorial006.py!} +``` + +API 文档会把该路径操作标记为弃用: + + + +下图显示了正常*路径操作*与弃用*路径操作* 的区别: + + + +## 小结 + +通过传递参数给*路径操作装饰器* ,即可轻松地配置*路径操作*、添加元数据。 diff --git a/docs/zh/mkdocs.yml b/docs/zh/mkdocs.yml index a929e3388..1d050fddd 100644 --- a/docs/zh/mkdocs.yml +++ b/docs/zh/mkdocs.yml @@ -78,6 +78,7 @@ nav: - tutorial/request-files.md - tutorial/request-forms-and-files.md - tutorial/handling-errors.md + - tutorial/path-operation-configuration.md - tutorial/body-updates.md - 依赖项: - tutorial/dependencies/index.md From 436261b3ea75a095efbf67c4d537baa588331301 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 16 Jan 2022 19:35:00 +0000 Subject: [PATCH 28/59] =?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 2ed9729d0..9066792d8 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Chinese translation for `docs\tutorial\path-operation-configuration.md`. PR [#3312](https://github.com/tiangolo/fastapi/pull/3312) by [@jaystone776](https://github.com/jaystone776). * ✨ Enable configuring Swagger UI parameters. PR [#2568](https://github.com/tiangolo/fastapi/pull/2568) by [@jmriebold](https://github.com/jmriebold). * 📝 Update Python Types docs, add missing 3.6 / 3.9 example. PR [#4434](https://github.com/tiangolo/fastapi/pull/4434) by [@tiangolo](https://github.com/tiangolo). * 🔧 Enable MkDocs Material Insiders' `content.tabs.link`. PR [#4399](https://github.com/tiangolo/fastapi/pull/4399) by [@tiangolo](https://github.com/tiangolo). From 5c5b889288f6330e2a64ab74c9bf50c95e829190 Mon Sep 17 00:00:00 2001 From: MicroPanda123 Date: Sun, 16 Jan 2022 19:36:42 +0000 Subject: [PATCH 29/59] =?UTF-8?q?=F0=9F=8C=90=20Add=20Polish=20translation?= =?UTF-8?q?=20for=20`docs/pl/docs/index.md`=20(#4245)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Dawid Dutkiewicz Co-authored-by: Dima Tisnek Co-authored-by: Bart Skowron Co-authored-by: Bart Skowron --- docs/pl/docs/index.md | 306 +++++++++++++++++++++--------------------- 1 file changed, 150 insertions(+), 156 deletions(-) diff --git a/docs/pl/docs/index.md b/docs/pl/docs/index.md index 95fb7ae21..4a300ae63 100644 --- a/docs/pl/docs/index.md +++ b/docs/pl/docs/index.md @@ -1,12 +1,8 @@ - -{!../../../docs/missing-translation.md!} - -

FastAPI

- FastAPI framework, high performance, easy to learn, fast to code, ready for production + FastAPI to szybki, prosty w nauce i gotowy do użycia w produkcji framework

@@ -22,29 +18,28 @@ --- -**Documentation**: https://fastapi.tiangolo.com +**Dokumentacja**: https://fastapi.tiangolo.com -**Source Code**: https://github.com/tiangolo/fastapi +**Kod żródłowy**: 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. +FastAPI to nowoczesny, wydajny framework webowy do budowania API z użyciem Pythona 3.6+ bazujący na standardowym typowaniu Pythona. -The key features are: +Kluczowe cechy: -* **Fast**: Very high performance, on par with **NodeJS** and **Go** (thanks to Starlette and Pydantic). [One of the fastest Python frameworks available](#performance). +* **Wydajność**: FastAPI jest bardzo wydajny, na równi z **NodeJS** oraz **Go** (dzięki Starlette i Pydantic). [Jeden z najszybszych dostępnych frameworków Pythonowych](#wydajnosc). +* **Szybkość kodowania**: Przyśpiesza szybkość pisania nowych funkcjonalności o około 200% do 300%. * +* **Mniejsza ilość błędów**: Zmniejsza ilość ludzkich (dewelopera) błędy o około 40%. * +* **Intuicyjność**: Wspaniałe wsparcie dla edytorów kodu. Dostępne wszędzie automatyczne uzupełnianie kodu. Krótszy czas debugowania. +* **Łatwość**: Zaprojektowany by być prosty i łatwy do nauczenia. Mniej czasu spędzonego na czytanie dokumentacji. +* **Kompaktowość**: Minimalizacja powtarzającego się kodu. Wiele funkcjonalności dla każdej deklaracji parametru. Mniej błędów. +* **Solidność**: Kod gotowy dla środowiska produkcyjnego. Wraz z automatyczną interaktywną dokumentacją. +* **Bazujący na standardach**: Oparty na (i w pełni kompatybilny z) otwartych standardach API: OpenAPI (wcześniej znane jako Swagger) oraz JSON Schema. -* **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. +* oszacowania bazowane na testach wykonanych przez wewnętrzny zespół deweloperów, budujących aplikacie używane na środowisku produkcyjnym. -* estimation based on tests on an internal development team, building production applications. - -## Sponsors +## Sponsorzy @@ -59,9 +54,9 @@ The key features are: -Other sponsors +Inni sponsorzy -## Opinions +## Opinie "_[...] 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._" @@ -101,24 +96,24 @@ The key features are: --- -## **Typer**, the FastAPI of CLIs +## **Typer**, FastAPI aplikacji konsolowych -If you are building a CLI app to be used in the terminal instead of a web API, check out **Typer**. +Jeżeli tworzysz aplikacje CLI, która ma być używana w terminalu zamiast API, sprawdź **Typer**. -**Typer** is FastAPI's little sibling. And it's intended to be the **FastAPI of CLIs**. ⌨️ 🚀 +**Typer** to młodsze rodzeństwo FastAPI. Jego celem jest pozostanie **FastAPI aplikacji konsolowych** . ⌨️ 🚀 -## Requirements +## Wymagania Python 3.6+ -FastAPI stands on the shoulders of giants: +FastAPI oparty jest na: -* Starlette for the web parts. -* Pydantic for the data parts. +* Starlette dla części webowej. +* Pydantic dla części obsługujących dane. -## Installation +## Instalacja

@@ -130,7 +125,7 @@ $ pip install fastapi
-You will also need an ASGI server, for production such as Uvicorn or Hypercorn. +Na serwerze produkcyjnym będziesz także potrzebował serwera ASGI, np. Uvicorn lub Hypercorn.
@@ -142,11 +137,11 @@ $ pip install uvicorn[standard]
-## Example +## Przykład -### Create it +### Stwórz -* Create a file `main.py` with: +* Utwórz plik o nazwie `main.py` z: ```Python from typing import Optional @@ -167,9 +162,9 @@ def read_item(item_id: int, q: Optional[str] = None): ```
-Or use async def... +Albo użyj async def... -If your code uses `async` / `await`, use `async def`: +Jeżeli twój kod korzysta z `async` / `await`, użyj `async def`: ```Python hl_lines="9 14" from typing import Optional @@ -189,15 +184,15 @@ async def read_item(item_id: int, q: Optional[str] = None): return {"item_id": item_id, "q": q} ``` -**Note**: +**Przypis**: -If you don't know, check the _"In a hurry?"_ section about `async` and `await` in the docs. +Jeżeli nie znasz, sprawdź sekcję _"In a hurry?"_ o `async` i `await` w dokumentacji.
-### Run it +### Uruchom -Run the server with: +Uruchom serwer używając:
@@ -214,54 +209,53 @@ INFO: Application startup complete.
-About the command uvicorn main:app --reload... +O komendzie uvicorn main:app --reload... +Komenda `uvicorn main:app` odnosi się do: -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. +* `main`: plik `main.py` ("moduł" w Pythonie). +* `app`: obiekt stworzony w `main.py` w lini `app = FastAPI()`. +* `--reload`: spraw by serwer resetował się po każdej zmianie w kodzie. Używaj tego tylko w środowisku deweloperskim.
-### Check it +### Wypróbuj -Open your browser at http://127.0.0.1:8000/items/5?q=somequery. +Otwórz link http://127.0.0.1:8000/items/5?q=somequery w przeglądarce. -You will see the JSON response as: +Zobaczysz następującą odpowiedź JSON: ```JSON {"item_id": 5, "q": "somequery"} ``` -You already created an API that: +Właśnie stworzyłeś API które: -* 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`. +* Otrzymuje żądania HTTP w _ścieżce_ `/` i `/items/{item_id}`. +* Obie _ścieżki_ używają operacji `GET` (znane także jako _metody_ HTTP). +* _Ścieżka_ `/items/{item_id}` ma _parametr ścieżki_ `item_id` który powinien być obiektem typu `int`. +* _Ścieżka_ `/items/{item_id}` ma opcjonalny _parametr zapytania_ typu `str` o nazwie `q`. -### Interactive API docs +### Interaktywna dokumentacja API -Now go to http://127.0.0.1:8000/docs. +Otwórz teraz stronę http://127.0.0.1:8000/docs. -You will see the automatic interactive API documentation (provided by Swagger UI): +Zobaczysz automatyczną interaktywną dokumentację API (dostarczoną z pomocą Swagger UI): ![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) -### Alternative API docs +### Alternatywna dokumentacja API -And now, go to http://127.0.0.1:8000/redoc. +Otwórz teraz http://127.0.0.1:8000/redoc. -You will see the alternative automatic documentation (provided by ReDoc): +Zobaczysz alternatywną, lecz wciąż automatyczną dokumentację (wygenerowaną z pomocą ReDoc): ![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) -## Example upgrade +## Aktualizacja przykładu -Now modify the file `main.py` to receive a body from a `PUT` request. +Zmodyfikuj teraz plik `main.py`, aby otrzmywał treść (body) żądania `PUT`. -Declare the body using standard Python types, thanks to Pydantic. +Zadeklaruj treść żądania, używając standardowych typów w Pythonie dzięki Pydantic. ```Python hl_lines="4 9-12 25-27" from typing import Optional @@ -293,175 +287,175 @@ 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). +Serwer powinien przeładować się automatycznie (ponieważ dodałeś `--reload` do komendy `uvicorn` powyżej). -### Interactive API docs upgrade +### Zaktualizowana interaktywna dokumentacja API -Now go to http://127.0.0.1:8000/docs. +Wejdź teraz na http://127.0.0.1:8000/docs. -* The interactive API documentation will be automatically updated, including the new body: +* Interaktywna dokumentacja API zaktualizuje sie automatycznie, także z nową treścią żądania (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: +* Kliknij przycisk "Try it out" (wypróbuj), pozwoli Ci to wypełnić parametry i bezpośrednio użyć 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: +* Kliknij potem przycisk "Execute" (wykonaj), interfejs użytkownika połączy się z API, wyśle parametry, otrzyma odpowiedź i wyświetli ją na ekranie: ![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) -### Alternative API docs upgrade +### Zaktualizowana alternatywna dokumentacja API -And now, go to http://127.0.0.1:8000/redoc. +Otwórz teraz http://127.0.0.1:8000/redoc. -* The alternative documentation will also reflect the new query parameter and body: +* Alternatywna dokumentacja również pokaże zaktualizowane parametry i treść żądania (body): ![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) -### Recap +### Podsumowanie -In summary, you declare **once** the types of parameters, body, etc. as function parameters. +Podsumowując, musiałeś zadeklarować typy parametrów, treści żądania (body) itp. tylko **raz**, i są one dostępne jako parametry funkcji. -You do that with standard modern Python types. +Robisz to tak samo jak ze standardowymi typami w Pythonie. -You don't have to learn a new syntax, the methods or classes of a specific library, etc. +Nie musisz sie uczyć żadnej nowej składni, metod lub klas ze specyficznych bibliotek itp. -Just standard **Python 3.6+**. +Po prostu standardowy **Python 3.6+**. -For example, for an `int`: +Na przykład, dla danych typu `int`: ```Python item_id: int ``` -or for a more complex `Item` model: +albo dla bardziej złożonego obiektu `Item`: ```Python item: Item ``` -...and with that single declaration you get: +...i z pojedyńczą deklaracją otrzymujesz: -* 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: +* Wsparcie edytorów kodu, wliczając: + * Auto-uzupełnianie. + * Sprawdzanie typów. +* Walidacja danych: + * Automatyczne i przejrzyste błędy gdy dane są niepoprawne. + * Walidacja nawet dla głęboko zagnieżdżonych obiektów JSON. +* Konwersja danych wejściowych: przychodzących z sieci na Pythonowe typy. Pozwala na przetwarzanie danych: * 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: + * Parametrów ścieżki. + * Parametrów zapytania. + * Dane cookies. + * Dane nagłówków (headers). + * Formularze. + * Pliki. +* Konwersja danych wyjściowych: wychodzących z Pythona do sieci (jako JSON): + * Przetwarzanie Pythonowych typów (`str`, `int`, `float`, `bool`, `list`, itp). + * Obiekty `datetime`. + * Obiekty `UUID`. + * Modele baz danych. + * ...i wiele więcej. +* Automatyczne interaktywne dokumentacje API, wliczając 2 alternatywne interfejsy użytkownika: * Swagger UI. * ReDoc. --- -Coming back to the previous code example, **FastAPI** will: +Wracając do poprzedniego przykładu, **FastAPI** : -* 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. +* Potwierdzi, że w ścieżce jest `item_id` dla żądań `GET` i `PUT`. +* Potwierdzi, że `item_id` jest typu `int` dla żądań `GET` i `PUT`. + * Jeżeli nie jest, odbiorca zobaczy przydatną, przejrzystą wiadomość z błędem. +* Sprawdzi czy w ścieżce jest opcjonalny parametr zapytania `q` (np. `http://127.0.0.1:8000/items/foo?q=somequery`) dla żądania `GET`. + * Jako że parametr `q` jest zadeklarowany jako `= None`, jest on opcjonalny. + * Gdyby tego `None` nie było, parametr ten byłby wymagany (tak jak treść żądania w żądaniu `PUT`). +* Dla żądania `PUT` z ścieżką `/items/{item_id}`, odczyta treść żądania jako JSON: + * Sprawdzi czy posiada wymagany atrybut `name`, który powinien być typu `str`. + * Sprawdzi czy posiada wymagany atrybut `price`, który musi być typu `float`. + * Sprawdzi czy posiada opcjonalny atrybut `is_offer`, który (jeżeli obecny) powinien być typu `bool`. + * To wszystko będzie również działać dla głęboko zagnieżdżonych obiektów JSON. +* Automatycznie konwertuje z i do JSON. +* Dokumentuje wszystko w OpenAPI, które może być używane przez: + * Interaktywne systemy dokumentacji. + * Systemy automatycznego generowania kodu klienckiego, dla wielu języków. +* Dostarczy bezpośrednio 2 interaktywne dokumentacje webowe. --- -We just scratched the surface, but you already get the idea of how it all works. +To dopiero początek, ale już masz mniej-więcej pojęcie jak to wszystko działa. -Try changing the line with: +Spróbuj zmienić linijkę: ```Python return {"item_name": item.name, "item_id": item_id} ``` -...from: +...z: ```Python ... "item_name": item.name ... ``` -...to: +...na: ```Python ... "item_price": item.price ... ``` -...and see how your editor will auto-complete the attributes and know their types: +...i zobacz jak edytor kodu automatycznie uzupełni atrybuty i będzie znał ich typy: ![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) -For a more complete example including more features, see the Tutorial - User Guide. +Dla bardziej kompletnych przykładów posiadających więcej funkcjonalności, zobacz Tutorial - User Guide. -**Spoiler alert**: the tutorial - user guide includes: +**Uwaga Spoiler**: tutorial - user guide zawiera: -* 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** +* Deklaracje **parametrów** z innych miejsc takich jak: **nagłówki**, **pliki cookies**, **formularze** i **pliki**. +* Jak ustawić **ograniczenia walidacyjne** takie jak `maksymalna długość` lub `regex`. +* Potężny i łatwy w użyciu system **Dependency Injection**. +* Zabezpieczenia i autentykacja, wliczając wsparcie dla **OAuth2** z **tokenami JWT** oraz autoryzacją **HTTP Basic**. +* Bardziej zaawansowane (ale równie proste) techniki deklarowania **głęboko zagnieżdżonych modeli JSON** (dzięki Pydantic). +* Wiele dodatkowych funkcji (dzięki Starlette) takie jak: + * **WebSockety** * **GraphQL** - * extremely easy tests based on `requests` and `pytest` + * bardzo proste testy bazujące na `requests` oraz `pytest` * **CORS** - * **Cookie Sessions** - * ...and more. + * **Sesje cookie** + * ...i więcej. -## Performance +## Wydajność -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). (*) +Niezależne benchmarki TechEmpower pokazują, że **FastAPI** (uruchomiony na serwerze Uvicorn) jest jednym z najszybszych dostępnych Pythonowych frameworków, zaraz po Starlette i Uvicorn (używanymi wewnątrznie przez FastAPI). (*) -To understand more about it, see the section Benchmarks. +Aby dowiedzieć się o tym więcej, zobacz sekcję Benchmarks. -## Optional Dependencies +## Opcjonalne zależności -Used by Pydantic: +Używane przez Pydantic: -* ujson - for faster JSON "parsing". -* email_validator - for email validation. +* ujson - dla szybszego "parsowania" danych JSON. +* email_validator - dla walidacji adresów email. -Used by Starlette: +Używane przez Starlette: -* requests - Required if you want to use the `TestClient`. -* aiofiles - Required if you want to use `FileResponse` or `StaticFiles`. -* 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`. +* requests - Wymagane jeżeli chcesz korzystać z `TestClient`. +* aiofiles - Wymagane jeżeli chcesz korzystać z `FileResponse` albo `StaticFiles`. +* jinja2 - Wymagane jeżeli chcesz używać domyślnej konfiguracji szablonów. +* python-multipart - Wymagane jeżelich chcesz wsparcie "parsowania" formularzy, używając `request.form()`. +* itsdangerous - Wymagany dla wsparcia `SessionMiddleware`. +* pyyaml - Wymagane dla wsparcia `SchemaGenerator` z Starlette (z FastAPI prawdopodobnie tego nie potrzebujesz). +* graphene - Wymagane dla wsparcia `GraphQLApp`. +* ujson - Wymagane jeżeli chcesz korzystać z `UJSONResponse`. -Used by FastAPI / Starlette: +Używane przez FastAPI / Starlette: -* uvicorn - for the server that loads and serves your application. -* orjson - Required if you want to use `ORJSONResponse`. +* uvicorn - jako serwer, który ładuje i obsługuje Twoją aplikację. +* orjson - Wymagane jeżeli chcesz używać `ORJSONResponse`. -You can install all of these with `pip install fastapi[all]`. +Możesz zainstalować wszystkie te aplikacje przy pomocy `pip install fastapi[all]`. -## License +## Licencja -This project is licensed under the terms of the MIT license. +Ten projekt jest na licencji MIT. From 24968937e5a788fd15f801c705da9afba35c2517 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 16 Jan 2022 19:37:21 +0000 Subject: [PATCH 30/59] =?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 9066792d8..658fd4255 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Polish translation for `docs/pl/docs/index.md`. PR [#4245](https://github.com/tiangolo/fastapi/pull/4245) by [@MicroPanda123](https://github.com/MicroPanda123). * 🌐 Add Chinese translation for `docs\tutorial\path-operation-configuration.md`. PR [#3312](https://github.com/tiangolo/fastapi/pull/3312) by [@jaystone776](https://github.com/jaystone776). * ✨ Enable configuring Swagger UI parameters. PR [#2568](https://github.com/tiangolo/fastapi/pull/2568) by [@jmriebold](https://github.com/jmriebold). * 📝 Update Python Types docs, add missing 3.6 / 3.9 example. PR [#4434](https://github.com/tiangolo/fastapi/pull/4434) by [@tiangolo](https://github.com/tiangolo). From 26e94116c12f46035ea3f4949c74d566aaa89116 Mon Sep 17 00:00:00 2001 From: kty4119 <49435654+kty4119@users.noreply.github.com> Date: Mon, 17 Jan 2022 04:41:13 +0900 Subject: [PATCH 31/59] =?UTF-8?q?=F0=9F=8C=90=20Fix=20Korean=20translation?= =?UTF-8?q?=20for=20`docs/ko/docs/index.md`=20(#4195)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/ko/docs/index.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/ko/docs/index.md b/docs/ko/docs/index.md index ee3edded6..d0c236906 100644 --- a/docs/ko/docs/index.md +++ b/docs/ko/docs/index.md @@ -35,7 +35,7 @@ FastAPI는 현대적이고, 빠르며(고성능), 파이썬 표준 타입 힌트 * **직관적**: 훌륭한 편집기 지원. 모든 곳에서 자동완성. 적은 디버깅 시간. * **쉬움**: 쉽게 사용하고 배우도록 설계. 적은 문서 읽기 시간. * **짧음**: 코드 중복 최소화. 각 매개변수 선언의 여러 기능. 적은 버그. -* **견고함**: 준비된 프로덕션 용 코드를 얻으세요. 자동 대화형 문서와 함께. +* **견고함**: 준비된 프로덕션 용 코드를 얻으십시오. 자동 대화형 문서와 함께. * **표준 기반**: API에 대한 (완전히 호환되는) 개방형 표준 기반: OpenAPI (이전에 Swagger로 알려졌던) 및 JSON 스키마. * 내부 개발팀의 프로덕션 애플리케이션을 빌드한 테스트에 근거한 측정 @@ -89,9 +89,9 @@ FastAPI는 현대적이고, 빠르며(고성능), 파이썬 표준 타입 힌트 --- -"_REST API를 만들기 위해 **현대적인 프레임워크**를 찾고 있다면 **FastAPI**를 확인해 보세요. [...] 빠르고, 쓰기 쉽고, 배우기도 쉽습니다 [...]_" +"_REST API를 만들기 위해 **현대적인 프레임워크**를 찾고 있다면 **FastAPI**를 확인해 보십시오. [...] 빠르고, 쓰기 쉽고, 배우기도 쉽습니다 [...]_" -"_우리 **API**를 **FastAPI**로 바꿨습니다 [...] 아마 여러분도 좋아하실 겁니다 [...]_" +"_우리 **API**를 **FastAPI**로 바꿨습니다 [...] 아마 여러분도 좋아하실 것입니다 [...]_"
Ines Montani - Matthew Honnibal - Explosion AI 설립자 - spaCy 제작자 (ref) - (ref)
@@ -193,7 +193,7 @@ async def read_item(item_id: int, q: Optional[str] = None): ### 실행하기 -서버를 실행하세요: +서버를 실행하십시오:
@@ -239,7 +239,7 @@ INFO: Application startup complete. ### 대화형 API 문서 -이제 http://127.0.0.1:8000/docs로 가보세요. +이제 http://127.0.0.1:8000/docs로 가보십시오. 자동 대화형 API 문서를 볼 수 있습니다 (Swagger UI 제공): @@ -388,7 +388,7 @@ item: Item 우리는 그저 수박 겉핡기만 했을 뿐인데 여러분은 벌써 어떻게 작동하는지 알고 있습니다. -다음 줄을 바꿔보세요: +다음 줄을 바꿔보십시오: ```Python return {"item_name": item.name, "item_id": item_id} @@ -447,7 +447,7 @@ Starlette이 사용하는: * jinja2 - 기본 템플릿 설정을 사용하려면 필요. * python-multipart - `request.form()`과 함께 "parsing"의 지원을 원하면 필요. * itsdangerous - `SessionMiddleware` 지원을 위해 필요. -* pyyaml - Starlette의 `SchemaGenerator` 지원을 위해 필요 (FastAPI와 쓸때는 필요가 없을 겁니다). +* pyyaml - Starlette의 `SchemaGenerator` 지원을 위해 필요 (FastAPI와 쓸때는 필요 없을 것입니다). * graphene - `GraphQLApp` 지원을 위해 필요. * ujson - `UJSONResponse`를 사용하려면 필요. From d23b295b96ca9a4c9ebef381a111de4435acd222 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 16 Jan 2022 19:41:49 +0000 Subject: [PATCH 32/59] =?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 658fd4255..15602b1e8 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Fix Korean translation for `docs/ko/docs/index.md`. PR [#4195](https://github.com/tiangolo/fastapi/pull/4195) by [@kty4119](https://github.com/kty4119). * 🌐 Add Polish translation for `docs/pl/docs/index.md`. PR [#4245](https://github.com/tiangolo/fastapi/pull/4245) by [@MicroPanda123](https://github.com/MicroPanda123). * 🌐 Add Chinese translation for `docs\tutorial\path-operation-configuration.md`. PR [#3312](https://github.com/tiangolo/fastapi/pull/3312) by [@jaystone776](https://github.com/jaystone776). * ✨ Enable configuring Swagger UI parameters. PR [#2568](https://github.com/tiangolo/fastapi/pull/2568) by [@jmriebold](https://github.com/jmriebold). From e1c6d7d31083e3c637c352d36b7d9f8d93508b15 Mon Sep 17 00:00:00 2001 From: jaystone776 Date: Mon, 17 Jan 2022 03:41:59 +0800 Subject: [PATCH 33/59] =?UTF-8?q?=F0=9F=8C=90=20Update=20Chinese=20transla?= =?UTF-8?q?tion=20for=20`docs/help-fastapi.md`=20(#3847)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/zh/docs/help-fastapi.md | 164 ++++++++++++++++++++++------------- 1 file changed, 103 insertions(+), 61 deletions(-) diff --git a/docs/zh/docs/help-fastapi.md b/docs/zh/docs/help-fastapi.md index 99e37b7c1..6f3f5159b 100644 --- a/docs/zh/docs/help-fastapi.md +++ b/docs/zh/docs/help-fastapi.md @@ -1,107 +1,149 @@ -# 帮助 FastAPI - 获取帮助 +# 帮助 FastAPI 与求助 -你喜欢 **FastAPI** 吗? +您喜欢 **FastAPI** 吗? -您愿意去帮助 FastAPI,帮助其他用户以及作者吗? +想帮助 FastAPI?其它用户?还有项目作者? -或者你想要获得有关 **FastAPI** 的帮助? +或要求助怎么使用 **FastAPI**? -下面是一些非常简单的方式去提供帮助(有些只需单击一两次链接)。 +以下几种帮助的方式都非常简单(有些只需要点击一两下鼠标)。 -以及几种获取帮助的途径。 +求助的渠道也很多。 -## 在 GitHub 上 Star **FastAPI** +## 订阅新闻邮件 -你可以在 GitHub 上 "star" FastAPI(点击右上角的 star 按钮):https://github.com/tiangolo/fastapi。 +您可以订阅 [**FastAPI 和它的小伙伴** 新闻邮件](/newsletter/){.internal-link target=_blank}(不会经常收到) -通过添加 star,其他用户将会更容易发现 FastAPI,并了解已经有许多人认为它有用。 +* FastAPI 及其小伙伴的新闻 🚀 +* 指南 📝 +* 功能 ✨ +* 破坏性更改 🚨 +* 开发技巧 ✅ -## Watch GitHub 仓库的版本发布 +## 在推特上关注 FastAPI -你可以在 GitHub 上 "watch" FastAPI(点击右上角的 watch 按钮):https://github.com/tiangolo/fastapi。 +在 **Twitter** 上关注 @fastapi 获取 **FastAPI** 的最新消息。🐦 -这时你可以选择 "Releases only" 选项。 +## 在 GitHub 上为 **FastAPI** 加星 -之后,只要有 **FastAPI** 的新版本(包含缺陷修复和新功能)发布,你都会(通过电子邮件)收到通知。 +您可以在 GitHub 上 **Star** FastAPI(只要点击右上角的星星就可以了): https://github.com/tiangolo/fastapi。⭐️ -## 加入聊天室 +**Star** 以后,其它用户就能更容易找到 FastAPI,并了解到已经有其他用户在使用它了。 -加入 Gitter 上的聊天室:https://gitter.im/tiangolo/fastapi。 +## 关注 GitHub 资源库的版本发布 -在这里你可以快速提问、帮助他人、分享想法等。 +您还可以在 GitHub 上 **Watch** FastAPI,(点击右上角的 **Watch** 按钮)https://github.com/tiangolo/fastapi。👀 -## 与作者联系 +您可以选择只关注发布(**Releases only**)。 -你可以联系 我 (Sebastián Ramírez / `tiangolo`) - FastAPI 的作者。 +这样,您就可以(在电子邮件里)接收到 **FastAPI** 新版发布的通知,及时了解 bug 修复与新功能。 -你可以: +## 联系作者 -* 在 **GitHub** 上关注我。 - * 查看我创建的其他的可能对你有帮助的开源项目。 - * 关注我以了解我创建的新开源项目。 -* 在 **Twitter** 上关注我。 - * 告诉我你是如何使用 FastAPI 的(我很乐意听到)。 - * 提出问题。 -* 在 **Linkedin** 上联系我。 - * 与我交流。 - * 认可我的技能或推荐我 :) -* 在 **Medium** 上阅读我写的文章(或关注我)。 - * 阅读我创建的其他想法,文章和工具。 - * 关注我以了解我发布的新内容。 +您可以联系项目作者,就是我(Sebastián Ramírez / `tiangolo`)。 -## 发布和 **FastAPI** 有关的推特 +您可以: - 发布和 **FastAPI** 有关的推特 让我和其他人知道你为什么喜欢它。 +* 在 **GitHub** 上关注我 + * 了解其它我创建的开源项目,或许对您会有帮助 + * 关注我什么时候创建新的开源项目 +* 在 **Twitter** 上关注我 + * 告诉我您使用 FastAPI(我非常乐意听到这种消息) + * 接收我发布公告或新工具的消息 + * 您还可以关注@fastapi on Twitter,这是个独立的账号 +* 在**领英**上联系我 + * 接收我发布公告或新工具的消息(虽然我用 Twitter 比较多) +* 阅读我在 **Dev.to****Medium** 上的文章,或关注我 + * 阅读我的其它想法、文章,了解我创建的工具 + * 关注我,这样就可以随时看到我发布的新文章 -## 告诉我你正在如何使用 **FastAPI** +## Tweet about **FastAPI** -我很乐意听到有关 **FastAPI** 被如何使用、你喜欢它的哪一点、被投入使用的项目/公司等等信息。 +Tweet about **FastAPI** 让我和大家知道您为什么喜欢 FastAPI。🎉 -你可以通过以下平台让我知道: - -* **Twitter**。 -* **Linkedin**。 -* **Medium**。 +知道有人使用 **FastAPI**,我会很开心,我也想知道您为什么喜欢 FastAPI,以及您在什么项目/哪些公司使用 FastAPI,等等。 ## 为 FastAPI 投票 -* 在 Slant 上为 **FastAPI** 投票。 +* 在 Slant 上为 **FastAPI** 投票 +* 在 AlternativeTo 上为 **FastAPI** 投票 -## 帮助他人解决 GitHub 的 issues +## 在 GitHub 上帮助其他人解决问题 -你可以查看 已有的 issues 并尝试帮助其他人。 +您可以查看现有 issues,并尝试帮助其他人解决问题,说不定您能解决这些问题呢。🤓 -## Watch GitHub 仓库 +如果帮助很多人解决了问题,您就有可能成为 [FastAPI 的官方专家](fastapi-people.md#experts){.internal-link target=_blank}。🎉 -你可以在 GitHub 上 "watch" FastAPI(点击右上角的 "watch" 按钮):https://github.com/tiangolo/fastapi。 +## 监听 GitHub 资源库 -如果你选择的是 "Watching" 而不是 "Releases only" 选项,你会在其他人创建了新的 issue 时收到通知。 +您可以在 GitHub 上「监听」FastAPI(点击右上角的 "watch" 按钮): https://github.com/tiangolo/fastapi. 👀 -然后你可以尝试帮助他们解决这些 issue。 +如果您选择 "Watching" 而不是 "Releases only",有人创建新 Issue 时,您会接收到通知。 -## 创建 issue +然后您就可以尝试并帮助他们解决问题。 -你可以在 GitHub 仓库中 创建一个新 issue 用来: +## 创建 Issue -* 报告 bug 或问题。 -* 提议新的特性。 -* 提问。 +您可以在 GitHub 资源库中创建 Issue,例如: -## 创建 Pull Request +* 提出**问题**或**意见** +* 提出新**特性**建议 -你可以 创建一个 Pull Request 用来: +**注意**:如果您创建 Issue,我会要求您也要帮助别的用户。😉 -* 纠正你在文档中发现的错别字。 -* 添加新的文档内容。 -* 修复已有的 bug 或问题。 -* 添加新的特性。 +## 创建 PR + +您可以创建 PR 为源代码做[贡献](contributing.md){.internal-link target=_blank},例如: + +* 修改文档错别字 +* 编辑这个文件,分享 FastAPI 的文章、视频、博客,不论是您自己的,还是您看到的都成 + * 注意,添加的链接要放在对应区块的开头 +* [翻译文档](contributing.md#translations){.internal-link target=_blank} + * 审阅别人翻译的文档 +* 添加新的文档内容 +* 修复现有问题/Bug +* 添加新功能 + +## 加入聊天 + +快加入 👥 Discord 聊天服务器 👥 和 FastAPI 社区里的小伙伴一起哈皮吧。 + +!!! tip "提示" + + 如有问题,请在 GitHub Issues 里提问,在这里更容易得到 [FastAPI 专家](fastapi-people.md#experts){.internal-link target=_blank}的帮助。 + + 聊天室仅供闲聊。 + +我们之前还使用过 Gitter chat,但它不支持频道等高级功能,聊天也比较麻烦,所以现在推荐使用 Discord。 + +### 别在聊天室里提问 + +注意,聊天室更倾向于“闲聊”,经常有人会提出一些笼统得让人难以回答的问题,所以在这里提问一般没人回答。 + +GitHub Issues 里提供了模板,指引您提出正确的问题,有利于获得优质的回答,甚至可能解决您还没有想到的问题。而且就算答疑解惑要耗费不少时间,我还是会尽量在 GitHub 里回答问题。但在聊天室里,我就没功夫这么做了。😅 + +聊天室里的聊天内容也不如 GitHub 里好搜索,聊天里的问答很容易就找不到了。只有在 GitHub Issues 里的问答才能帮助您成为 [FastAPI 专家](fastapi-people.md#experts){.internal-link target=_blank},在 GitHub Issues 中为您带来更多关注。 + +另一方面,聊天室里有成千上万的用户,在这里,您有很大可能遇到聊得来的人。😄 ## 赞助作者 -你还可以通过 GitHub sponsors 在经济上支持作者(我)。 +您还可以通过 GitHub 赞助商资助本项目的作者(就是我)。 -这样你可以给我买杯咖啡☕️以示谢意😄。 +给我买杯咖啡 ☕️ 以示感谢 😄 + +当然您也可以成为 FastAPI 的金牌或银牌赞助商。🏅🎉 + +## 赞助 FastAPI 使用的工具 + +如您在本文档中所见,FastAPI 站在巨人的肩膀上,它们分别是 Starlette 和 Pydantic。 + +您还可以赞助: + +* Samuel Colvin (Pydantic) +* Encode (Starlette, Uvicorn) --- -感谢! +谢谢!🚀 + From 93e4a19e3526369b2a9a0c5ef71101833faa2987 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 16 Jan 2022 19:42:33 +0000 Subject: [PATCH 34/59] =?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 15602b1e8..6ce21a8af 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Update Chinese translation for `docs/help-fastapi.md`. PR [#3847](https://github.com/tiangolo/fastapi/pull/3847) by [@jaystone776](https://github.com/jaystone776). * 🌐 Fix Korean translation for `docs/ko/docs/index.md`. PR [#4195](https://github.com/tiangolo/fastapi/pull/4195) by [@kty4119](https://github.com/kty4119). * 🌐 Add Polish translation for `docs/pl/docs/index.md`. PR [#4245](https://github.com/tiangolo/fastapi/pull/4245) by [@MicroPanda123](https://github.com/MicroPanda123). * 🌐 Add Chinese translation for `docs\tutorial\path-operation-configuration.md`. PR [#3312](https://github.com/tiangolo/fastapi/pull/3312) by [@jaystone776](https://github.com/jaystone776). From 9e2f5c67b603d73a77c420b629e2ee4e7378de1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 16 Jan 2022 21:08:04 +0100 Subject: [PATCH 35/59] =?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 | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 6ce21a8af..090a344a6 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,13 +2,25 @@ ## Latest Changes +### Features + +* ✨ Enable configuring Swagger UI parameters. Original PR [#2568](https://github.com/tiangolo/fastapi/pull/2568) by [@jmriebold](https://github.com/jmriebold). Here are the new docs: [Configuring Swagger UI](https://fastapi.tiangolo.com/advanced/extending-openapi/#configuring-swagger-ui). + +### Docs + +* 📝 Update Python Types docs, add missing 3.6 / 3.9 example. PR [#4434](https://github.com/tiangolo/fastapi/pull/4434) by [@tiangolo](https://github.com/tiangolo). + +### Translations + * 🌐 Update Chinese translation for `docs/help-fastapi.md`. PR [#3847](https://github.com/tiangolo/fastapi/pull/3847) by [@jaystone776](https://github.com/jaystone776). * 🌐 Fix Korean translation for `docs/ko/docs/index.md`. PR [#4195](https://github.com/tiangolo/fastapi/pull/4195) by [@kty4119](https://github.com/kty4119). * 🌐 Add Polish translation for `docs/pl/docs/index.md`. PR [#4245](https://github.com/tiangolo/fastapi/pull/4245) by [@MicroPanda123](https://github.com/MicroPanda123). * 🌐 Add Chinese translation for `docs\tutorial\path-operation-configuration.md`. PR [#3312](https://github.com/tiangolo/fastapi/pull/3312) by [@jaystone776](https://github.com/jaystone776). -* ✨ Enable configuring Swagger UI parameters. PR [#2568](https://github.com/tiangolo/fastapi/pull/2568) by [@jmriebold](https://github.com/jmriebold). -* 📝 Update Python Types docs, add missing 3.6 / 3.9 example. PR [#4434](https://github.com/tiangolo/fastapi/pull/4434) by [@tiangolo](https://github.com/tiangolo). + +### Internal + * 🔧 Enable MkDocs Material Insiders' `content.tabs.link`. PR [#4399](https://github.com/tiangolo/fastapi/pull/4399) by [@tiangolo](https://github.com/tiangolo). + ## 0.71.0 ### Features From f0388915a8b1cd9f3ae2259bace234ac6249c51a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 16 Jan 2022 21:09:10 +0100 Subject: [PATCH 36/59] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.72.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 090a344a6..2dfb47c12 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,9 @@ ## Latest Changes + +## 0.72.0 + ### Features * ✨ Enable configuring Swagger UI parameters. Original PR [#2568](https://github.com/tiangolo/fastapi/pull/2568) by [@jmriebold](https://github.com/jmriebold). Here are the new docs: [Configuring Swagger UI](https://fastapi.tiangolo.com/advanced/extending-openapi/#configuring-swagger-ui). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 5b735aed5..d83fe6fbd 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.71.0" +__version__ = "0.72.0" from starlette import status as status From a75d0801241dc59590d928c48da7665856a52963 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 23 Jan 2022 15:56:14 +0100 Subject: [PATCH 37/59] =?UTF-8?q?=F0=9F=94=A7=20Add=20sponsor=20Dropbase?= =?UTF-8?q?=20(#4465)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + docs/en/data/sponsors.yml | 3 + docs/en/data/sponsors_badge.yml | 1 + docs/en/docs/img/sponsors/dropbase-banner.svg | 117 +++++++++++++++++ docs/en/docs/img/sponsors/dropbase.svg | 124 ++++++++++++++++++ docs/en/overrides/main.html | 6 + 6 files changed, 252 insertions(+) create mode 100644 docs/en/docs/img/sponsors/dropbase-banner.svg create mode 100644 docs/en/docs/img/sponsors/dropbase.svg diff --git a/README.md b/README.md index 53de38bd9..c9c69d3e8 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,7 @@ The key features are: + diff --git a/docs/en/data/sponsors.yml b/docs/en/data/sponsors.yml index baa2e440d..b98e68b65 100644 --- a/docs/en/data/sponsors.yml +++ b/docs/en/data/sponsors.yml @@ -5,6 +5,9 @@ gold: - url: https://cryptapi.io/ title: "CryptAPI: Your easy to use, secure and privacy oriented payment gateway." img: https://fastapi.tiangolo.com/img/sponsors/cryptapi.svg + - url: https://www.dropbase.io/careers + title: Dropbase - seamlessly collect, clean, and centralize data. + img: https://fastapi.tiangolo.com/img/sponsors/dropbase.svg silver: - url: https://www.deta.sh/?ref=fastapi title: The launchpad for all your (team's) ideas diff --git a/docs/en/data/sponsors_badge.yml b/docs/en/data/sponsors_badge.yml index 759748728..0c4e716d7 100644 --- a/docs/en/data/sponsors_badge.yml +++ b/docs/en/data/sponsors_badge.yml @@ -7,3 +7,4 @@ logins: - koaning - deepset-ai - cryptapi + - DropbaseHQ diff --git a/docs/en/docs/img/sponsors/dropbase-banner.svg b/docs/en/docs/img/sponsors/dropbase-banner.svg new file mode 100644 index 000000000..d65abf1d9 --- /dev/null +++ b/docs/en/docs/img/sponsors/dropbase-banner.svg @@ -0,0 +1,117 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/en/docs/img/sponsors/dropbase.svg b/docs/en/docs/img/sponsors/dropbase.svg new file mode 100644 index 000000000..d0defb4df --- /dev/null +++ b/docs/en/docs/img/sponsors/dropbase.svg @@ -0,0 +1,124 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/en/overrides/main.html b/docs/en/overrides/main.html index 70b0253bd..0f452b515 100644 --- a/docs/en/overrides/main.html +++ b/docs/en/overrides/main.html @@ -46,6 +46,12 @@
+ {% endblock %} From 347e391271e09244c3d95ac46dd5493a9b472ee4 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 23 Jan 2022 14:56:44 +0000 Subject: [PATCH 38/59] =?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 2dfb47c12..41fa8493e 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔧 Add sponsor Dropbase. PR [#4465](https://github.com/tiangolo/fastapi/pull/4465) by [@tiangolo](https://github.com/tiangolo). ## 0.72.0 From ca5d57ea799028d771101bd711ca87a301dd45d8 Mon Sep 17 00:00:00 2001 From: Mark Date: Sun, 23 Jan 2022 23:54:59 +0800 Subject: [PATCH 39/59] =?UTF-8?q?=E2=9C=A8=20Allow=20hiding=20from=20OpenA?= =?UTF-8?q?PI=20(and=20Swagger=20UI)=20`Query`,=20`Cookie`,=20`Header`,=20?= =?UTF-8?q?and=20`Path`=20parameters=20(#3144)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- .../tutorial/query-params-str-validations.md | 16 ++ .../tutorial014.py | 15 ++ .../tutorial014_py310.py | 11 + fastapi/openapi/utils.py | 2 + fastapi/param_functions.py | 8 + fastapi/params.py | 10 + tests/test_param_include_in_schema.py | 239 ++++++++++++++++++ .../test_tutorial014.py | 82 ++++++ .../test_tutorial014_py310.py | 91 +++++++ 9 files changed, 474 insertions(+) create mode 100644 docs_src/query_params_str_validations/tutorial014.py create mode 100644 docs_src/query_params_str_validations/tutorial014_py310.py create mode 100644 tests/test_param_include_in_schema.py create mode 100644 tests/test_tutorial/test_query_params_str_validations/test_tutorial014.py create mode 100644 tests/test_tutorial/test_query_params_str_validations/test_tutorial014_py310.py diff --git a/docs/en/docs/tutorial/query-params-str-validations.md b/docs/en/docs/tutorial/query-params-str-validations.md index fcac1a4e0..ee62b9718 100644 --- a/docs/en/docs/tutorial/query-params-str-validations.md +++ b/docs/en/docs/tutorial/query-params-str-validations.md @@ -387,6 +387,22 @@ The docs will show it like this: +## Exclude from OpenAPI + +To exclude a query parameter from the generated OpenAPI schema (and thus, from the automatic documentation systems), set the parameter `include_in_schema` of `Query` to `False`: + +=== "Python 3.6 and above" + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial014.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial014_py310.py!} + ``` + ## Recap You can declare additional validations and metadata for your parameters. diff --git a/docs_src/query_params_str_validations/tutorial014.py b/docs_src/query_params_str_validations/tutorial014.py new file mode 100644 index 000000000..fb50bc27b --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial014.py @@ -0,0 +1,15 @@ +from typing import Optional + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + hidden_query: Optional[str] = Query(None, include_in_schema=False) +): + if hidden_query: + return {"hidden_query": hidden_query} + else: + return {"hidden_query": "Not found"} diff --git a/docs_src/query_params_str_validations/tutorial014_py310.py b/docs_src/query_params_str_validations/tutorial014_py310.py new file mode 100644 index 000000000..7ae39c7f9 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial014_py310.py @@ -0,0 +1,11 @@ +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items(hidden_query: str | None = Query(None, include_in_schema=False)): + if hidden_query: + return {"hidden_query": hidden_query} + else: + return {"hidden_query": "Not found"} diff --git a/fastapi/openapi/utils.py b/fastapi/openapi/utils.py index 0e73e21bf..aff76b15e 100644 --- a/fastapi/openapi/utils.py +++ b/fastapi/openapi/utils.py @@ -92,6 +92,8 @@ def get_openapi_operation_parameters( for param in all_route_params: field_info = param.field_info field_info = cast(Param, field_info) + if not field_info.include_in_schema: + continue parameter = { "name": param.alias, "in": field_info.in_.value, diff --git a/fastapi/param_functions.py b/fastapi/param_functions.py index ff65d7271..a553a1461 100644 --- a/fastapi/param_functions.py +++ b/fastapi/param_functions.py @@ -20,6 +20,7 @@ def Path( # noqa: N802 example: Any = Undefined, examples: Optional[Dict[str, Any]] = None, deprecated: Optional[bool] = None, + include_in_schema: bool = True, **extra: Any, ) -> Any: return params.Path( @@ -37,6 +38,7 @@ def Path( # noqa: N802 example=example, examples=examples, deprecated=deprecated, + include_in_schema=include_in_schema, **extra, ) @@ -57,6 +59,7 @@ def Query( # noqa: N802 example: Any = Undefined, examples: Optional[Dict[str, Any]] = None, deprecated: Optional[bool] = None, + include_in_schema: bool = True, **extra: Any, ) -> Any: return params.Query( @@ -74,6 +77,7 @@ def Query( # noqa: N802 example=example, examples=examples, deprecated=deprecated, + include_in_schema=include_in_schema, **extra, ) @@ -95,6 +99,7 @@ def Header( # noqa: N802 example: Any = Undefined, examples: Optional[Dict[str, Any]] = None, deprecated: Optional[bool] = None, + include_in_schema: bool = True, **extra: Any, ) -> Any: return params.Header( @@ -113,6 +118,7 @@ def Header( # noqa: N802 example=example, examples=examples, deprecated=deprecated, + include_in_schema=include_in_schema, **extra, ) @@ -133,6 +139,7 @@ def Cookie( # noqa: N802 example: Any = Undefined, examples: Optional[Dict[str, Any]] = None, deprecated: Optional[bool] = None, + include_in_schema: bool = True, **extra: Any, ) -> Any: return params.Cookie( @@ -150,6 +157,7 @@ def Cookie( # noqa: N802 example=example, examples=examples, deprecated=deprecated, + include_in_schema=include_in_schema, **extra, ) diff --git a/fastapi/params.py b/fastapi/params.py index 3cab98b78..042bbd42f 100644 --- a/fastapi/params.py +++ b/fastapi/params.py @@ -31,11 +31,13 @@ class Param(FieldInfo): example: Any = Undefined, examples: Optional[Dict[str, Any]] = None, deprecated: Optional[bool] = None, + include_in_schema: bool = True, **extra: Any, ): self.deprecated = deprecated self.example = example self.examples = examples + self.include_in_schema = include_in_schema super().__init__( default, alias=alias, @@ -75,6 +77,7 @@ class Path(Param): example: Any = Undefined, examples: Optional[Dict[str, Any]] = None, deprecated: Optional[bool] = None, + include_in_schema: bool = True, **extra: Any, ): self.in_ = self.in_ @@ -93,6 +96,7 @@ class Path(Param): deprecated=deprecated, example=example, examples=examples, + include_in_schema=include_in_schema, **extra, ) @@ -117,6 +121,7 @@ class Query(Param): example: Any = Undefined, examples: Optional[Dict[str, Any]] = None, deprecated: Optional[bool] = None, + include_in_schema: bool = True, **extra: Any, ): super().__init__( @@ -134,6 +139,7 @@ class Query(Param): deprecated=deprecated, example=example, examples=examples, + include_in_schema=include_in_schema, **extra, ) @@ -159,6 +165,7 @@ class Header(Param): example: Any = Undefined, examples: Optional[Dict[str, Any]] = None, deprecated: Optional[bool] = None, + include_in_schema: bool = True, **extra: Any, ): self.convert_underscores = convert_underscores @@ -177,6 +184,7 @@ class Header(Param): deprecated=deprecated, example=example, examples=examples, + include_in_schema=include_in_schema, **extra, ) @@ -201,6 +209,7 @@ class Cookie(Param): example: Any = Undefined, examples: Optional[Dict[str, Any]] = None, deprecated: Optional[bool] = None, + include_in_schema: bool = True, **extra: Any, ): super().__init__( @@ -218,6 +227,7 @@ class Cookie(Param): deprecated=deprecated, example=example, examples=examples, + include_in_schema=include_in_schema, **extra, ) diff --git a/tests/test_param_include_in_schema.py b/tests/test_param_include_in_schema.py new file mode 100644 index 000000000..4eaac72d8 --- /dev/null +++ b/tests/test_param_include_in_schema.py @@ -0,0 +1,239 @@ +from typing import Optional + +import pytest +from fastapi import Cookie, FastAPI, Header, Path, Query +from fastapi.testclient import TestClient + +app = FastAPI() + + +@app.get("/hidden_cookie") +async def hidden_cookie( + hidden_cookie: Optional[str] = Cookie(None, include_in_schema=False) +): + return {"hidden_cookie": hidden_cookie} + + +@app.get("/hidden_header") +async def hidden_header( + hidden_header: Optional[str] = Header(None, include_in_schema=False) +): + return {"hidden_header": hidden_header} + + +@app.get("/hidden_path/{hidden_path}") +async def hidden_path(hidden_path: str = Path(..., include_in_schema=False)): + return {"hidden_path": hidden_path} + + +@app.get("/hidden_query") +async def hidden_query( + hidden_query: Optional[str] = Query(None, include_in_schema=False) +): + return {"hidden_query": hidden_query} + + +client = TestClient(app) + +openapi_shema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/hidden_cookie": { + "get": { + "summary": "Hidden Cookie", + "operationId": "hidden_cookie_hidden_cookie_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/hidden_header": { + "get": { + "summary": "Hidden Header", + "operationId": "hidden_header_hidden_header_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/hidden_path/{hidden_path}": { + "get": { + "summary": "Hidden Path", + "operationId": "hidden_path_hidden_path__hidden_path__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/hidden_query": { + "get": { + "summary": "Hidden Query", + "operationId": "hidden_query_hidden_query_get", + "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": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == openapi_shema + + +@pytest.mark.parametrize( + "path,cookies,expected_status,expected_response", + [ + ( + "/hidden_cookie", + {}, + 200, + {"hidden_cookie": None}, + ), + ( + "/hidden_cookie", + {"hidden_cookie": "somevalue"}, + 200, + {"hidden_cookie": "somevalue"}, + ), + ], +) +def test_hidden_cookie(path, cookies, expected_status, expected_response): + response = client.get(path, cookies=cookies) + assert response.status_code == expected_status + assert response.json() == expected_response + + +@pytest.mark.parametrize( + "path,headers,expected_status,expected_response", + [ + ( + "/hidden_header", + {}, + 200, + {"hidden_header": None}, + ), + ( + "/hidden_header", + {"Hidden-Header": "somevalue"}, + 200, + {"hidden_header": "somevalue"}, + ), + ], +) +def test_hidden_header(path, headers, expected_status, expected_response): + response = client.get(path, headers=headers) + assert response.status_code == expected_status + assert response.json() == expected_response + + +def test_hidden_path(): + response = client.get("/hidden_path/hidden_path") + assert response.status_code == 200 + assert response.json() == {"hidden_path": "hidden_path"} + + +@pytest.mark.parametrize( + "path,expected_status,expected_response", + [ + ( + "/hidden_query", + 200, + {"hidden_query": None}, + ), + ( + "/hidden_query?hidden_query=somevalue", + 200, + {"hidden_query": "somevalue"}, + ), + ], +) +def test_hidden_query(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_tutorial/test_query_params_str_validations/test_tutorial014.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014.py new file mode 100644 index 000000000..98ae5a684 --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014.py @@ -0,0 +1,82 @@ +from fastapi.testclient import TestClient + +from docs_src.query_params_str_validations.tutorial014 import app + +client = TestClient(app) + + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "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": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def test_hidden_query(): + response = client.get("/items?hidden_query=somevalue") + assert response.status_code == 200, response.text + assert response.json() == {"hidden_query": "somevalue"} + + +def test_no_hidden_query(): + response = client.get("/items") + assert response.status_code == 200, response.text + assert response.json() == {"hidden_query": "Not found"} 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 new file mode 100644 index 000000000..33f3d5f77 --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_py310.py @@ -0,0 +1,91 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "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": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.query_params_str_validations.tutorial014_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py310 +def test_hidden_query(client: TestClient): + response = client.get("/items?hidden_query=somevalue") + assert response.status_code == 200, response.text + assert response.json() == {"hidden_query": "somevalue"} + + +@needs_py310 +def test_no_hidden_query(client: TestClient): + response = client.get("/items") + assert response.status_code == 200, response.text + assert response.json() == {"hidden_query": "Not found"} From 85518bc58b131ddc8d7f251c9349f473d194a9b2 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 23 Jan 2022 15:55:36 +0000 Subject: [PATCH 40/59] =?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 41fa8493e..afd109d88 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✨ Allow hiding from OpenAPI (and Swagger UI) `Query`, `Cookie`, `Header`, and `Path` parameters. PR [#3144](https://github.com/tiangolo/fastapi/pull/3144) by [@astraldawn](https://github.com/astraldawn). * 🔧 Add sponsor Dropbase. PR [#4465](https://github.com/tiangolo/fastapi/pull/4465) by [@tiangolo](https://github.com/tiangolo). ## 0.72.0 From 3de0fb82bf17fa4179caa38c3126786a7d99cf67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 23 Jan 2022 17:13:49 +0100 Subject: [PATCH 41/59] =?UTF-8?q?=F0=9F=90=9B=20Fix=20docs=20dependencies?= =?UTF-8?q?=20cache,=20to=20get=20the=20latest=20Material=20for=20MkDocs?= =?UTF-8?q?=20(#4466)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build-docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index eba5fc57e..ccf964486 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -20,7 +20,7 @@ jobs: id: cache with: path: ${{ env.pythonLocation }} - key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-docs + key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-docs-v2 - name: Install Flit if: steps.cache.outputs.cache-hit != 'true' run: python3.7 -m pip install flit From 699b5ef84198a352a332beea9953fe1db33315b6 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 23 Jan 2022 16:14:28 +0000 Subject: [PATCH 42/59] =?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 afd109d88..997fb7529 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🐛 Fix docs dependencies cache, to get the latest Material for MkDocs. PR [#4466](https://github.com/tiangolo/fastapi/pull/4466) by [@tiangolo](https://github.com/tiangolo). * ✨ Allow hiding from OpenAPI (and Swagger UI) `Query`, `Cookie`, `Header`, and `Path` parameters. PR [#3144](https://github.com/tiangolo/fastapi/pull/3144) by [@astraldawn](https://github.com/astraldawn). * 🔧 Add sponsor Dropbase. PR [#4465](https://github.com/tiangolo/fastapi/pull/4465) by [@tiangolo](https://github.com/tiangolo). From d4608a00cf4855021dfb1a780556e24dedc94b14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 23 Jan 2022 17:32:04 +0100 Subject: [PATCH 43/59] =?UTF-8?q?=F0=9F=90=9B=20Prefer=20custom=20encoder?= =?UTF-8?q?=20over=20defaults=20if=20specified=20in=20`jsonable=5Fencoder`?= =?UTF-8?q?=20(#4467)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Vivek Sunder --- fastapi/encoders.py | 18 +++++++++--------- tests/test_jsonable_encoder.py | 15 +++++++++++++++ 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/fastapi/encoders.py b/fastapi/encoders.py index 3f599c9fa..4b7ffe313 100644 --- a/fastapi/encoders.py +++ b/fastapi/encoders.py @@ -34,9 +34,17 @@ def jsonable_encoder( exclude_unset: bool = False, exclude_defaults: bool = False, exclude_none: bool = False, - custom_encoder: Dict[Any, Callable[[Any], Any]] = {}, + custom_encoder: Optional[Dict[Any, Callable[[Any], Any]]] = None, sqlalchemy_safe: bool = True, ) -> Any: + custom_encoder = custom_encoder or {} + if custom_encoder: + if type(obj) in custom_encoder: + return custom_encoder[type(obj)](obj) + else: + for encoder_type, encoder_instance in custom_encoder.items(): + if isinstance(obj, encoder_type): + return encoder_instance(obj) if include is not None and not isinstance(include, (set, dict)): include = set(include) if exclude is not None and not isinstance(exclude, (set, dict)): @@ -118,14 +126,6 @@ def jsonable_encoder( ) return encoded_list - if custom_encoder: - if type(obj) in custom_encoder: - return custom_encoder[type(obj)](obj) - else: - for encoder_type, encoder in custom_encoder.items(): - if isinstance(obj, encoder_type): - return encoder(obj) - if type(obj) in ENCODERS_BY_TYPE: return ENCODERS_BY_TYPE[type(obj)](obj) for encoder, classes_tuple in encoders_by_class_tuples.items(): diff --git a/tests/test_jsonable_encoder.py b/tests/test_jsonable_encoder.py index e2aa8adf8..fa82b5ea8 100644 --- a/tests/test_jsonable_encoder.py +++ b/tests/test_jsonable_encoder.py @@ -161,6 +161,21 @@ def test_custom_encoders(): assert encoded_instance["dt_field"] == instance.dt_field.isoformat() +def test_custom_enum_encoders(): + def custom_enum_encoder(v: Enum): + return v.value.lower() + + class MyEnum(Enum): + ENUM_VAL_1 = "ENUM_VAL_1" + + instance = MyEnum.ENUM_VAL_1 + + encoded_instance = jsonable_encoder( + instance, custom_encoder={MyEnum: custom_enum_encoder} + ) + 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" From f4963f05bf4295e02cce1a28386712a5e6776fc4 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 23 Jan 2022 16:32:35 +0000 Subject: [PATCH 44/59] =?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 997fb7529..49747539d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🐛 Prefer custom encoder over defaults if specified in `jsonable_encoder`. PR [#4467](https://github.com/tiangolo/fastapi/pull/4467) by [@tiangolo](https://github.com/tiangolo). * 🐛 Fix docs dependencies cache, to get the latest Material for MkDocs. PR [#4466](https://github.com/tiangolo/fastapi/pull/4466) by [@tiangolo](https://github.com/tiangolo). * ✨ Allow hiding from OpenAPI (and Swagger UI) `Query`, `Cookie`, `Header`, and `Path` parameters. PR [#3144](https://github.com/tiangolo/fastapi/pull/3144) by [@astraldawn](https://github.com/astraldawn). * 🔧 Add sponsor Dropbase. PR [#4465](https://github.com/tiangolo/fastapi/pull/4465) by [@tiangolo](https://github.com/tiangolo). From 6215fdd39e54422561d51d7e6159c219053d41cb Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 23 Jan 2022 16:34:52 +0000 Subject: [PATCH 46/59] =?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 49747539d..468d450a8 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🐛 Prefer custom encoder over defaults if specified in `jsonable_encoder`. PR [#2061](https://github.com/tiangolo/fastapi/pull/2061) by [@viveksunder](https://github.com/viveksunder). * 🐛 Prefer custom encoder over defaults if specified in `jsonable_encoder`. PR [#4467](https://github.com/tiangolo/fastapi/pull/4467) by [@tiangolo](https://github.com/tiangolo). * 🐛 Fix docs dependencies cache, to get the latest Material for MkDocs. PR [#4466](https://github.com/tiangolo/fastapi/pull/4466) by [@tiangolo](https://github.com/tiangolo). * ✨ Allow hiding from OpenAPI (and Swagger UI) `Query`, `Cookie`, `Header`, and `Path` parameters. PR [#3144](https://github.com/tiangolo/fastapi/pull/3144) by [@astraldawn](https://github.com/astraldawn). From 0f8349fcb7c57921d28e78296a7dc8d0504459e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 23 Jan 2022 18:03:42 +0100 Subject: [PATCH 47/59] =?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 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 468d450a8..54017ceb3 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -3,7 +3,7 @@ ## Latest Changes * 🐛 Prefer custom encoder over defaults if specified in `jsonable_encoder`. PR [#2061](https://github.com/tiangolo/fastapi/pull/2061) by [@viveksunder](https://github.com/viveksunder). -* 🐛 Prefer custom encoder over defaults if specified in `jsonable_encoder`. PR [#4467](https://github.com/tiangolo/fastapi/pull/4467) by [@tiangolo](https://github.com/tiangolo). + * 💚 Duplicate PR to trigger CI. PR [#4467](https://github.com/tiangolo/fastapi/pull/4467) by [@tiangolo](https://github.com/tiangolo). * 🐛 Fix docs dependencies cache, to get the latest Material for MkDocs. PR [#4466](https://github.com/tiangolo/fastapi/pull/4466) by [@tiangolo](https://github.com/tiangolo). * ✨ Allow hiding from OpenAPI (and Swagger UI) `Query`, `Cookie`, `Header`, and `Path` parameters. PR [#3144](https://github.com/tiangolo/fastapi/pull/3144) by [@astraldawn](https://github.com/astraldawn). * 🔧 Add sponsor Dropbase. PR [#4465](https://github.com/tiangolo/fastapi/pull/4465) by [@tiangolo](https://github.com/tiangolo). From 569afb4378c80e0bff5dc4a45f26d012e498eda6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 23 Jan 2022 18:43:04 +0100 Subject: [PATCH 48/59] =?UTF-8?q?=E2=9C=A8=20Add=20support=20for=20tags=20?= =?UTF-8?q?with=20Enums=20(#4468)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tutorial/path-operation-configuration.md | 12 ++++ .../tutorial002b.py | 20 +++++++ fastapi/applications.py | 23 ++++---- fastapi/routing.py | 32 +++++------ .../test_tutorial002b.py | 56 +++++++++++++++++++ 5 files changed, 116 insertions(+), 27 deletions(-) create mode 100644 docs_src/path_operation_configuration/tutorial002b.py create mode 100644 tests/test_tutorial/test_path_operation_configurations/test_tutorial002b.py diff --git a/docs/en/docs/tutorial/path-operation-configuration.md b/docs/en/docs/tutorial/path-operation-configuration.md index 1ff448e76..884a762e2 100644 --- a/docs/en/docs/tutorial/path-operation-configuration.md +++ b/docs/en/docs/tutorial/path-operation-configuration.md @@ -64,6 +64,18 @@ They will be added to the OpenAPI schema and used by the automatic documentation +### Tags with Enums + +If you have a big application, you might end up accumulating **several tags**, and you would want to make sure you always use the **same tag** for related *path operations*. + +In these cases, it could make sense to store the tags in an `Enum`. + +**FastAPI** supports that the same way as with plain strings: + +```Python hl_lines="1 8-10 13 18" +{!../../../docs_src/path_operation_configuration/tutorial002b.py!} +``` + ## Summary and description You can add a `summary` and `description`: diff --git a/docs_src/path_operation_configuration/tutorial002b.py b/docs_src/path_operation_configuration/tutorial002b.py new file mode 100644 index 000000000..d53b4d817 --- /dev/null +++ b/docs_src/path_operation_configuration/tutorial002b.py @@ -0,0 +1,20 @@ +from enum import Enum + +from fastapi import FastAPI + +app = FastAPI() + + +class Tags(Enum): + items = "items" + users = "users" + + +@app.get("/items/", tags=[Tags.items]) +async def get_items(): + return ["Portal gun", "Plumbus"] + + +@app.get("/users/", tags=[Tags.users]) +async def read_users(): + return ["Rick", "Morty"] diff --git a/fastapi/applications.py b/fastapi/applications.py index d71d4190a..dbfd76fb9 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -1,3 +1,4 @@ +from enum import Enum from typing import Any, Callable, Coroutine, Dict, List, Optional, Sequence, Type, Union from fastapi import routing @@ -219,7 +220,7 @@ class FastAPI(Starlette): *, response_model: Optional[Type[Any]] = None, status_code: Optional[int] = None, - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[Depends]] = None, summary: Optional[str] = None, description: Optional[str] = None, @@ -273,7 +274,7 @@ class FastAPI(Starlette): *, response_model: Optional[Type[Any]] = None, status_code: Optional[int] = None, - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[Depends]] = None, summary: Optional[str] = None, description: Optional[str] = None, @@ -342,7 +343,7 @@ class FastAPI(Starlette): router: routing.APIRouter, *, prefix: str = "", - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[Depends]] = None, responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, deprecated: Optional[bool] = None, @@ -368,7 +369,7 @@ class FastAPI(Starlette): *, response_model: Optional[Type[Any]] = None, status_code: Optional[int] = None, - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[Depends]] = None, summary: Optional[str] = None, description: Optional[str] = None, @@ -419,7 +420,7 @@ class FastAPI(Starlette): *, response_model: Optional[Type[Any]] = None, status_code: Optional[int] = None, - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[Depends]] = None, summary: Optional[str] = None, description: Optional[str] = None, @@ -470,7 +471,7 @@ class FastAPI(Starlette): *, response_model: Optional[Type[Any]] = None, status_code: Optional[int] = None, - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[Depends]] = None, summary: Optional[str] = None, description: Optional[str] = None, @@ -521,7 +522,7 @@ class FastAPI(Starlette): *, response_model: Optional[Type[Any]] = None, status_code: Optional[int] = None, - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[Depends]] = None, summary: Optional[str] = None, description: Optional[str] = None, @@ -572,7 +573,7 @@ class FastAPI(Starlette): *, response_model: Optional[Type[Any]] = None, status_code: Optional[int] = None, - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[Depends]] = None, summary: Optional[str] = None, description: Optional[str] = None, @@ -623,7 +624,7 @@ class FastAPI(Starlette): *, response_model: Optional[Type[Any]] = None, status_code: Optional[int] = None, - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[Depends]] = None, summary: Optional[str] = None, description: Optional[str] = None, @@ -674,7 +675,7 @@ class FastAPI(Starlette): *, response_model: Optional[Type[Any]] = None, status_code: Optional[int] = None, - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[Depends]] = None, summary: Optional[str] = None, description: Optional[str] = None, @@ -725,7 +726,7 @@ class FastAPI(Starlette): *, response_model: Optional[Type[Any]] = None, status_code: Optional[int] = None, - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[Depends]] = None, summary: Optional[str] = None, description: Optional[str] = None, diff --git a/fastapi/routing.py b/fastapi/routing.py index 63ad72964..f6d5370d6 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -1,9 +1,9 @@ import asyncio import dataclasses import email.message -import enum import inspect import json +from enum import Enum, IntEnum from typing import ( Any, Callable, @@ -305,7 +305,7 @@ class APIRoute(routing.Route): *, response_model: Optional[Type[Any]] = None, status_code: Optional[int] = None, - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, summary: Optional[str] = None, description: Optional[str] = None, @@ -330,7 +330,7 @@ class APIRoute(routing.Route): openapi_extra: Optional[Dict[str, Any]] = None, ) -> None: # normalise enums e.g. http.HTTPStatus - if isinstance(status_code, enum.IntEnum): + if isinstance(status_code, IntEnum): status_code = int(status_code) self.path = path self.endpoint = endpoint @@ -438,7 +438,7 @@ class APIRouter(routing.Router): self, *, prefix: str = "", - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, default_response_class: Type[Response] = Default(JSONResponse), responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, @@ -466,7 +466,7 @@ class APIRouter(routing.Router): "/" ), "A path prefix must not end with '/', as the routes will start with '/'" self.prefix = prefix - self.tags: List[str] = tags or [] + self.tags: List[Union[str, Enum]] = tags or [] self.dependencies = list(dependencies or []) or [] self.deprecated = deprecated self.include_in_schema = include_in_schema @@ -483,7 +483,7 @@ class APIRouter(routing.Router): *, response_model: Optional[Type[Any]] = None, status_code: Optional[int] = None, - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, summary: Optional[str] = None, description: Optional[str] = None, @@ -557,7 +557,7 @@ class APIRouter(routing.Router): *, response_model: Optional[Type[Any]] = None, status_code: Optional[int] = None, - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, summary: Optional[str] = None, description: Optional[str] = None, @@ -634,7 +634,7 @@ class APIRouter(routing.Router): router: "APIRouter", *, prefix: str = "", - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, default_response_class: Type[Response] = Default(JSONResponse), responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, @@ -738,7 +738,7 @@ class APIRouter(routing.Router): *, response_model: Optional[Type[Any]] = None, status_code: Optional[int] = None, - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, summary: Optional[str] = None, description: Optional[str] = None, @@ -790,7 +790,7 @@ class APIRouter(routing.Router): *, response_model: Optional[Type[Any]] = None, status_code: Optional[int] = None, - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, summary: Optional[str] = None, description: Optional[str] = None, @@ -842,7 +842,7 @@ class APIRouter(routing.Router): *, response_model: Optional[Type[Any]] = None, status_code: Optional[int] = None, - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, summary: Optional[str] = None, description: Optional[str] = None, @@ -894,7 +894,7 @@ class APIRouter(routing.Router): *, response_model: Optional[Type[Any]] = None, status_code: Optional[int] = None, - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, summary: Optional[str] = None, description: Optional[str] = None, @@ -946,7 +946,7 @@ class APIRouter(routing.Router): *, response_model: Optional[Type[Any]] = None, status_code: Optional[int] = None, - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, summary: Optional[str] = None, description: Optional[str] = None, @@ -998,7 +998,7 @@ class APIRouter(routing.Router): *, response_model: Optional[Type[Any]] = None, status_code: Optional[int] = None, - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, summary: Optional[str] = None, description: Optional[str] = None, @@ -1050,7 +1050,7 @@ class APIRouter(routing.Router): *, response_model: Optional[Type[Any]] = None, status_code: Optional[int] = None, - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, summary: Optional[str] = None, description: Optional[str] = None, @@ -1102,7 +1102,7 @@ class APIRouter(routing.Router): *, response_model: Optional[Type[Any]] = None, status_code: Optional[int] = None, - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, summary: Optional[str] = None, description: Optional[str] = None, diff --git a/tests/test_tutorial/test_path_operation_configurations/test_tutorial002b.py b/tests/test_tutorial/test_path_operation_configurations/test_tutorial002b.py new file mode 100644 index 000000000..be9f2afec --- /dev/null +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial002b.py @@ -0,0 +1,56 @@ +from fastapi.testclient import TestClient + +from docs_src.path_operation_configuration.tutorial002b import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "tags": ["items"], + "summary": "Get Items", + "operationId": "get_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + }, + "/users/": { + "get": { + "tags": ["users"], + "summary": "Read Users", + "operationId": "read_users_users__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + }, + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def test_get_items(): + response = client.get("/items/") + assert response.status_code == 200, response.text + assert response.json() == ["Portal gun", "Plumbus"] + + +def test_get_users(): + response = client.get("/users/") + assert response.status_code == 200, response.text + assert response.json() == ["Rick", "Morty"] From 59b1f353b3fe77cf801242e3d120372ad8519710 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 23 Jan 2022 17:43:36 +0000 Subject: [PATCH 49/59] =?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 54017ceb3..7026d0eb1 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✨ Add support for tags with Enums. PR [#4468](https://github.com/tiangolo/fastapi/pull/4468) by [@tiangolo](https://github.com/tiangolo). * 🐛 Prefer custom encoder over defaults if specified in `jsonable_encoder`. PR [#2061](https://github.com/tiangolo/fastapi/pull/2061) by [@viveksunder](https://github.com/viveksunder). * 💚 Duplicate PR to trigger CI. PR [#4467](https://github.com/tiangolo/fastapi/pull/4467) by [@tiangolo](https://github.com/tiangolo). * 🐛 Fix docs dependencies cache, to get the latest Material for MkDocs. PR [#4466](https://github.com/tiangolo/fastapi/pull/4466) by [@tiangolo](https://github.com/tiangolo). From 1bf55200a90b04229f665cd2ee83edde91e936e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 23 Jan 2022 20:14:13 +0100 Subject: [PATCH 50/59] =?UTF-8?q?=E2=9C=A8=20Add=20support=20for=20declari?= =?UTF-8?q?ng=20`UploadFile`=20parameters=20without=20explicit=20`File()`?= =?UTF-8?q?=20(#4469)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/tutorial/request-files.md | 47 +++- docs_src/request_files/tutorial001.py | 2 +- docs_src/request_files/tutorial001_02.py | 21 ++ .../request_files/tutorial001_02_py310.py | 19 ++ docs_src/request_files/tutorial001_03.py | 15 ++ docs_src/request_files/tutorial002.py | 2 +- docs_src/request_files/tutorial002_py39.py | 2 +- docs_src/request_files/tutorial003.py | 37 +++ docs_src/request_files/tutorial003_py39.py | 35 +++ fastapi/datastructures.py | 6 +- fastapi/dependencies/utils.py | 28 +-- .../test_request_files/test_tutorial001_02.py | 157 ++++++++++++ .../test_tutorial001_02_py310.py | 169 +++++++++++++ .../test_request_files/test_tutorial001_03.py | 159 +++++++++++++ .../test_request_files/test_tutorial003.py | 194 +++++++++++++++ .../test_tutorial003_py39.py | 223 ++++++++++++++++++ 16 files changed, 1086 insertions(+), 30 deletions(-) create mode 100644 docs_src/request_files/tutorial001_02.py create mode 100644 docs_src/request_files/tutorial001_02_py310.py create mode 100644 docs_src/request_files/tutorial001_03.py create mode 100644 docs_src/request_files/tutorial003.py create mode 100644 docs_src/request_files/tutorial003_py39.py create mode 100644 tests/test_tutorial/test_request_files/test_tutorial001_02.py create mode 100644 tests/test_tutorial/test_request_files/test_tutorial001_02_py310.py create mode 100644 tests/test_tutorial/test_request_files/test_tutorial001_03.py create mode 100644 tests/test_tutorial/test_request_files/test_tutorial003.py create mode 100644 tests/test_tutorial/test_request_files/test_tutorial003_py39.py diff --git a/docs/en/docs/tutorial/request-files.md b/docs/en/docs/tutorial/request-files.md index b7257c7eb..ed2c8b6af 100644 --- a/docs/en/docs/tutorial/request-files.md +++ b/docs/en/docs/tutorial/request-files.md @@ -17,7 +17,7 @@ Import `File` and `UploadFile` from `fastapi`: {!../../../docs_src/request_files/tutorial001.py!} ``` -## Define `File` parameters +## Define `File` Parameters Create file parameters the same way you would for `Body` or `Form`: @@ -41,7 +41,7 @@ Have in mind that this means that the whole contents will be stored in memory. T But there are several cases in which you might benefit from using `UploadFile`. -## `File` parameters with `UploadFile` +## `File` Parameters with `UploadFile` Define a `File` parameter with a type of `UploadFile`: @@ -51,6 +51,7 @@ Define a `File` parameter with a type of `UploadFile`: Using `UploadFile` has several advantages over `bytes`: +* You don't have to use `File()` in the default value. * It uses a "spooled" file: * A file stored in memory up to a maximum size limit, and after passing this limit it will be stored in disk. * This means that it will work well for large files like images, videos, large binaries, etc. without consuming all the memory. @@ -113,7 +114,31 @@ The way HTML forms (`
`) sends the data to the server normally uses This is not a limitation of **FastAPI**, it's part of the HTTP protocol. -## Multiple file uploads +## Optional File Upload + +You can make a file optional by using standard type annotations: + +=== "Python 3.6 and above" + + ```Python hl_lines="9 17" + {!> ../../../docs_src/request_files/tutorial001_02.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="7 14" + {!> ../../../docs_src/request_files/tutorial001_02_py310.py!} + ``` + +## `UploadFile` with Additional Metadata + +You can also use `File()` with `UploadFile` to set additional parameters in `File()`, for example additional metadata: + +```Python hl_lines="13" +{!../../../docs_src/request_files/tutorial001_03.py!} +``` + +## Multiple File Uploads It's possible to upload several files at the same time. @@ -140,6 +165,22 @@ You will receive, as declared, a `list` of `bytes` or `UploadFile`s. **FastAPI** provides the same `starlette.responses` as `fastapi.responses` just as a convenience for you, the developer. But most of the available responses come directly from Starlette. +### Multiple File Uploads with Additional Metadata + +And the same way as before, you can use `File()` to set additional parameters, even for `UploadFile`: + +=== "Python 3.6 and above" + + ```Python hl_lines="18" + {!> ../../../docs_src/request_files/tutorial003.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="16" + {!> ../../../docs_src/request_files/tutorial003_py39.py!} + ``` + ## Recap Use `File` to declare files to be uploaded as input parameters (as form data). diff --git a/docs_src/request_files/tutorial001.py b/docs_src/request_files/tutorial001.py index fffb56af8..0fb1dd571 100644 --- a/docs_src/request_files/tutorial001.py +++ b/docs_src/request_files/tutorial001.py @@ -9,5 +9,5 @@ async def create_file(file: bytes = File(...)): @app.post("/uploadfile/") -async def create_upload_file(file: UploadFile = File(...)): +async def create_upload_file(file: UploadFile): return {"filename": file.filename} diff --git a/docs_src/request_files/tutorial001_02.py b/docs_src/request_files/tutorial001_02.py new file mode 100644 index 000000000..26a4c9cbf --- /dev/null +++ b/docs_src/request_files/tutorial001_02.py @@ -0,0 +1,21 @@ +from typing import Optional + +from fastapi import FastAPI, File, UploadFile + +app = FastAPI() + + +@app.post("/files/") +async def create_file(file: Optional[bytes] = File(None)): + if not file: + return {"message": "No file sent"} + else: + return {"file_size": len(file)} + + +@app.post("/uploadfile/") +async def create_upload_file(file: Optional[UploadFile] = None): + if not file: + return {"message": "No upload file sent"} + else: + return {"filename": file.filename} diff --git a/docs_src/request_files/tutorial001_02_py310.py b/docs_src/request_files/tutorial001_02_py310.py new file mode 100644 index 000000000..0e576251b --- /dev/null +++ b/docs_src/request_files/tutorial001_02_py310.py @@ -0,0 +1,19 @@ +from fastapi import FastAPI, File, UploadFile + +app = FastAPI() + + +@app.post("/files/") +async def create_file(file: bytes | None = File(None)): + if not file: + return {"message": "No file sent"} + else: + return {"file_size": len(file)} + + +@app.post("/uploadfile/") +async def create_upload_file(file: UploadFile | None = None): + if not file: + return {"message": "No upload file sent"} + else: + return {"filename": file.filename} diff --git a/docs_src/request_files/tutorial001_03.py b/docs_src/request_files/tutorial001_03.py new file mode 100644 index 000000000..abcac9e4c --- /dev/null +++ b/docs_src/request_files/tutorial001_03.py @@ -0,0 +1,15 @@ +from fastapi import FastAPI, File, UploadFile + +app = FastAPI() + + +@app.post("/files/") +async def create_file(file: bytes = File(..., description="A file read as bytes")): + return {"file_size": len(file)} + + +@app.post("/uploadfile/") +async def create_upload_file( + file: UploadFile = File(..., description="A file read as UploadFile") +): + return {"filename": file.filename} diff --git a/docs_src/request_files/tutorial002.py b/docs_src/request_files/tutorial002.py index 6fdf16a75..94abb7c6c 100644 --- a/docs_src/request_files/tutorial002.py +++ b/docs_src/request_files/tutorial002.py @@ -12,7 +12,7 @@ async def create_files(files: List[bytes] = File(...)): @app.post("/uploadfiles/") -async def create_upload_files(files: List[UploadFile] = File(...)): +async def create_upload_files(files: List[UploadFile]): return {"filenames": [file.filename for file in files]} diff --git a/docs_src/request_files/tutorial002_py39.py b/docs_src/request_files/tutorial002_py39.py index 26cd56769..2779618bd 100644 --- a/docs_src/request_files/tutorial002_py39.py +++ b/docs_src/request_files/tutorial002_py39.py @@ -10,7 +10,7 @@ async def create_files(files: list[bytes] = File(...)): @app.post("/uploadfiles/") -async def create_upload_files(files: list[UploadFile] = File(...)): +async def create_upload_files(files: list[UploadFile]): return {"filenames": [file.filename for file in files]} diff --git a/docs_src/request_files/tutorial003.py b/docs_src/request_files/tutorial003.py new file mode 100644 index 000000000..4a91b7a8b --- /dev/null +++ b/docs_src/request_files/tutorial003.py @@ -0,0 +1,37 @@ +from typing import List + +from fastapi import FastAPI, File, UploadFile +from fastapi.responses import HTMLResponse + +app = FastAPI() + + +@app.post("/files/") +async def create_files( + files: List[bytes] = File(..., description="Multiple files as bytes") +): + return {"file_sizes": [len(file) for file in files]} + + +@app.post("/uploadfiles/") +async def create_upload_files( + files: List[UploadFile] = File(..., description="Multiple files as UploadFile") +): + return {"filenames": [file.filename for file in files]} + + +@app.get("/") +async def main(): + content = """ + +
+ + +
+
+ + +
+ + """ + return HTMLResponse(content=content) diff --git a/docs_src/request_files/tutorial003_py39.py b/docs_src/request_files/tutorial003_py39.py new file mode 100644 index 000000000..d853f48d1 --- /dev/null +++ b/docs_src/request_files/tutorial003_py39.py @@ -0,0 +1,35 @@ +from fastapi import FastAPI, File, UploadFile +from fastapi.responses import HTMLResponse + +app = FastAPI() + + +@app.post("/files/") +async def create_files( + files: list[bytes] = File(..., description="Multiple files as bytes") +): + return {"file_sizes": [len(file) for file in files]} + + +@app.post("/uploadfiles/") +async def create_upload_files( + files: list[UploadFile] = File(..., description="Multiple files as UploadFile") +): + return {"filenames": [file.filename for file in files]} + + +@app.get("/") +async def main(): + content = """ + +
+ + +
+
+ + +
+ + """ + return HTMLResponse(content=content) diff --git a/fastapi/datastructures.py b/fastapi/datastructures.py index b13171287..b20a25ab6 100644 --- a/fastapi/datastructures.py +++ b/fastapi/datastructures.py @@ -1,4 +1,4 @@ -from typing import Any, Callable, Iterable, Type, TypeVar +from typing import Any, Callable, Dict, Iterable, Type, TypeVar from starlette.datastructures import URL as URL # noqa: F401 from starlette.datastructures import Address as Address # noqa: F401 @@ -20,6 +20,10 @@ class UploadFile(StarletteUploadFile): raise ValueError(f"Expected UploadFile, received: {type(v)}") return v + @classmethod + def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: + field_schema.update({"type": "string", "format": "binary"}) + class DefaultPlaceholder: """ diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 35ba44aab..d4028d067 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -390,6 +390,8 @@ def get_param_field( field.required = required if not had_schema and not is_scalar_field(field=field): field.field_info = params.Body(field_info.default) + if not had_schema and lenient_issubclass(field.type_, UploadFile): + field.field_info = params.File(field_info.default) return field @@ -701,25 +703,6 @@ def get_missing_field_error(loc: Tuple[str, ...]) -> ErrorWrapper: return missing_field_error -def get_schema_compatible_field(*, field: ModelField) -> ModelField: - out_field = field - if lenient_issubclass(field.type_, UploadFile): - use_type: type = bytes - if field.shape in sequence_shapes: - use_type = List[bytes] - out_field = create_response_field( - name=field.name, - type_=use_type, - class_validators=field.class_validators, - model_config=field.model_config, - default=field.default, - required=field.required, - alias=field.alias, - field_info=field.field_info, - ) - return out_field - - def get_body_field(*, dependant: Dependant, name: str) -> Optional[ModelField]: flat_dependant = get_flat_dependant(dependant) if not flat_dependant.body_params: @@ -729,9 +712,8 @@ def get_body_field(*, dependant: Dependant, name: str) -> Optional[ModelField]: embed = getattr(field_info, "embed", None) body_param_names_set = {param.name for param in flat_dependant.body_params} if len(body_param_names_set) == 1 and not embed: - final_field = get_schema_compatible_field(field=first_param) - check_file_field(final_field) - return final_field + check_file_field(first_param) + return first_param # If one field requires to embed, all have to be embedded # in case a sub-dependency is evaluated with a single unique body field # That is combined (embedded) with other body fields @@ -740,7 +722,7 @@ def get_body_field(*, dependant: Dependant, name: str) -> Optional[ModelField]: model_name = "Body_" + name BodyModel: Type[BaseModel] = create_model(model_name) for f in flat_dependant.body_params: - BodyModel.__fields__[f.name] = get_schema_compatible_field(field=f) + BodyModel.__fields__[f.name] = f required = any(True for f in flat_dependant.body_params if f.required) BodyFieldInfo_kwargs: Dict[str, Any] = dict(default=None) diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_02.py b/tests/test_tutorial/test_request_files/test_tutorial001_02.py new file mode 100644 index 000000000..e852a1b31 --- /dev/null +++ b/tests/test_tutorial/test_request_files/test_tutorial001_02.py @@ -0,0 +1,157 @@ +from fastapi.testclient import TestClient + +from docs_src.request_files.tutorial001_02 import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "summary": "Create File", + "operationId": "create_file_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/uploadfile/": { + "post": { + "summary": "Create Upload File", + "operationId": "create_upload_file_uploadfile__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__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_create_file_files__post": { + "title": "Body_create_file_files__post", + "type": "object", + "properties": { + "file": {"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"} + }, + }, + "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": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def test_post_form_no_body(): + response = client.post("/files/") + assert response.status_code == 200, response.text + assert response.json() == {"message": "No file sent"} + + +def test_post_uploadfile_no_body(): + response = client.post("/uploadfile/") + assert response.status_code == 200, response.text + assert response.json() == {"message": "No upload file sent"} + + +def test_post_file(tmp_path): + path = tmp_path / "test.txt" + path.write_bytes(b"") + + client = TestClient(app) + with path.open("rb") as file: + response = client.post("/files/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": 14} + + +def test_post_upload_file(tmp_path): + path = tmp_path / "test.txt" + path.write_bytes(b"") + + client = TestClient(app) + with path.open("rb") as file: + response = client.post("/uploadfile/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"filename": "test.txt"} 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 new file mode 100644 index 000000000..62e9f98d0 --- /dev/null +++ b/tests/test_tutorial/test_request_files/test_tutorial001_02_py310.py @@ -0,0 +1,169 @@ +from pathlib import Path + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "summary": "Create File", + "operationId": "create_file_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/uploadfile/": { + "post": { + "summary": "Create Upload File", + "operationId": "create_upload_file_uploadfile__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__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_create_file_files__post": { + "title": "Body_create_file_files__post", + "type": "object", + "properties": { + "file": {"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"} + }, + }, + "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": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.request_files.tutorial001_02_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py310 +def test_post_form_no_body(client: TestClient): + response = client.post("/files/") + assert response.status_code == 200, response.text + assert response.json() == {"message": "No file sent"} + + +@needs_py310 +def test_post_uploadfile_no_body(client: TestClient): + response = client.post("/uploadfile/") + assert response.status_code == 200, response.text + assert response.json() == {"message": "No upload file sent"} + + +@needs_py310 +def test_post_file(tmp_path: Path, client: TestClient): + path = tmp_path / "test.txt" + path.write_bytes(b"") + + with path.open("rb") as file: + response = client.post("/files/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": 14} + + +@needs_py310 +def test_post_upload_file(tmp_path: Path, client: TestClient): + path = tmp_path / "test.txt" + path.write_bytes(b"") + + with path.open("rb") as file: + response = client.post("/uploadfile/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"filename": "test.txt"} diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_03.py b/tests/test_tutorial/test_request_files/test_tutorial001_03.py new file mode 100644 index 000000000..ec7509ea2 --- /dev/null +++ b/tests/test_tutorial/test_request_files/test_tutorial001_03.py @@ -0,0 +1,159 @@ +from fastapi.testclient import TestClient + +from docs_src.request_files.tutorial001_03 import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "summary": "Create File", + "operationId": "create_file_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/uploadfile/": { + "post": { + "summary": "Create Upload File", + "operationId": "create_upload_file_uploadfile__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + } + }, + "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": { + "Body_create_file_files__post": { + "title": "Body_create_file_files__post", + "required": ["file"], + "type": "object", + "properties": { + "file": { + "title": "File", + "type": "string", + "description": "A file read as bytes", + "format": "binary", + } + }, + }, + "Body_create_upload_file_uploadfile__post": { + "title": "Body_create_upload_file_uploadfile__post", + "required": ["file"], + "type": "object", + "properties": { + "file": { + "title": "File", + "type": "string", + "description": "A file read as UploadFile", + "format": "binary", + } + }, + }, + "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": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def test_post_file(tmp_path): + path = tmp_path / "test.txt" + path.write_bytes(b"") + + client = TestClient(app) + with path.open("rb") as file: + response = client.post("/files/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": 14} + + +def test_post_upload_file(tmp_path): + path = tmp_path / "test.txt" + path.write_bytes(b"") + + client = TestClient(app) + with path.open("rb") as file: + response = client.post("/uploadfile/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"filename": "test.txt"} diff --git a/tests/test_tutorial/test_request_files/test_tutorial003.py b/tests/test_tutorial/test_request_files/test_tutorial003.py new file mode 100644 index 000000000..943b235ab --- /dev/null +++ b/tests/test_tutorial/test_request_files/test_tutorial003.py @@ -0,0 +1,194 @@ +from fastapi.testclient import TestClient + +from docs_src.request_files.tutorial003 import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "summary": "Create Files", + "operationId": "create_files_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_files_files__post" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/uploadfiles/": { + "post": { + "summary": "Create Upload Files", + "operationId": "create_upload_files_uploadfiles__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_upload_files_uploadfiles__post" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/": { + "get": { + "summary": "Main", + "operationId": "main__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + }, + }, + "components": { + "schemas": { + "Body_create_files_files__post": { + "title": "Body_create_files_files__post", + "required": ["files"], + "type": "object", + "properties": { + "files": { + "title": "Files", + "type": "array", + "items": {"type": "string", "format": "binary"}, + "description": "Multiple files as bytes", + } + }, + }, + "Body_create_upload_files_uploadfiles__post": { + "title": "Body_create_upload_files_uploadfiles__post", + "required": ["files"], + "type": "object", + "properties": { + "files": { + "title": "Files", + "type": "array", + "items": {"type": "string", "format": "binary"}, + "description": "Multiple files as UploadFile", + } + }, + }, + "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": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def test_post_files(tmp_path): + path = tmp_path / "test.txt" + path.write_bytes(b"") + path2 = tmp_path / "test2.txt" + path2.write_bytes(b"") + + client = TestClient(app) + with path.open("rb") as file, path2.open("rb") as file2: + response = client.post( + "/files/", + files=( + ("files", ("test.txt", file)), + ("files", ("test2.txt", file2)), + ), + ) + assert response.status_code == 200, response.text + assert response.json() == {"file_sizes": [14, 15]} + + +def test_post_upload_file(tmp_path): + path = tmp_path / "test.txt" + path.write_bytes(b"") + path2 = tmp_path / "test2.txt" + path2.write_bytes(b"") + + client = TestClient(app) + with path.open("rb") as file, path2.open("rb") as file2: + response = client.post( + "/uploadfiles/", + files=( + ("files", ("test.txt", file)), + ("files", ("test2.txt", file2)), + ), + ) + assert response.status_code == 200, response.text + assert response.json() == {"filenames": ["test.txt", "test2.txt"]} + + +def test_get_root(): + client = TestClient(app) + response = client.get("/") + assert response.status_code == 200, response.text + assert b"") + path2 = tmp_path / "test2.txt" + path2.write_bytes(b"") + + client = TestClient(app) + with path.open("rb") as file, path2.open("rb") as file2: + response = client.post( + "/files/", + files=( + ("files", ("test.txt", file)), + ("files", ("test2.txt", file2)), + ), + ) + assert response.status_code == 200, response.text + assert response.json() == {"file_sizes": [14, 15]} + + +@needs_py39 +def test_post_upload_file(tmp_path, app: FastAPI): + path = tmp_path / "test.txt" + path.write_bytes(b"") + path2 = tmp_path / "test2.txt" + path2.write_bytes(b"") + + client = TestClient(app) + with path.open("rb") as file, path2.open("rb") as file2: + response = client.post( + "/uploadfiles/", + files=( + ("files", ("test.txt", file)), + ("files", ("test2.txt", file2)), + ), + ) + assert response.status_code == 200, response.text + assert response.json() == {"filenames": ["test.txt", "test2.txt"]} + + +@needs_py39 +def test_get_root(app: FastAPI): + client = TestClient(app) + response = client.get("/") + assert response.status_code == 200, response.text + assert b" Date: Sun, 23 Jan 2022 19:14:47 +0000 Subject: [PATCH 51/59] =?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 7026d0eb1..b7593ee3c 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✨ Add support for declaring `UploadFile` parameters without explicit `File()`. PR [#4469](https://github.com/tiangolo/fastapi/pull/4469) by [@tiangolo](https://github.com/tiangolo). * ✨ Add support for tags with Enums. PR [#4468](https://github.com/tiangolo/fastapi/pull/4468) by [@tiangolo](https://github.com/tiangolo). * 🐛 Prefer custom encoder over defaults if specified in `jsonable_encoder`. PR [#2061](https://github.com/tiangolo/fastapi/pull/2061) by [@viveksunder](https://github.com/viveksunder). * 💚 Duplicate PR to trigger CI. PR [#4467](https://github.com/tiangolo/fastapi/pull/4467) by [@tiangolo](https://github.com/tiangolo). From f8d4d040155a58ecfdbc2ed58f0739b07e417516 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 23 Jan 2022 22:30:35 +0100 Subject: [PATCH 52/59] =?UTF-8?q?=F0=9F=93=9D=20Tweak=20and=20improve=20do?= =?UTF-8?q?cs=20for=20Request=20Files=20(#4470)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/tutorial/request-files.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/en/docs/tutorial/request-files.md b/docs/en/docs/tutorial/request-files.md index ed2c8b6af..3ca471a91 100644 --- a/docs/en/docs/tutorial/request-files.md +++ b/docs/en/docs/tutorial/request-files.md @@ -41,9 +41,9 @@ Have in mind that this means that the whole contents will be stored in memory. T But there are several cases in which you might benefit from using `UploadFile`. -## `File` Parameters with `UploadFile` +## File Parameters with `UploadFile` -Define a `File` parameter with a type of `UploadFile`: +Define a file parameter with a type of `UploadFile`: ```Python hl_lines="12" {!../../../docs_src/request_files/tutorial001.py!} @@ -51,7 +51,7 @@ Define a `File` parameter with a type of `UploadFile`: Using `UploadFile` has several advantages over `bytes`: -* You don't have to use `File()` in the default value. +* You don't have to use `File()` in the default value of the parameter. * It uses a "spooled" file: * A file stored in memory up to a maximum size limit, and after passing this limit it will be stored in disk. * This means that it will work well for large files like images, videos, large binaries, etc. without consuming all the memory. @@ -116,7 +116,7 @@ The way HTML forms (`
`) sends the data to the server normally uses ## Optional File Upload -You can make a file optional by using standard type annotations: +You can make a file optional by using standard type annotations and setting a default value of `None`: === "Python 3.6 and above" @@ -132,7 +132,7 @@ You can make a file optional by using standard type annotations: ## `UploadFile` with Additional Metadata -You can also use `File()` with `UploadFile` to set additional parameters in `File()`, for example additional metadata: +You can also use `File()` with `UploadFile`, for example, to set additional metadata: ```Python hl_lines="13" {!../../../docs_src/request_files/tutorial001_03.py!} @@ -183,4 +183,4 @@ And the same way as before, you can use `File()` to set additional parameters, e ## Recap -Use `File` to declare files to be uploaded as input parameters (as form data). +Use `File`, `bytes`, and `UploadFile` to declare files to be uploaded in the request, sent as form data. From dba9ea81208078bdd91fbfff4fdbfe203dcc303f Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 23 Jan 2022 21:31:08 +0000 Subject: [PATCH 53/59] =?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 b7593ee3c..84908dda5 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Tweak and improve docs for Request Files. PR [#4470](https://github.com/tiangolo/fastapi/pull/4470) by [@tiangolo](https://github.com/tiangolo). * ✨ Add support for declaring `UploadFile` parameters without explicit `File()`. PR [#4469](https://github.com/tiangolo/fastapi/pull/4469) by [@tiangolo](https://github.com/tiangolo). * ✨ Add support for tags with Enums. PR [#4468](https://github.com/tiangolo/fastapi/pull/4468) by [@tiangolo](https://github.com/tiangolo). * 🐛 Prefer custom encoder over defaults if specified in `jsonable_encoder`. PR [#2061](https://github.com/tiangolo/fastapi/pull/2061) by [@viveksunder](https://github.com/viveksunder). From a698908ed65d887a17245a580ecbf3bf3c848406 Mon Sep 17 00:00:00 2001 From: Victor Benichoux Date: Sun, 23 Jan 2022 23:13:55 +0100 Subject: [PATCH 54/59] =?UTF-8?q?=F0=9F=90=9B=20Fix=20bug=20preventing=20t?= =?UTF-8?q?o=20use=20OpenAPI=20when=20using=20tuples=20(#3874)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- fastapi/openapi/models.py | 2 +- tests/test_tuples.py | 267 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 268 insertions(+), 1 deletion(-) create mode 100644 tests/test_tuples.py diff --git a/fastapi/openapi/models.py b/fastapi/openapi/models.py index 361c75005..9c6598d2d 100644 --- a/fastapi/openapi/models.py +++ b/fastapi/openapi/models.py @@ -123,7 +123,7 @@ class Schema(BaseModel): oneOf: Optional[List["Schema"]] = None anyOf: Optional[List["Schema"]] = None not_: Optional["Schema"] = Field(None, alias="not") - items: Optional["Schema"] = None + items: Optional[Union["Schema", List["Schema"]]] = None properties: Optional[Dict[str, "Schema"]] = None additionalProperties: Optional[Union["Schema", Reference, bool]] = None description: Optional[str] = None diff --git a/tests/test_tuples.py b/tests/test_tuples.py new file mode 100644 index 000000000..4cd5ee3af --- /dev/null +++ b/tests/test_tuples.py @@ -0,0 +1,267 @@ +from typing import List, Tuple + +from fastapi import FastAPI, Form +from fastapi.testclient import TestClient +from pydantic import BaseModel + +app = FastAPI() + + +class ItemGroup(BaseModel): + items: List[Tuple[str, str]] + + +class Coordinate(BaseModel): + x: float + y: float + + +@app.post("/model-with-tuple/") +def post_model_with_tuple(item_group: ItemGroup): + return item_group + + +@app.post("/tuple-of-models/") +def post_tuple_of_models(square: Tuple[Coordinate, Coordinate]): + return square + + +@app.post("/tuple-form/") +def hello(values: Tuple[int, int] = Form(...)): + return values + + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/model-with-tuple/": { + "post": { + "summary": "Post Model With Tuple", + "operationId": "post_model_with_tuple_model_with_tuple__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/ItemGroup"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/tuple-of-models/": { + "post": { + "summary": "Post Tuple Of Models", + "operationId": "post_tuple_of_models_tuple_of_models__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "title": "Square", + "maxItems": 2, + "minItems": 2, + "type": "array", + "items": [ + {"$ref": "#/components/schemas/Coordinate"}, + {"$ref": "#/components/schemas/Coordinate"}, + ], + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/tuple-form/": { + "post": { + "summary": "Hello", + "operationId": "hello_tuple_form__post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Body_hello_tuple_form__post" + } + } + }, + "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": { + "Body_hello_tuple_form__post": { + "title": "Body_hello_tuple_form__post", + "required": ["values"], + "type": "object", + "properties": { + "values": { + "title": "Values", + "maxItems": 2, + "minItems": 2, + "type": "array", + "items": [{"type": "integer"}, {"type": "integer"}], + } + }, + }, + "Coordinate": { + "title": "Coordinate", + "required": ["x", "y"], + "type": "object", + "properties": { + "x": {"title": "X", "type": "number"}, + "y": {"title": "Y", "type": "number"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ItemGroup": { + "title": "ItemGroup", + "required": ["items"], + "type": "object", + "properties": { + "items": { + "title": "Items", + "type": "array", + "items": { + "maxItems": 2, + "minItems": 2, + "type": "array", + "items": [{"type": "string"}, {"type": "string"}], + }, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def test_model_with_tuple_valid(): + data = {"items": [["foo", "bar"], ["baz", "whatelse"]]} + response = client.post("/model-with-tuple/", json=data) + assert response.status_code == 200, response.text + assert response.json() == data + + +def test_model_with_tuple_invalid(): + data = {"items": [["foo", "bar"], ["baz", "whatelse", "too", "much"]]} + response = client.post("/model-with-tuple/", json=data) + assert response.status_code == 422, response.text + + data = {"items": [["foo", "bar"], ["baz"]]} + response = client.post("/model-with-tuple/", json=data) + assert response.status_code == 422, response.text + + +def test_tuple_with_model_valid(): + data = [{"x": 1, "y": 2}, {"x": 3, "y": 4}] + response = client.post("/tuple-of-models/", json=data) + assert response.status_code == 200, response.text + assert response.json() == data + + +def test_tuple_with_model_invalid(): + data = [{"x": 1, "y": 2}, {"x": 3, "y": 4}, {"x": 5, "y": 6}] + response = client.post("/tuple-of-models/", json=data) + assert response.status_code == 422, response.text + + data = [{"x": 1, "y": 2}] + response = client.post("/tuple-of-models/", json=data) + assert response.status_code == 422, response.text + + +def test_tuple_form_valid(): + response = client.post("/tuple-form/", data=[("values", "1"), ("values", "2")]) + assert response.status_code == 200, response.text + assert response.json() == [1, 2] + + +def test_tuple_form_invalid(): + response = client.post( + "/tuple-form/", data=[("values", "1"), ("values", "2"), ("values", "3")] + ) + assert response.status_code == 422, response.text + + response = client.post("/tuple-form/", data=[("values", "1")]) + assert response.status_code == 422, response.text From af18d5c49fde32e79e4dfd7a82819a2c642c6c17 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 23 Jan 2022 22:14:28 +0000 Subject: [PATCH 55/59] =?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 84908dda5..f9f3aabde 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🐛 Fix bug preventing to use OpenAPI when using tuples. PR [#3874](https://github.com/tiangolo/fastapi/pull/3874) by [@victorbenichoux](https://github.com/victorbenichoux). * 📝 Tweak and improve docs for Request Files. PR [#4470](https://github.com/tiangolo/fastapi/pull/4470) by [@tiangolo](https://github.com/tiangolo). * ✨ Add support for declaring `UploadFile` parameters without explicit `File()`. PR [#4469](https://github.com/tiangolo/fastapi/pull/4469) by [@tiangolo](https://github.com/tiangolo). * ✨ Add support for tags with Enums. PR [#4468](https://github.com/tiangolo/fastapi/pull/4468) by [@tiangolo](https://github.com/tiangolo). From cbe8d552c1eee5b48f8ab0eab6b517e98ae8523b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 23 Jan 2022 23:37:48 +0100 Subject: [PATCH 56/59] =?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 | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index f9f3aabde..e75d46706 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,14 +2,25 @@ ## Latest Changes -* 🐛 Fix bug preventing to use OpenAPI when using tuples. PR [#3874](https://github.com/tiangolo/fastapi/pull/3874) by [@victorbenichoux](https://github.com/victorbenichoux). +### Features + +* ✨ Add support for declaring `UploadFile` parameters without explicit `File()`. PR [#4469](https://github.com/tiangolo/fastapi/pull/4469) by [@tiangolo](https://github.com/tiangolo). New docs: [Request Files - File Parameters with UploadFile](https://fastapi.tiangolo.com/tutorial/request-files/#file-parameters-with-uploadfile). +* ✨ Add support for tags with Enums. PR [#4468](https://github.com/tiangolo/fastapi/pull/4468) by [@tiangolo](https://github.com/tiangolo). New docs: [Path Operation Configuration - Tags with Enums](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/#tags-with-enums). +* ✨ Allow hiding from OpenAPI (and Swagger UI) `Query`, `Cookie`, `Header`, and `Path` parameters. PR [#3144](https://github.com/tiangolo/fastapi/pull/3144) by [@astraldawn](https://github.com/astraldawn). New docs: [Query Parameters and String Validations - Exclude from OpenAPI](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#exclude-from-openapi). + +### Docs + * 📝 Tweak and improve docs for Request Files. PR [#4470](https://github.com/tiangolo/fastapi/pull/4470) by [@tiangolo](https://github.com/tiangolo). -* ✨ Add support for declaring `UploadFile` parameters without explicit `File()`. PR [#4469](https://github.com/tiangolo/fastapi/pull/4469) by [@tiangolo](https://github.com/tiangolo). -* ✨ Add support for tags with Enums. PR [#4468](https://github.com/tiangolo/fastapi/pull/4468) by [@tiangolo](https://github.com/tiangolo). + +### Fixes + +* 🐛 Fix bug preventing to use OpenAPI when using tuples. PR [#3874](https://github.com/tiangolo/fastapi/pull/3874) by [@victorbenichoux](https://github.com/victorbenichoux). * 🐛 Prefer custom encoder over defaults if specified in `jsonable_encoder`. PR [#2061](https://github.com/tiangolo/fastapi/pull/2061) by [@viveksunder](https://github.com/viveksunder). * 💚 Duplicate PR to trigger CI. PR [#4467](https://github.com/tiangolo/fastapi/pull/4467) by [@tiangolo](https://github.com/tiangolo). + +### Internal + * 🐛 Fix docs dependencies cache, to get the latest Material for MkDocs. PR [#4466](https://github.com/tiangolo/fastapi/pull/4466) by [@tiangolo](https://github.com/tiangolo). -* ✨ Allow hiding from OpenAPI (and Swagger UI) `Query`, `Cookie`, `Header`, and `Path` parameters. PR [#3144](https://github.com/tiangolo/fastapi/pull/3144) by [@astraldawn](https://github.com/astraldawn). * 🔧 Add sponsor Dropbase. PR [#4465](https://github.com/tiangolo/fastapi/pull/4465) by [@tiangolo](https://github.com/tiangolo). ## 0.72.0 From 291180bf2d8c39e84860c2426b1d58b6c80f6fef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 23 Jan 2022 23:38:51 +0100 Subject: [PATCH 57/59] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.73.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index e75d46706..68b75e702 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,8 @@ ## Latest Changes +## 0.73.0 + ### Features * ✨ Add support for declaring `UploadFile` parameters without explicit `File()`. PR [#4469](https://github.com/tiangolo/fastapi/pull/4469) by [@tiangolo](https://github.com/tiangolo). New docs: [Request Files - File Parameters with UploadFile](https://fastapi.tiangolo.com/tutorial/request-files/#file-parameters-with-uploadfile). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index d83fe6fbd..8718788fa 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.72.0" +__version__ = "0.73.0" from starlette import status as status From 618c99d77444e383e7b95ebe32ededbd95155c43 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 1 Feb 2022 15:27:34 +0100 Subject: [PATCH 58/59] =?UTF-8?q?=F0=9F=91=A5=20Update=20FastAPI=20People?= =?UTF-8?q?=20(#4502)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions --- docs/en/data/people.yml | 192 ++++++++++++++++++++++------------------ 1 file changed, 106 insertions(+), 86 deletions(-) diff --git a/docs/en/data/people.yml b/docs/en/data/people.yml index df088f39f..ebbe446ee 100644 --- a/docs/en/data/people.yml +++ b/docs/en/data/people.yml @@ -1,12 +1,12 @@ maintainers: - login: tiangolo - answers: 1230 - prs: 269 + answers: 1237 + prs: 280 avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=5cad72c846b7aba2e960546af490edc7375dafc4&v=4 url: https://github.com/tiangolo experts: - login: Kludex - count: 316 + count: 319 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=3682d9b9b93bef272f379ab623dc031c8d71432e&v=4 url: https://github.com/Kludex - login: dmontagu @@ -14,7 +14,7 @@ experts: avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=58ed2a45798a4339700e2f62b2e12e6e54bf0396&v=4 url: https://github.com/dmontagu - login: ycd - count: 219 + count: 221 avatarUrl: https://avatars.githubusercontent.com/u/62724709?u=826f228edf0bab0d19ad1d5c4ba4df1047ccffef&v=4 url: https://github.com/ycd - login: Mause @@ -34,7 +34,7 @@ experts: avatarUrl: https://avatars.githubusercontent.com/u/31127044?u=81a84af39c89b898b0fbc5a04e8834f60f23e55a&v=4 url: https://github.com/ArcLightSlavik - login: raphaelauv - count: 67 + count: 68 avatarUrl: https://avatars.githubusercontent.com/u/10202690?u=e6f86f5c0c3026a15d6b51792fa3e532b12f1371&v=4 url: https://github.com/raphaelauv - login: falkben @@ -57,18 +57,22 @@ experts: count: 38 avatarUrl: https://avatars.githubusercontent.com/u/11836741?u=8bd5ef7e62fe6a82055e33c4c0e0a7879ff8cfb6&v=4 url: https://github.com/includeamin +- login: STeveShary + count: 37 + avatarUrl: https://avatars.githubusercontent.com/u/5167622?u=de8f597c81d6336fcebc37b32dfd61a3f877160c&v=4 + url: https://github.com/STeveShary - login: prostomarkeloff count: 33 avatarUrl: https://avatars.githubusercontent.com/u/28061158?u=72309cc1f2e04e40fa38b29969cb4e9d3f722e7b&v=4 url: https://github.com/prostomarkeloff -- login: STeveShary - count: 32 - avatarUrl: https://avatars.githubusercontent.com/u/5167622?u=de8f597c81d6336fcebc37b32dfd61a3f877160c&v=4 - url: https://github.com/STeveShary - login: krishnardt count: 31 avatarUrl: https://avatars.githubusercontent.com/u/31960541?u=47f4829c77f4962ab437ffb7995951e41eeebe9b&v=4 url: https://github.com/krishnardt +- login: adriangb + count: 30 + avatarUrl: https://avatars.githubusercontent.com/u/1755071?u=81f0262df34e1460ca546fbd0c211169c2478532&v=4 + url: https://github.com/adriangb - login: wshayes count: 29 avatarUrl: https://avatars.githubusercontent.com/u/365303?u=07ca03c5ee811eb0920e633cc3c3db73dbec1aa5&v=4 @@ -77,10 +81,10 @@ experts: count: 29 avatarUrl: https://avatars.githubusercontent.com/u/1144727?u=85c025e3fcc7bd79a5665c63ee87cdf8aae13374&v=4 url: https://github.com/frankie567 -- login: adriangb - count: 28 - avatarUrl: https://avatars.githubusercontent.com/u/1755071?u=81f0262df34e1460ca546fbd0c211169c2478532&v=4 - url: https://github.com/adriangb +- login: chbndrhnns + count: 25 + avatarUrl: https://avatars.githubusercontent.com/u/7534547?v=4 + url: https://github.com/chbndrhnns - login: ghandic count: 25 avatarUrl: https://avatars.githubusercontent.com/u/23500353?u=e2e1d736f924d9be81e8bfc565b6d8836ba99773&v=4 @@ -89,18 +93,14 @@ experts: count: 25 avatarUrl: https://avatars.githubusercontent.com/u/43723790?u=9bcce836bbce55835291c5b2ac93a4e311f4b3c3&v=4 url: https://github.com/dbanty -- login: chbndrhnns - count: 24 - avatarUrl: https://avatars.githubusercontent.com/u/7534547?v=4 - url: https://github.com/chbndrhnns +- login: panla + count: 25 + avatarUrl: https://avatars.githubusercontent.com/u/41326348?u=ba2fda6b30110411ecbf406d187907e2b420ac19&v=4 + url: https://github.com/panla - login: SirTelemak count: 24 avatarUrl: https://avatars.githubusercontent.com/u/9435877?u=719327b7d2c4c62212456d771bfa7c6b8dbb9eac&v=4 url: https://github.com/SirTelemak -- login: panla - count: 23 - avatarUrl: https://avatars.githubusercontent.com/u/41326348?u=ba2fda6b30110411ecbf406d187907e2b420ac19&v=4 - url: https://github.com/panla - login: acnebs count: 22 avatarUrl: https://avatars.githubusercontent.com/u/9054108?u=c27e50269f1ef8ea950cc6f0268c8ec5cebbe9c9&v=4 @@ -129,22 +129,34 @@ experts: count: 17 avatarUrl: https://avatars.githubusercontent.com/u/28262306?u=66ee21316275ef356081c2efc4ed7a4572e690dc&v=4 url: https://github.com/nkhitrov +- login: acidjunk + count: 16 + avatarUrl: https://avatars.githubusercontent.com/u/685002?u=b5094ab4527fc84b006c0ac9ff54367bdebb2267&v=4 + url: https://github.com/acidjunk - login: waynerv count: 16 avatarUrl: https://avatars.githubusercontent.com/u/39515546?u=ec35139777597cdbbbddda29bf8b9d4396b429a9&v=4 url: https://github.com/waynerv -- login: acidjunk - count: 15 - avatarUrl: https://avatars.githubusercontent.com/u/685002?u=b5094ab4527fc84b006c0ac9ff54367bdebb2267&v=4 - url: https://github.com/acidjunk - login: dstlny - count: 14 + count: 16 avatarUrl: https://avatars.githubusercontent.com/u/41964673?u=9f2174f9d61c15c6e3a4c9e3aeee66f711ce311f&v=4 url: https://github.com/dstlny +- login: jgould22 + count: 14 + avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 + url: https://github.com/jgould22 +- login: harunyasar + count: 14 + avatarUrl: https://avatars.githubusercontent.com/u/1765494?u=5b1ab7c582db4b4016fa31affe977d10af108ad4&v=4 + url: https://github.com/harunyasar - login: haizaar count: 13 avatarUrl: https://avatars.githubusercontent.com/u/58201?u=4f1f9843d69433ca0d380d95146cfe119e5fdac4&v=4 url: https://github.com/haizaar +- login: hellocoldworld + count: 12 + avatarUrl: https://avatars.githubusercontent.com/u/47581948?v=4 + url: https://github.com/hellocoldworld - login: David-Lor count: 12 avatarUrl: https://avatars.githubusercontent.com/u/17401854?u=474680c02b94cba810cb9032fb7eb787d9cc9d22&v=4 @@ -173,39 +185,47 @@ experts: count: 10 avatarUrl: https://avatars.githubusercontent.com/u/20441825?u=ee1e59446b98f8ec2363caeda4c17164d0d9cc7d&v=4 url: https://github.com/stefanondisponibile -- login: hellocoldworld - count: 10 - avatarUrl: https://avatars.githubusercontent.com/u/47581948?v=4 - url: https://github.com/hellocoldworld - login: oligond count: 10 avatarUrl: https://avatars.githubusercontent.com/u/2858306?u=1bb1182a5944e93624b7fb26585f22c8f7a9d76e&v=4 url: https://github.com/oligond last_month_active: -- login: insomnes - count: 10 - avatarUrl: https://avatars.githubusercontent.com/u/16958893?u=f8be7088d5076d963984a21f95f44e559192d912&v=4 - url: https://github.com/insomnes -- login: raphaelauv - count: 6 - avatarUrl: https://avatars.githubusercontent.com/u/10202690?u=e6f86f5c0c3026a15d6b51792fa3e532b12f1371&v=4 - url: https://github.com/raphaelauv -- login: jgould22 - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 - url: https://github.com/jgould22 - login: harunyasar - count: 4 + count: 10 avatarUrl: https://avatars.githubusercontent.com/u/1765494?u=5b1ab7c582db4b4016fa31affe977d10af108ad4&v=4 url: https://github.com/harunyasar +- login: jgould22 + count: 10 + avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 + url: https://github.com/jgould22 +- login: rafsaf + count: 9 + avatarUrl: https://avatars.githubusercontent.com/u/51059348?u=be9f06b8ced2d2b677297decc781fa8ce4f7ddbd&v=4 + url: https://github.com/rafsaf +- login: STeveShary + count: 5 + avatarUrl: https://avatars.githubusercontent.com/u/5167622?u=de8f597c81d6336fcebc37b32dfd61a3f877160c&v=4 + url: https://github.com/STeveShary +- login: ahnaf-zamil + count: 3 + avatarUrl: https://avatars.githubusercontent.com/u/57180217?u=849128b146771ace47beca5b5ff68eb82905dd6d&v=4 + url: https://github.com/ahnaf-zamil +- login: lucastosetto + count: 3 + avatarUrl: https://avatars.githubusercontent.com/u/89307132?u=56326696423df7126c9e7c702ee58f294db69a2a&v=4 + url: https://github.com/lucastosetto +- login: blokje + count: 3 + avatarUrl: https://avatars.githubusercontent.com/u/851418?v=4 + url: https://github.com/blokje +- login: MatthijsKok + count: 3 + avatarUrl: https://avatars.githubusercontent.com/u/7658129?u=1243e32d57e13abc45e3f5235ed5b9197e0d2b41&v=4 + url: https://github.com/MatthijsKok - login: Kludex count: 3 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=3682d9b9b93bef272f379ab623dc031c8d71432e&v=4 url: https://github.com/Kludex -- login: panla - count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/41326348?u=ba2fda6b30110411ecbf406d187907e2b420ac19&v=4 - url: https://github.com/panla top_contributors: - login: waynerv count: 25 @@ -219,14 +239,14 @@ top_contributors: count: 16 avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=58ed2a45798a4339700e2f62b2e12e6e54bf0396&v=4 url: https://github.com/dmontagu +- login: jaystone776 + count: 15 + avatarUrl: https://avatars.githubusercontent.com/u/11191137?u=299205a95e9b6817a43144a48b643346a5aac5cc&v=4 + url: https://github.com/jaystone776 - login: euri10 count: 13 avatarUrl: https://avatars.githubusercontent.com/u/1104190?u=321a2e953e6645a7d09b732786c7a8061e0f8a8b&v=4 url: https://github.com/euri10 -- login: jaystone776 - count: 13 - avatarUrl: https://avatars.githubusercontent.com/u/11191137?u=299205a95e9b6817a43144a48b643346a5aac5cc&v=4 - url: https://github.com/jaystone776 - login: mariacamilagl count: 12 avatarUrl: https://avatars.githubusercontent.com/u/11489395?u=4adb6986bf3debfc2b8216ae701f2bd47d73da7d&v=4 @@ -285,7 +305,7 @@ top_contributors: url: https://github.com/NinaHwang top_reviewers: - login: Kludex - count: 91 + count: 93 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=3682d9b9b93bef272f379ab623dc031c8d71432e&v=4 url: https://github.com/Kludex - login: waynerv @@ -301,7 +321,7 @@ top_reviewers: avatarUrl: https://avatars.githubusercontent.com/u/41147016?u=55010621aece725aa702270b54fed829b6a1fe60&v=4 url: https://github.com/tokusumi - login: ycd - count: 44 + count: 45 avatarUrl: https://avatars.githubusercontent.com/u/62724709?u=826f228edf0bab0d19ad1d5c4ba4df1047ccffef&v=4 url: https://github.com/ycd - login: AdrianDeAnda @@ -312,12 +332,16 @@ top_reviewers: count: 31 avatarUrl: https://avatars.githubusercontent.com/u/31127044?u=81a84af39c89b898b0fbc5a04e8834f60f23e55a&v=4 url: https://github.com/ArcLightSlavik +- login: cikay + count: 24 + avatarUrl: https://avatars.githubusercontent.com/u/24587499?u=e772190a051ab0eaa9c8542fcff1892471638f2b&v=4 + url: https://github.com/cikay - login: dmontagu count: 23 avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=58ed2a45798a4339700e2f62b2e12e6e54bf0396&v=4 url: https://github.com/dmontagu - login: cassiobotaro - count: 22 + count: 23 avatarUrl: https://avatars.githubusercontent.com/u/3127847?u=b0a652331da17efeb85cd6e3a4969182e5004804&v=4 url: https://github.com/cassiobotaro - login: komtaki @@ -336,6 +360,10 @@ top_reviewers: count: 16 avatarUrl: https://avatars.githubusercontent.com/u/21978760?v=4 url: https://github.com/yanever +- login: lsglucas + count: 16 + avatarUrl: https://avatars.githubusercontent.com/u/61513630?u=320e43fe4dc7bc6efc64e9b8f325f8075634fd20&v=4 + url: https://github.com/lsglucas - login: SwftAlpc count: 16 avatarUrl: https://avatars.githubusercontent.com/u/52768429?u=6a3aa15277406520ad37f6236e89466ed44bc5b8&v=4 @@ -356,26 +384,22 @@ top_reviewers: count: 15 avatarUrl: https://avatars.githubusercontent.com/u/63476957?u=6c86e59b48e0394d4db230f37fc9ad4d7e2c27c7&v=4 url: https://github.com/delhi09 -- login: lsglucas - count: 14 - avatarUrl: https://avatars.githubusercontent.com/u/61513630?u=320e43fe4dc7bc6efc64e9b8f325f8075634fd20&v=4 - url: https://github.com/lsglucas - login: rjNemo - count: 13 + count: 14 avatarUrl: https://avatars.githubusercontent.com/u/56785022?u=d5c3a02567c8649e146fcfc51b6060ccaf8adef8&v=4 url: https://github.com/rjNemo - login: RunningIkkyu count: 12 avatarUrl: https://avatars.githubusercontent.com/u/31848542?u=706e1ee3f248245f2d68b976d149d06fd5a2010d&v=4 url: https://github.com/RunningIkkyu +- login: yezz123 + count: 12 + avatarUrl: https://avatars.githubusercontent.com/u/52716203?u=636b4f79645176df4527dd45c12d5dbb5a4193cf&v=4 + url: https://github.com/yezz123 - login: sh0nk count: 12 avatarUrl: https://avatars.githubusercontent.com/u/6478810?u=af15d724875cec682ed8088a86d36b2798f981c0&v=4 url: https://github.com/sh0nk -- login: yezz123 - count: 11 - avatarUrl: https://avatars.githubusercontent.com/u/52716203?u=636b4f79645176df4527dd45c12d5dbb5a4193cf&v=4 - url: https://github.com/yezz123 - login: mariacamilagl count: 10 avatarUrl: https://avatars.githubusercontent.com/u/11489395?u=4adb6986bf3debfc2b8216ae701f2bd47d73da7d&v=4 @@ -400,6 +424,10 @@ top_reviewers: count: 9 avatarUrl: https://avatars.githubusercontent.com/u/49435654?v=4 url: https://github.com/kty4119 +- login: zy7y + count: 9 + avatarUrl: https://avatars.githubusercontent.com/u/67154681?u=5d634834cc514028ea3f9115f7030b99a1f4d5a4&v=4 + url: https://github.com/zy7y - login: bezaca count: 9 avatarUrl: https://avatars.githubusercontent.com/u/69092910?u=4ac58eab99bd37d663f3d23551df96d4fbdbf760&v=4 @@ -444,39 +472,31 @@ top_reviewers: count: 7 avatarUrl: https://avatars.githubusercontent.com/u/34248814?v=4 url: https://github.com/krocdort +- login: dimaqq + count: 7 + avatarUrl: https://avatars.githubusercontent.com/u/662249?v=4 + url: https://github.com/dimaqq - login: jovicon count: 6 avatarUrl: https://avatars.githubusercontent.com/u/21287303?u=b049eac3e51a4c0473c2efe66b4d28a7d8f2b572&v=4 url: https://github.com/jovicon +- login: NinaHwang + count: 6 + avatarUrl: https://avatars.githubusercontent.com/u/79563565?u=1741703bd6c8f491503354b363a86e879b4c1cab&v=4 + url: https://github.com/NinaHwang - login: diogoduartec count: 5 avatarUrl: https://avatars.githubusercontent.com/u/31852339?u=b50fc11c531e9b77922e19edfc9e7233d4d7b92e&v=4 url: https://github.com/diogoduartec -- login: nimctl +- login: n25a count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/49960770?u=e39b11d47188744ee07b2a1c7ce1a1bdf3c80760&v=4 - url: https://github.com/nimctl + avatarUrl: https://avatars.githubusercontent.com/u/49960770?u=eb3c95338741c78fff7d9d5d7ace9617e53eee4a&v=4 + url: https://github.com/n25a +- login: izaguerreiro + count: 5 + avatarUrl: https://avatars.githubusercontent.com/u/2241504?v=4 + url: https://github.com/izaguerreiro - login: israteneda count: 5 avatarUrl: https://avatars.githubusercontent.com/u/20668624?u=d7b2961d330aca65fbce5bdb26a0800a3d23ed2d&v=4 url: https://github.com/israteneda -- login: juntatalor - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/8134632?v=4 - url: https://github.com/juntatalor -- login: SnkSynthesis - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/63564282?u=0078826509dbecb2fdb543f4e881c9cd06157893&v=4 - url: https://github.com/SnkSynthesis -- login: anthonycepeda - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/72019805?u=892f700c79f9732211bd5221bf16eec32356a732&v=4 - url: https://github.com/anthonycepeda -- login: oandersonmagalhaes - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/83456692?v=4 - url: https://github.com/oandersonmagalhaes -- login: qysfblog - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/52229895?v=4 - url: https://github.com/qysfblog From b93f8a709ab3923d1268dbc845f41985c0302b33 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 1 Feb 2022 14:28:16 +0000 Subject: [PATCH 59/59] =?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 68b75e702..6d5ee8ea1 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 [#4502](https://github.com/tiangolo/fastapi/pull/4502) by [@github-actions[bot]](https://github.com/apps/github-actions). ## 0.73.0 ### Features