From 994d6cc912ecb160e1495132bcd347f4511708bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 17 Nov 2025 20:33:53 +0100 Subject: [PATCH 01/47] =?UTF-8?q?=F0=9F=93=9D=20Add=20docs=20for=20using?= =?UTF-8?q?=20FastAPI=20Cloud=20(#14359)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 57 ++++++++++++++++++ docs/en/data/sponsors.yml | 4 ++ docs/en/docs/deployment/cloud.md | 16 +++-- docs/en/docs/deployment/fastapicloud.md | 65 +++++++++++++++++++++ docs/en/docs/deployment/index.md | 2 + docs/en/docs/img/sponsors/fastapicloud.png | Bin 0 -> 16777 bytes docs/en/docs/index.md | 62 +++++++++++++++++++- docs/en/docs/tutorial/first-steps.md | 57 ++++++++++++++++++ docs/en/mkdocs.yml | 1 + docs/en/overrides/main.html | 7 +++ scripts/docs.py | 10 +++- 11 files changed, 273 insertions(+), 8 deletions(-) create mode 100644 docs/en/docs/deployment/fastapicloud.md create mode 100644 docs/en/docs/img/sponsors/fastapicloud.png diff --git a/README.md b/README.md index 09cd38da1..9864fa1ef 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,11 @@ The key features are: ## Sponsors +### Keystone Sponsor + + + +### Gold and Silver Sponsors @@ -447,6 +452,58 @@ For a more complete example including more features, see the FastAPI Cloud, go and join the waiting list if you haven't. πŸš€ + +If you already have a **FastAPI Cloud** account (we invited you from the waiting list πŸ˜‰), you can deploy your application with one command. + +Before deploying, make sure you are logged in: + +
+ +```console +$ fastapi login + +You are logged in to FastAPI Cloud πŸš€ +``` + +
+ +Then deploy your app: + +
+ +```console +$ fastapi deploy + +Deploying to FastAPI Cloud... + +βœ… Deployment successful! + +πŸ” Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev +``` + +
+ +That's it! Now you can access your app at that URL. ✨ + +#### About FastAPI Cloud + +**FastAPI Cloud** is built by the same author and team behind **FastAPI**. + +It streamlines the process of **building**, **deploying**, and **accessing** an API with minimal effort. + +It brings the same **developer experience** of building apps with FastAPI to **deploying** them to the cloud. πŸŽ‰ + +FastAPI Cloud is the primary sponsor and funding provider for the *FastAPI and friends* open source projects. ✨ + +#### Deploy to other cloud providers + +FastAPI is open source and based on standards. You can deploy FastAPI apps to any cloud provider you choose. + +Follow your cloud provider's guides to deploy FastAPI apps with them. πŸ€“ + ## Performance Independent TechEmpower benchmarks show **FastAPI** applications running under Uvicorn as one of the fastest Python frameworks available, only below Starlette and Uvicorn themselves (used internally by FastAPI). (*) diff --git a/docs/en/data/sponsors.yml b/docs/en/data/sponsors.yml index 943b92adb..b8cc31dbe 100644 --- a/docs/en/data/sponsors.yml +++ b/docs/en/data/sponsors.yml @@ -1,3 +1,7 @@ +keystone: + - url: https://fastapicloud.com + title: FastAPI Cloud. By the same team behind FastAPI. You code. We Cloud. + img: https://fastapi.tiangolo.com/img/sponsors/fastapicloud.png gold: - url: https://blockbee.io?ref=fastapi title: BlockBee Cryptocurrency Payment Gateway diff --git a/docs/en/docs/deployment/cloud.md b/docs/en/docs/deployment/cloud.md index c88c4b51a..bdba87bce 100644 --- a/docs/en/docs/deployment/cloud.md +++ b/docs/en/docs/deployment/cloud.md @@ -4,13 +4,21 @@ You can use virtually **any cloud provider** to deploy your FastAPI application. In most of the cases, the main cloud providers have guides to deploy FastAPI with them. +## FastAPI Cloud + +**FastAPI Cloud** is built by the same author and team behind **FastAPI**. + +It streamlines the process of **building**, **deploying**, and **accessing** an API with minimal effort. + +It brings the same **developer experience** of building apps with FastAPI to **deploying** them to the cloud. πŸŽ‰ + +FastAPI Cloud is the primary sponsor and funding provider for the *FastAPI and friends* open source projects. ✨ + ## Cloud Providers - Sponsors { #cloud-providers-sponsors } -Some cloud providers ✨ [**sponsor FastAPI**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨, this ensures the continued and healthy **development** of FastAPI and its **ecosystem**. +Some other cloud providers ✨ [**sponsor FastAPI**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨ too. πŸ™‡ -And it shows their true commitment to FastAPI and its **community** (you), as they not only want to provide you a **good service** but also want to make sure you have a **good and healthy framework**, FastAPI. πŸ™‡ - -You might want to try their services and follow their guides: +You might also want to consider them to follow their guides and try their services: * Render * Railway diff --git a/docs/en/docs/deployment/fastapicloud.md b/docs/en/docs/deployment/fastapicloud.md new file mode 100644 index 000000000..b0889974f --- /dev/null +++ b/docs/en/docs/deployment/fastapicloud.md @@ -0,0 +1,65 @@ +# FastAPI Cloud { #fastapi-cloud } + +You can deploy your FastAPI app to FastAPI Cloud with **one command**, go and join the waiting list if you haven't. πŸš€ + +## Login { #login } + +Make sure you already have a **FastAPI Cloud** account (we invited you from the waiting list πŸ˜‰). + +Then log in: + +
+ +```console +$ fastapi login + +You are logged in to FastAPI Cloud πŸš€ +``` + +
+ +## Deploy { #deploy } + +Now deploy your app, with **one command**: + +
+ +```console +$ fastapi deploy + +Deploying to FastAPI Cloud... + +βœ… Deployment successful! + +πŸ” Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev +``` + +
+ +That's it! Now you can access your app at that URL. ✨ + +## About FastAPI Cloud { #about-fastapi-cloud } + +**FastAPI Cloud** is built by the same author and team behind **FastAPI**. + +It streamlines the process of **building**, **deploying**, and **accessing** an API with minimal effort. + +It brings the same **developer experience** of building apps with FastAPI to **deploying** them to the cloud. πŸŽ‰ + +It will also take care of most of the things you would need when deploying an app, like: + +* HTTPS +* Replication, with autoscaling based on requests +* etc. + +FastAPI Cloud is the primary sponsor and funding provider for the *FastAPI and friends* open source projects. ✨ + +## Deploy to other cloud providers { #deploy-to-other-cloud-providers } + +FastAPI is open source and based on standards. You can deploy FastAPI apps to any cloud provider you choose. + +Follow your cloud provider's guides to deploy FastAPI apps with them. πŸ€“ + +## Deploy your own server { #deploy-your-own-server } + +I will also teach you later in this **Deployment** guide all the details, so you can understand what is going on, what needs to happen, or how to deploy FastAPI apps on your own, also with your own servers. πŸ€“ diff --git a/docs/en/docs/deployment/index.md b/docs/en/docs/deployment/index.md index 2364791a7..8d7521e73 100644 --- a/docs/en/docs/deployment/index.md +++ b/docs/en/docs/deployment/index.md @@ -16,6 +16,8 @@ There are several ways to do it depending on your specific use case and the tool You could **deploy a server** yourself using a combination of tools, you could use a **cloud service** that does part of the work for you, or other possible options. +For example, we, the team behind FastAPI, built **FastAPI Cloud**, to make deploying FastAPI apps to the cloud as streamlined as possible, with the same developer experience of working with FastAPI. + I will show you some of the main concepts you should probably keep in mind when deploying a **FastAPI** application (although most of it applies to any other type of web application). You will see more details to keep in mind and some of the techniques to do it in the next sections. ✨ diff --git a/docs/en/docs/img/sponsors/fastapicloud.png b/docs/en/docs/img/sponsors/fastapicloud.png new file mode 100644 index 0000000000000000000000000000000000000000..c23dec220951ca1e5321411ea7200aee48cbe89d GIT binary patch literal 16777 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 -{% if sponsors %} +### Keystone Sponsor + +{% for sponsor in sponsors.keystone -%} + +{% endfor -%} + +### Gold and Silver Sponsors + {% for sponsor in sponsors.gold -%} {% endfor -%} {%- for sponsor in sponsors.silver -%} {% endfor %} -{% endif %} @@ -444,6 +450,58 @@ For a more complete example including more features, see the FastAPI Cloud, go and join the waiting list if you haven't. πŸš€ + +If you already have a **FastAPI Cloud** account (we invited you from the waiting list πŸ˜‰), you can deploy your application with one command. + +Before deploying, make sure you are logged in: + +
+ +```console +$ fastapi login + +You are logged in to FastAPI Cloud πŸš€ +``` + +
+ +Then deploy your app: + +
+ +```console +$ fastapi deploy + +Deploying to FastAPI Cloud... + +βœ… Deployment successful! + +πŸ” Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev +``` + +
+ +That's it! Now you can access your app at that URL. ✨ + +#### About FastAPI Cloud { #about-fastapi-cloud } + +**FastAPI Cloud** is built by the same author and team behind **FastAPI**. + +It streamlines the process of **building**, **deploying**, and **accessing** an API with minimal effort. + +It brings the same **developer experience** of building apps with FastAPI to **deploying** them to the cloud. πŸŽ‰ + +FastAPI Cloud is the primary sponsor and funding provider for the *FastAPI and friends* open source projects. ✨ + +#### Deploy to other cloud providers { #deploy-to-other-cloud-providers } + +FastAPI is open source and based on standards. You can deploy FastAPI apps to any cloud provider you choose. + +Follow your cloud provider's guides to deploy FastAPI apps with them. πŸ€“ + ## Performance { #performance } Independent TechEmpower benchmarks show **FastAPI** applications running under Uvicorn as one of the fastest Python frameworks available, only below Starlette and Uvicorn themselves (used internally by FastAPI). (*) diff --git a/docs/en/docs/tutorial/first-steps.md b/docs/en/docs/tutorial/first-steps.md index 7d4c12de8..b88ff6a18 100644 --- a/docs/en/docs/tutorial/first-steps.md +++ b/docs/en/docs/tutorial/first-steps.md @@ -143,6 +143,42 @@ And there are dozens of alternatives, all based on OpenAPI. You could easily add You could also use it to generate code automatically, for clients that communicate with your API. For example, frontend, mobile or IoT applications. +### Deploy your app (optional) { #deploy-your-app-optional } + +You can optionally deploy your FastAPI app to FastAPI Cloud, go and join the waiting list if you haven't. πŸš€ + +If you already have a **FastAPI Cloud** account (we invited you from the waiting list πŸ˜‰), you can deploy your application with one command. + +Before deploying, make sure you are logged in: + +
+ +```console +$ fastapi login + +You are logged in to FastAPI Cloud πŸš€ +``` + +
+ +Then deploy your app: + +
+ +```console +$ fastapi deploy + +Deploying to FastAPI Cloud... + +βœ… Deployment successful! + +πŸ” Ready the chicken! Your app is ready at https://myapp.fastapicloud.dev +``` + +
+ +That's it! Now you can access your app at that URL. ✨ + ## Recap, step by step { #recap-step-by-step } ### Step 1: import `FastAPI` { #step-1-import-fastapi } @@ -314,6 +350,26 @@ You can also return Pydantic models (you'll see more about that later). There are many other objects and models that will be automatically converted to JSON (including ORMs, etc). Try using your favorite ones, it's highly probable that they are already supported. +### Step 6: Deploy it { #step-6-deploy-it } + +Deploy your app to **FastAPI Cloud** with one command: `fastapi deploy`. πŸŽ‰ + +#### About FastAPI Cloud { #about-fastapi-cloud } + +**FastAPI Cloud** is built by the same author and team behind **FastAPI**. + +It streamlines the process of **building**, **deploying**, and **accessing** an API with minimal effort. + +It brings the same **developer experience** of building apps with FastAPI to **deploying** them to the cloud. πŸŽ‰ + +FastAPI Cloud is the primary sponsor and funding provider for the *FastAPI and friends* open source projects. ✨ + +#### Deploy to other cloud providers { #deploy-to-other-cloud-providers } + +FastAPI is open source and based on standards. You can deploy FastAPI apps to any cloud provider you choose. + +Follow your cloud provider's guides to deploy FastAPI apps with them. πŸ€“ + ## Recap { #recap } * Import `FastAPI`. @@ -321,3 +377,4 @@ There are many other objects and models that will be automatically converted to * Write a **path operation decorator** using decorators like `@app.get("/")`. * Define a **path operation function**; for example, `def root(): ...`. * Run the development server using the command `fastapi dev`. +* Optionally deploy your app with `fastapi deploy`. diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index 323035240..df47c6f9c 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -192,6 +192,7 @@ nav: - Deployment: - deployment/index.md - deployment/versions.md + - deployment/fastapicloud.md - deployment/https.md - deployment/manually.md - deployment/concepts.md diff --git a/docs/en/overrides/main.html b/docs/en/overrides/main.html index be31bd75c..01d39817b 100644 --- a/docs/en/overrides/main.html +++ b/docs/en/overrides/main.html @@ -3,6 +3,13 @@ {% block announce %}
+
diff --git a/scripts/docs.py b/scripts/docs.py index 56ffb9d36..1a336a036 100644 --- a/scripts/docs.py +++ b/scripts/docs.py @@ -145,14 +145,20 @@ def build_lang( index_sponsors_template = """ -{% if sponsors %} +### Keystone Sponsor + +{% for sponsor in sponsors.keystone -%} + +{% endfor %} +### Gold and Silver Sponsors + {% for sponsor in sponsors.gold -%} {% endfor -%} {%- for sponsor in sponsors.silver -%} {% endfor %} -{% endif %} + """ From 4e84f3169436dfe06c963928a6af297d06cc560e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 17 Nov 2025 19:34:16 +0000 Subject: [PATCH 02/47] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index a3e051a30..a3a83473d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Docs + +* πŸ“ Add docs for using FastAPI Cloud. PR [#14359](https://github.com/fastapi/fastapi/pull/14359) by [@tiangolo](https://github.com/tiangolo). + ## 0.121.2 ### Fixes From df83eb7278edf036612dc1b4c33c02415123d6a7 Mon Sep 17 00:00:00 2001 From: Edge-Seven <143301646+Edge-Seven@users.noreply.github.com> Date: Tue, 18 Nov 2025 15:30:20 +0700 Subject: [PATCH 03/47] =?UTF-8?q?=F0=9F=93=9D=20Fix=20typos=20in=20code=20?= =?UTF-8?q?comments=20(#14364)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix typos in some files Co-authored-by: khanhkhanhlele --- tests/test_tutorial/test_sql_databases/test_tutorial001.py | 2 +- tests/test_tutorial/test_sql_databases/test_tutorial002.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_tutorial/test_sql_databases/test_tutorial001.py b/tests/test_tutorial/test_sql_databases/test_tutorial001.py index 6604a2fd3..b45be4884 100644 --- a/tests/test_tutorial/test_sql_databases/test_tutorial001.py +++ b/tests/test_tutorial/test_sql_databases/test_tutorial001.py @@ -45,7 +45,7 @@ def get_client(request: pytest.FixtureRequest): with TestClient(mod.app) as c: yield c - # Clean up connection explicitely to avoid resource warning + # Clean up connection explicitly to avoid resource warning mod.engine.dispose() diff --git a/tests/test_tutorial/test_sql_databases/test_tutorial002.py b/tests/test_tutorial/test_sql_databases/test_tutorial002.py index 2c4e0988c..da0b8b7ce 100644 --- a/tests/test_tutorial/test_sql_databases/test_tutorial002.py +++ b/tests/test_tutorial/test_sql_databases/test_tutorial002.py @@ -45,7 +45,7 @@ def get_client(request: pytest.FixtureRequest): with TestClient(mod.app) as c: yield c - # Clean up connection explicitely to avoid resource warning + # Clean up connection explicitly to avoid resource warning mod.engine.dispose() From 827ed1e6a24300af9b46adb8a3d208b8685eef85 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 18 Nov 2025 08:30:46 +0000 Subject: [PATCH 04/47] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index a3a83473d..c3ee858ae 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Docs +* πŸ“ Fix typos in code comments. PR [#14364](https://github.com/fastapi/fastapi/pull/14364) by [@Edge-Seven](https://github.com/Edge-Seven). * πŸ“ Add docs for using FastAPI Cloud. PR [#14359](https://github.com/fastapi/fastapi/pull/14359) by [@tiangolo](https://github.com/tiangolo). ## 0.121.2 From 89baa704a9f88ab09b0b01cd2fa7dcf758d6d417 Mon Sep 17 00:00:00 2001 From: Nils-Hero Lindemann Date: Wed, 19 Nov 2025 11:12:00 +0100 Subject: [PATCH 05/47] =?UTF-8?q?=F0=9F=93=9D=20Add=20missing=20hash=20par?= =?UTF-8?q?t=20(#14369)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add missing hash part Was forgotten in #14359. I already added it in #14367 --- docs/en/docs/deployment/cloud.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/deployment/cloud.md b/docs/en/docs/deployment/cloud.md index bdba87bce..4f5c23e4b 100644 --- a/docs/en/docs/deployment/cloud.md +++ b/docs/en/docs/deployment/cloud.md @@ -4,7 +4,7 @@ You can use virtually **any cloud provider** to deploy your FastAPI application. In most of the cases, the main cloud providers have guides to deploy FastAPI with them. -## FastAPI Cloud +## FastAPI Cloud { #fastapi-cloud } **FastAPI Cloud** is built by the same author and team behind **FastAPI**. From 33a75f481738c3d20ebc9fb17773d2da08b0e869 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 19 Nov 2025 10:12:24 +0000 Subject: [PATCH 06/47] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index c3ee858ae..6001079cd 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Docs +* πŸ“ Add missing hash part. PR [#14369](https://github.com/fastapi/fastapi/pull/14369) by [@nilslindemann](https://github.com/nilslindemann). * πŸ“ Fix typos in code comments. PR [#14364](https://github.com/fastapi/fastapi/pull/14364) by [@Edge-Seven](https://github.com/Edge-Seven). * πŸ“ Add docs for using FastAPI Cloud. PR [#14359](https://github.com/fastapi/fastapi/pull/14359) by [@tiangolo](https://github.com/tiangolo). From 569226e753f601954a129331f61147f89abdb52a Mon Sep 17 00:00:00 2001 From: Ben Beasley Date: Wed, 19 Nov 2025 11:55:05 +0000 Subject: [PATCH 07/47] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Bump=20Starlette=20t?= =?UTF-8?q?o=20<`0.51.0`=20(#14282)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 7d2be0074..cafcf65c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,7 @@ classifiers = [ "Topic :: Internet :: WWW/HTTP", ] dependencies = [ - "starlette>=0.40.0,<0.50.0", + "starlette>=0.40.0,<0.51.0", "pydantic>=1.7.4,!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0", "typing-extensions>=4.8.0", "annotated-doc>=0.0.2", From 566e3157a5517784c741dbf36e39687af326fe90 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 19 Nov 2025 11:55:32 +0000 Subject: [PATCH 08/47] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 6001079cd..69b15e645 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Upgrades + +* ⬆️ Bump Starlette to <`0.51.0`. PR [#14282](https://github.com/fastapi/fastapi/pull/14282) by [@musicinmybrain](https://github.com/musicinmybrain). + ### Docs * πŸ“ Add missing hash part. PR [#14369](https://github.com/fastapi/fastapi/pull/14369) by [@nilslindemann](https://github.com/nilslindemann). From 85701631a0241c5f02b4940734a5428f66abe167 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 19 Nov 2025 17:50:18 +0100 Subject: [PATCH 09/47] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Make=20the=20result?= =?UTF-8?q?=20of=20`Depends()`=20and=20`Security()`=20hashable,=20as=20a?= =?UTF-8?q?=20workaround=20for=20other=20tools=20interacting=20with=20thes?= =?UTF-8?q?e=20internal=20parts=20(#14372)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/dependencies/utils.py | 3 ++- fastapi/params.py | 4 ++-- tests/test_depends_hashable.py | 25 +++++++++++++++++++++++++ 3 files changed, 29 insertions(+), 3 deletions(-) create mode 100644 tests/test_depends_hashable.py diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 4b69e39a1..1e92c1ba2 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -1,3 +1,4 @@ +import dataclasses import inspect from contextlib import AsyncExitStack, contextmanager from copy import copy, deepcopy @@ -428,7 +429,7 @@ def analyze_param( if depends is not None and depends.dependency is None: # Copy `depends` before mutating it depends = copy(depends) - depends.dependency = type_annotation + depends = dataclasses.replace(depends, dependency=type_annotation) # Handle non-param type annotations like Request if lenient_issubclass( diff --git a/fastapi/params.py b/fastapi/params.py index 6a58d5808..6d07df35e 100644 --- a/fastapi/params.py +++ b/fastapi/params.py @@ -762,13 +762,13 @@ class File(Form): # type: ignore[misc] ) -@dataclass +@dataclass(frozen=True) class Depends: dependency: Optional[Callable[..., Any]] = None use_cache: bool = True scope: Union[Literal["function", "request"], None] = None -@dataclass +@dataclass(frozen=True) class Security(Depends): scopes: Optional[Sequence[str]] = None diff --git a/tests/test_depends_hashable.py b/tests/test_depends_hashable.py new file mode 100644 index 000000000..d57f2726e --- /dev/null +++ b/tests/test_depends_hashable.py @@ -0,0 +1,25 @@ +# This is more or less a workaround to make Depends and Security hashable +# as other tools that use them depend on that +# Ref: https://github.com/fastapi/fastapi/pull/14320 + +from fastapi import Depends, Security + + +def dep(): + pass + + +def test_depends_hashable(): + dep() # just for coverage + d1 = Depends(dep) + d2 = Depends(dep) + d3 = Depends(dep, scope="function") + d4 = Depends(dep, scope="function") + + s1 = Security(dep) + s2 = Security(dep) + + assert hash(d1) == hash(d2) + assert hash(s1) == hash(s2) + assert hash(d1) != hash(d3) + assert hash(d3) == hash(d4) From 7659b70da0aea0678a0842a5d7a8aaaef2527bab Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 19 Nov 2025 16:50:42 +0000 Subject: [PATCH 10/47] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 69b15e645..c466152f2 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Refactors + +* ♻️ Make the result of `Depends()` and `Security()` hashable, as a workaround for other tools interacting with these internal parts. PR [#14372](https://github.com/fastapi/fastapi/pull/14372) by [@tiangolo](https://github.com/tiangolo). + ### Upgrades * ⬆️ Bump Starlette to <`0.51.0`. PR [#14282](https://github.com/fastapi/fastapi/pull/14282) by [@musicinmybrain](https://github.com/musicinmybrain). From 325fd16d32fd815633d6caf3d0d75aa7f30c51c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 19 Nov 2025 17:51:59 +0100 Subject: [PATCH 11/47] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.121.?= =?UTF-8?q?3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index c466152f2..c02fe075d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +## 0.121.3 + ### Refactors * ♻️ Make the result of `Depends()` and `Security()` hashable, as a workaround for other tools interacting with these internal parts. PR [#14372](https://github.com/fastapi/fastapi/pull/14372) by [@tiangolo](https://github.com/tiangolo). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 0672423cf..85a7ea7b5 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.121.2" +__version__ = "0.121.3" from starlette import status as status From be5a6311f5ad542f253f48ca95cd863026b67c05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 20 Nov 2025 11:45:16 +0100 Subject: [PATCH 12/47] =?UTF-8?q?=F0=9F=94=A7=20Upgrade=20Material=20for?= =?UTF-8?q?=20MkDocs=20and=20remove=20insiders=20(#14375)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build-docs.yml | 16 +--------------- ...{mkdocs.maybe-insiders.yml => mkdocs.env.yml} | 1 - docs/en/mkdocs.insiders.yml | 10 ---------- docs/en/mkdocs.no-insiders.yml | 0 docs/en/mkdocs.yml | 10 +++++++++- requirements-docs-insiders.txt | 3 --- requirements-docs.txt | 3 ++- scripts/docs.py | 16 +--------------- 8 files changed, 13 insertions(+), 46 deletions(-) rename docs/en/{mkdocs.maybe-insiders.yml => mkdocs.env.yml} (78%) delete mode 100644 docs/en/mkdocs.insiders.yml delete mode 100644 docs/en/mkdocs.no-insiders.yml delete mode 100644 requirements-docs-insiders.txt diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index f78b6730e..995a0eb41 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -32,12 +32,9 @@ jobs: - docs/** - docs_src/** - requirements-docs.txt - - requirements-docs-insiders.txt - pyproject.toml - mkdocs.yml - - mkdocs.insiders.yml - - mkdocs.maybe-insiders.yml - - mkdocs.no-insiders.yml + - mkdocs.env.yml - .github/workflows/build-docs.yml - .github/workflows/deploy-docs.yml - scripts/mkdocs_hooks.py @@ -63,12 +60,6 @@ jobs: pyproject.toml - name: Install docs extras run: uv pip install -r requirements-docs.txt - # Install MkDocs Material Insiders here just to put it in the cache for the rest of the steps - - name: Install Material for MkDocs Insiders - if: ( github.event_name != 'pull_request' || github.secret_source == 'Actions' ) - run: uv pip install -r requirements-docs-insiders.txt - env: - TOKEN: ${{ secrets.FASTAPI_MKDOCS_MATERIAL_INSIDERS }} - name: Verify Docs run: python ./scripts/docs.py verify-docs - name: Export Language Codes @@ -105,11 +96,6 @@ jobs: pyproject.toml - name: Install docs extras run: uv pip install -r requirements-docs.txt - - name: Install Material for MkDocs Insiders - if: ( github.event_name != 'pull_request' || github.secret_source == 'Actions' ) - run: uv pip install -r requirements-docs-insiders.txt - env: - TOKEN: ${{ secrets.FASTAPI_MKDOCS_MATERIAL_INSIDERS }} - name: Update Languages run: python ./scripts/docs.py update-languages - uses: actions/cache@v4 diff --git a/docs/en/mkdocs.maybe-insiders.yml b/docs/en/mkdocs.env.yml similarity index 78% rename from docs/en/mkdocs.maybe-insiders.yml rename to docs/en/mkdocs.env.yml index 37fd9338e..c5f6e07d7 100644 --- a/docs/en/mkdocs.maybe-insiders.yml +++ b/docs/en/mkdocs.env.yml @@ -1,6 +1,5 @@ # Define this here and not in the main mkdocs.yml file because that one is auto # updated and written, and the script would remove the env var -INHERIT: !ENV [INSIDERS_FILE, '../en/mkdocs.no-insiders.yml'] markdown_extensions: pymdownx.highlight: linenums: !ENV [LINENUMS, false] diff --git a/docs/en/mkdocs.insiders.yml b/docs/en/mkdocs.insiders.yml deleted file mode 100644 index 8d6d26e17..000000000 --- a/docs/en/mkdocs.insiders.yml +++ /dev/null @@ -1,10 +0,0 @@ -plugins: - social: - cards_layout_options: - logo: ../en/docs/img/icon-white.svg - typeset: -markdown_extensions: - material.extensions.preview: - targets: - include: - - "*" diff --git a/docs/en/mkdocs.no-insiders.yml b/docs/en/mkdocs.no-insiders.yml deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index df47c6f9c..8be832f11 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -1,4 +1,4 @@ -INHERIT: ../en/mkdocs.maybe-insiders.yml +INHERIT: ../en/mkdocs.env.yml site_name: FastAPI site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production site_url: https://fastapi.tiangolo.com/ @@ -52,6 +52,10 @@ theme: repo_name: fastapi/fastapi repo_url: https://github.com/fastapi/fastapi plugins: + social: + cards_layout_options: + logo: ../en/docs/img/icon-white.svg + typeset: search: null macros: include_yaml: @@ -253,6 +257,10 @@ nav: - management.md - release-notes.md markdown_extensions: + material.extensions.preview: + targets: + include: + - "*" abbr: null attr_list: null footnotes: null diff --git a/requirements-docs-insiders.txt b/requirements-docs-insiders.txt deleted file mode 100644 index d8d3c37a9..000000000 --- a/requirements-docs-insiders.txt +++ /dev/null @@ -1,3 +0,0 @@ -git+https://${TOKEN}@github.com/squidfunk/mkdocs-material-insiders.git@9.5.30-insiders-4.53.11 -git+https://${TOKEN}@github.com/pawamoy-insiders/griffe-typing-deprecated.git -git+https://${TOKEN}@github.com/pawamoy-insiders/mkdocstrings-python.git diff --git a/requirements-docs.txt b/requirements-docs.txt index 696eb2a33..d60125bbe 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,6 +1,6 @@ -e . -r requirements-docs-tests.txt -mkdocs-material==9.6.16 +mkdocs-material==9.7.0 mdx-include >=1.4.1,<2.0.0 mkdocs-redirects>=1.2.1,<1.3.0 typer == 0.16.0 @@ -13,6 +13,7 @@ pillow==11.3.0 cairosvg==2.8.2 mkdocstrings[python]==0.30.1 griffe-typingdoc==0.3.0 +griffe-warnings-deprecated==1.1.0 # For griffe, it formats with black black==25.1.0 mkdocs-macros-plugin==1.4.1 diff --git a/scripts/docs.py b/scripts/docs.py index 1a336a036..d08a218f8 100644 --- a/scripts/docs.py +++ b/scripts/docs.py @@ -4,9 +4,7 @@ import os import re import shutil import subprocess -from functools import lru_cache from http.server import HTTPServer, SimpleHTTPRequestHandler -from importlib import metadata from multiprocessing import Pool from pathlib import Path from typing import Any, Dict, List, Optional, Union @@ -47,12 +45,6 @@ build_site_path = Path("site_build").absolute() header_with_permalink_pattern = re.compile(r"^(#{1,6}) (.+?)(\s*\{\s*#.*\s*\})\s*$") -@lru_cache -def is_mkdocs_insiders() -> bool: - version = metadata.version("mkdocs-material") - return "insiders" in version - - def get_en_config() -> Dict[str, Any]: return mkdocs.utils.yaml_load(en_config_path.read_text(encoding="utf-8")) @@ -77,9 +69,7 @@ def complete_existing_lang(incomplete: str): @app.callback() def callback() -> None: - if is_mkdocs_insiders(): - os.environ["INSIDERS_FILE"] = "../en/mkdocs.insiders.yml" - # For MacOS with insiders and Cairo + # For MacOS with Cairo os.environ["DYLD_FALLBACK_LIBRARY_PATH"] = "/opt/homebrew/lib" @@ -115,10 +105,6 @@ def build_lang( """ Build the docs for a language. """ - insiders_env_file = os.environ.get("INSIDERS_FILE") - print(f"Insiders file {insiders_env_file}") - if is_mkdocs_insiders(): - print("Using insiders") lang_path: Path = Path("docs") / lang if not lang_path.is_dir(): typer.echo(f"The language translation doesn't seem to exist yet: {lang}") From 456008a52b01b570a0f03f8c9d53722eda137728 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 20 Nov 2025 10:45:39 +0000 Subject: [PATCH 13/47] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index c02fe075d..bf4120f36 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Internal + +* πŸ”§ Upgrade Material for MkDocs and remove insiders. PR [#14375](https://github.com/fastapi/fastapi/pull/14375) by [@tiangolo](https://github.com/tiangolo). + ## 0.121.3 ### Refactors From 32b375c5e445efb1efd129d825016bec8dbfb08a Mon Sep 17 00:00:00 2001 From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Fri, 21 Nov 2025 13:49:11 +0100 Subject: [PATCH 14/47] =?UTF-8?q?=F0=9F=9B=A0=EF=B8=8F=20Add=20`add-permal?= =?UTF-8?q?inks`=20and=20`add-permalinks-page`=20to=20`scripts/docs.py`=20?= =?UTF-8?q?(#14033)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: SebastiΓ‘n RamΓ­rez --- requirements-docs.txt | 1 + scripts/docs.py | 109 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 107 insertions(+), 3 deletions(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index d60125bbe..05b47fe92 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -18,3 +18,4 @@ griffe-warnings-deprecated==1.1.0 black==25.1.0 mkdocs-macros-plugin==1.4.1 markdown-include-variants==0.0.5 +python-slugify==8.0.4 diff --git a/scripts/docs.py b/scripts/docs.py index d08a218f8..d67ab50f7 100644 --- a/scripts/docs.py +++ b/scripts/docs.py @@ -4,6 +4,7 @@ import os import re import shutil import subprocess +from html.parser import HTMLParser from http.server import HTTPServer, SimpleHTTPRequestHandler from multiprocessing import Pool from pathlib import Path @@ -14,6 +15,7 @@ import typer import yaml from jinja2 import Template from ruff.__main__ import find_ruff_bin +from slugify import slugify as py_slugify logging.basicConfig(level=logging.INFO) @@ -25,8 +27,8 @@ missing_translation_snippet = """ {!../../docs/missing-translation.md!} """ -non_translated_sections = [ - "reference/", +non_translated_sections = ( + f"reference{os.sep}", "release-notes.md", "fastapi-people.md", "external-links.md", @@ -34,7 +36,7 @@ non_translated_sections = [ "management-tasks.md", "management.md", "contributing.md", -] +) docs_path = Path("docs") en_docs_path = Path("docs/en") @@ -42,7 +44,39 @@ en_config_path: Path = en_docs_path / mkdocs_name site_path = Path("site").absolute() build_site_path = Path("site_build").absolute() +header_pattern = re.compile(r"^(#{1,6}) (.+?)(?:\s*\{\s*(#.*)\s*\})?\s*$") header_with_permalink_pattern = re.compile(r"^(#{1,6}) (.+?)(\s*\{\s*#.*\s*\})\s*$") +code_block3_pattern = re.compile(r"^\s*```") +code_block4_pattern = re.compile(r"^\s*````") + + +class VisibleTextExtractor(HTMLParser): + """Extract visible text from a string with HTML tags.""" + + def __init__(self): + super().__init__() + self.text_parts = [] + + def handle_data(self, data): + self.text_parts.append(data) + + def extract_visible_text(self, html: str) -> str: + self.reset() + self.text_parts = [] + self.feed(html) + return "".join(self.text_parts).strip() + + +def slugify(text: str) -> str: + return py_slugify( + text, + replacements=[ + ("`", ""), # `dict`s -> dicts + ("'s", "s"), # it's -> its + ("'t", "t"), # don't -> dont + ("**", ""), # **FastAPI**s -> FastAPIs + ], + ) def get_en_config() -> Dict[str, Any]: @@ -426,5 +460,74 @@ def generate_docs_src_versions_for_file(file_path: Path) -> None: version_file.write_text(content_format, encoding="utf-8") +@app.command() +def add_permalinks_page(path: Path, update_existing: bool = False): + """ + Add or update header permalinks in specific page of En docs. + """ + + if not path.is_relative_to(en_docs_path / "docs"): + raise RuntimeError(f"Path must be inside {en_docs_path}") + rel_path = path.relative_to(en_docs_path / "docs") + + # Skip excluded sections + if str(rel_path).startswith(non_translated_sections): + return + + visible_text_extractor = VisibleTextExtractor() + updated_lines = [] + in_code_block3 = False + in_code_block4 = False + permalinks = set() + + with path.open("r", encoding="utf-8") as f: + lines = f.readlines() + + for line in lines: + # Handle codeblocks start and end + if not (in_code_block3 or in_code_block4): + if code_block4_pattern.match(line): + in_code_block4 = True + elif code_block3_pattern.match(line): + in_code_block3 = True + else: + if in_code_block4 and code_block4_pattern.match(line): + in_code_block4 = False + elif in_code_block3 and code_block3_pattern.match(line): + in_code_block3 = False + + # Process Headers only outside codeblocks + if not (in_code_block3 or in_code_block4): + match = header_pattern.match(line) + if match: + hashes, title, _permalink = match.groups() + if (not _permalink) or update_existing: + slug = slugify(visible_text_extractor.extract_visible_text(title)) + if slug in permalinks: + # If the slug is already used, append a number to make it unique + count = 1 + original_slug = slug + while slug in permalinks: + slug = f"{original_slug}_{count}" + count += 1 + permalinks.add(slug) + + line = f"{hashes} {title} {{ #{slug} }}\n" + + updated_lines.append(line) + + with path.open("w", encoding="utf-8") as f: + f.writelines(updated_lines) + + +@app.command() +def add_permalinks(update_existing: bool = False) -> None: + """ + Add or update header permalinks in all pages of En docs. + """ + for md_file in en_docs_path.rglob("*.md"): + add_permalinks_page(md_file, update_existing=update_existing) + + if __name__ == "__main__": app() From 2909f8a6281611bcf9b0f127a147893bcd5a0716 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 21 Nov 2025 12:49:34 +0000 Subject: [PATCH 15/47] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index bf4120f36..4a01df4b8 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Internal +* πŸ› οΈ Add `add-permalinks` and `add-permalinks-page` to `scripts/docs.py`. PR [#14033](https://github.com/fastapi/fastapi/pull/14033) by [@YuriiMotov](https://github.com/YuriiMotov). * πŸ”§ Upgrade Material for MkDocs and remove insiders. PR [#14375](https://github.com/fastapi/fastapi/pull/14375) by [@tiangolo](https://github.com/tiangolo). ## 0.121.3 From cbe5bdb85f393712e5176cfa27275781ffcc07c0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 21 Nov 2025 14:03:21 +0100 Subject: [PATCH 16/47] =?UTF-8?q?=E2=AC=86=20Bump=20actions/checkout=20fro?= =?UTF-8?q?m=205=20to=206=20(#14381)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build-docs.yml | 6 +++--- .github/workflows/contributors.yml | 2 +- .github/workflows/deploy-docs.yml | 2 +- .github/workflows/label-approved.yml | 2 +- .github/workflows/latest-changes.yml | 2 +- .github/workflows/notify-translations.yml | 2 +- .github/workflows/people.yml | 2 +- .github/workflows/publish.yml | 2 +- .github/workflows/smokeshow.yml | 2 +- .github/workflows/sponsors.yml | 2 +- .github/workflows/test-redistribute.yml | 2 +- .github/workflows/test.yml | 6 +++--- .github/workflows/topic-repos.yml | 2 +- .github/workflows/translate.yml | 2 +- 14 files changed, 18 insertions(+), 18 deletions(-) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 995a0eb41..73e1c6b67 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -21,7 +21,7 @@ jobs: outputs: docs: ${{ steps.filter.outputs.docs }} steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 # For pull requests it's not necessary to checkout the code but for the main branch it is - uses: dorny/paths-filter@v3 id: filter @@ -45,7 +45,7 @@ jobs: outputs: langs: ${{ steps.show-langs.outputs.langs }} steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: @@ -81,7 +81,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: diff --git a/.github/workflows/contributors.yml b/.github/workflows/contributors.yml index 7d5449c6a..2abd2fdcf 100644 --- a/.github/workflows/contributors.yml +++ b/.github/workflows/contributors.yml @@ -24,7 +24,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index aa4fd6b65..50662a190 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -23,7 +23,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: diff --git a/.github/workflows/label-approved.yml b/.github/workflows/label-approved.yml index e6ae3d963..7f16254db 100644 --- a/.github/workflows/label-approved.yml +++ b/.github/workflows/label-approved.yml @@ -20,7 +20,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: diff --git a/.github/workflows/latest-changes.yml b/.github/workflows/latest-changes.yml index 2fa832fab..3bff707c0 100644 --- a/.github/workflows/latest-changes.yml +++ b/.github/workflows/latest-changes.yml @@ -24,7 +24,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: # To allow latest-changes to commit to the main branch token: ${{ secrets.FASTAPI_LATEST_CHANGES }} diff --git a/.github/workflows/notify-translations.yml b/.github/workflows/notify-translations.yml index 04beeb64e..971e6bbd8 100644 --- a/.github/workflows/notify-translations.yml +++ b/.github/workflows/notify-translations.yml @@ -28,7 +28,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: diff --git a/.github/workflows/people.yml b/.github/workflows/people.yml index f15b92137..9b35a3d7e 100644 --- a/.github/workflows/people.yml +++ b/.github/workflows/people.yml @@ -24,7 +24,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 441eb4560..6d9a00b49 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -20,7 +20,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: diff --git a/.github/workflows/smokeshow.yml b/.github/workflows/smokeshow.yml index eed5fbec0..84c743019 100644 --- a/.github/workflows/smokeshow.yml +++ b/.github/workflows/smokeshow.yml @@ -21,7 +21,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: actions/setup-python@v6 with: python-version: '3.9' diff --git a/.github/workflows/sponsors.yml b/.github/workflows/sponsors.yml index 7d29469a5..8b0249001 100644 --- a/.github/workflows/sponsors.yml +++ b/.github/workflows/sponsors.yml @@ -24,7 +24,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: diff --git a/.github/workflows/test-redistribute.yml b/.github/workflows/test-redistribute.yml index a44f0b681..653ab2a74 100644 --- a/.github/workflows/test-redistribute.yml +++ b/.github/workflows/test-redistribute.yml @@ -22,7 +22,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9c3e2218b..8157e364b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -23,7 +23,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: @@ -65,7 +65,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: @@ -111,7 +111,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: actions/setup-python@v6 with: python-version: '3.8' diff --git a/.github/workflows/topic-repos.yml b/.github/workflows/topic-repos.yml index 22b37d59d..41dabee1e 100644 --- a/.github/workflows/topic-repos.yml +++ b/.github/workflows/topic-repos.yml @@ -19,7 +19,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: diff --git a/.github/workflows/translate.yml b/.github/workflows/translate.yml index a7fcf84df..6506b8e28 100644 --- a/.github/workflows/translate.yml +++ b/.github/workflows/translate.yml @@ -42,7 +42,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: From ae951f698177b7fc00a61768376c78a92d8b3995 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 23 Nov 2025 01:27:40 -0800 Subject: [PATCH 17/47] =?UTF-8?q?=F0=9F=92=84=20Use=20font=20Fira=20Code?= =?UTF-8?q?=20to=20fix=20display=20of=20Rich=20panels=20in=20docs=20in=20W?= =?UTF-8?q?indows=20(#14387)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/css/custom.css | 8 ++++++++ docs/en/docs/css/termynal.css | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/css/custom.css b/docs/en/docs/css/custom.css index a38df772f..8849d8741 100644 --- a/docs/en/docs/css/custom.css +++ b/docs/en/docs/css/custom.css @@ -1,3 +1,11 @@ +/* Fira Code, including characters used by Rich output, like the "heavy right-pointing angle bracket ornament", not included in Google Fonts */ +@import url(https://cdn.jsdelivr.net/npm/firacode@6.2.0/distr/fira_code.css); + +/* Override default code font in Material for MkDocs to Fira Code */ +:root { + --md-code-font: "Fira Code", monospace; +} + .termynal-comment { color: #4a968f; font-style: italic; diff --git a/docs/en/docs/css/termynal.css b/docs/en/docs/css/termynal.css index 8534f9102..a2564e286 100644 --- a/docs/en/docs/css/termynal.css +++ b/docs/en/docs/css/termynal.css @@ -20,7 +20,7 @@ /* font-size: 18px; */ font-size: 15px; /* font-family: 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier, monospace; */ - font-family: 'Roboto Mono', 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier, monospace; + font-family: var(--md-code-font-family), 'Roboto Mono', 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier, monospace; border-radius: 4px; padding: 75px 45px 35px; position: relative; From 79bc4b9ca0d039921310e13f726f8a17c0469d8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 23 Nov 2025 08:36:34 -0800 Subject: [PATCH 18/47] =?UTF-8?q?=F0=9F=91=B7=20Add=20custom=20pre-commit?= =?UTF-8?q?=20CI=20(#14397)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/pre-commit.yml | 85 ++++++++++++++++++++++++++++++++ .pre-commit-config.yaml | 5 -- 2 files changed, 85 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/pre-commit.yml diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 000000000..838764057 --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,85 @@ +name: pre-commit + +on: + pull_request: + types: + - opened + - synchronize + +env: + UV_SYSTEM_PYTHON: 1 + IS_FORK: ${{ github.event.pull_request.head.repo.full_name != github.repository }} + +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" + - uses: actions/checkout@v5 + name: Checkout PR for own repo + if: env.IS_FORK == 'false' + with: + # To be able to commit it needs more than the last commit + ref: ${{ github.head_ref }} + # A token other than the default GITHUB_TOKEN is needed to be able to trigger CI + token: ${{ secrets.PRE_COMMIT }} + # pre-commit lite ci needs the default checkout configs to work + - uses: actions/checkout@v5 + name: Checkout PR for fork + if: env.IS_FORK == 'true' + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.14" + - name: Setup uv + uses: astral-sh/setup-uv@v7 + with: + cache-dependency-glob: | + requirements**.txt + pyproject.toml + uv.lock + - name: Run pre-commit + id: precommit + run: | + # Fetch the base branch for comparison + git fetch origin ${{ github.base_ref }} + uvx pre-commit run --from-ref origin/${{ github.base_ref }} --to-ref HEAD --show-diff-on-failure + continue-on-error: true + - name: Commit and push changes + if: env.IS_FORK == 'false' + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add -A + if git diff --staged --quiet; then + echo "No changes to commit" + else + git commit -m "🎨 Auto format" + git push + fi + - uses: pre-commit-ci/lite-action@v1.1.0 + if: env.IS_FORK == 'true' + with: + msg: 🎨 Auto format + - name: Error out on pre-commit errors + if: steps.precommit.outcome == 'failure' + run: exit 1 + + # https://github.com/marketplace/actions/alls-green#why + alls-green: # This job does nothing and is only used for the branch protection + if: always() + needs: + - pre-commit + runs-on: ubuntu-latest + steps: + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" + - name: Decide whether the needed jobs succeeded or failed + uses: re-actors/alls-green@release/v1 + with: + jobs: ${{ toJSON(needs) }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8e5eba4c4..cc3d93779 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,5 @@ # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks -default_language_version: - python: python3.10 repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 @@ -20,6 +18,3 @@ repos: args: - --fix - id: ruff-format -ci: - autofix_commit_msg: 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks - autoupdate_commit_msg: ⬆ [pre-commit.ci] pre-commit autoupdate From 4f3ff797361a7a1d953353d90fd809d54469267f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 23 Nov 2025 09:41:43 -0800 Subject: [PATCH 19/47] =?UTF-8?q?=F0=9F=91=B7=20Add=20pre-commit=20config?= =?UTF-8?q?=20with=20local=20script=20for=20permalinks=20(#14398)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions[bot] --- .github/workflows/pre-commit.yml | 7 +++++-- .gitignore | 3 +++ .pre-commit-config.yaml | 29 +++++++++++++++++++---------- requirements.txt | 2 +- scripts/docs.py | 9 +++++++++ 5 files changed, 37 insertions(+), 13 deletions(-) diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 838764057..fa0574d7d 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -7,7 +7,6 @@ on: - synchronize env: - UV_SYSTEM_PYTHON: 1 IS_FORK: ${{ github.event.pull_request.head.repo.full_name != github.repository }} jobs: @@ -41,6 +40,10 @@ jobs: requirements**.txt pyproject.toml uv.lock + - name: Install Dependencies + run: | + uv venv + uv pip install -r requirements.txt - name: Run pre-commit id: precommit run: | @@ -69,7 +72,7 @@ jobs: run: exit 1 # https://github.com/marketplace/actions/alls-green#why - alls-green: # This job does nothing and is only used for the branch protection + pre-commit-alls-green: # This job does nothing and is only used for the branch protection if: always() needs: - pre-commit diff --git a/.gitignore b/.gitignore index ef6364a9a..6016ffa59 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,6 @@ archive.zip # macOS .DS_Store + +# Ignore while the setup still depends on requirements.txt files +uv.lock diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cc3d93779..8e6d93fb7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,20 +1,29 @@ # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks repos: -- repo: https://github.com/pre-commit/pre-commit-hooks + - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 hooks: - - id: check-added-large-files - - id: check-toml - - id: check-yaml + - id: check-added-large-files + - id: check-toml + - id: check-yaml args: - - --unsafe - - id: end-of-file-fixer - - id: trailing-whitespace -- repo: https://github.com/astral-sh/ruff-pre-commit + - --unsafe + - id: end-of-file-fixer + - id: trailing-whitespace + - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.14.3 hooks: - - id: ruff + - id: ruff args: - --fix - - id: ruff-format + - id: ruff-format + - repo: local + hooks: + - id: local-script + language: unsupported + name: local script + entry: uv run ./scripts/docs.py add-permalinks-pages + args: + - --update-existing + files: ^docs/en/docs/.*\.md$ diff --git a/requirements.txt b/requirements.txt index 9180bf1be..5d9f97b75 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ -e .[all] -r requirements-tests.txt -r requirements-docs.txt -pre-commit >=2.17.0,<5.0.0 +pre-commit >=4.5.0,<5.0.0 # For generating screenshots playwright diff --git a/scripts/docs.py b/scripts/docs.py index d67ab50f7..73f60e68c 100644 --- a/scripts/docs.py +++ b/scripts/docs.py @@ -520,6 +520,15 @@ def add_permalinks_page(path: Path, update_existing: bool = False): f.writelines(updated_lines) +@app.command() +def add_permalinks_pages(pages: List[Path], update_existing: bool = False) -> None: + """ + Add or update header permalinks in specific pages of En docs. + """ + for md_file in pages: + add_permalinks_page(md_file, update_existing=update_existing) + + @app.command() def add_permalinks(update_existing: bool = False) -> None: """ From 5265c4f5cb4d8fb4fb5015bccf1a6de8f3fc6044 Mon Sep 17 00:00:00 2001 From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Sun, 23 Nov 2025 21:10:04 +0100 Subject: [PATCH 20/47] =?UTF-8?q?=F0=9F=94=A7=20Configure=20labeler=20to?= =?UTF-8?q?=20exclude=20files=20that=20start=20from=20underscore=20for=20`?= =?UTF-8?q?lang-all`=20label=20(#14213)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/labeler.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/labeler.yml b/.github/labeler.yml index c5b1f84f3..cdaefbf2d 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -17,6 +17,7 @@ lang-all: - docs/*/docs/** - all-globs-to-all-files: - '!docs/en/docs/**' + - '!docs/*/**/_*.md' - '!fastapi/**' - '!pyproject.toml' From ab33b457182976c244d43fd0af838e72e6feee72 Mon Sep 17 00:00:00 2001 From: Sofie Van Landeghem Date: Mon, 24 Nov 2025 15:58:32 +0100 Subject: [PATCH 21/47] =?UTF-8?q?=F0=9F=91=B7=20Upgrade=20`latest-changes`?= =?UTF-8?q?=20GitHub=20Action=20and=20pin=20`actions/checkout@v5`=20(#1440?= =?UTF-8?q?3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit πŸ‘· Upgrade latest-changes and pin actions/checkout@v5 --- .github/workflows/latest-changes.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/latest-changes.yml b/.github/workflows/latest-changes.yml index 3bff707c0..b9e45ea62 100644 --- a/.github/workflows/latest-changes.yml +++ b/.github/workflows/latest-changes.yml @@ -24,7 +24,9 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v6 + # pin to actions/checkout@v5 for compatibility with latest-changes + # Ref: https://github.com/actions/checkout/issues/2313 + - uses: actions/checkout@v5 with: # To allow latest-changes to commit to the main branch token: ${{ secrets.FASTAPI_LATEST_CHANGES }} @@ -34,7 +36,7 @@ jobs: if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled == 'true' }} with: limit-access-to-actor: true - - uses: tiangolo/latest-changes@0.4.0 + - uses: tiangolo/latest-changes@0.4.1 with: token: ${{ secrets.GITHUB_TOKEN }} latest_changes_file: docs/en/docs/release-notes.md From c7d05a903ce34e8578237cf2aab7242cefe51af2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 24 Nov 2025 14:58:56 +0000 Subject: [PATCH 22/47] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 4a01df4b8..c38766dc1 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Internal +* πŸ‘· Upgrade `latest-changes` GitHub Action and pin `actions/checkout@v5`. PR [#14403](https://github.com/fastapi/fastapi/pull/14403) by [@svlandeg](https://github.com/svlandeg). * πŸ› οΈ Add `add-permalinks` and `add-permalinks-page` to `scripts/docs.py`. PR [#14033](https://github.com/fastapi/fastapi/pull/14033) by [@YuriiMotov](https://github.com/YuriiMotov). * πŸ”§ Upgrade Material for MkDocs and remove insiders. PR [#14375](https://github.com/fastapi/fastapi/pull/14375) by [@tiangolo](https://github.com/tiangolo). From a2395e02436a3788400d864696120fcd91af38cc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 24 Nov 2025 14:59:55 +0000 Subject: [PATCH 23/47] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index c38766dc1..9fd9817ed 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Internal +* ⬆ Bump actions/checkout from 5 to 6. PR [#14381](https://github.com/fastapi/fastapi/pull/14381) by [@dependabot[bot]](https://github.com/apps/dependabot). * πŸ‘· Upgrade `latest-changes` GitHub Action and pin `actions/checkout@v5`. PR [#14403](https://github.com/fastapi/fastapi/pull/14403) by [@svlandeg](https://github.com/svlandeg). * πŸ› οΈ Add `add-permalinks` and `add-permalinks-page` to `scripts/docs.py`. PR [#14033](https://github.com/fastapi/fastapi/pull/14033) by [@YuriiMotov](https://github.com/YuriiMotov). * πŸ”§ Upgrade Material for MkDocs and remove insiders. PR [#14375](https://github.com/fastapi/fastapi/pull/14375) by [@tiangolo](https://github.com/tiangolo). From 8b18522205b9ac738b241c4143c983e968fe6e15 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 24 Nov 2025 15:00:12 +0000 Subject: [PATCH 24/47] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 9fd9817ed..892eeb8f4 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Internal +* πŸ‘· Add custom pre-commit CI. PR [#14397](https://github.com/fastapi/fastapi/pull/14397) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump actions/checkout from 5 to 6. PR [#14381](https://github.com/fastapi/fastapi/pull/14381) by [@dependabot[bot]](https://github.com/apps/dependabot). * πŸ‘· Upgrade `latest-changes` GitHub Action and pin `actions/checkout@v5`. PR [#14403](https://github.com/fastapi/fastapi/pull/14403) by [@svlandeg](https://github.com/svlandeg). * πŸ› οΈ Add `add-permalinks` and `add-permalinks-page` to `scripts/docs.py`. PR [#14033](https://github.com/fastapi/fastapi/pull/14033) by [@YuriiMotov](https://github.com/YuriiMotov). From ecfb752487bc3abef35b2786297bc575005c9e36 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 24 Nov 2025 15:00:13 +0000 Subject: [PATCH 25/47] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 892eeb8f4..fbb108994 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Internal +* πŸ’„ Use font Fira Code to fix display of Rich panels in docs in Windows. PR [#14387](https://github.com/fastapi/fastapi/pull/14387) by [@tiangolo](https://github.com/tiangolo). * πŸ‘· Add custom pre-commit CI. PR [#14397](https://github.com/fastapi/fastapi/pull/14397) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump actions/checkout from 5 to 6. PR [#14381](https://github.com/fastapi/fastapi/pull/14381) by [@dependabot[bot]](https://github.com/apps/dependabot). * πŸ‘· Upgrade `latest-changes` GitHub Action and pin `actions/checkout@v5`. PR [#14403](https://github.com/fastapi/fastapi/pull/14403) by [@svlandeg](https://github.com/svlandeg). From cc66dee55c9a0f34c2e277c0509c45c74abcefd1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 24 Nov 2025 15:00:29 +0000 Subject: [PATCH 26/47] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index fbb108994..d8495d571 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Internal +* πŸ‘· Add pre-commit config with local script for permalinks. PR [#14398](https://github.com/fastapi/fastapi/pull/14398) by [@tiangolo](https://github.com/tiangolo). * πŸ’„ Use font Fira Code to fix display of Rich panels in docs in Windows. PR [#14387](https://github.com/fastapi/fastapi/pull/14387) by [@tiangolo](https://github.com/tiangolo). * πŸ‘· Add custom pre-commit CI. PR [#14397](https://github.com/fastapi/fastapi/pull/14397) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump actions/checkout from 5 to 6. PR [#14381](https://github.com/fastapi/fastapi/pull/14381) by [@dependabot[bot]](https://github.com/apps/dependabot). From e2354a0a063f2fcb890ec568f1a98e136a39fd25 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 24 Nov 2025 15:00:36 +0000 Subject: [PATCH 27/47] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index d8495d571..ef1b98b67 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Internal +* πŸ”§ Configure labeler to exclude files that start from underscore for `lang-all` label. PR [#14213](https://github.com/fastapi/fastapi/pull/14213) by [@YuriiMotov](https://github.com/YuriiMotov). * πŸ‘· Add pre-commit config with local script for permalinks. PR [#14398](https://github.com/fastapi/fastapi/pull/14398) by [@tiangolo](https://github.com/tiangolo). * πŸ’„ Use font Fira Code to fix display of Rich panels in docs in Windows. PR [#14387](https://github.com/fastapi/fastapi/pull/14387) by [@tiangolo](https://github.com/tiangolo). * πŸ‘· Add custom pre-commit CI. PR [#14397](https://github.com/fastapi/fastapi/pull/14397) by [@tiangolo](https://github.com/tiangolo). From 51ad909ffe9f5b2d5c9315554e75e39a8a2d725c Mon Sep 17 00:00:00 2001 From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Mon, 24 Nov 2025 20:03:06 +0100 Subject: [PATCH 28/47] =?UTF-8?q?=F0=9F=90=9B=20Use=20`401`=20status=20cod?= =?UTF-8?q?e=20in=20security=20classes=20when=20credentials=20are=20missin?= =?UTF-8?q?g=20(#13786)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: SebastiΓ‘n RamΓ­rez --- .../authentication-error-status-code.md | 17 +++++ docs/en/mkdocs.yml | 1 + .../tutorial001_an.py | 20 +++++ .../tutorial001_an_py39.py | 21 +++++ docs_src/security/tutorial003.py | 2 +- docs_src/security/tutorial003_an.py | 2 +- docs_src/security/tutorial003_an_py310.py | 2 +- docs_src/security/tutorial003_an_py39.py | 2 +- docs_src/security/tutorial003_py310.py | 2 +- fastapi/security/api_key.py | 76 +++++++++++++------ fastapi/security/http.py | 74 +++++++++--------- fastapi/security/oauth2.py | 40 ++++++---- fastapi/security/open_id_connect_url.py | 18 ++++- tests/test_security_api_key_cookie.py | 3 +- ...est_security_api_key_cookie_description.py | 3 +- tests/test_security_api_key_header.py | 3 +- ...est_security_api_key_header_description.py | 3 +- tests/test_security_api_key_query.py | 3 +- ...test_security_api_key_query_description.py | 3 +- tests/test_security_http_base.py | 3 +- tests/test_security_http_base_description.py | 3 +- tests/test_security_http_basic_optional.py | 4 +- tests/test_security_http_basic_realm.py | 4 +- ...t_security_http_basic_realm_description.py | 4 +- tests/test_security_http_bearer.py | 8 +- .../test_security_http_bearer_description.py | 8 +- tests/test_security_http_digest.py | 8 +- .../test_security_http_digest_description.py | 8 +- tests/test_security_oauth2.py | 3 +- tests/test_security_openid_connect.py | 3 +- ...est_security_openid_connect_description.py | 3 +- ...st_top_level_security_scheme_in_openapi.py | 2 +- .../__init__.py | 0 .../test_tutorial001.py | 69 +++++++++++++++++ .../test_security/test_tutorial003.py | 2 +- .../test_security/test_tutorial006.py | 4 +- 36 files changed, 315 insertions(+), 116 deletions(-) create mode 100644 docs/en/docs/how-to/authentication-error-status-code.md create mode 100644 docs_src/authentication_error_status_code/tutorial001_an.py create mode 100644 docs_src/authentication_error_status_code/tutorial001_an_py39.py create mode 100644 tests/test_tutorial/test_authentication_error_status_code/__init__.py create mode 100644 tests/test_tutorial/test_authentication_error_status_code/test_tutorial001.py diff --git a/docs/en/docs/how-to/authentication-error-status-code.md b/docs/en/docs/how-to/authentication-error-status-code.md new file mode 100644 index 000000000..f9433e5dd --- /dev/null +++ b/docs/en/docs/how-to/authentication-error-status-code.md @@ -0,0 +1,17 @@ +# Use Old 403 Authentication Error Status Codes { #use-old-403-authentication-error-status-codes } + +Before FastAPI version `0.122.0`, when the integrated security utilities returned an error to the client after a failed authentication, they used the HTTP status code `403 Forbidden`. + +Starting with FastAPI version `0.122.0`, they use the more appropriate HTTP status code `401 Unauthorized`, and return a sensible `WWW-Authenticate` header in the response, following the HTTP specifications, RFC 7235, RFC 9110. + +But if for some reason your clients depend on the old behavior, you can revert to it by overriding the method `make_not_authenticated_error` in your security classes. + +For example, you can create a subclass of `HTTPBearer` that returns a `403 Forbidden` error instead of the default `401 Unauthorized` error: + +{* ../../docs_src/authentication_error_status_code/tutorial001_an_py39.py hl[9:13] *} + +/// tip + +Notice that the function returns the exception instance, it doesn't raise it. The raising is done in the rest of the internal code. + +/// diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index 8be832f11..fd346a3d3 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -215,6 +215,7 @@ nav: - how-to/custom-docs-ui-assets.md - how-to/configure-swagger-ui.md - how-to/testing-database.md + - how-to/authentication-error-status-code.md - Reference (Code API): - reference/index.md - reference/fastapi.md diff --git a/docs_src/authentication_error_status_code/tutorial001_an.py b/docs_src/authentication_error_status_code/tutorial001_an.py new file mode 100644 index 000000000..40678e858 --- /dev/null +++ b/docs_src/authentication_error_status_code/tutorial001_an.py @@ -0,0 +1,20 @@ +from fastapi import Depends, FastAPI, HTTPException, status +from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer +from typing_extensions import Annotated + +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/authentication_error_status_code/tutorial001_an_py39.py b/docs_src/authentication_error_status_code/tutorial001_an_py39.py new file mode 100644 index 000000000..7bbc2f717 --- /dev/null +++ b/docs_src/authentication_error_status_code/tutorial001_an_py39.py @@ -0,0 +1,21 @@ +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/security/tutorial003.py b/docs_src/security/tutorial003.py index 4b324866f..ce7a71b68 100644 --- a/docs_src/security/tutorial003.py +++ b/docs_src/security/tutorial003.py @@ -60,7 +60,7 @@ async def get_current_user(token: str = Depends(oauth2_scheme)): if not user: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, - detail="Invalid authentication credentials", + detail="Not authenticated", headers={"WWW-Authenticate": "Bearer"}, ) return user diff --git a/docs_src/security/tutorial003_an.py b/docs_src/security/tutorial003_an.py index 8fb40dd4a..1b7056a20 100644 --- a/docs_src/security/tutorial003_an.py +++ b/docs_src/security/tutorial003_an.py @@ -61,7 +61,7 @@ async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]): if not user: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, - detail="Invalid authentication credentials", + detail="Not authenticated", headers={"WWW-Authenticate": "Bearer"}, ) return user diff --git a/docs_src/security/tutorial003_an_py310.py b/docs_src/security/tutorial003_an_py310.py index ced4a2fbc..4a2743f6f 100644 --- a/docs_src/security/tutorial003_an_py310.py +++ b/docs_src/security/tutorial003_an_py310.py @@ -60,7 +60,7 @@ async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]): if not user: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, - detail="Invalid authentication credentials", + detail="Not authenticated", headers={"WWW-Authenticate": "Bearer"}, ) return user diff --git a/docs_src/security/tutorial003_an_py39.py b/docs_src/security/tutorial003_an_py39.py index 068a3933e..b396210c8 100644 --- a/docs_src/security/tutorial003_an_py39.py +++ b/docs_src/security/tutorial003_an_py39.py @@ -60,7 +60,7 @@ async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]): if not user: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, - detail="Invalid authentication credentials", + detail="Not authenticated", headers={"WWW-Authenticate": "Bearer"}, ) return user diff --git a/docs_src/security/tutorial003_py310.py b/docs_src/security/tutorial003_py310.py index af935e997..081259b31 100644 --- a/docs_src/security/tutorial003_py310.py +++ b/docs_src/security/tutorial003_py310.py @@ -58,7 +58,7 @@ async def get_current_user(token: str = Depends(oauth2_scheme)): if not user: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, - detail="Invalid authentication credentials", + detail="Not authenticated", headers={"WWW-Authenticate": "Bearer"}, ) return user diff --git a/fastapi/security/api_key.py b/fastapi/security/api_key.py index 496c815a7..81c7be10d 100644 --- a/fastapi/security/api_key.py +++ b/fastapi/security/api_key.py @@ -1,22 +1,52 @@ -from typing import Optional +from typing import Optional, Union from annotated_doc import Doc from fastapi.openapi.models import APIKey, APIKeyIn from fastapi.security.base import SecurityBase from starlette.exceptions import HTTPException from starlette.requests import Request -from starlette.status import HTTP_403_FORBIDDEN +from starlette.status import HTTP_401_UNAUTHORIZED from typing_extensions import Annotated class APIKeyBase(SecurityBase): - @staticmethod - def check_api_key(api_key: Optional[str], auto_error: bool) -> Optional[str]: + def __init__( + self, + location: APIKeyIn, + name: str, + description: Union[str, None], + scheme_name: Union[str, None], + auto_error: bool, + ): + self.auto_error = auto_error + + self.model: APIKey = APIKey( + **{"in": location}, + name=name, + description=description, + ) + self.scheme_name = scheme_name or self.__class__.__name__ + + def make_not_authenticated_error(self) -> HTTPException: + """ + The WWW-Authenticate header is not standardized for API Key authentication but + the HTTP specification requires that an error of 401 "Unauthorized" must + include a WWW-Authenticate header. + + Ref: https://datatracker.ietf.org/doc/html/rfc9110#name-401-unauthorized + + For this, this method sends a custom challenge `APIKey`. + """ + return HTTPException( + status_code=HTTP_401_UNAUTHORIZED, + detail="Not authenticated", + headers={"WWW-Authenticate": "APIKey"}, + ) + + def check_api_key(self, api_key: Optional[str]) -> Optional[str]: if not api_key: - if auto_error: - raise HTTPException( - status_code=HTTP_403_FORBIDDEN, detail="Not authenticated" - ) + if self.auto_error: + raise self.make_not_authenticated_error() return None return api_key @@ -100,17 +130,17 @@ class APIKeyQuery(APIKeyBase): ), ] = True, ): - self.model: APIKey = APIKey( - **{"in": APIKeyIn.query}, + super().__init__( + location=APIKeyIn.query, name=name, + scheme_name=scheme_name, description=description, + auto_error=auto_error, ) - self.scheme_name = scheme_name or self.__class__.__name__ - self.auto_error = auto_error async def __call__(self, request: Request) -> Optional[str]: api_key = request.query_params.get(self.model.name) - return self.check_api_key(api_key, self.auto_error) + return self.check_api_key(api_key) class APIKeyHeader(APIKeyBase): @@ -188,17 +218,17 @@ class APIKeyHeader(APIKeyBase): ), ] = True, ): - self.model: APIKey = APIKey( - **{"in": APIKeyIn.header}, + super().__init__( + location=APIKeyIn.header, name=name, + scheme_name=scheme_name, description=description, + auto_error=auto_error, ) - self.scheme_name = scheme_name or self.__class__.__name__ - self.auto_error = auto_error async def __call__(self, request: Request) -> Optional[str]: api_key = request.headers.get(self.model.name) - return self.check_api_key(api_key, self.auto_error) + return self.check_api_key(api_key) class APIKeyCookie(APIKeyBase): @@ -276,14 +306,14 @@ class APIKeyCookie(APIKeyBase): ), ] = True, ): - self.model: APIKey = APIKey( - **{"in": APIKeyIn.cookie}, + super().__init__( + location=APIKeyIn.cookie, name=name, + scheme_name=scheme_name, description=description, + auto_error=auto_error, ) - self.scheme_name = scheme_name or self.__class__.__name__ - self.auto_error = auto_error async def __call__(self, request: Request) -> Optional[str]: api_key = request.cookies.get(self.model.name) - return self.check_api_key(api_key, self.auto_error) + return self.check_api_key(api_key) diff --git a/fastapi/security/http.py b/fastapi/security/http.py index 3a5985650..0d1bbba3a 100644 --- a/fastapi/security/http.py +++ b/fastapi/security/http.py @@ -1,6 +1,6 @@ import binascii from base64 import b64decode -from typing import Optional +from typing import Dict, Optional from annotated_doc import Doc from fastapi.exceptions import HTTPException @@ -10,7 +10,7 @@ from fastapi.security.base import SecurityBase from fastapi.security.utils import get_authorization_scheme_param from pydantic import BaseModel from starlette.requests import Request -from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN +from starlette.status import HTTP_401_UNAUTHORIZED from typing_extensions import Annotated @@ -76,10 +76,22 @@ class HTTPBase(SecurityBase): description: Optional[str] = None, auto_error: bool = True, ): - self.model = HTTPBaseModel(scheme=scheme, description=description) + self.model: HTTPBaseModel = HTTPBaseModel( + scheme=scheme, description=description + ) self.scheme_name = scheme_name or self.__class__.__name__ self.auto_error = auto_error + def make_authenticate_headers(self) -> Dict[str, str]: + return {"WWW-Authenticate": f"{self.model.scheme.title()}"} + + def make_not_authenticated_error(self) -> HTTPException: + return HTTPException( + status_code=HTTP_401_UNAUTHORIZED, + detail="Not authenticated", + headers=self.make_authenticate_headers(), + ) + async def __call__( self, request: Request ) -> Optional[HTTPAuthorizationCredentials]: @@ -87,9 +99,7 @@ class HTTPBase(SecurityBase): scheme, credentials = get_authorization_scheme_param(authorization) if not (authorization and scheme and credentials): if self.auto_error: - raise HTTPException( - status_code=HTTP_403_FORBIDDEN, detail="Not authenticated" - ) + raise self.make_not_authenticated_error() else: return None return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials) @@ -99,6 +109,8 @@ class HTTPBasic(HTTPBase): """ HTTP Basic authentication. + Ref: https://datatracker.ietf.org/doc/html/rfc7617 + ## Usage Create an instance object and use that object as the dependency in `Depends()`. @@ -185,36 +197,28 @@ class HTTPBasic(HTTPBase): self.realm = realm self.auto_error = auto_error + def make_authenticate_headers(self) -> Dict[str, str]: + if self.realm: + return {"WWW-Authenticate": f'Basic realm="{self.realm}"'} + return {"WWW-Authenticate": "Basic"} + async def __call__( # type: ignore self, request: Request ) -> Optional[HTTPBasicCredentials]: authorization = request.headers.get("Authorization") scheme, param = get_authorization_scheme_param(authorization) - if self.realm: - unauthorized_headers = {"WWW-Authenticate": f'Basic realm="{self.realm}"'} - else: - unauthorized_headers = {"WWW-Authenticate": "Basic"} if not authorization or scheme.lower() != "basic": if self.auto_error: - raise HTTPException( - status_code=HTTP_401_UNAUTHORIZED, - detail="Not authenticated", - headers=unauthorized_headers, - ) + raise self.make_not_authenticated_error() else: return None - invalid_user_credentials_exc = HTTPException( - status_code=HTTP_401_UNAUTHORIZED, - detail="Invalid authentication credentials", - headers=unauthorized_headers, - ) try: data = b64decode(param).decode("ascii") - except (ValueError, UnicodeDecodeError, binascii.Error): - raise invalid_user_credentials_exc # noqa: B904 + except (ValueError, UnicodeDecodeError, binascii.Error) as e: + raise self.make_not_authenticated_error() from e username, separator, password = data.partition(":") if not separator: - raise invalid_user_credentials_exc + raise self.make_not_authenticated_error() return HTTPBasicCredentials(username=username, password=password) @@ -306,17 +310,12 @@ class HTTPBearer(HTTPBase): scheme, credentials = get_authorization_scheme_param(authorization) if not (authorization and scheme and credentials): if self.auto_error: - raise HTTPException( - status_code=HTTP_403_FORBIDDEN, detail="Not authenticated" - ) + raise self.make_not_authenticated_error() else: return None if scheme.lower() != "bearer": if self.auto_error: - raise HTTPException( - status_code=HTTP_403_FORBIDDEN, - detail="Invalid authentication credentials", - ) + raise self.make_not_authenticated_error() else: return None return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials) @@ -326,6 +325,12 @@ class HTTPDigest(HTTPBase): """ HTTP Digest authentication. + **Warning**: this is only a stub to connect the components with OpenAPI in FastAPI, + but it doesn't implement the full Digest scheme, you would need to to subclass it + and implement it in your code. + + Ref: https://datatracker.ietf.org/doc/html/rfc7616 + ## Usage Create an instance object and use that object as the dependency in `Depends()`. @@ -408,17 +413,12 @@ class HTTPDigest(HTTPBase): scheme, credentials = get_authorization_scheme_param(authorization) if not (authorization and scheme and credentials): if self.auto_error: - raise HTTPException( - status_code=HTTP_403_FORBIDDEN, detail="Not authenticated" - ) + raise self.make_not_authenticated_error() else: return None if scheme.lower() != "digest": if self.auto_error: - raise HTTPException( - status_code=HTTP_403_FORBIDDEN, - detail="Invalid authentication credentials", - ) + raise self.make_not_authenticated_error() else: return None return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials) diff --git a/fastapi/security/oauth2.py b/fastapi/security/oauth2.py index f8d97d762..b41b0f877 100644 --- a/fastapi/security/oauth2.py +++ b/fastapi/security/oauth2.py @@ -8,7 +8,7 @@ from fastapi.param_functions import Form from fastapi.security.base import SecurityBase from fastapi.security.utils import get_authorization_scheme_param from starlette.requests import Request -from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN +from starlette.status import HTTP_401_UNAUTHORIZED # TODO: import from typing when deprecating Python 3.9 from typing_extensions import Annotated @@ -377,13 +377,33 @@ class OAuth2(SecurityBase): self.scheme_name = scheme_name or self.__class__.__name__ self.auto_error = auto_error + def make_not_authenticated_error(self) -> HTTPException: + """ + The OAuth 2 specification doesn't define the challenge that should be used, + because a `Bearer` token is not really the only option to authenticate. + + But declaring any other authentication challenge would be application-specific + as it's not defined in the specification. + + For practical reasons, this method uses the `Bearer` challenge by default, as + it's probably the most common one. + + If you are implementing an OAuth2 authentication scheme other than the provided + ones in FastAPI (based on bearer tokens), you might want to override this. + + Ref: https://datatracker.ietf.org/doc/html/rfc6749 + """ + return HTTPException( + status_code=HTTP_401_UNAUTHORIZED, + detail="Not authenticated", + headers={"WWW-Authenticate": "Bearer"}, + ) + async def __call__(self, request: Request) -> Optional[str]: authorization = request.headers.get("Authorization") if not authorization: if self.auto_error: - raise HTTPException( - status_code=HTTP_403_FORBIDDEN, detail="Not authenticated" - ) + raise self.make_not_authenticated_error() else: return None return authorization @@ -491,11 +511,7 @@ class OAuth2PasswordBearer(OAuth2): scheme, param = get_authorization_scheme_param(authorization) if not authorization or scheme.lower() != "bearer": if self.auto_error: - raise HTTPException( - status_code=HTTP_401_UNAUTHORIZED, - detail="Not authenticated", - headers={"WWW-Authenticate": "Bearer"}, - ) + raise self.make_not_authenticated_error() else: return None return param @@ -601,11 +617,7 @@ class OAuth2AuthorizationCodeBearer(OAuth2): scheme, param = get_authorization_scheme_param(authorization) if not authorization or scheme.lower() != "bearer": if self.auto_error: - raise HTTPException( - status_code=HTTP_401_UNAUTHORIZED, - detail="Not authenticated", - headers={"WWW-Authenticate": "Bearer"}, - ) + raise self.make_not_authenticated_error() else: return None # pragma: nocover return param diff --git a/fastapi/security/open_id_connect_url.py b/fastapi/security/open_id_connect_url.py index 5e99798e6..e574a56a8 100644 --- a/fastapi/security/open_id_connect_url.py +++ b/fastapi/security/open_id_connect_url.py @@ -5,7 +5,7 @@ from fastapi.openapi.models import OpenIdConnect as OpenIdConnectModel from fastapi.security.base import SecurityBase from starlette.exceptions import HTTPException from starlette.requests import Request -from starlette.status import HTTP_403_FORBIDDEN +from starlette.status import HTTP_401_UNAUTHORIZED from typing_extensions import Annotated @@ -13,6 +13,11 @@ class OpenIdConnect(SecurityBase): """ OpenID Connect authentication class. An instance of it would be used as a dependency. + + **Warning**: this is only a stub to connect the components with OpenAPI in FastAPI, + but it doesn't implement the full OpenIdConnect scheme, for example, it doesn't use + the OpenIDConnect URL. You would need to to subclass it and implement it in your + code. """ def __init__( @@ -73,13 +78,18 @@ class OpenIdConnect(SecurityBase): self.scheme_name = scheme_name or self.__class__.__name__ self.auto_error = auto_error + def make_not_authenticated_error(self) -> HTTPException: + return HTTPException( + status_code=HTTP_401_UNAUTHORIZED, + detail="Not authenticated", + headers={"WWW-Authenticate": "Bearer"}, + ) + async def __call__(self, request: Request) -> Optional[str]: authorization = request.headers.get("Authorization") if not authorization: if self.auto_error: - raise HTTPException( - status_code=HTTP_403_FORBIDDEN, detail="Not authenticated" - ) + raise self.make_not_authenticated_error() else: return None return authorization diff --git a/tests/test_security_api_key_cookie.py b/tests/test_security_api_key_cookie.py index 4ddb8e2ee..9bacfc56e 100644 --- a/tests/test_security_api_key_cookie.py +++ b/tests/test_security_api_key_cookie.py @@ -32,8 +32,9 @@ def test_security_api_key(): def test_security_api_key_no_key(): client = TestClient(app) response = client.get("/users/me") - assert response.status_code == 403, response.text + assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "APIKey" def test_openapi_schema(): diff --git a/tests/test_security_api_key_cookie_description.py b/tests/test_security_api_key_cookie_description.py index d99d616e0..d0cab324e 100644 --- a/tests/test_security_api_key_cookie_description.py +++ b/tests/test_security_api_key_cookie_description.py @@ -32,8 +32,9 @@ def test_security_api_key(): def test_security_api_key_no_key(): client = TestClient(app) response = client.get("/users/me") - assert response.status_code == 403, response.text + assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "APIKey" def test_openapi_schema(): diff --git a/tests/test_security_api_key_header.py b/tests/test_security_api_key_header.py index 1ff883703..3e761b150 100644 --- a/tests/test_security_api_key_header.py +++ b/tests/test_security_api_key_header.py @@ -33,8 +33,9 @@ def test_security_api_key(): def test_security_api_key_no_key(): response = client.get("/users/me") - assert response.status_code == 403, response.text + assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "APIKey" def test_openapi_schema(): diff --git a/tests/test_security_api_key_header_description.py b/tests/test_security_api_key_header_description.py index 27f9d0f29..38a1a8881 100644 --- a/tests/test_security_api_key_header_description.py +++ b/tests/test_security_api_key_header_description.py @@ -33,8 +33,9 @@ def test_security_api_key(): def test_security_api_key_no_key(): response = client.get("/users/me") - assert response.status_code == 403, response.text + assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "APIKey" def test_openapi_schema(): diff --git a/tests/test_security_api_key_query.py b/tests/test_security_api_key_query.py index dc7a0a621..11ed19468 100644 --- a/tests/test_security_api_key_query.py +++ b/tests/test_security_api_key_query.py @@ -33,8 +33,9 @@ def test_security_api_key(): def test_security_api_key_no_key(): response = client.get("/users/me") - assert response.status_code == 403, response.text + assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "APIKey" def test_openapi_schema(): diff --git a/tests/test_security_api_key_query_description.py b/tests/test_security_api_key_query_description.py index 35dc7743a..658798326 100644 --- a/tests/test_security_api_key_query_description.py +++ b/tests/test_security_api_key_query_description.py @@ -33,8 +33,9 @@ def test_security_api_key(): def test_security_api_key_no_key(): response = client.get("/users/me") - assert response.status_code == 403, response.text + assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "APIKey" def test_openapi_schema(): diff --git a/tests/test_security_http_base.py b/tests/test_security_http_base.py index 51928bafd..8cf259a75 100644 --- a/tests/test_security_http_base.py +++ b/tests/test_security_http_base.py @@ -23,8 +23,9 @@ def test_security_http_base(): def test_security_http_base_no_credentials(): response = client.get("/users/me") - assert response.status_code == 403, response.text + assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Other" def test_openapi_schema(): diff --git a/tests/test_security_http_base_description.py b/tests/test_security_http_base_description.py index bc79f3242..791ea59f4 100644 --- a/tests/test_security_http_base_description.py +++ b/tests/test_security_http_base_description.py @@ -23,8 +23,9 @@ def test_security_http_base(): def test_security_http_base_no_credentials(): response = client.get("/users/me") - assert response.status_code == 403, response.text + assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Other" def test_openapi_schema(): diff --git a/tests/test_security_http_basic_optional.py b/tests/test_security_http_basic_optional.py index 9b6cb6c45..7071f381a 100644 --- a/tests/test_security_http_basic_optional.py +++ b/tests/test_security_http_basic_optional.py @@ -38,7 +38,7 @@ def test_security_http_basic_invalid_credentials(): ) assert response.status_code == 401, response.text assert response.headers["WWW-Authenticate"] == "Basic" - assert response.json() == {"detail": "Invalid authentication credentials"} + assert response.json() == {"detail": "Not authenticated"} def test_security_http_basic_non_basic_credentials(): @@ -47,7 +47,7 @@ def test_security_http_basic_non_basic_credentials(): response = client.get("/users/me", headers={"Authorization": auth_header}) assert response.status_code == 401, response.text assert response.headers["WWW-Authenticate"] == "Basic" - assert response.json() == {"detail": "Invalid authentication credentials"} + assert response.json() == {"detail": "Not authenticated"} def test_openapi_schema(): diff --git a/tests/test_security_http_basic_realm.py b/tests/test_security_http_basic_realm.py index 9fc33971a..ec7371f90 100644 --- a/tests/test_security_http_basic_realm.py +++ b/tests/test_security_http_basic_realm.py @@ -36,7 +36,7 @@ def test_security_http_basic_invalid_credentials(): ) assert response.status_code == 401, response.text assert response.headers["WWW-Authenticate"] == 'Basic realm="simple"' - assert response.json() == {"detail": "Invalid authentication credentials"} + assert response.json() == {"detail": "Not authenticated"} def test_security_http_basic_non_basic_credentials(): @@ -45,7 +45,7 @@ def test_security_http_basic_non_basic_credentials(): response = client.get("/users/me", headers={"Authorization": auth_header}) assert response.status_code == 401, response.text assert response.headers["WWW-Authenticate"] == 'Basic realm="simple"' - assert response.json() == {"detail": "Invalid authentication credentials"} + assert response.json() == {"detail": "Not authenticated"} def test_openapi_schema(): diff --git a/tests/test_security_http_basic_realm_description.py b/tests/test_security_http_basic_realm_description.py index 02122442e..a93d5fc86 100644 --- a/tests/test_security_http_basic_realm_description.py +++ b/tests/test_security_http_basic_realm_description.py @@ -36,7 +36,7 @@ def test_security_http_basic_invalid_credentials(): ) assert response.status_code == 401, response.text assert response.headers["WWW-Authenticate"] == 'Basic realm="simple"' - assert response.json() == {"detail": "Invalid authentication credentials"} + assert response.json() == {"detail": "Not authenticated"} def test_security_http_basic_non_basic_credentials(): @@ -45,7 +45,7 @@ def test_security_http_basic_non_basic_credentials(): response = client.get("/users/me", headers={"Authorization": auth_header}) assert response.status_code == 401, response.text assert response.headers["WWW-Authenticate"] == 'Basic realm="simple"' - assert response.json() == {"detail": "Invalid authentication credentials"} + assert response.json() == {"detail": "Not authenticated"} def test_openapi_schema(): diff --git a/tests/test_security_http_bearer.py b/tests/test_security_http_bearer.py index 5b9e2d691..961b42f4d 100644 --- a/tests/test_security_http_bearer.py +++ b/tests/test_security_http_bearer.py @@ -23,14 +23,16 @@ def test_security_http_bearer(): def test_security_http_bearer_no_credentials(): response = client.get("/users/me") - assert response.status_code == 403, response.text + assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" def test_security_http_bearer_incorrect_scheme_credentials(): response = client.get("/users/me", headers={"Authorization": "Basic notreally"}) - assert response.status_code == 403, response.text - assert response.json() == {"detail": "Invalid authentication credentials"} + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" def test_openapi_schema(): diff --git a/tests/test_security_http_bearer_description.py b/tests/test_security_http_bearer_description.py index 2f11c3a14..e16994abc 100644 --- a/tests/test_security_http_bearer_description.py +++ b/tests/test_security_http_bearer_description.py @@ -23,14 +23,16 @@ def test_security_http_bearer(): def test_security_http_bearer_no_credentials(): response = client.get("/users/me") - assert response.status_code == 403, response.text + assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" def test_security_http_bearer_incorrect_scheme_credentials(): response = client.get("/users/me", headers={"Authorization": "Basic notreally"}) - assert response.status_code == 403, response.text - assert response.json() == {"detail": "Invalid authentication credentials"} + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" def test_openapi_schema(): diff --git a/tests/test_security_http_digest.py b/tests/test_security_http_digest.py index 133d35763..3fad4c7a5 100644 --- a/tests/test_security_http_digest.py +++ b/tests/test_security_http_digest.py @@ -23,16 +23,18 @@ def test_security_http_digest(): def test_security_http_digest_no_credentials(): response = client.get("/users/me") - assert response.status_code == 403, response.text + assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Digest" def test_security_http_digest_incorrect_scheme_credentials(): response = client.get( "/users/me", headers={"Authorization": "Other invalidauthorization"} ) - assert response.status_code == 403, response.text - assert response.json() == {"detail": "Invalid authentication credentials"} + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Digest" def test_openapi_schema(): diff --git a/tests/test_security_http_digest_description.py b/tests/test_security_http_digest_description.py index 4e31a0c00..319416a07 100644 --- a/tests/test_security_http_digest_description.py +++ b/tests/test_security_http_digest_description.py @@ -23,16 +23,18 @@ def test_security_http_digest(): def test_security_http_digest_no_credentials(): response = client.get("/users/me") - assert response.status_code == 403, response.text + assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Digest" def test_security_http_digest_incorrect_scheme_credentials(): response = client.get( "/users/me", headers={"Authorization": "Other invalidauthorization"} ) - assert response.status_code == 403, response.text - assert response.json() == {"detail": "Invalid authentication credentials"} + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Digest" def test_openapi_schema(): diff --git a/tests/test_security_oauth2.py b/tests/test_security_oauth2.py index 2b7e3457a..804e4152d 100644 --- a/tests/test_security_oauth2.py +++ b/tests/test_security_oauth2.py @@ -56,8 +56,9 @@ def test_security_oauth2_password_other_header(): def test_security_oauth2_password_bearer_no_header(): response = client.get("/users/me") - assert response.status_code == 403, response.text + assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" def test_strict_login_no_data(): diff --git a/tests/test_security_openid_connect.py b/tests/test_security_openid_connect.py index 1e322e640..c9a0a8db7 100644 --- a/tests/test_security_openid_connect.py +++ b/tests/test_security_openid_connect.py @@ -39,8 +39,9 @@ def test_security_oauth2_password_other_header(): def test_security_oauth2_password_bearer_no_header(): response = client.get("/users/me") - assert response.status_code == 403, response.text + assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" def test_openapi_schema(): diff --git a/tests/test_security_openid_connect_description.py b/tests/test_security_openid_connect_description.py index 44cf57f86..d008cbc63 100644 --- a/tests/test_security_openid_connect_description.py +++ b/tests/test_security_openid_connect_description.py @@ -41,8 +41,9 @@ def test_security_oauth2_password_other_header(): def test_security_oauth2_password_bearer_no_header(): response = client.get("/users/me") - assert response.status_code == 403, response.text + assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" def test_openapi_schema(): diff --git a/tests/test_top_level_security_scheme_in_openapi.py b/tests/test_top_level_security_scheme_in_openapi.py index e2de31af5..a36c66d1a 100644 --- a/tests/test_top_level_security_scheme_in_openapi.py +++ b/tests/test_top_level_security_scheme_in_openapi.py @@ -27,7 +27,7 @@ def test_get_root(): def test_get_root_no_token(): response = client.get("/") - assert response.status_code == 403, response.text + assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} diff --git a/tests/test_tutorial/test_authentication_error_status_code/__init__.py b/tests/test_tutorial/test_authentication_error_status_code/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_tutorial/test_authentication_error_status_code/test_tutorial001.py b/tests/test_tutorial/test_authentication_error_status_code/test_tutorial001.py new file mode 100644 index 000000000..bbd7bff30 --- /dev/null +++ b/tests/test_tutorial/test_authentication_error_status_code/test_tutorial001.py @@ -0,0 +1,69 @@ +import importlib + +import pytest +from fastapi.testclient import TestClient +from inline_snapshot import snapshot + +from ...utils import needs_py39 + + +@pytest.fixture( + name="client", + params=[ + "tutorial001_an", + pytest.param("tutorial001_an_py39", marks=needs_py39), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module( + f"docs_src.authentication_error_status_code.{request.param}" + ) + + client = TestClient(mod.app) + return client + + +def test_get_me(client: TestClient): + response = client.get("/me", headers={"Authorization": "Bearer secrettoken"}) + assert response.status_code == 200 + assert response.json() == { + "message": "You are authenticated", + "token": "secrettoken", + } + + +def test_get_me_no_credentials(client: TestClient): + response = client.get("/me") + assert response.status_code == 403 + assert response.json() == {"detail": "Not authenticated"} + + +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": { + "/me": { + "get": { + "summary": "Read Me", + "operationId": "read_me_me_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "security": [{"HTTPBearer403": []}], + } + } + }, + "components": { + "securitySchemes": { + "HTTPBearer403": {"type": "http", "scheme": "bearer"} + } + }, + } + ) diff --git a/tests/test_tutorial/test_security/test_tutorial003.py b/tests/test_tutorial/test_security/test_tutorial003.py index 2bbb2e851..6b8735113 100644 --- a/tests/test_tutorial/test_security/test_tutorial003.py +++ b/tests/test_tutorial/test_security/test_tutorial003.py @@ -66,7 +66,7 @@ def test_token(client: TestClient): def test_incorrect_token(client: TestClient): response = client.get("/users/me", headers={"Authorization": "Bearer nonexistent"}) assert response.status_code == 401, response.text - assert response.json() == {"detail": "Invalid authentication credentials"} + assert response.json() == {"detail": "Not authenticated"} assert response.headers["WWW-Authenticate"] == "Bearer" diff --git a/tests/test_tutorial/test_security/test_tutorial006.py b/tests/test_tutorial/test_security/test_tutorial006.py index 40b413806..9587159dc 100644 --- a/tests/test_tutorial/test_security/test_tutorial006.py +++ b/tests/test_tutorial/test_security/test_tutorial006.py @@ -41,7 +41,7 @@ def test_security_http_basic_invalid_credentials(client: TestClient): ) assert response.status_code == 401, response.text assert response.headers["WWW-Authenticate"] == "Basic" - assert response.json() == {"detail": "Invalid authentication credentials"} + assert response.json() == {"detail": "Not authenticated"} def test_security_http_basic_non_basic_credentials(client: TestClient): @@ -50,7 +50,7 @@ def test_security_http_basic_non_basic_credentials(client: TestClient): response = client.get("/users/me", headers={"Authorization": auth_header}) assert response.status_code == 401, response.text assert response.headers["WWW-Authenticate"] == "Basic" - assert response.json() == {"detail": "Invalid authentication credentials"} + assert response.json() == {"detail": "Not authenticated"} def test_openapi_schema(client: TestClient): From a4ef97afd937a8fd180a78e11c3648509e5bc14d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 24 Nov 2025 19:03:33 +0000 Subject: [PATCH 29/47] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index ef1b98b67..c2190dafe 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Fixes + +* πŸ› Use `401` status code in security classes when credentials are missing. PR [#13786](https://github.com/fastapi/fastapi/pull/13786) by [@YuriiMotov](https://github.com/YuriiMotov). + ### Internal * πŸ”§ Configure labeler to exclude files that start from underscore for `lang-all` label. PR [#14213](https://github.com/fastapi/fastapi/pull/14213) by [@YuriiMotov](https://github.com/YuriiMotov). From 8732c53478513ddd35ae152ff9bf5e6217ed3d99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 24 Nov 2025 20:12:28 +0100 Subject: [PATCH 30/47] =?UTF-8?q?=F0=9F=93=9D=20Updates=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index c2190dafe..0ccb6b04e 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -10,6 +10,7 @@ hide: ### Fixes * πŸ› Use `401` status code in security classes when credentials are missing. PR [#13786](https://github.com/fastapi/fastapi/pull/13786) by [@YuriiMotov](https://github.com/YuriiMotov). + * If your code depended on these classes raising the old (less correct) `403` status code, check the new docs about how to override the classes, to use the same old behavior: [Use Old 403 Authentication Error Status Codes](https://fastapi.tiangolo.com/how-to/authentication-error-status-code/). ### Internal From 5b0625df96e4ea11b54fcb2a76f21f7ad94764fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 24 Nov 2025 20:14:34 +0100 Subject: [PATCH 31/47] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.122.?= =?UTF-8?q?0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 0ccb6b04e..2c50bc9f2 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +## 0.122.0 + ### Fixes * πŸ› Use `401` status code in security classes when credentials are missing. PR [#13786](https://github.com/fastapi/fastapi/pull/13786) by [@YuriiMotov](https://github.com/YuriiMotov). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 85a7ea7b5..3fbd7fc28 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.121.3" +__version__ = "0.122.0" from starlette import status as status From 8ab7167eaf046fb1c7a700dd72e773bb16e7d88f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 28 Nov 2025 07:55:15 -0800 Subject: [PATCH 32/47] =?UTF-8?q?=F0=9F=92=85=20Update=20CSS=20to=20explic?= =?UTF-8?q?itly=20use=20emoji=20font=20(#14415)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/css/custom.css | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/css/custom.css b/docs/en/docs/css/custom.css index 8849d8741..87111ff64 100644 --- a/docs/en/docs/css/custom.css +++ b/docs/en/docs/css/custom.css @@ -1,9 +1,16 @@ /* Fira Code, including characters used by Rich output, like the "heavy right-pointing angle bracket ornament", not included in Google Fonts */ @import url(https://cdn.jsdelivr.net/npm/firacode@6.2.0/distr/fira_code.css); +/* Noto Color Emoji for emoji support with the same font everywhere */ +@import url(https://fonts.googleapis.com/css2?family=Noto+Color+Emoji&display=swap); /* Override default code font in Material for MkDocs to Fira Code */ :root { - --md-code-font: "Fira Code", monospace; + --md-code-font: "Fira Code", monospace, "Noto Color Emoji"; +} + +/* Override default regular font in Material for MkDocs to include Noto Color Emoji */ +:root { + --md-text-font: "Roboto", "Noto Color Emoji"; } .termynal-comment { From 998288261af114efd39fb2061ed7ceba32f8699f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 28 Nov 2025 15:55:40 +0000 Subject: [PATCH 33/47] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 2c50bc9f2..4b2cb937d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Docs + +* πŸ’… Update CSS to explicitly use emoji font. PR [#14415](https://github.com/fastapi/fastapi/pull/14415) by [@tiangolo](https://github.com/tiangolo). + ## 0.122.0 ### Fixes From 62a69740041726b8c27815f6246272db5ebf7ee5 Mon Sep 17 00:00:00 2001 From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Sat, 29 Nov 2025 13:08:57 +0100 Subject: [PATCH 34/47] =?UTF-8?q?=E2=AC=86=20Bump=20markdown-include-varia?= =?UTF-8?q?nts=20from=200.0.5=20to=200.0.6=20(#14418)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index 05b47fe92..ae1ddbc3d 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -17,5 +17,5 @@ griffe-warnings-deprecated==1.1.0 # For griffe, it formats with black black==25.1.0 mkdocs-macros-plugin==1.4.1 -markdown-include-variants==0.0.5 +markdown-include-variants==0.0.6 python-slugify==8.0.4 From c6487ed632056e450d844846a1b63be551a3cbc6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 29 Nov 2025 12:09:26 +0000 Subject: [PATCH 35/47] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 4b2cb937d..fa8ddcdad 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -11,6 +11,10 @@ hide: * πŸ’… Update CSS to explicitly use emoji font. PR [#14415](https://github.com/fastapi/fastapi/pull/14415) by [@tiangolo](https://github.com/tiangolo). +### Internal + +* ⬆ Bump markdown-include-variants from 0.0.5 to 0.0.6. PR [#14418](https://github.com/fastapi/fastapi/pull/14418) by [@YuriiMotov](https://github.com/YuriiMotov). + ## 0.122.0 ### Fixes From 378ad688b7e57efb190506b3b36be65eb8ad5e6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristj=C3=A1n=20Valur=20J=C3=B3nsson?= Date: Sun, 30 Nov 2025 11:57:01 +0000 Subject: [PATCH 36/47] =?UTF-8?q?=F0=9F=90=9B=20Fix=20hierarchical=20secur?= =?UTF-8?q?ity=20scope=20propagation=20(#5624)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: SebastiΓ‘n RamΓ­rez Co-authored-by: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Co-authored-by: svlandeg Co-authored-by: Sofie Van Landeghem --- fastapi/dependencies/utils.py | 4 +- tests/test_security_scopes_dont_propagate.py | 45 ++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 tests/test_security_scopes_dont_propagate.py diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 1e92c1ba2..45353835b 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -278,7 +278,9 @@ def get_dependant( use_security_scopes = security_scopes or [] if isinstance(param_details.depends, params.Security): if param_details.depends.scopes: - use_security_scopes.extend(param_details.depends.scopes) + use_security_scopes = use_security_scopes + list( + param_details.depends.scopes + ) sub_dependant = get_dependant( path=path, call=param_details.depends.dependency, diff --git a/tests/test_security_scopes_dont_propagate.py b/tests/test_security_scopes_dont_propagate.py new file mode 100644 index 000000000..2bbcc749d --- /dev/null +++ b/tests/test_security_scopes_dont_propagate.py @@ -0,0 +1,45 @@ +# Ref: https://github.com/tiangolo/fastapi/issues/5623 + +from typing import Any, Dict, List + +from fastapi import FastAPI, Security +from fastapi.security import SecurityScopes +from fastapi.testclient import TestClient +from typing_extensions import Annotated + + +async def security1(scopes: SecurityScopes): + return scopes.scopes + + +async def security2(scopes: SecurityScopes): + return scopes.scopes + + +async def dep3( + dep1: Annotated[List[str], Security(security1, scopes=["scope1"])], + dep2: Annotated[List[str], Security(security2, scopes=["scope2"])], +): + return {"dep1": dep1, "dep2": dep2} + + +app = FastAPI() + + +@app.get("/scopes") +def get_scopes( + dep3: Annotated[Dict[str, Any], Security(dep3, scopes=["scope3"])], +): + return dep3 + + +client = TestClient(app) + + +def test_security_scopes_dont_propagate(): + response = client.get("/scopes") + assert response.status_code == 200 + assert response.json() == { + "dep1": ["scope3", "scope1"], + "dep2": ["scope3", "scope2"], + } From 7681f2904d2f902057e357c107adf39ecfb14ea9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 30 Nov 2025 11:57:24 +0000 Subject: [PATCH 37/47] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index fa8ddcdad..65306828d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Fixes + +* πŸ› Fix hierarchical security scope propagation. PR [#5624](https://github.com/fastapi/fastapi/pull/5624) by [@kristjanvalur](https://github.com/kristjanvalur). + ### Docs * πŸ’… Update CSS to explicitly use emoji font. PR [#14415](https://github.com/fastapi/fastapi/pull/14415) by [@tiangolo](https://github.com/tiangolo). From 63d7a2b9978258d13dfc22664e60fc2110d30e20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 30 Nov 2025 13:00:20 +0100 Subject: [PATCH 38/47] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.122.?= =?UTF-8?q?1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 65306828d..975166a63 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +## 0.122.1 + ### Fixes * πŸ› Fix hierarchical security scope propagation. PR [#5624](https://github.com/fastapi/fastapi/pull/5624) by [@kristjanvalur](https://github.com/kristjanvalur). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 3fbd7fc28..92c067e50 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.122.0" +__version__ = "0.122.1" from starlette import status as status From 7fbd30460f480e90faf321b9158bffb5116000d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 30 Nov 2025 06:45:49 -0800 Subject: [PATCH 39/47] =?UTF-8?q?=F0=9F=90=9B=20Cache=20dependencies=20tha?= =?UTF-8?q?t=20don't=20use=20scopes=20and=20don't=20have=20sub-dependencie?= =?UTF-8?q?s=20with=20scopes=20(#14419)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions[bot] --- fastapi/dependencies/models.py | 28 ++++- fastapi/dependencies/utils.py | 34 +++--- tests/test_security_scopes.py | 46 ++++++++ tests/test_security_scopes_sub_dependency.py | 107 +++++++++++++++++++ 4 files changed, 195 insertions(+), 20 deletions(-) create mode 100644 tests/test_security_scopes.py create mode 100644 tests/test_security_scopes_sub_dependency.py diff --git a/fastapi/dependencies/models.py b/fastapi/dependencies/models.py index d6359c0f5..fbb666a7d 100644 --- a/fastapi/dependencies/models.py +++ b/fastapi/dependencies/models.py @@ -38,19 +38,43 @@ class Dependant: response_param_name: Optional[str] = None background_tasks_param_name: Optional[str] = None security_scopes_param_name: Optional[str] = None - security_scopes: Optional[List[str]] = None + own_oauth_scopes: Optional[List[str]] = None + parent_oauth_scopes: Optional[List[str]] = None use_cache: bool = True path: Optional[str] = None scope: Union[Literal["function", "request"], None] = None + @cached_property + def oauth_scopes(self) -> List[str]: + scopes = self.parent_oauth_scopes.copy() if self.parent_oauth_scopes else [] + # This doesn't use a set to preserve order, just in case + for scope in self.own_oauth_scopes or []: + if scope not in scopes: + scopes.append(scope) + return scopes + @cached_property def cache_key(self) -> DependencyCacheKey: + scopes_for_cache = ( + tuple(sorted(set(self.oauth_scopes or []))) if self._uses_scopes else () + ) return ( self.call, - tuple(sorted(set(self.security_scopes or []))), + scopes_for_cache, self.computed_scope or "", ) + @cached_property + def _uses_scopes(self) -> bool: + if self.own_oauth_scopes: + return True + if self.security_scopes_param_name is not None: + return True + for sub_dep in self.dependencies: + if sub_dep._uses_scopes: + return True + return False + @cached_property def is_gen_callable(self) -> bool: if inspect.isgeneratorfunction(self.call): diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 45353835b..d43fa8a51 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -58,8 +58,7 @@ from fastapi.dependencies.models import Dependant, SecurityRequirement from fastapi.exceptions import DependencyScopeError from fastapi.logger import logger from fastapi.security.base import SecurityBase -from fastapi.security.oauth2 import OAuth2, SecurityScopes -from fastapi.security.open_id_connect_url import OpenIdConnect +from fastapi.security.oauth2 import SecurityScopes from fastapi.types import DependencyCacheKey from fastapi.utils import create_model_field, get_path_param_names from pydantic import BaseModel @@ -126,14 +125,14 @@ def get_parameterless_sub_dependant(*, depends: params.Depends, path: str) -> De assert callable(depends.dependency), ( "A parameter-less dependency must have a callable dependency" ) - use_security_scopes: List[str] = [] + own_oauth_scopes: List[str] = [] if isinstance(depends, params.Security) and depends.scopes: - use_security_scopes.extend(depends.scopes) + own_oauth_scopes.extend(depends.scopes) return get_dependant( path=path, call=depends.dependency, scope=depends.scope, - security_scopes=use_security_scopes, + own_oauth_scopes=own_oauth_scopes, ) @@ -232,7 +231,8 @@ def get_dependant( path: str, call: Callable[..., Any], name: Optional[str] = None, - security_scopes: Optional[List[str]] = None, + own_oauth_scopes: Optional[List[str]] = None, + parent_oauth_scopes: Optional[List[str]] = None, use_cache: bool = True, scope: Union[Literal["function", "request"], None] = None, ) -> Dependant: @@ -240,19 +240,18 @@ def get_dependant( call=call, name=name, path=path, - security_scopes=security_scopes, use_cache=use_cache, scope=scope, + own_oauth_scopes=own_oauth_scopes, + parent_oauth_scopes=parent_oauth_scopes, ) + current_scopes = (parent_oauth_scopes or []) + (own_oauth_scopes or []) path_param_names = get_path_param_names(path) endpoint_signature = get_typed_signature(call) signature_params = endpoint_signature.parameters if isinstance(call, SecurityBase): - use_scopes: List[str] = [] - if isinstance(call, (OAuth2, OpenIdConnect)): - use_scopes = security_scopes or use_scopes security_requirement = SecurityRequirement( - security_scheme=call, scopes=use_scopes + security_scheme=call, scopes=current_scopes ) dependant.security_requirements.append(security_requirement) for param_name, param in signature_params.items(): @@ -275,17 +274,16 @@ def get_dependant( f'The dependency "{dependant.call.__name__}" has a scope of ' '"request", it cannot depend on dependencies with scope "function".' ) - use_security_scopes = security_scopes or [] + sub_own_oauth_scopes: List[str] = [] if isinstance(param_details.depends, params.Security): if param_details.depends.scopes: - use_security_scopes = use_security_scopes + list( - param_details.depends.scopes - ) + sub_own_oauth_scopes = list(param_details.depends.scopes) sub_dependant = get_dependant( path=path, call=param_details.depends.dependency, name=param_name, - security_scopes=use_security_scopes, + own_oauth_scopes=sub_own_oauth_scopes, + parent_oauth_scopes=current_scopes, use_cache=param_details.depends.use_cache, scope=param_details.depends.scope, ) @@ -611,7 +609,7 @@ async def solve_dependencies( path=use_path, call=call, name=sub_dependant.name, - security_scopes=sub_dependant.security_scopes, + parent_oauth_scopes=sub_dependant.oauth_scopes, scope=sub_dependant.scope, ) @@ -693,7 +691,7 @@ async def solve_dependencies( values[dependant.response_param_name] = response if dependant.security_scopes_param_name: values[dependant.security_scopes_param_name] = SecurityScopes( - scopes=dependant.security_scopes + scopes=dependant.oauth_scopes ) return SolvedDependency( values=values, diff --git a/tests/test_security_scopes.py b/tests/test_security_scopes.py new file mode 100644 index 000000000..248fd2bcc --- /dev/null +++ b/tests/test_security_scopes.py @@ -0,0 +1,46 @@ +from typing import Dict + +import pytest +from fastapi import Depends, FastAPI, Security +from fastapi.testclient import TestClient +from typing_extensions import Annotated + + +@pytest.fixture(name="call_counter") +def call_counter_fixture(): + return {"count": 0} + + +@pytest.fixture(name="app") +def app_fixture(call_counter: Dict[str, int]): + def get_db(): + call_counter["count"] += 1 + return f"db_{call_counter['count']}" + + def get_user(db: Annotated[str, Depends(get_db)]): + return "user" + + app = FastAPI() + + @app.get("/") + def endpoint( + db: Annotated[str, Depends(get_db)], + user: Annotated[str, Security(get_user, scopes=["read"])], + ): + return {"db": db} + + return app + + +@pytest.fixture(name="client") +def client_fixture(app: FastAPI): + return TestClient(app) + + +def test_security_scopes_dependency_called_once( + client: TestClient, call_counter: Dict[str, int] +): + response = client.get("/") + + assert response.status_code == 200 + assert call_counter["count"] == 1 diff --git a/tests/test_security_scopes_sub_dependency.py b/tests/test_security_scopes_sub_dependency.py new file mode 100644 index 000000000..9cc668d8e --- /dev/null +++ b/tests/test_security_scopes_sub_dependency.py @@ -0,0 +1,107 @@ +# Ref: https://github.com/fastapi/fastapi/discussions/6024#discussioncomment-8541913 + +from typing import Dict + +import pytest +from fastapi import Depends, FastAPI, Security +from fastapi.security import SecurityScopes +from fastapi.testclient import TestClient +from typing_extensions import Annotated + + +@pytest.fixture(name="call_counts") +def call_counts_fixture(): + return { + "get_db_session": 0, + "get_current_user": 0, + "get_user_me": 0, + "get_user_items": 0, + } + + +@pytest.fixture(name="app") +def app_fixture(call_counts: Dict[str, int]): + def get_db_session(): + call_counts["get_db_session"] += 1 + return f"db_session_{call_counts['get_db_session']}" + + def get_current_user( + security_scopes: SecurityScopes, + db_session: Annotated[str, Depends(get_db_session)], + ): + call_counts["get_current_user"] += 1 + return { + "user": f"user_{call_counts['get_current_user']}", + "scopes": security_scopes.scopes, + "db_session": db_session, + } + + def get_user_me( + current_user: Annotated[dict, Security(get_current_user, scopes=["me"])], + ): + call_counts["get_user_me"] += 1 + return { + "user_me": f"user_me_{call_counts['get_user_me']}", + "current_user": current_user, + } + + def get_user_items( + user_me: Annotated[dict, Depends(get_user_me)], + ): + call_counts["get_user_items"] += 1 + return { + "user_items": f"user_items_{call_counts['get_user_items']}", + "user_me": user_me, + } + + app = FastAPI() + + @app.get("/") + def path_operation( + user_me: Annotated[dict, Depends(get_user_me)], + user_items: Annotated[dict, Security(get_user_items, scopes=["items"])], + ): + return { + "user_me": user_me, + "user_items": user_items, + } + + return app + + +@pytest.fixture(name="client") +def client_fixture(app: FastAPI): + return TestClient(app) + + +def test_security_scopes_sub_dependency_caching( + client: TestClient, call_counts: Dict[str, int] +): + response = client.get("/") + + assert response.status_code == 200 + assert call_counts["get_db_session"] == 1 + assert call_counts["get_current_user"] == 2 + assert call_counts["get_user_me"] == 2 + assert call_counts["get_user_items"] == 1 + assert response.json() == { + "user_me": { + "user_me": "user_me_1", + "current_user": { + "user": "user_1", + "scopes": ["me"], + "db_session": "db_session_1", + }, + }, + "user_items": { + "user_items": "user_items_1", + "user_me": { + "user_me": "user_me_2", + "current_user": { + "user": "user_2", + "scopes": ["items", "me"], + "db_session": "db_session_1", + }, + }, + }, + } From c38e3e0108852f0dfec0e9bb5fec7b3ccf7ddad3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 30 Nov 2025 14:46:13 +0000 Subject: [PATCH 40/47] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 975166a63..505ae48a5 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Fixes + +* πŸ› Cache dependencies that don't use scopes and don't have sub-dependencies with scopes. PR [#14419](https://github.com/fastapi/fastapi/pull/14419) by [@tiangolo](https://github.com/tiangolo). + ## 0.122.1 ### Fixes From f2bab952678f301349c9805dd576af9425a95953 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 30 Nov 2025 15:47:35 +0100 Subject: [PATCH 41/47] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.123.?= =?UTF-8?q?0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 505ae48a5..8848784b1 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +## 0.123.0 + ### Fixes * πŸ› Cache dependencies that don't use scopes and don't have sub-dependencies with scopes. PR [#14419](https://github.com/fastapi/fastapi/pull/14419) by [@tiangolo](https://github.com/tiangolo). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 92c067e50..25ed2bbeb 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.122.1" +__version__ = "0.123.0" from starlette import status as status From 32aba57b499c9ec01a87f4e3a9aca980941e8959 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 30 Nov 2025 22:27:43 -0800 Subject: [PATCH 42/47] =?UTF-8?q?=F0=9F=91=A5=20Update=20FastAPI=20People?= =?UTF-8?q?=20-=20Contributors=20and=20Translators=20(#14420)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions --- docs/en/data/contributors.yml | 45 ++++++++++++++------------ docs/en/data/translation_reviewers.yml | 16 ++++----- docs/en/data/translators.yml | 24 +++++++------- 3 files changed, 45 insertions(+), 40 deletions(-) diff --git a/docs/en/data/contributors.yml b/docs/en/data/contributors.yml index 592c79af0..163dc68e3 100644 --- a/docs/en/data/contributors.yml +++ b/docs/en/data/contributors.yml @@ -1,21 +1,21 @@ tiangolo: login: tiangolo - count: 794 + count: 808 avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=cb5d06e73a9e1998141b1641aa88e443c6717651&v=4 url: https://github.com/tiangolo dependabot: login: dependabot - count: 126 + count: 130 avatarUrl: https://avatars.githubusercontent.com/in/29110?v=4 url: https://github.com/apps/dependabot alejsdev: login: alejsdev count: 52 - avatarUrl: https://avatars.githubusercontent.com/u/90076947?u=447d12a1b347f466b35378bee4c7104cc9b2c571&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/90076947?u=85ceac49fb87138aebe8d663912e359447329090&v=4 url: https://github.com/alejsdev pre-commit-ci: login: pre-commit-ci - count: 49 + count: 50 avatarUrl: https://avatars.githubusercontent.com/in/68672?v=4 url: https://github.com/apps/pre-commit-ci github-actions: @@ -28,31 +28,31 @@ Kludex: count: 25 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=df8a3f06ba8f55ae1967a3e2d5ed882903a4e330&v=4 url: https://github.com/Kludex +YuriiMotov: + login: YuriiMotov + count: 20 + avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=b9b13d598dddfab529a52d264df80a900bfe7060&v=4 + url: https://github.com/YuriiMotov dmontagu: login: dmontagu count: 17 avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=540f30c937a6450812628b9592a1dfe91bbe148e&v=4 url: https://github.com/dmontagu -YuriiMotov: - login: YuriiMotov - count: 15 - avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=b9b13d598dddfab529a52d264df80a900bfe7060&v=4 - url: https://github.com/YuriiMotov nilslindemann: login: nilslindemann - count: 14 + count: 15 avatarUrl: https://avatars.githubusercontent.com/u/6892179?u=1dca6a22195d6cd1ab20737c0e19a4c55d639472&v=4 url: https://github.com/nilslindemann +svlandeg: + login: svlandeg + count: 14 + avatarUrl: https://avatars.githubusercontent.com/u/8796347?u=556c97650c27021911b0b9447ec55e75987b0e8a&v=4 + url: https://github.com/svlandeg euri10: login: euri10 count: 13 avatarUrl: https://avatars.githubusercontent.com/u/1104190?u=321a2e953e6645a7d09b732786c7a8061e0f8a8b&v=4 url: https://github.com/euri10 -svlandeg: - login: svlandeg - count: 13 - avatarUrl: https://avatars.githubusercontent.com/u/8796347?u=556c97650c27021911b0b9447ec55e75987b0e8a&v=4 - url: https://github.com/svlandeg kantandane: login: kantandane count: 13 @@ -103,6 +103,11 @@ waynerv: count: 5 avatarUrl: https://avatars.githubusercontent.com/u/39515546?u=ec35139777597cdbbbddda29bf8b9d4396b429a9&v=4 url: https://github.com/waynerv +musicinmybrain: + login: musicinmybrain + count: 5 + avatarUrl: https://avatars.githubusercontent.com/u/6898909?u=9010312053e7141383b9bdf538036c7f37fbaba0&v=4 + url: https://github.com/musicinmybrain krishnamadhavan: login: krishnamadhavan count: 5 @@ -133,11 +138,6 @@ iudeen: count: 4 avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=f09cdd745e5bf16138f29b42732dd57c7f02bee1&v=4 url: https://github.com/iudeen -musicinmybrain: - login: musicinmybrain - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/6898909?u=9010312053e7141383b9bdf538036c7f37fbaba0&v=4 - url: https://github.com/musicinmybrain philipokiokio: login: philipokiokio count: 4 @@ -483,6 +483,11 @@ nzig: count: 2 avatarUrl: https://avatars.githubusercontent.com/u/7372858?u=e769add36ed73c778cdb136eb10bf96b1e119671&v=4 url: https://github.com/nzig +kristjanvalur: + login: kristjanvalur + count: 2 + avatarUrl: https://avatars.githubusercontent.com/u/6009543?u=1419f20bbfff8f031be8cb470962e7e62de2595e&v=4 + url: https://github.com/kristjanvalur yezz123: login: yezz123 count: 2 diff --git a/docs/en/data/translation_reviewers.yml b/docs/en/data/translation_reviewers.yml index 45aa55e5e..c3d3d0388 100644 --- a/docs/en/data/translation_reviewers.yml +++ b/docs/en/data/translation_reviewers.yml @@ -75,7 +75,7 @@ mattwang44: url: https://github.com/mattwang44 tiangolo: login: tiangolo - count: 55 + count: 56 avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=cb5d06e73a9e1998141b1641aa88e443c6717651&v=4 url: https://github.com/tiangolo Laineyzhang55: @@ -136,7 +136,7 @@ JavierSanchezCastro: alejsdev: login: alejsdev count: 37 - avatarUrl: https://avatars.githubusercontent.com/u/90076947?u=447d12a1b347f466b35378bee4c7104cc9b2c571&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/90076947?u=85ceac49fb87138aebe8d663912e359447329090&v=4 url: https://github.com/alejsdev stlucasgarcia: login: stlucasgarcia @@ -436,7 +436,7 @@ jburckel: peidrao: login: peidrao count: 13 - avatarUrl: https://avatars.githubusercontent.com/u/32584628?u=64c634bb10381905038ff7faf3c8c3df47fb799a&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/32584628?u=979c62398e16ff000cc0faa028e028efd679887c&v=4 url: https://github.com/peidrao impocode: login: impocode @@ -1006,7 +1006,7 @@ takacs: anton2yakovlev: login: anton2yakovlev count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/44229180?u=bdd445ba99074b378e7298d23c4bf6d707d2c282&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/44229180?u=ac245e57bc834ff80f08ca8128000bb650a77a3d&v=4 url: https://github.com/anton2yakovlev ILoveSorasakiHina: login: ILoveSorasakiHina @@ -1161,7 +1161,7 @@ cookie-byte217: AbolfazlKameli: login: AbolfazlKameli count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/120686133?u=e41743da3c1820efafc59c5870cacd4f4425334c&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/120686133?u=af8f025278cce0d489007071254e4055df60b78c&v=4 url: https://github.com/AbolfazlKameli tyronedamasceno: login: tyronedamasceno @@ -1196,7 +1196,7 @@ Xaraxx: Suyoung789: login: Suyoung789 count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/31277231?u=744bd3e641413e19bfad6b06a90bb0887c3f9332&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/31277231?u=1591aaf651eb860017231a36590050e154c026b6&v=4 url: https://github.com/Suyoung789 akagaeng: login: akagaeng @@ -1806,7 +1806,7 @@ MrL8199: ivintoiu: login: ivintoiu count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/1853336?u=b537c905ad08b69993de8796fb235c8d4d47f039&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/1853336?u=e3de5fd0ab17efc12256b4295285b504ca281440&v=4 url: https://github.com/ivintoiu TechnoService2: login: TechnoService2 @@ -1841,7 +1841,7 @@ NavesSapnis: eqsdxr: login: eqsdxr count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/157279130?u=d7aaffb29f542b647cf0f6b0e05722490863658a&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/157279130?u=7927dc0366995334f9a18c3204a41d3a34d6d96f&v=4 url: https://github.com/eqsdxr syedasamina56: login: syedasamina56 diff --git a/docs/en/data/translators.yml b/docs/en/data/translators.yml index a4b87e1bf..c66eff4d4 100644 --- a/docs/en/data/translators.yml +++ b/docs/en/data/translators.yml @@ -1,6 +1,6 @@ nilslindemann: login: nilslindemann - count: 124 + count: 125 avatarUrl: https://avatars.githubusercontent.com/u/6892179?u=1dca6a22195d6cd1ab20737c0e19a4c55d639472&v=4 url: https://github.com/nilslindemann jaystone776: @@ -8,16 +8,16 @@ jaystone776: count: 46 avatarUrl: https://avatars.githubusercontent.com/u/11191137?u=299205a95e9b6817a43144a48b643346a5aac5cc&v=4 url: https://github.com/jaystone776 +ceb10n: + login: ceb10n + count: 29 + avatarUrl: https://avatars.githubusercontent.com/u/235213?u=edcce471814a1eba9f0cdaa4cd0de18921a940a6&v=4 + url: https://github.com/ceb10n valentinDruzhinin: login: valentinDruzhinin count: 29 avatarUrl: https://avatars.githubusercontent.com/u/12831905?u=aae1ebc675c91e8fa582df4fcc4fc4128106344d&v=4 url: https://github.com/valentinDruzhinin -ceb10n: - login: ceb10n - count: 27 - avatarUrl: https://avatars.githubusercontent.com/u/235213?u=edcce471814a1eba9f0cdaa4cd0de18921a940a6&v=4 - url: https://github.com/ceb10n tokusumi: login: tokusumi count: 23 @@ -286,7 +286,7 @@ hsuanchi: alejsdev: login: alejsdev count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/90076947?u=447d12a1b347f466b35378bee4c7104cc9b2c571&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/90076947?u=85ceac49fb87138aebe8d663912e359447329090&v=4 url: https://github.com/alejsdev riroan: login: riroan @@ -358,6 +358,11 @@ ruzia: count: 3 avatarUrl: https://avatars.githubusercontent.com/u/24503?v=4 url: https://github.com/ruzia +YuriiMotov: + login: YuriiMotov + count: 3 + avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=b9b13d598dddfab529a52d264df80a900bfe7060&v=4 + url: https://github.com/YuriiMotov izaguerreiro: login: izaguerreiro count: 2 @@ -543,8 +548,3 @@ EdmilsonRodrigues: count: 2 avatarUrl: https://avatars.githubusercontent.com/u/62777025?u=217d6f3cd6cc750bb8818a3af7726c8d74eb7c2d&v=4 url: https://github.com/EdmilsonRodrigues -YuriiMotov: - login: YuriiMotov - count: 2 - avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=b9b13d598dddfab529a52d264df80a900bfe7060&v=4 - url: https://github.com/YuriiMotov From d661bb1324b84d53512874c34429488db600cbef Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 1 Dec 2025 06:28:06 +0000 Subject: [PATCH 43/47] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 8848784b1..431e6149e 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Internal + +* πŸ‘₯ Update FastAPI People - Contributors and Translators. PR [#14420](https://github.com/fastapi/fastapi/pull/14420) by [@tiangolo](https://github.com/tiangolo). + ## 0.123.0 ### Fixes From 8a7ad3d255f7318b67bf5dd03f8f231d2ade4b42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 30 Nov 2025 22:30:56 -0800 Subject: [PATCH 44/47] =?UTF-8?q?=F0=9F=91=A5=20Update=20FastAPI=20People?= =?UTF-8?q?=20-=20Sponsors=20(#14422)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions --- docs/en/data/github_sponsors.yml | 128 +++++++++++++------------------ 1 file changed, 52 insertions(+), 76 deletions(-) diff --git a/docs/en/data/github_sponsors.yml b/docs/en/data/github_sponsors.yml index 3d8ecdb7a..24780603d 100644 --- a/docs/en/data/github_sponsors.yml +++ b/docs/en/data/github_sponsors.yml @@ -23,9 +23,6 @@ sponsors: - login: railwayapp avatarUrl: https://avatars.githubusercontent.com/u/66716858?v=4 url: https://github.com/railwayapp - - login: scalar - avatarUrl: https://avatars.githubusercontent.com/u/301879?v=4 - url: https://github.com/scalar - - login: dribia avatarUrl: https://avatars.githubusercontent.com/u/41189616?v=4 url: https://github.com/dribia @@ -44,25 +41,25 @@ sponsors: - login: permitio avatarUrl: https://avatars.githubusercontent.com/u/71775833?v=4 url: https://github.com/permitio -- - login: BoostryJP - avatarUrl: https://avatars.githubusercontent.com/u/57932412?v=4 - url: https://github.com/BoostryJP - - login: mercedes-benz - avatarUrl: https://avatars.githubusercontent.com/u/34240465?v=4 - url: https://github.com/mercedes-benz - - login: Ponte-Energy-Partners +- - login: Ponte-Energy-Partners avatarUrl: https://avatars.githubusercontent.com/u/114745848?v=4 url: https://github.com/Ponte-Energy-Partners - login: LambdaTest-Inc avatarUrl: https://avatars.githubusercontent.com/u/171592363?u=96606606a45fa170427206199014f2a5a2a4920b&v=4 url: https://github.com/LambdaTest-Inc + - login: BoostryJP + avatarUrl: https://avatars.githubusercontent.com/u/57932412?v=4 + url: https://github.com/BoostryJP - login: requestly avatarUrl: https://avatars.githubusercontent.com/u/12287519?v=4 url: https://github.com/requestly - login: acsone avatarUrl: https://avatars.githubusercontent.com/u/7601056?v=4 url: https://github.com/acsone -- - login: Trivie +- - login: scalar + avatarUrl: https://avatars.githubusercontent.com/u/301879?v=4 + url: https://github.com/scalar + - login: Trivie avatarUrl: https://avatars.githubusercontent.com/u/8161763?v=4 url: https://github.com/Trivie - - login: takashi-yoneya @@ -71,42 +68,30 @@ sponsors: - login: Doist avatarUrl: https://avatars.githubusercontent.com/u/2565372?v=4 url: https://github.com/Doist + - login: bholagabbar + avatarUrl: https://avatars.githubusercontent.com/u/11693595?v=4 + url: https://github.com/bholagabbar - - login: mainframeindustries avatarUrl: https://avatars.githubusercontent.com/u/55092103?v=4 url: https://github.com/mainframeindustries - - login: alixlahuec avatarUrl: https://avatars.githubusercontent.com/u/29543316?u=44357eb2a93bccf30fb9d389b8befe94a3d00985&v=4 url: https://github.com/alixlahuec - - login: Partho - avatarUrl: https://avatars.githubusercontent.com/u/2034301?u=ce195ac36835cca0cdfe6dd6e897bd38873a1524&v=4 - url: https://github.com/Partho - - login: primer-io avatarUrl: https://avatars.githubusercontent.com/u/62146168?v=4 url: https://github.com/primer-io - - login: xsalagarcia - avatarUrl: https://avatars.githubusercontent.com/u/66035908?v=4 - url: https://github.com/xsalagarcia - - login: upciti avatarUrl: https://avatars.githubusercontent.com/u/43346262?v=4 url: https://github.com/upciti - - login: GonnaFlyMethod - avatarUrl: https://avatars.githubusercontent.com/u/60840539?u=edf70b373fd4f1a83d3eb7c6802f4b6addb572cf&v=4 - url: https://github.com/GonnaFlyMethod - login: ChargeStorm avatarUrl: https://avatars.githubusercontent.com/u/26000165?v=4 url: https://github.com/ChargeStorm - - login: DanielYang59 - avatarUrl: https://avatars.githubusercontent.com/u/80093591?u=63873f701c7c74aac83c906800a1dddc0bc8c92f&v=4 - url: https://github.com/DanielYang59 - login: nilslindemann avatarUrl: https://avatars.githubusercontent.com/u/6892179?u=1dca6a22195d6cd1ab20737c0e19a4c55d639472&v=4 url: https://github.com/nilslindemann - - login: samuelcolvin avatarUrl: https://avatars.githubusercontent.com/u/4039449?u=42eb3b833047c8c4b4f647a031eaef148c16d93f&v=4 url: https://github.com/samuelcolvin - - login: vincentkoc - avatarUrl: https://avatars.githubusercontent.com/u/25068?u=fbd5b2d51142daa4bdbc21e21953a3b8b8188a4a&v=4 - url: https://github.com/vincentkoc - login: otosky avatarUrl: https://avatars.githubusercontent.com/u/42260747?u=69d089387c743d89427aa4ad8740cfb34045a9e0&v=4 url: https://github.com/otosky @@ -137,9 +122,6 @@ sponsors: - login: jugeeem avatarUrl: https://avatars.githubusercontent.com/u/116043716?u=ae590d79c38ac79c91b9c5caa6887d061e865a3d&v=4 url: https://github.com/jugeeem - - login: connorpark24 - avatarUrl: https://avatars.githubusercontent.com/u/142128990?u=09b84a4beb1f629b77287a837bcf3729785cdd89&v=4 - url: https://github.com/connorpark24 - login: patsatsia avatarUrl: https://avatars.githubusercontent.com/u/61111267?u=3271b85f7a37b479c8d0ae0a235182e83c166edf&v=4 url: https://github.com/patsatsia @@ -155,9 +137,9 @@ sponsors: - login: kaoru0310 avatarUrl: https://avatars.githubusercontent.com/u/80977929?u=1b61d10142b490e56af932ddf08a390fae8ee94f&v=4 url: https://github.com/kaoru0310 - - login: DelfinaCare - avatarUrl: https://avatars.githubusercontent.com/u/83734439?v=4 - url: https://github.com/DelfinaCare + - login: jstanden + avatarUrl: https://avatars.githubusercontent.com/u/63288?u=c3658d57d2862c607a0e19c2101c3c51876e36ad&v=4 + url: https://github.com/jstanden - login: knallgelb avatarUrl: https://avatars.githubusercontent.com/u/2358812?u=c48cb6362b309d74cbf144bd6ad3aed3eb443e82&v=4 url: https://github.com/knallgelb @@ -191,9 +173,6 @@ sponsors: - login: oliverxchen avatarUrl: https://avatars.githubusercontent.com/u/4471774?u=534191f25e32eeaadda22dfab4b0a428733d5489&v=4 url: https://github.com/oliverxchen - - login: jstanden - avatarUrl: https://avatars.githubusercontent.com/u/63288?u=c3658d57d2862c607a0e19c2101c3c51876e36ad&v=4 - url: https://github.com/jstanden - login: paulcwatts avatarUrl: https://avatars.githubusercontent.com/u/150269?u=1819e145d573b44f0ad74b87206d21cd60331d4e&v=4 url: https://github.com/paulcwatts @@ -233,9 +212,6 @@ sponsors: - login: mjohnsey avatarUrl: https://avatars.githubusercontent.com/u/16784016?u=38fad2e6b411244560b3af99c5f5a4751bc81865&v=4 url: https://github.com/mjohnsey - - login: enguy-hub - avatarUrl: https://avatars.githubusercontent.com/u/16822912?u=2c45f9e7f427b2f2f3b023d7fdb0d44764c92ae8&v=4 - url: https://github.com/enguy-hub - login: ashi-agrawal avatarUrl: https://avatars.githubusercontent.com/u/17105294?u=99c7a854035e5398d8e7b674f2d42baae6c957f8&v=4 url: https://github.com/ashi-agrawal @@ -260,10 +236,7 @@ sponsors: - - login: manoelpqueiroz avatarUrl: https://avatars.githubusercontent.com/u/23669137?u=b12e84b28a84369ab5b30bd5a79e5788df5a0756&v=4 url: https://github.com/manoelpqueiroz -- - login: ceb10n - avatarUrl: https://avatars.githubusercontent.com/u/235213?u=edcce471814a1eba9f0cdaa4cd0de18921a940a6&v=4 - url: https://github.com/ceb10n - - login: pawamoy +- - login: pawamoy avatarUrl: https://avatars.githubusercontent.com/u/3999221?u=b030e4c89df2f3a36bc4710b925bdeb6745c9856&v=4 url: https://github.com/pawamoy - login: siavashyj @@ -281,9 +254,9 @@ sponsors: - login: hgalytoby avatarUrl: https://avatars.githubusercontent.com/u/50397689?u=6cc9028f3db63f8f60ad21c17b1ce4b88c4e2e60&v=4 url: https://github.com/hgalytoby - - login: johnl28 - avatarUrl: https://avatars.githubusercontent.com/u/54412955?u=47dd06082d1c39caa90c752eb55566e4f3813957&v=4 - url: https://github.com/johnl28 + - login: nisutec + avatarUrl: https://avatars.githubusercontent.com/u/25281462?u=e562484c451fdfc59053163f64405f8eb262b8b0&v=4 + url: https://github.com/nisutec - login: hoenie-ams avatarUrl: https://avatars.githubusercontent.com/u/25708487?u=cda07434f0509ac728d9edf5e681117c0f6b818b&v=4 url: https://github.com/hoenie-ams @@ -299,21 +272,24 @@ sponsors: - login: petercool avatarUrl: https://avatars.githubusercontent.com/u/37613029?u=75aa8c6729e6e8f85a300561c4dbeef9d65c8797&v=4 url: https://github.com/petercool + - login: johnl28 + avatarUrl: https://avatars.githubusercontent.com/u/54412955?u=47dd06082d1c39caa90c752eb55566e4f3813957&v=4 + url: https://github.com/johnl28 - login: PunRabbit avatarUrl: https://avatars.githubusercontent.com/u/70463212?u=1a835cfbc99295a60c8282f6aa6199d1b42241a5&v=4 url: https://github.com/PunRabbit - login: PelicanQ avatarUrl: https://avatars.githubusercontent.com/u/77930606?v=4 url: https://github.com/PelicanQ + - login: WillHogan + avatarUrl: https://avatars.githubusercontent.com/u/1661551?u=8a80356e3e7d5a417157aba7ea565dabc8678327&v=4 + url: https://github.com/WillHogan - login: my3 avatarUrl: https://avatars.githubusercontent.com/u/1825270?v=4 url: https://github.com/my3 - login: danielunderwood avatarUrl: https://avatars.githubusercontent.com/u/4472301?v=4 url: https://github.com/danielunderwood - - login: rangulvers - avatarUrl: https://avatars.githubusercontent.com/u/5235430?u=e254d4af4ace5a05fa58372ae677c7d26f0d5a53&v=4 - url: https://github.com/rangulvers - login: ddanier avatarUrl: https://avatars.githubusercontent.com/u/113563?u=ed1dc79de72f93bd78581f88ebc6952b62f472da&v=4 url: https://github.com/ddanier @@ -323,15 +299,21 @@ sponsors: - login: slafs avatarUrl: https://avatars.githubusercontent.com/u/210173?v=4 url: https://github.com/slafs + - login: ceb10n + avatarUrl: https://avatars.githubusercontent.com/u/235213?u=edcce471814a1eba9f0cdaa4cd0de18921a940a6&v=4 + url: https://github.com/ceb10n - login: tochikuji avatarUrl: https://avatars.githubusercontent.com/u/851759?v=4 url: https://github.com/tochikuji - login: miguelgr avatarUrl: https://avatars.githubusercontent.com/u/1484589?u=54556072b8136efa12ae3b6902032ea2a39ace4b&v=4 url: https://github.com/miguelgr - - login: WillHogan - avatarUrl: https://avatars.githubusercontent.com/u/1661551?u=8a80356e3e7d5a417157aba7ea565dabc8678327&v=4 - url: https://github.com/WillHogan + - login: xncbf + avatarUrl: https://avatars.githubusercontent.com/u/9462045?u=a80a7bb349555b277645632ed66639ff43400614&v=4 + url: https://github.com/xncbf + - login: DMantis + avatarUrl: https://avatars.githubusercontent.com/u/9536869?u=652dd0d49717803c0cbcbf44f7740e53cf2d4892&v=4 + url: https://github.com/DMantis - login: hard-coders avatarUrl: https://avatars.githubusercontent.com/u/9651103?u=95db33927bbff1ed1c07efddeb97ac2ff33068ed&v=4 url: https://github.com/hard-coders @@ -347,9 +329,9 @@ sponsors: - login: joshuatz avatarUrl: https://avatars.githubusercontent.com/u/17817563?u=f1bf05b690d1fc164218f0b420cdd3acb7913e21&v=4 url: https://github.com/joshuatz - - login: nisutec - avatarUrl: https://avatars.githubusercontent.com/u/25281462?u=e562484c451fdfc59053163f64405f8eb262b8b0&v=4 - url: https://github.com/nisutec + - login: rangulvers + avatarUrl: https://avatars.githubusercontent.com/u/5235430?u=e254d4af4ace5a05fa58372ae677c7d26f0d5a53&v=4 + url: https://github.com/rangulvers - login: sdevkota avatarUrl: https://avatars.githubusercontent.com/u/5250987?u=4ed9a120c89805a8aefda1cbdc0cf6512e64d1b4&v=4 url: https://github.com/sdevkota @@ -368,19 +350,7 @@ sponsors: - login: moonape1226 avatarUrl: https://avatars.githubusercontent.com/u/8532038?u=d9f8b855a429fff9397c3833c2ff83849ebf989d&v=4 url: https://github.com/moonape1226 - - login: xncbf - avatarUrl: https://avatars.githubusercontent.com/u/9462045?u=a80a7bb349555b277645632ed66639ff43400614&v=4 - url: https://github.com/xncbf - - login: DMantis - avatarUrl: https://avatars.githubusercontent.com/u/9536869?u=652dd0d49717803c0cbcbf44f7740e53cf2d4892&v=4 - url: https://github.com/DMantis -- - login: morzan1001 - avatarUrl: https://avatars.githubusercontent.com/u/47593005?u=c30ab7230f82a12a9b938dcb54f84a996931409a&v=4 - url: https://github.com/morzan1001 - - login: larsyngvelundin - avatarUrl: https://avatars.githubusercontent.com/u/34173819?u=74958599695bf83ac9f1addd935a51548a10c6b0&v=4 - url: https://github.com/larsyngvelundin - - login: andrecorumba +- - login: andrecorumba avatarUrl: https://avatars.githubusercontent.com/u/37807517?u=9b9be3b41da9bda60957da9ef37b50dbf65baa61&v=4 url: https://github.com/andrecorumba - login: KOZ39 @@ -389,21 +359,30 @@ sponsors: - login: rwxd avatarUrl: https://avatars.githubusercontent.com/u/40308458?u=cd04a39e3655923be4f25c2ba8a5a07b3da3230a&v=4 url: https://github.com/rwxd + - login: morzan1001 + avatarUrl: https://avatars.githubusercontent.com/u/47593005?u=c30ab7230f82a12a9b938dcb54f84a996931409a&v=4 + url: https://github.com/morzan1001 + - login: Olegt0rr + avatarUrl: https://avatars.githubusercontent.com/u/25399456?u=3e87b5239a2f4600975ba13be73054f8567c6060&v=4 + url: https://github.com/Olegt0rr + - login: dinoz0rg + avatarUrl: https://avatars.githubusercontent.com/u/32940067?u=739cda1eb123a2dd5e1db45c361396f239e23f8b&v=4 + url: https://github.com/dinoz0rg + - login: larsyngvelundin + avatarUrl: https://avatars.githubusercontent.com/u/34173819?u=74958599695bf83ac9f1addd935a51548a10c6b0&v=4 + url: https://github.com/larsyngvelundin - login: hippoley avatarUrl: https://avatars.githubusercontent.com/u/135493401?u=1164ef48a645a7c12664fabc1638fbb7e1c459b0&v=4 url: https://github.com/hippoley + - login: 4anklee + avatarUrl: https://avatars.githubusercontent.com/u/144109238?u=a79c0d581b2a3d8f3897e7ef4c012640a6c1eb3a&v=4 + url: https://github.com/4anklee - login: CoderDeltaLAN avatarUrl: https://avatars.githubusercontent.com/u/152043745?u=4ff541efffb7d134e60c5fcf2dd1e343f90bb782&v=4 url: https://github.com/CoderDeltaLAN - - login: chris1ding1 - avatarUrl: https://avatars.githubusercontent.com/u/194386334?u=5500604b50e35ed8a5aeb82ce34aa5d3ee3f88c7&v=4 - url: https://github.com/chris1ding1 - login: onestn avatarUrl: https://avatars.githubusercontent.com/u/62360849?u=746dd21c34e7e06eefb11b03e8bb01aaae3c2a4f&v=4 url: https://github.com/onestn - - login: Rubinskiy - avatarUrl: https://avatars.githubusercontent.com/u/62457878?u=f2e35ed3d196a99cfadb5a29a91950342af07e34&v=4 - url: https://github.com/Rubinskiy - login: nayasinghania avatarUrl: https://avatars.githubusercontent.com/u/74111380?u=752e99a5e139389fdc0a0677122adc08438eb076&v=4 url: https://github.com/nayasinghania @@ -413,9 +392,6 @@ sponsors: - login: andreagrandi avatarUrl: https://avatars.githubusercontent.com/u/636391?u=13d90cb8ec313593a5b71fbd4e33b78d6da736f5&v=4 url: https://github.com/andreagrandi - - login: Olegt0rr - avatarUrl: https://avatars.githubusercontent.com/u/25399456?u=3e87b5239a2f4600975ba13be73054f8567c6060&v=4 - url: https://github.com/Olegt0rr - login: msserpa avatarUrl: https://avatars.githubusercontent.com/u/6334934?u=82c4489eb1559d88d2990d60001901b14f722bbb&v=4 url: https://github.com/msserpa From f8e46d98a07fd8102598659f0b7151384b4c69cf Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 1 Dec 2025 06:31:20 +0000 Subject: [PATCH 45/47] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 431e6149e..1d348cb65 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Internal +* πŸ‘₯ Update FastAPI People - Sponsors. PR [#14422](https://github.com/fastapi/fastapi/pull/14422) by [@tiangolo](https://github.com/tiangolo). * πŸ‘₯ Update FastAPI People - Contributors and Translators. PR [#14420](https://github.com/fastapi/fastapi/pull/14420) by [@tiangolo](https://github.com/tiangolo). ## 0.123.0 From 6400d8a6239dd8fbb9e0b27a068281d26ce0e929 Mon Sep 17 00:00:00 2001 From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Mon, 1 Dec 2025 07:32:32 +0100 Subject: [PATCH 46/47] =?UTF-8?q?=E2=AC=86=20Bump=20markdown-include-varia?= =?UTF-8?q?nts=20from=200.0.6=20to=200.0.7=20(#14423)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index ae1ddbc3d..4f1863a4a 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -17,5 +17,5 @@ griffe-warnings-deprecated==1.1.0 # For griffe, it formats with black black==25.1.0 mkdocs-macros-plugin==1.4.1 -markdown-include-variants==0.0.6 +markdown-include-variants==0.0.7 python-slugify==8.0.4 From 938f4710793e89f4ba31b8867d35185dd1771a20 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 1 Dec 2025 06:33:00 +0000 Subject: [PATCH 47/47] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 1d348cb65..93a4c5c97 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Internal +* ⬆ Bump markdown-include-variants from 0.0.6 to 0.0.7. PR [#14423](https://github.com/fastapi/fastapi/pull/14423) by [@YuriiMotov](https://github.com/YuriiMotov). * πŸ‘₯ Update FastAPI People - Sponsors. PR [#14422](https://github.com/fastapi/fastapi/pull/14422) by [@tiangolo](https://github.com/tiangolo). * πŸ‘₯ Update FastAPI People - Contributors and Translators. PR [#14420](https://github.com/fastapi/fastapi/pull/14420) by [@tiangolo](https://github.com/tiangolo).