diff --git a/.github/labeler.yml b/.github/labeler.yml index 57c5e1120f..3c0bf473e0 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -29,8 +29,6 @@ internal: - scripts/** - .gitignore - .pre-commit-config.yaml - - pdm_build.py - - requirements*.txt - uv.lock - docs/en/data/sponsors.yml - docs/en/overrides/main.html diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 2232498cb1..58f4f6dd8a 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -8,11 +8,6 @@ on: jobs: publish: runs-on: ubuntu-latest - strategy: - matrix: - package: - - fastapi - - fastapi-slim permissions: id-token: write contents: read @@ -26,14 +21,9 @@ jobs: uses: actions/setup-python@v6 with: python-version-file: ".python-version" - # Issue ref: https://github.com/actions/setup-python/issues/436 - # cache: "pip" - # cache-dependency-path: pyproject.toml - name: Install uv uses: astral-sh/setup-uv@v7 - name: Build distribution run: uv build - env: - TIANGOLO_BUILD_PACKAGE: ${{ matrix.package }} - name: Publish run: uv publish diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 338f6c390f..6046a4560d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -45,7 +45,7 @@ jobs: test: needs: - changes - if: needs.changes.outputs.src == 'true' + if: needs.changes.outputs.src == 'true' || github.ref == 'refs/heads/master' strategy: matrix: os: [ windows-latest, macos-latest ] @@ -68,10 +68,8 @@ jobs: python-version: "3.13" coverage: coverage uv-resolution: highest - # Ubuntu with 3.13 needs coverage for CodSpeed benchmarks - os: ubuntu-latest python-version: "3.13" - coverage: coverage uv-resolution: highest codspeed: codspeed - os: ubuntu-latest @@ -109,20 +107,10 @@ jobs: run: uv pip install "git+https://github.com/Kludex/starlette@main" - run: mkdir coverage - name: Test - if: matrix.codspeed != 'codspeed' run: uv run --no-sync bash scripts/test.sh env: COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }} CONTEXT: ${{ runner.os }}-py${{ matrix.python-version }} - - name: CodSpeed benchmarks - if: matrix.codspeed == 'codspeed' - uses: CodSpeedHQ/action@v4 - env: - COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }} - CONTEXT: ${{ runner.os }}-py${{ matrix.python-version }} - with: - mode: simulation - run: uv run --no-sync coverage run -m pytest tests/ --codspeed # Do not store coverage for all possible combinations to avoid file size max errors in Smokeshow - name: Store coverage files if: matrix.coverage == 'coverage' @@ -132,6 +120,39 @@ jobs: path: coverage include-hidden-files: true + benchmark: + needs: + - changes + if: needs.changes.outputs.src == 'true' || github.ref == 'refs/heads/master' + runs-on: ubuntu-latest + env: + UV_PYTHON: "3.13" + UV_RESOLUTION: highest + steps: + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" + - uses: actions/checkout@v6 + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.13" + - name: Setup uv + uses: astral-sh/setup-uv@v7 + with: + enable-cache: true + cache-dependency-glob: | + pyproject.toml + uv.lock + - name: Install Dependencies + run: uv sync --no-dev --group tests --extra all + - name: CodSpeed benchmarks + uses: CodSpeedHQ/action@v4 + with: + mode: simulation + run: uv run --no-sync pytest tests/benchmarks --codspeed + coverage-combine: needs: - test @@ -176,6 +197,7 @@ jobs: if: always() needs: - coverage-combine + - benchmark runs-on: ubuntu-latest steps: - name: Dump GitHub context @@ -186,4 +208,4 @@ jobs: uses: re-actors/alls-green@release/v1 with: jobs: ${{ toJSON(needs) }} - allowed-skips: coverage-combine,test + allowed-skips: coverage-combine,test,benchmark diff --git a/docs/en/data/people.yml b/docs/en/data/people.yml index 2fdb21a059..89269ecd69 100644 --- a/docs/en/data/people.yml +++ b/docs/en/data/people.yml @@ -1,23 +1,23 @@ maintainers: - login: tiangolo - answers: 1900 + answers: 1923 avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=cb5d06e73a9e1998141b1641aa88e443c6717651&v=4 url: https://github.com/tiangolo experts: - login: tiangolo - count: 1900 + count: 1923 avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=cb5d06e73a9e1998141b1641aa88e443c6717651&v=4 url: https://github.com/tiangolo - login: YuriiMotov - count: 971 - avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=b9b13d598dddfab529a52d264df80a900bfe7060&v=4 + count: 1107 + avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=bc48be95c429989224786106b027f3c5e40cc354&v=4 url: https://github.com/YuriiMotov - login: github-actions - count: 769 + count: 770 avatarUrl: https://avatars.githubusercontent.com/in/15368?v=4 url: https://github.com/apps/github-actions - login: Kludex - count: 654 + count: 656 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=df8a3f06ba8f55ae1967a3e2d5ed882903a4e330&v=4 url: https://github.com/Kludex - login: jgould22 @@ -37,7 +37,7 @@ experts: avatarUrl: https://avatars.githubusercontent.com/u/62724709?u=f1e7bae394a315da950912c92dc861a8eaf95d4c&v=4 url: https://github.com/ycd - login: JarroVGIT - count: 190 + count: 192 avatarUrl: https://avatars.githubusercontent.com/u/13659033?u=e8bea32d07a5ef72f7dde3b2079ceb714923ca05&v=4 url: https://github.com/JarroVGIT - login: euri10 @@ -53,11 +53,11 @@ experts: avatarUrl: https://avatars.githubusercontent.com/u/331403?v=4 url: https://github.com/phy25 - login: JavierSanchezCastro - count: 94 + count: 106 avatarUrl: https://avatars.githubusercontent.com/u/72013291?u=ae5679e6bd971d9d98cd5e76e8683f83642ba950&v=4 url: https://github.com/JavierSanchezCastro - login: luzzodev - count: 89 + count: 104 avatarUrl: https://avatars.githubusercontent.com/u/27291415?u=5607ae1ce75c5f54f09500ca854227f7bfd2033b&v=4 url: https://github.com/luzzodev - login: raphaelauv @@ -81,32 +81,32 @@ experts: avatarUrl: https://avatars.githubusercontent.com/u/653031?u=ad9838e089058c9e5a0bab94c0eec7cc181e0cd0&v=4 url: https://github.com/falkben - login: yinziyan1206 - count: 54 + count: 55 avatarUrl: https://avatars.githubusercontent.com/u/37829370?u=da44ca53aefd5c23f346fab8e9fd2e108294c179&v=4 url: https://github.com/yinziyan1206 +- login: acidjunk + count: 50 + avatarUrl: https://avatars.githubusercontent.com/u/685002?u=b5094ab4527fc84b006c0ac9ff54367bdebb2267&v=4 + url: https://github.com/acidjunk - login: sm-Fifteen count: 49 avatarUrl: https://avatars.githubusercontent.com/u/516999?u=437c0c5038558c67e887ccd863c1ba0f846c03da&v=4 url: https://github.com/sm-Fifteen -- login: acidjunk - count: 49 - avatarUrl: https://avatars.githubusercontent.com/u/685002?u=b5094ab4527fc84b006c0ac9ff54367bdebb2267&v=4 - url: https://github.com/acidjunk - login: adriangb count: 46 avatarUrl: https://avatars.githubusercontent.com/u/1755071?u=612704256e38d6ac9cbed24f10e4b6ac2da74ecb&v=4 url: https://github.com/adriangb -- login: Dustyposa - count: 45 - avatarUrl: https://avatars.githubusercontent.com/u/27180793?u=5cf2877f50b3eb2bc55086089a78a36f07042889&v=4 - url: https://github.com/Dustyposa - login: insomnes count: 45 avatarUrl: https://avatars.githubusercontent.com/u/16958893?u=f8be7088d5076d963984a21f95f44e559192d912&v=4 url: https://github.com/insomnes +- login: Dustyposa + count: 45 + avatarUrl: https://avatars.githubusercontent.com/u/27180793?u=5cf2877f50b3eb2bc55086089a78a36f07042889&v=4 + url: https://github.com/Dustyposa - login: frankie567 count: 43 - avatarUrl: https://avatars.githubusercontent.com/u/1144727?u=c159fe047727aedecbbeeaa96a1b03ceb9d39add&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/1144727?u=f3e79acfe4ed207e15c2145161a8a9759925fcd2&v=4 url: https://github.com/frankie567 - login: odiseo0 count: 43 @@ -120,14 +120,14 @@ experts: count: 40 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: chbndrhnns count: 37 avatarUrl: https://avatars.githubusercontent.com/u/7534547?v=4 url: https://github.com/chbndrhnns +- login: STeveShary + count: 37 + avatarUrl: https://avatars.githubusercontent.com/u/5167622?u=de8f597c81d6336fcebc37b32dfd61a3f877160c&v=4 + url: https://github.com/STeveShary - login: krishnardt count: 35 avatarUrl: https://avatars.githubusercontent.com/u/31960541?u=47f4829c77f4962ab437ffb7995951e41eeebe9b&v=4 @@ -136,18 +136,22 @@ experts: count: 32 avatarUrl: https://avatars.githubusercontent.com/u/41326348?u=ba2fda6b30110411ecbf406d187907e2b420ac19&v=4 url: https://github.com/panla +- login: valentinDruzhinin + count: 30 + avatarUrl: https://avatars.githubusercontent.com/u/12831905?u=aae1ebc675c91e8fa582df4fcc4fc4128106344d&v=4 + url: https://github.com/valentinDruzhinin - login: prostomarkeloff count: 28 avatarUrl: https://avatars.githubusercontent.com/u/28061158?u=6918e39a1224194ba636e897461a02a20126d7ad&v=4 url: https://github.com/prostomarkeloff -- login: hasansezertasan - count: 27 - avatarUrl: https://avatars.githubusercontent.com/u/13135006?u=99f0b0f0fc47e88e8abb337b4447357939ef93e7&v=4 - url: https://github.com/hasansezertasan - login: alv2017 - count: 26 + count: 27 avatarUrl: https://avatars.githubusercontent.com/u/31544722?v=4 url: https://github.com/alv2017 +- login: hasansezertasan + count: 27 + avatarUrl: https://avatars.githubusercontent.com/u/13135006?u=d36995e41a00590da64e6204cfd112e0484ac1ca&v=4 + url: https://github.com/hasansezertasan - login: dbanty count: 26 avatarUrl: https://avatars.githubusercontent.com/u/43723790?u=9d726785d08e50b1e1cd96505800c8ea8405bce2&v=4 @@ -156,10 +160,6 @@ experts: count: 25 avatarUrl: https://avatars.githubusercontent.com/u/365303?u=07ca03c5ee811eb0920e633cc3c3db73dbec1aa5&v=4 url: https://github.com/wshayes -- login: valentinDruzhinin - count: 24 - avatarUrl: https://avatars.githubusercontent.com/u/12831905?u=aae1ebc675c91e8fa582df4fcc4fc4128106344d&v=4 - url: https://github.com/valentinDruzhinin - login: SirTelemak count: 23 avatarUrl: https://avatars.githubusercontent.com/u/9435877?u=719327b7d2c4c62212456d771bfa7c6b8dbb9eac&v=4 @@ -176,6 +176,10 @@ experts: count: 22 avatarUrl: https://avatars.githubusercontent.com/u/79946379?u=03d85b22d696a58a9603e55fbbbe2de6b0f4face&v=4 url: https://github.com/chrisK824 +- login: ceb10n + count: 21 + avatarUrl: https://avatars.githubusercontent.com/u/235213?u=edcce471814a1eba9f0cdaa4cd0de18921a940a6&v=4 + url: https://github.com/ceb10n - login: rafsaf count: 21 avatarUrl: https://avatars.githubusercontent.com/u/51059348?u=5fe59a56e1f2f9ccd8005d71752a8276f133ae1a&v=4 @@ -194,7 +198,7 @@ experts: url: https://github.com/ebottos94 - login: estebanx64 count: 19 - avatarUrl: https://avatars.githubusercontent.com/u/10840422?u=1900887aeed268699e5ea6f3fb7db614f7b77cd3&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/10840422?u=2ca073ee47a625e495a9573bd374ddcd7be5ec91&v=4 url: https://github.com/estebanx64 - login: sehraramiz count: 18 @@ -236,467 +240,471 @@ experts: count: 16 avatarUrl: https://avatars.githubusercontent.com/u/26334101?u=f601c3f111f2148bd9244c2cb3ebbd57b592e674&v=4 url: https://github.com/jonatasoli -- login: ghost - count: 15 - avatarUrl: https://avatars.githubusercontent.com/u/10137?u=b1951d34a583cf12ec0d3b0781ba19be97726318&v=4 - url: https://github.com/ghost -- login: abhint - count: 15 - avatarUrl: https://avatars.githubusercontent.com/u/25699289?u=b5d219277b4d001ac26fb8be357fddd88c29d51b&v=4 - url: https://github.com/abhint -last_month_experts: -- login: YuriiMotov - count: 17 - avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=b9b13d598dddfab529a52d264df80a900bfe7060&v=4 - url: https://github.com/YuriiMotov -- login: valentinDruzhinin - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/12831905?u=aae1ebc675c91e8fa582df4fcc4fc4128106344d&v=4 - url: https://github.com/valentinDruzhinin -- login: yinziyan1206 - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/37829370?u=da44ca53aefd5c23f346fab8e9fd2e108294c179&v=4 - url: https://github.com/yinziyan1206 -- login: tiangolo - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=cb5d06e73a9e1998141b1641aa88e443c6717651&v=4 - url: https://github.com/tiangolo -- login: luzzodev - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/27291415?u=5607ae1ce75c5f54f09500ca854227f7bfd2033b&v=4 - url: https://github.com/luzzodev -three_months_experts: -- login: YuriiMotov - count: 397 - avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=b9b13d598dddfab529a52d264df80a900bfe7060&v=4 - url: https://github.com/YuriiMotov -- login: valentinDruzhinin - count: 24 - avatarUrl: https://avatars.githubusercontent.com/u/12831905?u=aae1ebc675c91e8fa582df4fcc4fc4128106344d&v=4 - url: https://github.com/valentinDruzhinin -- login: luzzodev - count: 17 - avatarUrl: https://avatars.githubusercontent.com/u/27291415?u=5607ae1ce75c5f54f09500ca854227f7bfd2033b&v=4 - url: https://github.com/luzzodev -- login: raceychan - count: 6 - avatarUrl: https://avatars.githubusercontent.com/u/75417963?u=060c62870ec5a791765e63ac20d8885d11143786&v=4 - url: https://github.com/raceychan -- login: yinziyan1206 - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/37829370?u=da44ca53aefd5c23f346fab8e9fd2e108294c179&v=4 - url: https://github.com/yinziyan1206 -- login: DoctorJohn - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/14076775?u=2913e70a6142772847e91e2aaa5b9152391715e9&v=4 - url: https://github.com/DoctorJohn -- login: tiangolo - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=cb5d06e73a9e1998141b1641aa88e443c6717651&v=4 - url: https://github.com/tiangolo -- login: sachinh35 - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/21972708?u=8560b97b8b41e175f476270b56de8a493b84f302&v=4 - url: https://github.com/sachinh35 -- login: eqsdxr - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/157279130?u=58fddf77ed76966eaa8c73eea9bea4bb0c53b673&v=4 - url: https://github.com/eqsdxr -- login: Jelle-tenB - count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/210023470?u=c25d66addf36a747bd9fab773c4a6e7b238f45d4&v=4 - url: https://github.com/Jelle-tenB -- login: pythonweb2 - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/32141163?v=4 - url: https://github.com/pythonweb2 -- login: WilliamDEdwards - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/12184311?u=9b29d5d1d71f5f1a7ef9e439963ad3529e3b33a4&v=4 - url: https://github.com/WilliamDEdwards -- login: Brikas - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/80290187?v=4 - url: https://github.com/Brikas -- login: purepani - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/7587353?v=4 - url: https://github.com/purepani -- login: JavierSanchezCastro - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/72013291?u=ae5679e6bd971d9d98cd5e76e8683f83642ba950&v=4 - url: https://github.com/JavierSanchezCastro -- login: TaigoFr - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/17792131?u=372b27056ec82f1ae03d8b3f37ef55b04a7cfdd1&v=4 - url: https://github.com/TaigoFr -- login: Garrett-R - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/6614695?u=c128fd775002882f6e391bda5a89d1bdc5bdf45f&v=4 - url: https://github.com/Garrett-R -- login: jymchng - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/27895426?u=fb88c47775147d62a395fdb895d1af4148c7b566&v=4 - url: https://github.com/jymchng -- login: davidhuser - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/4357648?u=6ed702f8f6d49a8b2a0ed33cbd8ab59c2d7db7f7&v=4 - url: https://github.com/davidhuser -six_months_experts: -- login: YuriiMotov - count: 763 - avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=b9b13d598dddfab529a52d264df80a900bfe7060&v=4 - url: https://github.com/YuriiMotov -- login: luzzodev - count: 45 - avatarUrl: https://avatars.githubusercontent.com/u/27291415?u=5607ae1ce75c5f54f09500ca854227f7bfd2033b&v=4 - url: https://github.com/luzzodev -- login: valentinDruzhinin - count: 24 - avatarUrl: https://avatars.githubusercontent.com/u/12831905?u=aae1ebc675c91e8fa582df4fcc4fc4128106344d&v=4 - url: https://github.com/valentinDruzhinin -- login: alv2017 - count: 16 - avatarUrl: https://avatars.githubusercontent.com/u/31544722?v=4 - url: https://github.com/alv2017 -- login: sachinh35 - count: 9 - avatarUrl: https://avatars.githubusercontent.com/u/21972708?u=8560b97b8b41e175f476270b56de8a493b84f302&v=4 - url: https://github.com/sachinh35 -- login: yauhen-sobaleu - count: 9 - avatarUrl: https://avatars.githubusercontent.com/u/51629535?u=fc1817060daf2df438bfca86c44f33da5cd667db&v=4 - url: https://github.com/yauhen-sobaleu -- login: tiangolo - count: 6 - avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=cb5d06e73a9e1998141b1641aa88e443c6717651&v=4 - url: https://github.com/tiangolo -- login: JavierSanchezCastro - count: 6 - avatarUrl: https://avatars.githubusercontent.com/u/72013291?u=ae5679e6bd971d9d98cd5e76e8683f83642ba950&v=4 - url: https://github.com/JavierSanchezCastro -- login: raceychan - count: 6 - avatarUrl: https://avatars.githubusercontent.com/u/75417963?u=060c62870ec5a791765e63ac20d8885d11143786&v=4 - url: https://github.com/raceychan -- login: yinziyan1206 - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/37829370?u=da44ca53aefd5c23f346fab8e9fd2e108294c179&v=4 - url: https://github.com/yinziyan1206 -- login: DoctorJohn - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/14076775?u=2913e70a6142772847e91e2aaa5b9152391715e9&v=4 - url: https://github.com/DoctorJohn -- login: eqsdxr - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/157279130?u=58fddf77ed76966eaa8c73eea9bea4bb0c53b673&v=4 - url: https://github.com/eqsdxr -- login: Kludex - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=df8a3f06ba8f55ae1967a3e2d5ed882903a4e330&v=4 - url: https://github.com/Kludex -- login: Jelle-tenB - count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/210023470?u=c25d66addf36a747bd9fab773c4a6e7b238f45d4&v=4 - url: https://github.com/Jelle-tenB -- login: adsouza - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/275832?u=f90f110cfafeafed2f14339e840941c2c328c186&v=4 - url: https://github.com/adsouza -- login: pythonweb2 - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/32141163?v=4 - url: https://github.com/pythonweb2 -- login: WilliamDEdwards - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/12184311?u=9b29d5d1d71f5f1a7ef9e439963ad3529e3b33a4&v=4 - url: https://github.com/WilliamDEdwards -- login: Brikas - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/80290187?v=4 - url: https://github.com/Brikas -- login: purepani - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/7587353?v=4 - url: https://github.com/purepani -- login: TaigoFr - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/17792131?u=372b27056ec82f1ae03d8b3f37ef55b04a7cfdd1&v=4 - url: https://github.com/TaigoFr -- login: Garrett-R - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/6614695?u=c128fd775002882f6e391bda5a89d1bdc5bdf45f&v=4 - url: https://github.com/Garrett-R -- login: EverStarck - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/51029456?u=343409b7cb6b3ea6a59359f4e8370d9c3f140ecd&v=4 - url: https://github.com/EverStarck -- login: henrymcl - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/26480299?v=4 - url: https://github.com/henrymcl -- login: jymchng - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/27895426?u=fb88c47775147d62a395fdb895d1af4148c7b566&v=4 - url: https://github.com/jymchng -- login: davidhuser - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/4357648?u=6ed702f8f6d49a8b2a0ed33cbd8ab59c2d7db7f7&v=4 - url: https://github.com/davidhuser -- login: PidgeyBE - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/19860056?u=47b584eb1c1ab45e31c1b474109a962d7e82be49&v=4 - url: https://github.com/PidgeyBE -- login: KianAnbarestani - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/145364424?u=dcc3d8fb4ca07d36fb52a17f38b6650565de40be&v=4 - url: https://github.com/KianAnbarestani -- login: jgould22 - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 - url: https://github.com/jgould22 -- login: marsboy02 - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/86903678?u=04cc319d6605f8d1ba3a0bed9f4f55a582719ae6&v=4 - url: https://github.com/marsboy02 -one_year_experts: -- login: YuriiMotov - count: 824 - avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=b9b13d598dddfab529a52d264df80a900bfe7060&v=4 - url: https://github.com/YuriiMotov -- login: luzzodev - count: 89 - avatarUrl: https://avatars.githubusercontent.com/u/27291415?u=5607ae1ce75c5f54f09500ca854227f7bfd2033b&v=4 - url: https://github.com/luzzodev -- login: Kludex - count: 50 - avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=df8a3f06ba8f55ae1967a3e2d5ed882903a4e330&v=4 - url: https://github.com/Kludex -- login: sinisaos - count: 33 - avatarUrl: https://avatars.githubusercontent.com/u/30960668?v=4 - url: https://github.com/sinisaos -- login: alv2017 - count: 26 - avatarUrl: https://avatars.githubusercontent.com/u/31544722?v=4 - url: https://github.com/alv2017 -- login: valentinDruzhinin - count: 24 - avatarUrl: https://avatars.githubusercontent.com/u/12831905?u=aae1ebc675c91e8fa582df4fcc4fc4128106344d&v=4 - url: https://github.com/valentinDruzhinin -- login: JavierSanchezCastro - count: 24 - avatarUrl: https://avatars.githubusercontent.com/u/72013291?u=ae5679e6bd971d9d98cd5e76e8683f83642ba950&v=4 - url: https://github.com/JavierSanchezCastro -- login: jgould22 - count: 17 - avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 - url: https://github.com/jgould22 -- login: tiangolo - count: 14 - avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=cb5d06e73a9e1998141b1641aa88e443c6717651&v=4 - url: https://github.com/tiangolo -- login: Kfir-G - count: 13 - avatarUrl: https://avatars.githubusercontent.com/u/57500876?u=a3bf923ab27bce3d1b13779a8dd22eb7675017fd&v=4 - url: https://github.com/Kfir-G -- login: sehraramiz - count: 11 - avatarUrl: https://avatars.githubusercontent.com/u/14166324?u=8fac65e84dfff24245d304a5b5b09f7b5bd69dc9&v=4 - url: https://github.com/sehraramiz -- login: sachinh35 - count: 9 - avatarUrl: https://avatars.githubusercontent.com/u/21972708?u=8560b97b8b41e175f476270b56de8a493b84f302&v=4 - url: https://github.com/sachinh35 -- login: yauhen-sobaleu - count: 9 - avatarUrl: https://avatars.githubusercontent.com/u/51629535?u=fc1817060daf2df438bfca86c44f33da5cd667db&v=4 - url: https://github.com/yauhen-sobaleu -- login: estebanx64 - count: 7 - avatarUrl: https://avatars.githubusercontent.com/u/10840422?u=1900887aeed268699e5ea6f3fb7db614f7b77cd3&v=4 - url: https://github.com/estebanx64 -- login: ceb10n - count: 7 - avatarUrl: https://avatars.githubusercontent.com/u/235213?u=edcce471814a1eba9f0cdaa4cd0de18921a940a6&v=4 - url: https://github.com/ceb10n -- login: yvallois - count: 7 - avatarUrl: https://avatars.githubusercontent.com/u/36999744?v=4 - url: https://github.com/yvallois -- login: raceychan - count: 6 - avatarUrl: https://avatars.githubusercontent.com/u/75417963?u=060c62870ec5a791765e63ac20d8885d11143786&v=4 - url: https://github.com/raceychan -- login: yinziyan1206 - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/37829370?u=da44ca53aefd5c23f346fab8e9fd2e108294c179&v=4 - url: https://github.com/yinziyan1206 -- login: DoctorJohn - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/14076775?u=2913e70a6142772847e91e2aaa5b9152391715e9&v=4 - url: https://github.com/DoctorJohn -- login: n8sty - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/2964996?v=4 - url: https://github.com/n8sty -- login: pythonweb2 - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/32141163?v=4 - url: https://github.com/pythonweb2 -- login: eqsdxr - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/157279130?u=58fddf77ed76966eaa8c73eea9bea4bb0c53b673&v=4 - url: https://github.com/eqsdxr -- login: yokwejuste - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/71908316?u=4ba43bd63c169b5c015137d8916752a44001445a&v=4 - url: https://github.com/yokwejuste -- login: WilliamDEdwards - count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/12184311?u=9b29d5d1d71f5f1a7ef9e439963ad3529e3b33a4&v=4 - url: https://github.com/WilliamDEdwards - login: mattmess1221 - count: 3 + count: 15 avatarUrl: https://avatars.githubusercontent.com/u/3409962?u=d22ea18aa8ea688af25a45df306134d593621a44&v=4 url: https://github.com/mattmess1221 -- login: Jelle-tenB - count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/210023470?u=c25d66addf36a747bd9fab773c4a6e7b238f45d4&v=4 - url: https://github.com/Jelle-tenB -- login: viniciusCalcantara - count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/108818737?u=80f3ec7427fa6a41d5896984d0c526432f2299fa&v=4 - url: https://github.com/viniciusCalcantara -- login: davidhuser - count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/4357648?u=6ed702f8f6d49a8b2a0ed33cbd8ab59c2d7db7f7&v=4 - url: https://github.com/davidhuser -- login: dbfreem - count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/9778569?u=f2f1e9135b5e4f1b0c6821a548b17f97572720fc&v=4 - url: https://github.com/dbfreem -- login: SobikXexe - count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/87701130?v=4 - url: https://github.com/SobikXexe -- login: pawelad - count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/7062874?u=d27dc220545a8401ad21840590a97d474d7101e6&v=4 - url: https://github.com/pawelad -- login: Isuxiz - count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/48672727?u=34d7b4ade252687d22a27cf53037b735b244bfc1&v=4 - url: https://github.com/Isuxiz -- login: Minibrams - count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/8108085?u=b028dbc308fa8485e0e2e9402b3d03d8deb22bf9&v=4 - url: https://github.com/Minibrams -- login: adsouza +last_month_experts: +- login: YuriiMotov + count: 20 + avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=bc48be95c429989224786106b027f3c5e40cc354&v=4 + url: https://github.com/YuriiMotov +- login: Toygarmetu + count: 8 + avatarUrl: https://avatars.githubusercontent.com/u/92878791?u=538530cb6d5554e71f9c28709d794db9a74d23d9&v=4 + url: https://github.com/Toygarmetu +- login: JavierSanchezCastro + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/72013291?u=ae5679e6bd971d9d98cd5e76e8683f83642ba950&v=4 + url: https://github.com/JavierSanchezCastro +- login: valentinDruzhinin count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/275832?u=f90f110cfafeafed2f14339e840941c2c328c186&v=4 - url: https://github.com/adsouza -- login: Synrom + avatarUrl: https://avatars.githubusercontent.com/u/12831905?u=aae1ebc675c91e8fa582df4fcc4fc4128106344d&v=4 + url: https://github.com/valentinDruzhinin +three_months_experts: +- login: YuriiMotov + count: 77 + avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=bc48be95c429989224786106b027f3c5e40cc354&v=4 + url: https://github.com/YuriiMotov +- login: tiangolo + count: 13 + avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=cb5d06e73a9e1998141b1641aa88e443c6717651&v=4 + url: https://github.com/tiangolo +- login: Toygarmetu + count: 8 + avatarUrl: https://avatars.githubusercontent.com/u/92878791?u=538530cb6d5554e71f9c28709d794db9a74d23d9&v=4 + url: https://github.com/Toygarmetu +- login: JavierSanchezCastro + count: 7 + avatarUrl: https://avatars.githubusercontent.com/u/72013291?u=ae5679e6bd971d9d98cd5e76e8683f83642ba950&v=4 + url: https://github.com/JavierSanchezCastro +- login: ceb10n + count: 5 + avatarUrl: https://avatars.githubusercontent.com/u/235213?u=edcce471814a1eba9f0cdaa4cd0de18921a940a6&v=4 + url: https://github.com/ceb10n +- login: valentinDruzhinin + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/12831905?u=aae1ebc675c91e8fa582df4fcc4fc4128106344d&v=4 + url: https://github.com/valentinDruzhinin +- login: RichieB2B + count: 3 + avatarUrl: https://avatars.githubusercontent.com/u/1461970?u=edaa57d1077705244ea5c9244f4783d94ff11f12&v=4 + url: https://github.com/RichieB2B +- login: sachinh35 + count: 3 + avatarUrl: https://avatars.githubusercontent.com/u/21972708?u=8560b97b8b41e175f476270b56de8a493b84f302&v=4 + url: https://github.com/sachinh35 +- login: EmmanuelNiyonshuti count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/30272537?v=4 - url: https://github.com/Synrom -- login: gaby + avatarUrl: https://avatars.githubusercontent.com/u/142030687?u=ab131d5ad4670280a978f489babe71c9bf9c1097&v=4 + url: https://github.com/EmmanuelNiyonshuti +- login: luzzodev count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/835733?u=8c72dec16fa560bdc81113354f2ffd79ad062bde&v=4 - url: https://github.com/gaby -- login: Ale-Cas + avatarUrl: https://avatars.githubusercontent.com/u/27291415?u=5607ae1ce75c5f54f09500ca854227f7bfd2033b&v=4 + url: https://github.com/luzzodev +- login: davidbrochart count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/64859146?u=d52a6ecf8d83d2927e2ae270bdfcc83495dba8c9&v=4 - url: https://github.com/Ale-Cas -- login: CharlesPerrotMinotHCHB + avatarUrl: https://avatars.githubusercontent.com/u/4711805?u=d39696d995a9e02ec3613ffb2f62b20b14f92f26&v=4 + url: https://github.com/davidbrochart +- login: CharlieReitzel count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/112571330?u=e3a666718ff5ad1d1c49d6c31358a9f80c841b30&v=4 - url: https://github.com/CharlesPerrotMinotHCHB -- login: yanggeorge + avatarUrl: https://avatars.githubusercontent.com/u/20848272?v=4 + url: https://github.com/CharlieReitzel +- login: dotmitsu count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/2434407?v=4 - url: https://github.com/yanggeorge -- login: Brikas - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/80290187?v=4 - url: https://github.com/Brikas + avatarUrl: https://avatars.githubusercontent.com/u/42657211?u=3bccc9a2f386a3f24230ec393080f8904fe2a5b2&v=4 + url: https://github.com/dotmitsu - login: dolfinus count: 2 avatarUrl: https://avatars.githubusercontent.com/u/4661021?u=ed5ddadcf36d9b943ebe61febe0b96ee34e5425d&v=4 url: https://github.com/dolfinus -- login: slafs +- login: garg-khushi count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/210173?v=4 - url: https://github.com/slafs -- login: purepani + avatarUrl: https://avatars.githubusercontent.com/u/139839680?u=7faffa70275f8ab16f163e0c742a11d2662f9c66&v=4 + url: https://github.com/garg-khushi +- login: florentx count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/7587353?v=4 - url: https://github.com/purepani -- login: ddahan + avatarUrl: https://avatars.githubusercontent.com/u/142113?u=bf10f10080026346b092633c380977b61cee0d9c&v=4 + url: https://github.com/florentx +- login: JunjieAraoXiong count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/1933516?u=1d200a620e8d6841df017e9f2bb7efb58b580f40&v=4 - url: https://github.com/ddahan -- login: TaigoFr + avatarUrl: https://avatars.githubusercontent.com/u/167785867?u=b69afe090c8bf5fd73f2d23fc3a887b28f68f192&v=4 + url: https://github.com/JunjieAraoXiong +six_months_experts: +- login: YuriiMotov + count: 150 + avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=bc48be95c429989224786106b027f3c5e40cc354&v=4 + url: https://github.com/YuriiMotov +- login: tiangolo + count: 24 + avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=cb5d06e73a9e1998141b1641aa88e443c6717651&v=4 + url: https://github.com/tiangolo +- login: luzzodev + count: 15 + avatarUrl: https://avatars.githubusercontent.com/u/27291415?u=5607ae1ce75c5f54f09500ca854227f7bfd2033b&v=4 + url: https://github.com/luzzodev +- login: engripaye + count: 14 + avatarUrl: https://avatars.githubusercontent.com/u/155247530?u=645169bc81856b7f1bd20090ecb0171a56dcbeb4&v=4 + url: https://github.com/engripaye +- login: JavierSanchezCastro + count: 12 + avatarUrl: https://avatars.githubusercontent.com/u/72013291?u=ae5679e6bd971d9d98cd5e76e8683f83642ba950&v=4 + url: https://github.com/JavierSanchezCastro +- login: Toygarmetu + count: 8 + avatarUrl: https://avatars.githubusercontent.com/u/92878791?u=538530cb6d5554e71f9c28709d794db9a74d23d9&v=4 + url: https://github.com/Toygarmetu +- login: valentinDruzhinin + count: 6 + avatarUrl: https://avatars.githubusercontent.com/u/12831905?u=aae1ebc675c91e8fa582df4fcc4fc4128106344d&v=4 + url: https://github.com/valentinDruzhinin +- login: ceb10n + count: 5 + avatarUrl: https://avatars.githubusercontent.com/u/235213?u=edcce471814a1eba9f0cdaa4cd0de18921a940a6&v=4 + url: https://github.com/ceb10n +- login: RichieB2B + count: 5 + avatarUrl: https://avatars.githubusercontent.com/u/1461970?u=edaa57d1077705244ea5c9244f4783d94ff11f12&v=4 + url: https://github.com/RichieB2B +- login: JunjieAraoXiong + count: 5 + avatarUrl: https://avatars.githubusercontent.com/u/167785867?u=b69afe090c8bf5fd73f2d23fc3a887b28f68f192&v=4 + url: https://github.com/JunjieAraoXiong +- login: CodeKraken-cmd + count: 5 + avatarUrl: https://avatars.githubusercontent.com/u/48470371?u=e7c0e7ec8e35ca5fb3ae40a586ed5e788fd0fe6d&v=4 + url: https://github.com/CodeKraken-cmd +- login: svlandeg + count: 5 + avatarUrl: https://avatars.githubusercontent.com/u/8796347?u=556c97650c27021911b0b9447ec55e75987b0e8a&v=4 + url: https://github.com/svlandeg +- login: ArmanShirzad + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/68951175?u=1f1efae2fa5d0d17c38a1a8413bedca5e538cedb&v=4 + url: https://github.com/ArmanShirzad +- login: krylosov-aa + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/242901957?u=4c9c7b468203b09bca64936fb464620e32cdd252&v=4 + url: https://github.com/krylosov-aa +- login: sachinh35 + count: 3 + avatarUrl: https://avatars.githubusercontent.com/u/21972708?u=8560b97b8b41e175f476270b56de8a493b84f302&v=4 + url: https://github.com/sachinh35 +- login: simone-trubian + count: 3 + avatarUrl: https://avatars.githubusercontent.com/u/5606840?u=65703af3c605feca61ce49e4009bb4e26495b425&v=4 + url: https://github.com/simone-trubian +- login: mahimairaja + count: 3 + avatarUrl: https://avatars.githubusercontent.com/u/81288263?u=4eef6b4a36b96e84bd666fc1937aa589036ccb9a&v=4 + url: https://github.com/mahimairaja +- login: pankeshpatel + count: 3 + avatarUrl: https://avatars.githubusercontent.com/u/1482917?u=666f39197a88cfa38b8bd78d39ef04d95c948b6b&v=4 + url: https://github.com/pankeshpatel +- login: huynguyengl99 count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/17792131?u=372b27056ec82f1ae03d8b3f37ef55b04a7cfdd1&v=4 - url: https://github.com/TaigoFr -- login: Garrett-R + avatarUrl: https://avatars.githubusercontent.com/u/49433085?u=7b626115686c5d97a2a32a03119f5300e425cc9f&v=4 + url: https://github.com/huynguyengl99 +- login: EmmanuelNiyonshuti count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/6614695?u=c128fd775002882f6e391bda5a89d1bdc5bdf45f&v=4 - url: https://github.com/Garrett-R -- login: jd-solanki + avatarUrl: https://avatars.githubusercontent.com/u/142030687?u=ab131d5ad4670280a978f489babe71c9bf9c1097&v=4 + url: https://github.com/EmmanuelNiyonshuti +- login: davidbrochart count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/47495003?u=6e225cb42c688d0cd70e65c6baedb9f5922b1178&v=4 - url: https://github.com/jd-solanki -- login: EverStarck + avatarUrl: https://avatars.githubusercontent.com/u/4711805?u=d39696d995a9e02ec3613ffb2f62b20b14f92f26&v=4 + url: https://github.com/davidbrochart +- login: CharlieReitzel count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/51029456?u=343409b7cb6b3ea6a59359f4e8370d9c3f140ecd&v=4 - url: https://github.com/EverStarck -- login: henrymcl + avatarUrl: https://avatars.githubusercontent.com/u/20848272?v=4 + url: https://github.com/CharlieReitzel +- login: dotmitsu count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/26480299?v=4 - url: https://github.com/henrymcl + avatarUrl: https://avatars.githubusercontent.com/u/42657211?u=3bccc9a2f386a3f24230ec393080f8904fe2a5b2&v=4 + url: https://github.com/dotmitsu +- login: dolfinus + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/4661021?u=ed5ddadcf36d9b943ebe61febe0b96ee34e5425d&v=4 + url: https://github.com/dolfinus +- login: Kludex + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=df8a3f06ba8f55ae1967a3e2d5ed882903a4e330&v=4 + url: https://github.com/Kludex +- login: garg-khushi + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/139839680?u=7faffa70275f8ab16f163e0c742a11d2662f9c66&v=4 + url: https://github.com/garg-khushi +- login: skion + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/532192?v=4 + url: https://github.com/skion +- login: florentx + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/142113?u=bf10f10080026346b092633c380977b61cee0d9c&v=4 + url: https://github.com/florentx +- login: jc-louis + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/51329768?v=4 + url: https://github.com/jc-louis +- login: WilliamDEdwards + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/12184311?u=9b29d5d1d71f5f1a7ef9e439963ad3529e3b33a4&v=4 + url: https://github.com/WilliamDEdwards +- login: bughuntr7 + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/236391583?u=7f51ff690e3a5711f845a115903c39e21c8af938&v=4 + url: https://github.com/bughuntr7 - login: jymchng count: 2 avatarUrl: https://avatars.githubusercontent.com/u/27895426?u=fb88c47775147d62a395fdb895d1af4148c7b566&v=4 url: https://github.com/jymchng -- login: christiansicari +- login: XieJiSS count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/29756552?v=4 - url: https://github.com/christiansicari -- login: JacobHayes + avatarUrl: https://avatars.githubusercontent.com/u/24671280?u=7ea0d9bfe46cf762594d62fd2f3c6d3813c3584c&v=4 + url: https://github.com/XieJiSS +- login: profatsky count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/2555532?u=354a525847a276bbb4426b0c95791a8ba5970f9b&v=4 - url: https://github.com/JacobHayes -- login: iloveitaly + avatarUrl: https://avatars.githubusercontent.com/u/92920843?u=81e54bb0b613c171f7cd0ab3cbb58873782c9c9c&v=4 + url: https://github.com/profatsky +one_year_experts: +- login: YuriiMotov + count: 906 + avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=bc48be95c429989224786106b027f3c5e40cc354&v=4 + url: https://github.com/YuriiMotov +- login: luzzodev + count: 62 + avatarUrl: https://avatars.githubusercontent.com/u/27291415?u=5607ae1ce75c5f54f09500ca854227f7bfd2033b&v=4 + url: https://github.com/luzzodev +- login: tiangolo + count: 30 + avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=cb5d06e73a9e1998141b1641aa88e443c6717651&v=4 + url: https://github.com/tiangolo +- login: valentinDruzhinin + count: 30 + avatarUrl: https://avatars.githubusercontent.com/u/12831905?u=aae1ebc675c91e8fa582df4fcc4fc4128106344d&v=4 + url: https://github.com/valentinDruzhinin +- login: alv2017 + count: 19 + avatarUrl: https://avatars.githubusercontent.com/u/31544722?v=4 + url: https://github.com/alv2017 +- login: JavierSanchezCastro + count: 18 + avatarUrl: https://avatars.githubusercontent.com/u/72013291?u=ae5679e6bd971d9d98cd5e76e8683f83642ba950&v=4 + url: https://github.com/JavierSanchezCastro +- login: engripaye + count: 14 + avatarUrl: https://avatars.githubusercontent.com/u/155247530?u=645169bc81856b7f1bd20090ecb0171a56dcbeb4&v=4 + url: https://github.com/engripaye +- login: sachinh35 + count: 12 + avatarUrl: https://avatars.githubusercontent.com/u/21972708?u=8560b97b8b41e175f476270b56de8a493b84f302&v=4 + url: https://github.com/sachinh35 +- login: yauhen-sobaleu + count: 9 + avatarUrl: https://avatars.githubusercontent.com/u/51629535?u=fc1817060daf2df438bfca86c44f33da5cd667db&v=4 + url: https://github.com/yauhen-sobaleu +- login: Toygarmetu + count: 8 + avatarUrl: https://avatars.githubusercontent.com/u/92878791?u=538530cb6d5554e71f9c28709d794db9a74d23d9&v=4 + url: https://github.com/Toygarmetu +- login: yinziyan1206 + count: 6 + avatarUrl: https://avatars.githubusercontent.com/u/37829370?u=da44ca53aefd5c23f346fab8e9fd2e108294c179&v=4 + url: https://github.com/yinziyan1206 +- login: Kludex + count: 6 + avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=df8a3f06ba8f55ae1967a3e2d5ed882903a4e330&v=4 + url: https://github.com/Kludex +- login: raceychan + count: 6 + avatarUrl: https://avatars.githubusercontent.com/u/75417963?u=060c62870ec5a791765e63ac20d8885d11143786&v=4 + url: https://github.com/raceychan +- login: ceb10n + count: 5 + avatarUrl: https://avatars.githubusercontent.com/u/235213?u=edcce471814a1eba9f0cdaa4cd0de18921a940a6&v=4 + url: https://github.com/ceb10n +- login: RichieB2B + count: 5 + avatarUrl: https://avatars.githubusercontent.com/u/1461970?u=edaa57d1077705244ea5c9244f4783d94ff11f12&v=4 + url: https://github.com/RichieB2B +- login: JunjieAraoXiong + count: 5 + avatarUrl: https://avatars.githubusercontent.com/u/167785867?u=b69afe090c8bf5fd73f2d23fc3a887b28f68f192&v=4 + url: https://github.com/JunjieAraoXiong +- login: CodeKraken-cmd + count: 5 + avatarUrl: https://avatars.githubusercontent.com/u/48470371?u=e7c0e7ec8e35ca5fb3ae40a586ed5e788fd0fe6d&v=4 + url: https://github.com/CodeKraken-cmd +- login: svlandeg + count: 5 + avatarUrl: https://avatars.githubusercontent.com/u/8796347?u=556c97650c27021911b0b9447ec55e75987b0e8a&v=4 + url: https://github.com/svlandeg +- login: DoctorJohn + count: 5 + avatarUrl: https://avatars.githubusercontent.com/u/14076775?u=ec43fe79a98dbc864b428afc7220753e25ca3af2&v=4 + url: https://github.com/DoctorJohn +- login: WilliamDEdwards + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/12184311?u=9b29d5d1d71f5f1a7ef9e439963ad3529e3b33a4&v=4 + url: https://github.com/WilliamDEdwards +- login: ArmanShirzad + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/68951175?u=1f1efae2fa5d0d17c38a1a8413bedca5e538cedb&v=4 + url: https://github.com/ArmanShirzad +- login: krylosov-aa + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/242901957?u=4c9c7b468203b09bca64936fb464620e32cdd252&v=4 + url: https://github.com/krylosov-aa +- login: isgin01 + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/157279130?u=16d6466476cf7dbc55a4cd575b6ea920ebdd81e1&v=4 + url: https://github.com/isgin01 +- login: sinisaos + count: 3 + avatarUrl: https://avatars.githubusercontent.com/u/30960668?v=4 + url: https://github.com/sinisaos +- login: dolfinus + count: 3 + avatarUrl: https://avatars.githubusercontent.com/u/4661021?u=ed5ddadcf36d9b943ebe61febe0b96ee34e5425d&v=4 + url: https://github.com/dolfinus +- login: jymchng + count: 3 + avatarUrl: https://avatars.githubusercontent.com/u/27895426?u=fb88c47775147d62a395fdb895d1af4148c7b566&v=4 + url: https://github.com/jymchng +- login: simone-trubian + count: 3 + avatarUrl: https://avatars.githubusercontent.com/u/5606840?u=65703af3c605feca61ce49e4009bb4e26495b425&v=4 + url: https://github.com/simone-trubian +- login: mahimairaja + count: 3 + avatarUrl: https://avatars.githubusercontent.com/u/81288263?u=4eef6b4a36b96e84bd666fc1937aa589036ccb9a&v=4 + url: https://github.com/mahimairaja +- login: pankeshpatel + count: 3 + avatarUrl: https://avatars.githubusercontent.com/u/1482917?u=666f39197a88cfa38b8bd78d39ef04d95c948b6b&v=4 + url: https://github.com/pankeshpatel +- login: Jelle-tenB + count: 3 + avatarUrl: https://avatars.githubusercontent.com/u/210023470?u=c25d66addf36a747bd9fab773c4a6e7b238f45d4&v=4 + url: https://github.com/Jelle-tenB +- login: jgould22 + count: 3 + avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 + url: https://github.com/jgould22 +- login: stan-dot count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/150855?v=4 - url: https://github.com/iloveitaly -- login: iiotsrc + avatarUrl: https://avatars.githubusercontent.com/u/56644812?u=a7dd773084f1c17c5f05019cc25a984e24873691&v=4 + url: https://github.com/stan-dot +- login: Damon0603 count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/131771119?u=bcaf2559ef6266af70b151b7fda31a1ee3dbecb3&v=4 - url: https://github.com/iiotsrc -- login: PidgeyBE + avatarUrl: https://avatars.githubusercontent.com/u/110039208?u=f24bf5c30317bc4959118d1b919587c473a865b6&v=4 + url: https://github.com/Damon0603 +- login: huynguyengl99 count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/19860056?u=47b584eb1c1ab45e31c1b474109a962d7e82be49&v=4 - url: https://github.com/PidgeyBE -- login: KianAnbarestani + avatarUrl: https://avatars.githubusercontent.com/u/49433085?u=7b626115686c5d97a2a32a03119f5300e425cc9f&v=4 + url: https://github.com/huynguyengl99 +- login: EmmanuelNiyonshuti count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/145364424?u=dcc3d8fb4ca07d36fb52a17f38b6650565de40be&v=4 - url: https://github.com/KianAnbarestani -- login: ykaiqx + avatarUrl: https://avatars.githubusercontent.com/u/142030687?u=ab131d5ad4670280a978f489babe71c9bf9c1097&v=4 + url: https://github.com/EmmanuelNiyonshuti +- login: Ale-Cas count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/56395004?u=1eebf5ce25a8067f7bfa6251a24f667be492d9d6&v=4 - url: https://github.com/ykaiqx -- login: AliYmn + avatarUrl: https://avatars.githubusercontent.com/u/64859146?u=d52a6ecf8d83d2927e2ae270bdfcc83495dba8c9&v=4 + url: https://github.com/Ale-Cas +- login: tiborrr count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/18416653?u=a77e2605e3ce6aaf6fef8ad4a7b0d32954fba47a&v=4 - url: https://github.com/AliYmn -- login: gelezo43 + avatarUrl: https://avatars.githubusercontent.com/u/16014746?u=0ce47015e53009e90393582fe86b7b90e809bc28&v=4 + url: https://github.com/tiborrr +- login: davidbrochart count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/40732698?u=611f39d3c1d2f4207a590937a78c1f10eed6232c&v=4 - url: https://github.com/gelezo43 -- login: jfeaver + avatarUrl: https://avatars.githubusercontent.com/u/4711805?u=d39696d995a9e02ec3613ffb2f62b20b14f92f26&v=4 + url: https://github.com/davidbrochart +- login: CharlieReitzel count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/1091338?u=0bcba366447d8fadad63f6705a52d128da4c7ec2&v=4 - url: https://github.com/jfeaver + avatarUrl: https://avatars.githubusercontent.com/u/20848272?v=4 + url: https://github.com/CharlieReitzel +- login: kiranzo + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/1070878?u=68f78a891c9751dd87571ac712a6309090c4bc01&v=4 + url: https://github.com/kiranzo +- login: dotmitsu + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/42657211?u=3bccc9a2f386a3f24230ec393080f8904fe2a5b2&v=4 + url: https://github.com/dotmitsu +- login: Brikas + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/80290187?u=2b72e497ca4444ecec1f9dc2d1b8d5437a27b83f&v=4 + url: https://github.com/Brikas +- login: BloodyRain2k + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/1014362?v=4 + url: https://github.com/BloodyRain2k +- login: usiqwerty + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/37992525?u=0c6e91d7b3887aa558755f4225ce74a003cbe852&v=4 + url: https://github.com/usiqwerty +- login: garg-khushi + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/139839680?u=7faffa70275f8ab16f163e0c742a11d2662f9c66&v=4 + url: https://github.com/garg-khushi +- login: sk- + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/911768?u=3bfaf87089eb03ef0fa378f316b9c783f431aa9b&v=4 + url: https://github.com/sk- +- login: skion + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/532192?v=4 + url: https://github.com/skion +- login: Danstiv + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/50794055?v=4 + url: https://github.com/Danstiv +- login: florentx + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/142113?u=bf10f10080026346b092633c380977b61cee0d9c&v=4 + url: https://github.com/florentx +- login: jc-louis + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/51329768?v=4 + url: https://github.com/jc-louis +- login: bughuntr7 + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/236391583?u=7f51ff690e3a5711f845a115903c39e21c8af938&v=4 + url: https://github.com/bughuntr7 +- login: purepani + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/7587353?v=4 + url: https://github.com/purepani +- login: asmaier + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/3169297?v=4 + url: https://github.com/asmaier +- login: henrymcl + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/26480299?v=4 + url: https://github.com/henrymcl +- login: potiuk + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/595491?v=4 + url: https://github.com/potiuk +- login: EverStarck + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/51029456?u=343409b7cb6b3ea6a59359f4e8370d9c3f140ecd&v=4 + url: https://github.com/EverStarck +- login: sanderbollen-clockworks + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/183479560?v=4 + url: https://github.com/sanderbollen-clockworks +- login: davidhuser + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/4357648?u=6ed702f8f6d49a8b2a0ed33cbd8ab59c2d7db7f7&v=4 + url: https://github.com/davidhuser +- login: XieJiSS + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/24671280?u=7ea0d9bfe46cf762594d62fd2f3c6d3813c3584c&v=4 + url: https://github.com/XieJiSS +- login: profatsky + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/92920843?u=81e54bb0b613c171f7cd0ab3cbb58873782c9c9c&v=4 + url: https://github.com/profatsky diff --git a/docs/en/docs/advanced/custom-response.md b/docs/en/docs/advanced/custom-response.md index 8b4b3da339..e88e958657 100644 --- a/docs/en/docs/advanced/custom-response.md +++ b/docs/en/docs/advanced/custom-response.md @@ -1,6 +1,6 @@ # Custom Response - HTML, Stream, File, others { #custom-response-html-stream-file-others } -By default, **FastAPI** will return the responses using `JSONResponse`. +By default, **FastAPI** will return JSON responses. You can override it by returning a `Response` directly as seen in [Return a Response directly](response-directly.md){.internal-link target=_blank}. @@ -10,43 +10,27 @@ But you can also declare the `Response` that you want to be used (e.g. any `Resp The contents that you return from your *path operation function* will be put inside of that `Response`. -And if that `Response` has a JSON media type (`application/json`), like is the case with the `JSONResponse` and `UJSONResponse`, the data you return will be automatically converted (and filtered) with any Pydantic `response_model` that you declared in the *path operation decorator*. - /// note If you use a response class with no media type, FastAPI will expect your response to have no content, so it will not document the response format in its generated OpenAPI docs. /// -## Use `ORJSONResponse` { #use-orjsonresponse } +## JSON Responses { #json-responses } -For example, if you are squeezing performance, you can install and use `orjson` and set the response to be `ORJSONResponse`. +By default FastAPI returns JSON responses. -Import the `Response` class (sub-class) you want to use and declare it in the *path operation decorator*. +If you declare a [Response Model](../tutorial/response-model.md){.internal-link target=_blank} FastAPI will use it to serialize the data to JSON, using Pydantic. -For large responses, returning a `Response` directly is much faster than returning a dictionary. +If you don't declare a response model, FastAPI will use the `jsonable_encoder` explained in [JSON Compatible Encoder](../tutorial/encoder.md){.internal-link target=_blank} and put it in a `JSONResponse`. -This is because by default, FastAPI will inspect every item inside and make sure it is serializable as JSON, using the same [JSON Compatible Encoder](../tutorial/encoder.md){.internal-link target=_blank} explained in the tutorial. This is what allows you to return **arbitrary objects**, for example database models. +If you declare a `response_class` with a JSON media type (`application/json`), like is the case with the `JSONResponse`, the data you return will be automatically converted (and filtered) with any Pydantic `response_model` that you declared in the *path operation decorator*. But the data won't be serialized to JSON bytes with Pydantic, instead it will be converted with the `jsonable_encoder` and then passed to the `JSONResponse` class, which will serialize it to bytes using the standard JSON library in Python. -But if you are certain that the content that you are returning is **serializable with JSON**, you can pass it directly to the response class and avoid the extra overhead that FastAPI would have by passing your return content through the `jsonable_encoder` before passing it to the response class. +### JSON Performance { #json-performance } -{* ../../docs_src/custom_response/tutorial001b_py310.py hl[2,7] *} +In short, if you want the maximum performance, use a [Response Model](../tutorial/response-model.md){.internal-link target=_blank} and don't declare a `response_class` in the *path operation decorator*. -/// info - -The parameter `response_class` will also be used to define the "media type" of the response. - -In this case, the HTTP header `Content-Type` will be set to `application/json`. - -And it will be documented as such in OpenAPI. - -/// - -/// tip - -The `ORJSONResponse` is only available in FastAPI, not in Starlette. - -/// +{* ../../docs_src/response_model/tutorial001_01_py310.py ln[15:17] hl[16] *} ## HTML Response { #html-response } @@ -154,40 +138,6 @@ Takes some data and returns an `application/json` encoded response. This is the default response used in **FastAPI**, as you read above. -### `ORJSONResponse` { #orjsonresponse } - -A fast alternative JSON response using `orjson`, as you read above. - -/// info - -This requires installing `orjson` for example with `pip install orjson`. - -/// - -### `UJSONResponse` { #ujsonresponse } - -An alternative JSON response using `ujson`. - -/// info - -This requires installing `ujson` for example with `pip install ujson`. - -/// - -/// warning - -`ujson` is less careful than Python's built-in implementation in how it handles some edge-cases. - -/// - -{* ../../docs_src/custom_response/tutorial001_py310.py hl[2,7] *} - -/// tip - -It's possible that `ORJSONResponse` might be a faster alternative. - -/// - ### `RedirectResponse` { #redirectresponse } Returns an HTTP redirect. Uses a 307 status code (Temporary Redirect) by default. @@ -268,7 +218,7 @@ In this case, you can return the file path directly from your *path operation* f You can create your own custom response class, inheriting from `Response` and using it. -For example, let's say that you want to use `orjson`, but with some custom settings not used in the included `ORJSONResponse` class. +For example, let's say that you want to use `orjson` with some settings. Let's say you want it to return indented and formatted JSON, so you want to use the orjson option `orjson.OPT_INDENT_2`. @@ -292,13 +242,21 @@ Now instead of returning: Of course, you will probably find much better ways to take advantage of this than formatting JSON. 😉 +### `orjson` or Response Model { #orjson-or-response-model } + +If what you are looking for is performance, you are probably better off using a [Response Model](../tutorial/response-model.md){.internal-link target=_blank} than an `orjson` response. + +With a response model, FastAPI will use Pydantic to serialize the data to JSON, without using intermediate steps, like converting it with `jsonable_encoder`, which would happen in any other case. + +And under the hood, Pydantic uses the same underlying Rust mechanisms as `orjson` to serialize to JSON, so you will already get the best performance with a response model. + ## Default response class { #default-response-class } When creating a **FastAPI** class instance or an `APIRouter` you can specify which response class to use by default. The parameter that defines this is `default_response_class`. -In the example below, **FastAPI** will use `ORJSONResponse` by default, in all *path operations*, instead of `JSONResponse`. +In the example below, **FastAPI** will use `HTMLResponse` by default, in all *path operations*, instead of JSON. {* ../../docs_src/custom_response/tutorial010_py310.py hl[2,4] *} diff --git a/docs/en/docs/advanced/json-base64-bytes.md b/docs/en/docs/advanced/json-base64-bytes.md new file mode 100644 index 0000000000..c0dfec72b9 --- /dev/null +++ b/docs/en/docs/advanced/json-base64-bytes.md @@ -0,0 +1,63 @@ +# JSON with Bytes as Base64 { #json-with-bytes-as-base64 } + +If your app needs to receive and send JSON data, but you need to include binary data in it, you can encode it as base64. + +## Base64 vs Files { #base64-vs-files } + +Consider first if you can use [Request Files](../tutorial/request-files.md){.internal-link target=_blank} for uploading binary data and [Custom Response - FileResponse](./custom-response.md#fileresponse--fileresponse-){.internal-link target=_blank} for sending binary data, instead of encoding it in JSON. + +JSON can only contain UTF-8 encoded strings, so it can't contain raw bytes. + +Base64 can encode binary data in strings, but to do it, it needs to use more characters than the original binary data, so it would normally be less efficient than regular files. + +Use base64 only if you definitely need to include binary data in JSON, and you can't use files for that. + +## Pydantic `bytes` { #pydantic-bytes } + +You can declare a Pydantic model with `bytes` fields, and then use `val_json_bytes` in the model config to tell it to use base64 to *validate* input JSON data, as part of that validation it will decode the base64 string into bytes. + +{* ../../docs_src/json_base64_bytes/tutorial001_py310.py ln[1:9,29:35] hl[9] *} + +If you check the `/docs`, they will show that the field `data` expects base64 encoded bytes: + +
+ +
+ +You could send a request like: + +```json +{ + "description": "Some data", + "data": "aGVsbG8=" +} +``` + +/// tip + +`aGVsbG8=` is the base64 encoding of `hello`. + +/// + +And then Pydantic will decode the base64 string and give you the original bytes in the `data` field of the model. + +You will receive a response like: + +```json +{ + "description": "Some data", + "content": "hello" +} +``` + +## Pydantic `bytes` for Output Data { #pydantic-bytes-for-output-data } + +You can also use `bytes` fields with `ser_json_bytes` in the model config for output data, and Pydantic will *serialize* the bytes as base64 when generating the JSON response. + +{* ../../docs_src/json_base64_bytes/tutorial001_py310.py ln[1:2,12:16,29,38:41] hl[16] *} + +## Pydantic `bytes` for Input and Output Data { #pydantic-bytes-for-input-and-output-data } + +And of course, you can use the same model configured to use base64 to handle both input (*validate*) with `val_json_bytes` and output (*serialize*) with `ser_json_bytes` when receiving and sending JSON data. + +{* ../../docs_src/json_base64_bytes/tutorial001_py310.py ln[1:2,19:26,29,44:46] hl[23:26] *} diff --git a/docs/en/docs/advanced/response-directly.md b/docs/en/docs/advanced/response-directly.md index 76cc50d03c..9d58490eb1 100644 --- a/docs/en/docs/advanced/response-directly.md +++ b/docs/en/docs/advanced/response-directly.md @@ -2,19 +2,23 @@ When you create a **FastAPI** *path operation* you can normally return any data from it: a `dict`, a `list`, a Pydantic model, a database model, etc. -By default, **FastAPI** would automatically convert that return value to JSON using the `jsonable_encoder` explained in [JSON Compatible Encoder](../tutorial/encoder.md){.internal-link target=_blank}. +If you declare a [Response Model](../tutorial/response-model.md){.internal-link target=_blank} FastAPI will use it to serialize the data to JSON, using Pydantic. -Then, behind the scenes, it would put that JSON-compatible data (e.g. a `dict`) inside of a `JSONResponse` that would be used to send the response to the client. +If you don't declare a response model, FastAPI will use the `jsonable_encoder` explained in [JSON Compatible Encoder](../tutorial/encoder.md){.internal-link target=_blank} and put it in a `JSONResponse`. -But you can return a `JSONResponse` directly from your *path operations*. +You could also create a `JSONResponse` directly and return it. -It might be useful, for example, to return custom headers or cookies. +/// tip + +You will normally have much better performance using a [Response Model](../tutorial/response-model.md){.internal-link target=_blank} than returning a `JSONResponse` directly, as that way it serializes the data using Pydantic, in Rust. + +/// ## Return a `Response` { #return-a-response } -In fact, you can return any `Response` or any sub-class of it. +You can return any `Response` or any sub-class of it. -/// tip +/// info `JSONResponse` itself is a sub-class of `Response`. @@ -56,6 +60,18 @@ You could put your XML content in a string, put that in a `Response`, and return {* ../../docs_src/response_directly/tutorial002_py310.py hl[1,18] *} +## How a Response Model Works { #how-a-response-model-works } + +When you declare a [Response Model](../tutorial/response-model.md){.internal-link target=_blank} in a path operation, **FastAPI** will use it to serialize the data to JSON, using Pydantic. + +{* ../../docs_src/response_model/tutorial001_01_py310.py hl[16,21] *} + +As that will happen on the Rust side, the performance will be much better than if it was done with regular Python and the `JSONResponse` class. + +When using a response model FastAPI won't use the `jsonable_encoder` to convert the data (which would be slower) nor the `JSONResponse` class. + +Instead it takes the JSON bytes generated with Pydantic using the response model and returns a `Response` with the right media type for JSON directly (`application/json`). + ## Notes { #notes } When you return a `Response` directly its data is not validated, converted (serialized), or documented automatically. diff --git a/docs/en/docs/advanced/strict-content-type.md b/docs/en/docs/advanced/strict-content-type.md new file mode 100644 index 0000000000..54c099410c --- /dev/null +++ b/docs/en/docs/advanced/strict-content-type.md @@ -0,0 +1,88 @@ +# Strict Content-Type Checking { #strict-content-type-checking } + +By default, **FastAPI** uses strict `Content-Type` header checking for JSON request bodies, this means that JSON requests **must** include a valid `Content-Type` header (e.g. `application/json`) in order for the body to be parsed as JSON. + +## CSRF Risk { #csrf-risk } + +This default behavior provides protection against a class of **Cross-Site Request Forgery (CSRF)** attacks in a very specific scenario. + +These attacks exploit the fact that browsers allow scripts to send requests without doing any CORS preflight check when they: + +* don't have a `Content-Type` header (e.g. using `fetch()` with a `Blob` body) +* and don't send any authentication credentials. + +This type of attack is mainly relevant when: + +* the application is running locally (e.g. on `localhost`) or in an internal network +* and the application doesn't have any authentication, it expects that any request from the same network can be trusted. + +## Example Attack { #example-attack } + +Imagine you build a way to run a local AI agent. + +It provides an API at + +``` +http://localhost:8000/v1/agents/multivac +``` + +There's also a frontend at + +``` +http://localhost:8000 +``` + +/// tip + +Note that both have the same host. + +/// + +Then using the frontend you can make the AI agent do things on your behalf. + +As it's running **locally**, and not in the open internet, you decide to **not have any authentication** set up, just trusting the access to the local network. + +Then one of your users could install it and run it locally. + +Then they could open a malicious website, e.g. something like + +``` +https://evilhackers.example.com +``` + +And that malicious website sends requests using `fetch()` with a `Blob` body to the local API at + +``` +http://localhost:8000/v1/agents/multivac +``` + +Even though the host of the malicious website and the local app is different, the browser won't trigger a CORS preflight request because: + +* It's running without any authentication, it doesn't have to send any credentials. +* The browser thinks it's not sending JSON (because of the missing `Content-Type` header). + +Then the malicious website could make the local AI agent send angry messages to the user's ex-boss... or worse. 😅 + +## Open Internet { #open-internet } + +If your app is in the open internet, you wouldn't "trust the network" and let anyone send privileged requests without authentication. + +Attackers could simply run a script to send requests to your API, no need for browser interaction, so you are probably already securing any privileged endpoints. + +In that case **this attack / risk doesn't apply to you**. + +This risk and attack is mainly relevant when the app runs on the **local network** and that is the **only assumed protection**. + +## Allowing Requests Without Content-Type { #allowing-requests-without-content-type } + +If you need to support clients that don't send a `Content-Type` header, you can disable strict checking by setting `strict_content_type=False`: + +{* ../../docs_src/strict_content_type/tutorial001_py310.py hl[4] *} + +With this setting, requests without a `Content-Type` header will have their body parsed as JSON, which is the same behavior as older versions of FastAPI. + +/// info + +This behavior and configuration was added in FastAPI 0.132.0. + +/// diff --git a/docs/en/docs/css/custom.css b/docs/en/docs/css/custom.css index 7c50dbd9be..dc9c7d63b4 100644 --- a/docs/en/docs/css/custom.css +++ b/docs/en/docs/css/custom.css @@ -61,6 +61,10 @@ a.internal-link::after { padding-bottom: 2em; } +.md-footer-meta .md-social { + padding-right: 4rem; +} + .user-list { display: flex; flex-wrap: wrap; diff --git a/docs/en/docs/how-to/general.md b/docs/en/docs/how-to/general.md index 9347192607..4f611dab05 100644 --- a/docs/en/docs/how-to/general.md +++ b/docs/en/docs/how-to/general.md @@ -6,6 +6,10 @@ Here are several pointers to other places in the docs, for general or frequent q To ensure that you don't return more data than you should, read the docs for [Tutorial - Response Model - Return Type](../tutorial/response-model.md){.internal-link target=_blank}. +## Optimize Response Performance - Response Model - Return Type { #optimize-response-performance-response-model-return-type } + +To optimize performance when returning JSON data, use a return type or response model, that way Pydantic will handle the serialization to JSON on the Rust side, without going through Python. Read more in the docs for [Tutorial - Response Model - Return Type](../tutorial/response-model.md){.internal-link target=_blank}. + ## Documentation Tags - OpenAPI { #documentation-tags-openapi } To add tags to your *path operations*, and group them in the docs UI, read the docs for [Tutorial - Path Operation Configurations - Tags](../tutorial/path-operation-configuration.md#tags){.internal-link target=_blank}. diff --git a/docs/en/docs/img/tutorial/json-base64-bytes/image01.png b/docs/en/docs/img/tutorial/json-base64-bytes/image01.png new file mode 100644 index 0000000000..996732b749 Binary files /dev/null and b/docs/en/docs/img/tutorial/json-base64-bytes/image01.png differ diff --git a/docs/en/docs/js/init_kapa_widget.js b/docs/en/docs/js/init_kapa_widget.js new file mode 100644 index 0000000000..eaf123bf3f --- /dev/null +++ b/docs/en/docs/js/init_kapa_widget.js @@ -0,0 +1,29 @@ +document.addEventListener("DOMContentLoaded", function () { + var script = document.createElement("script"); + script.src = "https://widget.kapa.ai/kapa-widget.bundle.js"; + script.setAttribute("data-website-id", "91f47f27-b405-4299-bf5f-a1c0ec07b3cc"); + script.setAttribute("data-project-name", "FastAPI"); + script.setAttribute("data-project-color", "#009485"); + script.setAttribute("data-project-logo", "https://fastapi.tiangolo.com/img/favicon.png"); + script.setAttribute("data-bot-protection-mechanism", "hcaptcha"); + script.setAttribute("data-button-height", "3rem"); + script.setAttribute("data-button-width", "3rem"); + script.setAttribute("data-button-border-radius", "50%"); + script.setAttribute("data-button-padding", "0"); + script.setAttribute("data-button-image", "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 8V4H8'/%3E%3Crect width='16' height='12' x='4' y='8' rx='2'/%3E%3Cpath d='M2 14h2'/%3E%3Cpath d='M20 14h2'/%3E%3Cpath d='M15 13v2'/%3E%3Cpath d='M9 13v2'/%3E%3C/svg%3E"); + script.setAttribute("data-button-image-height", "20px"); + script.setAttribute("data-button-image-width", "20px"); + script.setAttribute("data-button-text", "Ask AI"); + script.setAttribute("data-button-text-font-size", "0.5rem"); + script.setAttribute("data-button-text-font-family", "Roboto, sans-serif"); + script.setAttribute("data-button-text-color", "#FFFFFF"); + script.setAttribute("data-modal-border-radius", "0.5rem"); + script.setAttribute("data-modal-header-bg-color", "#009485"); + script.setAttribute("data-modal-title", "FastAPI AI Assistant"); + script.setAttribute("data-modal-title-color", "#FFFFFF"); + script.setAttribute("data-modal-title-font-family", "Roboto, sans-serif"); + script.setAttribute("data-modal-example-questions", "How to define a route?,How to validate models?,How to handle responses?,How to deploy FastAPI?"); + script.setAttribute("data-modal-disclaimer", "AI-generated answers based on FastAPI [documentation](https://fastapi.tiangolo.com/) and [community discussions](https://github.com/fastapi/fastapi/discussions). Always verify important information."); + script.async = true; + document.head.appendChild(script); +}); diff --git a/docs/en/docs/reference/responses.md b/docs/en/docs/reference/responses.md index bd57861294..2df53e9701 100644 --- a/docs/en/docs/reference/responses.md +++ b/docs/en/docs/reference/responses.md @@ -22,7 +22,13 @@ from fastapi.responses import ( ## FastAPI Responses -There are a couple of custom FastAPI response classes, you can use them to optimize JSON performance. +There were a couple of custom FastAPI response classes that were intended to optimize JSON performance. + +However, they are now deprecated as you will now get better performance by using a [Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/). + +That way, Pydantic will serialize the data into JSON bytes on the Rust side, which will achieve better performance than these custom JSON responses. + +Read more about it in [Custom Response - HTML, Stream, File, others - `orjson` or Response Model](https://fastapi.tiangolo.com/advanced/custom-response/#orjson-or-response-model). ::: fastapi.responses.UJSONResponse options: diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 5e1435065c..f56d34b5e7 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,12 +7,66 @@ hide: ## Latest Changes +### Internal + +* 👥 Update FastAPI People - Experts. PR [#14972](https://github.com/fastapi/fastapi/pull/14972) by [@tiangolo](https://github.com/tiangolo). +* 👷 Allow skipping `benchmark` job in `test` workflow. PR [#14974](https://github.com/fastapi/fastapi/pull/14974) by [@YuriiMotov](https://github.com/YuriiMotov). + +## 0.132.0 + +### Breaking Changes + +* 🔒️ Add `strict_content_type` checking for JSON requests. PR [#14978](https://github.com/fastapi/fastapi/pull/14978) by [@tiangolo](https://github.com/tiangolo). + * Now FastAPI checks, by default, that JSON requests have a `Content-Type` header with a valid JSON value, like `application/json`, and rejects requests that don't. + * If the clients for your app don't send a valid `Content-Type` header you can disable this with `strict_content_type=False`. + * Check the new docs: [Strict Content-Type Checking](https://fastapi.tiangolo.com/advanced/strict-content-type/). + +### Internal + +* ⬆ Bump flask from 3.1.2 to 3.1.3. PR [#14949](https://github.com/fastapi/fastapi/pull/14949) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Update all dependencies to use `griffelib` instead of `griffe`. PR [#14973](https://github.com/fastapi/fastapi/pull/14973) by [@svlandeg](https://github.com/svlandeg). +* 🔨 Fix `FastAPI People` workflow. PR [#14951](https://github.com/fastapi/fastapi/pull/14951) by [@YuriiMotov](https://github.com/YuriiMotov). +* 👷 Do not run codspeed with coverage as it's not tracked. PR [#14966](https://github.com/fastapi/fastapi/pull/14966) by [@tiangolo](https://github.com/tiangolo). +* 👷 Do not include benchmark tests in coverage to speed up coverage processing. PR [#14965](https://github.com/fastapi/fastapi/pull/14965) by [@tiangolo](https://github.com/tiangolo). + +## 0.131.0 + +### Breaking Changes + +* 🗑️ Deprecate `ORJSONResponse` and `UJSONResponse`. PR [#14964](https://github.com/fastapi/fastapi/pull/14964) by [@tiangolo](https://github.com/tiangolo). + +## 0.130.0 + +### Features + +* ✨ Serialize JSON response with Pydantic (in Rust), when there's a Pydantic return type or response model. PR [#14962](https://github.com/fastapi/fastapi/pull/14962) by [@tiangolo](https://github.com/tiangolo). + * This results in 2x (or more) performance increase for JSON responses. + * New docs: [Custom Response - JSON Performance](https://fastapi.tiangolo.com/advanced/custom-response/#json-performance). + +## 0.129.2 + +### Internal + +* ⬆️ Upgrade pytest. PR [#14959](https://github.com/fastapi/fastapi/pull/14959) by [@tiangolo](https://github.com/tiangolo). +* 👷 Fix CI, do not attempt to publish `fastapi-slim`. PR [#14958](https://github.com/fastapi/fastapi/pull/14958) by [@tiangolo](https://github.com/tiangolo). +* ➖ Drop support for `fastapi-slim`, no more versions will be released, use only `"fastapi[standard]"` or `fastapi`. PR [#14957](https://github.com/fastapi/fastapi/pull/14957) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Update pyproject.toml, remove unneeded lines. PR [#14956](https://github.com/fastapi/fastapi/pull/14956) by [@tiangolo](https://github.com/tiangolo). + +## 0.129.1 + +### Fixes + +* ♻️ Fix JSON Schema for bytes, use `"contentMediaType": "application/octet-stream"` instead of `"format": "binary"`. PR [#14953](https://github.com/fastapi/fastapi/pull/14953) by [@tiangolo](https://github.com/tiangolo). + ### Docs +* 🔨 Add Kapa.ai widget (AI chatbot). PR [#14938](https://github.com/fastapi/fastapi/pull/14938) by [@tiangolo](https://github.com/tiangolo). +* 🔥 Remove Python 3.9 specific files, no longer needed after updating translations. PR [#14931](https://github.com/fastapi/fastapi/pull/14931) by [@tiangolo](https://github.com/tiangolo). * 📝 Update docs for JWT to prevent timing attacks. PR [#14908](https://github.com/fastapi/fastapi/pull/14908) by [@tiangolo](https://github.com/tiangolo). ### Translations +* ✏️ Fix several typos in ru translations. PR [#14934](https://github.com/fastapi/fastapi/pull/14934) by [@argoarsiks](https://github.com/argoarsiks). * 🌐 Update translations for ko (update-all and add-missing). PR [#14923](https://github.com/fastapi/fastapi/pull/14923) by [@YuriiMotov](https://github.com/YuriiMotov). * 🌐 Update translations for uk (add-missing). PR [#14922](https://github.com/fastapi/fastapi/pull/14922) by [@YuriiMotov](https://github.com/YuriiMotov). * 🌐 Update translations for zh-hant (update-all and add-missing). PR [#14921](https://github.com/fastapi/fastapi/pull/14921) by [@YuriiMotov](https://github.com/YuriiMotov). @@ -28,6 +82,8 @@ hide: ### Internal +* 👷 Always run tests on push to `master` branch and when run by scheduler. PR [#14940](https://github.com/fastapi/fastapi/pull/14940) by [@YuriiMotov](https://github.com/YuriiMotov). +* 🎨 Upgrade typing syntax for Python 3.10. PR [#14932](https://github.com/fastapi/fastapi/pull/14932) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump cryptography from 46.0.4 to 46.0.5. PR [#14892](https://github.com/fastapi/fastapi/pull/14892) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump pillow from 12.1.0 to 12.1.1. PR [#14899](https://github.com/fastapi/fastapi/pull/14899) by [@dependabot[bot]](https://github.com/apps/dependabot). diff --git a/docs/en/docs/tutorial/response-model.md b/docs/en/docs/tutorial/response-model.md index 51492722ae..c8312d92c6 100644 --- a/docs/en/docs/tutorial/response-model.md +++ b/docs/en/docs/tutorial/response-model.md @@ -13,6 +13,7 @@ FastAPI will use this return type to: * Add a **JSON Schema** for the response, in the OpenAPI *path operation*. * This will be used by the **automatic docs**. * It will also be used by automatic client code generation tools. +* **Serialize** the returned data to JSON using Pydantic, which is written in **Rust**, so it will be **much faster**. But most importantly: diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index 66ad67e9d0..e86e7b9c41 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -192,6 +192,8 @@ nav: - advanced/wsgi.md - advanced/generate-clients.md - advanced/advanced-python-types.md + - advanced/json-base64-bytes.md + - advanced/strict-content-type.md - fastapi-cli.md - Deployment: - deployment/index.md @@ -342,5 +344,6 @@ extra_css: extra_javascript: - js/termynal.js - js/custom.js +- js/init_kapa_widget.js hooks: - ../../scripts/mkdocs_hooks.py diff --git a/docs/ru/docs/advanced/middleware.md b/docs/ru/docs/advanced/middleware.md index 034feae7eb..1f1a160604 100644 --- a/docs/ru/docs/advanced/middleware.md +++ b/docs/ru/docs/advanced/middleware.md @@ -83,7 +83,7 @@ app.add_middleware(UnicornMiddleware, some_config="rainbow") Поддерживаются следующие аргументы: - `minimum_size` — не сжимать GZip‑ом ответы, размер которых меньше этого минимального значения в байтах. По умолчанию — `500`. -- `compresslevel` — уровень GZip‑сжатия. Целое число от 1 до 9. По умолчанию — `9`. Более низкое значение — быстреее сжатие, но больший размер файла; более высокое значение — более медленное сжатие, но меньший размер файла. +- `compresslevel` — уровень GZip‑сжатия. Целое число от 1 до 9. По умолчанию — `9`. Более низкое значение — быстрее сжатие, но больший размер файла; более высокое значение — более медленное сжатие, но меньший размер файла. ## Другие middleware { #other-middlewares } diff --git a/docs/ru/docs/deployment/docker.md b/docs/ru/docs/deployment/docker.md index 791057fe56..5dfa211599 100644 --- a/docs/ru/docs/deployment/docker.md +++ b/docs/ru/docs/deployment/docker.md @@ -214,7 +214,7 @@ CMD ["fastapi", "run", "app/main.py", "--port", "80"] 5. Копируем директорию `./app` внутрь директории `/code`. - Так как здесь весь код, который **меняется чаще всего**, кэш Docker **вряд ли** будет использоваться для этого шагa или **последующих шагов**. + Так как здесь весь код, который **меняется чаще всего**, кэш Docker **вряд ли** будет использоваться для этого шага или **последующих шагов**. Поэтому важно разместить этот шаг **ближе к концу** `Dockerfile`, чтобы оптимизировать время сборки образа контейнера. diff --git a/docs/ru/docs/history-design-future.md b/docs/ru/docs/history-design-future.md index e2395fe8b9..5019157600 100644 --- a/docs/ru/docs/history-design-future.md +++ b/docs/ru/docs/history-design-future.md @@ -76,4 +76,4 @@ У **FastAPI** великое будущее. -И [ваш вклад в это](help-fastapi.md){.internal-link target=_blank} - очень ценнен. +И [ваш вклад в это](help-fastapi.md){.internal-link target=_blank} - очень ценен. diff --git a/docs/ru/docs/tutorial/security/oauth2-jwt.md b/docs/ru/docs/tutorial/security/oauth2-jwt.md index 7838b07df4..f7853d48f7 100644 --- a/docs/ru/docs/tutorial/security/oauth2-jwt.md +++ b/docs/ru/docs/tutorial/security/oauth2-jwt.md @@ -20,7 +20,7 @@ eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4 Но он подписан. Следовательно, когда вы получаете токен, который вы эмитировали (выдавали), вы можете убедиться, что это именно вы его эмитировали. -Таким образом, можно создать токен со сроком действия, скажем, 1 неделя. А когда пользователь вернется на следующий день с тем же токеном, вы будете знать, что он все еще авторизирован в вашей системе. +Таким образом, можно создать токен со сроком действия, скажем, 1 неделя. А когда пользователь вернется на следующий день с тем же токеном, вы будете знать, что он все еще авторизован в вашей системе. Через неделю срок действия токена истечет, пользователь не будет авторизован и ему придется заново входить в систему, чтобы получить новый токен. А если пользователь (или третье лицо) попытается модифицировать токен, чтобы изменить срок действия, вы сможете это обнаружить, поскольку подписи не будут совпадать. diff --git a/docs_src/additional_responses/tutorial001_py39.py b/docs_src/additional_responses/tutorial001_py39.py deleted file mode 100644 index ffa821b910..0000000000 --- a/docs_src/additional_responses/tutorial001_py39.py +++ /dev/null @@ -1,22 +0,0 @@ -from fastapi import FastAPI -from fastapi.responses import JSONResponse -from pydantic import BaseModel - - -class Item(BaseModel): - id: str - value: str - - -class Message(BaseModel): - message: str - - -app = FastAPI() - - -@app.get("/items/{item_id}", response_model=Item, responses={404: {"model": Message}}) -async def read_item(item_id: str): - if item_id == "foo": - return {"id": "foo", "value": "there goes my hero"} - return JSONResponse(status_code=404, content={"message": "Item not found"}) diff --git a/docs_src/additional_responses/tutorial003_py39.py b/docs_src/additional_responses/tutorial003_py39.py deleted file mode 100644 index f3e41e8d26..0000000000 --- a/docs_src/additional_responses/tutorial003_py39.py +++ /dev/null @@ -1,37 +0,0 @@ -from fastapi import FastAPI -from fastapi.responses import JSONResponse -from pydantic import BaseModel - - -class Item(BaseModel): - id: str - value: str - - -class Message(BaseModel): - message: str - - -app = FastAPI() - - -@app.get( - "/items/{item_id}", - response_model=Item, - responses={ - 404: {"model": Message, "description": "The item was not found"}, - 200: { - "description": "Item requested by ID", - "content": { - "application/json": { - "example": {"id": "bar", "value": "The bar tenders"} - } - }, - }, - }, -) -async def read_item(item_id: str): - if item_id == "foo": - return {"id": "foo", "value": "there goes my hero"} - else: - return JSONResponse(status_code=404, content={"message": "Item not found"}) diff --git a/docs_src/advanced_middleware/tutorial001_py39.py b/docs_src/advanced_middleware/tutorial001_py39.py deleted file mode 100644 index 35dbd30461..0000000000 --- a/docs_src/advanced_middleware/tutorial001_py39.py +++ /dev/null @@ -1,11 +0,0 @@ -from fastapi import FastAPI -from fastapi.middleware.httpsredirect import HTTPSRedirectMiddleware - -app = FastAPI() - -app.add_middleware(HTTPSRedirectMiddleware) - - -@app.get("/") -async def main(): - return {"message": "Hello World"} diff --git a/docs_src/advanced_middleware/tutorial002_py39.py b/docs_src/advanced_middleware/tutorial002_py39.py deleted file mode 100644 index 405235ab9b..0000000000 --- a/docs_src/advanced_middleware/tutorial002_py39.py +++ /dev/null @@ -1,13 +0,0 @@ -from fastapi import FastAPI -from fastapi.middleware.trustedhost import TrustedHostMiddleware - -app = FastAPI() - -app.add_middleware( - TrustedHostMiddleware, allowed_hosts=["example.com", "*.example.com"] -) - - -@app.get("/") -async def main(): - return {"message": "Hello World"} diff --git a/docs_src/advanced_middleware/tutorial003_py39.py b/docs_src/advanced_middleware/tutorial003_py39.py deleted file mode 100644 index e2c87e67d8..0000000000 --- a/docs_src/advanced_middleware/tutorial003_py39.py +++ /dev/null @@ -1,11 +0,0 @@ -from fastapi import FastAPI -from fastapi.middleware.gzip import GZipMiddleware - -app = FastAPI() - -app.add_middleware(GZipMiddleware, minimum_size=1000, compresslevel=5) - - -@app.get("/") -async def main(): - return "somebigcontent" diff --git a/docs_src/app_testing/app_a_py39/main.py b/docs_src/app_testing/app_a_py39/main.py deleted file mode 100644 index 4679aec9cc..0000000000 --- a/docs_src/app_testing/app_a_py39/main.py +++ /dev/null @@ -1,8 +0,0 @@ -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -async def read_main(): - return {"msg": "Hello World"} diff --git a/docs_src/app_testing/app_a_py39/test_main.py b/docs_src/app_testing/app_a_py39/test_main.py deleted file mode 100644 index ddc013f40c..0000000000 --- a/docs_src/app_testing/app_a_py39/test_main.py +++ /dev/null @@ -1,11 +0,0 @@ -from fastapi.testclient import TestClient - -from .main import app - -client = TestClient(app) - - -def test_read_main(): - response = client.get("/") - assert response.status_code == 200 - assert response.json() == {"msg": "Hello World"} diff --git a/docs_src/app_testing/tutorial001_py39.py b/docs_src/app_testing/tutorial001_py39.py deleted file mode 100644 index 79a853b487..0000000000 --- a/docs_src/app_testing/tutorial001_py39.py +++ /dev/null @@ -1,18 +0,0 @@ -from fastapi import FastAPI -from fastapi.testclient import TestClient - -app = FastAPI() - - -@app.get("/") -async def read_main(): - return {"msg": "Hello World"} - - -client = TestClient(app) - - -def test_read_main(): - response = client.get("/") - assert response.status_code == 200 - assert response.json() == {"msg": "Hello World"} diff --git a/docs_src/app_testing/tutorial002_py39.py b/docs_src/app_testing/tutorial002_py39.py deleted file mode 100644 index 71c898b3cf..0000000000 --- a/docs_src/app_testing/tutorial002_py39.py +++ /dev/null @@ -1,31 +0,0 @@ -from fastapi import FastAPI -from fastapi.testclient import TestClient -from fastapi.websockets import WebSocket - -app = FastAPI() - - -@app.get("/") -async def read_main(): - return {"msg": "Hello World"} - - -@app.websocket("/ws") -async def websocket(websocket: WebSocket): - await websocket.accept() - await websocket.send_json({"msg": "Hello WebSocket"}) - await websocket.close() - - -def test_read_main(): - client = TestClient(app) - response = client.get("/") - assert response.status_code == 200 - assert response.json() == {"msg": "Hello World"} - - -def test_websocket(): - client = TestClient(app) - with client.websocket_connect("/ws") as websocket: - data = websocket.receive_json() - assert data == {"msg": "Hello WebSocket"} diff --git a/docs_src/app_testing/tutorial003_py39.py b/docs_src/app_testing/tutorial003_py39.py deleted file mode 100644 index ca6b45ce03..0000000000 --- a/docs_src/app_testing/tutorial003_py39.py +++ /dev/null @@ -1,24 +0,0 @@ -from fastapi import FastAPI -from fastapi.testclient import TestClient - -app = FastAPI() - -items = {} - - -@app.on_event("startup") -async def startup_event(): - items["foo"] = {"name": "Fighters"} - items["bar"] = {"name": "Tenders"} - - -@app.get("/items/{item_id}") -async def read_items(item_id: str): - return items[item_id] - - -def test_read_items(): - with TestClient(app) as client: - response = client.get("/items/foo") - assert response.status_code == 200 - assert response.json() == {"name": "Fighters"} diff --git a/docs_src/app_testing/tutorial004_py39.py b/docs_src/app_testing/tutorial004_py39.py deleted file mode 100644 index f83ac9ae9a..0000000000 --- a/docs_src/app_testing/tutorial004_py39.py +++ /dev/null @@ -1,43 +0,0 @@ -from contextlib import asynccontextmanager - -from fastapi import FastAPI -from fastapi.testclient import TestClient - -items = {} - - -@asynccontextmanager -async def lifespan(app: FastAPI): - items["foo"] = {"name": "Fighters"} - items["bar"] = {"name": "Tenders"} - yield - # clean up items - items.clear() - - -app = FastAPI(lifespan=lifespan) - - -@app.get("/items/{item_id}") -async def read_items(item_id: str): - return items[item_id] - - -def test_read_items(): - # Before the lifespan starts, "items" is still empty - assert items == {} - - with TestClient(app) as client: - # Inside the "with TestClient" block, the lifespan starts and items added - assert items == {"foo": {"name": "Fighters"}, "bar": {"name": "Tenders"}} - - response = client.get("/items/foo") - assert response.status_code == 200 - assert response.json() == {"name": "Fighters"} - - # After the requests is done, the items are still there - assert items == {"foo": {"name": "Fighters"}, "bar": {"name": "Tenders"}} - - # The end of the "with TestClient" block simulates terminating the app, so - # the lifespan ends and items are cleaned up - assert items == {} diff --git a/docs_src/async_tests/app_a_py39/main.py b/docs_src/async_tests/app_a_py39/main.py deleted file mode 100644 index 9594f859c7..0000000000 --- a/docs_src/async_tests/app_a_py39/main.py +++ /dev/null @@ -1,8 +0,0 @@ -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -async def root(): - return {"message": "Tomato"} diff --git a/docs_src/async_tests/app_a_py39/test_main.py b/docs_src/async_tests/app_a_py39/test_main.py deleted file mode 100644 index a57a31f7d8..0000000000 --- a/docs_src/async_tests/app_a_py39/test_main.py +++ /dev/null @@ -1,14 +0,0 @@ -import pytest -from httpx import ASGITransport, AsyncClient - -from .main import app - - -@pytest.mark.anyio -async def test_root(): - async with AsyncClient( - transport=ASGITransport(app=app), base_url="http://test" - ) as ac: - response = await ac.get("/") - assert response.status_code == 200 - assert response.json() == {"message": "Tomato"} diff --git a/docs_src/authentication_error_status_code/tutorial001_an_py39.py b/docs_src/authentication_error_status_code/tutorial001_an_py39.py deleted file mode 100644 index 7bbc2f717d..0000000000 --- a/docs_src/authentication_error_status_code/tutorial001_an_py39.py +++ /dev/null @@ -1,21 +0,0 @@ -from typing import Annotated - -from fastapi import Depends, FastAPI, HTTPException, status -from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer - -app = FastAPI() - - -class HTTPBearer403(HTTPBearer): - def make_not_authenticated_error(self) -> HTTPException: - return HTTPException( - status_code=status.HTTP_403_FORBIDDEN, detail="Not authenticated" - ) - - -CredentialsDep = Annotated[HTTPAuthorizationCredentials, Depends(HTTPBearer403())] - - -@app.get("/me") -def read_me(credentials: CredentialsDep): - return {"message": "You are authenticated", "token": credentials.credentials} diff --git a/docs_src/background_tasks/tutorial001_py39.py b/docs_src/background_tasks/tutorial001_py39.py deleted file mode 100644 index 1720a74333..0000000000 --- a/docs_src/background_tasks/tutorial001_py39.py +++ /dev/null @@ -1,15 +0,0 @@ -from fastapi import BackgroundTasks, FastAPI - -app = FastAPI() - - -def write_notification(email: str, message=""): - with open("log.txt", mode="w") as email_file: - content = f"notification for {email}: {message}" - email_file.write(content) - - -@app.post("/send-notification/{email}") -async def send_notification(email: str, background_tasks: BackgroundTasks): - background_tasks.add_task(write_notification, email, message="some notification") - return {"message": "Notification sent in the background"} diff --git a/docs_src/behind_a_proxy/tutorial001_01_py39.py b/docs_src/behind_a_proxy/tutorial001_01_py39.py deleted file mode 100644 index 52b114395b..0000000000 --- a/docs_src/behind_a_proxy/tutorial001_01_py39.py +++ /dev/null @@ -1,8 +0,0 @@ -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/items/") -def read_items(): - return ["plumbus", "portal gun"] diff --git a/docs_src/behind_a_proxy/tutorial001_py39.py b/docs_src/behind_a_proxy/tutorial001_py39.py deleted file mode 100644 index ede59ada11..0000000000 --- a/docs_src/behind_a_proxy/tutorial001_py39.py +++ /dev/null @@ -1,8 +0,0 @@ -from fastapi import FastAPI, Request - -app = FastAPI() - - -@app.get("/app") -def read_main(request: Request): - return {"message": "Hello World", "root_path": request.scope.get("root_path")} diff --git a/docs_src/behind_a_proxy/tutorial002_py39.py b/docs_src/behind_a_proxy/tutorial002_py39.py deleted file mode 100644 index c1600cde9e..0000000000 --- a/docs_src/behind_a_proxy/tutorial002_py39.py +++ /dev/null @@ -1,8 +0,0 @@ -from fastapi import FastAPI, Request - -app = FastAPI(root_path="/api/v1") - - -@app.get("/app") -def read_main(request: Request): - return {"message": "Hello World", "root_path": request.scope.get("root_path")} diff --git a/docs_src/behind_a_proxy/tutorial003_py39.py b/docs_src/behind_a_proxy/tutorial003_py39.py deleted file mode 100644 index 3b7d8be018..0000000000 --- a/docs_src/behind_a_proxy/tutorial003_py39.py +++ /dev/null @@ -1,14 +0,0 @@ -from fastapi import FastAPI, Request - -app = FastAPI( - servers=[ - {"url": "https://stag.example.com", "description": "Staging environment"}, - {"url": "https://prod.example.com", "description": "Production environment"}, - ], - root_path="/api/v1", -) - - -@app.get("/app") -def read_main(request: Request): - return {"message": "Hello World", "root_path": request.scope.get("root_path")} diff --git a/docs_src/behind_a_proxy/tutorial004_py39.py b/docs_src/behind_a_proxy/tutorial004_py39.py deleted file mode 100644 index 51bd5babc1..0000000000 --- a/docs_src/behind_a_proxy/tutorial004_py39.py +++ /dev/null @@ -1,15 +0,0 @@ -from fastapi import FastAPI, Request - -app = FastAPI( - servers=[ - {"url": "https://stag.example.com", "description": "Staging environment"}, - {"url": "https://prod.example.com", "description": "Production environment"}, - ], - root_path="/api/v1", - root_path_in_servers=False, -) - - -@app.get("/app") -def read_main(request: Request): - return {"message": "Hello World", "root_path": request.scope.get("root_path")} diff --git a/docs_src/bigger_applications/app_an_py39/dependencies.py b/docs_src/bigger_applications/app_an_py39/dependencies.py deleted file mode 100644 index 5c7612aa09..0000000000 --- a/docs_src/bigger_applications/app_an_py39/dependencies.py +++ /dev/null @@ -1,13 +0,0 @@ -from typing import Annotated - -from fastapi import Header, HTTPException - - -async def get_token_header(x_token: Annotated[str, Header()]): - if x_token != "fake-super-secret-token": - raise HTTPException(status_code=400, detail="X-Token header invalid") - - -async def get_query_token(token: str): - if token != "jessica": - raise HTTPException(status_code=400, detail="No Jessica token provided") diff --git a/docs_src/bigger_applications/app_an_py39/internal/admin.py b/docs_src/bigger_applications/app_an_py39/internal/admin.py deleted file mode 100644 index 99d3da86b9..0000000000 --- a/docs_src/bigger_applications/app_an_py39/internal/admin.py +++ /dev/null @@ -1,8 +0,0 @@ -from fastapi import APIRouter - -router = APIRouter() - - -@router.post("/") -async def update_admin(): - return {"message": "Admin getting schwifty"} diff --git a/docs_src/bigger_applications/app_an_py39/main.py b/docs_src/bigger_applications/app_an_py39/main.py deleted file mode 100644 index ae544a3aac..0000000000 --- a/docs_src/bigger_applications/app_an_py39/main.py +++ /dev/null @@ -1,23 +0,0 @@ -from fastapi import Depends, FastAPI - -from .dependencies import get_query_token, get_token_header -from .internal import admin -from .routers import items, users - -app = FastAPI(dependencies=[Depends(get_query_token)]) - - -app.include_router(users.router) -app.include_router(items.router) -app.include_router( - admin.router, - prefix="/admin", - tags=["admin"], - dependencies=[Depends(get_token_header)], - responses={418: {"description": "I'm a teapot"}}, -) - - -@app.get("/") -async def root(): - return {"message": "Hello Bigger Applications!"} diff --git a/docs_src/bigger_applications/app_an_py39/routers/__init__.py b/docs_src/bigger_applications/app_an_py39/routers/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/docs_src/bigger_applications/app_an_py39/routers/items.py b/docs_src/bigger_applications/app_an_py39/routers/items.py deleted file mode 100644 index bde9ff4d55..0000000000 --- a/docs_src/bigger_applications/app_an_py39/routers/items.py +++ /dev/null @@ -1,38 +0,0 @@ -from fastapi import APIRouter, Depends, HTTPException - -from ..dependencies import get_token_header - -router = APIRouter( - prefix="/items", - tags=["items"], - dependencies=[Depends(get_token_header)], - responses={404: {"description": "Not found"}}, -) - - -fake_items_db = {"plumbus": {"name": "Plumbus"}, "gun": {"name": "Portal Gun"}} - - -@router.get("/") -async def read_items(): - return fake_items_db - - -@router.get("/{item_id}") -async def read_item(item_id: str): - if item_id not in fake_items_db: - raise HTTPException(status_code=404, detail="Item not found") - return {"name": fake_items_db[item_id]["name"], "item_id": item_id} - - -@router.put( - "/{item_id}", - tags=["custom"], - responses={403: {"description": "Operation forbidden"}}, -) -async def update_item(item_id: str): - if item_id != "plumbus": - raise HTTPException( - status_code=403, detail="You can only update the item: plumbus" - ) - return {"item_id": item_id, "name": "The great Plumbus"} diff --git a/docs_src/bigger_applications/app_an_py39/routers/users.py b/docs_src/bigger_applications/app_an_py39/routers/users.py deleted file mode 100644 index 39b3d7e7cf..0000000000 --- a/docs_src/bigger_applications/app_an_py39/routers/users.py +++ /dev/null @@ -1,18 +0,0 @@ -from fastapi import APIRouter - -router = APIRouter() - - -@router.get("/users/", tags=["users"]) -async def read_users(): - return [{"username": "Rick"}, {"username": "Morty"}] - - -@router.get("/users/me", tags=["users"]) -async def read_user_me(): - return {"username": "fakecurrentuser"} - - -@router.get("/users/{username}", tags=["users"]) -async def read_user(username: str): - return {"username": username} diff --git a/docs_src/bigger_applications/app_py39/__init__.py b/docs_src/bigger_applications/app_py39/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/docs_src/bigger_applications/app_py39/dependencies.py b/docs_src/bigger_applications/app_py39/dependencies.py deleted file mode 100644 index 8e45f004b4..0000000000 --- a/docs_src/bigger_applications/app_py39/dependencies.py +++ /dev/null @@ -1,11 +0,0 @@ -from fastapi import Header, HTTPException - - -async def get_token_header(x_token: str = Header()): - if x_token != "fake-super-secret-token": - raise HTTPException(status_code=400, detail="X-Token header invalid") - - -async def get_query_token(token: str): - if token != "jessica": - raise HTTPException(status_code=400, detail="No Jessica token provided") diff --git a/docs_src/bigger_applications/app_py39/main.py b/docs_src/bigger_applications/app_py39/main.py deleted file mode 100644 index ae544a3aac..0000000000 --- a/docs_src/bigger_applications/app_py39/main.py +++ /dev/null @@ -1,23 +0,0 @@ -from fastapi import Depends, FastAPI - -from .dependencies import get_query_token, get_token_header -from .internal import admin -from .routers import items, users - -app = FastAPI(dependencies=[Depends(get_query_token)]) - - -app.include_router(users.router) -app.include_router(items.router) -app.include_router( - admin.router, - prefix="/admin", - tags=["admin"], - dependencies=[Depends(get_token_header)], - responses={418: {"description": "I'm a teapot"}}, -) - - -@app.get("/") -async def root(): - return {"message": "Hello Bigger Applications!"} diff --git a/docs_src/body_nested_models/tutorial008_py39.py b/docs_src/body_nested_models/tutorial008_py39.py deleted file mode 100644 index 854a7a5a46..0000000000 --- a/docs_src/body_nested_models/tutorial008_py39.py +++ /dev/null @@ -1,14 +0,0 @@ -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 deleted file mode 100644 index 59c1e50828..0000000000 --- a/docs_src/body_nested_models/tutorial009_py39.py +++ /dev/null @@ -1,8 +0,0 @@ -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/conditional_openapi/tutorial001_py39.py b/docs_src/conditional_openapi/tutorial001_py39.py deleted file mode 100644 index eedb0d2742..0000000000 --- a/docs_src/conditional_openapi/tutorial001_py39.py +++ /dev/null @@ -1,16 +0,0 @@ -from fastapi import FastAPI -from pydantic_settings import BaseSettings - - -class Settings(BaseSettings): - openapi_url: str = "/openapi.json" - - -settings = Settings() - -app = FastAPI(openapi_url=settings.openapi_url) - - -@app.get("/") -def root(): - return {"message": "Hello World"} diff --git a/docs_src/configure_swagger_ui/tutorial001_py39.py b/docs_src/configure_swagger_ui/tutorial001_py39.py deleted file mode 100644 index 6c24ce7583..0000000000 --- a/docs_src/configure_swagger_ui/tutorial001_py39.py +++ /dev/null @@ -1,8 +0,0 @@ -from fastapi import FastAPI - -app = FastAPI(swagger_ui_parameters={"syntaxHighlight": False}) - - -@app.get("/users/{username}") -async def read_user(username: str): - return {"message": f"Hello {username}"} diff --git a/docs_src/configure_swagger_ui/tutorial002_py39.py b/docs_src/configure_swagger_ui/tutorial002_py39.py deleted file mode 100644 index cc75c21968..0000000000 --- a/docs_src/configure_swagger_ui/tutorial002_py39.py +++ /dev/null @@ -1,8 +0,0 @@ -from fastapi import FastAPI - -app = FastAPI(swagger_ui_parameters={"syntaxHighlight": {"theme": "obsidian"}}) - - -@app.get("/users/{username}") -async def read_user(username: str): - return {"message": f"Hello {username}"} diff --git a/docs_src/configure_swagger_ui/tutorial003_py39.py b/docs_src/configure_swagger_ui/tutorial003_py39.py deleted file mode 100644 index b4449f5c6a..0000000000 --- a/docs_src/configure_swagger_ui/tutorial003_py39.py +++ /dev/null @@ -1,8 +0,0 @@ -from fastapi import FastAPI - -app = FastAPI(swagger_ui_parameters={"deepLinking": False}) - - -@app.get("/users/{username}") -async def read_user(username: str): - return {"message": f"Hello {username}"} diff --git a/docs_src/cors/tutorial001_py39.py b/docs_src/cors/tutorial001_py39.py deleted file mode 100644 index d59ab27acc..0000000000 --- a/docs_src/cors/tutorial001_py39.py +++ /dev/null @@ -1,24 +0,0 @@ -from fastapi import FastAPI -from fastapi.middleware.cors import CORSMiddleware - -app = FastAPI() - -origins = [ - "http://localhost.tiangolo.com", - "https://localhost.tiangolo.com", - "http://localhost", - "http://localhost:8080", -] - -app.add_middleware( - CORSMiddleware, - allow_origins=origins, - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - - -@app.get("/") -async def main(): - return {"message": "Hello World"} diff --git a/docs_src/custom_docs_ui/tutorial001_py39.py b/docs_src/custom_docs_ui/tutorial001_py39.py deleted file mode 100644 index 1cfcce19aa..0000000000 --- a/docs_src/custom_docs_ui/tutorial001_py39.py +++ /dev/null @@ -1,38 +0,0 @@ -from fastapi import FastAPI -from fastapi.openapi.docs import ( - get_redoc_html, - get_swagger_ui_html, - get_swagger_ui_oauth2_redirect_html, -) - -app = FastAPI(docs_url=None, redoc_url=None) - - -@app.get("/docs", include_in_schema=False) -async def custom_swagger_ui_html(): - return get_swagger_ui_html( - openapi_url=app.openapi_url, - title=app.title + " - Swagger UI", - oauth2_redirect_url=app.swagger_ui_oauth2_redirect_url, - swagger_js_url="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js", - swagger_css_url="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css", - ) - - -@app.get(app.swagger_ui_oauth2_redirect_url, include_in_schema=False) -async def swagger_ui_redirect(): - return get_swagger_ui_oauth2_redirect_html() - - -@app.get("/redoc", include_in_schema=False) -async def redoc_html(): - return get_redoc_html( - openapi_url=app.openapi_url, - title=app.title + " - ReDoc", - redoc_js_url="https://unpkg.com/redoc@2/bundles/redoc.standalone.js", - ) - - -@app.get("/users/{username}") -async def read_user(username: str): - return {"message": f"Hello {username}"} diff --git a/docs_src/custom_docs_ui/tutorial002_py39.py b/docs_src/custom_docs_ui/tutorial002_py39.py deleted file mode 100644 index 23ea368f8b..0000000000 --- a/docs_src/custom_docs_ui/tutorial002_py39.py +++ /dev/null @@ -1,41 +0,0 @@ -from fastapi import FastAPI -from fastapi.openapi.docs import ( - get_redoc_html, - get_swagger_ui_html, - get_swagger_ui_oauth2_redirect_html, -) -from fastapi.staticfiles import StaticFiles - -app = FastAPI(docs_url=None, redoc_url=None) - -app.mount("/static", StaticFiles(directory="static"), name="static") - - -@app.get("/docs", include_in_schema=False) -async def custom_swagger_ui_html(): - return get_swagger_ui_html( - openapi_url=app.openapi_url, - title=app.title + " - Swagger UI", - oauth2_redirect_url=app.swagger_ui_oauth2_redirect_url, - swagger_js_url="/static/swagger-ui-bundle.js", - swagger_css_url="/static/swagger-ui.css", - ) - - -@app.get(app.swagger_ui_oauth2_redirect_url, include_in_schema=False) -async def swagger_ui_redirect(): - return get_swagger_ui_oauth2_redirect_html() - - -@app.get("/redoc", include_in_schema=False) -async def redoc_html(): - return get_redoc_html( - openapi_url=app.openapi_url, - title=app.title + " - ReDoc", - redoc_js_url="/static/redoc.standalone.js", - ) - - -@app.get("/users/{username}") -async def read_user(username: str): - return {"message": f"Hello {username}"} diff --git a/docs_src/custom_response/tutorial001_py39.py b/docs_src/custom_response/tutorial001_py39.py deleted file mode 100644 index 0f09bdf77f..0000000000 --- a/docs_src/custom_response/tutorial001_py39.py +++ /dev/null @@ -1,9 +0,0 @@ -from fastapi import FastAPI -from fastapi.responses import UJSONResponse - -app = FastAPI() - - -@app.get("/items/", response_class=UJSONResponse) -async def read_items(): - return [{"item_id": "Foo"}] diff --git a/docs_src/custom_response/tutorial001b_py39.py b/docs_src/custom_response/tutorial001b_py39.py deleted file mode 100644 index 95e6ca7637..0000000000 --- a/docs_src/custom_response/tutorial001b_py39.py +++ /dev/null @@ -1,9 +0,0 @@ -from fastapi import FastAPI -from fastapi.responses import ORJSONResponse - -app = FastAPI() - - -@app.get("/items/", response_class=ORJSONResponse) -async def read_items(): - return ORJSONResponse([{"item_id": "Foo"}]) diff --git a/docs_src/custom_response/tutorial002_py39.py b/docs_src/custom_response/tutorial002_py39.py deleted file mode 100644 index 23c495867b..0000000000 --- a/docs_src/custom_response/tutorial002_py39.py +++ /dev/null @@ -1,18 +0,0 @@ -from fastapi import FastAPI -from fastapi.responses import HTMLResponse - -app = FastAPI() - - -@app.get("/items/", response_class=HTMLResponse) -async def read_items(): - return """ - - - Some HTML in here - - -

Look ma! HTML!

- - - """ diff --git a/docs_src/custom_response/tutorial003_py39.py b/docs_src/custom_response/tutorial003_py39.py deleted file mode 100644 index 51ad3c146b..0000000000 --- a/docs_src/custom_response/tutorial003_py39.py +++ /dev/null @@ -1,19 +0,0 @@ -from fastapi import FastAPI -from fastapi.responses import HTMLResponse - -app = FastAPI() - - -@app.get("/items/") -async def read_items(): - html_content = """ - - - Some HTML in here - - -

Look ma! HTML!

- - - """ - return HTMLResponse(content=html_content, status_code=200) diff --git a/docs_src/custom_response/tutorial004_py39.py b/docs_src/custom_response/tutorial004_py39.py deleted file mode 100644 index 0e90f20126..0000000000 --- a/docs_src/custom_response/tutorial004_py39.py +++ /dev/null @@ -1,23 +0,0 @@ -from fastapi import FastAPI -from fastapi.responses import HTMLResponse - -app = FastAPI() - - -def generate_html_response(): - html_content = """ - - - Some HTML in here - - -

Look ma! HTML!

- - - """ - return HTMLResponse(content=html_content, status_code=200) - - -@app.get("/items/", response_class=HTMLResponse) -async def read_items(): - return generate_html_response() diff --git a/docs_src/custom_response/tutorial005_py39.py b/docs_src/custom_response/tutorial005_py39.py deleted file mode 100644 index 3d58f57fb6..0000000000 --- a/docs_src/custom_response/tutorial005_py39.py +++ /dev/null @@ -1,9 +0,0 @@ -from fastapi import FastAPI -from fastapi.responses import PlainTextResponse - -app = FastAPI() - - -@app.get("/", response_class=PlainTextResponse) -async def main(): - return "Hello World" diff --git a/docs_src/custom_response/tutorial006_py39.py b/docs_src/custom_response/tutorial006_py39.py deleted file mode 100644 index 332f8f87f1..0000000000 --- a/docs_src/custom_response/tutorial006_py39.py +++ /dev/null @@ -1,9 +0,0 @@ -from fastapi import FastAPI -from fastapi.responses import RedirectResponse - -app = FastAPI() - - -@app.get("/typer") -async def redirect_typer(): - return RedirectResponse("https://typer.tiangolo.com") diff --git a/docs_src/custom_response/tutorial006b_py39.py b/docs_src/custom_response/tutorial006b_py39.py deleted file mode 100644 index 03a7be3995..0000000000 --- a/docs_src/custom_response/tutorial006b_py39.py +++ /dev/null @@ -1,9 +0,0 @@ -from fastapi import FastAPI -from fastapi.responses import RedirectResponse - -app = FastAPI() - - -@app.get("/fastapi", response_class=RedirectResponse) -async def redirect_fastapi(): - return "https://fastapi.tiangolo.com" diff --git a/docs_src/custom_response/tutorial006c_py39.py b/docs_src/custom_response/tutorial006c_py39.py deleted file mode 100644 index 87c720364b..0000000000 --- a/docs_src/custom_response/tutorial006c_py39.py +++ /dev/null @@ -1,9 +0,0 @@ -from fastapi import FastAPI -from fastapi.responses import RedirectResponse - -app = FastAPI() - - -@app.get("/pydantic", response_class=RedirectResponse, status_code=302) -async def redirect_pydantic(): - return "https://docs.pydantic.dev/" diff --git a/docs_src/custom_response/tutorial007_py39.py b/docs_src/custom_response/tutorial007_py39.py deleted file mode 100644 index e2a53a2119..0000000000 --- a/docs_src/custom_response/tutorial007_py39.py +++ /dev/null @@ -1,14 +0,0 @@ -from fastapi import FastAPI -from fastapi.responses import StreamingResponse - -app = FastAPI() - - -async def fake_video_streamer(): - for i in range(10): - yield b"some fake video bytes" - - -@app.get("/") -async def main(): - return StreamingResponse(fake_video_streamer()) diff --git a/docs_src/custom_response/tutorial008_py39.py b/docs_src/custom_response/tutorial008_py39.py deleted file mode 100644 index fc071cbee4..0000000000 --- a/docs_src/custom_response/tutorial008_py39.py +++ /dev/null @@ -1,14 +0,0 @@ -from fastapi import FastAPI -from fastapi.responses import StreamingResponse - -some_file_path = "large-video-file.mp4" -app = FastAPI() - - -@app.get("/") -def main(): - def iterfile(): # (1) - with open(some_file_path, mode="rb") as file_like: # (2) - yield from file_like # (3) - - return StreamingResponse(iterfile(), media_type="video/mp4") diff --git a/docs_src/custom_response/tutorial009_py39.py b/docs_src/custom_response/tutorial009_py39.py deleted file mode 100644 index 71cf50cc18..0000000000 --- a/docs_src/custom_response/tutorial009_py39.py +++ /dev/null @@ -1,10 +0,0 @@ -from fastapi import FastAPI -from fastapi.responses import FileResponse - -some_file_path = "large-video-file.mp4" -app = FastAPI() - - -@app.get("/") -async def main(): - return FileResponse(some_file_path) diff --git a/docs_src/custom_response/tutorial009b_py39.py b/docs_src/custom_response/tutorial009b_py39.py deleted file mode 100644 index 27200ee4ba..0000000000 --- a/docs_src/custom_response/tutorial009b_py39.py +++ /dev/null @@ -1,10 +0,0 @@ -from fastapi import FastAPI -from fastapi.responses import FileResponse - -some_file_path = "large-video-file.mp4" -app = FastAPI() - - -@app.get("/", response_class=FileResponse) -async def main(): - return some_file_path diff --git a/docs_src/custom_response/tutorial009c_py39.py b/docs_src/custom_response/tutorial009c_py39.py deleted file mode 100644 index de6b6688e6..0000000000 --- a/docs_src/custom_response/tutorial009c_py39.py +++ /dev/null @@ -1,19 +0,0 @@ -from typing import Any - -import orjson -from fastapi import FastAPI, Response - -app = FastAPI() - - -class CustomORJSONResponse(Response): - media_type = "application/json" - - def render(self, content: Any) -> bytes: - assert orjson is not None, "orjson must be installed" - return orjson.dumps(content, option=orjson.OPT_INDENT_2) - - -@app.get("/", response_class=CustomORJSONResponse) -async def main(): - return {"message": "Hello World"} diff --git a/docs_src/custom_response/tutorial010_py310.py b/docs_src/custom_response/tutorial010_py310.py index 57cb062604..d5bc783aa0 100644 --- a/docs_src/custom_response/tutorial010_py310.py +++ b/docs_src/custom_response/tutorial010_py310.py @@ -1,9 +1,9 @@ from fastapi import FastAPI -from fastapi.responses import ORJSONResponse +from fastapi.responses import HTMLResponse -app = FastAPI(default_response_class=ORJSONResponse) +app = FastAPI(default_response_class=HTMLResponse) @app.get("/items/") async def read_items(): - return [{"item_id": "Foo"}] + return "

Items

This is a list of items.

" diff --git a/docs_src/custom_response/tutorial010_py39.py b/docs_src/custom_response/tutorial010_py39.py deleted file mode 100644 index 57cb062604..0000000000 --- a/docs_src/custom_response/tutorial010_py39.py +++ /dev/null @@ -1,9 +0,0 @@ -from fastapi import FastAPI -from fastapi.responses import ORJSONResponse - -app = FastAPI(default_response_class=ORJSONResponse) - - -@app.get("/items/") -async def read_items(): - return [{"item_id": "Foo"}] diff --git a/docs_src/debugging/tutorial001_py39.py b/docs_src/debugging/tutorial001_py39.py deleted file mode 100644 index 3de21d2a82..0000000000 --- a/docs_src/debugging/tutorial001_py39.py +++ /dev/null @@ -1,15 +0,0 @@ -import uvicorn -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -def root(): - a = "a" - b = "b" + a - return {"hello world": b} - - -if __name__ == "__main__": - uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/docs_src/dependencies/tutorial006_an_py39.py b/docs_src/dependencies/tutorial006_an_py39.py deleted file mode 100644 index 11976ed6a2..0000000000 --- a/docs_src/dependencies/tutorial006_an_py39.py +++ /dev/null @@ -1,21 +0,0 @@ -from typing import Annotated - -from fastapi import Depends, FastAPI, Header, HTTPException - -app = FastAPI() - - -async def verify_token(x_token: Annotated[str, Header()]): - if x_token != "fake-super-secret-token": - raise HTTPException(status_code=400, detail="X-Token header invalid") - - -async def verify_key(x_key: Annotated[str, Header()]): - if x_key != "fake-super-secret-key": - raise HTTPException(status_code=400, detail="X-Key header invalid") - return x_key - - -@app.get("/items/", dependencies=[Depends(verify_token), Depends(verify_key)]) -async def read_items(): - return [{"item": "Foo"}, {"item": "Bar"}] diff --git a/docs_src/dependencies/tutorial006_py39.py b/docs_src/dependencies/tutorial006_py39.py deleted file mode 100644 index 9aff4154f4..0000000000 --- a/docs_src/dependencies/tutorial006_py39.py +++ /dev/null @@ -1,19 +0,0 @@ -from fastapi import Depends, FastAPI, Header, HTTPException - -app = FastAPI() - - -async def verify_token(x_token: str = Header()): - if x_token != "fake-super-secret-token": - raise HTTPException(status_code=400, detail="X-Token header invalid") - - -async def verify_key(x_key: str = Header()): - if x_key != "fake-super-secret-key": - raise HTTPException(status_code=400, detail="X-Key header invalid") - return x_key - - -@app.get("/items/", dependencies=[Depends(verify_token), Depends(verify_key)]) -async def read_items(): - return [{"item": "Foo"}, {"item": "Bar"}] diff --git a/docs_src/dependencies/tutorial007_py39.py b/docs_src/dependencies/tutorial007_py39.py deleted file mode 100644 index 2e4ab4777b..0000000000 --- a/docs_src/dependencies/tutorial007_py39.py +++ /dev/null @@ -1,6 +0,0 @@ -async def get_db(): - db = DBSession() - try: - yield db - finally: - db.close() diff --git a/docs_src/dependencies/tutorial008_an_py39.py b/docs_src/dependencies/tutorial008_an_py39.py deleted file mode 100644 index acc804c320..0000000000 --- a/docs_src/dependencies/tutorial008_an_py39.py +++ /dev/null @@ -1,27 +0,0 @@ -from typing import Annotated - -from fastapi import Depends - - -async def dependency_a(): - dep_a = generate_dep_a() - try: - yield dep_a - finally: - dep_a.close() - - -async def dependency_b(dep_a: Annotated[DepA, Depends(dependency_a)]): - dep_b = generate_dep_b() - try: - yield dep_b - finally: - dep_b.close(dep_a) - - -async def dependency_c(dep_b: Annotated[DepB, Depends(dependency_b)]): - dep_c = generate_dep_c() - try: - yield dep_c - finally: - dep_c.close(dep_b) diff --git a/docs_src/dependencies/tutorial008_py39.py b/docs_src/dependencies/tutorial008_py39.py deleted file mode 100644 index 8472f642de..0000000000 --- a/docs_src/dependencies/tutorial008_py39.py +++ /dev/null @@ -1,25 +0,0 @@ -from fastapi import Depends - - -async def dependency_a(): - dep_a = generate_dep_a() - try: - yield dep_a - finally: - dep_a.close() - - -async def dependency_b(dep_a=Depends(dependency_a)): - dep_b = generate_dep_b() - try: - yield dep_b - finally: - dep_b.close(dep_a) - - -async def dependency_c(dep_b=Depends(dependency_b)): - dep_c = generate_dep_c() - try: - yield dep_c - finally: - dep_c.close(dep_b) diff --git a/docs_src/dependencies/tutorial008b_an_py39.py b/docs_src/dependencies/tutorial008b_an_py39.py deleted file mode 100644 index 3b8434c816..0000000000 --- a/docs_src/dependencies/tutorial008b_an_py39.py +++ /dev/null @@ -1,32 +0,0 @@ -from typing import Annotated - -from fastapi import Depends, FastAPI, HTTPException - -app = FastAPI() - - -data = { - "plumbus": {"description": "Freshly pickled plumbus", "owner": "Morty"}, - "portal-gun": {"description": "Gun to create portals", "owner": "Rick"}, -} - - -class OwnerError(Exception): - pass - - -def get_username(): - try: - yield "Rick" - except OwnerError as e: - raise HTTPException(status_code=400, detail=f"Owner error: {e}") - - -@app.get("/items/{item_id}") -def get_item(item_id: str, username: Annotated[str, Depends(get_username)]): - if item_id not in data: - raise HTTPException(status_code=404, detail="Item not found") - item = data[item_id] - if item["owner"] != username: - raise OwnerError(username) - return item diff --git a/docs_src/dependencies/tutorial008b_py39.py b/docs_src/dependencies/tutorial008b_py39.py deleted file mode 100644 index 163e96600f..0000000000 --- a/docs_src/dependencies/tutorial008b_py39.py +++ /dev/null @@ -1,30 +0,0 @@ -from fastapi import Depends, FastAPI, HTTPException - -app = FastAPI() - - -data = { - "plumbus": {"description": "Freshly pickled plumbus", "owner": "Morty"}, - "portal-gun": {"description": "Gun to create portals", "owner": "Rick"}, -} - - -class OwnerError(Exception): - pass - - -def get_username(): - try: - yield "Rick" - except OwnerError as e: - raise HTTPException(status_code=400, detail=f"Owner error: {e}") - - -@app.get("/items/{item_id}") -def get_item(item_id: str, username: str = Depends(get_username)): - if item_id not in data: - raise HTTPException(status_code=404, detail="Item not found") - item = data[item_id] - if item["owner"] != username: - raise OwnerError(username) - return item diff --git a/docs_src/dependencies/tutorial008c_an_py39.py b/docs_src/dependencies/tutorial008c_an_py39.py deleted file mode 100644 index da92efa9c3..0000000000 --- a/docs_src/dependencies/tutorial008c_an_py39.py +++ /dev/null @@ -1,29 +0,0 @@ -from typing import Annotated - -from fastapi import Depends, FastAPI, HTTPException - -app = FastAPI() - - -class InternalError(Exception): - pass - - -def get_username(): - try: - yield "Rick" - except InternalError: - print("Oops, we didn't raise again, Britney 😱") - - -@app.get("/items/{item_id}") -def get_item(item_id: str, username: Annotated[str, Depends(get_username)]): - if item_id == "portal-gun": - raise InternalError( - f"The portal gun is too dangerous to be owned by {username}" - ) - if item_id != "plumbus": - raise HTTPException( - status_code=404, detail="Item not found, there's only a plumbus here" - ) - return item_id diff --git a/docs_src/dependencies/tutorial008c_py39.py b/docs_src/dependencies/tutorial008c_py39.py deleted file mode 100644 index 4b99a5a311..0000000000 --- a/docs_src/dependencies/tutorial008c_py39.py +++ /dev/null @@ -1,27 +0,0 @@ -from fastapi import Depends, FastAPI, HTTPException - -app = FastAPI() - - -class InternalError(Exception): - pass - - -def get_username(): - try: - yield "Rick" - except InternalError: - print("Oops, we didn't raise again, Britney 😱") - - -@app.get("/items/{item_id}") -def get_item(item_id: str, username: str = Depends(get_username)): - if item_id == "portal-gun": - raise InternalError( - f"The portal gun is too dangerous to be owned by {username}" - ) - if item_id != "plumbus": - raise HTTPException( - status_code=404, detail="Item not found, there's only a plumbus here" - ) - return item_id diff --git a/docs_src/dependencies/tutorial008d_an_py39.py b/docs_src/dependencies/tutorial008d_an_py39.py deleted file mode 100644 index 99bd5cb911..0000000000 --- a/docs_src/dependencies/tutorial008d_an_py39.py +++ /dev/null @@ -1,30 +0,0 @@ -from typing import Annotated - -from fastapi import Depends, FastAPI, HTTPException - -app = FastAPI() - - -class InternalError(Exception): - pass - - -def get_username(): - try: - yield "Rick" - except InternalError: - print("We don't swallow the internal error here, we raise again 😎") - raise - - -@app.get("/items/{item_id}") -def get_item(item_id: str, username: Annotated[str, Depends(get_username)]): - if item_id == "portal-gun": - raise InternalError( - f"The portal gun is too dangerous to be owned by {username}" - ) - if item_id != "plumbus": - raise HTTPException( - status_code=404, detail="Item not found, there's only a plumbus here" - ) - return item_id diff --git a/docs_src/dependencies/tutorial008d_py39.py b/docs_src/dependencies/tutorial008d_py39.py deleted file mode 100644 index 93039343d1..0000000000 --- a/docs_src/dependencies/tutorial008d_py39.py +++ /dev/null @@ -1,28 +0,0 @@ -from fastapi import Depends, FastAPI, HTTPException - -app = FastAPI() - - -class InternalError(Exception): - pass - - -def get_username(): - try: - yield "Rick" - except InternalError: - print("We don't swallow the internal error here, we raise again 😎") - raise - - -@app.get("/items/{item_id}") -def get_item(item_id: str, username: str = Depends(get_username)): - if item_id == "portal-gun": - raise InternalError( - f"The portal gun is too dangerous to be owned by {username}" - ) - if item_id != "plumbus": - raise HTTPException( - status_code=404, detail="Item not found, there's only a plumbus here" - ) - return item_id diff --git a/docs_src/dependencies/tutorial008e_an_py39.py b/docs_src/dependencies/tutorial008e_an_py39.py deleted file mode 100644 index 80a44c7e23..0000000000 --- a/docs_src/dependencies/tutorial008e_an_py39.py +++ /dev/null @@ -1,17 +0,0 @@ -from typing import Annotated - -from fastapi import Depends, FastAPI - -app = FastAPI() - - -def get_username(): - try: - yield "Rick" - finally: - print("Cleanup up before response is sent") - - -@app.get("/users/me") -def get_user_me(username: Annotated[str, Depends(get_username, scope="function")]): - return username diff --git a/docs_src/dependencies/tutorial008e_py39.py b/docs_src/dependencies/tutorial008e_py39.py deleted file mode 100644 index 1ed056e91e..0000000000 --- a/docs_src/dependencies/tutorial008e_py39.py +++ /dev/null @@ -1,15 +0,0 @@ -from fastapi import Depends, FastAPI - -app = FastAPI() - - -def get_username(): - try: - yield "Rick" - finally: - print("Cleanup up before response is sent") - - -@app.get("/users/me") -def get_user_me(username: str = Depends(get_username, scope="function")): - return username diff --git a/docs_src/dependencies/tutorial010_py39.py b/docs_src/dependencies/tutorial010_py39.py deleted file mode 100644 index c27f1b1702..0000000000 --- a/docs_src/dependencies/tutorial010_py39.py +++ /dev/null @@ -1,14 +0,0 @@ -class MySuperContextManager: - def __init__(self): - self.db = DBSession() - - def __enter__(self): - return self.db - - def __exit__(self, exc_type, exc_value, traceback): - self.db.close() - - -async def get_db(): - with MySuperContextManager() as db: - yield db diff --git a/docs_src/dependencies/tutorial011_an_py39.py b/docs_src/dependencies/tutorial011_an_py39.py deleted file mode 100644 index 68b7434ec2..0000000000 --- a/docs_src/dependencies/tutorial011_an_py39.py +++ /dev/null @@ -1,23 +0,0 @@ -from typing import Annotated - -from fastapi import Depends, FastAPI - -app = FastAPI() - - -class FixedContentQueryChecker: - def __init__(self, fixed_content: str): - self.fixed_content = fixed_content - - def __call__(self, q: str = ""): - if q: - return self.fixed_content in q - return False - - -checker = FixedContentQueryChecker("bar") - - -@app.get("/query-checker/") -async def read_query_check(fixed_content_included: Annotated[bool, Depends(checker)]): - return {"fixed_content_in_query": fixed_content_included} diff --git a/docs_src/dependencies/tutorial011_py39.py b/docs_src/dependencies/tutorial011_py39.py deleted file mode 100644 index 5d22f68237..0000000000 --- a/docs_src/dependencies/tutorial011_py39.py +++ /dev/null @@ -1,21 +0,0 @@ -from fastapi import Depends, FastAPI - -app = FastAPI() - - -class FixedContentQueryChecker: - def __init__(self, fixed_content: str): - self.fixed_content = fixed_content - - def __call__(self, q: str = ""): - if q: - return self.fixed_content in q - return False - - -checker = FixedContentQueryChecker("bar") - - -@app.get("/query-checker/") -async def read_query_check(fixed_content_included: bool = Depends(checker)): - return {"fixed_content_in_query": fixed_content_included} diff --git a/docs_src/dependencies/tutorial012_an_py39.py b/docs_src/dependencies/tutorial012_an_py39.py deleted file mode 100644 index 6503591fc3..0000000000 --- a/docs_src/dependencies/tutorial012_an_py39.py +++ /dev/null @@ -1,27 +0,0 @@ -from typing import Annotated - -from fastapi import Depends, FastAPI, Header, HTTPException - - -async def verify_token(x_token: Annotated[str, Header()]): - if x_token != "fake-super-secret-token": - raise HTTPException(status_code=400, detail="X-Token header invalid") - - -async def verify_key(x_key: Annotated[str, Header()]): - if x_key != "fake-super-secret-key": - raise HTTPException(status_code=400, detail="X-Key header invalid") - return x_key - - -app = FastAPI(dependencies=[Depends(verify_token), Depends(verify_key)]) - - -@app.get("/items/") -async def read_items(): - return [{"item": "Portal Gun"}, {"item": "Plumbus"}] - - -@app.get("/users/") -async def read_users(): - return [{"username": "Rick"}, {"username": "Morty"}] diff --git a/docs_src/dependencies/tutorial012_py39.py b/docs_src/dependencies/tutorial012_py39.py deleted file mode 100644 index 36ce6c7111..0000000000 --- a/docs_src/dependencies/tutorial012_py39.py +++ /dev/null @@ -1,25 +0,0 @@ -from fastapi import Depends, FastAPI, Header, HTTPException - - -async def verify_token(x_token: str = Header()): - if x_token != "fake-super-secret-token": - raise HTTPException(status_code=400, detail="X-Token header invalid") - - -async def verify_key(x_key: str = Header()): - if x_key != "fake-super-secret-key": - raise HTTPException(status_code=400, detail="X-Key header invalid") - return x_key - - -app = FastAPI(dependencies=[Depends(verify_token), Depends(verify_key)]) - - -@app.get("/items/") -async def read_items(): - return [{"item": "Portal Gun"}, {"item": "Plumbus"}] - - -@app.get("/users/") -async def read_users(): - return [{"username": "Rick"}, {"username": "Morty"}] diff --git a/docs_src/events/tutorial001_py39.py b/docs_src/events/tutorial001_py39.py deleted file mode 100644 index 128004c9fb..0000000000 --- a/docs_src/events/tutorial001_py39.py +++ /dev/null @@ -1,16 +0,0 @@ -from fastapi import FastAPI - -app = FastAPI() - -items = {} - - -@app.on_event("startup") -async def startup_event(): - items["foo"] = {"name": "Fighters"} - items["bar"] = {"name": "Tenders"} - - -@app.get("/items/{item_id}") -async def read_items(item_id: str): - return items[item_id] diff --git a/docs_src/events/tutorial002_py39.py b/docs_src/events/tutorial002_py39.py deleted file mode 100644 index a71fea8025..0000000000 --- a/docs_src/events/tutorial002_py39.py +++ /dev/null @@ -1,14 +0,0 @@ -from fastapi import FastAPI - -app = FastAPI() - - -@app.on_event("shutdown") -def shutdown_event(): - with open("log.txt", mode="a") as log: - log.write("Application shutdown") - - -@app.get("/items/") -async def read_items(): - return [{"name": "Foo"}] diff --git a/docs_src/events/tutorial003_py39.py b/docs_src/events/tutorial003_py39.py deleted file mode 100644 index 2b650590b0..0000000000 --- a/docs_src/events/tutorial003_py39.py +++ /dev/null @@ -1,28 +0,0 @@ -from contextlib import asynccontextmanager - -from fastapi import FastAPI - - -def fake_answer_to_everything_ml_model(x: float): - return x * 42 - - -ml_models = {} - - -@asynccontextmanager -async def lifespan(app: FastAPI): - # Load the ML model - ml_models["answer_to_everything"] = fake_answer_to_everything_ml_model - yield - # Clean up the ML models and release the resources - ml_models.clear() - - -app = FastAPI(lifespan=lifespan) - - -@app.get("/predict") -async def predict(x: float): - result = ml_models["answer_to_everything"](x) - return {"result": result} diff --git a/docs_src/extending_openapi/tutorial001_py39.py b/docs_src/extending_openapi/tutorial001_py39.py deleted file mode 100644 index 35e31c0e0c..0000000000 --- a/docs_src/extending_openapi/tutorial001_py39.py +++ /dev/null @@ -1,29 +0,0 @@ -from fastapi import FastAPI -from fastapi.openapi.utils import get_openapi - -app = FastAPI() - - -@app.get("/items/") -async def read_items(): - return [{"name": "Foo"}] - - -def custom_openapi(): - if app.openapi_schema: - return app.openapi_schema - openapi_schema = get_openapi( - title="Custom title", - version="2.5.0", - summary="This is a very custom OpenAPI schema", - description="Here's a longer description of the custom **OpenAPI** schema", - routes=app.routes, - ) - openapi_schema["info"]["x-logo"] = { - "url": "https://fastapi.tiangolo.com/img/logo-margin/logo-teal.png" - } - app.openapi_schema = openapi_schema - return app.openapi_schema - - -app.openapi = custom_openapi diff --git a/docs_src/extra_models/tutorial003_py310.py b/docs_src/extra_models/tutorial003_py310.py index 06675cbc09..8fe6f7136e 100644 --- a/docs_src/extra_models/tutorial003_py310.py +++ b/docs_src/extra_models/tutorial003_py310.py @@ -1,5 +1,3 @@ -from typing import Union - from fastapi import FastAPI from pydantic import BaseModel @@ -30,6 +28,6 @@ items = { } -@app.get("/items/{item_id}", response_model=Union[PlaneItem, CarItem]) +@app.get("/items/{item_id}", response_model=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 deleted file mode 100644 index 28cacde4d4..0000000000 --- a/docs_src/extra_models/tutorial004_py39.py +++ /dev/null @@ -1,20 +0,0 @@ -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 deleted file mode 100644 index 9da2a0a0f4..0000000000 --- a/docs_src/extra_models/tutorial005_py39.py +++ /dev/null @@ -1,8 +0,0 @@ -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/first_steps/tutorial001_py39.py b/docs_src/first_steps/tutorial001_py39.py deleted file mode 100644 index ee60be1f96..0000000000 --- a/docs_src/first_steps/tutorial001_py39.py +++ /dev/null @@ -1,8 +0,0 @@ -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -async def root(): - return {"message": "Hello World"} diff --git a/docs_src/first_steps/tutorial003_py39.py b/docs_src/first_steps/tutorial003_py39.py deleted file mode 100644 index e30b827eab..0000000000 --- a/docs_src/first_steps/tutorial003_py39.py +++ /dev/null @@ -1,8 +0,0 @@ -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -def root(): - return {"message": "Hello World"} diff --git a/docs_src/generate_clients/tutorial001_py39.py b/docs_src/generate_clients/tutorial001_py39.py deleted file mode 100644 index 6a5ae23202..0000000000 --- a/docs_src/generate_clients/tutorial001_py39.py +++ /dev/null @@ -1,26 +0,0 @@ -from fastapi import FastAPI -from pydantic import BaseModel - -app = FastAPI() - - -class Item(BaseModel): - name: str - price: float - - -class ResponseMessage(BaseModel): - message: str - - -@app.post("/items/", response_model=ResponseMessage) -async def create_item(item: Item): - return {"message": "item received"} - - -@app.get("/items/", response_model=list[Item]) -async def get_items(): - return [ - {"name": "Plumbus", "price": 3}, - {"name": "Portal Gun", "price": 9001}, - ] diff --git a/docs_src/generate_clients/tutorial002_py39.py b/docs_src/generate_clients/tutorial002_py39.py deleted file mode 100644 index 83309760b9..0000000000 --- a/docs_src/generate_clients/tutorial002_py39.py +++ /dev/null @@ -1,36 +0,0 @@ -from fastapi import FastAPI -from pydantic import BaseModel - -app = FastAPI() - - -class Item(BaseModel): - name: str - price: float - - -class ResponseMessage(BaseModel): - message: str - - -class User(BaseModel): - username: str - email: str - - -@app.post("/items/", response_model=ResponseMessage, tags=["items"]) -async def create_item(item: Item): - return {"message": "Item received"} - - -@app.get("/items/", response_model=list[Item], tags=["items"]) -async def get_items(): - return [ - {"name": "Plumbus", "price": 3}, - {"name": "Portal Gun", "price": 9001}, - ] - - -@app.post("/users/", response_model=ResponseMessage, tags=["users"]) -async def create_user(user: User): - return {"message": "User received"} diff --git a/docs_src/generate_clients/tutorial003_py39.py b/docs_src/generate_clients/tutorial003_py39.py deleted file mode 100644 index 40722cf103..0000000000 --- a/docs_src/generate_clients/tutorial003_py39.py +++ /dev/null @@ -1,42 +0,0 @@ -from fastapi import FastAPI -from fastapi.routing import APIRoute -from pydantic import BaseModel - - -def custom_generate_unique_id(route: APIRoute): - return f"{route.tags[0]}-{route.name}" - - -app = FastAPI(generate_unique_id_function=custom_generate_unique_id) - - -class Item(BaseModel): - name: str - price: float - - -class ResponseMessage(BaseModel): - message: str - - -class User(BaseModel): - username: str - email: str - - -@app.post("/items/", response_model=ResponseMessage, tags=["items"]) -async def create_item(item: Item): - return {"message": "Item received"} - - -@app.get("/items/", response_model=list[Item], tags=["items"]) -async def get_items(): - return [ - {"name": "Plumbus", "price": 3}, - {"name": "Portal Gun", "price": 9001}, - ] - - -@app.post("/users/", response_model=ResponseMessage, tags=["users"]) -async def create_user(user: User): - return {"message": "User received"} diff --git a/docs_src/generate_clients/tutorial004_py39.py b/docs_src/generate_clients/tutorial004_py39.py deleted file mode 100644 index 894dc7f8df..0000000000 --- a/docs_src/generate_clients/tutorial004_py39.py +++ /dev/null @@ -1,15 +0,0 @@ -import json -from pathlib import Path - -file_path = Path("./openapi.json") -openapi_content = json.loads(file_path.read_text()) - -for path_data in openapi_content["paths"].values(): - for operation in path_data.values(): - tag = operation["tags"][0] - operation_id = operation["operationId"] - to_remove = f"{tag}-" - new_operation_id = operation_id[len(to_remove) :] - operation["operationId"] = new_operation_id - -file_path.write_text(json.dumps(openapi_content)) diff --git a/docs_src/graphql_/tutorial001_py39.py b/docs_src/graphql_/tutorial001_py39.py deleted file mode 100644 index e92b2d71c4..0000000000 --- a/docs_src/graphql_/tutorial001_py39.py +++ /dev/null @@ -1,25 +0,0 @@ -import strawberry -from fastapi import FastAPI -from strawberry.fastapi import GraphQLRouter - - -@strawberry.type -class User: - name: str - age: int - - -@strawberry.type -class Query: - @strawberry.field - def user(self) -> User: - return User(name="Patrick", age=100) - - -schema = strawberry.Schema(query=Query) - - -graphql_app = GraphQLRouter(schema) - -app = FastAPI() -app.include_router(graphql_app, prefix="/graphql") diff --git a/docs_src/handling_errors/tutorial001_py39.py b/docs_src/handling_errors/tutorial001_py39.py deleted file mode 100644 index e5f32aac29..0000000000 --- a/docs_src/handling_errors/tutorial001_py39.py +++ /dev/null @@ -1,12 +0,0 @@ -from fastapi import FastAPI, HTTPException - -app = FastAPI() - -items = {"foo": "The Foo Wrestlers"} - - -@app.get("/items/{item_id}") -async def read_item(item_id: str): - if item_id not in items: - raise HTTPException(status_code=404, detail="Item not found") - return {"item": items[item_id]} diff --git a/docs_src/handling_errors/tutorial002_py39.py b/docs_src/handling_errors/tutorial002_py39.py deleted file mode 100644 index e48c295c9c..0000000000 --- a/docs_src/handling_errors/tutorial002_py39.py +++ /dev/null @@ -1,16 +0,0 @@ -from fastapi import FastAPI, HTTPException - -app = FastAPI() - -items = {"foo": "The Foo Wrestlers"} - - -@app.get("/items-header/{item_id}") -async def read_item_header(item_id: str): - if item_id not in items: - raise HTTPException( - status_code=404, - detail="Item not found", - headers={"X-Error": "There goes my error"}, - ) - return {"item": items[item_id]} diff --git a/docs_src/handling_errors/tutorial003_py39.py b/docs_src/handling_errors/tutorial003_py39.py deleted file mode 100644 index 791cd6838c..0000000000 --- a/docs_src/handling_errors/tutorial003_py39.py +++ /dev/null @@ -1,25 +0,0 @@ -from fastapi import FastAPI, Request -from fastapi.responses import JSONResponse - - -class UnicornException(Exception): - def __init__(self, name: str): - self.name = name - - -app = FastAPI() - - -@app.exception_handler(UnicornException) -async def unicorn_exception_handler(request: Request, exc: UnicornException): - return JSONResponse( - status_code=418, - content={"message": f"Oops! {exc.name} did something. There goes a rainbow..."}, - ) - - -@app.get("/unicorns/{name}") -async def read_unicorn(name: str): - if name == "yolo": - raise UnicornException(name=name) - return {"unicorn_name": name} diff --git a/docs_src/handling_errors/tutorial004_py39.py b/docs_src/handling_errors/tutorial004_py39.py deleted file mode 100644 index ae50807e97..0000000000 --- a/docs_src/handling_errors/tutorial004_py39.py +++ /dev/null @@ -1,26 +0,0 @@ -from fastapi import FastAPI, HTTPException -from fastapi.exceptions import RequestValidationError -from fastapi.responses import PlainTextResponse -from starlette.exceptions import HTTPException as StarletteHTTPException - -app = FastAPI() - - -@app.exception_handler(StarletteHTTPException) -async def http_exception_handler(request, exc): - return PlainTextResponse(str(exc.detail), status_code=exc.status_code) - - -@app.exception_handler(RequestValidationError) -async def validation_exception_handler(request, exc: RequestValidationError): - message = "Validation errors:" - for error in exc.errors(): - message += f"\nField: {error['loc']}, Error: {error['msg']}" - return PlainTextResponse(message, status_code=400) - - -@app.get("/items/{item_id}") -async def read_item(item_id: int): - if item_id == 3: - raise HTTPException(status_code=418, detail="Nope! I don't like 3.") - return {"item_id": item_id} diff --git a/docs_src/handling_errors/tutorial005_py39.py b/docs_src/handling_errors/tutorial005_py39.py deleted file mode 100644 index 0e04fa0864..0000000000 --- a/docs_src/handling_errors/tutorial005_py39.py +++ /dev/null @@ -1,25 +0,0 @@ -from fastapi import FastAPI, Request -from fastapi.encoders import jsonable_encoder -from fastapi.exceptions import RequestValidationError -from fastapi.responses import JSONResponse -from pydantic import BaseModel - -app = FastAPI() - - -@app.exception_handler(RequestValidationError) -async def validation_exception_handler(request: Request, exc: RequestValidationError): - return JSONResponse( - status_code=422, - content=jsonable_encoder({"detail": exc.errors(), "body": exc.body}), - ) - - -class Item(BaseModel): - title: str - size: int - - -@app.post("/items/") -async def create_item(item: Item): - return item diff --git a/docs_src/handling_errors/tutorial006_py39.py b/docs_src/handling_errors/tutorial006_py39.py deleted file mode 100644 index e05160d7e1..0000000000 --- a/docs_src/handling_errors/tutorial006_py39.py +++ /dev/null @@ -1,28 +0,0 @@ -from fastapi import FastAPI, HTTPException -from fastapi.exception_handlers import ( - http_exception_handler, - request_validation_exception_handler, -) -from fastapi.exceptions import RequestValidationError -from starlette.exceptions import HTTPException as StarletteHTTPException - -app = FastAPI() - - -@app.exception_handler(StarletteHTTPException) -async def custom_http_exception_handler(request, exc): - print(f"OMG! An HTTP error!: {repr(exc)}") - return await http_exception_handler(request, exc) - - -@app.exception_handler(RequestValidationError) -async def validation_exception_handler(request, exc): - print(f"OMG! The client sent invalid data!: {exc}") - return await request_validation_exception_handler(request, exc) - - -@app.get("/items/{item_id}") -async def read_item(item_id: int): - if item_id == 3: - raise HTTPException(status_code=418, detail="Nope! I don't like 3.") - return {"item_id": item_id} diff --git a/docs_src/app_testing/app_a_py39/__init__.py b/docs_src/json_base64_bytes/__init__.py similarity index 100% rename from docs_src/app_testing/app_a_py39/__init__.py rename to docs_src/json_base64_bytes/__init__.py diff --git a/docs_src/json_base64_bytes/tutorial001_py310.py b/docs_src/json_base64_bytes/tutorial001_py310.py new file mode 100644 index 0000000000..3262ffb7f0 --- /dev/null +++ b/docs_src/json_base64_bytes/tutorial001_py310.py @@ -0,0 +1,46 @@ +from fastapi import FastAPI +from pydantic import BaseModel + + +class DataInput(BaseModel): + description: str + data: bytes + + model_config = {"val_json_bytes": "base64"} + + +class DataOutput(BaseModel): + description: str + data: bytes + + model_config = {"ser_json_bytes": "base64"} + + +class DataInputOutput(BaseModel): + description: str + data: bytes + + model_config = { + "val_json_bytes": "base64", + "ser_json_bytes": "base64", + } + + +app = FastAPI() + + +@app.post("/data") +def post_data(body: DataInput): + content = body.data.decode("utf-8") + return {"description": body.description, "content": content} + + +@app.get("/data") +def get_data() -> DataOutput: + data = "hello".encode("utf-8") + return DataOutput(description="A plumbus", data=data) + + +@app.post("/data-in-out") +def post_data_in_out(body: DataInputOutput) -> DataInputOutput: + return body diff --git a/docs_src/metadata/tutorial001_1_py39.py b/docs_src/metadata/tutorial001_1_py39.py deleted file mode 100644 index 419232d861..0000000000 --- a/docs_src/metadata/tutorial001_1_py39.py +++ /dev/null @@ -1,38 +0,0 @@ -from fastapi import FastAPI - -description = """ -ChimichangApp API helps you do awesome stuff. 🚀 - -## Items - -You can **read items**. - -## Users - -You will be able to: - -* **Create users** (_not implemented_). -* **Read users** (_not implemented_). -""" - -app = FastAPI( - title="ChimichangApp", - description=description, - summary="Deadpool's favorite app. Nuff said.", - version="0.0.1", - terms_of_service="http://example.com/terms/", - contact={ - "name": "Deadpoolio the Amazing", - "url": "http://x-force.example.com/contact/", - "email": "dp@x-force.example.com", - }, - license_info={ - "name": "Apache 2.0", - "identifier": "Apache-2.0", - }, -) - - -@app.get("/items/") -async def read_items(): - return [{"name": "Katana"}] diff --git a/docs_src/metadata/tutorial001_py39.py b/docs_src/metadata/tutorial001_py39.py deleted file mode 100644 index 76656e81b4..0000000000 --- a/docs_src/metadata/tutorial001_py39.py +++ /dev/null @@ -1,38 +0,0 @@ -from fastapi import FastAPI - -description = """ -ChimichangApp API helps you do awesome stuff. 🚀 - -## Items - -You can **read items**. - -## Users - -You will be able to: - -* **Create users** (_not implemented_). -* **Read users** (_not implemented_). -""" - -app = FastAPI( - title="ChimichangApp", - description=description, - summary="Deadpool's favorite app. Nuff said.", - version="0.0.1", - terms_of_service="http://example.com/terms/", - contact={ - "name": "Deadpoolio the Amazing", - "url": "http://x-force.example.com/contact/", - "email": "dp@x-force.example.com", - }, - license_info={ - "name": "Apache 2.0", - "url": "https://www.apache.org/licenses/LICENSE-2.0.html", - }, -) - - -@app.get("/items/") -async def read_items(): - return [{"name": "Katana"}] diff --git a/docs_src/metadata/tutorial002_py39.py b/docs_src/metadata/tutorial002_py39.py deleted file mode 100644 index cf9ed7087e..0000000000 --- a/docs_src/metadata/tutorial002_py39.py +++ /dev/null @@ -1,8 +0,0 @@ -from fastapi import FastAPI - -app = FastAPI(openapi_url="/api/v1/openapi.json") - - -@app.get("/items/") -async def read_items(): - return [{"name": "Foo"}] diff --git a/docs_src/metadata/tutorial003_py39.py b/docs_src/metadata/tutorial003_py39.py deleted file mode 100644 index ee09c7f37b..0000000000 --- a/docs_src/metadata/tutorial003_py39.py +++ /dev/null @@ -1,8 +0,0 @@ -from fastapi import FastAPI - -app = FastAPI(docs_url="/documentation", redoc_url=None) - - -@app.get("/items/") -async def read_items(): - return [{"name": "Foo"}] diff --git a/docs_src/metadata/tutorial004_py39.py b/docs_src/metadata/tutorial004_py39.py deleted file mode 100644 index 465bd659d5..0000000000 --- a/docs_src/metadata/tutorial004_py39.py +++ /dev/null @@ -1,28 +0,0 @@ -from fastapi import FastAPI - -tags_metadata = [ - { - "name": "users", - "description": "Operations with users. The **login** logic is also here.", - }, - { - "name": "items", - "description": "Manage items. So _fancy_ they have their own docs.", - "externalDocs": { - "description": "Items external docs", - "url": "https://fastapi.tiangolo.com/", - }, - }, -] - -app = FastAPI(openapi_tags=tags_metadata) - - -@app.get("/users/", tags=["users"]) -async def get_users(): - return [{"name": "Harry"}, {"name": "Ron"}] - - -@app.get("/items/", tags=["items"]) -async def get_items(): - return [{"name": "wand"}, {"name": "flying broom"}] diff --git a/docs_src/middleware/tutorial001_py39.py b/docs_src/middleware/tutorial001_py39.py deleted file mode 100644 index e65a7dade1..0000000000 --- a/docs_src/middleware/tutorial001_py39.py +++ /dev/null @@ -1,14 +0,0 @@ -import time - -from fastapi import FastAPI, Request - -app = FastAPI() - - -@app.middleware("http") -async def add_process_time_header(request: Request, call_next): - start_time = time.perf_counter() - response = await call_next(request) - process_time = time.perf_counter() - start_time - response.headers["X-Process-Time"] = str(process_time) - return response diff --git a/docs_src/openapi_webhooks/tutorial001_py39.py b/docs_src/openapi_webhooks/tutorial001_py39.py deleted file mode 100644 index 55822bb48f..0000000000 --- a/docs_src/openapi_webhooks/tutorial001_py39.py +++ /dev/null @@ -1,25 +0,0 @@ -from datetime import datetime - -from fastapi import FastAPI -from pydantic import BaseModel - -app = FastAPI() - - -class Subscription(BaseModel): - username: str - monthly_fee: float - start_date: datetime - - -@app.webhooks.post("new-subscription") -def new_subscription(body: Subscription): - """ - When a new user subscribes to your service we'll send you a POST request with this - data to the URL that you register for the event `new-subscription` in the dashboard. - """ - - -@app.get("/users/") -def read_users(): - return ["Rick", "Morty"] diff --git a/docs_src/path_operation_advanced_configuration/tutorial001_py39.py b/docs_src/path_operation_advanced_configuration/tutorial001_py39.py deleted file mode 100644 index fafa8ffb8e..0000000000 --- a/docs_src/path_operation_advanced_configuration/tutorial001_py39.py +++ /dev/null @@ -1,8 +0,0 @@ -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/items/", operation_id="some_specific_id_you_define") -async def read_items(): - return [{"item_id": "Foo"}] diff --git a/docs_src/path_operation_advanced_configuration/tutorial002_py39.py b/docs_src/path_operation_advanced_configuration/tutorial002_py39.py deleted file mode 100644 index 3aaae9b371..0000000000 --- a/docs_src/path_operation_advanced_configuration/tutorial002_py39.py +++ /dev/null @@ -1,24 +0,0 @@ -from fastapi import FastAPI -from fastapi.routing import APIRoute - -app = FastAPI() - - -@app.get("/items/") -async def read_items(): - return [{"item_id": "Foo"}] - - -def use_route_names_as_operation_ids(app: FastAPI) -> None: - """ - Simplify operation IDs so that generated API clients have simpler function - names. - - Should be called only after all routes have been added. - """ - for route in app.routes: - if isinstance(route, APIRoute): - route.operation_id = route.name # in this case, 'read_items' - - -use_route_names_as_operation_ids(app) diff --git a/docs_src/path_operation_advanced_configuration/tutorial003_py39.py b/docs_src/path_operation_advanced_configuration/tutorial003_py39.py deleted file mode 100644 index dcc358e323..0000000000 --- a/docs_src/path_operation_advanced_configuration/tutorial003_py39.py +++ /dev/null @@ -1,8 +0,0 @@ -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/items/", include_in_schema=False) -async def read_items(): - return [{"item_id": "Foo"}] diff --git a/docs_src/path_operation_advanced_configuration/tutorial005_py39.py b/docs_src/path_operation_advanced_configuration/tutorial005_py39.py deleted file mode 100644 index 5837ad8351..0000000000 --- a/docs_src/path_operation_advanced_configuration/tutorial005_py39.py +++ /dev/null @@ -1,8 +0,0 @@ -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/items/", openapi_extra={"x-aperture-labs-portal": "blue"}) -async def read_items(): - return [{"item_id": "portal-gun"}] diff --git a/docs_src/path_operation_advanced_configuration/tutorial006_py39.py b/docs_src/path_operation_advanced_configuration/tutorial006_py39.py deleted file mode 100644 index 403c3ee3fb..0000000000 --- a/docs_src/path_operation_advanced_configuration/tutorial006_py39.py +++ /dev/null @@ -1,41 +0,0 @@ -from fastapi import FastAPI, Request - -app = FastAPI() - - -def magic_data_reader(raw_body: bytes): - return { - "size": len(raw_body), - "content": { - "name": "Maaaagic", - "price": 42, - "description": "Just kiddin', no magic here. ✨", - }, - } - - -@app.post( - "/items/", - openapi_extra={ - "requestBody": { - "content": { - "application/json": { - "schema": { - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"type": "string"}, - "price": {"type": "number"}, - "description": {"type": "string"}, - }, - } - } - }, - "required": True, - }, - }, -) -async def create_item(request: Request): - raw_body = await request.body() - data = magic_data_reader(raw_body) - return data diff --git a/docs_src/path_operation_advanced_configuration/tutorial007_py39.py b/docs_src/path_operation_advanced_configuration/tutorial007_py39.py deleted file mode 100644 index ff64ef7923..0000000000 --- a/docs_src/path_operation_advanced_configuration/tutorial007_py39.py +++ /dev/null @@ -1,32 +0,0 @@ -import yaml -from fastapi import FastAPI, HTTPException, Request -from pydantic import BaseModel, ValidationError - -app = FastAPI() - - -class Item(BaseModel): - name: str - tags: list[str] - - -@app.post( - "/items/", - openapi_extra={ - "requestBody": { - "content": {"application/x-yaml": {"schema": Item.model_json_schema()}}, - "required": True, - }, - }, -) -async def create_item(request: Request): - raw_body = await request.body() - try: - data = yaml.safe_load(raw_body) - except yaml.YAMLError: - raise HTTPException(status_code=422, detail="Invalid YAML") - try: - item = Item.model_validate(data) - except ValidationError as e: - raise HTTPException(status_code=422, detail=e.errors(include_url=False)) - return item diff --git a/docs_src/path_operation_configuration/tutorial002b_py39.py b/docs_src/path_operation_configuration/tutorial002b_py39.py deleted file mode 100644 index d53b4d817d..0000000000 --- a/docs_src/path_operation_configuration/tutorial002b_py39.py +++ /dev/null @@ -1,20 +0,0 @@ -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/docs_src/path_operation_configuration/tutorial006_py39.py b/docs_src/path_operation_configuration/tutorial006_py39.py deleted file mode 100644 index 7c1aa9b206..0000000000 --- a/docs_src/path_operation_configuration/tutorial006_py39.py +++ /dev/null @@ -1,18 +0,0 @@ -from fastapi import FastAPI - -app = FastAPI() - - -@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"}] - - -@app.get("/elements/", tags=["items"], deprecated=True) -async def read_elements(): - return [{"item_id": "Foo"}] diff --git a/docs_src/path_params/tutorial001_py39.py b/docs_src/path_params/tutorial001_py39.py deleted file mode 100644 index 7bbf70e6c8..0000000000 --- a/docs_src/path_params/tutorial001_py39.py +++ /dev/null @@ -1,8 +0,0 @@ -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/items/{item_id}") -async def read_item(item_id): - return {"item_id": item_id} diff --git a/docs_src/path_params/tutorial002_py39.py b/docs_src/path_params/tutorial002_py39.py deleted file mode 100644 index 8272ad70d4..0000000000 --- a/docs_src/path_params/tutorial002_py39.py +++ /dev/null @@ -1,8 +0,0 @@ -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/items/{item_id}") -async def read_item(item_id: int): - return {"item_id": item_id} diff --git a/docs_src/path_params/tutorial003_py39.py b/docs_src/path_params/tutorial003_py39.py deleted file mode 100644 index 5f0aa09234..0000000000 --- a/docs_src/path_params/tutorial003_py39.py +++ /dev/null @@ -1,13 +0,0 @@ -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/users/me") -async def read_user_me(): - return {"user_id": "the current user"} - - -@app.get("/users/{user_id}") -async def read_user(user_id: str): - return {"user_id": user_id} diff --git a/docs_src/path_params/tutorial003b_py39.py b/docs_src/path_params/tutorial003b_py39.py deleted file mode 100644 index 822d373694..0000000000 --- a/docs_src/path_params/tutorial003b_py39.py +++ /dev/null @@ -1,13 +0,0 @@ -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/users") -async def read_users(): - return ["Rick", "Morty"] - - -@app.get("/users") -async def read_users2(): - return ["Bean", "Elfo"] diff --git a/docs_src/path_params/tutorial004_py39.py b/docs_src/path_params/tutorial004_py39.py deleted file mode 100644 index 2961e6178e..0000000000 --- a/docs_src/path_params/tutorial004_py39.py +++ /dev/null @@ -1,8 +0,0 @@ -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/files/{file_path:path}") -async def read_file(file_path: str): - return {"file_path": file_path} diff --git a/docs_src/path_params/tutorial005_py39.py b/docs_src/path_params/tutorial005_py39.py deleted file mode 100644 index 9a24a4963a..0000000000 --- a/docs_src/path_params/tutorial005_py39.py +++ /dev/null @@ -1,23 +0,0 @@ -from enum import Enum - -from fastapi import FastAPI - - -class ModelName(str, Enum): - alexnet = "alexnet" - resnet = "resnet" - lenet = "lenet" - - -app = FastAPI() - - -@app.get("/models/{model_name}") -async def get_model(model_name: ModelName): - if model_name is ModelName.alexnet: - return {"model_name": model_name, "message": "Deep Learning FTW!"} - - if model_name.value == "lenet": - return {"model_name": model_name, "message": "LeCNN all the images"} - - return {"model_name": model_name, "message": "Have some residuals"} diff --git a/docs_src/path_params_numeric_validations/tutorial002_an_py39.py b/docs_src/path_params_numeric_validations/tutorial002_an_py39.py deleted file mode 100644 index cd882abb2c..0000000000 --- a/docs_src/path_params_numeric_validations/tutorial002_an_py39.py +++ /dev/null @@ -1,15 +0,0 @@ -from typing import Annotated - -from fastapi import FastAPI, Path - -app = FastAPI() - - -@app.get("/items/{item_id}") -async def read_items( - q: str, item_id: Annotated[int, Path(title="The ID of the item to get")] -): - results = {"item_id": item_id} - if q: - results.update({"q": q}) - return results diff --git a/docs_src/path_params_numeric_validations/tutorial002_py39.py b/docs_src/path_params_numeric_validations/tutorial002_py39.py deleted file mode 100644 index 63ac691a83..0000000000 --- a/docs_src/path_params_numeric_validations/tutorial002_py39.py +++ /dev/null @@ -1,11 +0,0 @@ -from fastapi import FastAPI, Path - -app = FastAPI() - - -@app.get("/items/{item_id}") -async def read_items(q: str, item_id: int = Path(title="The ID of the item to get")): - results = {"item_id": item_id} - if q: - results.update({"q": q}) - return results diff --git a/docs_src/path_params_numeric_validations/tutorial003_an_py39.py b/docs_src/path_params_numeric_validations/tutorial003_an_py39.py deleted file mode 100644 index 1588556e78..0000000000 --- a/docs_src/path_params_numeric_validations/tutorial003_an_py39.py +++ /dev/null @@ -1,15 +0,0 @@ -from typing import Annotated - -from fastapi import FastAPI, Path - -app = FastAPI() - - -@app.get("/items/{item_id}") -async def read_items( - item_id: Annotated[int, Path(title="The ID of the item to get")], q: str -): - results = {"item_id": item_id} - if q: - results.update({"q": q}) - return results diff --git a/docs_src/path_params_numeric_validations/tutorial003_py39.py b/docs_src/path_params_numeric_validations/tutorial003_py39.py deleted file mode 100644 index 8df0ffc620..0000000000 --- a/docs_src/path_params_numeric_validations/tutorial003_py39.py +++ /dev/null @@ -1,11 +0,0 @@ -from fastapi import FastAPI, Path - -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): - results = {"item_id": item_id} - if q: - results.update({"q": q}) - return results diff --git a/docs_src/path_params_numeric_validations/tutorial004_an_py39.py b/docs_src/path_params_numeric_validations/tutorial004_an_py39.py deleted file mode 100644 index f67f6450e7..0000000000 --- a/docs_src/path_params_numeric_validations/tutorial004_an_py39.py +++ /dev/null @@ -1,15 +0,0 @@ -from typing import Annotated - -from fastapi import FastAPI, Path - -app = FastAPI() - - -@app.get("/items/{item_id}") -async def read_items( - item_id: Annotated[int, Path(title="The ID of the item to get", ge=1)], q: str -): - results = {"item_id": item_id} - if q: - results.update({"q": q}) - return results diff --git a/docs_src/path_params_numeric_validations/tutorial004_py39.py b/docs_src/path_params_numeric_validations/tutorial004_py39.py deleted file mode 100644 index 86651d47cf..0000000000 --- a/docs_src/path_params_numeric_validations/tutorial004_py39.py +++ /dev/null @@ -1,13 +0,0 @@ -from fastapi import FastAPI, Path - -app = FastAPI() - - -@app.get("/items/{item_id}") -async def read_items( - *, item_id: int = Path(title="The ID of the item to get", ge=1), q: str -): - results = {"item_id": item_id} - if q: - results.update({"q": q}) - return results diff --git a/docs_src/path_params_numeric_validations/tutorial005_an_py39.py b/docs_src/path_params_numeric_validations/tutorial005_an_py39.py deleted file mode 100644 index 571dd583ce..0000000000 --- a/docs_src/path_params_numeric_validations/tutorial005_an_py39.py +++ /dev/null @@ -1,16 +0,0 @@ -from typing import Annotated - -from fastapi import FastAPI, Path - -app = FastAPI() - - -@app.get("/items/{item_id}") -async def read_items( - item_id: Annotated[int, Path(title="The ID of the item to get", gt=0, le=1000)], - q: str, -): - results = {"item_id": item_id} - if q: - results.update({"q": q}) - return results diff --git a/docs_src/path_params_numeric_validations/tutorial005_py39.py b/docs_src/path_params_numeric_validations/tutorial005_py39.py deleted file mode 100644 index 8f12f2da02..0000000000 --- a/docs_src/path_params_numeric_validations/tutorial005_py39.py +++ /dev/null @@ -1,15 +0,0 @@ -from fastapi import FastAPI, Path - -app = FastAPI() - - -@app.get("/items/{item_id}") -async def read_items( - *, - item_id: int = Path(title="The ID of the item to get", gt=0, le=1000), - q: str, -): - results = {"item_id": item_id} - if q: - results.update({"q": q}) - return results diff --git a/docs_src/path_params_numeric_validations/tutorial006_an_py39.py b/docs_src/path_params_numeric_validations/tutorial006_an_py39.py deleted file mode 100644 index 426ec37764..0000000000 --- a/docs_src/path_params_numeric_validations/tutorial006_an_py39.py +++ /dev/null @@ -1,20 +0,0 @@ -from typing import Annotated - -from fastapi import FastAPI, Path, Query - -app = FastAPI() - - -@app.get("/items/{item_id}") -async def read_items( - *, - item_id: Annotated[int, Path(title="The ID of the item to get", ge=0, le=1000)], - q: str, - size: Annotated[float, Query(gt=0, lt=10.5)], -): - results = {"item_id": item_id} - if q: - results.update({"q": q}) - if size: - results.update({"size": size}) - return results diff --git a/docs_src/path_params_numeric_validations/tutorial006_py39.py b/docs_src/path_params_numeric_validations/tutorial006_py39.py deleted file mode 100644 index f07629aa0a..0000000000 --- a/docs_src/path_params_numeric_validations/tutorial006_py39.py +++ /dev/null @@ -1,18 +0,0 @@ -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", ge=0, le=1000), - q: str, - size: float = Query(gt=0, lt=10.5), -): - results = {"item_id": item_id} - if q: - results.update({"q": q}) - if size: - results.update({"size": size}) - return results diff --git a/docs_src/python_types/tutorial001_py39.py b/docs_src/python_types/tutorial001_py39.py deleted file mode 100644 index 09039435f1..0000000000 --- a/docs_src/python_types/tutorial001_py39.py +++ /dev/null @@ -1,6 +0,0 @@ -def get_full_name(first_name, last_name): - full_name = first_name.title() + " " + last_name.title() - return full_name - - -print(get_full_name("john", "doe")) diff --git a/docs_src/python_types/tutorial002_py39.py b/docs_src/python_types/tutorial002_py39.py deleted file mode 100644 index c0857a1160..0000000000 --- a/docs_src/python_types/tutorial002_py39.py +++ /dev/null @@ -1,6 +0,0 @@ -def get_full_name(first_name: str, last_name: str): - full_name = first_name.title() + " " + last_name.title() - return full_name - - -print(get_full_name("john", "doe")) diff --git a/docs_src/python_types/tutorial003_py39.py b/docs_src/python_types/tutorial003_py39.py deleted file mode 100644 index d021d82113..0000000000 --- a/docs_src/python_types/tutorial003_py39.py +++ /dev/null @@ -1,3 +0,0 @@ -def get_name_with_age(name: str, age: int): - name_with_age = name + " is this old: " + age - return name_with_age diff --git a/docs_src/python_types/tutorial004_py39.py b/docs_src/python_types/tutorial004_py39.py deleted file mode 100644 index 9400269e26..0000000000 --- a/docs_src/python_types/tutorial004_py39.py +++ /dev/null @@ -1,3 +0,0 @@ -def get_name_with_age(name: str, age: int): - name_with_age = name + " is this old: " + str(age) - return name_with_age diff --git a/docs_src/python_types/tutorial005_py39.py b/docs_src/python_types/tutorial005_py39.py deleted file mode 100644 index 6c8edb0ec4..0000000000 --- a/docs_src/python_types/tutorial005_py39.py +++ /dev/null @@ -1,2 +0,0 @@ -def get_items(item_a: str, item_b: int, item_c: float, item_d: bool, item_e: bytes): - return item_a, item_b, item_c, item_d, item_e diff --git a/docs_src/python_types/tutorial006_py39.py b/docs_src/python_types/tutorial006_py39.py deleted file mode 100644 index 486b67cafd..0000000000 --- a/docs_src/python_types/tutorial006_py39.py +++ /dev/null @@ -1,3 +0,0 @@ -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 deleted file mode 100644 index ea96c79642..0000000000 --- a/docs_src/python_types/tutorial007_py39.py +++ /dev/null @@ -1,2 +0,0 @@ -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_py39.py b/docs_src/python_types/tutorial008_py39.py deleted file mode 100644 index a393385b05..0000000000 --- a/docs_src/python_types/tutorial008_py39.py +++ /dev/null @@ -1,4 +0,0 @@ -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_py39.py b/docs_src/python_types/tutorial008b_py39.py deleted file mode 100644 index e52539ead0..0000000000 --- a/docs_src/python_types/tutorial008b_py39.py +++ /dev/null @@ -1,5 +0,0 @@ -from typing import Union - - -def process_item(item: Union[int, str]): - print(item) diff --git a/docs_src/python_types/tutorial009_py39.py b/docs_src/python_types/tutorial009_py39.py deleted file mode 100644 index 6328a1495c..0000000000 --- a/docs_src/python_types/tutorial009_py39.py +++ /dev/null @@ -1,8 +0,0 @@ -from typing import Optional - - -def say_hi(name: Optional[str] = None): - if name is not None: - print(f"Hey {name}!") - else: - print("Hello World") diff --git a/docs_src/python_types/tutorial009b_py39.py b/docs_src/python_types/tutorial009b_py39.py deleted file mode 100644 index 9f1a05bc0f..0000000000 --- a/docs_src/python_types/tutorial009b_py39.py +++ /dev/null @@ -1,8 +0,0 @@ -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/tutorial009c_py310.py b/docs_src/python_types/tutorial009c_py310.py deleted file mode 100644 index 96b1220fcc..0000000000 --- a/docs_src/python_types/tutorial009c_py310.py +++ /dev/null @@ -1,2 +0,0 @@ -def say_hi(name: str | None): - print(f"Hey {name}!") diff --git a/docs_src/python_types/tutorial009c_py39.py b/docs_src/python_types/tutorial009c_py39.py deleted file mode 100644 index 2f539a34b1..0000000000 --- a/docs_src/python_types/tutorial009c_py39.py +++ /dev/null @@ -1,5 +0,0 @@ -from typing import Optional - - -def say_hi(name: Optional[str]): - print(f"Hey {name}!") diff --git a/docs_src/python_types/tutorial010_py39.py b/docs_src/python_types/tutorial010_py39.py deleted file mode 100644 index 468cffc2dc..0000000000 --- a/docs_src/python_types/tutorial010_py39.py +++ /dev/null @@ -1,7 +0,0 @@ -class Person: - def __init__(self, name: str): - self.name = name - - -def get_person_name(one_person: Person): - return one_person.name diff --git a/docs_src/python_types/tutorial013_py39.py b/docs_src/python_types/tutorial013_py39.py deleted file mode 100644 index 65a0eaa939..0000000000 --- a/docs_src/python_types/tutorial013_py39.py +++ /dev/null @@ -1,5 +0,0 @@ -from typing import Annotated - - -def say_hello(name: Annotated[str, "this is just metadata"]) -> str: - return f"Hello {name}" diff --git a/docs_src/query_params/tutorial001_py39.py b/docs_src/query_params/tutorial001_py39.py deleted file mode 100644 index 74e1a17604..0000000000 --- a/docs_src/query_params/tutorial001_py39.py +++ /dev/null @@ -1,10 +0,0 @@ -from fastapi import FastAPI - -app = FastAPI() - -fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}] - - -@app.get("/items/") -async def read_item(skip: int = 0, limit: int = 10): - return fake_items_db[skip : skip + limit] diff --git a/docs_src/query_params/tutorial005_py39.py b/docs_src/query_params/tutorial005_py39.py deleted file mode 100644 index e16a40574d..0000000000 --- a/docs_src/query_params/tutorial005_py39.py +++ /dev/null @@ -1,9 +0,0 @@ -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/items/{item_id}") -async def read_user_item(item_id: str, needy: str): - item = {"item_id": item_id, "needy": needy} - return item diff --git a/docs_src/query_params_str_validations/tutorial005_an_py39.py b/docs_src/query_params_str_validations/tutorial005_an_py39.py deleted file mode 100644 index b1f6046b50..0000000000 --- a/docs_src/query_params_str_validations/tutorial005_an_py39.py +++ /dev/null @@ -1,13 +0,0 @@ -from typing import Annotated - -from fastapi import FastAPI, Query - -app = FastAPI() - - -@app.get("/items/") -async def read_items(q: Annotated[str, Query(min_length=3)] = "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/tutorial005_py39.py b/docs_src/query_params_str_validations/tutorial005_py39.py deleted file mode 100644 index 8ab42869e6..0000000000 --- a/docs_src/query_params_str_validations/tutorial005_py39.py +++ /dev/null @@ -1,11 +0,0 @@ -from fastapi import FastAPI, Query - -app = FastAPI() - - -@app.get("/items/") -async def read_items(q: str = Query(default="fixedquery", 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/tutorial006_an_py39.py b/docs_src/query_params_str_validations/tutorial006_an_py39.py deleted file mode 100644 index 3b4a676d2e..0000000000 --- a/docs_src/query_params_str_validations/tutorial006_an_py39.py +++ /dev/null @@ -1,13 +0,0 @@ -from typing import Annotated - -from fastapi import FastAPI, Query - -app = FastAPI() - - -@app.get("/items/") -async def read_items(q: Annotated[str, Query(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/tutorial006_py39.py b/docs_src/query_params_str_validations/tutorial006_py39.py deleted file mode 100644 index 9a90eb64ef..0000000000 --- a/docs_src/query_params_str_validations/tutorial006_py39.py +++ /dev/null @@ -1,11 +0,0 @@ -from fastapi import FastAPI, Query - -app = FastAPI() - - -@app.get("/items/") -async def read_items(q: str = Query(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/tutorial012_an_py39.py b/docs_src/query_params_str_validations/tutorial012_an_py39.py deleted file mode 100644 index 9b5a9c2fb2..0000000000 --- a/docs_src/query_params_str_validations/tutorial012_an_py39.py +++ /dev/null @@ -1,11 +0,0 @@ -from typing import Annotated - -from fastapi import FastAPI, Query - -app = FastAPI() - - -@app.get("/items/") -async def read_items(q: Annotated[list[str], Query()] = ["foo", "bar"]): - 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 deleted file mode 100644 index 070d0b04bf..0000000000 --- a/docs_src/query_params_str_validations/tutorial012_py39.py +++ /dev/null @@ -1,9 +0,0 @@ -from fastapi import FastAPI, Query - -app = FastAPI() - - -@app.get("/items/") -async def read_items(q: list[str] = Query(default=["foo", "bar"])): - query_items = {"q": q} - return query_items diff --git a/docs_src/query_params_str_validations/tutorial013_an_py39.py b/docs_src/query_params_str_validations/tutorial013_an_py39.py deleted file mode 100644 index 602734145d..0000000000 --- a/docs_src/query_params_str_validations/tutorial013_an_py39.py +++ /dev/null @@ -1,11 +0,0 @@ -from typing import Annotated - -from fastapi import FastAPI, Query - -app = FastAPI() - - -@app.get("/items/") -async def read_items(q: Annotated[list, Query()] = []): - query_items = {"q": q} - return query_items diff --git a/docs_src/query_params_str_validations/tutorial013_py39.py b/docs_src/query_params_str_validations/tutorial013_py39.py deleted file mode 100644 index 0b0f44869f..0000000000 --- a/docs_src/query_params_str_validations/tutorial013_py39.py +++ /dev/null @@ -1,9 +0,0 @@ -from fastapi import FastAPI, Query - -app = FastAPI() - - -@app.get("/items/") -async def read_items(q: list = Query(default=[])): - query_items = {"q": q} - return query_items diff --git a/docs_src/request_files/tutorial001_03_an_py39.py b/docs_src/request_files/tutorial001_03_an_py39.py deleted file mode 100644 index 93098a677a..0000000000 --- a/docs_src/request_files/tutorial001_03_an_py39.py +++ /dev/null @@ -1,17 +0,0 @@ -from typing import Annotated - -from fastapi import FastAPI, File, UploadFile - -app = FastAPI() - - -@app.post("/files/") -async def create_file(file: Annotated[bytes, File(description="A file read as bytes")]): - return {"file_size": len(file)} - - -@app.post("/uploadfile/") -async def create_upload_file( - file: Annotated[UploadFile, File(description="A file read as UploadFile")], -): - return {"filename": file.filename} diff --git a/docs_src/request_files/tutorial001_03_py39.py b/docs_src/request_files/tutorial001_03_py39.py deleted file mode 100644 index d8005cc7d2..0000000000 --- a/docs_src/request_files/tutorial001_03_py39.py +++ /dev/null @@ -1,15 +0,0 @@ -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/tutorial001_an_py39.py b/docs_src/request_files/tutorial001_an_py39.py deleted file mode 100644 index 26a7672216..0000000000 --- a/docs_src/request_files/tutorial001_an_py39.py +++ /dev/null @@ -1,15 +0,0 @@ -from typing import Annotated - -from fastapi import FastAPI, File, UploadFile - -app = FastAPI() - - -@app.post("/files/") -async def create_file(file: Annotated[bytes, File()]): - return {"file_size": len(file)} - - -@app.post("/uploadfile/") -async def create_upload_file(file: UploadFile): - return {"filename": file.filename} diff --git a/docs_src/request_files/tutorial001_py39.py b/docs_src/request_files/tutorial001_py39.py deleted file mode 100644 index 2e0ea63912..0000000000 --- a/docs_src/request_files/tutorial001_py39.py +++ /dev/null @@ -1,13 +0,0 @@ -from fastapi import FastAPI, File, UploadFile - -app = FastAPI() - - -@app.post("/files/") -async def create_file(file: bytes = File()): - return {"file_size": len(file)} - - -@app.post("/uploadfile/") -async def create_upload_file(file: UploadFile): - return {"filename": file.filename} diff --git a/docs_src/request_files/tutorial002_an_py39.py b/docs_src/request_files/tutorial002_an_py39.py deleted file mode 100644 index db524ceab6..0000000000 --- a/docs_src/request_files/tutorial002_an_py39.py +++ /dev/null @@ -1,33 +0,0 @@ -from typing import Annotated - -from fastapi import FastAPI, File, UploadFile -from fastapi.responses import HTMLResponse - -app = FastAPI() - - -@app.post("/files/") -async def create_files(files: Annotated[list[bytes], File()]): - return {"file_sizes": [len(file) for file in files]} - - -@app.post("/uploadfiles/") -async def create_upload_files(files: list[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/tutorial002_py39.py b/docs_src/request_files/tutorial002_py39.py deleted file mode 100644 index b64cf55987..0000000000 --- a/docs_src/request_files/tutorial002_py39.py +++ /dev/null @@ -1,31 +0,0 @@ -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]): - 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_an_py39.py b/docs_src/request_files/tutorial003_an_py39.py deleted file mode 100644 index 5a8c5dab5c..0000000000 --- a/docs_src/request_files/tutorial003_an_py39.py +++ /dev/null @@ -1,39 +0,0 @@ -from typing import Annotated - -from fastapi import FastAPI, File, UploadFile -from fastapi.responses import HTMLResponse - -app = FastAPI() - - -@app.post("/files/") -async def create_files( - files: Annotated[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: Annotated[ - 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 deleted file mode 100644 index 96f5e8742d..0000000000 --- a/docs_src/request_files/tutorial003_py39.py +++ /dev/null @@ -1,35 +0,0 @@ -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_form_models/tutorial001_an_py39.py b/docs_src/request_form_models/tutorial001_an_py39.py deleted file mode 100644 index 7cc81aae95..0000000000 --- a/docs_src/request_form_models/tutorial001_an_py39.py +++ /dev/null @@ -1,16 +0,0 @@ -from typing import Annotated - -from fastapi import FastAPI, Form -from pydantic import BaseModel - -app = FastAPI() - - -class FormData(BaseModel): - username: str - password: str - - -@app.post("/login/") -async def login(data: Annotated[FormData, Form()]): - return data diff --git a/docs_src/request_form_models/tutorial001_py39.py b/docs_src/request_form_models/tutorial001_py39.py deleted file mode 100644 index 98feff0b9f..0000000000 --- a/docs_src/request_form_models/tutorial001_py39.py +++ /dev/null @@ -1,14 +0,0 @@ -from fastapi import FastAPI, Form -from pydantic import BaseModel - -app = FastAPI() - - -class FormData(BaseModel): - username: str - password: str - - -@app.post("/login/") -async def login(data: FormData = Form()): - return data diff --git a/docs_src/request_form_models/tutorial002_an_py39.py b/docs_src/request_form_models/tutorial002_an_py39.py deleted file mode 100644 index 3004e08524..0000000000 --- a/docs_src/request_form_models/tutorial002_an_py39.py +++ /dev/null @@ -1,17 +0,0 @@ -from typing import Annotated - -from fastapi import FastAPI, Form -from pydantic import BaseModel - -app = FastAPI() - - -class FormData(BaseModel): - username: str - password: str - model_config = {"extra": "forbid"} - - -@app.post("/login/") -async def login(data: Annotated[FormData, Form()]): - return data diff --git a/docs_src/request_form_models/tutorial002_py39.py b/docs_src/request_form_models/tutorial002_py39.py deleted file mode 100644 index 59b329e8d8..0000000000 --- a/docs_src/request_form_models/tutorial002_py39.py +++ /dev/null @@ -1,15 +0,0 @@ -from fastapi import FastAPI, Form -from pydantic import BaseModel - -app = FastAPI() - - -class FormData(BaseModel): - username: str - password: str - model_config = {"extra": "forbid"} - - -@app.post("/login/") -async def login(data: FormData = Form()): - return data diff --git a/docs_src/request_forms/tutorial001_an_py39.py b/docs_src/request_forms/tutorial001_an_py39.py deleted file mode 100644 index 8e9d2ea53a..0000000000 --- a/docs_src/request_forms/tutorial001_an_py39.py +++ /dev/null @@ -1,10 +0,0 @@ -from typing import Annotated - -from fastapi import FastAPI, Form - -app = FastAPI() - - -@app.post("/login/") -async def login(username: Annotated[str, Form()], password: Annotated[str, Form()]): - return {"username": username} diff --git a/docs_src/request_forms/tutorial001_py39.py b/docs_src/request_forms/tutorial001_py39.py deleted file mode 100644 index a537700019..0000000000 --- a/docs_src/request_forms/tutorial001_py39.py +++ /dev/null @@ -1,8 +0,0 @@ -from fastapi import FastAPI, Form - -app = FastAPI() - - -@app.post("/login/") -async def login(username: str = Form(), password: str = Form()): - return {"username": username} diff --git a/docs_src/request_forms_and_files/tutorial001_an_py39.py b/docs_src/request_forms_and_files/tutorial001_an_py39.py deleted file mode 100644 index 12cc43e50a..0000000000 --- a/docs_src/request_forms_and_files/tutorial001_an_py39.py +++ /dev/null @@ -1,18 +0,0 @@ -from typing import Annotated - -from fastapi import FastAPI, File, Form, UploadFile - -app = FastAPI() - - -@app.post("/files/") -async def create_file( - file: Annotated[bytes, File()], - fileb: Annotated[UploadFile, File()], - token: Annotated[str, Form()], -): - return { - "file_size": len(file), - "token": token, - "fileb_content_type": fileb.content_type, - } diff --git a/docs_src/request_forms_and_files/tutorial001_py39.py b/docs_src/request_forms_and_files/tutorial001_py39.py deleted file mode 100644 index 7b5224ce53..0000000000 --- a/docs_src/request_forms_and_files/tutorial001_py39.py +++ /dev/null @@ -1,14 +0,0 @@ -from fastapi import FastAPI, File, Form, UploadFile - -app = FastAPI() - - -@app.post("/files/") -async def create_file( - file: bytes = File(), fileb: UploadFile = File(), token: str = Form() -): - return { - "file_size": len(file), - "token": token, - "fileb_content_type": fileb.content_type, - } diff --git a/docs_src/response_change_status_code/tutorial001_py39.py b/docs_src/response_change_status_code/tutorial001_py39.py deleted file mode 100644 index 197decbfbf..0000000000 --- a/docs_src/response_change_status_code/tutorial001_py39.py +++ /dev/null @@ -1,13 +0,0 @@ -from fastapi import FastAPI, Response, status - -app = FastAPI() - -tasks = {"foo": "Listen to the Bar Fighters"} - - -@app.put("/get-or-create-task/{task_id}", status_code=200) -def get_or_create_task(task_id: str, response: Response): - if task_id not in tasks: - tasks[task_id] = "This didn't exist before" - response.status_code = status.HTTP_201_CREATED - return tasks[task_id] diff --git a/docs_src/response_cookies/tutorial001_py39.py b/docs_src/response_cookies/tutorial001_py39.py deleted file mode 100644 index 33f8e8f6e9..0000000000 --- a/docs_src/response_cookies/tutorial001_py39.py +++ /dev/null @@ -1,12 +0,0 @@ -from fastapi import FastAPI -from fastapi.responses import JSONResponse - -app = FastAPI() - - -@app.post("/cookie/") -def create_cookie(): - content = {"message": "Come to the dark side, we have cookies"} - response = JSONResponse(content=content) - response.set_cookie(key="fakesession", value="fake-cookie-session-value") - return response diff --git a/docs_src/response_cookies/tutorial002_py39.py b/docs_src/response_cookies/tutorial002_py39.py deleted file mode 100644 index 76c06fdb9f..0000000000 --- a/docs_src/response_cookies/tutorial002_py39.py +++ /dev/null @@ -1,9 +0,0 @@ -from fastapi import FastAPI, Response - -app = FastAPI() - - -@app.post("/cookie-and-object/") -def create_cookie(response: Response): - response.set_cookie(key="fakesession", value="fake-cookie-session-value") - return {"message": "Come to the dark side, we have cookies"} diff --git a/docs_src/response_directly/tutorial002_py39.py b/docs_src/response_directly/tutorial002_py39.py deleted file mode 100644 index 6643da6e6d..0000000000 --- a/docs_src/response_directly/tutorial002_py39.py +++ /dev/null @@ -1,18 +0,0 @@ -from fastapi import FastAPI, Response - -app = FastAPI() - - -@app.get("/legacy/") -def get_legacy_data(): - data = """ - -
- Apply shampoo here. -
- - You'll have to use soap here. - -
- """ - return Response(content=data, media_type="application/xml") diff --git a/docs_src/response_headers/tutorial001_py39.py b/docs_src/response_headers/tutorial001_py39.py deleted file mode 100644 index 2da02a470b..0000000000 --- a/docs_src/response_headers/tutorial001_py39.py +++ /dev/null @@ -1,11 +0,0 @@ -from fastapi import FastAPI -from fastapi.responses import JSONResponse - -app = FastAPI() - - -@app.get("/headers/") -def get_headers(): - content = {"message": "Hello World"} - headers = {"X-Cat-Dog": "alone in the world", "Content-Language": "en-US"} - return JSONResponse(content=content, headers=headers) diff --git a/docs_src/response_headers/tutorial002_py39.py b/docs_src/response_headers/tutorial002_py39.py deleted file mode 100644 index d2c4983051..0000000000 --- a/docs_src/response_headers/tutorial002_py39.py +++ /dev/null @@ -1,9 +0,0 @@ -from fastapi import FastAPI, Response - -app = FastAPI() - - -@app.get("/headers-and-object/") -def get_headers(response: Response): - response.headers["X-Cat-Dog"] = "alone in the world" - return {"message": "Hello World"} diff --git a/docs_src/response_model/tutorial003_02_py39.py b/docs_src/response_model/tutorial003_02_py39.py deleted file mode 100644 index df6a09646d..0000000000 --- a/docs_src/response_model/tutorial003_02_py39.py +++ /dev/null @@ -1,11 +0,0 @@ -from fastapi import FastAPI, Response -from fastapi.responses import JSONResponse, RedirectResponse - -app = FastAPI() - - -@app.get("/portal") -async def get_portal(teleport: bool = False) -> Response: - if teleport: - return RedirectResponse(url="https://www.youtube.com/watch?v=dQw4w9WgXcQ") - return JSONResponse(content={"message": "Here's your interdimensional portal."}) diff --git a/docs_src/response_model/tutorial003_03_py39.py b/docs_src/response_model/tutorial003_03_py39.py deleted file mode 100644 index 0d4bd8de57..0000000000 --- a/docs_src/response_model/tutorial003_03_py39.py +++ /dev/null @@ -1,9 +0,0 @@ -from fastapi import FastAPI -from fastapi.responses import RedirectResponse - -app = FastAPI() - - -@app.get("/teleport") -async def get_teleport() -> RedirectResponse: - return RedirectResponse(url="https://www.youtube.com/watch?v=dQw4w9WgXcQ") diff --git a/docs_src/response_status_code/tutorial001_py39.py b/docs_src/response_status_code/tutorial001_py39.py deleted file mode 100644 index 14b6d6e675..0000000000 --- a/docs_src/response_status_code/tutorial001_py39.py +++ /dev/null @@ -1,8 +0,0 @@ -from fastapi import FastAPI - -app = FastAPI() - - -@app.post("/items/", status_code=201) -async def create_item(name: str): - return {"name": name} diff --git a/docs_src/response_status_code/tutorial002_py39.py b/docs_src/response_status_code/tutorial002_py39.py deleted file mode 100644 index 4fcc9829db..0000000000 --- a/docs_src/response_status_code/tutorial002_py39.py +++ /dev/null @@ -1,8 +0,0 @@ -from fastapi import FastAPI, status - -app = FastAPI() - - -@app.post("/items/", status_code=status.HTTP_201_CREATED) -async def create_item(name: str): - return {"name": name} diff --git a/docs_src/security/tutorial001_an_py39.py b/docs_src/security/tutorial001_an_py39.py deleted file mode 100644 index de110402ef..0000000000 --- a/docs_src/security/tutorial001_an_py39.py +++ /dev/null @@ -1,13 +0,0 @@ -from typing import Annotated - -from fastapi import Depends, FastAPI -from fastapi.security import OAuth2PasswordBearer - -app = FastAPI() - -oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") - - -@app.get("/items/") -async def read_items(token: Annotated[str, Depends(oauth2_scheme)]): - return {"token": token} diff --git a/docs_src/security/tutorial001_py39.py b/docs_src/security/tutorial001_py39.py deleted file mode 100644 index 224e59602e..0000000000 --- a/docs_src/security/tutorial001_py39.py +++ /dev/null @@ -1,11 +0,0 @@ -from fastapi import Depends, FastAPI -from fastapi.security import OAuth2PasswordBearer - -app = FastAPI() - -oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") - - -@app.get("/items/") -async def read_items(token: str = Depends(oauth2_scheme)): - return {"token": token} diff --git a/docs_src/security/tutorial006_an_py39.py b/docs_src/security/tutorial006_an_py39.py deleted file mode 100644 index 03c696a4b6..0000000000 --- a/docs_src/security/tutorial006_an_py39.py +++ /dev/null @@ -1,13 +0,0 @@ -from typing import Annotated - -from fastapi import Depends, FastAPI -from fastapi.security import HTTPBasic, HTTPBasicCredentials - -app = FastAPI() - -security = HTTPBasic() - - -@app.get("/users/me") -def read_current_user(credentials: Annotated[HTTPBasicCredentials, Depends(security)]): - return {"username": credentials.username, "password": credentials.password} diff --git a/docs_src/security/tutorial006_py39.py b/docs_src/security/tutorial006_py39.py deleted file mode 100644 index 29121ffd63..0000000000 --- a/docs_src/security/tutorial006_py39.py +++ /dev/null @@ -1,11 +0,0 @@ -from fastapi import Depends, FastAPI -from fastapi.security import HTTPBasic, HTTPBasicCredentials - -app = FastAPI() - -security = HTTPBasic() - - -@app.get("/users/me") -def read_current_user(credentials: HTTPBasicCredentials = Depends(security)): - return {"username": credentials.username, "password": credentials.password} diff --git a/docs_src/security/tutorial007_an_py39.py b/docs_src/security/tutorial007_an_py39.py deleted file mode 100644 index 87ef986574..0000000000 --- a/docs_src/security/tutorial007_an_py39.py +++ /dev/null @@ -1,36 +0,0 @@ -import secrets -from typing import Annotated - -from fastapi import Depends, FastAPI, HTTPException, status -from fastapi.security import HTTPBasic, HTTPBasicCredentials - -app = FastAPI() - -security = HTTPBasic() - - -def get_current_username( - credentials: Annotated[HTTPBasicCredentials, Depends(security)], -): - current_username_bytes = credentials.username.encode("utf8") - correct_username_bytes = b"stanleyjobson" - is_correct_username = secrets.compare_digest( - current_username_bytes, correct_username_bytes - ) - current_password_bytes = credentials.password.encode("utf8") - correct_password_bytes = b"swordfish" - is_correct_password = secrets.compare_digest( - current_password_bytes, correct_password_bytes - ) - if not (is_correct_username and is_correct_password): - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Incorrect username or password", - headers={"WWW-Authenticate": "Basic"}, - ) - return credentials.username - - -@app.get("/users/me") -def read_current_user(username: Annotated[str, Depends(get_current_username)]): - return {"username": username} diff --git a/docs_src/security/tutorial007_py39.py b/docs_src/security/tutorial007_py39.py deleted file mode 100644 index ac816eb0c1..0000000000 --- a/docs_src/security/tutorial007_py39.py +++ /dev/null @@ -1,33 +0,0 @@ -import secrets - -from fastapi import Depends, FastAPI, HTTPException, status -from fastapi.security import HTTPBasic, HTTPBasicCredentials - -app = FastAPI() - -security = HTTPBasic() - - -def get_current_username(credentials: HTTPBasicCredentials = Depends(security)): - current_username_bytes = credentials.username.encode("utf8") - correct_username_bytes = b"stanleyjobson" - is_correct_username = secrets.compare_digest( - current_username_bytes, correct_username_bytes - ) - current_password_bytes = credentials.password.encode("utf8") - correct_password_bytes = b"swordfish" - is_correct_password = secrets.compare_digest( - current_password_bytes, correct_password_bytes - ) - if not (is_correct_username and is_correct_password): - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Incorrect username or password", - headers={"WWW-Authenticate": "Basic"}, - ) - return credentials.username - - -@app.get("/users/me") -def read_current_user(username: str = Depends(get_current_username)): - return {"username": username} diff --git a/docs_src/settings/app01_py39/__init__.py b/docs_src/settings/app01_py39/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/docs_src/settings/app01_py39/config.py b/docs_src/settings/app01_py39/config.py deleted file mode 100644 index b31b8811d6..0000000000 --- a/docs_src/settings/app01_py39/config.py +++ /dev/null @@ -1,10 +0,0 @@ -from pydantic_settings import BaseSettings - - -class Settings(BaseSettings): - app_name: str = "Awesome API" - admin_email: str - items_per_user: int = 50 - - -settings = Settings() diff --git a/docs_src/settings/app01_py39/main.py b/docs_src/settings/app01_py39/main.py deleted file mode 100644 index 4a3a86ce20..0000000000 --- a/docs_src/settings/app01_py39/main.py +++ /dev/null @@ -1,14 +0,0 @@ -from fastapi import FastAPI - -from .config import settings - -app = FastAPI() - - -@app.get("/info") -async def info(): - return { - "app_name": settings.app_name, - "admin_email": settings.admin_email, - "items_per_user": settings.items_per_user, - } diff --git a/docs_src/settings/app02_an_py39/__init__.py b/docs_src/settings/app02_an_py39/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/docs_src/settings/app02_an_py39/config.py b/docs_src/settings/app02_an_py39/config.py deleted file mode 100644 index e17b5035dc..0000000000 --- a/docs_src/settings/app02_an_py39/config.py +++ /dev/null @@ -1,7 +0,0 @@ -from pydantic_settings import BaseSettings - - -class Settings(BaseSettings): - app_name: str = "Awesome API" - admin_email: str - items_per_user: int = 50 diff --git a/docs_src/settings/app02_an_py39/main.py b/docs_src/settings/app02_an_py39/main.py deleted file mode 100644 index 6d5db12a87..0000000000 --- a/docs_src/settings/app02_an_py39/main.py +++ /dev/null @@ -1,22 +0,0 @@ -from functools import lru_cache -from typing import Annotated - -from fastapi import Depends, FastAPI - -from .config import Settings - -app = FastAPI() - - -@lru_cache -def get_settings(): - return Settings() - - -@app.get("/info") -async def info(settings: Annotated[Settings, Depends(get_settings)]): - return { - "app_name": settings.app_name, - "admin_email": settings.admin_email, - "items_per_user": settings.items_per_user, - } diff --git a/docs_src/settings/app02_an_py39/test_main.py b/docs_src/settings/app02_an_py39/test_main.py deleted file mode 100644 index 7a04d7e8ee..0000000000 --- a/docs_src/settings/app02_an_py39/test_main.py +++ /dev/null @@ -1,23 +0,0 @@ -from fastapi.testclient import TestClient - -from .config import Settings -from .main import app, get_settings - -client = TestClient(app) - - -def get_settings_override(): - return Settings(admin_email="testing_admin@example.com") - - -app.dependency_overrides[get_settings] = get_settings_override - - -def test_app(): - response = client.get("/info") - data = response.json() - assert data == { - "app_name": "Awesome API", - "admin_email": "testing_admin@example.com", - "items_per_user": 50, - } diff --git a/docs_src/settings/app02_py39/__init__.py b/docs_src/settings/app02_py39/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/docs_src/settings/app02_py39/config.py b/docs_src/settings/app02_py39/config.py deleted file mode 100644 index e17b5035dc..0000000000 --- a/docs_src/settings/app02_py39/config.py +++ /dev/null @@ -1,7 +0,0 @@ -from pydantic_settings import BaseSettings - - -class Settings(BaseSettings): - app_name: str = "Awesome API" - admin_email: str - items_per_user: int = 50 diff --git a/docs_src/settings/app02_py39/main.py b/docs_src/settings/app02_py39/main.py deleted file mode 100644 index 941f82e6b3..0000000000 --- a/docs_src/settings/app02_py39/main.py +++ /dev/null @@ -1,21 +0,0 @@ -from functools import lru_cache - -from fastapi import Depends, FastAPI - -from .config import Settings - -app = FastAPI() - - -@lru_cache -def get_settings(): - return Settings() - - -@app.get("/info") -async def info(settings: Settings = Depends(get_settings)): - return { - "app_name": settings.app_name, - "admin_email": settings.admin_email, - "items_per_user": settings.items_per_user, - } diff --git a/docs_src/settings/app02_py39/test_main.py b/docs_src/settings/app02_py39/test_main.py deleted file mode 100644 index 7a04d7e8ee..0000000000 --- a/docs_src/settings/app02_py39/test_main.py +++ /dev/null @@ -1,23 +0,0 @@ -from fastapi.testclient import TestClient - -from .config import Settings -from .main import app, get_settings - -client = TestClient(app) - - -def get_settings_override(): - return Settings(admin_email="testing_admin@example.com") - - -app.dependency_overrides[get_settings] = get_settings_override - - -def test_app(): - response = client.get("/info") - data = response.json() - assert data == { - "app_name": "Awesome API", - "admin_email": "testing_admin@example.com", - "items_per_user": 50, - } diff --git a/docs_src/settings/app03_an_py39/__init__.py b/docs_src/settings/app03_an_py39/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/docs_src/settings/app03_an_py39/config.py b/docs_src/settings/app03_an_py39/config.py deleted file mode 100644 index 08f8f88c28..0000000000 --- a/docs_src/settings/app03_an_py39/config.py +++ /dev/null @@ -1,9 +0,0 @@ -from pydantic_settings import BaseSettings, SettingsConfigDict - - -class Settings(BaseSettings): - app_name: str = "Awesome API" - admin_email: str - items_per_user: int = 50 - - model_config = SettingsConfigDict(env_file=".env") diff --git a/docs_src/settings/app03_an_py39/main.py b/docs_src/settings/app03_an_py39/main.py deleted file mode 100644 index 2f64b9cd17..0000000000 --- a/docs_src/settings/app03_an_py39/main.py +++ /dev/null @@ -1,22 +0,0 @@ -from functools import lru_cache -from typing import Annotated - -from fastapi import Depends, FastAPI - -from . import config - -app = FastAPI() - - -@lru_cache -def get_settings(): - return config.Settings() - - -@app.get("/info") -async def info(settings: Annotated[config.Settings, Depends(get_settings)]): - return { - "app_name": settings.app_name, - "admin_email": settings.admin_email, - "items_per_user": settings.items_per_user, - } diff --git a/docs_src/settings/app03_py39/__init__.py b/docs_src/settings/app03_py39/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/docs_src/settings/app03_py39/config.py b/docs_src/settings/app03_py39/config.py deleted file mode 100644 index 08f8f88c28..0000000000 --- a/docs_src/settings/app03_py39/config.py +++ /dev/null @@ -1,9 +0,0 @@ -from pydantic_settings import BaseSettings, SettingsConfigDict - - -class Settings(BaseSettings): - app_name: str = "Awesome API" - admin_email: str - items_per_user: int = 50 - - model_config = SettingsConfigDict(env_file=".env") diff --git a/docs_src/settings/app03_py39/main.py b/docs_src/settings/app03_py39/main.py deleted file mode 100644 index ea64a5709c..0000000000 --- a/docs_src/settings/app03_py39/main.py +++ /dev/null @@ -1,21 +0,0 @@ -from functools import lru_cache - -from fastapi import Depends, FastAPI - -from . import config - -app = FastAPI() - - -@lru_cache -def get_settings(): - return config.Settings() - - -@app.get("/info") -async def info(settings: config.Settings = Depends(get_settings)): - return { - "app_name": settings.app_name, - "admin_email": settings.admin_email, - "items_per_user": settings.items_per_user, - } diff --git a/docs_src/settings/tutorial001_py39.py b/docs_src/settings/tutorial001_py39.py deleted file mode 100644 index d48c4c060c..0000000000 --- a/docs_src/settings/tutorial001_py39.py +++ /dev/null @@ -1,21 +0,0 @@ -from fastapi import FastAPI -from pydantic_settings import BaseSettings - - -class Settings(BaseSettings): - app_name: str = "Awesome API" - admin_email: str - items_per_user: int = 50 - - -settings = Settings() -app = FastAPI() - - -@app.get("/info") -async def info(): - return { - "app_name": settings.app_name, - "admin_email": settings.admin_email, - "items_per_user": settings.items_per_user, - } diff --git a/docs_src/static_files/tutorial001_py39.py b/docs_src/static_files/tutorial001_py39.py deleted file mode 100644 index 460352c7e1..0000000000 --- a/docs_src/static_files/tutorial001_py39.py +++ /dev/null @@ -1,6 +0,0 @@ -from fastapi import FastAPI -from fastapi.staticfiles import StaticFiles - -app = FastAPI() - -app.mount("/static", StaticFiles(directory="static"), name="static") diff --git a/docs_src/async_tests/app_a_py39/__init__.py b/docs_src/strict_content_type/__init__.py similarity index 100% rename from docs_src/async_tests/app_a_py39/__init__.py rename to docs_src/strict_content_type/__init__.py diff --git a/docs_src/strict_content_type/tutorial001_py310.py b/docs_src/strict_content_type/tutorial001_py310.py new file mode 100644 index 0000000000..a44f4b1386 --- /dev/null +++ b/docs_src/strict_content_type/tutorial001_py310.py @@ -0,0 +1,14 @@ +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI(strict_content_type=False) + + +class Item(BaseModel): + name: str + price: float + + +@app.post("/items/") +async def create_item(item: Item): + return item diff --git a/docs_src/sub_applications/tutorial001_py39.py b/docs_src/sub_applications/tutorial001_py39.py deleted file mode 100644 index 57e627e804..0000000000 --- a/docs_src/sub_applications/tutorial001_py39.py +++ /dev/null @@ -1,19 +0,0 @@ -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/app") -def read_main(): - return {"message": "Hello World from main app"} - - -subapi = FastAPI() - - -@subapi.get("/sub") -def read_sub(): - return {"message": "Hello World from sub API"} - - -app.mount("/subapi", subapi) diff --git a/docs_src/templates/tutorial001_py39.py b/docs_src/templates/tutorial001_py39.py deleted file mode 100644 index 81ccc8d4d0..0000000000 --- a/docs_src/templates/tutorial001_py39.py +++ /dev/null @@ -1,18 +0,0 @@ -from fastapi import FastAPI, Request -from fastapi.responses import HTMLResponse -from fastapi.staticfiles import StaticFiles -from fastapi.templating import Jinja2Templates - -app = FastAPI() - -app.mount("/static", StaticFiles(directory="static"), name="static") - - -templates = Jinja2Templates(directory="templates") - - -@app.get("/items/{id}", response_class=HTMLResponse) -async def read_item(request: Request, id: str): - return templates.TemplateResponse( - request=request, name="item.html", context={"id": id} - ) diff --git a/docs_src/using_request_directly/tutorial001_py39.py b/docs_src/using_request_directly/tutorial001_py39.py deleted file mode 100644 index 2d7288b54d..0000000000 --- a/docs_src/using_request_directly/tutorial001_py39.py +++ /dev/null @@ -1,9 +0,0 @@ -from fastapi import FastAPI, Request - -app = FastAPI() - - -@app.get("/items/{item_id}") -def read_root(item_id: str, request: Request): - client_host = request.client.host - return {"client_host": client_host, "item_id": item_id} diff --git a/docs_src/websockets/tutorial001_py39.py b/docs_src/websockets/tutorial001_py39.py deleted file mode 100644 index a43a2be17c..0000000000 --- a/docs_src/websockets/tutorial001_py39.py +++ /dev/null @@ -1,51 +0,0 @@ -from fastapi import FastAPI, WebSocket -from fastapi.responses import HTMLResponse - -app = FastAPI() - -html = """ - - - - Chat - - -

WebSocket Chat

-
- - -
- - - - -""" - - -@app.get("/") -async def get(): - return HTMLResponse(html) - - -@app.websocket("/ws") -async def websocket_endpoint(websocket: WebSocket): - await websocket.accept() - while True: - data = await websocket.receive_text() - await websocket.send_text(f"Message text was: {data}") diff --git a/docs_src/websockets/tutorial003_py39.py b/docs_src/websockets/tutorial003_py39.py deleted file mode 100644 index 3162180889..0000000000 --- a/docs_src/websockets/tutorial003_py39.py +++ /dev/null @@ -1,81 +0,0 @@ -from fastapi import FastAPI, WebSocket, WebSocketDisconnect -from fastapi.responses import HTMLResponse - -app = FastAPI() - -html = """ - - - - Chat - - -

WebSocket Chat

-

Your ID:

-
- - -
- - - - -""" - - -class ConnectionManager: - def __init__(self): - self.active_connections: list[WebSocket] = [] - - async def connect(self, websocket: WebSocket): - await websocket.accept() - self.active_connections.append(websocket) - - def disconnect(self, websocket: WebSocket): - self.active_connections.remove(websocket) - - async def send_personal_message(self, message: str, websocket: WebSocket): - await websocket.send_text(message) - - async def broadcast(self, message: str): - for connection in self.active_connections: - await connection.send_text(message) - - -manager = ConnectionManager() - - -@app.get("/") -async def get(): - return HTMLResponse(html) - - -@app.websocket("/ws/{client_id}") -async def websocket_endpoint(websocket: WebSocket, client_id: int): - await manager.connect(websocket) - try: - while True: - data = await websocket.receive_text() - await manager.send_personal_message(f"You wrote: {data}", websocket) - await manager.broadcast(f"Client #{client_id} says: {data}") - except WebSocketDisconnect: - manager.disconnect(websocket) - await manager.broadcast(f"Client #{client_id} left the chat") diff --git a/docs_src/wsgi/tutorial001_py39.py b/docs_src/wsgi/tutorial001_py39.py deleted file mode 100644 index 8eeceb829e..0000000000 --- a/docs_src/wsgi/tutorial001_py39.py +++ /dev/null @@ -1,23 +0,0 @@ -from a2wsgi import WSGIMiddleware -from fastapi import FastAPI -from flask import Flask, request -from markupsafe import escape - -flask_app = Flask(__name__) - - -@flask_app.route("/") -def flask_main(): - name = request.args.get("name", "World") - return f"Hello, {escape(name)} from Flask!" - - -app = FastAPI() - - -@app.get("/v2") -def read_main(): - return {"message": "Hello World"} - - -app.mount("/v1", WSGIMiddleware(flask_app)) diff --git a/fastapi/__init__.py b/fastapi/__init__.py index de5a0be382..d936bb7df3 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.129.0" +__version__ = "0.132.0" from starlette import status as status diff --git a/fastapi/_compat/v2.py b/fastapi/_compat/v2.py index b83bc1b55b..79fba93188 100644 --- a/fastapi/_compat/v2.py +++ b/fastapi/_compat/v2.py @@ -27,7 +27,7 @@ from pydantic._internal._schema_generation_shared import ( # type: ignore[attr- ) from pydantic._internal._typing_extra import eval_type_lenient from pydantic.fields import FieldInfo as FieldInfo -from pydantic.json_schema import GenerateJsonSchema as GenerateJsonSchema +from pydantic.json_schema import GenerateJsonSchema as _GenerateJsonSchema from pydantic.json_schema import JsonSchemaValue as JsonSchemaValue from pydantic_core import CoreSchema as CoreSchema from pydantic_core import PydanticUndefined @@ -40,6 +40,23 @@ RequiredParam = PydanticUndefined Undefined = PydanticUndefined evaluate_forwardref = eval_type_lenient + +class GenerateJsonSchema(_GenerateJsonSchema): + # TODO: remove when this is merged (or equivalent): https://github.com/pydantic/pydantic/pull/12841 + # and dropping support for any version of Pydantic before that one (so, in a very long time) + def bytes_schema(self, schema: CoreSchema) -> JsonSchemaValue: + json_schema = {"type": "string", "contentMediaType": "application/octet-stream"} + bytes_mode = ( + self._config.ser_json_bytes + if self.mode == "serialization" + else self._config.val_json_bytes + ) + if bytes_mode == "base64": + json_schema["contentEncoding"] = "base64" + self.update_with_validations(json_schema, schema, self.ValidationsMapping.bytes) + return json_schema + + # TODO: remove when dropping support for Pydantic < v2.12.3 _Attrs = { "default": ..., @@ -182,6 +199,32 @@ class ModelField: exclude_none=exclude_none, ) + def serialize_json( + self, + value: Any, + *, + include: IncEx | None = None, + exclude: IncEx | None = None, + by_alias: bool = True, + exclude_unset: bool = False, + exclude_defaults: bool = False, + exclude_none: bool = False, + ) -> bytes: + # What calls this code passes a value that already called + # self._type_adapter.validate_python(value) + # This uses Pydantic's dump_json() which serializes directly to JSON + # bytes in one pass (via Rust), avoiding the intermediate Python dict + # step of dump_python(mode="json") + json.dumps(). + return self._type_adapter.dump_json( + value, + include=include, + exclude=exclude, + by_alias=by_alias, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + ) + def __hash__(self) -> int: # Each ModelField is unique for our purposes, to allow making a dict from # ModelField to its JSON Schema. diff --git a/fastapi/applications.py b/fastapi/applications.py index 84f01d7a78..5e4f4a9921 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -840,6 +840,29 @@ class FastAPI(Starlette): """ ), ] = None, + strict_content_type: Annotated[ + bool, + Doc( + """ + Enable strict checking for request Content-Type headers. + + When `True` (the default), requests with a body that do not include + a `Content-Type` header will **not** be parsed as JSON. + + This prevents potential cross-site request forgery (CSRF) attacks + that exploit the browser's ability to send requests without a + Content-Type header, bypassing CORS preflight checks. In particular + applicable for apps that need to be run locally (in localhost). + + When `False`, requests without a `Content-Type` header will have + their body parsed as JSON, which maintains compatibility with + certain clients that don't send `Content-Type` headers. + + Read more about it in the + [FastAPI docs for Strict Content-Type](https://fastapi.tiangolo.com/advanced/strict-content-type/). + """ + ), + ] = True, **extra: Annotated[ Any, Doc( @@ -974,6 +997,7 @@ class FastAPI(Starlette): include_in_schema=include_in_schema, responses=responses, generate_unique_id_function=generate_unique_id_function, + strict_content_type=strict_content_type, defer_init=False, ) self.exception_handlers: dict[ diff --git a/fastapi/datastructures.py b/fastapi/datastructures.py index c04b5f0f39..479e1a7c3b 100644 --- a/fastapi/datastructures.py +++ b/fastapi/datastructures.py @@ -139,7 +139,7 @@ class UploadFile(StarletteUploadFile): def __get_pydantic_json_schema__( cls, core_schema: Mapping[str, Any], handler: GetJsonSchemaHandler ) -> dict[str, Any]: - return {"type": "string", "format": "binary"} + return {"type": "string", "contentMediaType": "application/octet-stream"} @classmethod def __get_pydantic_core_schema__( diff --git a/fastapi/responses.py b/fastapi/responses.py index 6c8db6f335..5b1154c046 100644 --- a/fastapi/responses.py +++ b/fastapi/responses.py @@ -1,5 +1,6 @@ from typing import Any +from fastapi.exceptions import FastAPIDeprecationWarning from starlette.responses import FileResponse as FileResponse # noqa from starlette.responses import HTMLResponse as HTMLResponse # noqa from starlette.responses import JSONResponse as JSONResponse # noqa @@ -7,6 +8,7 @@ from starlette.responses import PlainTextResponse as PlainTextResponse # noqa from starlette.responses import RedirectResponse as RedirectResponse # noqa from starlette.responses import Response as Response # noqa from starlette.responses import StreamingResponse as StreamingResponse # noqa +from typing_extensions import deprecated try: import ujson @@ -20,12 +22,29 @@ except ImportError: # pragma: nocover orjson = None # type: ignore +@deprecated( + "UJSONResponse is deprecated, FastAPI now serializes data directly to JSON " + "bytes via Pydantic when a return type or response model is set, which is " + "faster and doesn't need a custom response class. Read more in the FastAPI " + "docs: https://fastapi.tiangolo.com/advanced/custom-response/#orjson-or-response-model " + "and https://fastapi.tiangolo.com/tutorial/response-model/", + category=FastAPIDeprecationWarning, + stacklevel=2, +) class UJSONResponse(JSONResponse): - """ - JSON response using the high-performance ujson library to serialize data to JSON. + """JSON response using the ujson library to serialize data to JSON. - Read more about it in the - [FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/). + **Deprecated**: `UJSONResponse` is deprecated. FastAPI now serializes data + directly to JSON bytes via Pydantic when a return type or response model is + set, which is faster and doesn't need a custom response class. + + Read more in the + [FastAPI docs for Custom Response](https://fastapi.tiangolo.com/advanced/custom-response/#orjson-or-response-model) + and the + [FastAPI docs for Response Model](https://fastapi.tiangolo.com/tutorial/response-model/). + + **Note**: `ujson` is not included with FastAPI and must be installed + separately, e.g. `pip install ujson`. """ def render(self, content: Any) -> bytes: @@ -33,12 +52,29 @@ class UJSONResponse(JSONResponse): return ujson.dumps(content, ensure_ascii=False).encode("utf-8") +@deprecated( + "ORJSONResponse is deprecated, FastAPI now serializes data directly to JSON " + "bytes via Pydantic when a return type or response model is set, which is " + "faster and doesn't need a custom response class. Read more in the FastAPI " + "docs: https://fastapi.tiangolo.com/advanced/custom-response/#orjson-or-response-model " + "and https://fastapi.tiangolo.com/tutorial/response-model/", + category=FastAPIDeprecationWarning, + stacklevel=2, +) class ORJSONResponse(JSONResponse): - """ - JSON response using the high-performance orjson library to serialize data to JSON. + """JSON response using the orjson library to serialize data to JSON. - Read more about it in the - [FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/). + **Deprecated**: `ORJSONResponse` is deprecated. FastAPI now serializes data + directly to JSON bytes via Pydantic when a return type or response model is + set, which is faster and doesn't need a custom response class. + + Read more in the + [FastAPI docs for Custom Response](https://fastapi.tiangolo.com/advanced/custom-response/#orjson-or-response-model) + and the + [FastAPI docs for Response Model](https://fastapi.tiangolo.com/tutorial/response-model/). + + **Note**: `orjson` is not included with FastAPI and must be installed + separately, e.g. `pip install orjson`. """ def render(self, content: Any) -> bytes: diff --git a/fastapi/routing.py b/fastapi/routing.py index 3a525d9d26..7790b9da59 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -272,6 +272,7 @@ async def serialize_response( exclude_none: bool = False, is_coroutine: bool = True, endpoint_ctx: EndpointContext | None = None, + dump_json: bool = False, ) -> Any: if field: if is_coroutine: @@ -287,8 +288,8 @@ async def serialize_response( body=response_content, endpoint_ctx=ctx, ) - - return field.serialize( + serializer = field.serialize_json if dump_json else field.serialize + return serializer( value, include=include, exclude=exclude, @@ -329,6 +330,7 @@ def get_request_handler( response_model_exclude_none: bool = False, dependency_overrides_provider: Any | None = None, embed_body_fields: bool = False, + strict_content_type: bool | DefaultPlaceholder = Default(True), ) -> Callable[[Request], Coroutine[Any, Any, Response]]: assert dependant.call is not None, "dependant.call must be a function" is_coroutine = dependant.is_coroutine_callable @@ -337,6 +339,10 @@ def get_request_handler( actual_response_class: type[Response] = response_class.value else: actual_response_class = response_class + if isinstance(strict_content_type, DefaultPlaceholder): + actual_strict_content_type: bool = strict_content_type.value + else: + actual_strict_content_type = strict_content_type async def app(request: Request) -> Response: response: Response | None = None @@ -370,7 +376,8 @@ def get_request_handler( json_body: Any = Undefined content_type_value = request.headers.get("content-type") if not content_type_value: - json_body = await request.json() + if not actual_strict_content_type: + json_body = await request.json() else: message = email.message.Message() message["content-type"] = content_type_value @@ -444,6 +451,14 @@ def get_request_handler( response_args["status_code"] = current_status_code if solved_result.response.status_code: response_args["status_code"] = solved_result.response.status_code + # Use the fast path (dump_json) when no custom response + # class was set and a response field with a TypeAdapter + # exists. Serializes directly to JSON bytes via Pydantic's + # Rust core, skipping the intermediate Python dict + + # json.dumps() step. + use_dump_json = response_field is not None and isinstance( + response_class, DefaultPlaceholder + ) content = await serialize_response( field=response_field, response_content=raw_response, @@ -455,8 +470,16 @@ def get_request_handler( exclude_none=response_model_exclude_none, is_coroutine=is_coroutine, endpoint_ctx=endpoint_ctx, + dump_json=use_dump_json, ) - response = actual_response_class(content, **response_args) + if use_dump_json: + response = Response( + content=content, + media_type="application/json", + **response_args, + ) + else: + response = actual_response_class(content, **response_args) if not is_body_allowed_for_status_code(response.status_code): response.body = b"" response.headers.raw.extend(solved_result.response.headers.raw) @@ -583,6 +606,7 @@ class APIRoute(routing.Route): openapi_extra: dict[str, Any] | None = None, generate_unique_id_function: Callable[["APIRoute"], str] | DefaultPlaceholder = Default(generate_unique_id), + strict_content_type: bool | DefaultPlaceholder = Default(True), defer_init: bool = True, ) -> None: self.path = path @@ -610,6 +634,7 @@ class APIRoute(routing.Route): self.callbacks = callbacks self.openapi_extra = openapi_extra self.generate_unique_id_function = generate_unique_id_function + self.strict_content_type = strict_content_type self.tags = tags or [] self.responses = responses or {} self.name = get_name(endpoint) if name is None else name @@ -729,6 +754,7 @@ class APIRoute(routing.Route): response_model_exclude_none=self.response_model_exclude_none, dependency_overrides_provider=self.dependency_overrides_provider, embed_body_fields=self._embed_body_fields, + strict_content_type=self.strict_content_type, ) def matches(self, scope: Scope) -> tuple[Match, Scope]: @@ -979,6 +1005,29 @@ class APIRouter(routing.Router): """ ), ] = Default(generate_unique_id), + strict_content_type: Annotated[ + bool, + Doc( + """ + Enable strict checking for request Content-Type headers. + + When `True` (the default), requests with a body that do not include + a `Content-Type` header will **not** be parsed as JSON. + + This prevents potential cross-site request forgery (CSRF) attacks + that exploit the browser's ability to send requests without a + Content-Type header, bypassing CORS preflight checks. In particular + applicable for apps that need to be run locally (in localhost). + + When `False`, requests without a `Content-Type` header will have + their body parsed as JSON, which maintains compatibility with + certain clients that don't send `Content-Type` headers. + + Read more about it in the + [FastAPI docs for Strict Content-Type](https://fastapi.tiangolo.com/advanced/strict-content-type/). + """ + ), + ] = Default(True), defer_init: Annotated[ bool, Doc( @@ -1035,7 +1084,9 @@ class APIRouter(routing.Router): self.route_class = route_class self.default_response_class = default_response_class self.generate_unique_id_function = generate_unique_id_function + self.strict_content_type = strict_content_type self.defer_init = defer_init + if not self.defer_init: self.init_routes() @@ -1088,6 +1139,7 @@ class APIRouter(routing.Router): openapi_extra: dict[str, Any] | None = None, generate_unique_id_function: Callable[[APIRoute], str] | DefaultPlaceholder = Default(generate_unique_id), + strict_content_type: bool | DefaultPlaceholder = Default(True), ) -> None: route_class = route_class_override or self.route_class responses = responses or {} @@ -1134,6 +1186,9 @@ class APIRouter(routing.Router): callbacks=current_callbacks, openapi_extra=openapi_extra, generate_unique_id_function=current_generate_unique_id, + strict_content_type=get_value_or_default( + strict_content_type, self.strict_content_type + ), defer_init=self.defer_init, ) self.routes.append(route) @@ -1515,6 +1570,11 @@ class APIRouter(routing.Router): callbacks=current_callbacks, openapi_extra=route.openapi_extra, generate_unique_id_function=current_generate_unique_id, + strict_content_type=get_value_or_default( + route.strict_content_type, + router.strict_content_type, + self.strict_content_type, + ), ) elif isinstance(route, routing.Route): methods = list(route.methods or []) diff --git a/pdm_build.py b/pdm_build.py deleted file mode 100644 index b1b662bd39..0000000000 --- a/pdm_build.py +++ /dev/null @@ -1,40 +0,0 @@ -import os -from typing import Any - -from pdm.backend.hooks import Context - -TIANGOLO_BUILD_PACKAGE = os.getenv("TIANGOLO_BUILD_PACKAGE") - - -def pdm_build_initialize(context: Context) -> None: - metadata = context.config.metadata - # Get main version - version = metadata["version"] - # Get custom config for the current package, from the env var - all_configs_config: dict[str, Any] = context.config.data["tool"]["tiangolo"][ - "_internal-slim-build" - ]["packages"] - - if TIANGOLO_BUILD_PACKAGE not in all_configs_config: - return - - config = all_configs_config[TIANGOLO_BUILD_PACKAGE] - project_config: dict[str, Any] = config["project"] - # Override main [project] configs with custom configs for this package - for key, value in project_config.items(): - metadata[key] = value - # Get custom build config for the current package - build_config: dict[str, Any] = ( - config.get("tool", {}).get("pdm", {}).get("build", {}) - ) - # Override PDM build config with custom build config for this package - for key, value in build_config.items(): - context.config.build_config[key] = value - # Get main dependencies - dependencies: list[str] = metadata.get("dependencies", []) - # Sync versions in dependencies - new_dependencies = [] - for dep in dependencies: - new_dep = f"{dep}>={version}" - new_dependencies.append(new_dep) - metadata["dependencies"] = new_dependencies diff --git a/pyproject.toml b/pyproject.toml index 1e6fda3b1c..76b53726ac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,7 +57,6 @@ Issues = "https://github.com/fastapi/fastapi/issues" Changelog = "https://fastapi.tiangolo.com/release-notes/" [project.optional-dependencies] - standard = [ "fastapi-cli[standard] >=0.0.8", # For the test client @@ -106,10 +105,6 @@ all = [ "itsdangerous >=1.1.0", # For Starlette's schema generation, would not be used with FastAPI "pyyaml >=5.3.1", - # For UJSONResponse - "ujson >=5.8.0", - # For ORJSONResponse - "orjson >=3.9.3", # To validate email fields "email-validator >=2.0.0", # Uvicorn with uvloop @@ -152,6 +147,10 @@ docs = [ docs-tests = [ "httpx >=0.23.0,<1.0.0", "ruff >=0.14.14", + # For UJSONResponse + "ujson >=5.8.0", + # For ORJSONResponse + "orjson >=3.9.3", ] github-actions = [ "httpx >=0.27.0,<1.0.0", @@ -171,7 +170,7 @@ tests = [ "mypy >=1.14.1", "pwdlib[argon2] >=0.2.1", "pyjwt >=2.9.0", - "pytest >=7.1.3,<9.0.0", + "pytest >=9.0.0", "pytest-codspeed >=4.2.0", "pyyaml >=5.3.1,<7.0.0", "sqlmodel >=0.0.31", @@ -199,32 +198,6 @@ source-includes = [ "docs/en/docs/img/favicon.png", ] -[tool.tiangolo._internal-slim-build.packages.fastapi-slim.project] -name = "fastapi-slim" -readme = "fastapi-slim/README.md" -dependencies = [ - "fastapi", -] -optional-dependencies = {} -scripts = {} - -[tool.tiangolo._internal-slim-build.packages.fastapi-slim.tool.pdm.build] -# excludes needs to explicitly exclude the top level python packages, -# otherwise PDM includes them by default -# A "*" glob pattern can't be used here because in PDM internals, the patterns are put -# in a set (unordered, order varies) and each excluded file is assigned one of the -# glob patterns that matches, as the set is unordered, the matched pattern could be "*" -# independent of the order here. And then the internal code would give it a lower score -# than the one for a default included file. -# By not using "*" and explicitly excluding the top level packages, they get a higher -# score than the default inclusion -excludes = ["fastapi", "tests", "pdm_build.py"] -# source-includes needs to explicitly define some value because PDM will check the -# truthy value of the list, and if empty, will include some defaults, including "tests", -# an empty string doesn't match anything, but makes the list truthy, so that PDM -# doesn't override it during the build. -source-includes = [""] - [tool.mypy] plugins = ["pydantic.mypy"] strict = true @@ -245,25 +218,16 @@ disallow_incomplete_defs = false disallow_untyped_defs = false disallow_untyped_calls = false -[tool.pytest.ini_options] +[tool.pytest] +minversion = "9.0" addopts = [ "--strict-config", "--strict-markers", "--ignore=docs_src", ] -xfail_strict = true -junit_family = "xunit2" +strict_xfail = true filterwarnings = [ "error", - # see https://trio.readthedocs.io/en/stable/history.html#trio-0-22-0-2022-09-28 - "ignore:You seem to already have a custom.*:RuntimeWarning:trio", - # TODO: remove after upgrading SQLAlchemy to a version that includes the following changes - # https://github.com/sqlalchemy/sqlalchemy/commit/59521abcc0676e936b31a523bd968fc157fef0c2 - 'ignore:datetime\.datetime\.utcfromtimestamp\(\) is deprecated and scheduled for removal in a future version\..*:DeprecationWarning:sqlalchemy', - # Trio 24.1.0 raises a warning from attrs - # Ref: https://github.com/python-trio/trio/pull/3054 - # Remove once there's a new version of Trio - 'ignore:The `hash` argument is deprecated*:DeprecationWarning:trio', ] [tool.coverage.run] @@ -278,9 +242,9 @@ relative_files = true context = '${CONTEXT}' dynamic_context = "test_function" omit = [ + "tests/benchmarks/*", "docs_src/response_model/tutorial003_04_py39.py", "docs_src/response_model/tutorial003_04_py310.py", - "docs_src/dependencies/tutorial008_an_py39.py", # difficult to mock "docs_src/dependencies/tutorial013_an_py310.py", # temporary code example? "docs_src/dependencies/tutorial014_an_py310.py", # temporary code example? # Pydantic v1 migration, no longer tested @@ -288,202 +252,6 @@ omit = [ "docs_src/pydantic_v1_in_v2/tutorial002_an_py310.py", "docs_src/pydantic_v1_in_v2/tutorial003_an_py310.py", "docs_src/pydantic_v1_in_v2/tutorial004_an_py310.py", - # TODO: remove all the ignores below when all translations use the new Python 3.10 files - "docs_src/additional_responses/tutorial001_py39.py", - "docs_src/additional_responses/tutorial003_py39.py", - "docs_src/advanced_middleware/tutorial001_py39.py", - "docs_src/advanced_middleware/tutorial002_py39.py", - "docs_src/advanced_middleware/tutorial003_py39.py", - "docs_src/app_testing/app_a_py39/main.py", - "docs_src/app_testing/app_a_py39/test_main.py", - "docs_src/app_testing/tutorial001_py39.py", - "docs_src/app_testing/tutorial002_py39.py", - "docs_src/app_testing/tutorial003_py39.py", - "docs_src/app_testing/tutorial004_py39.py", - "docs_src/async_tests/app_a_py39/main.py", - "docs_src/async_tests/app_a_py39/test_main.py", - "docs_src/authentication_error_status_code/tutorial001_an_py39.py", - "docs_src/background_tasks/tutorial001_py39.py", - "docs_src/behind_a_proxy/tutorial001_01_py39.py", - "docs_src/behind_a_proxy/tutorial001_py39.py", - "docs_src/behind_a_proxy/tutorial002_py39.py", - "docs_src/behind_a_proxy/tutorial003_py39.py", - "docs_src/behind_a_proxy/tutorial004_py39.py", - "docs_src/bigger_applications/app_an_py39/dependencies.py", - "docs_src/bigger_applications/app_an_py39/internal/admin.py", - "docs_src/bigger_applications/app_an_py39/main.py", - "docs_src/bigger_applications/app_an_py39/routers/items.py", - "docs_src/bigger_applications/app_an_py39/routers/users.py", - "docs_src/bigger_applications/app_py39/dependencies.py", - "docs_src/bigger_applications/app_py39/main.py", - "docs_src/body_nested_models/tutorial008_py39.py", - "docs_src/body_nested_models/tutorial009_py39.py", - "docs_src/conditional_openapi/tutorial001_py39.py", - "docs_src/configure_swagger_ui/tutorial001_py39.py", - "docs_src/configure_swagger_ui/tutorial002_py39.py", - "docs_src/configure_swagger_ui/tutorial003_py39.py", - "docs_src/cors/tutorial001_py39.py", - "docs_src/custom_docs_ui/tutorial001_py39.py", - "docs_src/custom_docs_ui/tutorial002_py39.py", - "docs_src/custom_response/tutorial001_py39.py", - "docs_src/custom_response/tutorial001b_py39.py", - "docs_src/custom_response/tutorial002_py39.py", - "docs_src/custom_response/tutorial003_py39.py", - "docs_src/custom_response/tutorial004_py39.py", - "docs_src/custom_response/tutorial005_py39.py", - "docs_src/custom_response/tutorial006_py39.py", - "docs_src/custom_response/tutorial006b_py39.py", - "docs_src/custom_response/tutorial006c_py39.py", - "docs_src/custom_response/tutorial007_py39.py", - "docs_src/custom_response/tutorial008_py39.py", - "docs_src/custom_response/tutorial009_py39.py", - "docs_src/custom_response/tutorial009b_py39.py", - "docs_src/custom_response/tutorial009c_py39.py", - "docs_src/custom_response/tutorial010_py39.py", - "docs_src/debugging/tutorial001_py39.py", - "docs_src/dependencies/tutorial006_an_py39.py", - "docs_src/dependencies/tutorial006_py39.py", - "docs_src/dependencies/tutorial007_py39.py", - "docs_src/dependencies/tutorial008_py39.py", - "docs_src/dependencies/tutorial008b_an_py39.py", - "docs_src/dependencies/tutorial008b_py39.py", - "docs_src/dependencies/tutorial008c_an_py39.py", - "docs_src/dependencies/tutorial008c_py39.py", - "docs_src/dependencies/tutorial008d_an_py39.py", - "docs_src/dependencies/tutorial008d_py39.py", - "docs_src/dependencies/tutorial008e_an_py39.py", - "docs_src/dependencies/tutorial008e_py39.py", - "docs_src/dependencies/tutorial010_py39.py", - "docs_src/dependencies/tutorial011_an_py39.py", - "docs_src/dependencies/tutorial011_py39.py", - "docs_src/dependencies/tutorial012_an_py39.py", - "docs_src/dependencies/tutorial012_py39.py", - "docs_src/events/tutorial001_py39.py", - "docs_src/events/tutorial002_py39.py", - "docs_src/events/tutorial003_py39.py", - "docs_src/extending_openapi/tutorial001_py39.py", - "docs_src/extra_models/tutorial004_py39.py", - "docs_src/extra_models/tutorial005_py39.py", - "docs_src/first_steps/tutorial001_py39.py", - "docs_src/first_steps/tutorial003_py39.py", - "docs_src/generate_clients/tutorial001_py39.py", - "docs_src/generate_clients/tutorial002_py39.py", - "docs_src/generate_clients/tutorial003_py39.py", - "docs_src/generate_clients/tutorial004_py39.py", - "docs_src/graphql_/tutorial001_py39.py", - "docs_src/handling_errors/tutorial001_py39.py", - "docs_src/handling_errors/tutorial002_py39.py", - "docs_src/handling_errors/tutorial003_py39.py", - "docs_src/handling_errors/tutorial004_py39.py", - "docs_src/handling_errors/tutorial005_py39.py", - "docs_src/handling_errors/tutorial006_py39.py", - "docs_src/metadata/tutorial001_1_py39.py", - "docs_src/metadata/tutorial001_py39.py", - "docs_src/metadata/tutorial002_py39.py", - "docs_src/metadata/tutorial003_py39.py", - "docs_src/metadata/tutorial004_py39.py", - "docs_src/middleware/tutorial001_py39.py", - "docs_src/openapi_webhooks/tutorial001_py39.py", - "docs_src/path_operation_advanced_configuration/tutorial001_py39.py", - "docs_src/path_operation_advanced_configuration/tutorial002_py39.py", - "docs_src/path_operation_advanced_configuration/tutorial003_py39.py", - "docs_src/path_operation_advanced_configuration/tutorial005_py39.py", - "docs_src/path_operation_advanced_configuration/tutorial006_py39.py", - "docs_src/path_operation_advanced_configuration/tutorial007_py39.py", - "docs_src/path_operation_configuration/tutorial002b_py39.py", - "docs_src/path_operation_configuration/tutorial006_py39.py", - "docs_src/path_params/tutorial001_py39.py", - "docs_src/path_params/tutorial002_py39.py", - "docs_src/path_params/tutorial003_py39.py", - "docs_src/path_params/tutorial003b_py39.py", - "docs_src/path_params/tutorial004_py39.py", - "docs_src/path_params/tutorial005_py39.py", - "docs_src/path_params_numeric_validations/tutorial002_an_py39.py", - "docs_src/path_params_numeric_validations/tutorial002_py39.py", - "docs_src/path_params_numeric_validations/tutorial003_an_py39.py", - "docs_src/path_params_numeric_validations/tutorial003_py39.py", - "docs_src/path_params_numeric_validations/tutorial004_an_py39.py", - "docs_src/path_params_numeric_validations/tutorial004_py39.py", - "docs_src/path_params_numeric_validations/tutorial005_an_py39.py", - "docs_src/path_params_numeric_validations/tutorial005_py39.py", - "docs_src/path_params_numeric_validations/tutorial006_an_py39.py", - "docs_src/path_params_numeric_validations/tutorial006_py39.py", - "docs_src/python_types/tutorial001_py39.py", - "docs_src/python_types/tutorial002_py39.py", - "docs_src/python_types/tutorial003_py39.py", - "docs_src/python_types/tutorial004_py39.py", - "docs_src/python_types/tutorial005_py39.py", - "docs_src/python_types/tutorial006_py39.py", - "docs_src/python_types/tutorial007_py39.py", - "docs_src/python_types/tutorial008_py39.py", - "docs_src/python_types/tutorial008b_py39.py", - "docs_src/python_types/tutorial009_py39.py", - "docs_src/python_types/tutorial009b_py39.py", - "docs_src/python_types/tutorial009c_py39.py", - "docs_src/python_types/tutorial010_py39.py", - "docs_src/python_types/tutorial013_py39.py", - "docs_src/query_params/tutorial001_py39.py", - "docs_src/query_params/tutorial005_py39.py", - "docs_src/query_params_str_validations/tutorial005_an_py39.py", - "docs_src/query_params_str_validations/tutorial005_py39.py", - "docs_src/query_params_str_validations/tutorial006_an_py39.py", - "docs_src/query_params_str_validations/tutorial006_py39.py", - "docs_src/query_params_str_validations/tutorial012_an_py39.py", - "docs_src/query_params_str_validations/tutorial012_py39.py", - "docs_src/query_params_str_validations/tutorial013_an_py39.py", - "docs_src/query_params_str_validations/tutorial013_py39.py", - "docs_src/request_files/tutorial001_03_an_py39.py", - "docs_src/request_files/tutorial001_03_py39.py", - "docs_src/request_files/tutorial001_an_py39.py", - "docs_src/request_files/tutorial001_py39.py", - "docs_src/request_files/tutorial002_an_py39.py", - "docs_src/request_files/tutorial002_py39.py", - "docs_src/request_files/tutorial003_an_py39.py", - "docs_src/request_files/tutorial003_py39.py", - "docs_src/request_form_models/tutorial001_an_py39.py", - "docs_src/request_form_models/tutorial001_py39.py", - "docs_src/request_form_models/tutorial002_an_py39.py", - "docs_src/request_form_models/tutorial002_py39.py", - "docs_src/request_forms/tutorial001_an_py39.py", - "docs_src/request_forms/tutorial001_py39.py", - "docs_src/request_forms_and_files/tutorial001_an_py39.py", - "docs_src/request_forms_and_files/tutorial001_py39.py", - "docs_src/response_change_status_code/tutorial001_py39.py", - "docs_src/response_cookies/tutorial001_py39.py", - "docs_src/response_cookies/tutorial002_py39.py", - "docs_src/response_directly/tutorial002_py39.py", - "docs_src/response_headers/tutorial001_py39.py", - "docs_src/response_headers/tutorial002_py39.py", - "docs_src/response_model/tutorial003_02_py39.py", - "docs_src/response_model/tutorial003_03_py39.py", - "docs_src/response_status_code/tutorial001_py39.py", - "docs_src/response_status_code/tutorial002_py39.py", - "docs_src/security/tutorial001_an_py39.py", - "docs_src/security/tutorial001_py39.py", - "docs_src/security/tutorial006_an_py39.py", - "docs_src/security/tutorial006_py39.py", - "docs_src/security/tutorial007_an_py39.py", - "docs_src/security/tutorial007_py39.py", - "docs_src/settings/app01_py39/config.py", - "docs_src/settings/app01_py39/main.py", - "docs_src/settings/app02_an_py39/config.py", - "docs_src/settings/app02_an_py39/main.py", - "docs_src/settings/app02_an_py39/test_main.py", - "docs_src/settings/app02_py39/config.py", - "docs_src/settings/app02_py39/main.py", - "docs_src/settings/app02_py39/test_main.py", - "docs_src/settings/app03_an_py39/config.py", - "docs_src/settings/app03_an_py39/main.py", - "docs_src/settings/app03_py39/config.py", - "docs_src/settings/app03_py39/main.py", - "docs_src/settings/tutorial001_py39.py", - "docs_src/static_files/tutorial001_py39.py", - "docs_src/sub_applications/tutorial001_py39.py", - "docs_src/templates/tutorial001_py39.py", - "docs_src/using_request_directly/tutorial001_py39.py", - "docs_src/websockets/tutorial001_py39.py", - "docs_src/websockets/tutorial003_py39.py", - "docs_src/wsgi/tutorial001_py39.py", ] [tool.coverage.report] @@ -548,6 +316,7 @@ ignore = [ "docs_src/security/tutorial005_an_py39.py" = ["B904"] "docs_src/security/tutorial005_py310.py" = ["B904"] "docs_src/security/tutorial005_py39.py" = ["B904"] +"docs_src/json_base64_bytes/tutorial001_py310.py" = ["UP012"] [tool.ruff.lint.isort] known-third-party = ["fastapi", "pydantic", "starlette"] diff --git a/scripts/doc_parsing_utils.py b/scripts/doc_parsing_utils.py index 79f2e9ec0a..1cd2299e66 100644 --- a/scripts/doc_parsing_utils.py +++ b/scripts/doc_parsing_utils.py @@ -1,5 +1,5 @@ import re -from typing import TypedDict, Union +from typing import TypedDict CODE_INCLUDE_RE = re.compile(r"^\{\*\s*(\S+)\s*(.*)\*\}$") CODE_INCLUDE_PLACEHOLDER = "" @@ -50,8 +50,8 @@ class MarkdownLinkInfo(TypedDict): line_no: int url: str text: str - title: Union[str, None] - attributes: Union[str, None] + title: str | None + attributes: str | None full_match: str @@ -287,8 +287,8 @@ def _add_lang_code_to_url(url: str, lang_code: str) -> str: def _construct_markdown_link( url: str, text: str, - title: Union[str, None], - attributes: Union[str, None], + title: str | None, + attributes: str | None, lang_code: str, ) -> str: """ @@ -549,7 +549,7 @@ def extract_multiline_code_blocks(text: list[str]) -> list[MultilineCodeBlockInf return blocks -def _split_hash_comment(line: str) -> tuple[str, Union[str, None]]: +def _split_hash_comment(line: str) -> tuple[str, str | None]: match = HASH_COMMENT_RE.match(line) if match: code = match.group("code").rstrip() @@ -558,7 +558,7 @@ def _split_hash_comment(line: str) -> tuple[str, Union[str, None]]: return line.rstrip(), None -def _split_slashes_comment(line: str) -> tuple[str, Union[str, None]]: +def _split_slashes_comment(line: str) -> tuple[str, str | None]: match = SLASHES_COMMENT_RE.match(line) if match: code = match.group("code").rstrip() @@ -603,9 +603,9 @@ def replace_multiline_code_block( return block_a["content"].copy() # We don't handle mermaid code blocks for now code_block: list[str] = [] - for line_a, line_b in zip(block_a["content"], block_b["content"]): - line_a_comment: Union[str, None] = None - line_b_comment: Union[str, None] = None + for line_a, line_b in zip(block_a["content"], block_b["content"], strict=False): + line_a_comment: str | None = None + line_b_comment: str | None = None # Handle comments based on language if block_language in { @@ -659,7 +659,7 @@ def replace_multiline_code_blocks_in_text( ) modified_text = text.copy() - for block, original_block in zip(code_blocks, original_code_blocks): + for block, original_block in zip(code_blocks, original_code_blocks, strict=True): updated_content = replace_multiline_code_block(block, original_block) start_line_index = block["start_line_no"] - 1 diff --git a/scripts/mkdocs_hooks.py b/scripts/mkdocs_hooks.py index 567c0111dc..97006953d6 100644 --- a/scripts/mkdocs_hooks.py +++ b/scripts/mkdocs_hooks.py @@ -1,6 +1,6 @@ from functools import lru_cache from pathlib import Path -from typing import Any, Union +from typing import Any import material from mkdocs.config.defaults import MkDocsConfig @@ -105,9 +105,9 @@ def on_files(files: Files, *, config: MkDocsConfig) -> Files: def generate_renamed_section_items( - items: list[Union[Page, Section, Link]], *, config: MkDocsConfig -) -> list[Union[Page, Section, Link]]: - new_items: list[Union[Page, Section, Link]] = [] + items: list[Page | Section | Link], *, config: MkDocsConfig +) -> list[Page | Section | Link]: + new_items: list[Page | Section | Link] = [] for item in items: if isinstance(item, Section): new_title = item.title diff --git a/scripts/notify_translations.py b/scripts/notify_translations.py index 74cdf0dffe..3484b69c70 100644 --- a/scripts/notify_translations.py +++ b/scripts/notify_translations.py @@ -3,7 +3,7 @@ import random import sys import time from pathlib import Path -from typing import Any, Union, cast +from typing import Any, cast import httpx from github import Github @@ -181,9 +181,9 @@ class Settings(BaseSettings): github_repository: str github_token: SecretStr github_event_path: Path - github_event_name: Union[str, None] = None + github_event_name: str | None = None httpx_timeout: int = 30 - debug: Union[bool, None] = False + debug: bool | None = False number: int | None = None @@ -199,12 +199,12 @@ def get_graphql_response( *, settings: Settings, query: str, - after: Union[str, None] = None, - category_id: Union[str, None] = None, - discussion_number: Union[int, None] = None, - discussion_id: Union[str, None] = None, - comment_id: Union[str, None] = None, - body: Union[str, None] = None, + after: str | None = None, + category_id: str | None = None, + discussion_number: int | None = None, + discussion_id: str | None = None, + comment_id: str | None = None, + body: str | None = None, ) -> dict[str, Any]: headers = {"Authorization": f"token {settings.github_token.get_secret_value()}"} variables = { @@ -249,7 +249,7 @@ def get_graphql_translation_discussions( def get_graphql_translation_discussion_comments_edges( - *, settings: Settings, discussion_number: int, after: Union[str, None] = None + *, settings: Settings, discussion_number: int, after: str | None = None ) -> list[CommentsEdge]: data = get_graphql_response( settings=settings, @@ -372,8 +372,8 @@ def main() -> None: f"Found a translation discussion for language: {lang} in discussion: #{discussion.number}" ) - already_notified_comment: Union[Comment, None] = None - already_done_comment: Union[Comment, None] = None + already_notified_comment: Comment | None = None + already_done_comment: Comment | None = None logging.info( f"Checking current comments in discussion: #{discussion.number} to see if already notified about this PR: #{pr.number}" diff --git a/scripts/people.py b/scripts/people.py index 207ab46493..2e84fcc455 100644 --- a/scripts/people.py +++ b/scripts/people.py @@ -5,8 +5,9 @@ import time from collections import Counter from collections.abc import Container from datetime import datetime, timedelta, timezone +from math import ceil from pathlib import Path -from typing import Any, Union +from typing import Any import httpx import yaml @@ -15,12 +16,63 @@ from pydantic import BaseModel, SecretStr from pydantic_settings import BaseSettings github_graphql_url = "https://api.github.com/graphql" -questions_category_id = "MDE4OkRpc2N1c3Npb25DYXRlZ29yeTMyMDAxNDM0" +questions_category_id = "DIC_kwDOCZduT84B6E2a" + + +POINTS_PER_MINUTE_LIMIT = 84 # 5000 points per hour + + +class RateLimiter: + def __init__(self) -> None: + self.last_query_cost: int = 1 + self.remaining_points: int = 5000 + self.reset_at: datetime = datetime.fromtimestamp(0, timezone.utc) + self.last_request_start_time: datetime = datetime.fromtimestamp(0, timezone.utc) + self.speed_multiplier: float = 1.0 + + def __enter__(self) -> "RateLimiter": + now = datetime.now(tz=timezone.utc) + + # Handle primary rate limits + primary_limit_wait_time = 0.0 + if self.remaining_points <= self.last_query_cost: + primary_limit_wait_time = (self.reset_at - now).total_seconds() + 2 + logging.warning( + f"Approaching GitHub API rate limit, remaining points: {self.remaining_points}, " + f"reset time in {primary_limit_wait_time} seconds" + ) + + # Handle secondary rate limits + secondary_limit_wait_time = 0.0 + points_per_minute = POINTS_PER_MINUTE_LIMIT * self.speed_multiplier + interval = 60 / (points_per_minute / self.last_query_cost) + time_since_last_request = (now - self.last_request_start_time).total_seconds() + if time_since_last_request < interval: + secondary_limit_wait_time = interval - time_since_last_request + + final_wait_time = ceil(max(primary_limit_wait_time, secondary_limit_wait_time)) + logging.info(f"Sleeping for {final_wait_time} seconds to respect rate limit") + time.sleep(max(final_wait_time, 1)) + + self.last_request_start_time = datetime.now(tz=timezone.utc) + return self + + def __exit__(self, exc_type, exc_val, exc_tb) -> None: + pass + + def update_request_info(self, cost: int, remaining: int, reset_at: str) -> None: + self.last_query_cost = cost + self.remaining_points = remaining + self.reset_at = datetime.fromisoformat(reset_at.replace("Z", "+00:00")) + + +rate_limiter = RateLimiter() + discussions_query = """ query Q($after: String, $category_id: ID) { repository(name: "fastapi", owner: "fastapi") { - discussions(first: 100, after: $after, categoryId: $category_id) { + discussions(first: 30, after: $after, categoryId: $category_id) { edges { cursor node { @@ -58,6 +110,11 @@ query Q($after: String, $category_id: ID) { } } } + rateLimit { + cost + remaining + resetAt + } } """ @@ -70,7 +127,7 @@ class Author(BaseModel): class CommentsNode(BaseModel): createdAt: datetime - author: Union[Author, None] = None + author: Author | None = None class Replies(BaseModel): @@ -89,7 +146,7 @@ class DiscussionsComments(BaseModel): class DiscussionsNode(BaseModel): number: int - author: Union[Author, None] = None + author: Author | None = None title: str | None = None createdAt: datetime comments: DiscussionsComments @@ -120,15 +177,15 @@ class Settings(BaseSettings): github_token: SecretStr github_repository: str httpx_timeout: int = 30 - sleep_interval: int = 5 + speed_multiplier: float = 1.0 def get_graphql_response( *, settings: Settings, query: str, - after: Union[str, None] = None, - category_id: Union[str, None] = None, + after: str | None = None, + category_id: str | None = None, ) -> dict[str, Any]: headers = {"Authorization": f"token {settings.github_token.get_secret_value()}"} variables = {"after": after, "category_id": category_id} @@ -156,13 +213,20 @@ def get_graphql_response( def get_graphql_question_discussion_edges( *, settings: Settings, - after: Union[str, None] = None, + after: str | None = None, ) -> list[DiscussionsEdge]: - data = get_graphql_response( - settings=settings, - query=discussions_query, - after=after, - category_id=questions_category_id, + with rate_limiter: + data = get_graphql_response( + settings=settings, + query=discussions_query, + after=after, + category_id=questions_category_id, + ) + + rate_limiter.update_request_info( + cost=data["data"]["rateLimit"]["cost"], + remaining=data["data"]["rateLimit"]["remaining"], + reset_at=data["data"]["rateLimit"]["resetAt"], ) graphql_response = DiscussionsResponse.model_validate(data) return graphql_response.data.repository.discussions.edges @@ -185,8 +249,6 @@ def get_discussion_nodes(settings: Settings) -> list[DiscussionsNode]: for discussion_edge in discussion_edges: discussion_nodes.append(discussion_edge.node) last_edge = discussion_edges[-1] - # Handle GitHub secondary rate limits, requests per minute - time.sleep(settings.sleep_interval) discussion_edges = get_graphql_question_discussion_edges( settings=settings, after=last_edge.cursor ) @@ -318,6 +380,7 @@ def main() -> None: logging.basicConfig(level=logging.INFO) settings = Settings() logging.info(f"Using config: {settings.model_dump_json()}") + rate_limiter.speed_multiplier = settings.speed_multiplier g = Github(settings.github_token.get_secret_value()) repo = g.get_repo(settings.github_repository) diff --git a/scripts/playwright/json_base64_bytes/image01.py b/scripts/playwright/json_base64_bytes/image01.py new file mode 100644 index 0000000000..56c57e1c32 --- /dev/null +++ b/scripts/playwright/json_base64_bytes/image01.py @@ -0,0 +1,37 @@ +import subprocess +import time + +import httpx +from playwright.sync_api import Playwright, sync_playwright + + +# Run playwright codegen to generate the code below, copy paste the sections in run() +def run(playwright: Playwright) -> None: + browser = playwright.chromium.launch(headless=False) + # Update the viewport manually + context = browser.new_context(viewport={"width": 960, "height": 1080}) + page = context.new_page() + page.goto("http://localhost:8000/docs") + page.get_by_role("button", name="POST /data Post Data").click() + # Manually add the screenshot + page.screenshot(path="docs/en/docs/img/tutorial/json-base64-bytes/image01.png") + + # --------------------- + context.close() + browser.close() + + +process = subprocess.Popen( + ["fastapi", "run", "docs_src/json_base64_bytes/tutorial001_py310.py"] +) +try: + for _ in range(3): + try: + response = httpx.get("http://localhost:8000/docs") + except httpx.ConnectError: + time.sleep(1) + break + with sync_playwright() as playwright: + run(playwright) +finally: + process.terminate() diff --git a/scripts/tests/test_translation_fixer/test_code_includes/data/en_doc.md b/scripts/tests/test_translation_fixer/test_code_includes/data/en_doc.md index 593da0b327..0e021dadea 100644 --- a/scripts/tests/test_translation_fixer/test_code_includes/data/en_doc.md +++ b/scripts/tests/test_translation_fixer/test_code_includes/data/en_doc.md @@ -4,7 +4,7 @@ Some text -{* ../../docs_src/bigger_applications/app_an_py39/internal/admin.py hl[3] title["app/internal/admin.py"] *} +{* ../../docs_src/bigger_applications/app_an_py310/internal/admin.py hl[3] title["app/internal/admin.py"] *} Some more text diff --git a/scripts/tests/test_translation_fixer/test_code_includes/data/translated_doc_number_gt.md b/scripts/tests/test_translation_fixer/test_code_includes/data/translated_doc_number_gt.md index c1ad94d276..aca1464ff2 100644 --- a/scripts/tests/test_translation_fixer/test_code_includes/data/translated_doc_number_gt.md +++ b/scripts/tests/test_translation_fixer/test_code_includes/data/translated_doc_number_gt.md @@ -4,7 +4,7 @@ Some text -{* ../../docs_src/bigger_applications/app_an_py39/internal/admin.py hl[3] title["app/internal/admin.py"] *} +{* ../../docs_src/bigger_applications/app_an_py310/internal/admin.py hl[3] title["app/internal/admin.py"] *} Some more text @@ -12,4 +12,4 @@ Some more text And even more text -{* ../../docs_src/python_types/tutorial001_py39.py *} +{* ../../docs_src/python_types/tutorial001_py310.py *} diff --git a/scripts/tests/test_translation_fixer/test_code_includes/data/translated_doc_number_lt.md b/scripts/tests/test_translation_fixer/test_code_includes/data/translated_doc_number_lt.md index 07eaf2c23d..12573aa196 100644 --- a/scripts/tests/test_translation_fixer/test_code_includes/data/translated_doc_number_lt.md +++ b/scripts/tests/test_translation_fixer/test_code_includes/data/translated_doc_number_lt.md @@ -4,7 +4,7 @@ Some text -{* ../../docs_src/bigger_applications/app_an_py39/internal/admin.py hl[3] title["app/internal/admin.py"] *} +{* ../../docs_src/bigger_applications/app_an_py310/internal/admin.py hl[3] title["app/internal/admin.py"] *} Some more text diff --git a/scripts/tests/test_translation_fixer/test_complex_doc/data/en_doc.md b/scripts/tests/test_translation_fixer/test_complex_doc/data/en_doc.md index 69cd3f3fd7..d6c1e3d2fe 100644 --- a/scripts/tests/test_translation_fixer/test_complex_doc/data/en_doc.md +++ b/scripts/tests/test_translation_fixer/test_complex_doc/data/en_doc.md @@ -141,16 +141,16 @@ def hello_world(): ## Simple code includes { #simple-code-includes } -{* ../../docs_src/python_types/tutorial001_py39.py *} +{* ../../docs_src/python_types/tutorial001_py310.py *} -{* ../../docs_src/python_types/tutorial002_py39.py *} +{* ../../docs_src/python_types/tutorial002_py310.py *} ## Code includes with highlighting { #code-includes-with-highlighting } -{* ../../docs_src/python_types/tutorial002_py39.py hl[1] *} +{* ../../docs_src/python_types/tutorial002_py310.py hl[1] *} -{* ../../docs_src/python_types/tutorial006_py39.py hl[10] *} +{* ../../docs_src/python_types/tutorial006_py310.py hl[10] *} ## Code includes with line ranges { #code-includes-with-line-ranges } @@ -169,19 +169,19 @@ def hello_world(): ## Code includes qith title { #code-includes-with-title } -{* ../../docs_src/bigger_applications/app_an_py39/routers/users.py hl[1,3] title["app/routers/users.py"] *} +{* ../../docs_src/bigger_applications/app_an_py310/routers/users.py hl[1,3] title["app/routers/users.py"] *} -{* ../../docs_src/bigger_applications/app_an_py39/internal/admin.py hl[3] title["app/internal/admin.py"] *} +{* ../../docs_src/bigger_applications/app_an_py310/internal/admin.py hl[3] title["app/internal/admin.py"] *} ## Code includes with unknown attributes { #code-includes-with-unknown-attributes } -{* ../../docs_src/python_types/tutorial001_py39.py unknown[123] *} +{* ../../docs_src/python_types/tutorial001_py310.py unknown[123] *} ## Some more code includes to test fixing { #some-more-code-includes-to-test-fixing } {* ../../docs_src/dependencies/tutorial013_an_py310.py ln[19:21] *} -{* ../../docs_src/bigger_applications/app_an_py39/internal/admin.py hl[3] title["app/internal/admin.py"] *} +{* ../../docs_src/bigger_applications/app_an_py310/internal/admin.py hl[3] title["app/internal/admin.py"] *} {* ../../docs_src/dependencies/tutorial013_an_py310.py ln[30:38] hl[31:33] *} diff --git a/scripts/tests/test_translation_fixer/test_complex_doc/data/translated_doc.md b/scripts/tests/test_translation_fixer/test_complex_doc/data/translated_doc.md index c922d7b133..b27eef202c 100644 --- a/scripts/tests/test_translation_fixer/test_complex_doc/data/translated_doc.md +++ b/scripts/tests/test_translation_fixer/test_complex_doc/data/translated_doc.md @@ -139,16 +139,16 @@ def hello_world(): ## Простые включения кода { #simple-code-includes } -{* ../../docs_src/python_types/tutorial001_py39.py *} +{* ../../docs_src/python_types/tutorial001_py310.py *} -{* ../../docs_src/python_types/tutorial002_py39.py *} +{* ../../docs_src/python_types/tutorial002_py310.py *} ## Включения кода с подсветкой { #code-includes-with-highlighting } -{* ../../docs_src/python_types/tutorial002_py39.py hl[1] *} +{* ../../docs_src/python_types/tutorial002_py310.py hl[1] *} -{* ../../docs_src/python_types/tutorial006_py39.py hl[10] *} +{* ../../docs_src/python_types/tutorial006_py310.py hl[10] *} ## Включения кода с диапазонами строк { #code-includes-with-line-ranges } @@ -167,19 +167,19 @@ def hello_world(): ## Включения кода с заголовком { #code-includes-with-title } -{* ../../docs_src/bigger_applications/app_an_py39/routers/users.py hl[1,3] title["app/routers/users.py"] *} +{* ../../docs_src/bigger_applications/app_an_py310/routers/users.py hl[1,3] title["app/routers/users.py"] *} -{* ../../docs_src/bigger_applications/app_an_py39/internal/admin.py hl[3] title["app/internal/admin.py"] *} +{* ../../docs_src/bigger_applications/app_an_py310/internal/admin.py hl[3] title["app/internal/admin.py"] *} ## Включения кода с неизвестными атрибутами { #code-includes-with-unknown-attributes } -{* ../../docs_src/python_types/tutorial001_py39.py unknown[123] *} +{* ../../docs_src/python_types/tutorial001_py310.py unknown[123] *} ## Ещё включения кода для тестирования исправления { #some-more-code-includes-to-test-fixing } {* ../../docs_src/dependencies/tutorial013_an_py310.py ln[19 : 21] *} -{* ../../docs_src/bigger_applications/app_an_py39/wrong.py hl[3] title["app/internal/admin.py"] *} +{* ../../docs_src/bigger_applications/app_an_py310/wrong.py hl[3] title["app/internal/admin.py"] *} {* ../../docs_src/dependencies/tutorial013_an_py310.py ln[1:30] hl[1:10] *} diff --git a/scripts/tests/test_translation_fixer/test_complex_doc/data/translated_doc_expected.md b/scripts/tests/test_translation_fixer/test_complex_doc/data/translated_doc_expected.md index b33f36e772..8e2394f14f 100644 --- a/scripts/tests/test_translation_fixer/test_complex_doc/data/translated_doc_expected.md +++ b/scripts/tests/test_translation_fixer/test_complex_doc/data/translated_doc_expected.md @@ -139,16 +139,16 @@ def hello_world(): ## Простые включения кода { #simple-code-includes } -{* ../../docs_src/python_types/tutorial001_py39.py *} +{* ../../docs_src/python_types/tutorial001_py310.py *} -{* ../../docs_src/python_types/tutorial002_py39.py *} +{* ../../docs_src/python_types/tutorial002_py310.py *} ## Включения кода с подсветкой { #code-includes-with-highlighting } -{* ../../docs_src/python_types/tutorial002_py39.py hl[1] *} +{* ../../docs_src/python_types/tutorial002_py310.py hl[1] *} -{* ../../docs_src/python_types/tutorial006_py39.py hl[10] *} +{* ../../docs_src/python_types/tutorial006_py310.py hl[10] *} ## Включения кода с диапазонами строк { #code-includes-with-line-ranges } @@ -167,19 +167,19 @@ def hello_world(): ## Включения кода с заголовком { #code-includes-with-title } -{* ../../docs_src/bigger_applications/app_an_py39/routers/users.py hl[1,3] title["app/routers/users.py"] *} +{* ../../docs_src/bigger_applications/app_an_py310/routers/users.py hl[1,3] title["app/routers/users.py"] *} -{* ../../docs_src/bigger_applications/app_an_py39/internal/admin.py hl[3] title["app/internal/admin.py"] *} +{* ../../docs_src/bigger_applications/app_an_py310/internal/admin.py hl[3] title["app/internal/admin.py"] *} ## Включения кода с неизвестными атрибутами { #code-includes-with-unknown-attributes } -{* ../../docs_src/python_types/tutorial001_py39.py unknown[123] *} +{* ../../docs_src/python_types/tutorial001_py310.py unknown[123] *} ## Ещё включения кода для тестирования исправления { #some-more-code-includes-to-test-fixing } {* ../../docs_src/dependencies/tutorial013_an_py310.py ln[19:21] *} -{* ../../docs_src/bigger_applications/app_an_py39/internal/admin.py hl[3] title["app/internal/admin.py"] *} +{* ../../docs_src/bigger_applications/app_an_py310/internal/admin.py hl[3] title["app/internal/admin.py"] *} {* ../../docs_src/dependencies/tutorial013_an_py310.py ln[30:38] hl[31:33] *} diff --git a/tests/main.py b/tests/main.py index 7edb16c615..d2fbbe6153 100644 --- a/tests/main.py +++ b/tests/main.py @@ -1,5 +1,4 @@ import http -from typing import Optional from fastapi import FastAPI, Path, Query @@ -54,7 +53,7 @@ def get_bool_id(item_id: bool): @app.get("/path/param/{item_id}") -def get_path_param_id(item_id: Optional[str] = Path()): +def get_path_param_id(item_id: str | None = Path()): return item_id @@ -161,7 +160,7 @@ def get_query_type(query: int): @app.get("/query/int/optional") -def get_query_type_optional(query: Optional[int] = None): +def get_query_type_optional(query: int | None = None): if query is None: return "foo bar" return f"foo bar {query}" diff --git a/tests/test_additional_properties_bool.py b/tests/test_additional_properties_bool.py index c02841cde1..9a1e139eaa 100644 --- a/tests/test_additional_properties_bool.py +++ b/tests/test_additional_properties_bool.py @@ -1,5 +1,3 @@ -from typing import Union - from fastapi import FastAPI from fastapi.testclient import TestClient from inline_snapshot import snapshot @@ -19,7 +17,7 @@ app = FastAPI() @app.post("/") async def post( - foo: Union[Foo, None] = None, + foo: Foo | None = None, ): return foo diff --git a/tests/test_additional_responses_union_duplicate_anyof.py b/tests/test_additional_responses_union_duplicate_anyof.py index 5d833fce4a..401bc0a744 100644 --- a/tests/test_additional_responses_union_duplicate_anyof.py +++ b/tests/test_additional_responses_union_duplicate_anyof.py @@ -4,8 +4,6 @@ don't accumulate duplicate $ref entries in anyOf arrays. See https://github.com/fastapi/fastapi/pull/14463 """ -from typing import Union - from fastapi import FastAPI from fastapi.testclient import TestClient from inline_snapshot import snapshot @@ -23,7 +21,7 @@ class ModelB(BaseModel): app = FastAPI( responses={ 500: { - "model": Union[ModelA, ModelB], + "model": ModelA | ModelB, "content": {"application/json": {"examples": {"Case A": {"value": "a"}}}}, } } diff --git a/tests/test_callable_endpoint.py b/tests/test_callable_endpoint.py index 1882e9053a..28999d3833 100644 --- a/tests/test_callable_endpoint.py +++ b/tests/test_callable_endpoint.py @@ -1,11 +1,10 @@ from functools import partial -from typing import Optional from fastapi import FastAPI from fastapi.testclient import TestClient -def main(some_arg, q: Optional[str] = None): +def main(some_arg, q: str | None = None): return {"some_arg": some_arg, "q": q} diff --git a/tests/test_compat.py b/tests/test_compat.py index 0b5600f8f5..772bd305eb 100644 --- a/tests/test_compat.py +++ b/tests/test_compat.py @@ -1,5 +1,3 @@ -from typing import Union - from fastapi import FastAPI, UploadFile from fastapi._compat import ( Undefined, @@ -10,8 +8,6 @@ from fastapi.testclient import TestClient from pydantic import BaseModel, ConfigDict from pydantic.fields import FieldInfo -from .utils import needs_py310 - def test_model_field_default_required(): from fastapi._compat import v2 @@ -26,7 +22,7 @@ def test_complex(): app = FastAPI() @app.post("/") - def foo(foo: Union[str, list[int]]): + def foo(foo: str | list[int]): return foo client = TestClient(app) @@ -49,17 +45,17 @@ def test_propagates_pydantic2_model_config(): class EmbeddedModel(BaseModel): model_config = ConfigDict(arbitrary_types_allowed=True) - value: Union[str, Missing] = Missing() + value: str | Missing = Missing() class Model(BaseModel): model_config = ConfigDict( arbitrary_types_allowed=True, ) - value: Union[str, Missing] = Missing() + value: str | Missing = Missing() embedded_model: EmbeddedModel = EmbeddedModel() @app.post("/") - def foo(req: Model) -> dict[str, Union[str, None]]: + def foo(req: Model) -> dict[str, str | None]: return { "value": req.value or None, "embedded_value": req.embedded_model.value or None, @@ -89,7 +85,7 @@ def test_is_bytes_sequence_annotation_union(): # TODO: in theory this would allow declaring types that could be lists of bytes # to be read from files and other types, but I'm not even sure it's a good idea # to support it as a first class "feature" - assert is_bytes_sequence_annotation(Union[list[str], list[bytes]]) + assert is_bytes_sequence_annotation(list[str] | list[bytes]) def test_is_uploadfile_sequence_annotation(): @@ -97,21 +93,20 @@ def test_is_uploadfile_sequence_annotation(): # TODO: in theory this would allow declaring types that could be lists of UploadFile # and other types, but I'm not even sure it's a good idea to support it as a first # class "feature" - assert is_uploadfile_sequence_annotation(Union[list[str], list[UploadFile]]) + assert is_uploadfile_sequence_annotation(list[str] | list[UploadFile]) def test_serialize_sequence_value_with_optional_list(): """Test that serialize_sequence_value handles optional lists correctly.""" from fastapi._compat import v2 - field_info = FieldInfo(annotation=Union[list[str], None]) + field_info = FieldInfo(annotation=list[str] | None) field = v2.ModelField(name="items", field_info=field_info) result = v2.serialize_sequence_value(field=field, value=["a", "b", "c"]) assert result == ["a", "b", "c"] assert isinstance(result, list) -@needs_py310 def test_serialize_sequence_value_with_optional_list_pipe_union(): """Test that serialize_sequence_value handles optional lists correctly (with new syntax).""" from fastapi._compat import v2 @@ -125,9 +120,12 @@ def test_serialize_sequence_value_with_optional_list_pipe_union(): def test_serialize_sequence_value_with_none_first_in_union(): """Test that serialize_sequence_value handles Union[None, List[...]] correctly.""" + from typing import Union + from fastapi._compat import v2 - field_info = FieldInfo(annotation=Union[None, list[str]]) + # Use Union[None, list[str]] to ensure None comes first in the union args + field_info = FieldInfo(annotation=Union[None, list[str]]) # noqa: UP007 field = v2.ModelField(name="items", field_info=field_info) result = v2.serialize_sequence_value(field=field, value=["x", "y"]) assert result == ["x", "y"] diff --git a/tests/test_custom_middleware_exception.py b/tests/test_custom_middleware_exception.py index d9b81e7c2e..cf548f4aed 100644 --- a/tests/test_custom_middleware_exception.py +++ b/tests/test_custom_middleware_exception.py @@ -1,5 +1,4 @@ from pathlib import Path -from typing import Optional from fastapi import APIRouter, FastAPI, File, UploadFile from fastapi.exceptions import HTTPException @@ -17,7 +16,7 @@ class ContentSizeLimitMiddleware: max_content_size (optional): the maximum content size allowed in bytes, None for no limit """ - def __init__(self, app: APIRouter, max_content_size: Optional[int] = None): + def __init__(self, app: APIRouter, max_content_size: int | None = None): self.app = app self.max_content_size = max_content_size diff --git a/tests/test_custom_schema_fields.py b/tests/test_custom_schema_fields.py index 60b795e9ba..c907c54242 100644 --- a/tests/test_custom_schema_fields.py +++ b/tests/test_custom_schema_fields.py @@ -1,4 +1,4 @@ -from typing import Annotated, Optional +from typing import Annotated from fastapi import FastAPI from fastapi.testclient import TestClient @@ -10,9 +10,9 @@ app = FastAPI() class Item(BaseModel): name: str - description: Annotated[ - Optional[str], WithJsonSchema({"type": ["string", "null"]}) - ] = None + description: Annotated[str | None, WithJsonSchema({"type": ["string", "null"]})] = ( + None + ) model_config = { "json_schema_extra": { diff --git a/tests/test_dependency_contextvars.py b/tests/test_dependency_contextvars.py index 0c2e5594b6..eba135785b 100644 --- a/tests/test_dependency_contextvars.py +++ b/tests/test_dependency_contextvars.py @@ -1,11 +1,11 @@ -from collections.abc import Awaitable +from collections.abc import Awaitable, Callable from contextvars import ContextVar -from typing import Any, Callable, Optional +from typing import Any from fastapi import Depends, FastAPI, Request, Response from fastapi.testclient import TestClient -legacy_request_state_context_var: ContextVar[Optional[dict[str, Any]]] = ContextVar( +legacy_request_state_context_var: ContextVar[dict[str, Any] | None] = ContextVar( "legacy_request_state_context_var", default=None ) diff --git a/tests/test_dependency_overrides.py b/tests/test_dependency_overrides.py index e25db624d8..7c99d9d9d3 100644 --- a/tests/test_dependency_overrides.py +++ b/tests/test_dependency_overrides.py @@ -1,5 +1,3 @@ -from typing import Optional - import pytest from fastapi import APIRouter, Depends, FastAPI from fastapi.testclient import TestClient @@ -38,7 +36,7 @@ app.include_router(router) client = TestClient(app) -async def overrider_dependency_simple(q: Optional[str] = None): +async def overrider_dependency_simple(q: str | None = None): return {"q": q, "skip": 5, "limit": 10} diff --git a/tests/test_dependency_paramless.py b/tests/test_dependency_paramless.py index 1774196fe4..62c977b825 100644 --- a/tests/test_dependency_paramless.py +++ b/tests/test_dependency_paramless.py @@ -1,4 +1,4 @@ -from typing import Annotated, Union +from typing import Annotated from fastapi import FastAPI, HTTPException, Security from fastapi.security import ( @@ -13,7 +13,7 @@ oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") def process_auth( - credentials: Annotated[Union[str, None], Security(oauth2_scheme)], + credentials: Annotated[str | None, Security(oauth2_scheme)], security_scopes: SecurityScopes, ): # This is an incorrect way of using it, this is not checking if the scopes are diff --git a/tests/test_deprecated_responses.py b/tests/test_deprecated_responses.py new file mode 100644 index 0000000000..eff5792717 --- /dev/null +++ b/tests/test_deprecated_responses.py @@ -0,0 +1,73 @@ +import warnings + +import pytest +from fastapi import FastAPI +from fastapi.exceptions import FastAPIDeprecationWarning +from fastapi.responses import ORJSONResponse, UJSONResponse +from fastapi.testclient import TestClient +from pydantic import BaseModel + + +class Item(BaseModel): + name: str + price: float + + +# ORJSON + + +def _make_orjson_app() -> FastAPI: + with warnings.catch_warnings(): + warnings.simplefilter("ignore", FastAPIDeprecationWarning) + app = FastAPI(default_response_class=ORJSONResponse) + + @app.get("/items") + def get_items() -> Item: + return Item(name="widget", price=9.99) + + return app + + +def test_orjson_response_returns_correct_data(): + app = _make_orjson_app() + client = TestClient(app) + with warnings.catch_warnings(): + warnings.simplefilter("ignore", FastAPIDeprecationWarning) + response = client.get("/items") + assert response.status_code == 200 + assert response.json() == {"name": "widget", "price": 9.99} + + +def test_orjson_response_emits_deprecation_warning(): + with pytest.warns(FastAPIDeprecationWarning, match="ORJSONResponse is deprecated"): + ORJSONResponse(content={"hello": "world"}) + + +# UJSON + + +def _make_ujson_app() -> FastAPI: + with warnings.catch_warnings(): + warnings.simplefilter("ignore", FastAPIDeprecationWarning) + app = FastAPI(default_response_class=UJSONResponse) + + @app.get("/items") + def get_items() -> Item: + return Item(name="widget", price=9.99) + + return app + + +def test_ujson_response_returns_correct_data(): + app = _make_ujson_app() + client = TestClient(app) + with warnings.catch_warnings(): + warnings.simplefilter("ignore", FastAPIDeprecationWarning) + response = client.get("/items") + assert response.status_code == 200 + assert response.json() == {"name": "widget", "price": 9.99} + + +def test_ujson_response_emits_deprecation_warning(): + with pytest.warns(FastAPIDeprecationWarning, match="UJSONResponse is deprecated"): + UJSONResponse(content={"hello": "world"}) diff --git a/tests/test_dump_json_fast_path.py b/tests/test_dump_json_fast_path.py new file mode 100644 index 0000000000..d41d5aa66f --- /dev/null +++ b/tests/test_dump_json_fast_path.py @@ -0,0 +1,51 @@ +from unittest.mock import patch + +from fastapi import FastAPI +from fastapi.responses import JSONResponse +from fastapi.testclient import TestClient +from pydantic import BaseModel + + +class Item(BaseModel): + name: str + price: float + + +app = FastAPI() + + +@app.get("/default") +def get_default() -> Item: + return Item(name="widget", price=9.99) + + +@app.get("/explicit", response_class=JSONResponse) +def get_explicit() -> Item: + return Item(name="widget", price=9.99) + + +client = TestClient(app) + + +def test_default_response_class_skips_json_dumps(): + """When no response_class is set, the fast path serializes directly to + JSON bytes via Pydantic's dump_json and never calls json.dumps.""" + with patch( + "starlette.responses.json.dumps", wraps=__import__("json").dumps + ) as mock_dumps: + response = client.get("/default") + assert response.status_code == 200 + assert response.json() == {"name": "widget", "price": 9.99} + mock_dumps.assert_not_called() + + +def test_explicit_response_class_uses_json_dumps(): + """When response_class is explicitly set to JSONResponse, the normal path + is used and json.dumps is called via JSONResponse.render().""" + with patch( + "starlette.responses.json.dumps", wraps=__import__("json").dumps + ) as mock_dumps: + response = client.get("/explicit") + assert response.status_code == 200 + assert response.json() == {"name": "widget", "price": 9.99} + mock_dumps.assert_called_once() diff --git a/tests/test_enforce_once_required_parameter.py b/tests/test_enforce_once_required_parameter.py index 0dee15c25d..9f8a964543 100644 --- a/tests/test_enforce_once_required_parameter.py +++ b/tests/test_enforce_once_required_parameter.py @@ -1,5 +1,3 @@ -from typing import Optional - from fastapi import Depends, FastAPI, Query from fastapi.testclient import TestClient from inline_snapshot import snapshot @@ -11,7 +9,7 @@ def _get_client_key(client_id: str = Query(...)) -> str: return f"{client_id}_key" -def _get_client_tag(client_id: Optional[str] = Query(None)) -> Optional[str]: +def _get_client_tag(client_id: str | None = Query(None)) -> str | None: if client_id is None: return None return f"{client_id}_tag" @@ -20,7 +18,7 @@ def _get_client_tag(client_id: Optional[str] = Query(None)) -> Optional[str]: @app.get("/foo") def foo_handler( client_key: str = Depends(_get_client_key), - client_tag: Optional[str] = Depends(_get_client_tag), + client_tag: str | None = Depends(_get_client_tag), ): return {"client_id": client_key, "client_tag": client_tag} diff --git a/tests/test_extra_routes.py b/tests/test_extra_routes.py index 96f85b4465..985adb9439 100644 --- a/tests/test_extra_routes.py +++ b/tests/test_extra_routes.py @@ -1,5 +1,3 @@ -from typing import Optional - from fastapi import FastAPI from fastapi.responses import JSONResponse from fastapi.testclient import TestClient @@ -11,7 +9,7 @@ app = FastAPI() class Item(BaseModel): name: str - price: Optional[float] = None + price: float | None = None @app.api_route("/items/{item_id}", methods=["GET"]) diff --git a/tests/test_filter_pydantic_sub_model_pv2.py b/tests/test_filter_pydantic_sub_model_pv2.py index 1de2b50f7f..1f39581c23 100644 --- a/tests/test_filter_pydantic_sub_model_pv2.py +++ b/tests/test_filter_pydantic_sub_model_pv2.py @@ -1,5 +1,3 @@ -from typing import Optional - import pytest from dirty_equals import HasRepr from fastapi import Depends, FastAPI @@ -22,7 +20,7 @@ def get_client(): class ModelA(BaseModel): name: str - description: Optional[str] = None + description: str | None = None foo: ModelB tags: dict[str, str] = {} diff --git a/tests/test_form_default.py b/tests/test_form_default.py index 0b3eb8f2e2..c4d33e3fb6 100644 --- a/tests/test_form_default.py +++ b/tests/test_form_default.py @@ -1,4 +1,4 @@ -from typing import Annotated, Optional +from typing import Annotated from fastapi import FastAPI, File, Form from starlette.testclient import TestClient @@ -7,14 +7,14 @@ app = FastAPI() @app.post("/urlencoded") -async def post_url_encoded(age: Annotated[Optional[int], Form()] = None): +async def post_url_encoded(age: Annotated[int | None, Form()] = None): return age @app.post("/multipart") async def post_multi_part( - age: Annotated[Optional[int], Form()] = None, - file: Annotated[Optional[bytes], File()] = None, + age: Annotated[int | None, Form()] = None, + file: Annotated[bytes | None, File()] = None, ): return {"file": file, "age": age} diff --git a/tests/test_forms_single_model.py b/tests/test_forms_single_model.py index 7d03d29572..4575e3335e 100644 --- a/tests/test_forms_single_model.py +++ b/tests/test_forms_single_model.py @@ -1,4 +1,4 @@ -from typing import Annotated, Optional +from typing import Annotated from fastapi import FastAPI, Form from fastapi.testclient import TestClient @@ -10,7 +10,7 @@ app = FastAPI() class FormModel(BaseModel): username: str lastname: str - age: Optional[int] = None + age: int | None = None tags: list[str] = ["foo", "bar"] alias_with: str = Field(alias="with", default="nothing") diff --git a/tests/test_infer_param_optionality.py b/tests/test_infer_param_optionality.py index bb20a4a1aa..2cf74e187a 100644 --- a/tests/test_infer_param_optionality.py +++ b/tests/test_infer_param_optionality.py @@ -1,5 +1,3 @@ -from typing import Optional - from fastapi import APIRouter, FastAPI from fastapi.testclient import TestClient from inline_snapshot import snapshot @@ -22,7 +20,7 @@ def get_user(user_id: str): @item_router.get("/") -def get_items(user_id: Optional[str] = None): +def get_items(user_id: str | None = None): if user_id is None: return [{"item_id": "i1", "user_id": "u1"}, {"item_id": "i2", "user_id": "u2"}] else: @@ -30,7 +28,7 @@ def get_items(user_id: Optional[str] = None): @item_router.get("/{item_id}") -def get_item(item_id: str, user_id: Optional[str] = None): +def get_item(item_id: str, user_id: str | None = None): if user_id is None: return {"item_id": item_id} else: diff --git a/tests/test_invalid_sequence_param.py b/tests/test_invalid_sequence_param.py index 3695344f7a..d137f6805e 100644 --- a/tests/test_invalid_sequence_param.py +++ b/tests/test_invalid_sequence_param.py @@ -1,5 +1,3 @@ -from typing import Optional - import pytest from fastapi import FastAPI, Query from pydantic import BaseModel @@ -61,5 +59,5 @@ def test_invalid_simple_dict(): title: str @app.get("/items/") - def read_items(q: Optional[dict] = Query(default=None)): + def read_items(q: dict | None = Query(default=None)): pass # pragma: no cover diff --git a/tests/test_jsonable_encoder.py b/tests/test_jsonable_encoder.py index 4528dff440..595202beaf 100644 --- a/tests/test_jsonable_encoder.py +++ b/tests/test_jsonable_encoder.py @@ -6,7 +6,7 @@ from decimal import Decimal from enum import Enum from math import isinf, isnan from pathlib import PurePath, PurePosixPath, PureWindowsPath -from typing import Optional, TypedDict +from typing import TypedDict import pytest from fastapi._compat import Undefined @@ -57,7 +57,7 @@ class RoleEnum(Enum): class ModelWithConfig(BaseModel): - role: Optional[RoleEnum] = None + role: RoleEnum | None = None model_config = {"use_enum_values": True} diff --git a/tests/test_openapi_examples.py b/tests/test_openapi_examples.py index deb74d8a0f..e27dd2be08 100644 --- a/tests/test_openapi_examples.py +++ b/tests/test_openapi_examples.py @@ -1,5 +1,3 @@ -from typing import Union - from fastapi import Body, Cookie, FastAPI, Header, Path, Query from fastapi.testclient import TestClient from inline_snapshot import snapshot @@ -57,7 +55,7 @@ def path_examples( @app.get("/query_examples/") def query_examples( - data: Union[str, None] = Query( + data: str | None = Query( default=None, examples=[ "json_schema_query1", @@ -80,7 +78,7 @@ def query_examples( @app.get("/header_examples/") def header_examples( - data: Union[str, None] = Header( + data: str | None = Header( default=None, examples=[ "json_schema_header1", @@ -103,7 +101,7 @@ def header_examples( @app.get("/cookie_examples/") def cookie_examples( - data: Union[str, None] = Cookie( + data: str | None = Cookie( default=None, examples=["json_schema_cookie1", "json_schema_cookie2"], openapi_examples={ diff --git a/tests/test_openapi_query_parameter_extension.py b/tests/test_openapi_query_parameter_extension.py index 836a0a7ee5..118d518149 100644 --- a/tests/test_openapi_query_parameter_extension.py +++ b/tests/test_openapi_query_parameter_extension.py @@ -1,5 +1,3 @@ -from typing import Optional - from fastapi import FastAPI from fastapi.testclient import TestClient from inline_snapshot import snapshot @@ -26,7 +24,7 @@ app = FastAPI() ] }, ) -def route_with_extra_query_parameters(standard_query_param: Optional[int] = 50): +def route_with_extra_query_parameters(standard_query_param: int | None = 50): return {} diff --git a/tests/test_openapi_schema_type.py b/tests/test_openapi_schema_type.py index 98d9787455..e8166d2fb9 100644 --- a/tests/test_openapi_schema_type.py +++ b/tests/test_openapi_schema_type.py @@ -1,5 +1,3 @@ -from typing import Optional, Union - import pytest from fastapi.openapi.models import Schema, SchemaType @@ -13,7 +11,7 @@ from fastapi.openapi.models import Schema, SchemaType ], ) def test_allowed_schema_type( - type_value: Optional[Union[SchemaType, list[SchemaType]]], + type_value: SchemaType | list[SchemaType] | None, ) -> None: """Test that Schema accepts SchemaType, List[SchemaType] and None for type field.""" schema = Schema(type=type_value) diff --git a/tests/test_openapi_separate_input_output_schemas.py b/tests/test_openapi_separate_input_output_schemas.py index 0efeece017..50255ed09a 100644 --- a/tests/test_openapi_separate_input_output_schemas.py +++ b/tests/test_openapi_separate_input_output_schemas.py @@ -1,5 +1,3 @@ -from typing import Optional - from fastapi import FastAPI from fastapi.testclient import TestClient from inline_snapshot import snapshot @@ -8,15 +6,15 @@ from pydantic import BaseModel, computed_field class SubItem(BaseModel): subname: str - sub_description: Optional[str] = None + sub_description: str | None = None tags: list[str] = [] model_config = {"json_schema_serialization_defaults_required": True} class Item(BaseModel): name: str - description: Optional[str] = None - sub: Optional[SubItem] = None + description: str | None = None + sub: SubItem | None = None model_config = {"json_schema_serialization_defaults_required": True} diff --git a/tests/test_optional_file_list.py b/tests/test_optional_file_list.py index 6860258643..a57e6358fd 100644 --- a/tests/test_optional_file_list.py +++ b/tests/test_optional_file_list.py @@ -1,5 +1,3 @@ -from typing import Optional - from fastapi import FastAPI, File from fastapi.testclient import TestClient @@ -7,7 +5,7 @@ app = FastAPI() @app.post("/files") -async def upload_files(files: Optional[list[bytes]] = File(None)): +async def upload_files(files: list[bytes] | None = File(None)): if files is None: return {"files_count": 0} return {"files_count": len(files), "sizes": [len(f) for f in files]} diff --git a/tests/test_orjson_response_class.py b/tests/test_orjson_response_class.py index 6fe62daf97..63ea054d1f 100644 --- a/tests/test_orjson_response_class.py +++ b/tests/test_orjson_response_class.py @@ -1,9 +1,14 @@ +import warnings + from fastapi import FastAPI +from fastapi.exceptions import FastAPIDeprecationWarning from fastapi.responses import ORJSONResponse from fastapi.testclient import TestClient from sqlalchemy.sql.elements import quoted_name -app = FastAPI(default_response_class=ORJSONResponse) +with warnings.catch_warnings(): + warnings.simplefilter("ignore", FastAPIDeprecationWarning) + app = FastAPI(default_response_class=ORJSONResponse) @app.get("/orjson_non_str_keys") @@ -16,6 +21,8 @@ client = TestClient(app) def test_orjson_non_str_keys(): - with client: - response = client.get("/orjson_non_str_keys") + with warnings.catch_warnings(): + warnings.simplefilter("ignore", FastAPIDeprecationWarning) + with client: + response = client.get("/orjson_non_str_keys") assert response.json() == {"msg": "Hello World", "1": 1} diff --git a/tests/test_param_class.py b/tests/test_param_class.py index 1fd40dcd21..e6642daeac 100644 --- a/tests/test_param_class.py +++ b/tests/test_param_class.py @@ -1,5 +1,3 @@ -from typing import Optional - from fastapi import FastAPI from fastapi.params import Param from fastapi.testclient import TestClient @@ -8,7 +6,7 @@ app = FastAPI() @app.get("/items/") -def read_items(q: Optional[str] = Param(default=None)): # type: ignore +def read_items(q: str | None = Param(default=None)): # type: ignore return {"q": q} diff --git a/tests/test_param_include_in_schema.py b/tests/test_param_include_in_schema.py index 463fb51b10..727552b466 100644 --- a/tests/test_param_include_in_schema.py +++ b/tests/test_param_include_in_schema.py @@ -1,5 +1,3 @@ -from typing import Optional - import pytest from fastapi import Cookie, FastAPI, Header, Path, Query from fastapi.testclient import TestClient @@ -10,14 +8,14 @@ app = FastAPI() @app.get("/hidden_cookie") async def hidden_cookie( - hidden_cookie: Optional[str] = Cookie(default=None, include_in_schema=False), + hidden_cookie: str | None = Cookie(default=None, include_in_schema=False), ): return {"hidden_cookie": hidden_cookie} @app.get("/hidden_header") async def hidden_header( - hidden_header: Optional[str] = Header(default=None, include_in_schema=False), + hidden_header: str | None = Header(default=None, include_in_schema=False), ): return {"hidden_header": hidden_header} @@ -29,7 +27,7 @@ async def hidden_path(hidden_path: str = Path(include_in_schema=False)): @app.get("/hidden_query") async def hidden_query( - hidden_query: Optional[str] = Query(default=None, include_in_schema=False), + hidden_query: str | None = Query(default=None, include_in_schema=False), ): return {"hidden_query": hidden_query} diff --git a/tests/test_pydantic_v1_error.py b/tests/test_pydantic_v1_error.py index 13229a3137..044fdf0d65 100644 --- a/tests/test_pydantic_v1_error.py +++ b/tests/test_pydantic_v1_error.py @@ -1,6 +1,5 @@ import sys import warnings -from typing import Union import pytest @@ -80,7 +79,7 @@ def test_raises_pydantic_v1_model_in_union() -> None: with pytest.raises(PydanticV1NotSupportedError): @app.post("/union") - def endpoint(data: Union[dict, ModelV1A]): # pragma: no cover + def endpoint(data: dict | ModelV1A): # pragma: no cover return data diff --git a/tests/test_pydanticv2_dataclasses_uuid_stringified_annotations.py b/tests/test_pydanticv2_dataclasses_uuid_stringified_annotations.py index b72b0518a1..4f7b0b2a0a 100644 --- a/tests/test_pydanticv2_dataclasses_uuid_stringified_annotations.py +++ b/tests/test_pydanticv2_dataclasses_uuid_stringified_annotations.py @@ -2,7 +2,6 @@ from __future__ import annotations import uuid from dataclasses import dataclass, field -from typing import Union from dirty_equals import IsUUID from fastapi import FastAPI @@ -16,8 +15,8 @@ class Item: name: str price: float tags: list[str] = field(default_factory=list) - description: Union[str, None] = None - tax: Union[float, None] = None + description: str | None = None + tax: float | None = None app = FastAPI() diff --git a/tests/test_request_params/test_body/test_list.py b/tests/test_request_params/test_body/test_list.py index 970e6a6607..aa9745f84f 100644 --- a/tests/test_request_params/test_body/test_list.py +++ b/tests/test_request_params/test_body/test_list.py @@ -1,4 +1,4 @@ -from typing import Annotated, Union +from typing import Annotated import pytest from dirty_equals import IsOneOf, IsPartialDict @@ -55,7 +55,7 @@ def test_required_list_str_schema(path: str): "path", ["/required-list-str", "/model-required-list-str"], ) -def test_required_list_str_missing(path: str, json: Union[dict, None]): +def test_required_list_str_missing(path: str, json: dict | None): client = TestClient(app) response = client.post(path, json=json) assert response.status_code == 422 @@ -132,7 +132,7 @@ def test_required_list_str_alias_schema(path: str): "path", ["/required-list-alias", "/model-required-list-alias"], ) -def test_required_list_alias_missing(path: str, json: Union[dict, None]): +def test_required_list_alias_missing(path: str, json: dict | None): client = TestClient(app) response = client.post(path, json=json) assert response.status_code == 422 @@ -236,7 +236,7 @@ def test_required_list_validation_alias_schema(path: str): "/model-required-list-validation-alias", ], ) -def test_required_list_validation_alias_missing(path: str, json: Union[dict, None]): +def test_required_list_validation_alias_missing(path: str, json: dict | None): client = TestClient(app) response = client.post(path, json=json) assert response.status_code == 422 diff --git a/tests/test_request_params/test_body/test_optional_list.py b/tests/test_request_params/test_body/test_optional_list.py index ba8ba9092e..2c5c5d61b6 100644 --- a/tests/test_request_params/test_body/test_optional_list.py +++ b/tests/test_request_params/test_body/test_optional_list.py @@ -1,4 +1,4 @@ -from typing import Annotated, Optional +from typing import Annotated import pytest from fastapi import Body, FastAPI @@ -15,13 +15,13 @@ app = FastAPI() @app.post("/optional-list-str", operation_id="optional_list_str") async def read_optional_list_str( - p: Annotated[Optional[list[str]], Body(embed=True)] = None, + p: Annotated[list[str] | None, Body(embed=True)] = None, ): return {"p": p} class BodyModelOptionalListStr(BaseModel): - p: Optional[list[str]] = None + p: list[str] | None = None @app.post("/model-optional-list-str", operation_id="model_optional_list_str") @@ -103,13 +103,13 @@ def test_optional_list_str(path: str): @app.post("/optional-list-alias", operation_id="optional_list_alias") async def read_optional_list_alias( - p: Annotated[Optional[list[str]], Body(embed=True, alias="p_alias")] = None, + p: Annotated[list[str] | None, Body(embed=True, alias="p_alias")] = None, ): return {"p": p} class BodyModelOptionalListAlias(BaseModel): - p: Optional[list[str]] = Field(None, alias="p_alias") + p: list[str] | None = Field(None, alias="p_alias") @app.post("/model-optional-list-alias", operation_id="model_optional_list_alias") @@ -208,14 +208,14 @@ def test_optional_list_alias_by_alias(path: str): ) def read_optional_list_validation_alias( p: Annotated[ - Optional[list[str]], Body(embed=True, validation_alias="p_val_alias") + list[str] | None, Body(embed=True, validation_alias="p_val_alias") ] = None, ): return {"p": p} class BodyModelOptionalListValidationAlias(BaseModel): - p: Optional[list[str]] = Field(None, validation_alias="p_val_alias") + p: list[str] | None = Field(None, validation_alias="p_val_alias") @app.post( @@ -323,7 +323,7 @@ def test_optional_list_validation_alias_by_validation_alias(path: str): ) def read_optional_list_alias_and_validation_alias( p: Annotated[ - Optional[list[str]], + list[str] | None, Body(embed=True, alias="p_alias", validation_alias="p_val_alias"), ] = None, ): @@ -331,9 +331,7 @@ def read_optional_list_alias_and_validation_alias( class BodyModelOptionalListAliasAndValidationAlias(BaseModel): - p: Optional[list[str]] = Field( - None, alias="p_alias", validation_alias="p_val_alias" - ) + p: list[str] | None = Field(None, alias="p_alias", validation_alias="p_val_alias") @app.post( diff --git a/tests/test_request_params/test_body/test_optional_str.py b/tests/test_request_params/test_body/test_optional_str.py index b9c18034da..184fc94cb2 100644 --- a/tests/test_request_params/test_body/test_optional_str.py +++ b/tests/test_request_params/test_body/test_optional_str.py @@ -1,4 +1,4 @@ -from typing import Annotated, Optional +from typing import Annotated import pytest from fastapi import Body, FastAPI @@ -14,12 +14,12 @@ app = FastAPI() @app.post("/optional-str", operation_id="optional_str") -async def read_optional_str(p: Annotated[Optional[str], Body(embed=True)] = None): +async def read_optional_str(p: Annotated[str | None, Body(embed=True)] = None): return {"p": p} class BodyModelOptionalStr(BaseModel): - p: Optional[str] = None + p: str | None = None @app.post("/model-optional-str", operation_id="model_optional_str") @@ -98,13 +98,13 @@ def test_optional_str(path: str): @app.post("/optional-alias", operation_id="optional_alias") async def read_optional_alias( - p: Annotated[Optional[str], Body(embed=True, alias="p_alias")] = None, + p: Annotated[str | None, Body(embed=True, alias="p_alias")] = None, ): return {"p": p} class BodyModelOptionalAlias(BaseModel): - p: Optional[str] = Field(None, alias="p_alias") + p: str | None = Field(None, alias="p_alias") @app.post("/model-optional-alias", operation_id="model_optional_alias") @@ -197,15 +197,13 @@ def test_optional_alias_by_alias(path: str): @app.post("/optional-validation-alias", operation_id="optional_validation_alias") def read_optional_validation_alias( - p: Annotated[ - Optional[str], Body(embed=True, validation_alias="p_val_alias") - ] = None, + p: Annotated[str | None, Body(embed=True, validation_alias="p_val_alias")] = None, ): return {"p": p} class BodyModelOptionalValidationAlias(BaseModel): - p: Optional[str] = Field(None, validation_alias="p_val_alias") + p: str | None = Field(None, validation_alias="p_val_alias") @app.post( @@ -309,14 +307,14 @@ def test_optional_validation_alias_by_validation_alias(path: str): ) def read_optional_alias_and_validation_alias( p: Annotated[ - Optional[str], Body(embed=True, alias="p_alias", validation_alias="p_val_alias") + str | None, Body(embed=True, alias="p_alias", validation_alias="p_val_alias") ] = None, ): return {"p": p} class BodyModelOptionalAliasAndValidationAlias(BaseModel): - p: Optional[str] = Field(None, alias="p_alias", validation_alias="p_val_alias") + p: str | None = Field(None, alias="p_alias", validation_alias="p_val_alias") @app.post( diff --git a/tests/test_request_params/test_body/test_required_str.py b/tests/test_request_params/test_body/test_required_str.py index 5b434fa1db..2e02f8d203 100644 --- a/tests/test_request_params/test_body/test_required_str.py +++ b/tests/test_request_params/test_body/test_required_str.py @@ -1,4 +1,4 @@ -from typing import Annotated, Any, Union +from typing import Annotated, Any import pytest from dirty_equals import IsOneOf @@ -51,7 +51,7 @@ def test_required_str_schema(path: str): "path", ["/required-str", "/model-required-str"], ) -def test_required_str_missing(path: str, json: Union[dict[str, Any], None]): +def test_required_str_missing(path: str, json: dict[str, Any] | None): client = TestClient(app) response = client.post(path, json=json) assert response.status_code == 422 @@ -124,7 +124,7 @@ def test_required_str_alias_schema(path: str): "path", ["/required-alias", "/model-required-alias"], ) -def test_required_alias_missing(path: str, json: Union[dict[str, Any], None]): +def test_required_alias_missing(path: str, json: dict[str, Any] | None): client = TestClient(app) response = client.post(path, json=json) assert response.status_code == 422 @@ -221,9 +221,7 @@ def test_required_validation_alias_schema(path: str): "/model-required-validation-alias", ], ) -def test_required_validation_alias_missing( - path: str, json: Union[dict[str, Any], None] -): +def test_required_validation_alias_missing(path: str, json: dict[str, Any] | None): client = TestClient(app) response = client.post(path, json=json) assert response.status_code == 422 @@ -338,7 +336,7 @@ def test_required_alias_and_validation_alias_schema(path: str): ], ) def test_required_alias_and_validation_alias_missing( - path: str, json: Union[dict[str, Any], None] + path: str, json: dict[str, Any] | None ): client = TestClient(app) response = client.post(path, json=json) diff --git a/tests/test_request_params/test_cookie/test_optional_str.py b/tests/test_request_params/test_cookie/test_optional_str.py index 1b2a18b072..227d2bccc2 100644 --- a/tests/test_request_params/test_cookie/test_optional_str.py +++ b/tests/test_request_params/test_cookie/test_optional_str.py @@ -1,4 +1,4 @@ -from typing import Annotated, Optional +from typing import Annotated import pytest from fastapi import Cookie, FastAPI @@ -13,12 +13,12 @@ app = FastAPI() @app.get("/optional-str") -async def read_optional_str(p: Annotated[Optional[str], Cookie()] = None): +async def read_optional_str(p: Annotated[str | None, Cookie()] = None): return {"p": p} class CookieModelOptionalStr(BaseModel): - p: Optional[str] = None + p: str | None = None @app.get("/model-optional-str") @@ -75,13 +75,13 @@ def test_optional_str(path: str): @app.get("/optional-alias") async def read_optional_alias( - p: Annotated[Optional[str], Cookie(alias="p_alias")] = None, + p: Annotated[str | None, Cookie(alias="p_alias")] = None, ): return {"p": p} class CookieModelOptionalAlias(BaseModel): - p: Optional[str] = Field(None, alias="p_alias") + p: str | None = Field(None, alias="p_alias") @app.get("/model-optional-alias") @@ -153,13 +153,13 @@ def test_optional_alias_by_alias(path: str): @app.get("/optional-validation-alias") def read_optional_validation_alias( - p: Annotated[Optional[str], Cookie(validation_alias="p_val_alias")] = None, + p: Annotated[str | None, Cookie(validation_alias="p_val_alias")] = None, ): return {"p": p} class CookieModelOptionalValidationAlias(BaseModel): - p: Optional[str] = Field(None, validation_alias="p_val_alias") + p: str | None = Field(None, validation_alias="p_val_alias") @app.get("/model-optional-validation-alias") @@ -237,14 +237,14 @@ def test_optional_validation_alias_by_validation_alias(path: str): @app.get("/optional-alias-and-validation-alias") def read_optional_alias_and_validation_alias( p: Annotated[ - Optional[str], Cookie(alias="p_alias", validation_alias="p_val_alias") + str | None, Cookie(alias="p_alias", validation_alias="p_val_alias") ] = None, ): return {"p": p} class CookieModelOptionalAliasAndValidationAlias(BaseModel): - p: Optional[str] = Field(None, alias="p_alias", validation_alias="p_val_alias") + p: str | None = Field(None, alias="p_alias", validation_alias="p_val_alias") @app.get("/model-optional-alias-and-validation-alias") diff --git a/tests/test_request_params/test_file/test_list.py b/tests/test_request_params/test_file/test_list.py index 68280fcf32..5332795f4c 100644 --- a/tests/test_request_params/test_file/test_list.py +++ b/tests/test_request_params/test_file/test_list.py @@ -37,7 +37,10 @@ def test_list_schema(path: str): "properties": { "p": { "type": "array", - "items": {"type": "string", "format": "binary"}, + "items": { + "type": "string", + "contentMediaType": "application/octet-stream", + }, "title": "P", }, }, @@ -115,7 +118,10 @@ def test_list_alias_schema(path: str): "properties": { "p_alias": { "type": "array", - "items": {"type": "string", "format": "binary"}, + "items": { + "type": "string", + "contentMediaType": "application/octet-stream", + }, "title": "P Alias", }, }, @@ -221,7 +227,10 @@ def test_list_validation_alias_schema(path: str): "properties": { "p_val_alias": { "type": "array", - "items": {"type": "string", "format": "binary"}, + "items": { + "type": "string", + "contentMediaType": "application/octet-stream", + }, "title": "P Val Alias", }, }, @@ -338,7 +347,10 @@ def test_list_alias_and_validation_alias_schema(path: str): "properties": { "p_val_alias": { "type": "array", - "items": {"type": "string", "format": "binary"}, + "items": { + "type": "string", + "contentMediaType": "application/octet-stream", + }, "title": "P Val Alias", }, }, diff --git a/tests/test_request_params/test_file/test_optional.py b/tests/test_request_params/test_file/test_optional.py index 45ef7bdec4..3d1aac25e2 100644 --- a/tests/test_request_params/test_file/test_optional.py +++ b/tests/test_request_params/test_file/test_optional.py @@ -1,4 +1,4 @@ -from typing import Annotated, Optional +from typing import Annotated import pytest from fastapi import FastAPI, File, UploadFile @@ -13,12 +13,12 @@ app = FastAPI() @app.post("/optional-bytes", operation_id="optional_bytes") -async def read_optional_bytes(p: Annotated[Optional[bytes], File()] = None): +async def read_optional_bytes(p: Annotated[bytes | None, File()] = None): return {"file_size": len(p) if p else None} @app.post("/optional-uploadfile", operation_id="optional_uploadfile") -async def read_optional_uploadfile(p: Annotated[Optional[UploadFile], File()] = None): +async def read_optional_uploadfile(p: Annotated[UploadFile | None, File()] = None): return {"file_size": p.size if p else None} @@ -37,7 +37,7 @@ def test_optional_schema(path: str): "properties": { "p": { "anyOf": [ - {"type": "string", "format": "binary"}, + {"type": "string", "contentMediaType": "application/octet-stream"}, {"type": "null"}, ], "title": "P", @@ -82,14 +82,14 @@ def test_optional(path: str): @app.post("/optional-bytes-alias", operation_id="optional_bytes_alias") async def read_optional_bytes_alias( - p: Annotated[Optional[bytes], File(alias="p_alias")] = None, + p: Annotated[bytes | None, File(alias="p_alias")] = None, ): return {"file_size": len(p) if p else None} @app.post("/optional-uploadfile-alias", operation_id="optional_uploadfile_alias") async def read_optional_uploadfile_alias( - p: Annotated[Optional[UploadFile], File(alias="p_alias")] = None, + p: Annotated[UploadFile | None, File(alias="p_alias")] = None, ): return {"file_size": p.size if p else None} @@ -109,7 +109,7 @@ def test_optional_alias_schema(path: str): "properties": { "p_alias": { "anyOf": [ - {"type": "string", "format": "binary"}, + {"type": "string", "contentMediaType": "application/octet-stream"}, {"type": "null"}, ], "title": "P Alias", @@ -170,7 +170,7 @@ def test_optional_alias_by_alias(path: str): "/optional-bytes-validation-alias", operation_id="optional_bytes_validation_alias" ) def read_optional_bytes_validation_alias( - p: Annotated[Optional[bytes], File(validation_alias="p_val_alias")] = None, + p: Annotated[bytes | None, File(validation_alias="p_val_alias")] = None, ): return {"file_size": len(p) if p else None} @@ -180,7 +180,7 @@ def read_optional_bytes_validation_alias( operation_id="optional_uploadfile_validation_alias", ) def read_optional_uploadfile_validation_alias( - p: Annotated[Optional[UploadFile], File(validation_alias="p_val_alias")] = None, + p: Annotated[UploadFile | None, File(validation_alias="p_val_alias")] = None, ): return {"file_size": p.size if p else None} @@ -200,7 +200,7 @@ def test_optional_validation_alias_schema(path: str): "properties": { "p_val_alias": { "anyOf": [ - {"type": "string", "format": "binary"}, + {"type": "string", "contentMediaType": "application/octet-stream"}, {"type": "null"}, ], "title": "P Val Alias", @@ -263,7 +263,7 @@ def test_optional_validation_alias_by_validation_alias(path: str): ) def read_optional_bytes_alias_and_validation_alias( p: Annotated[ - Optional[bytes], File(alias="p_alias", validation_alias="p_val_alias") + bytes | None, File(alias="p_alias", validation_alias="p_val_alias") ] = None, ): return {"file_size": len(p) if p else None} @@ -275,7 +275,7 @@ def read_optional_bytes_alias_and_validation_alias( ) def read_optional_uploadfile_alias_and_validation_alias( p: Annotated[ - Optional[UploadFile], File(alias="p_alias", validation_alias="p_val_alias") + UploadFile | None, File(alias="p_alias", validation_alias="p_val_alias") ] = None, ): return {"file_size": p.size if p else None} @@ -296,7 +296,7 @@ def test_optional_alias_and_validation_alias_schema(path: str): "properties": { "p_val_alias": { "anyOf": [ - {"type": "string", "format": "binary"}, + {"type": "string", "contentMediaType": "application/octet-stream"}, {"type": "null"}, ], "title": "P Val Alias", diff --git a/tests/test_request_params/test_file/test_optional_list.py b/tests/test_request_params/test_file/test_optional_list.py index 162fbe08ae..3c211b1e8e 100644 --- a/tests/test_request_params/test_file/test_optional_list.py +++ b/tests/test_request_params/test_file/test_optional_list.py @@ -1,4 +1,4 @@ -from typing import Annotated, Optional +from typing import Annotated import pytest from fastapi import FastAPI, File, UploadFile @@ -13,13 +13,13 @@ app = FastAPI() @app.post("/optional-list-bytes") -async def read_optional_list_bytes(p: Annotated[Optional[list[bytes]], File()] = None): +async def read_optional_list_bytes(p: Annotated[list[bytes] | None, File()] = None): return {"file_size": [len(file) for file in p] if p else None} @app.post("/optional-list-uploadfile") async def read_optional_list_uploadfile( - p: Annotated[Optional[list[UploadFile]], File()] = None, + p: Annotated[list[UploadFile] | None, File()] = None, ): return {"file_size": [file.size for file in p] if p else None} @@ -41,7 +41,10 @@ def test_optional_list_schema(path: str): "anyOf": [ { "type": "array", - "items": {"type": "string", "format": "binary"}, + "items": { + "type": "string", + "contentMediaType": "application/octet-stream", + }, }, {"type": "null"}, ], @@ -87,14 +90,14 @@ def test_optional_list(path: str): @app.post("/optional-list-bytes-alias") async def read_optional_list_bytes_alias( - p: Annotated[Optional[list[bytes]], File(alias="p_alias")] = None, + p: Annotated[list[bytes] | None, File(alias="p_alias")] = None, ): return {"file_size": [len(file) for file in p] if p else None} @app.post("/optional-list-uploadfile-alias") async def read_optional_list_uploadfile_alias( - p: Annotated[Optional[list[UploadFile]], File(alias="p_alias")] = None, + p: Annotated[list[UploadFile] | None, File(alias="p_alias")] = None, ): return {"file_size": [file.size for file in p] if p else None} @@ -116,7 +119,10 @@ def test_optional_list_alias_schema(path: str): "anyOf": [ { "type": "array", - "items": {"type": "string", "format": "binary"}, + "items": { + "type": "string", + "contentMediaType": "application/octet-stream", + }, }, {"type": "null"}, ], @@ -176,16 +182,14 @@ def test_optional_list_alias_by_alias(path: str): @app.post("/optional-list-bytes-validation-alias") def read_optional_list_bytes_validation_alias( - p: Annotated[Optional[list[bytes]], File(validation_alias="p_val_alias")] = None, + p: Annotated[list[bytes] | None, File(validation_alias="p_val_alias")] = None, ): return {"file_size": [len(file) for file in p] if p else None} @app.post("/optional-list-uploadfile-validation-alias") def read_optional_list_uploadfile_validation_alias( - p: Annotated[ - Optional[list[UploadFile]], File(validation_alias="p_val_alias") - ] = None, + p: Annotated[list[UploadFile] | None, File(validation_alias="p_val_alias")] = None, ): return {"file_size": [file.size for file in p] if p else None} @@ -207,7 +211,10 @@ def test_optional_validation_alias_schema(path: str): "anyOf": [ { "type": "array", - "items": {"type": "string", "format": "binary"}, + "items": { + "type": "string", + "contentMediaType": "application/octet-stream", + }, }, {"type": "null"}, ], @@ -270,7 +277,7 @@ def test_optional_validation_alias_by_validation_alias(path: str): @app.post("/optional-list-bytes-alias-and-validation-alias") def read_optional_list_bytes_alias_and_validation_alias( p: Annotated[ - Optional[list[bytes]], File(alias="p_alias", validation_alias="p_val_alias") + list[bytes] | None, File(alias="p_alias", validation_alias="p_val_alias") ] = None, ): return {"file_size": [len(file) for file in p] if p else None} @@ -279,7 +286,7 @@ def read_optional_list_bytes_alias_and_validation_alias( @app.post("/optional-list-uploadfile-alias-and-validation-alias") def read_optional_list_uploadfile_alias_and_validation_alias( p: Annotated[ - Optional[list[UploadFile]], + list[UploadFile] | None, File(alias="p_alias", validation_alias="p_val_alias"), ] = None, ): @@ -303,7 +310,10 @@ def test_optional_list_alias_and_validation_alias_schema(path: str): "anyOf": [ { "type": "array", - "items": {"type": "string", "format": "binary"}, + "items": { + "type": "string", + "contentMediaType": "application/octet-stream", + }, }, {"type": "null"}, ], diff --git a/tests/test_request_params/test_file/test_required.py b/tests/test_request_params/test_file/test_required.py index a0f9d23a6b..22d6c0fffd 100644 --- a/tests/test_request_params/test_file/test_required.py +++ b/tests/test_request_params/test_file/test_required.py @@ -35,7 +35,11 @@ def test_required_schema(path: str): assert app.openapi()["components"]["schemas"][body_model_name] == { "properties": { - "p": {"title": "P", "type": "string", "format": "binary"}, + "p": { + "title": "P", + "type": "string", + "contentMediaType": "application/octet-stream", + }, }, "required": ["p"], "title": body_model_name, @@ -109,7 +113,11 @@ def test_required_alias_schema(path: str): assert app.openapi()["components"]["schemas"][body_model_name] == { "properties": { - "p_alias": {"title": "P Alias", "type": "string", "format": "binary"}, + "p_alias": { + "title": "P Alias", + "type": "string", + "contentMediaType": "application/octet-stream", + }, }, "required": ["p_alias"], "title": body_model_name, @@ -216,7 +224,7 @@ def test_required_validation_alias_schema(path: str): "p_val_alias": { "title": "P Val Alias", "type": "string", - "format": "binary", + "contentMediaType": "application/octet-stream", }, }, "required": ["p_val_alias"], @@ -329,7 +337,7 @@ def test_required_alias_and_validation_alias_schema(path: str): "p_val_alias": { "title": "P Val Alias", "type": "string", - "format": "binary", + "contentMediaType": "application/octet-stream", }, }, "required": ["p_val_alias"], diff --git a/tests/test_request_params/test_form/test_optional_list.py b/tests/test_request_params/test_form/test_optional_list.py index 6d1957a18c..7ecf9c9bfc 100644 --- a/tests/test_request_params/test_form/test_optional_list.py +++ b/tests/test_request_params/test_form/test_optional_list.py @@ -1,4 +1,4 @@ -from typing import Annotated, Optional +from typing import Annotated import pytest from fastapi import FastAPI, Form @@ -15,13 +15,13 @@ app = FastAPI() @app.post("/optional-list-str", operation_id="optional_list_str") async def read_optional_list_str( - p: Annotated[Optional[list[str]], Form()] = None, + p: Annotated[list[str] | None, Form()] = None, ): return {"p": p} class FormModelOptionalListStr(BaseModel): - p: Optional[list[str]] = None + p: list[str] | None = None @app.post("/model-optional-list-str", operation_id="model_optional_list_str") @@ -80,13 +80,13 @@ def test_optional_list_str(path: str): @app.post("/optional-list-alias", operation_id="optional_list_alias") async def read_optional_list_alias( - p: Annotated[Optional[list[str]], Form(alias="p_alias")] = None, + p: Annotated[list[str] | None, Form(alias="p_alias")] = None, ): return {"p": p} class FormModelOptionalListAlias(BaseModel): - p: Optional[list[str]] = Field(None, alias="p_alias") + p: list[str] | None = Field(None, alias="p_alias") @app.post("/model-optional-list-alias", operation_id="model_optional_list_alias") @@ -163,13 +163,13 @@ def test_optional_list_alias_by_alias(path: str): "/optional-list-validation-alias", operation_id="optional_list_validation_alias" ) def read_optional_list_validation_alias( - p: Annotated[Optional[list[str]], Form(validation_alias="p_val_alias")] = None, + p: Annotated[list[str] | None, Form(validation_alias="p_val_alias")] = None, ): return {"p": p} class FormModelOptionalListValidationAlias(BaseModel): - p: Optional[list[str]] = Field(None, validation_alias="p_val_alias") + p: list[str] | None = Field(None, validation_alias="p_val_alias") @app.post( @@ -251,16 +251,14 @@ def test_optional_list_validation_alias_by_validation_alias(path: str): ) def read_optional_list_alias_and_validation_alias( p: Annotated[ - Optional[list[str]], Form(alias="p_alias", validation_alias="p_val_alias") + list[str] | None, Form(alias="p_alias", validation_alias="p_val_alias") ] = None, ): return {"p": p} class FormModelOptionalListAliasAndValidationAlias(BaseModel): - p: Optional[list[str]] = Field( - None, alias="p_alias", validation_alias="p_val_alias" - ) + p: list[str] | None = Field(None, alias="p_alias", validation_alias="p_val_alias") @app.post( diff --git a/tests/test_request_params/test_form/test_optional_str.py b/tests/test_request_params/test_form/test_optional_str.py index 810e83caa3..4ef16756ef 100644 --- a/tests/test_request_params/test_form/test_optional_str.py +++ b/tests/test_request_params/test_form/test_optional_str.py @@ -1,4 +1,4 @@ -from typing import Annotated, Optional +from typing import Annotated import pytest from fastapi import FastAPI, Form @@ -14,12 +14,12 @@ app = FastAPI() @app.post("/optional-str", operation_id="optional_str") -async def read_optional_str(p: Annotated[Optional[str], Form()] = None): +async def read_optional_str(p: Annotated[str | None, Form()] = None): return {"p": p} class FormModelOptionalStr(BaseModel): - p: Optional[str] = None + p: str | None = None @app.post("/model-optional-str", operation_id="model_optional_str") @@ -75,13 +75,13 @@ def test_optional_str(path: str): @app.post("/optional-alias", operation_id="optional_alias") async def read_optional_alias( - p: Annotated[Optional[str], Form(alias="p_alias")] = None, + p: Annotated[str | None, Form(alias="p_alias")] = None, ): return {"p": p} class FormModelOptionalAlias(BaseModel): - p: Optional[str] = Field(None, alias="p_alias") + p: str | None = Field(None, alias="p_alias") @app.post("/model-optional-alias", operation_id="model_optional_alias") @@ -151,13 +151,13 @@ def test_optional_alias_by_alias(path: str): @app.post("/optional-validation-alias", operation_id="optional_validation_alias") def read_optional_validation_alias( - p: Annotated[Optional[str], Form(validation_alias="p_val_alias")] = None, + p: Annotated[str | None, Form(validation_alias="p_val_alias")] = None, ): return {"p": p} class FormModelOptionalValidationAlias(BaseModel): - p: Optional[str] = Field(None, validation_alias="p_val_alias") + p: str | None = Field(None, validation_alias="p_val_alias") @app.post( @@ -238,14 +238,14 @@ def test_optional_validation_alias_by_validation_alias(path: str): ) def read_optional_alias_and_validation_alias( p: Annotated[ - Optional[str], Form(alias="p_alias", validation_alias="p_val_alias") + str | None, Form(alias="p_alias", validation_alias="p_val_alias") ] = None, ): return {"p": p} class FormModelOptionalAliasAndValidationAlias(BaseModel): - p: Optional[str] = Field(None, alias="p_alias", validation_alias="p_val_alias") + p: str | None = Field(None, alias="p_alias", validation_alias="p_val_alias") @app.post( diff --git a/tests/test_request_params/test_header/test_optional_list.py b/tests/test_request_params/test_header/test_optional_list.py index 3bbb73d544..9f4eacc235 100644 --- a/tests/test_request_params/test_header/test_optional_list.py +++ b/tests/test_request_params/test_header/test_optional_list.py @@ -1,4 +1,4 @@ -from typing import Annotated, Optional +from typing import Annotated import pytest from fastapi import FastAPI, Header @@ -14,13 +14,13 @@ app = FastAPI() @app.get("/optional-list-str") async def read_optional_list_str( - p: Annotated[Optional[list[str]], Header()] = None, + p: Annotated[list[str] | None, Header()] = None, ): return {"p": p} class HeaderModelOptionalListStr(BaseModel): - p: Optional[list[str]] = None + p: list[str] | None = None @app.get("/model-optional-list-str") @@ -81,13 +81,13 @@ def test_optional_list_str(path: str): @app.get("/optional-list-alias") async def read_optional_list_alias( - p: Annotated[Optional[list[str]], Header(alias="p_alias")] = None, + p: Annotated[list[str] | None, Header(alias="p_alias")] = None, ): return {"p": p} class HeaderModelOptionalListAlias(BaseModel): - p: Optional[list[str]] = Field(None, alias="p_alias") + p: list[str] | None = Field(None, alias="p_alias") @app.get("/model-optional-list-alias") @@ -162,13 +162,13 @@ def test_optional_list_alias_by_alias(path: str): @app.get("/optional-list-validation-alias") def read_optional_list_validation_alias( - p: Annotated[Optional[list[str]], Header(validation_alias="p_val_alias")] = None, + p: Annotated[list[str] | None, Header(validation_alias="p_val_alias")] = None, ): return {"p": p} class HeaderModelOptionalListValidationAlias(BaseModel): - p: Optional[list[str]] = Field(None, validation_alias="p_val_alias") + p: list[str] | None = Field(None, validation_alias="p_val_alias") @app.get("/model-optional-list-validation-alias") @@ -246,16 +246,14 @@ def test_optional_list_validation_alias_by_validation_alias(path: str): @app.get("/optional-list-alias-and-validation-alias") def read_optional_list_alias_and_validation_alias( p: Annotated[ - Optional[list[str]], Header(alias="p_alias", validation_alias="p_val_alias") + list[str] | None, Header(alias="p_alias", validation_alias="p_val_alias") ] = None, ): return {"p": p} class HeaderModelOptionalListAliasAndValidationAlias(BaseModel): - p: Optional[list[str]] = Field( - None, alias="p_alias", validation_alias="p_val_alias" - ) + p: list[str] | None = Field(None, alias="p_alias", validation_alias="p_val_alias") @app.get("/model-optional-list-alias-and-validation-alias") diff --git a/tests/test_request_params/test_header/test_optional_str.py b/tests/test_request_params/test_header/test_optional_str.py index a5174e59af..04773c83f9 100644 --- a/tests/test_request_params/test_header/test_optional_str.py +++ b/tests/test_request_params/test_header/test_optional_str.py @@ -1,4 +1,4 @@ -from typing import Annotated, Optional +from typing import Annotated import pytest from fastapi import FastAPI, Header @@ -13,12 +13,12 @@ app = FastAPI() @app.get("/optional-str") -async def read_optional_str(p: Annotated[Optional[str], Header()] = None): +async def read_optional_str(p: Annotated[str | None, Header()] = None): return {"p": p} class HeaderModelOptionalStr(BaseModel): - p: Optional[str] = None + p: str | None = None @app.get("/model-optional-str") @@ -74,13 +74,13 @@ def test_optional_str(path: str): @app.get("/optional-alias") async def read_optional_alias( - p: Annotated[Optional[str], Header(alias="p_alias")] = None, + p: Annotated[str | None, Header(alias="p_alias")] = None, ): return {"p": p} class HeaderModelOptionalAlias(BaseModel): - p: Optional[str] = Field(None, alias="p_alias") + p: str | None = Field(None, alias="p_alias") @app.get("/model-optional-alias") @@ -150,13 +150,13 @@ def test_optional_alias_by_alias(path: str): @app.get("/optional-validation-alias") def read_optional_validation_alias( - p: Annotated[Optional[str], Header(validation_alias="p_val_alias")] = None, + p: Annotated[str | None, Header(validation_alias="p_val_alias")] = None, ): return {"p": p} class HeaderModelOptionalValidationAlias(BaseModel): - p: Optional[str] = Field(None, validation_alias="p_val_alias") + p: str | None = Field(None, validation_alias="p_val_alias") @app.get("/model-optional-validation-alias") @@ -232,14 +232,14 @@ def test_optional_validation_alias_by_validation_alias(path: str): @app.get("/optional-alias-and-validation-alias") def read_optional_alias_and_validation_alias( p: Annotated[ - Optional[str], Header(alias="p_alias", validation_alias="p_val_alias") + str | None, Header(alias="p_alias", validation_alias="p_val_alias") ] = None, ): return {"p": p} class HeaderModelOptionalAliasAndValidationAlias(BaseModel): - p: Optional[str] = Field(None, alias="p_alias", validation_alias="p_val_alias") + p: str | None = Field(None, alias="p_alias", validation_alias="p_val_alias") @app.get("/model-optional-alias-and-validation-alias") diff --git a/tests/test_request_params/test_query/test_optional_list.py b/tests/test_request_params/test_query/test_optional_list.py index 5608c6499b..6b70b75a4f 100644 --- a/tests/test_request_params/test_query/test_optional_list.py +++ b/tests/test_request_params/test_query/test_optional_list.py @@ -1,4 +1,4 @@ -from typing import Annotated, Optional +from typing import Annotated import pytest from fastapi import FastAPI, Query @@ -14,13 +14,13 @@ app = FastAPI() @app.get("/optional-list-str") async def read_optional_list_str( - p: Annotated[Optional[list[str]], Query()] = None, + p: Annotated[list[str] | None, Query()] = None, ): return {"p": p} class QueryModelOptionalListStr(BaseModel): - p: Optional[list[str]] = None + p: list[str] | None = None @app.get("/model-optional-list-str") @@ -81,13 +81,13 @@ def test_optional_list_str(path: str): @app.get("/optional-list-alias") async def read_optional_list_alias( - p: Annotated[Optional[list[str]], Query(alias="p_alias")] = None, + p: Annotated[list[str] | None, Query(alias="p_alias")] = None, ): return {"p": p} class QueryModelOptionalListAlias(BaseModel): - p: Optional[list[str]] = Field(None, alias="p_alias") + p: list[str] | None = Field(None, alias="p_alias") @app.get("/model-optional-list-alias") @@ -162,13 +162,13 @@ def test_optional_list_alias_by_alias(path: str): @app.get("/optional-list-validation-alias") def read_optional_list_validation_alias( - p: Annotated[Optional[list[str]], Query(validation_alias="p_val_alias")] = None, + p: Annotated[list[str] | None, Query(validation_alias="p_val_alias")] = None, ): return {"p": p} class QueryModelOptionalListValidationAlias(BaseModel): - p: Optional[list[str]] = Field(None, validation_alias="p_val_alias") + p: list[str] | None = Field(None, validation_alias="p_val_alias") @app.get("/model-optional-list-validation-alias") @@ -244,16 +244,14 @@ def test_optional_list_validation_alias_by_validation_alias(path: str): @app.get("/optional-list-alias-and-validation-alias") def read_optional_list_alias_and_validation_alias( p: Annotated[ - Optional[list[str]], Query(alias="p_alias", validation_alias="p_val_alias") + list[str] | None, Query(alias="p_alias", validation_alias="p_val_alias") ] = None, ): return {"p": p} class QueryModelOptionalListAliasAndValidationAlias(BaseModel): - p: Optional[list[str]] = Field( - None, alias="p_alias", validation_alias="p_val_alias" - ) + p: list[str] | None = Field(None, alias="p_alias", validation_alias="p_val_alias") @app.get("/model-optional-list-alias-and-validation-alias") diff --git a/tests/test_request_params/test_query/test_optional_str.py b/tests/test_request_params/test_query/test_optional_str.py index b181686b05..f7f35860b6 100644 --- a/tests/test_request_params/test_query/test_optional_str.py +++ b/tests/test_request_params/test_query/test_optional_str.py @@ -1,4 +1,4 @@ -from typing import Annotated, Optional +from typing import Annotated import pytest from fastapi import FastAPI, Query @@ -13,12 +13,12 @@ app = FastAPI() @app.get("/optional-str") -async def read_optional_str(p: Optional[str] = None): +async def read_optional_str(p: str | None = None): return {"p": p} class QueryModelOptionalStr(BaseModel): - p: Optional[str] = None + p: str | None = None @app.get("/model-optional-str") @@ -74,13 +74,13 @@ def test_optional_str(path: str): @app.get("/optional-alias") async def read_optional_alias( - p: Annotated[Optional[str], Query(alias="p_alias")] = None, + p: Annotated[str | None, Query(alias="p_alias")] = None, ): return {"p": p} class QueryModelOptionalAlias(BaseModel): - p: Optional[str] = Field(None, alias="p_alias") + p: str | None = Field(None, alias="p_alias") @app.get("/model-optional-alias") @@ -150,13 +150,13 @@ def test_optional_alias_by_alias(path: str): @app.get("/optional-validation-alias") def read_optional_validation_alias( - p: Annotated[Optional[str], Query(validation_alias="p_val_alias")] = None, + p: Annotated[str | None, Query(validation_alias="p_val_alias")] = None, ): return {"p": p} class QueryModelOptionalValidationAlias(BaseModel): - p: Optional[str] = Field(None, validation_alias="p_val_alias") + p: str | None = Field(None, validation_alias="p_val_alias") @app.get("/model-optional-validation-alias") @@ -232,14 +232,14 @@ def test_optional_validation_alias_by_validation_alias(path: str): @app.get("/optional-alias-and-validation-alias") def read_optional_alias_and_validation_alias( p: Annotated[ - Optional[str], Query(alias="p_alias", validation_alias="p_val_alias") + str | None, Query(alias="p_alias", validation_alias="p_val_alias") ] = None, ): return {"p": p} class QueryModelOptionalAliasAndValidationAlias(BaseModel): - p: Optional[str] = Field(None, alias="p_alias", validation_alias="p_val_alias") + p: str | None = Field(None, alias="p_alias", validation_alias="p_val_alias") @app.get("/model-optional-alias-and-validation-alias") diff --git a/tests/test_required_noneable.py b/tests/test_required_noneable.py index 5da8cd4d09..c99f20212f 100644 --- a/tests/test_required_noneable.py +++ b/tests/test_required_noneable.py @@ -1,5 +1,3 @@ -from typing import Union - from fastapi import Body, FastAPI, Query from fastapi.testclient import TestClient @@ -7,17 +5,17 @@ app = FastAPI() @app.get("/query") -def read_query(q: Union[str, None]): +def read_query(q: str | None): return q @app.get("/explicit-query") -def read_explicit_query(q: Union[str, None] = Query()): +def read_explicit_query(q: str | None = Query()): return q @app.post("/body-embed") -def send_body_embed(b: Union[str, None] = Body(embed=True)): +def send_body_embed(b: str | None = Body(embed=True)): return b diff --git a/tests/test_response_model_as_return_annotation.py b/tests/test_response_model_as_return_annotation.py index ded5971027..7be7902ada 100644 --- a/tests/test_response_model_as_return_annotation.py +++ b/tests/test_response_model_as_return_annotation.py @@ -1,5 +1,3 @@ -from typing import Union - import pytest from fastapi import FastAPI from fastapi.exceptions import FastAPIError, ResponseValidationError @@ -216,7 +214,7 @@ def no_response_model_annotation_forward_ref_list_of_model() -> "list[User]": @app.get( "/response_model_union-no_annotation-return_model1", - response_model=Union[User, Item], + response_model=User | Item, ) def response_model_union_no_annotation_return_model1(): return DBUser(name="John", surname="Doe", password_hash="secret") @@ -224,19 +222,19 @@ def response_model_union_no_annotation_return_model1(): @app.get( "/response_model_union-no_annotation-return_model2", - response_model=Union[User, Item], + response_model=User | Item, ) def response_model_union_no_annotation_return_model2(): return Item(name="Foo", price=42.0) @app.get("/no_response_model-annotation_union-return_model1") -def no_response_model_annotation_union_return_model1() -> Union[User, Item]: +def no_response_model_annotation_union_return_model1() -> User | Item: return DBUser(name="John", surname="Doe", password_hash="secret") @app.get("/no_response_model-annotation_union-return_model2") -def no_response_model_annotation_union_return_model2() -> Union[User, Item]: +def no_response_model_annotation_union_return_model2() -> User | Item: return Item(name="Foo", price=42.0) @@ -503,7 +501,7 @@ def test_invalid_response_model_field(): with pytest.raises(FastAPIError) as e: @app.get("/") - def read_root() -> Union[Response, None]: + def read_root() -> Response | None: return Response(content="Foo") # pragma: no cover assert "valid Pydantic field type" in e.value.args[0] diff --git a/tests/test_router_events.py b/tests/test_router_events.py index a47d119139..7869a7afcd 100644 --- a/tests/test_router_events.py +++ b/tests/test_router_events.py @@ -1,6 +1,5 @@ from collections.abc import AsyncGenerator from contextlib import asynccontextmanager -from typing import Union import pytest from fastapi import APIRouter, FastAPI, Request @@ -176,7 +175,7 @@ def test_router_nested_lifespan_state_overriding_by_parent() -> None: @asynccontextmanager async def lifespan( app: FastAPI, - ) -> AsyncGenerator[dict[str, Union[str, bool]], None]: + ) -> AsyncGenerator[dict[str, str | bool], None]: yield { "app_specific": True, "overridden": "app", @@ -185,7 +184,7 @@ def test_router_nested_lifespan_state_overriding_by_parent() -> None: @asynccontextmanager async def router_lifespan( app: FastAPI, - ) -> AsyncGenerator[dict[str, Union[str, bool]], None]: + ) -> AsyncGenerator[dict[str, str | bool], None]: yield { "router_specific": True, "overridden": "router", # should override parent diff --git a/tests/test_schema_extra_examples.py b/tests/test_schema_extra_examples.py index 9ec41e7e84..32f5cea476 100644 --- a/tests/test_schema_extra_examples.py +++ b/tests/test_schema_extra_examples.py @@ -1,5 +1,3 @@ -from typing import Union - import pytest from fastapi import Body, Cookie, FastAPI, Header, Path, Query from fastapi.exceptions import FastAPIDeprecationWarning @@ -117,7 +115,7 @@ def create_app(): @app.get("/query_example/") def query_example( - data: Union[str, None] = Query( + data: str | None = Query( default=None, example="query1", ), @@ -126,7 +124,7 @@ def create_app(): @app.get("/query_examples/") def query_examples( - data: Union[str, None] = Query( + data: str | None = Query( default=None, examples=["query1", "query2"], ), @@ -137,7 +135,7 @@ def create_app(): @app.get("/query_example_examples/") def query_example_examples( - data: Union[str, None] = Query( + data: str | None = Query( default=None, example="query_overridden", examples=["query1", "query2"], @@ -149,7 +147,7 @@ def create_app(): @app.get("/header_example/") def header_example( - data: Union[str, None] = Header( + data: str | None = Header( default=None, example="header1", ), @@ -158,7 +156,7 @@ def create_app(): @app.get("/header_examples/") def header_examples( - data: Union[str, None] = Header( + data: str | None = Header( default=None, examples=[ "header1", @@ -172,7 +170,7 @@ def create_app(): @app.get("/header_example_examples/") def header_example_examples( - data: Union[str, None] = Header( + data: str | None = Header( default=None, example="header_overridden", examples=["header1", "header2"], @@ -184,7 +182,7 @@ def create_app(): @app.get("/cookie_example/") def cookie_example( - data: Union[str, None] = Cookie( + data: str | None = Cookie( default=None, example="cookie1", ), @@ -193,7 +191,7 @@ def create_app(): @app.get("/cookie_examples/") def cookie_examples( - data: Union[str, None] = Cookie( + data: str | None = Cookie( default=None, examples=["cookie1", "cookie2"], ), @@ -204,7 +202,7 @@ def create_app(): @app.get("/cookie_example_examples/") def cookie_example_examples( - data: Union[str, None] = Cookie( + data: str | None = Cookie( default=None, example="cookie_overridden", examples=["cookie1", "cookie2"], diff --git a/tests/test_security_api_key_cookie_optional.py b/tests/test_security_api_key_cookie_optional.py index 7988d80443..e911654fac 100644 --- a/tests/test_security_api_key_cookie_optional.py +++ b/tests/test_security_api_key_cookie_optional.py @@ -1,5 +1,3 @@ -from typing import Optional - from fastapi import Depends, FastAPI, Security from fastapi.security import APIKeyCookie from fastapi.testclient import TestClient @@ -15,7 +13,7 @@ class User(BaseModel): username: str -def get_current_user(oauth_header: Optional[str] = Security(api_key)): +def get_current_user(oauth_header: str | None = Security(api_key)): if oauth_header is None: return None user = User(username=oauth_header) diff --git a/tests/test_security_api_key_header_optional.py b/tests/test_security_api_key_header_optional.py index 51abd0bb96..0a8cf420ed 100644 --- a/tests/test_security_api_key_header_optional.py +++ b/tests/test_security_api_key_header_optional.py @@ -1,5 +1,3 @@ -from typing import Optional - from fastapi import Depends, FastAPI, Security from fastapi.security import APIKeyHeader from fastapi.testclient import TestClient @@ -15,7 +13,7 @@ class User(BaseModel): username: str -def get_current_user(oauth_header: Optional[str] = Security(api_key)): +def get_current_user(oauth_header: str | None = Security(api_key)): if oauth_header is None: return None user = User(username=oauth_header) @@ -23,7 +21,7 @@ def get_current_user(oauth_header: Optional[str] = Security(api_key)): @app.get("/users/me") -def read_current_user(current_user: Optional[User] = Depends(get_current_user)): +def read_current_user(current_user: User | None = Depends(get_current_user)): if current_user is None: return {"msg": "Create an account first"} return current_user diff --git a/tests/test_security_api_key_query_optional.py b/tests/test_security_api_key_query_optional.py index 26fbb9ee4f..e9fba30435 100644 --- a/tests/test_security_api_key_query_optional.py +++ b/tests/test_security_api_key_query_optional.py @@ -1,5 +1,3 @@ -from typing import Optional - from fastapi import Depends, FastAPI, Security from fastapi.security import APIKeyQuery from fastapi.testclient import TestClient @@ -15,7 +13,7 @@ class User(BaseModel): username: str -def get_current_user(oauth_header: Optional[str] = Security(api_key)): +def get_current_user(oauth_header: str | None = Security(api_key)): if oauth_header is None: return None user = User(username=oauth_header) @@ -23,7 +21,7 @@ def get_current_user(oauth_header: Optional[str] = Security(api_key)): @app.get("/users/me") -def read_current_user(current_user: Optional[User] = Depends(get_current_user)): +def read_current_user(current_user: User | None = Depends(get_current_user)): if current_user is None: return {"msg": "Create an account first"} return current_user diff --git a/tests/test_security_http_base_optional.py b/tests/test_security_http_base_optional.py index 612a7909fe..1d1944ab0a 100644 --- a/tests/test_security_http_base_optional.py +++ b/tests/test_security_http_base_optional.py @@ -1,5 +1,3 @@ -from typing import Optional - from fastapi import FastAPI, Security from fastapi.security.http import HTTPAuthorizationCredentials, HTTPBase from fastapi.testclient import TestClient @@ -12,7 +10,7 @@ security = HTTPBase(scheme="Other", auto_error=False) @app.get("/users/me") def read_current_user( - credentials: Optional[HTTPAuthorizationCredentials] = Security(security), + credentials: HTTPAuthorizationCredentials | None = Security(security), ): if credentials is None: return {"msg": "Create an account first"} diff --git a/tests/test_security_http_basic_optional.py b/tests/test_security_http_basic_optional.py index e94565c7bb..78abf2b680 100644 --- a/tests/test_security_http_basic_optional.py +++ b/tests/test_security_http_basic_optional.py @@ -1,5 +1,4 @@ from base64 import b64encode -from typing import Optional from fastapi import FastAPI, Security from fastapi.security import HTTPBasic, HTTPBasicCredentials @@ -12,7 +11,7 @@ security = HTTPBasic(auto_error=False) @app.get("/users/me") -def read_current_user(credentials: Optional[HTTPBasicCredentials] = Security(security)): +def read_current_user(credentials: HTTPBasicCredentials | None = Security(security)): if credentials is None: return {"msg": "Create an account first"} return {"username": credentials.username, "password": credentials.password} diff --git a/tests/test_security_http_bearer_optional.py b/tests/test_security_http_bearer_optional.py index b49a6593ec..06d9d03db4 100644 --- a/tests/test_security_http_bearer_optional.py +++ b/tests/test_security_http_bearer_optional.py @@ -1,5 +1,3 @@ -from typing import Optional - from fastapi import FastAPI, Security from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from fastapi.testclient import TestClient @@ -12,7 +10,7 @@ security = HTTPBearer(auto_error=False) @app.get("/users/me") def read_current_user( - credentials: Optional[HTTPAuthorizationCredentials] = Security(security), + credentials: HTTPAuthorizationCredentials | None = Security(security), ): if credentials is None: return {"msg": "Create an account first"} diff --git a/tests/test_security_http_digest_optional.py b/tests/test_security_http_digest_optional.py index 97e62634d8..d1056b1918 100644 --- a/tests/test_security_http_digest_optional.py +++ b/tests/test_security_http_digest_optional.py @@ -1,5 +1,3 @@ -from typing import Optional - from fastapi import FastAPI, Security from fastapi.security import HTTPAuthorizationCredentials, HTTPDigest from fastapi.testclient import TestClient @@ -12,7 +10,7 @@ security = HTTPDigest(auto_error=False) @app.get("/users/me") def read_current_user( - credentials: Optional[HTTPAuthorizationCredentials] = Security(security), + credentials: HTTPAuthorizationCredentials | None = Security(security), ): if credentials is None: return {"msg": "Create an account first"} diff --git a/tests/test_security_oauth2_authorization_code_bearer.py b/tests/test_security_oauth2_authorization_code_bearer.py index 1ba577e9ff..587486c76b 100644 --- a/tests/test_security_oauth2_authorization_code_bearer.py +++ b/tests/test_security_oauth2_authorization_code_bearer.py @@ -1,5 +1,3 @@ -from typing import Optional - from fastapi import FastAPI, Security from fastapi.security import OAuth2AuthorizationCodeBearer from fastapi.testclient import TestClient @@ -13,7 +11,7 @@ oauth2_scheme = OAuth2AuthorizationCodeBearer( @app.get("/items/") -async def read_items(token: Optional[str] = Security(oauth2_scheme)): +async def read_items(token: str | None = Security(oauth2_scheme)): return {"token": token} diff --git a/tests/test_security_oauth2_authorization_code_bearer_description.py b/tests/test_security_oauth2_authorization_code_bearer_description.py index 73807c31a3..f878ede643 100644 --- a/tests/test_security_oauth2_authorization_code_bearer_description.py +++ b/tests/test_security_oauth2_authorization_code_bearer_description.py @@ -1,5 +1,3 @@ -from typing import Optional - from fastapi import FastAPI, Security from fastapi.security import OAuth2AuthorizationCodeBearer from fastapi.testclient import TestClient @@ -16,7 +14,7 @@ oauth2_scheme = OAuth2AuthorizationCodeBearer( @app.get("/items/") -async def read_items(token: Optional[str] = Security(oauth2_scheme)): +async def read_items(token: str | None = Security(oauth2_scheme)): return {"token": token} diff --git a/tests/test_security_oauth2_authorization_code_bearer_scopes_openapi.py b/tests/test_security_oauth2_authorization_code_bearer_scopes_openapi.py index 583007c8b7..6fcce6fed7 100644 --- a/tests/test_security_oauth2_authorization_code_bearer_scopes_openapi.py +++ b/tests/test_security_oauth2_authorization_code_bearer_scopes_openapi.py @@ -1,6 +1,6 @@ # Ref: https://github.com/fastapi/fastapi/issues/14454 -from typing import Annotated, Optional +from typing import Annotated from fastapi import APIRouter, Depends, FastAPI, Security from fastapi.security import OAuth2AuthorizationCodeBearer @@ -46,13 +46,13 @@ router = APIRouter(dependencies=[Security(oauth2_scheme, scopes=["read"])]) @router.get("/items/") -async def read_items(token: Optional[str] = Depends(oauth2_scheme)): +async def read_items(token: str | None = Depends(oauth2_scheme)): return {"token": token} @router.post("/items/") async def create_item( - token: Optional[str] = Security(oauth2_scheme, scopes=["read", "write"]), + token: str | None = Security(oauth2_scheme, scopes=["read", "write"]), ): return {"token": token} diff --git a/tests/test_security_oauth2_optional.py b/tests/test_security_oauth2_optional.py index cb79afdb86..a7eaf59443 100644 --- a/tests/test_security_oauth2_optional.py +++ b/tests/test_security_oauth2_optional.py @@ -1,5 +1,3 @@ -from typing import Optional - import pytest from fastapi import Depends, FastAPI, Security from fastapi.security import OAuth2, OAuth2PasswordRequestFormStrict @@ -24,7 +22,7 @@ class User(BaseModel): username: str -def get_current_user(oauth_header: Optional[str] = Security(reusable_oauth2)): +def get_current_user(oauth_header: str | None = Security(reusable_oauth2)): if oauth_header is None: return None user = User(username=oauth_header) @@ -37,7 +35,7 @@ def login(form_data: OAuth2PasswordRequestFormStrict = Depends()): @app.get("/users/me") -def read_users_me(current_user: Optional[User] = Depends(get_current_user)): +def read_users_me(current_user: User | None = Depends(get_current_user)): if current_user is None: return {"msg": "Create an account first"} return current_user diff --git a/tests/test_security_oauth2_optional_description.py b/tests/test_security_oauth2_optional_description.py index b3fae37a17..0918d352ea 100644 --- a/tests/test_security_oauth2_optional_description.py +++ b/tests/test_security_oauth2_optional_description.py @@ -1,5 +1,3 @@ -from typing import Optional - import pytest from fastapi import Depends, FastAPI, Security from fastapi.security import OAuth2, OAuth2PasswordRequestFormStrict @@ -25,7 +23,7 @@ class User(BaseModel): username: str -def get_current_user(oauth_header: Optional[str] = Security(reusable_oauth2)): +def get_current_user(oauth_header: str | None = Security(reusable_oauth2)): if oauth_header is None: return None user = User(username=oauth_header) @@ -38,7 +36,7 @@ def login(form_data: OAuth2PasswordRequestFormStrict = Depends()): @app.get("/users/me") -def read_users_me(current_user: Optional[User] = Depends(get_current_user)): +def read_users_me(current_user: User | None = Depends(get_current_user)): if current_user is None: return {"msg": "Create an account first"} return current_user diff --git a/tests/test_security_oauth2_password_bearer_optional.py b/tests/test_security_oauth2_password_bearer_optional.py index 01e2f65ed9..263359c950 100644 --- a/tests/test_security_oauth2_password_bearer_optional.py +++ b/tests/test_security_oauth2_password_bearer_optional.py @@ -1,5 +1,3 @@ -from typing import Optional - from fastapi import FastAPI, Security from fastapi.security import OAuth2PasswordBearer from fastapi.testclient import TestClient @@ -11,7 +9,7 @@ oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token", auto_error=False) @app.get("/items/") -async def read_items(token: Optional[str] = Security(oauth2_scheme)): +async def read_items(token: str | None = Security(oauth2_scheme)): if token is None: return {"msg": "Create an account first"} return {"token": token} diff --git a/tests/test_security_oauth2_password_bearer_optional_description.py b/tests/test_security_oauth2_password_bearer_optional_description.py index fec8d03a7a..0deb7e48ff 100644 --- a/tests/test_security_oauth2_password_bearer_optional_description.py +++ b/tests/test_security_oauth2_password_bearer_optional_description.py @@ -1,5 +1,3 @@ -from typing import Optional - from fastapi import FastAPI, Security from fastapi.security import OAuth2PasswordBearer from fastapi.testclient import TestClient @@ -15,7 +13,7 @@ oauth2_scheme = OAuth2PasswordBearer( @app.get("/items/") -async def read_items(token: Optional[str] = Security(oauth2_scheme)): +async def read_items(token: str | None = Security(oauth2_scheme)): if token is None: return {"msg": "Create an account first"} return {"token": token} diff --git a/tests/test_security_openid_connect_optional.py b/tests/test_security_openid_connect_optional.py index ebaf394dc9..44e1a4866e 100644 --- a/tests/test_security_openid_connect_optional.py +++ b/tests/test_security_openid_connect_optional.py @@ -1,5 +1,3 @@ -from typing import Optional - from fastapi import Depends, FastAPI, Security from fastapi.security.open_id_connect_url import OpenIdConnect from fastapi.testclient import TestClient @@ -15,7 +13,7 @@ class User(BaseModel): username: str -def get_current_user(oauth_header: Optional[str] = Security(oid)): +def get_current_user(oauth_header: str | None = Security(oid)): if oauth_header is None: return None user = User(username=oauth_header) @@ -23,7 +21,7 @@ def get_current_user(oauth_header: Optional[str] = Security(oid)): @app.get("/users/me") -def read_current_user(current_user: Optional[User] = Depends(get_current_user)): +def read_current_user(current_user: User | None = Depends(get_current_user)): if current_user is None: return {"msg": "Create an account first"} return current_user diff --git a/tests/test_serialize_response.py b/tests/test_serialize_response.py index 14f88dd931..114c3c6cb2 100644 --- a/tests/test_serialize_response.py +++ b/tests/test_serialize_response.py @@ -1,5 +1,3 @@ -from typing import Optional - from fastapi import FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel @@ -9,8 +7,8 @@ app = FastAPI() class Item(BaseModel): name: str - price: Optional[float] = None - owner_ids: Optional[list[int]] = None + price: float | None = None + owner_ids: list[int] | None = None @app.get("/items/valid", response_model=Item) diff --git a/tests/test_serialize_response_dataclass.py b/tests/test_serialize_response_dataclass.py index ee695368b8..ae05f14d1a 100644 --- a/tests/test_serialize_response_dataclass.py +++ b/tests/test_serialize_response_dataclass.py @@ -1,6 +1,5 @@ from dataclasses import dataclass from datetime import datetime -from typing import Optional from fastapi import FastAPI from fastapi.testclient import TestClient @@ -12,8 +11,8 @@ app = FastAPI() class Item: name: str date: datetime - price: Optional[float] = None - owner_ids: Optional[list[int]] = None + price: float | None = None + owner_ids: list[int] | None = None @app.get("/items/valid", response_model=Item) diff --git a/tests/test_serialize_response_model.py b/tests/test_serialize_response_model.py index 79c90c9c29..bb05f7bc40 100644 --- a/tests/test_serialize_response_model.py +++ b/tests/test_serialize_response_model.py @@ -1,5 +1,3 @@ -from typing import Optional - from fastapi import FastAPI from pydantic import BaseModel, Field from starlette.testclient import TestClient @@ -9,8 +7,8 @@ app = FastAPI() class Item(BaseModel): name: str = Field(alias="aliased_name") - price: Optional[float] = None - owner_ids: Optional[list[int]] = None + price: float | None = None + owner_ids: list[int] | None = None @app.get("/items/valid", response_model=Item) diff --git a/tests/test_skip_defaults.py b/tests/test_skip_defaults.py index 02765291cb..238da7392f 100644 --- a/tests/test_skip_defaults.py +++ b/tests/test_skip_defaults.py @@ -1,5 +1,3 @@ -from typing import Optional - from fastapi import FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel @@ -8,23 +6,23 @@ app = FastAPI() class SubModel(BaseModel): - a: Optional[str] = "foo" + a: str | None = "foo" class Model(BaseModel): - x: Optional[int] = None + x: int | None = None sub: SubModel class ModelSubclass(Model): y: int z: int = 0 - w: Optional[int] = None + w: int | None = None class ModelDefaults(BaseModel): - w: Optional[str] = None - x: Optional[str] = None + w: str | None = None + x: str | None = None y: str = "y" z: str = "z" diff --git a/tests/test_strict_content_type_app_level.py b/tests/test_strict_content_type_app_level.py new file mode 100644 index 0000000000..42a0821a47 --- /dev/null +++ b/tests/test_strict_content_type_app_level.py @@ -0,0 +1,44 @@ +from fastapi import FastAPI +from fastapi.testclient import TestClient + +app_default = FastAPI() + + +@app_default.post("/items/") +async def app_default_post(data: dict): + return data + + +app_lax = FastAPI(strict_content_type=False) + + +@app_lax.post("/items/") +async def app_lax_post(data: dict): + return data + + +client_default = TestClient(app_default) +client_lax = TestClient(app_lax) + + +def test_default_strict_rejects_no_content_type(): + response = client_default.post("/items/", content='{"key": "value"}') + assert response.status_code == 422 + + +def test_default_strict_accepts_json_content_type(): + response = client_default.post("/items/", json={"key": "value"}) + assert response.status_code == 200 + assert response.json() == {"key": "value"} + + +def test_lax_accepts_no_content_type(): + response = client_lax.post("/items/", content='{"key": "value"}') + assert response.status_code == 200 + assert response.json() == {"key": "value"} + + +def test_lax_accepts_json_content_type(): + response = client_lax.post("/items/", json={"key": "value"}) + assert response.status_code == 200 + assert response.json() == {"key": "value"} diff --git a/tests/test_strict_content_type_nested.py b/tests/test_strict_content_type_nested.py new file mode 100644 index 0000000000..922d01571a --- /dev/null +++ b/tests/test_strict_content_type_nested.py @@ -0,0 +1,91 @@ +from fastapi import APIRouter, FastAPI +from fastapi.testclient import TestClient + +# Lax app with nested routers, inner overrides to strict + +app_nested = FastAPI(strict_content_type=False) # lax app +outer_router = APIRouter(prefix="/outer") # inherits lax from app +inner_strict = APIRouter(prefix="/strict", strict_content_type=True) +inner_default = APIRouter(prefix="/default") + + +@inner_strict.post("/items/") +async def inner_strict_post(data: dict): + return data + + +@inner_default.post("/items/") +async def inner_default_post(data: dict): + return data + + +outer_router.include_router(inner_strict) +outer_router.include_router(inner_default) +app_nested.include_router(outer_router) + +client_nested = TestClient(app_nested) + + +def test_strict_inner_on_lax_app_rejects_no_content_type(): + response = client_nested.post("/outer/strict/items/", content='{"key": "value"}') + assert response.status_code == 422 + + +def test_default_inner_inherits_lax_from_app(): + response = client_nested.post("/outer/default/items/", content='{"key": "value"}') + assert response.status_code == 200 + assert response.json() == {"key": "value"} + + +def test_strict_inner_accepts_json_content_type(): + response = client_nested.post("/outer/strict/items/", json={"key": "value"}) + assert response.status_code == 200 + + +def test_default_inner_accepts_json_content_type(): + response = client_nested.post("/outer/default/items/", json={"key": "value"}) + assert response.status_code == 200 + + +# Strict app -> lax outer router -> strict inner router + +app_mixed = FastAPI(strict_content_type=True) +mixed_outer = APIRouter(prefix="/outer", strict_content_type=False) +mixed_inner = APIRouter(prefix="/inner", strict_content_type=True) + + +@mixed_outer.post("/items/") +async def mixed_outer_post(data: dict): + return data + + +@mixed_inner.post("/items/") +async def mixed_inner_post(data: dict): + return data + + +mixed_outer.include_router(mixed_inner) +app_mixed.include_router(mixed_outer) + +client_mixed = TestClient(app_mixed) + + +def test_lax_outer_on_strict_app_accepts_no_content_type(): + response = client_mixed.post("/outer/items/", content='{"key": "value"}') + assert response.status_code == 200 + assert response.json() == {"key": "value"} + + +def test_strict_inner_on_lax_outer_rejects_no_content_type(): + response = client_mixed.post("/outer/inner/items/", content='{"key": "value"}') + assert response.status_code == 422 + + +def test_lax_outer_accepts_json_content_type(): + response = client_mixed.post("/outer/items/", json={"key": "value"}) + assert response.status_code == 200 + + +def test_strict_inner_on_lax_outer_accepts_json_content_type(): + response = client_mixed.post("/outer/inner/items/", json={"key": "value"}) + assert response.status_code == 200 diff --git a/tests/test_strict_content_type_router_level.py b/tests/test_strict_content_type_router_level.py new file mode 100644 index 0000000000..72a02d6c91 --- /dev/null +++ b/tests/test_strict_content_type_router_level.py @@ -0,0 +1,61 @@ +from fastapi import APIRouter, FastAPI +from fastapi.testclient import TestClient + +app = FastAPI() + +router_lax = APIRouter(prefix="/lax", strict_content_type=False) +router_strict = APIRouter(prefix="/strict", strict_content_type=True) +router_default = APIRouter(prefix="/default") + + +@router_lax.post("/items/") +async def router_lax_post(data: dict): + return data + + +@router_strict.post("/items/") +async def router_strict_post(data: dict): + return data + + +@router_default.post("/items/") +async def router_default_post(data: dict): + return data + + +app.include_router(router_lax) +app.include_router(router_strict) +app.include_router(router_default) + +client = TestClient(app) + + +def test_lax_router_on_strict_app_accepts_no_content_type(): + response = client.post("/lax/items/", content='{"key": "value"}') + assert response.status_code == 200 + assert response.json() == {"key": "value"} + + +def test_strict_router_on_strict_app_rejects_no_content_type(): + response = client.post("/strict/items/", content='{"key": "value"}') + assert response.status_code == 422 + + +def test_default_router_inherits_strict_from_app(): + response = client.post("/default/items/", content='{"key": "value"}') + assert response.status_code == 422 + + +def test_lax_router_accepts_json_content_type(): + response = client.post("/lax/items/", json={"key": "value"}) + assert response.status_code == 200 + + +def test_strict_router_accepts_json_content_type(): + response = client.post("/strict/items/", json={"key": "value"}) + assert response.status_code == 200 + + +def test_default_router_accepts_json_content_type(): + response = client.post("/default/items/", json={"key": "value"}) + assert response.status_code == 200 diff --git a/tests/test_sub_callbacks.py b/tests/test_sub_callbacks.py index 86dc4d00e2..b8a9dd2921 100644 --- a/tests/test_sub_callbacks.py +++ b/tests/test_sub_callbacks.py @@ -1,5 +1,3 @@ -from typing import Optional - from fastapi import APIRouter, FastAPI from fastapi.testclient import TestClient from inline_snapshot import snapshot @@ -10,7 +8,7 @@ app = FastAPI() class Invoice(BaseModel): id: str - title: Optional[str] = None + title: str | None = None customer: str total: float @@ -51,7 +49,7 @@ subrouter = APIRouter() @subrouter.post("/invoices/", callbacks=invoices_callback_router.routes) -def create_invoice(invoice: Invoice, callback_url: Optional[HttpUrl] = None): +def create_invoice(invoice: Invoice, callback_url: HttpUrl | None = None): """ Create an invoice. diff --git a/tests/test_tutorial/test_body/test_tutorial001.py b/tests/test_tutorial/test_body/test_tutorial001.py index bdabf8d68b..8c883708a3 100644 --- a/tests/test_tutorial/test_body/test_tutorial001.py +++ b/tests/test_tutorial/test_body/test_tutorial001.py @@ -189,18 +189,12 @@ def test_geo_json(client: TestClient): assert response.status_code == 200, response.text -def test_no_content_type_is_json(client: TestClient): +def test_no_content_type_json(client: TestClient): response = client.post( "/items/", content='{"name": "Foo", "price": 50.5}', ) - assert response.status_code == 200, response.text - assert response.json() == { - "name": "Foo", - "description": None, - "price": 50.5, - "tax": None, - } + assert response.status_code == 422, response.text def test_wrong_headers(client: TestClient): diff --git a/tests/test_tutorial/test_custom_response/test_tutorial001.py b/tests/test_tutorial/test_custom_response/test_tutorial001.py index a5fe4c8f4c..a691dd3a84 100644 --- a/tests/test_tutorial/test_custom_response/test_tutorial001.py +++ b/tests/test_tutorial/test_custom_response/test_tutorial001.py @@ -9,7 +9,6 @@ from inline_snapshot import snapshot name="client", params=[ pytest.param("tutorial001_py310"), - pytest.param("tutorial010_py310"), ], ) def get_client(request: pytest.FixtureRequest): @@ -18,12 +17,14 @@ def get_client(request: pytest.FixtureRequest): return client +@pytest.mark.filterwarnings("ignore::fastapi.exceptions.FastAPIDeprecationWarning") def test_get_custom_response(client: TestClient): response = client.get("/items/") assert response.status_code == 200, response.text assert response.json() == [{"item_id": "Foo"}] +@pytest.mark.filterwarnings("ignore::fastapi.exceptions.FastAPIDeprecationWarning") def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text diff --git a/tests/test_tutorial/test_custom_response/test_tutorial001b.py b/tests/test_tutorial/test_custom_response/test_tutorial001b.py index 32437db86b..11ce813b76 100644 --- a/tests/test_tutorial/test_custom_response/test_tutorial001b.py +++ b/tests/test_tutorial/test_custom_response/test_tutorial001b.py @@ -1,17 +1,25 @@ +import warnings + +import pytest +from fastapi.exceptions import FastAPIDeprecationWarning from fastapi.testclient import TestClient from inline_snapshot import snapshot -from docs_src.custom_response.tutorial001b_py310 import app +with warnings.catch_warnings(): + warnings.simplefilter("ignore", FastAPIDeprecationWarning) + from docs_src.custom_response.tutorial001b_py310 import app client = TestClient(app) +@pytest.mark.filterwarnings("ignore::fastapi.exceptions.FastAPIDeprecationWarning") def test_get_custom_response(): response = client.get("/items/") assert response.status_code == 200, response.text assert response.json() == [{"item_id": "Foo"}] +@pytest.mark.filterwarnings("ignore::fastapi.exceptions.FastAPIDeprecationWarning") def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text diff --git a/tests/test_tutorial/test_custom_response/test_tutorial010.py b/tests/test_tutorial/test_custom_response/test_tutorial010.py new file mode 100644 index 0000000000..ffb005cb67 --- /dev/null +++ b/tests/test_tutorial/test_custom_response/test_tutorial010.py @@ -0,0 +1,50 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient +from inline_snapshot import snapshot + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial010_py310"), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.custom_response.{request.param}") + client = TestClient(mod.app) + return client + + +def test_get_custom_response(client: TestClient): + response = client.get("/items/") + assert response.status_code == 200, response.text + assert response.text == snapshot("

Items

This is a list of items.

") + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "text/html": {"schema": {"type": "string"}} + }, + } + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + } + } + }, + } + ) diff --git a/docs_src/bigger_applications/app_an_py39/__init__.py b/tests/test_tutorial/test_json_base64_bytes/__init__.py similarity index 100% rename from docs_src/bigger_applications/app_an_py39/__init__.py rename to tests/test_tutorial/test_json_base64_bytes/__init__.py diff --git a/tests/test_tutorial/test_json_base64_bytes/test_tutorial001.py b/tests/test_tutorial/test_json_base64_bytes/test_tutorial001.py new file mode 100644 index 0000000000..4d70bca5f3 --- /dev/null +++ b/tests/test_tutorial/test_json_base64_bytes/test_tutorial001.py @@ -0,0 +1,225 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient +from inline_snapshot import snapshot + +from tests.utils import needs_py310 + + +@pytest.fixture( + name="client", + params=[pytest.param("tutorial001_py310", marks=needs_py310)], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.json_base64_bytes.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_post_data(client: TestClient): + response = client.post( + "/data", + json={ + "description": "A file", + "data": "SGVsbG8sIFdvcmxkIQ==", + }, + ) + assert response.status_code == 200, response.text + assert response.json() == {"description": "A file", "content": "Hello, World!"} + + +def test_get_data(client: TestClient): + response = client.get("/data") + assert response.status_code == 200, response.text + assert response.json() == {"description": "A plumbus", "data": "aGVsbG8="} + + +def test_post_data_in_out(client: TestClient): + response = client.post( + "/data-in-out", + json={ + "description": "A plumbus", + "data": "SGVsbG8sIFdvcmxkIQ==", + }, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "description": "A plumbus", + "data": "SGVsbG8sIFdvcmxkIQ==", + } + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/data": { + "get": { + "summary": "Get Data", + "operationId": "get_data_data_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataOutput" + } + } + }, + } + }, + }, + "post": { + "summary": "Post Data", + "operationId": "post_data_data_post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/DataInput"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + "/data-in-out": { + "post": { + "summary": "Post Data In Out", + "operationId": "post_data_in_out_data_in_out_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataInputOutput" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataInputOutput" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "DataInput": { + "properties": { + "description": {"type": "string", "title": "Description"}, + "data": { + "type": "string", + "contentEncoding": "base64", + "contentMediaType": "application/octet-stream", + "title": "Data", + }, + }, + "type": "object", + "required": ["description", "data"], + "title": "DataInput", + }, + "DataInputOutput": { + "properties": { + "description": {"type": "string", "title": "Description"}, + "data": { + "type": "string", + "contentEncoding": "base64", + "contentMediaType": "application/octet-stream", + "title": "Data", + }, + }, + "type": "object", + "required": ["description", "data"], + "title": "DataInputOutput", + }, + "DataOutput": { + "properties": { + "description": {"type": "string", "title": "Description"}, + "data": { + "type": "string", + "contentEncoding": "base64", + "contentMediaType": "application/octet-stream", + "title": "Data", + }, + }, + "type": "object", + "required": ["description", "data"], + "title": "DataOutput", + }, + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "ValidationError": { + "properties": { + "ctx": {"title": "Context", "type": "object"}, + "input": {"title": "Input"}, + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } + ) diff --git a/tests/test_tutorial/test_python_types/test_tutorial009c.py b/tests/test_tutorial/test_python_types/test_tutorial009c.py deleted file mode 100644 index 17c4b9e0c2..0000000000 --- a/tests/test_tutorial/test_python_types/test_tutorial009c.py +++ /dev/null @@ -1,33 +0,0 @@ -import importlib -import re -from types import ModuleType -from unittest.mock import patch - -import pytest - -from ...utils import needs_py310 - - -@pytest.fixture( - name="module", - params=[ - pytest.param("tutorial009c_py310"), - pytest.param("tutorial009c_py310", marks=needs_py310), - ], -) -def get_module(request: pytest.FixtureRequest): - mod = importlib.import_module(f"docs_src.python_types.{request.param}") - return mod - - -def test_say_hi(module: ModuleType): - with patch("builtins.print") as mock_print: - module.say_hi("FastAPI") - - mock_print.assert_called_once_with("Hey FastAPI!") - - with pytest.raises( - TypeError, - match=re.escape("say_hi() missing 1 required positional argument: 'name'"), - ): - module.say_hi() diff --git a/tests/test_tutorial/test_request_files/test_tutorial001.py b/tests/test_tutorial/test_request_files/test_tutorial001.py index 4d3c35d65d..797225bc2d 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001.py @@ -162,8 +162,8 @@ def test_openapi_schema(client: TestClient): "properties": { "file": { "title": "File", + "contentMediaType": "application/octet-stream", "type": "string", - "format": "binary", } }, }, @@ -175,7 +175,7 @@ def test_openapi_schema(client: TestClient): "file": { "title": "File", "type": "string", - "format": "binary", + "contentMediaType": "application/octet-stream", } }, }, diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_02.py b/tests/test_tutorial/test_request_files/test_tutorial001_02.py index f199b992ae..4e3c33818e 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001_02.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001_02.py @@ -134,7 +134,10 @@ def test_openapi_schema(client: TestClient): "file": { "title": "File", "anyOf": [ - {"type": "string", "format": "binary"}, + { + "type": "string", + "contentMediaType": "application/octet-stream", + }, {"type": "null"}, ], } @@ -147,7 +150,10 @@ def test_openapi_schema(client: TestClient): "file": { "title": "File", "anyOf": [ - {"type": "string", "format": "binary"}, + { + "type": "string", + "contentMediaType": "application/octet-stream", + }, {"type": "null"}, ], } diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_03.py b/tests/test_tutorial/test_request_files/test_tutorial001_03.py index ce22c1b5c4..bccc617046 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001_03.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001_03.py @@ -123,7 +123,7 @@ def test_openapi_schema(client: TestClient): "title": "File", "type": "string", "description": "A file read as bytes", - "format": "binary", + "contentMediaType": "application/octet-stream", } }, }, @@ -134,9 +134,9 @@ def test_openapi_schema(client: TestClient): "properties": { "file": { "title": "File", + "contentMediaType": "application/octet-stream", "type": "string", "description": "A file read as UploadFile", - "format": "binary", } }, }, diff --git a/tests/test_tutorial/test_request_files/test_tutorial002.py b/tests/test_tutorial/test_request_files/test_tutorial002.py index ebf76b3a07..123468d48f 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial002.py +++ b/tests/test_tutorial/test_request_files/test_tutorial002.py @@ -195,7 +195,10 @@ def test_openapi_schema(client: TestClient): "files": { "title": "Files", "type": "array", - "items": {"type": "string", "format": "binary"}, + "items": { + "type": "string", + "contentMediaType": "application/octet-stream", + }, } }, }, @@ -207,7 +210,10 @@ def test_openapi_schema(client: TestClient): "files": { "title": "Files", "type": "array", - "items": {"type": "string", "format": "binary"}, + "items": { + "type": "string", + "contentMediaType": "application/octet-stream", + }, } }, }, diff --git a/tests/test_tutorial/test_request_files/test_tutorial003.py b/tests/test_tutorial/test_request_files/test_tutorial003.py index f11658d27c..2f554d9489 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial003.py +++ b/tests/test_tutorial/test_request_files/test_tutorial003.py @@ -165,7 +165,10 @@ def test_openapi_schema(client: TestClient): "files": { "title": "Files", "type": "array", - "items": {"type": "string", "format": "binary"}, + "items": { + "type": "string", + "contentMediaType": "application/octet-stream", + }, "description": "Multiple files as bytes", } }, @@ -178,7 +181,10 @@ def test_openapi_schema(client: TestClient): "files": { "title": "Files", "type": "array", - "items": {"type": "string", "format": "binary"}, + "items": { + "type": "string", + "contentMediaType": "application/octet-stream", + }, "description": "Multiple files as UploadFile", } }, diff --git a/tests/test_tutorial/test_request_forms_and_files/test_tutorial001.py b/tests/test_tutorial/test_request_forms_and_files/test_tutorial001.py index e2462e040b..cc10d8bec5 100644 --- a/tests/test_tutorial/test_request_forms_and_files/test_tutorial001.py +++ b/tests/test_tutorial/test_request_forms_and_files/test_tutorial001.py @@ -198,12 +198,12 @@ def test_openapi_schema(client: TestClient): "file": { "title": "File", "type": "string", - "format": "binary", + "contentMediaType": "application/octet-stream", }, "fileb": { "title": "Fileb", + "contentMediaType": "application/octet-stream", "type": "string", - "format": "binary", }, "token": {"title": "Token", "type": "string"}, }, diff --git a/docs_src/bigger_applications/app_an_py39/internal/__init__.py b/tests/test_tutorial/test_strict_content_type/__init__.py similarity index 100% rename from docs_src/bigger_applications/app_an_py39/internal/__init__.py rename to tests/test_tutorial/test_strict_content_type/__init__.py diff --git a/tests/test_tutorial/test_strict_content_type/test_tutorial001.py b/tests/test_tutorial/test_strict_content_type/test_tutorial001.py new file mode 100644 index 0000000000..81e2d3a0be --- /dev/null +++ b/tests/test_tutorial/test_strict_content_type/test_tutorial001.py @@ -0,0 +1,43 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient + + +@pytest.fixture( + name="client", + params=[ + "tutorial001_py310", + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.strict_content_type.{request.param}") + client = TestClient(mod.app) + return client + + +def test_lax_post_without_content_type_is_parsed_as_json(client: TestClient): + response = client.post( + "/items/", + content='{"name": "Foo", "price": 50.5}', + ) + assert response.status_code == 200, response.text + assert response.json() == {"name": "Foo", "price": 50.5} + + +def test_lax_post_with_json_content_type(client: TestClient): + response = client.post( + "/items/", + json={"name": "Foo", "price": 50.5}, + ) + assert response.status_code == 200, response.text + assert response.json() == {"name": "Foo", "price": 50.5} + + +def test_lax_post_with_text_plain_is_still_rejected(client: TestClient): + response = client.post( + "/items/", + content='{"name": "Foo", "price": 50.5}', + headers={"Content-Type": "text/plain"}, + ) + assert response.status_code == 422, response.text diff --git a/tests/test_union_body.py b/tests/test_union_body.py index e333e2499f..88f9e06cc8 100644 --- a/tests/test_union_body.py +++ b/tests/test_union_body.py @@ -1,5 +1,3 @@ -from typing import Optional, Union - from fastapi import FastAPI from fastapi.testclient import TestClient from inline_snapshot import snapshot @@ -9,7 +7,7 @@ app = FastAPI() class Item(BaseModel): - name: Optional[str] = None + name: str | None = None class OtherItem(BaseModel): @@ -17,7 +15,7 @@ class OtherItem(BaseModel): @app.post("/items/") -def save_union_body(item: Union[OtherItem, Item]): +def save_union_body(item: OtherItem | Item): return {"item": item} diff --git a/tests/test_union_body_discriminator.py b/tests/test_union_body_discriminator.py index 4afe7be4b4..1b682c7751 100644 --- a/tests/test_union_body_discriminator.py +++ b/tests/test_union_body_discriminator.py @@ -1,10 +1,9 @@ -from typing import Annotated, Any, Union +from typing import Annotated, Any, Literal from fastapi import FastAPI from fastapi.testclient import TestClient from inline_snapshot import snapshot from pydantic import BaseModel, Field -from typing_extensions import Literal def test_discriminator_pydantic_v2() -> None: @@ -21,7 +20,7 @@ def test_discriminator_pydantic_v2() -> None: price: float Item = Annotated[ - Union[Annotated[FirstItem, Tag("first")], Annotated[OtherItem, Tag("other")]], + Annotated[FirstItem, Tag("first")] | Annotated[OtherItem, Tag("other")], Field(discriminator="value"), ] diff --git a/tests/test_union_body_discriminator_annotated.py b/tests/test_union_body_discriminator_annotated.py index 6644d106c8..7e64ea75b4 100644 --- a/tests/test_union_body_discriminator_annotated.py +++ b/tests/test_union_body_discriminator_annotated.py @@ -1,6 +1,6 @@ # Ref: https://github.com/fastapi/fastapi/discussions/14495 -from typing import Annotated, Union +from typing import Annotated import pytest from fastapi import FastAPI @@ -27,7 +27,7 @@ def client_fixture() -> TestClient: return v.get("pet_type", "") Pet = Annotated[ - Union[Annotated[Cat, Tag("cat")], Annotated[Dog, Tag("dog")]], + Annotated[Cat, Tag("cat")] | Annotated[Dog, Tag("dog")], Discriminator(get_pet_type), ] diff --git a/tests/test_union_forms.py b/tests/test_union_forms.py index f6c2658f96..8cd7b4f017 100644 --- a/tests/test_union_forms.py +++ b/tests/test_union_forms.py @@ -1,4 +1,4 @@ -from typing import Annotated, Union +from typing import Annotated from fastapi import FastAPI, Form from fastapi.testclient import TestClient @@ -19,7 +19,7 @@ class CompanyForm(BaseModel): @app.post("/form-union/") -def post_union_form(data: Annotated[Union[UserForm, CompanyForm], Form()]): +def post_union_form(data: Annotated[UserForm | CompanyForm, Form()]): return {"received": data} diff --git a/tests/test_union_inherited_body.py b/tests/test_union_inherited_body.py index 5378880a47..c997a87a35 100644 --- a/tests/test_union_inherited_body.py +++ b/tests/test_union_inherited_body.py @@ -1,5 +1,3 @@ -from typing import Optional, Union - from fastapi import FastAPI from fastapi.testclient import TestClient from inline_snapshot import snapshot @@ -9,7 +7,7 @@ app = FastAPI() class Item(BaseModel): - name: Optional[str] = None + name: str | None = None class ExtendedItem(Item): @@ -17,7 +15,7 @@ class ExtendedItem(Item): @app.post("/items/") -def save_union_different_body(item: Union[ExtendedItem, Item]): +def save_union_different_body(item: ExtendedItem | Item): return {"item": item} diff --git a/tests/test_validate_response.py b/tests/test_validate_response.py index 938d419566..7288220eab 100644 --- a/tests/test_validate_response.py +++ b/tests/test_validate_response.py @@ -1,5 +1,3 @@ -from typing import Optional, Union - import pytest from fastapi import FastAPI from fastapi.exceptions import ResponseValidationError @@ -11,8 +9,8 @@ app = FastAPI() class Item(BaseModel): name: str - price: Optional[float] = None - owner_ids: Optional[list[int]] = None + price: float | None = None + owner_ids: list[int] | None = None @app.get("/items/invalid", response_model=Item) @@ -25,7 +23,7 @@ def get_invalid_none(): return None -@app.get("/items/validnone", response_model=Union[Item, None]) +@app.get("/items/validnone", response_model=Item | None) def get_valid_none(send_none: bool = False): if send_none: return None diff --git a/tests/test_validate_response_dataclass.py b/tests/test_validate_response_dataclass.py index 67282bcde1..03b7d5f338 100644 --- a/tests/test_validate_response_dataclass.py +++ b/tests/test_validate_response_dataclass.py @@ -1,5 +1,3 @@ -from typing import Optional - import pytest from fastapi import FastAPI from fastapi.exceptions import ResponseValidationError @@ -12,8 +10,8 @@ app = FastAPI() @dataclass class Item: name: str - price: Optional[float] = None - owner_ids: Optional[list[int]] = None + price: float | None = None + owner_ids: list[int] | None = None @app.get("/items/invalid", response_model=Item) diff --git a/tests/utils.py b/tests/utils.py index 4cbfee79f5..09c4e13b00 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -2,7 +2,6 @@ import sys import pytest -needs_py39 = pytest.mark.skipif(sys.version_info < (3, 9), reason="requires python3.9+") needs_py310 = pytest.mark.skipif( sys.version_info < (3, 10), reason="requires python3.10+" ) diff --git a/uv.lock b/uv.lock index aa8c558c7d..6faba96621 100644 --- a/uv.lock +++ b/uv.lock @@ -1083,12 +1083,10 @@ all = [ { name = "httpx" }, { name = "itsdangerous" }, { name = "jinja2" }, - { name = "orjson" }, { name = "pydantic-extra-types" }, { name = "pydantic-settings" }, { name = "python-multipart" }, { name = "pyyaml" }, - { name = "ujson" }, { name = "uvicorn", extra = ["standard"] }, ] standard = [ @@ -1134,6 +1132,7 @@ dev = [ { name = "mkdocs-redirects" }, { name = "mkdocstrings", extra = ["python"] }, { name = "mypy" }, + { name = "orjson" }, { name = "pillow" }, { name = "playwright" }, { name = "prek" }, @@ -1151,6 +1150,7 @@ dev = [ { name = "typer" }, { name = "types-orjson" }, { name = "types-ujson" }, + { name = "ujson" }, ] docs = [ { name = "black" }, @@ -1165,15 +1165,19 @@ docs = [ { name = "mkdocs-material" }, { name = "mkdocs-redirects" }, { name = "mkdocstrings", extra = ["python"] }, + { name = "orjson" }, { name = "pillow" }, { name = "python-slugify" }, { name = "pyyaml" }, { name = "ruff" }, { name = "typer" }, + { name = "ujson" }, ] docs-tests = [ { name = "httpx" }, + { name = "orjson" }, { name = "ruff" }, + { name = "ujson" }, ] github-actions = [ { name = "httpx" }, @@ -1192,6 +1196,7 @@ tests = [ { name = "httpx" }, { name = "inline-snapshot" }, { name = "mypy" }, + { name = "orjson" }, { name = "pwdlib", extra = ["argon2"] }, { name = "pyjwt" }, { name = "pytest" }, @@ -1202,6 +1207,7 @@ tests = [ { name = "strawberry-graphql" }, { name = "types-orjson" }, { name = "types-ujson" }, + { name = "ujson" }, ] translations = [ { name = "gitpython" }, @@ -1225,7 +1231,6 @@ requires-dist = [ { name = "jinja2", marker = "extra == 'all'", specifier = ">=3.1.5" }, { name = "jinja2", marker = "extra == 'standard'", specifier = ">=3.1.5" }, { name = "jinja2", marker = "extra == 'standard-no-fastapi-cloud-cli'", specifier = ">=3.1.5" }, - { name = "orjson", marker = "extra == 'all'", specifier = ">=3.9.3" }, { name = "pydantic", specifier = ">=2.7.0" }, { name = "pydantic-extra-types", marker = "extra == 'all'", specifier = ">=2.0.0" }, { name = "pydantic-extra-types", marker = "extra == 'standard'", specifier = ">=2.0.0" }, @@ -1240,7 +1245,6 @@ requires-dist = [ { name = "starlette", specifier = ">=0.40.0,<1.0.0" }, { name = "typing-extensions", specifier = ">=4.8.0" }, { name = "typing-inspection", specifier = ">=0.4.2" }, - { name = "ujson", marker = "extra == 'all'", specifier = ">=5.8.0" }, { name = "uvicorn", extras = ["standard"], marker = "extra == 'all'", specifier = ">=0.12.0" }, { name = "uvicorn", extras = ["standard"], marker = "extra == 'standard'", specifier = ">=0.12.0" }, { name = "uvicorn", extras = ["standard"], marker = "extra == 'standard-no-fastapi-cloud-cli'", specifier = ">=0.12.0" }, @@ -1269,6 +1273,7 @@ dev = [ { name = "mkdocs-redirects", specifier = ">=1.2.1,<1.3.0" }, { name = "mkdocstrings", extras = ["python"], specifier = ">=0.30.1" }, { name = "mypy", specifier = ">=1.14.1" }, + { name = "orjson", specifier = ">=3.9.3" }, { name = "pillow", specifier = ">=11.3.0" }, { name = "playwright", specifier = ">=1.57.0" }, { name = "prek", specifier = ">=0.2.22" }, @@ -1276,7 +1281,7 @@ dev = [ { name = "pydantic-ai", specifier = ">=0.4.10" }, { name = "pygithub", specifier = ">=2.8.1" }, { name = "pyjwt", specifier = ">=2.9.0" }, - { name = "pytest", specifier = ">=7.1.3,<9.0.0" }, + { name = "pytest", specifier = ">=9.0.0" }, { name = "pytest-codspeed", specifier = ">=4.2.0" }, { name = "python-slugify", specifier = ">=8.0.4" }, { name = "pyyaml", specifier = ">=5.3.1,<7.0.0" }, @@ -1286,6 +1291,7 @@ dev = [ { name = "typer", specifier = ">=0.21.1" }, { name = "types-orjson", specifier = ">=3.6.2" }, { name = "types-ujson", specifier = ">=5.10.0.20240515" }, + { name = "ujson", specifier = ">=5.8.0" }, ] docs = [ { name = "black", specifier = ">=25.1.0" }, @@ -1300,15 +1306,19 @@ docs = [ { name = "mkdocs-material", specifier = ">=9.7.0" }, { name = "mkdocs-redirects", specifier = ">=1.2.1,<1.3.0" }, { name = "mkdocstrings", extras = ["python"], specifier = ">=0.30.1" }, + { name = "orjson", specifier = ">=3.9.3" }, { name = "pillow", specifier = ">=11.3.0" }, { name = "python-slugify", specifier = ">=8.0.4" }, { name = "pyyaml", specifier = ">=5.3.1,<7.0.0" }, { name = "ruff", specifier = ">=0.14.14" }, { name = "typer", specifier = ">=0.21.1" }, + { name = "ujson", specifier = ">=5.8.0" }, ] docs-tests = [ { name = "httpx", specifier = ">=0.23.0,<1.0.0" }, + { name = "orjson", specifier = ">=3.9.3" }, { name = "ruff", specifier = ">=0.14.14" }, + { name = "ujson", specifier = ">=5.8.0" }, ] github-actions = [ { name = "httpx", specifier = ">=0.27.0,<1.0.0" }, @@ -1327,9 +1337,10 @@ tests = [ { name = "httpx", specifier = ">=0.23.0,<1.0.0" }, { name = "inline-snapshot", specifier = ">=0.21.1" }, { name = "mypy", specifier = ">=1.14.1" }, + { name = "orjson", specifier = ">=3.9.3" }, { name = "pwdlib", extras = ["argon2"], specifier = ">=0.2.1" }, { name = "pyjwt", specifier = ">=2.9.0" }, - { name = "pytest", specifier = ">=7.1.3,<9.0.0" }, + { name = "pytest", specifier = ">=9.0.0" }, { name = "pytest-codspeed", specifier = ">=4.2.0" }, { name = "pyyaml", specifier = ">=5.3.1,<7.0.0" }, { name = "ruff", specifier = ">=0.14.14" }, @@ -1337,6 +1348,7 @@ tests = [ { name = "strawberry-graphql", specifier = ">=0.200.0,<1.0.0" }, { name = "types-orjson", specifier = ">=3.6.2" }, { name = "types-ujson", specifier = ">=5.10.0.20240515" }, + { name = "ujson", specifier = ">=5.8.0" }, ] translations = [ { name = "gitpython", specifier = ">=3.1.46" }, @@ -1595,7 +1607,7 @@ wheels = [ [[package]] name = "flask" -version = "3.1.2" +version = "3.1.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "blinker" }, @@ -1605,9 +1617,9 @@ dependencies = [ { name = "markupsafe" }, { name = "werkzeug" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/dc/6d/cfe3c0fcc5e477df242b98bfe186a4c34357b4847e87ecaef04507332dab/flask-3.1.2.tar.gz", hash = "sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87", size = 720160, upload-time = "2025-08-19T21:03:21.205Z" } +sdist = { url = "https://files.pythonhosted.org/packages/26/00/35d85dcce6c57fdc871f3867d465d780f302a175ea360f62533f12b27e2b/flask-3.1.3.tar.gz", hash = "sha256:0ef0e52b8a9cd932855379197dd8f94047b359ca0a78695144304cb45f87c9eb", size = 759004, upload-time = "2026-02-19T05:00:57.678Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/f9/7f9263c5695f4bd0023734af91bedb2ff8209e8de6ead162f35d8dc762fd/flask-3.1.2-py3-none-any.whl", hash = "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c", size = 103308, upload-time = "2025-08-19T21:03:19.499Z" }, + { url = "https://files.pythonhosted.org/packages/7f/9c/34f6962f9b9e9c71f6e5ed806e0d0ff03c9d1b0b2340088a0cf4bce09b18/flask-3.1.3-py3-none-any.whl", hash = "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c", size = 103424, upload-time = "2026-02-19T05:00:56.027Z" }, ] [[package]] @@ -1911,40 +1923,36 @@ wheels = [ ] [[package]] -name = "griffe" -version = "1.15.0" +name = "griffelib" +version = "2.0.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0d/0c/3a471b6e31951dce2360477420d0a8d1e00dea6cf33b70f3e8c3ab6e28e1/griffe-1.15.0.tar.gz", hash = "sha256:7726e3afd6f298fbc3696e67958803e7ac843c1cfe59734b6251a40cdbfb5eea", size = 424112, upload-time = "2025-11-10T15:03:15.52Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/83/3b1d03d36f224edded98e9affd0467630fc09d766c0e56fb1498cbb04a9b/griffe-1.15.0-py3-none-any.whl", hash = "sha256:6f6762661949411031f5fcda9593f586e6ce8340f0ba88921a0f2ef7a81eb9a3", size = 150705, upload-time = "2025-11-10T15:03:13.549Z" }, + { url = "https://files.pythonhosted.org/packages/4d/51/c936033e16d12b627ea334aaaaf42229c37620d0f15593456ab69ab48161/griffelib-2.0.0-py3-none-any.whl", hash = "sha256:01284878c966508b6d6f1dbff9b6fa607bc062d8261c5c7253cb285b06422a7f", size = 142004, upload-time = "2026-02-09T19:09:40.561Z" }, ] [[package]] name = "griffe-typingdoc" -version = "0.3.0" +version = "0.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "griffe" }, + { name = "griffelib" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/be/77/d5e5fa0a8391bc2890ae45255847197299739833108dd76ee3c9b2ff0bba/griffe_typingdoc-0.3.0.tar.gz", hash = "sha256:59d9ef98d02caa7aed88d8df1119c9e48c02ed049ea50ce4018ace9331d20f8b", size = 33169, upload-time = "2025-10-23T12:01:39.037Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/26/28182e0c8055842bf3da774dee1d5b789c0f236c078dcbdca1937b5214dc/griffe_typingdoc-0.3.1.tar.gz", hash = "sha256:2ff4703115cb7f8a65b9fdcdd1f3c3a15f813b6554621b52eaad094c4782ce96", size = 31218, upload-time = "2026-02-21T09:38:54.409Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/48/af/aa32c13f753e2625ec895b1f56eee3c9380a2088a88a2c028955e223856e/griffe_typingdoc-0.3.0-py3-none-any.whl", hash = "sha256:4f6483fff7733a679d1dce142fb029f314125f3caaf0d620eb82e7390c8564bb", size = 9923, upload-time = "2025-10-23T12:01:37.601Z" }, + { url = "https://files.pythonhosted.org/packages/b6/c4/cf543fbde49e1ae44830ef0840a4d6ee9f4e4f338138a7766d4e37cf6440/griffe_typingdoc-0.3.1-py3-none-any.whl", hash = "sha256:ecbd457ef6883126b8b6023abf12e08c58e1c152238a2f0e2afdd67a64b07021", size = 10092, upload-time = "2026-02-20T14:53:47.84Z" }, ] [[package]] name = "griffe-warnings-deprecated" -version = "1.1.0" +version = "1.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "griffe" }, + { name = "griffelib" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7e/0e/f034e1714eb2c694d6196c75f77a02f9c69d19f9961c4804a016397bf3e5/griffe_warnings_deprecated-1.1.0.tar.gz", hash = "sha256:7bf21de327d59c66c7ce08d0166aa4292ce0577ff113de5878f428d102b6f7c5", size = 33260, upload-time = "2024-12-10T21:02:18.395Z" } +sdist = { url = "https://files.pythonhosted.org/packages/da/9e/fc86f1e9270f143a395a601de81aa42a871722c34d4b3c7763658dc2e04d/griffe_warnings_deprecated-1.1.1.tar.gz", hash = "sha256:9261369bf2acb8b5d24a0dc7895cce788208513d4349031d4ea315b979b2e99f", size = 26262, upload-time = "2026-02-21T09:38:55.858Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/43/4c/b7241f03ad1f22ec2eed33b0f90c4f8c949e3395c4b7488670b07225a20b/griffe_warnings_deprecated-1.1.0-py3-none-any.whl", hash = "sha256:e7b0e8bfd6e5add3945d4d9805b2a41c72409e456733965be276d55f01e8a7a2", size = 5854, upload-time = "2024-12-10T21:02:16.96Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3c/c2a9eee79bf2c8002d2fa370534bee93fdca39e8b1fc82e83d552d5d2c07/griffe_warnings_deprecated-1.1.1-py3-none-any.whl", hash = "sha256:4b7d765e82ca9139ed44ffe7bdebed0d3a46ce014ad5a35a2c22e9a16288737a", size = 6565, upload-time = "2026-02-20T15:35:23.577Z" }, ] [[package]] @@ -2991,17 +2999,17 @@ python = [ [[package]] name = "mkdocstrings-python" -version = "2.0.1" +version = "2.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "griffe" }, + { name = "griffelib" }, { name = "mkdocs-autorefs" }, { name = "mkdocstrings" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/24/75/d30af27a2906f00eb90143470272376d728521997800f5dce5b340ba35bc/mkdocstrings_python-2.0.1.tar.gz", hash = "sha256:843a562221e6a471fefdd4b45cc6c22d2607ccbad632879234fa9692e9cf7732", size = 199345, upload-time = "2025-12-03T14:26:11.755Z" } +sdist = { url = "https://files.pythonhosted.org/packages/29/33/c225eaf898634bdda489a6766fc35d1683c640bffe0e0acd10646b13536d/mkdocstrings_python-2.0.3.tar.gz", hash = "sha256:c518632751cc869439b31c9d3177678ad2bfa5c21b79b863956ad68fc92c13b8", size = 199083, upload-time = "2026-02-20T10:38:36.368Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/81/06/c5f8deba7d2cbdfa7967a716ae801aa9ca5f734b8f54fd473ef77a088dbe/mkdocstrings_python-2.0.1-py3-none-any.whl", hash = "sha256:66ecff45c5f8b71bf174e11d49afc845c2dfc7fc0ab17a86b6b337e0f24d8d90", size = 105055, upload-time = "2025-12-03T14:26:10.184Z" }, + { url = "https://files.pythonhosted.org/packages/32/28/79f0f8de97cce916d5ae88a7bee1ad724855e83e6019c0b4d5b3fabc80f3/mkdocstrings_python-2.0.3-py3-none-any.whl", hash = "sha256:0b83513478bdfd803ff05aa43e9b1fca9dd22bcd9471f09ca6257f009bc5ee12", size = 104779, upload-time = "2026-02-20T10:38:34.517Z" }, ] [[package]] @@ -3925,33 +3933,33 @@ email = [ [[package]] name = "pydantic-ai" -version = "1.56.0" +version = "1.62.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic-ai-slim", extra = ["ag-ui", "anthropic", "bedrock", "cli", "cohere", "evals", "fastmcp", "google", "groq", "huggingface", "logfire", "mcp", "mistral", "openai", "retries", "temporal", "ui", "vertexai", "xai"] }, ] -sdist = { url = "https://files.pythonhosted.org/packages/60/1a/800a1e02b259152a49d4c11d9103784a7482c7e9b067eeea23e949d3d80f/pydantic_ai-1.56.0.tar.gz", hash = "sha256:643ff71612df52315b3b4c4b41543657f603f567223eb33245dc8098f005bdc4", size = 11795, upload-time = "2026-02-06T01:13:21.122Z" } +sdist = { url = "https://files.pythonhosted.org/packages/20/97/e3158fa976a29e9580ba1c59601590424bbb81179c359fd29de0dc23aa09/pydantic_ai-1.62.0.tar.gz", hash = "sha256:d6ae517e365ea3ea162ca8ae643f319e105b71b0b6218b83dcad1d1eb2e38c9b", size = 12130, upload-time = "2026-02-19T05:07:07.853Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/35/f4a7fd2b9962ddb9b021f76f293e74fda71da190bb74b57ed5b343c93022/pydantic_ai-1.56.0-py3-none-any.whl", hash = "sha256:b6b3ac74bdc004693834750da4420ea2cde0d3cbc3f134c0b7544f98f1c00859", size = 7222, upload-time = "2026-02-06T01:13:11.755Z" }, + { url = "https://files.pythonhosted.org/packages/bc/7a/053aebfab576603e95fcfce1139de4a87e12bd5a2ef1ba00007a931c3ff0/pydantic_ai-1.62.0-py3-none-any.whl", hash = "sha256:1eb88f745ae045e63da41ad68966e8876c964d0f023fbf5d6a3f5d243370bd04", size = 7227, upload-time = "2026-02-19T05:06:58.341Z" }, ] [[package]] name = "pydantic-ai-slim" -version = "1.56.0" +version = "1.62.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "genai-prices" }, - { name = "griffe" }, + { name = "griffelib" }, { name = "httpx" }, { name = "opentelemetry-api" }, { name = "pydantic" }, { name = "pydantic-graph" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ce/5c/3a577825b9c1da8f287be7f2ee6fe9aab48bc8a80e65c8518052c589f51c/pydantic_ai_slim-1.56.0.tar.gz", hash = "sha256:9f9f9c56b1c735837880a515ae5661b465b40207b25f3a3434178098b2137f05", size = 415265, upload-time = "2026-02-06T01:13:23.58Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/8d/6350a49f2e4b636efbcfc233221420ab576e4ba4edba38254cb84ae4a1e6/pydantic_ai_slim-1.62.0.tar.gz", hash = "sha256:00d84f659107bbbd88823a3d3dbe7348385935a9870b9d7d4ba799256f6b6983", size = 422452, upload-time = "2026-02-19T05:07:10.292Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/62/4b/34682036528eeb9aaf093c2073540ddf399ab37b99d282a69ca41356f1aa/pydantic_ai_slim-1.56.0-py3-none-any.whl", hash = "sha256:d657e4113485020500b23b7390b0066e2a0277edc7577eaad2290735ca5dd7d5", size = 542270, upload-time = "2026-02-06T01:13:14.918Z" }, + { url = "https://files.pythonhosted.org/packages/3d/67/21e9b3b0944568662e3790c936226bd48a9f27c6b5f27b5916f5857bc4d8/pydantic_ai_slim-1.62.0-py3-none-any.whl", hash = "sha256:5210073fadd46f65859a67da67845093c487f025fa430ed027151f22ec684ab2", size = 549296, upload-time = "2026-02-19T05:07:01.624Z" }, ] [package.optional-dependencies] @@ -4169,7 +4177,7 @@ wheels = [ [[package]] name = "pydantic-graph" -version = "1.56.0" +version = "1.62.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, @@ -4177,9 +4185,9 @@ dependencies = [ { name = "pydantic" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ff/03/f92881cdb12d6f43e60e9bfd602e41c95408f06e2324d3729f7a194e2bcd/pydantic_graph-1.56.0.tar.gz", hash = "sha256:5e22972dbb43dbc379ab9944252ff864019abf3c7d465dcdf572fc8aec9a44a1", size = 58460, upload-time = "2026-02-06T01:13:26.708Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/b6/0b084c847ecd99624f4fbc5c8ecd3f67a2388a282a32612b2a68c3b3595f/pydantic_graph-1.62.0.tar.gz", hash = "sha256:efe56bee3a8ca35b11a3be6a5f7352419fe182ef1e1323a3267ee12dec95f3c7", size = 58529, upload-time = "2026-02-19T05:07:12.947Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/08/07/8c823eb4d196137c123d4d67434e185901d3cbaea3b0c2b7667da84e72c1/pydantic_graph-1.56.0-py3-none-any.whl", hash = "sha256:ec3f0a1d6fcedd4eb9c59fef45079c2ee4d4185878d70dae26440a9c974c6bb3", size = 72346, upload-time = "2026-02-06T01:13:18.792Z" }, + { url = "https://files.pythonhosted.org/packages/f0/12/1a9cbcd59fd070ba72b0fe544caa6ca97758518643523ec2bf1162084e0d/pydantic_graph-1.62.0-py3-none-any.whl", hash = "sha256:abe0e7b356b4d3202b069ec020d8dd1f647f55e9a0e85cd272dab48250bde87d", size = 72350, upload-time = "2026-02-19T05:07:05.305Z" }, ] [[package]] @@ -4330,7 +4338,7 @@ wheels = [ [[package]] name = "pytest" -version = "8.4.2" +version = "9.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -4341,9 +4349,9 @@ dependencies = [ { name = "pygments" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, ] [[package]]