diff --git a/docs/en/docs/advanced/additional-status-codes.md b/docs/en/docs/advanced/additional-status-codes.md index b61f88b93..d287f861a 100644 --- a/docs/en/docs/advanced/additional-status-codes.md +++ b/docs/en/docs/advanced/additional-status-codes.md @@ -14,9 +14,41 @@ But you also want it to accept new items. And when the items didn't exist before To achieve that, import `JSONResponse`, and return your content there directly, setting the `status_code` that you want: -```Python hl_lines="4 25" -{!../../../docs_src/additional_status_codes/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="4 26" + {!> ../../../docs_src/additional_status_codes/tutorial001_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="4 25" + {!> ../../../docs_src/additional_status_codes/tutorial001_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="4 25" + {!> ../../../docs_src/additional_status_codes/tutorial001_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="4 25" + {!> ../../../docs_src/additional_status_codes/tutorial001.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="2 23" + {!> ../../../docs_src/additional_status_codes/tutorial001_py310.py!} + ``` !!! warning When you return a `Response` directly, like in the example above, it will be returned directly. diff --git a/docs/en/docs/advanced/advanced-dependencies.md b/docs/en/docs/advanced/advanced-dependencies.md index 5e79776ca..97621fcf9 100644 --- a/docs/en/docs/advanced/advanced-dependencies.md +++ b/docs/en/docs/advanced/advanced-dependencies.md @@ -18,9 +18,26 @@ Not the class itself (which is already a callable), but an instance of that clas To do that, we declare a method `__call__`: -```Python hl_lines="10" -{!../../../docs_src/dependencies/tutorial011.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="11" + {!> ../../../docs_src/dependencies/tutorial011_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="12" + {!> ../../../docs_src/dependencies/tutorial011_an_py39.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="10" + {!> ../../../docs_src/dependencies/tutorial011.py!} + ``` In this case, this `__call__` is what **FastAPI** will use to check for additional parameters and sub-dependencies, and this is what will be called to pass a value to the parameter in your *path operation function* later. @@ -28,9 +45,26 @@ In this case, this `__call__` is what **FastAPI** will use to check for addition And now, we can use `__init__` to declare the parameters of the instance that we can use to "parameterize" the dependency: -```Python hl_lines="7" -{!../../../docs_src/dependencies/tutorial011.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="8" + {!> ../../../docs_src/dependencies/tutorial011_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/dependencies/tutorial011_an_py39.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="7" + {!> ../../../docs_src/dependencies/tutorial011.py!} + ``` In this case, **FastAPI** won't ever touch or care about `__init__`, we will use it directly in our code. @@ -38,9 +72,26 @@ In this case, **FastAPI** won't ever touch or care about `__init__`, we will use We could create an instance of this class with: -```Python hl_lines="16" -{!../../../docs_src/dependencies/tutorial011.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="17" + {!> ../../../docs_src/dependencies/tutorial011_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="18" + {!> ../../../docs_src/dependencies/tutorial011_an_py39.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="16" + {!> ../../../docs_src/dependencies/tutorial011.py!} + ``` And that way we are able to "parameterize" our dependency, that now has `"bar"` inside of it, as the attribute `checker.fixed_content`. @@ -56,9 +107,26 @@ checker(q="somequery") ...and pass whatever that returns as the value of the dependency in our *path operation function* as the parameter `fixed_content_included`: -```Python hl_lines="20" -{!../../../docs_src/dependencies/tutorial011.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="21" + {!> ../../../docs_src/dependencies/tutorial011_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="22" + {!> ../../../docs_src/dependencies/tutorial011_an_py39.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="20" + {!> ../../../docs_src/dependencies/tutorial011.py!} + ``` !!! tip All this might seem contrived. And it might not be very clear how is it useful yet. diff --git a/docs/en/docs/advanced/security/http-basic-auth.md b/docs/en/docs/advanced/security/http-basic-auth.md index 90c516808..a9ce3b1ef 100644 --- a/docs/en/docs/advanced/security/http-basic-auth.md +++ b/docs/en/docs/advanced/security/http-basic-auth.md @@ -20,9 +20,26 @@ Then, when you type that username and password, the browser sends them in the he * It returns an object of type `HTTPBasicCredentials`: * It contains the `username` and `password` sent. -```Python hl_lines="2 6 10" -{!../../../docs_src/security/tutorial006.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="2 7 11" + {!> ../../../docs_src/security/tutorial006_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="4 8 12" + {!> ../../../docs_src/security/tutorial006_an_py39.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="2 6 10" + {!> ../../../docs_src/security/tutorial006.py!} + ``` When you try to open the URL for the first time (or click the "Execute" button in the docs) the browser will ask you for your username and password: @@ -42,9 +59,26 @@ To handle that, we first convert the `username` and `password` to `bytes` encodi Then we can use `secrets.compare_digest()` to ensure that `credentials.username` is `"stanleyjobson"`, and that `credentials.password` is `"swordfish"`. -```Python hl_lines="1 11-21" -{!../../../docs_src/security/tutorial007.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="1 12-24" + {!> ../../../docs_src/security/tutorial007_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="1 12-24" + {!> ../../../docs_src/security/tutorial007_an_py39.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="1 11-21" + {!> ../../../docs_src/security/tutorial007.py!} + ``` This would be similar to: @@ -108,6 +142,23 @@ That way, using `secrets.compare_digest()` in your application code, it will be After detecting that the credentials are incorrect, return an `HTTPException` with a status code 401 (the same returned when no credentials are provided) and add the header `WWW-Authenticate` to make the browser show the login prompt again: -```Python hl_lines="23-27" -{!../../../docs_src/security/tutorial007.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="26-30" + {!> ../../../docs_src/security/tutorial007_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="26-30" + {!> ../../../docs_src/security/tutorial007_an_py39.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="23-27" + {!> ../../../docs_src/security/tutorial007.py!} + ``` diff --git a/docs/en/docs/advanced/security/oauth2-scopes.md b/docs/en/docs/advanced/security/oauth2-scopes.md index be51325cd..c216d7f50 100644 --- a/docs/en/docs/advanced/security/oauth2-scopes.md +++ b/docs/en/docs/advanced/security/oauth2-scopes.md @@ -56,9 +56,50 @@ They are normally used to declare specific security permissions, for example: First, let's quickly see the parts that change from the examples in the main **Tutorial - User Guide** for [OAuth2 with Password (and hashing), Bearer with JWT tokens](../../tutorial/security/oauth2-jwt.md){.internal-link target=_blank}. Now using OAuth2 scopes: -```Python hl_lines="2 4 8 12 46 64 105 107-115 121-124 128-134 139 153" -{!../../../docs_src/security/tutorial005.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="2 4 8 12 47 65 106 108-116 122-125 129-135 140 156" + {!> ../../../docs_src/security/tutorial005_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="2 4 8 12 46 64 105 107-115 121-124 128-134 139 155" + {!> ../../../docs_src/security/tutorial005_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="4 8 12 46 64 105 107-115 121-124 128-134 139 155" + {!> ../../../docs_src/security/tutorial005_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="2 4 8 12 46 64 105 107-115 121-124 128-134 139 153" + {!> ../../../docs_src/security/tutorial005.py!} + ``` + +=== "Python 3.9 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="2 4 8 12 46 64 105 107-115 121-124 128-134 139 153" + {!> ../../../docs_src/security/tutorial005_py39.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="3 7 11 45 63 104 106-114 120-123 127-133 138 152" + {!> ../../../docs_src/security/tutorial005_py310.py!} + ``` Now let's review those changes step by step. @@ -68,9 +109,50 @@ The first change is that now we are declaring the OAuth2 security scheme with tw The `scopes` parameter receives a `dict` with each scope as a key and the description as the value: -```Python hl_lines="62-65" -{!../../../docs_src/security/tutorial005.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="63-66" + {!> ../../../docs_src/security/tutorial005_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="62-65" + {!> ../../../docs_src/security/tutorial005_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="62-65" + {!> ../../../docs_src/security/tutorial005_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="62-65" + {!> ../../../docs_src/security/tutorial005.py!} + ``` + +=== "Python 3.9 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="62-65" + {!> ../../../docs_src/security/tutorial005_py39.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="61-64" + {!> ../../../docs_src/security/tutorial005_py310.py!} + ``` Because we are now declaring those scopes, they will show up in the API docs when you log-in/authorize. @@ -93,9 +175,50 @@ And we return the scopes as part of the JWT token. But in your application, for security, you should make sure you only add the scopes that the user is actually able to have, or the ones you have predefined. -```Python hl_lines="153" -{!../../../docs_src/security/tutorial005.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="156" + {!> ../../../docs_src/security/tutorial005_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="155" + {!> ../../../docs_src/security/tutorial005_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="155" + {!> ../../../docs_src/security/tutorial005_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="153" + {!> ../../../docs_src/security/tutorial005.py!} + ``` + +=== "Python 3.9 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="153" + {!> ../../../docs_src/security/tutorial005_py39.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="152" + {!> ../../../docs_src/security/tutorial005_py310.py!} + ``` ## Declare scopes in *path operations* and dependencies @@ -118,9 +241,50 @@ In this case, it requires the scope `me` (it could require more than one scope). We are doing it here to demonstrate how **FastAPI** handles scopes declared at different levels. -```Python hl_lines="4 139 166" -{!../../../docs_src/security/tutorial005.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="4 140 171" + {!> ../../../docs_src/security/tutorial005_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="4 139 170" + {!> ../../../docs_src/security/tutorial005_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="4 139 170" + {!> ../../../docs_src/security/tutorial005_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="4 139 166" + {!> ../../../docs_src/security/tutorial005.py!} + ``` + +=== "Python 3.9 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="4 139 166" + {!> ../../../docs_src/security/tutorial005_py39.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="3 138 165" + {!> ../../../docs_src/security/tutorial005_py310.py!} + ``` !!! info "Technical Details" `Security` is actually a subclass of `Depends`, and it has just one extra parameter that we'll see later. @@ -143,9 +307,50 @@ We also declare a special parameter of type `SecurityScopes`, imported from `fas This `SecurityScopes` class is similar to `Request` (`Request` was used to get the request object directly). -```Python hl_lines="8 105" -{!../../../docs_src/security/tutorial005.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="8 106" + {!> ../../../docs_src/security/tutorial005_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="8 105" + {!> ../../../docs_src/security/tutorial005_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="8 105" + {!> ../../../docs_src/security/tutorial005_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="8 105" + {!> ../../../docs_src/security/tutorial005.py!} + ``` + +=== "Python 3.9 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="8 105" + {!> ../../../docs_src/security/tutorial005_py39.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="7 104" + {!> ../../../docs_src/security/tutorial005_py310.py!} + ``` ## Use the `scopes` @@ -159,9 +364,50 @@ We create an `HTTPException` that we can re-use (`raise`) later at several point In this exception, we include the scopes required (if any) as a string separated by spaces (using `scope_str`). We put that string containing the scopes in the `WWW-Authenticate` header (this is part of the spec). -```Python hl_lines="105 107-115" -{!../../../docs_src/security/tutorial005.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="106 108-116" + {!> ../../../docs_src/security/tutorial005_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="105 107-115" + {!> ../../../docs_src/security/tutorial005_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="105 107-115" + {!> ../../../docs_src/security/tutorial005_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="105 107-115" + {!> ../../../docs_src/security/tutorial005.py!} + ``` + +=== "Python 3.9 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="105 107-115" + {!> ../../../docs_src/security/tutorial005_py39.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="104 106-114" + {!> ../../../docs_src/security/tutorial005_py310.py!} + ``` ## Verify the `username` and data shape @@ -177,9 +423,50 @@ Instead of, for example, a `dict`, or something else, as it could break the appl We also verify that we have a user with that username, and if not, we raise that same exception we created before. -```Python hl_lines="46 116-127" -{!../../../docs_src/security/tutorial005.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="47 117-128" + {!> ../../../docs_src/security/tutorial005_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="46 116-127" + {!> ../../../docs_src/security/tutorial005_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="46 116-127" + {!> ../../../docs_src/security/tutorial005_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="46 116-127" + {!> ../../../docs_src/security/tutorial005.py!} + ``` + +=== "Python 3.9 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="46 116-127" + {!> ../../../docs_src/security/tutorial005_py39.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="45 115-126" + {!> ../../../docs_src/security/tutorial005_py310.py!} + ``` ## Verify the `scopes` @@ -187,9 +474,50 @@ We now verify that all the scopes required, by this dependency and all the depen For this, we use `security_scopes.scopes`, that contains a `list` with all these scopes as `str`. -```Python hl_lines="128-134" -{!../../../docs_src/security/tutorial005.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="129-135" + {!> ../../../docs_src/security/tutorial005_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="128-134" + {!> ../../../docs_src/security/tutorial005_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="128-134" + {!> ../../../docs_src/security/tutorial005_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="128-134" + {!> ../../../docs_src/security/tutorial005.py!} + ``` + +=== "Python 3.9 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="128-134" + {!> ../../../docs_src/security/tutorial005_py39.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="127-133" + {!> ../../../docs_src/security/tutorial005_py310.py!} + ``` ## Dependency tree and scopes diff --git a/docs/en/docs/advanced/settings.md b/docs/en/docs/advanced/settings.md index d5151b585..52dbdf6fa 100644 --- a/docs/en/docs/advanced/settings.md +++ b/docs/en/docs/advanced/settings.md @@ -216,9 +216,26 @@ Notice that now we don't create a default instance `settings = Settings()`. Now we create a dependency that returns a new `config.Settings()`. -```Python hl_lines="5 11-12" -{!../../../docs_src/settings/app02/main.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="6 12-13" + {!> ../../../docs_src/settings/app02_an/main.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="6 12-13" + {!> ../../../docs_src/settings/app02_an_py39/main.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="5 11-12" + {!> ../../../docs_src/settings/app02/main.py!} + ``` !!! tip We'll discuss the `@lru_cache()` in a bit. @@ -227,9 +244,26 @@ Now we create a dependency that returns a new `config.Settings()`. And then we can require it from the *path operation function* as a dependency and use it anywhere we need it. -```Python hl_lines="16 18-20" -{!../../../docs_src/settings/app02/main.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="17 19-21" + {!> ../../../docs_src/settings/app02_an/main.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="17 19-21" + {!> ../../../docs_src/settings/app02_an_py39/main.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="16 18-20" + {!> ../../../docs_src/settings/app02/main.py!} + ``` ### Settings and testing @@ -304,9 +338,26 @@ we would create that object for each request, and we would be reading the `.env` But as we are using the `@lru_cache()` decorator on top, the `Settings` object will be created only once, the first time it's called. ✔️ -```Python hl_lines="1 10" -{!../../../docs_src/settings/app03/main.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="1 11" + {!> ../../../docs_src/settings/app03_an/main.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="1 11" + {!> ../../../docs_src/settings/app03_an_py39/main.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="1 10" + {!> ../../../docs_src/settings/app03/main.py!} + ``` Then for any subsequent calls of `get_settings()` in the dependencies for the next requests, instead of executing the internal code of `get_settings()` and creating a new `Settings` object, it will return the same object that was returned on the first call, again and again. diff --git a/docs/en/docs/advanced/testing-dependencies.md b/docs/en/docs/advanced/testing-dependencies.md index 7bba82fb7..c30dccd5d 100644 --- a/docs/en/docs/advanced/testing-dependencies.md +++ b/docs/en/docs/advanced/testing-dependencies.md @@ -28,9 +28,41 @@ To override a dependency for testing, you put as a key the original dependency ( And then **FastAPI** will call that override instead of the original dependency. -```Python hl_lines="28-29 32" -{!../../../docs_src/dependency_testing/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="29-30 33" + {!> ../../../docs_src/dependency_testing/tutorial001_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="28-29 32" + {!> ../../../docs_src/dependency_testing/tutorial001_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="26-27 30" + {!> ../../../docs_src/dependency_testing/tutorial001_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="28-29 32" + {!> ../../../docs_src/dependency_testing/tutorial001.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="24-25 28" + {!> ../../../docs_src/dependency_testing/tutorial001_py310.py!} + ``` !!! tip You can set a dependency override for a dependency used anywhere in your **FastAPI** application. diff --git a/docs/en/docs/advanced/websockets.md b/docs/en/docs/advanced/websockets.md index 3cf840819..8df32f4ca 100644 --- a/docs/en/docs/advanced/websockets.md +++ b/docs/en/docs/advanced/websockets.md @@ -112,9 +112,41 @@ In WebSocket endpoints you can import from `fastapi` and use: They work the same way as for other FastAPI endpoints/*path operations*: -```Python hl_lines="66-77 76-91" -{!../../../docs_src/websockets/tutorial002.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="69-70 83" + {!> ../../../docs_src/websockets/tutorial002_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="68-69 82" + {!> ../../../docs_src/websockets/tutorial002_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="68-69 82" + {!> ../../../docs_src/websockets/tutorial002_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="68-69 81" + {!> ../../../docs_src/websockets/tutorial002.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="66-67 79" + {!> ../../../docs_src/websockets/tutorial002_py310.py!} + ``` !!! info As this is a WebSocket it doesn't really make sense to raise an `HTTPException`, instead we raise a `WebSocketException`. @@ -153,9 +185,17 @@ With that you can connect the WebSocket and then send and receive messages: When a WebSocket connection is closed, the `await websocket.receive_text()` will raise a `WebSocketDisconnect` exception, which you can then catch and handle like in this example. -```Python hl_lines="81-83" -{!../../../docs_src/websockets/tutorial003.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="81-83" + {!> ../../../docs_src/websockets/tutorial003.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="79-81" + {!> ../../../docs_src/websockets/tutorial003_py39.py!} + ``` To try it out: diff --git a/docs/en/docs/python-types.md b/docs/en/docs/python-types.md index 3d22ee620..7ddfd41f2 100644 --- a/docs/en/docs/python-types.md +++ b/docs/en/docs/python-types.md @@ -1,8 +1,8 @@ # Python Types Intro -Python has support for optional "type hints". +Python has support for optional "type hints" (also called "type annotations"). -These **"type hints"** are a special syntax that allow declaring the type of a variable. +These **"type hints"** or annotations are a special syntax that allow declaring the type of a variable. By declaring types for your variables, editors and tools can give you better support. @@ -422,6 +422,10 @@ And then, again, you get all the editor support: +Notice that this means "`one_person` is an **instance** of the class `Person`". + +It doesn't mean "`one_person` is the **class** called `Person`". + ## Pydantic models Pydantic is a Python library to perform data validation. @@ -464,6 +468,43 @@ You will see a lot more of all this in practice in the [Tutorial - User Guide](t !!! tip Pydantic has a special behavior when you use `Optional` or `Union[Something, None]` without a default value, you can read more about it in the Pydantic docs about Required Optional fields. +## Type Hints with Metadata Annotations + +Python also has a feature that allows putting **additional metadata** in these type hints using `Annotated`. + +=== "Python 3.7 and above" + + In versions below Python 3.9, you import `Annotated` from `typing_extensions`. + + It will already be installed with **FastAPI**. + + ```Python hl_lines="1 4" + {!> ../../../docs_src/python_types/tutorial013.py!} + ``` + +=== "Python 3.9 and above" + + In Python 3.9, `Annotated` is part of the standard library, so you can import it from `typing`. + + ```Python hl_lines="1 4" + {!> ../../../docs_src/python_types/tutorial013_py39.py!} + ``` + +Python itself doesn't do anything with this `Annotated`. And for editors and other tools, the type is still `str`. + +But you can use this space in `Annotated` to provide **FastAPI** with additional metadata about how you want your application to behave. + +The important thing to remember is that **the first *type parameter*** you pass to `Annotated` is the **actual type**. The rest, is just metadata for other tools. + +For now, you just need to know that `Annotated` exists, and that it's standard Python. 😎 + +Later you will see how **powerful** it can be. + +!!! tip + The fact that this is **standard Python** means that you will still get the **best possible developer experience** in your editor, with the tools you use to analyze and refactor your code, etc. ✨ + + And also that your code will be very compatible with many other Python tools and libraries. 🚀 + ## Type hints in **FastAPI** **FastAPI** takes advantage of these type hints to do several things. diff --git a/docs/en/docs/tutorial/background-tasks.md b/docs/en/docs/tutorial/background-tasks.md index 191a4ca0f..909e2a72b 100644 --- a/docs/en/docs/tutorial/background-tasks.md +++ b/docs/en/docs/tutorial/background-tasks.md @@ -59,12 +59,36 @@ Using `BackgroundTasks` also works with the dependency injection system, you can === "Python 3.6 and above" + ```Python hl_lines="14 16 23 26" + {!> ../../../docs_src/background_tasks/tutorial002_an.py!} + ``` + +=== "Python 3.9 and above" + ```Python hl_lines="13 15 22 25" - {!> ../../../docs_src/background_tasks/tutorial002.py!} + {!> ../../../docs_src/background_tasks/tutorial002_an_py39.py!} ``` === "Python 3.10 and above" + ```Python hl_lines="13 15 22 25" + {!> ../../../docs_src/background_tasks/tutorial002_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="13 15 22 25" + {!> ../../../docs_src/background_tasks/tutorial002.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="11 13 20 23" {!> ../../../docs_src/background_tasks/tutorial002_py310.py!} ``` diff --git a/docs/en/docs/tutorial/bigger-applications.md b/docs/en/docs/tutorial/bigger-applications.md index 1de05a00a..9aeafe29e 100644 --- a/docs/en/docs/tutorial/bigger-applications.md +++ b/docs/en/docs/tutorial/bigger-applications.md @@ -112,9 +112,26 @@ So we put them in their own `dependencies` module (`app/dependencies.py`). We will now use a simple dependency to read a custom `X-Token` header: -```Python hl_lines="1 4-6" -{!../../../docs_src/bigger_applications/app/dependencies.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="1 5-7" + {!> ../../../docs_src/bigger_applications/app_an/dependencies.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="3 6-8" + {!> ../../../docs_src/bigger_applications/app_an_py39/dependencies.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="1 4-6" + {!> ../../../docs_src/bigger_applications/app/dependencies.py!} + ``` !!! tip We are using an invented header to simplify this example. diff --git a/docs/en/docs/tutorial/body-fields.md b/docs/en/docs/tutorial/body-fields.md index 0cfe576d6..301ee84f2 100644 --- a/docs/en/docs/tutorial/body-fields.md +++ b/docs/en/docs/tutorial/body-fields.md @@ -9,11 +9,35 @@ First, you have to import it: === "Python 3.6 and above" ```Python hl_lines="4" - {!> ../../../docs_src/body_fields/tutorial001.py!} + {!> ../../../docs_src/body_fields/tutorial001_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="4" + {!> ../../../docs_src/body_fields/tutorial001_an_py39.py!} ``` === "Python 3.10 and above" + ```Python hl_lines="4" + {!> ../../../docs_src/body_fields/tutorial001_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="4" + {!> ../../../docs_src/body_fields/tutorial001.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="2" {!> ../../../docs_src/body_fields/tutorial001_py310.py!} ``` @@ -27,12 +51,36 @@ You can then use `Field` with model attributes: === "Python 3.6 and above" + ```Python hl_lines="12-15" + {!> ../../../docs_src/body_fields/tutorial001_an.py!} + ``` + +=== "Python 3.9 and above" + ```Python hl_lines="11-14" - {!> ../../../docs_src/body_fields/tutorial001.py!} + {!> ../../../docs_src/body_fields/tutorial001_an_py39.py!} ``` === "Python 3.10 and above" + ```Python hl_lines="11-14" + {!> ../../../docs_src/body_fields/tutorial001_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="11-14" + {!> ../../../docs_src/body_fields/tutorial001.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="9-12" {!> ../../../docs_src/body_fields/tutorial001_py310.py!} ``` diff --git a/docs/en/docs/tutorial/body-multiple-params.md b/docs/en/docs/tutorial/body-multiple-params.md index 31dd27fed..1a6b572dc 100644 --- a/docs/en/docs/tutorial/body-multiple-params.md +++ b/docs/en/docs/tutorial/body-multiple-params.md @@ -11,11 +11,35 @@ And you can also declare body parameters as optional, by setting the default to === "Python 3.6 and above" ```Python hl_lines="19-21" - {!> ../../../docs_src/body_multiple_params/tutorial001.py!} + {!> ../../../docs_src/body_multiple_params/tutorial001_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="18-20" + {!> ../../../docs_src/body_multiple_params/tutorial001_an_py39.py!} ``` === "Python 3.10 and above" + ```Python hl_lines="18-20" + {!> ../../../docs_src/body_multiple_params/tutorial001_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="19-21" + {!> ../../../docs_src/body_multiple_params/tutorial001.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="17-19" {!> ../../../docs_src/body_multiple_params/tutorial001_py310.py!} ``` @@ -89,11 +113,35 @@ But you can instruct **FastAPI** to treat it as another body key using `Body`: === "Python 3.6 and above" + ```Python hl_lines="24" + {!> ../../../docs_src/body_multiple_params/tutorial003_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="23" + {!> ../../../docs_src/body_multiple_params/tutorial003_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="23" + {!> ../../../docs_src/body_multiple_params/tutorial003_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="22" {!> ../../../docs_src/body_multiple_params/tutorial003.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. ```Python hl_lines="20" {!> ../../../docs_src/body_multiple_params/tutorial003_py310.py!} @@ -139,13 +187,37 @@ For example: === "Python 3.6 and above" + ```Python hl_lines="28" + {!> ../../../docs_src/body_multiple_params/tutorial004_an.py!} + ``` + +=== "Python 3.9 and above" + ```Python hl_lines="27" - {!> ../../../docs_src/body_multiple_params/tutorial004.py!} + {!> ../../../docs_src/body_multiple_params/tutorial004_an_py39.py!} ``` === "Python 3.10 and above" - ```Python hl_lines="26" + ```Python hl_lines="27" + {!> ../../../docs_src/body_multiple_params/tutorial004_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="27" + {!> ../../../docs_src/body_multiple_params/tutorial004.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="25" {!> ../../../docs_src/body_multiple_params/tutorial004_py310.py!} ``` @@ -168,12 +240,36 @@ as in: === "Python 3.6 and above" + ```Python hl_lines="18" + {!> ../../../docs_src/body_multiple_params/tutorial005_an.py!} + ``` + +=== "Python 3.9 and above" + ```Python hl_lines="17" - {!> ../../../docs_src/body_multiple_params/tutorial005.py!} + {!> ../../../docs_src/body_multiple_params/tutorial005_an_py39.py!} ``` === "Python 3.10 and above" + ```Python hl_lines="17" + {!> ../../../docs_src/body_multiple_params/tutorial005_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="17" + {!> ../../../docs_src/body_multiple_params/tutorial005.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="15" {!> ../../../docs_src/body_multiple_params/tutorial005_py310.py!} ``` diff --git a/docs/en/docs/tutorial/cookie-params.md b/docs/en/docs/tutorial/cookie-params.md index 221cdfffb..2aab8d12d 100644 --- a/docs/en/docs/tutorial/cookie-params.md +++ b/docs/en/docs/tutorial/cookie-params.md @@ -9,11 +9,35 @@ First import `Cookie`: === "Python 3.6 and above" ```Python hl_lines="3" - {!> ../../../docs_src/cookie_params/tutorial001.py!} + {!> ../../../docs_src/cookie_params/tutorial001_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="3" + {!> ../../../docs_src/cookie_params/tutorial001_an_py39.py!} ``` === "Python 3.10 and above" + ```Python hl_lines="3" + {!> ../../../docs_src/cookie_params/tutorial001_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="3" + {!> ../../../docs_src/cookie_params/tutorial001.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="1" {!> ../../../docs_src/cookie_params/tutorial001_py310.py!} ``` @@ -26,12 +50,36 @@ The first value is the default value, you can pass all the extra validation or a === "Python 3.6 and above" + ```Python hl_lines="10" + {!> ../../../docs_src/cookie_params/tutorial001_an.py!} + ``` + +=== "Python 3.9 and above" + ```Python hl_lines="9" - {!> ../../../docs_src/cookie_params/tutorial001.py!} + {!> ../../../docs_src/cookie_params/tutorial001_an_py39.py!} ``` === "Python 3.10 and above" + ```Python hl_lines="9" + {!> ../../../docs_src/cookie_params/tutorial001_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="9" + {!> ../../../docs_src/cookie_params/tutorial001.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="7" {!> ../../../docs_src/cookie_params/tutorial001_py310.py!} ``` diff --git a/docs/en/docs/tutorial/dependencies/classes-as-dependencies.md b/docs/en/docs/tutorial/dependencies/classes-as-dependencies.md index fb41ba1f6..4fa05c98e 100644 --- a/docs/en/docs/tutorial/dependencies/classes-as-dependencies.md +++ b/docs/en/docs/tutorial/dependencies/classes-as-dependencies.md @@ -8,11 +8,35 @@ In the previous example, we were returning a `dict` from our dependency ("depend === "Python 3.6 and above" + ```Python hl_lines="12" + {!> ../../../docs_src/dependencies/tutorial001_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="11" + {!> ../../../docs_src/dependencies/tutorial001_an_py39.py!} + ``` + +=== "Python 3.10 and above" + ```Python hl_lines="9" + {!> ../../../docs_src/dependencies/tutorial001_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="11" {!> ../../../docs_src/dependencies/tutorial001.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. ```Python hl_lines="7" {!> ../../../docs_src/dependencies/tutorial001_py310.py!} @@ -81,12 +105,36 @@ Then, we can change the dependency "dependable" `common_parameters` from above t === "Python 3.6 and above" + ```Python hl_lines="12-16" + {!> ../../../docs_src/dependencies/tutorial002_an.py!} + ``` + +=== "Python 3.9 and above" + ```Python hl_lines="11-15" - {!> ../../../docs_src/dependencies/tutorial002.py!} + {!> ../../../docs_src/dependencies/tutorial002_an_py39.py!} ``` === "Python 3.10 and above" + ```Python hl_lines="11-15" + {!> ../../../docs_src/dependencies/tutorial002_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="11-15" + {!> ../../../docs_src/dependencies/tutorial002.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="9-13" {!> ../../../docs_src/dependencies/tutorial002_py310.py!} ``` @@ -95,12 +143,36 @@ Pay attention to the `__init__` method used to create the instance of the class: === "Python 3.6 and above" + ```Python hl_lines="13" + {!> ../../../docs_src/dependencies/tutorial002_an.py!} + ``` + +=== "Python 3.9 and above" + ```Python hl_lines="12" - {!> ../../../docs_src/dependencies/tutorial002.py!} + {!> ../../../docs_src/dependencies/tutorial002_an_py39.py!} ``` === "Python 3.10 and above" + ```Python hl_lines="12" + {!> ../../../docs_src/dependencies/tutorial002_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="12" + {!> ../../../docs_src/dependencies/tutorial002.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="10" {!> ../../../docs_src/dependencies/tutorial002_py310.py!} ``` @@ -109,12 +181,36 @@ Pay attention to the `__init__` method used to create the instance of the class: === "Python 3.6 and above" + ```Python hl_lines="10" + {!> ../../../docs_src/dependencies/tutorial001_an.py!} + ``` + +=== "Python 3.9 and above" + ```Python hl_lines="9" - {!> ../../../docs_src/dependencies/tutorial001.py!} + {!> ../../../docs_src/dependencies/tutorial001_an_py39.py!} ``` === "Python 3.10 and above" + ```Python hl_lines="8" + {!> ../../../docs_src/dependencies/tutorial001_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="9" + {!> ../../../docs_src/dependencies/tutorial001.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="6" {!> ../../../docs_src/dependencies/tutorial001_py310.py!} ``` @@ -133,14 +229,39 @@ In both cases the data will be converted, validated, documented on the OpenAPI s Now you can declare your dependency using this class. + === "Python 3.6 and above" + ```Python hl_lines="20" + {!> ../../../docs_src/dependencies/tutorial002_an.py!} + ``` + +=== "Python 3.9 and above" + ```Python hl_lines="19" - {!> ../../../docs_src/dependencies/tutorial002.py!} + {!> ../../../docs_src/dependencies/tutorial002_an_py39.py!} ``` === "Python 3.10 and above" + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial002_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial002.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="17" {!> ../../../docs_src/dependencies/tutorial002_py310.py!} ``` @@ -151,14 +272,25 @@ Now you can declare your dependency using this class. Notice how we write `CommonQueryParams` twice in the above code: -```Python -commons: CommonQueryParams = Depends(CommonQueryParams) -``` +=== "Python 3.6 and above" + + ```Python + commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python + commons: CommonQueryParams = Depends(CommonQueryParams) + ``` The last `CommonQueryParams`, in: ```Python -... = Depends(CommonQueryParams) +... Depends(CommonQueryParams) ``` ...is what **FastAPI** will actually use to know what is the dependency. @@ -169,28 +301,74 @@ From it is that FastAPI will extract the declared parameters and that is what Fa In this case, the first `CommonQueryParams`, in: -```Python -commons: CommonQueryParams ... -``` +=== "Python 3.6 and above" + + ```Python + commons: Annotated[CommonQueryParams, ... + ``` + +=== "Python 3.6 and above - non-Annotated" -...doesn't have any special meaning for **FastAPI**. FastAPI won't use it for data conversion, validation, etc. (as it is using the `= Depends(CommonQueryParams)` for that). + !!! tip + Try to use the main, `Annotated` version better. + + ```Python + commons: CommonQueryParams ... + ``` + +...doesn't have any special meaning for **FastAPI**. FastAPI won't use it for data conversion, validation, etc. (as it is using the `Depends(CommonQueryParams)` for that). You could actually write just: -```Python -commons = Depends(CommonQueryParams) -``` +=== "Python 3.6 and above" + + ```Python + commons: Annotated[Any, Depends(CommonQueryParams)] + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python + commons = Depends(CommonQueryParams) + ``` ..as in: === "Python 3.6 and above" + ```Python hl_lines="20" + {!> ../../../docs_src/dependencies/tutorial003_an.py!} + ``` + +=== "Python 3.9 and above" + ```Python hl_lines="19" - {!> ../../../docs_src/dependencies/tutorial003.py!} + {!> ../../../docs_src/dependencies/tutorial003_an_py39.py!} ``` === "Python 3.10 and above" + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial003_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial003.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="17" {!> ../../../docs_src/dependencies/tutorial003_py310.py!} ``` @@ -203,9 +381,20 @@ But declaring the type is encouraged as that way your editor will know what will But you see that we are having some code repetition here, writing `CommonQueryParams` twice: -```Python -commons: CommonQueryParams = Depends(CommonQueryParams) -``` +=== "Python 3.6 and above" + + ```Python + commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python + commons: CommonQueryParams = Depends(CommonQueryParams) + ``` **FastAPI** provides a shortcut for these cases, in where the dependency is *specifically* a class that **FastAPI** will "call" to create an instance of the class itself. @@ -213,28 +402,74 @@ For those specific cases, you can do the following: Instead of writing: -```Python -commons: CommonQueryParams = Depends(CommonQueryParams) -``` +=== "Python 3.6 and above" + + ```Python + commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python + commons: CommonQueryParams = Depends(CommonQueryParams) + ``` ...you write: -```Python -commons: CommonQueryParams = Depends() -``` +=== "Python 3.6 and above" + + ```Python + commons: Annotated[CommonQueryParams, Depends()] + ``` -You declare the dependency as the type of the parameter, and you use `Depends()` as its "default" value (that after the `=`) for that function's parameter, without any parameter in `Depends()`, instead of having to write the full class *again* inside of `Depends(CommonQueryParams)`. +=== "Python 3.6 - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python + commons: CommonQueryParams = Depends() + ``` + +You declare the dependency as the type of the parameter, and you use `Depends()` without any parameter, instead of having to write the full class *again* inside of `Depends(CommonQueryParams)`. The same example would then look like: === "Python 3.6 and above" + ```Python hl_lines="20" + {!> ../../../docs_src/dependencies/tutorial004_an.py!} + ``` + +=== "Python 3.9 and above" + ```Python hl_lines="19" - {!> ../../../docs_src/dependencies/tutorial004.py!} + {!> ../../../docs_src/dependencies/tutorial004_an_py39.py!} ``` === "Python 3.10 and above" + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial004_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial004.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="17" {!> ../../../docs_src/dependencies/tutorial004_py310.py!} ``` diff --git a/docs/en/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md b/docs/en/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md index a1bbcc6c7..d3a11c149 100644 --- a/docs/en/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md +++ b/docs/en/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md @@ -14,9 +14,26 @@ The *path operation decorator* receives an optional argument `dependencies`. It should be a `list` of `Depends()`: -```Python hl_lines="17" -{!../../../docs_src/dependencies/tutorial006.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="18" + {!> ../../../docs_src/dependencies/tutorial006_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial006_an_py39.py!} + ``` + +=== "Python 3.6 - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="17" + {!> ../../../docs_src/dependencies/tutorial006.py!} + ``` These dependencies will be executed/solved the same way normal dependencies. But their value (if they return any) won't be passed to your *path operation function*. @@ -40,17 +57,51 @@ You can use the same dependency *functions* you use normally. They can declare request requirements (like headers) or other sub-dependencies: -```Python hl_lines="6 11" -{!../../../docs_src/dependencies/tutorial006.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="7 12" + {!> ../../../docs_src/dependencies/tutorial006_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="8 13" + {!> ../../../docs_src/dependencies/tutorial006_an_py39.py!} + ``` + +=== "Python 3.6 - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="6 11" + {!> ../../../docs_src/dependencies/tutorial006.py!} + ``` ### Raise exceptions These dependencies can `raise` exceptions, the same as normal dependencies: -```Python hl_lines="8 13" -{!../../../docs_src/dependencies/tutorial006.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="9 14" + {!> ../../../docs_src/dependencies/tutorial006_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="10 15" + {!> ../../../docs_src/dependencies/tutorial006_an_py39.py!} + ``` + +=== "Python 3.6 - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="8 13" + {!> ../../../docs_src/dependencies/tutorial006.py!} + ``` ### Return values @@ -58,9 +109,26 @@ And they can return values or not, the values won't be used. So, you can re-use a normal dependency (that returns a value) you already use somewhere else, and even though the value won't be used, the dependency will be executed: -```Python hl_lines="9 14" -{!../../../docs_src/dependencies/tutorial006.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="10 15" + {!> ../../../docs_src/dependencies/tutorial006_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="11 16" + {!> ../../../docs_src/dependencies/tutorial006_an_py39.py!} + ``` + +=== "Python 3.6 - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="9 14" + {!> ../../../docs_src/dependencies/tutorial006.py!} + ``` ## Dependencies for a group of *path operations* diff --git a/docs/en/docs/tutorial/dependencies/dependencies-with-yield.md b/docs/en/docs/tutorial/dependencies/dependencies-with-yield.md index a7300f0b7..d0c263f40 100644 --- a/docs/en/docs/tutorial/dependencies/dependencies-with-yield.md +++ b/docs/en/docs/tutorial/dependencies/dependencies-with-yield.md @@ -66,9 +66,26 @@ You can have sub-dependencies and "trees" of sub-dependencies of any size and sh For example, `dependency_c` can have a dependency on `dependency_b`, and `dependency_b` on `dependency_a`: -```Python hl_lines="4 12 20" -{!../../../docs_src/dependencies/tutorial008.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="5 13 21" + {!> ../../../docs_src/dependencies/tutorial008_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="6 14 22" + {!> ../../../docs_src/dependencies/tutorial008_an_py39.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="4 12 20" + {!> ../../../docs_src/dependencies/tutorial008.py!} + ``` And all of them can use `yield`. @@ -76,9 +93,26 @@ In this case `dependency_c`, to execute its exit code, needs the value from `dep And, in turn, `dependency_b` needs the value from `dependency_a` (here named `dep_a`) to be available for its exit code. -```Python hl_lines="16-17 24-25" -{!../../../docs_src/dependencies/tutorial008.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="17-18 25-26" + {!> ../../../docs_src/dependencies/tutorial008_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="18-19 26-27" + {!> ../../../docs_src/dependencies/tutorial008_an_py39.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="16-17 24-25" + {!> ../../../docs_src/dependencies/tutorial008.py!} + ``` The same way, you could have dependencies with `yield` and `return` mixed. diff --git a/docs/en/docs/tutorial/dependencies/global-dependencies.md b/docs/en/docs/tutorial/dependencies/global-dependencies.md index bcd61d52b..9ad503d8f 100644 --- a/docs/en/docs/tutorial/dependencies/global-dependencies.md +++ b/docs/en/docs/tutorial/dependencies/global-dependencies.md @@ -6,9 +6,27 @@ Similar to the way you can [add `dependencies` to the *path operation decorators In that case, they will be applied to all the *path operations* in the application: -```Python hl_lines="15" -{!../../../docs_src/dependencies/tutorial012.py!} -``` + +=== "Python 3.6 and above" + + ```Python hl_lines="16" + {!> ../../../docs_src/dependencies/tutorial012_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="16" + {!> ../../../docs_src/dependencies/tutorial012_an_py39.py!} + ``` + +=== "Python 3.6 - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="15" + {!> ../../../docs_src/dependencies/tutorial012.py!} + ``` And all the ideas in the section about [adding `dependencies` to the *path operation decorators*](dependencies-in-path-operation-decorators.md){.internal-link target=_blank} still apply, but in this case, to all of the *path operations* in the app. diff --git a/docs/en/docs/tutorial/dependencies/index.md b/docs/en/docs/tutorial/dependencies/index.md index 5078c0096..5675b8dfd 100644 --- a/docs/en/docs/tutorial/dependencies/index.md +++ b/docs/en/docs/tutorial/dependencies/index.md @@ -33,12 +33,36 @@ It is just a function that can take all the same parameters that a *path operati === "Python 3.6 and above" + ```Python hl_lines="9-12" + {!> ../../../docs_src/dependencies/tutorial001_an.py!} + ``` + +=== "Python 3.9 and above" + ```Python hl_lines="8-11" - {!> ../../../docs_src/dependencies/tutorial001.py!} + {!> ../../../docs_src/dependencies/tutorial001_an_py39.py!} ``` === "Python 3.10 and above" + ```Python hl_lines="8-9" + {!> ../../../docs_src/dependencies/tutorial001_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="8-11" + {!> ../../../docs_src/dependencies/tutorial001.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="6-7" {!> ../../../docs_src/dependencies/tutorial001_py310.py!} ``` @@ -66,11 +90,35 @@ And then it just returns a `dict` containing those values. === "Python 3.6 and above" ```Python hl_lines="3" - {!> ../../../docs_src/dependencies/tutorial001.py!} + {!> ../../../docs_src/dependencies/tutorial001_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="3" + {!> ../../../docs_src/dependencies/tutorial001_an_py39.py!} ``` === "Python 3.10 and above" + ```Python hl_lines="3" + {!> ../../../docs_src/dependencies/tutorial001_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="3" + {!> ../../../docs_src/dependencies/tutorial001.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="1" {!> ../../../docs_src/dependencies/tutorial001_py310.py!} ``` @@ -81,12 +129,36 @@ The same way you use `Body`, `Query`, etc. with your *path operation function* p === "Python 3.6 and above" + ```Python hl_lines="16 21" + {!> ../../../docs_src/dependencies/tutorial001_an.py!} + ``` + +=== "Python 3.9 and above" + ```Python hl_lines="15 20" - {!> ../../../docs_src/dependencies/tutorial001.py!} + {!> ../../../docs_src/dependencies/tutorial001_an_py39.py!} ``` === "Python 3.10 and above" + ```Python hl_lines="13 18" + {!> ../../../docs_src/dependencies/tutorial001_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="15 20" + {!> ../../../docs_src/dependencies/tutorial001.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="11 16" {!> ../../../docs_src/dependencies/tutorial001_py310.py!} ``` @@ -97,6 +169,8 @@ You only give `Depends` a single parameter. This parameter must be something like a function. +You **don't call it** directly (don't add the parenthesis at the end), you just pass it as a parameter to `Depends()`. + And that function takes parameters in the same way that *path operation functions* do. !!! tip @@ -126,6 +200,45 @@ This way you write shared code once and **FastAPI** takes care of calling it for You just pass it to `Depends` and **FastAPI** knows how to do the rest. +## Share `Annotated` dependencies + +In the examples above, you see that there's a tiny bit of **code duplication**. + +When you need to use the `common_parameters()` dependency, you have to write the whole parameter with the type annotation and `Depends()`: + +```Python +commons: Annotated[dict, Depends(common_parameters)] +``` + +But because we are using `Annotated`, we can store that `Annotated` value in a variable and use it in multiple places: + +=== "Python 3.6 and above" + + ```Python hl_lines="15 19 24" + {!> ../../../docs_src/dependencies/tutorial001_02_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="14 18 23" + {!> ../../../docs_src/dependencies/tutorial001_02_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="12 16 21" + {!> ../../../docs_src/dependencies/tutorial001_02_an_py310.py!} + ``` + +!!! tip + This is just standard Python, it's called a "type alias", it's actually not specific to **FastAPI**. + + But because **FastAPI** is based on the Python standards, including `Annotated`, you can use this trick in your code. 😎 + +The dependencies will keep working as expected, and the **best part** is that the **type information will be preserved**, which means that your editor will be able to keep providing you with **autocompletion**, **inline errors**, etc. The same for other tools like `mypy`. + +This will be especially useful when you use it in a **large code base** where you use **the same dependencies** over and over again in **many *path operations***. + ## To `async` or not to `async` As dependencies will also be called by **FastAPI** (the same as your *path operation functions*), the same rules apply while defining your functions. diff --git a/docs/en/docs/tutorial/dependencies/sub-dependencies.md b/docs/en/docs/tutorial/dependencies/sub-dependencies.md index a5b40c9ad..8b70e2602 100644 --- a/docs/en/docs/tutorial/dependencies/sub-dependencies.md +++ b/docs/en/docs/tutorial/dependencies/sub-dependencies.md @@ -12,12 +12,36 @@ You could create a first dependency ("dependable") like: === "Python 3.6 and above" + ```Python hl_lines="9-10" + {!> ../../../docs_src/dependencies/tutorial005_an.py!} + ``` + +=== "Python 3.9 and above" + ```Python hl_lines="8-9" - {!> ../../../docs_src/dependencies/tutorial005.py!} + {!> ../../../docs_src/dependencies/tutorial005_an_py39.py!} ``` === "Python 3.10 and above" + ```Python hl_lines="8-9" + {!> ../../../docs_src/dependencies/tutorial005_an_py310.py!} + ``` + +=== "Python 3.6 - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="8-9" + {!> ../../../docs_src/dependencies/tutorial005.py!} + ``` + +=== "Python 3.10 - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="6-7" {!> ../../../docs_src/dependencies/tutorial005_py310.py!} ``` @@ -32,12 +56,36 @@ Then you can create another dependency function (a "dependable") that at the sam === "Python 3.6 and above" + ```Python hl_lines="14" + {!> ../../../docs_src/dependencies/tutorial005_an.py!} + ``` + +=== "Python 3.9 and above" + ```Python hl_lines="13" - {!> ../../../docs_src/dependencies/tutorial005.py!} + {!> ../../../docs_src/dependencies/tutorial005_an_py39.py!} ``` === "Python 3.10 and above" + ```Python hl_lines="13" + {!> ../../../docs_src/dependencies/tutorial005_an_py310.py!} + ``` + +=== "Python 3.6 - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="13" + {!> ../../../docs_src/dependencies/tutorial005.py!} + ``` + +=== "Python 3.10 - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="11" {!> ../../../docs_src/dependencies/tutorial005_py310.py!} ``` @@ -55,11 +103,35 @@ Then we can use the dependency with: === "Python 3.6 and above" + ```Python hl_lines="24" + {!> ../../../docs_src/dependencies/tutorial005_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="23" + {!> ../../../docs_src/dependencies/tutorial005_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="23" + {!> ../../../docs_src/dependencies/tutorial005_an_py310.py!} + ``` + +=== "Python 3.6 - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="22" {!> ../../../docs_src/dependencies/tutorial005.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.10 - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. ```Python hl_lines="19" {!> ../../../docs_src/dependencies/tutorial005_py310.py!} @@ -89,10 +161,22 @@ And it will save the returned value in a ../../../docs_src/extra_data_types/tutorial001_an.py!} + ``` + +=== "Python 3.9 and above" + ```Python hl_lines="1 3 12-16" - {!> ../../../docs_src/extra_data_types/tutorial001.py!} + {!> ../../../docs_src/extra_data_types/tutorial001_an_py39.py!} ``` === "Python 3.10 and above" + ```Python hl_lines="1 3 12-16" + {!> ../../../docs_src/extra_data_types/tutorial001_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="1 2 12-16" + {!> ../../../docs_src/extra_data_types/tutorial001.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="1 2 11-15" {!> ../../../docs_src/extra_data_types/tutorial001_py310.py!} ``` @@ -71,12 +95,36 @@ Note that the parameters inside the function have their natural data type, and y === "Python 3.6 and above" + ```Python hl_lines="19-20" + {!> ../../../docs_src/extra_data_types/tutorial001_an.py!} + ``` + +=== "Python 3.9 and above" + ```Python hl_lines="18-19" - {!> ../../../docs_src/extra_data_types/tutorial001.py!} + {!> ../../../docs_src/extra_data_types/tutorial001_an_py39.py!} ``` === "Python 3.10 and above" + ```Python hl_lines="18-19" + {!> ../../../docs_src/extra_data_types/tutorial001_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="18-19" + {!> ../../../docs_src/extra_data_types/tutorial001.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="17-18" {!> ../../../docs_src/extra_data_types/tutorial001_py310.py!} ``` diff --git a/docs/en/docs/tutorial/header-params.md b/docs/en/docs/tutorial/header-params.md index 8e294416c..475359134 100644 --- a/docs/en/docs/tutorial/header-params.md +++ b/docs/en/docs/tutorial/header-params.md @@ -9,11 +9,35 @@ First import `Header`: === "Python 3.6 and above" ```Python hl_lines="3" - {!> ../../../docs_src/header_params/tutorial001.py!} + {!> ../../../docs_src/header_params/tutorial001_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="3" + {!> ../../../docs_src/header_params/tutorial001_an_py39.py!} ``` === "Python 3.10 and above" + ```Python hl_lines="3" + {!> ../../../docs_src/header_params/tutorial001_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="3" + {!> ../../../docs_src/header_params/tutorial001.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="1" {!> ../../../docs_src/header_params/tutorial001_py310.py!} ``` @@ -26,12 +50,36 @@ The first value is the default value, you can pass all the extra validation or a === "Python 3.6 and above" + ```Python hl_lines="10" + {!> ../../../docs_src/header_params/tutorial001_an.py!} + ``` + +=== "Python 3.9 and above" + ```Python hl_lines="9" - {!> ../../../docs_src/header_params/tutorial001.py!} + {!> ../../../docs_src/header_params/tutorial001_an_py39.py!} ``` === "Python 3.10 and above" + ```Python hl_lines="9" + {!> ../../../docs_src/header_params/tutorial001_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="9" + {!> ../../../docs_src/header_params/tutorial001.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="7" {!> ../../../docs_src/header_params/tutorial001_py310.py!} ``` @@ -62,11 +110,35 @@ If for some reason you need to disable automatic conversion of underscores to hy === "Python 3.6 and above" + ```Python hl_lines="12" + {!> ../../../docs_src/header_params/tutorial002_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="11" + {!> ../../../docs_src/header_params/tutorial002_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="10" + {!> ../../../docs_src/header_params/tutorial002_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="10" {!> ../../../docs_src/header_params/tutorial002.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. ```Python hl_lines="8" {!> ../../../docs_src/header_params/tutorial002_py310.py!} @@ -87,17 +159,44 @@ For example, to declare a header of `X-Token` that can appear more than once, yo === "Python 3.6 and above" + ```Python hl_lines="10" + {!> ../../../docs_src/header_params/tutorial003_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/header_params/tutorial003_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/header_params/tutorial003_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="9" {!> ../../../docs_src/header_params/tutorial003.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.9 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. ```Python hl_lines="9" {!> ../../../docs_src/header_params/tutorial003_py39.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. ```Python hl_lines="7" {!> ../../../docs_src/header_params/tutorial003_py310.py!} diff --git a/docs/en/docs/tutorial/path-params-numeric-validations.md b/docs/en/docs/tutorial/path-params-numeric-validations.md index cec54b0fb..c6f158307 100644 --- a/docs/en/docs/tutorial/path-params-numeric-validations.md +++ b/docs/en/docs/tutorial/path-params-numeric-validations.md @@ -4,15 +4,39 @@ In the same way that you can declare more validations and metadata for query par ## Import Path -First, import `Path` from `fastapi`: +First, import `Path` from `fastapi`, and import `Annotated`: === "Python 3.6 and above" + ```Python hl_lines="3-4" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="1 3" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="1 3" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="3" {!> ../../../docs_src/path_params_numeric_validations/tutorial001.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. ```Python hl_lines="1" {!> ../../../docs_src/path_params_numeric_validations/tutorial001_py310.py!} @@ -26,12 +50,36 @@ For example, to declare a `title` metadata value for the path parameter `item_id === "Python 3.6 and above" + ```Python hl_lines="11" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_an.py!} + ``` + +=== "Python 3.9 and above" + ```Python hl_lines="10" - {!> ../../../docs_src/path_params_numeric_validations/tutorial001.py!} + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_an_py39.py!} ``` === "Python 3.10 and above" + ```Python hl_lines="10" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="10" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="8" {!> ../../../docs_src/path_params_numeric_validations/tutorial001_py310.py!} ``` @@ -45,11 +93,14 @@ For example, to declare a `title` metadata value for the path parameter `item_id ## Order the parameters as you need +!!! tip + This is probably not as important or necessary if you use `Annotated`. + Let's say that you want to declare the query parameter `q` as a required `str`. And you don't need to declare anything else for that parameter, so you don't really need to use `Query`. -But you still need to use `Path` for the `item_id` path parameter. +But you still need to use `Path` for the `item_id` path parameter. And you don't want to use `Annotated` for some reason. Python will complain if you put a value with a "default" before a value that doesn't have a "default". @@ -59,13 +110,44 @@ It doesn't matter for **FastAPI**. It will detect the parameters by their names, So, you can declare your function as: -```Python hl_lines="7" -{!../../../docs_src/path_params_numeric_validations/tutorial002.py!} -``` +=== "Python 3.6 - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="7" + {!> ../../../docs_src/path_params_numeric_validations/tutorial002.py!} + ``` + +But have in mind that if you use `Annotated`, you won't have this problem, it won't matter as you're not using the function parameter default values for `Query()` or `Path()`. + +=== "Python 3.6 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/path_params_numeric_validations/tutorial002_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="10" + {!> ../../../docs_src/path_params_numeric_validations/tutorial002_an_py39.py!} + ``` ## Order the parameters as you need, tricks -If you want to declare the `q` query parameter without a `Query` nor any default value, and the path parameter `item_id` using `Path`, and have them in a different order, Python has a little special syntax for that. +!!! tip + This is probably not as important or necessary if you use `Annotated`. + +Here's a **small trick** that can be handy, but you won't need it often. + +If you want to: + +* declare the `q` query parameter without a `Query` nor any default value +* declare the path parameter `item_id` using `Path` +* have them in a different order +* not use `Annotated` + +...Python has a little special syntax for that. Pass `*`, as the first parameter of the function. @@ -75,15 +157,48 @@ Python won't do anything with that `*`, but it will know that all the following {!../../../docs_src/path_params_numeric_validations/tutorial003.py!} ``` +### Better with `Annotated` + +Have in mind that if you use `Annotated`, as you are not using function parameter default values, you won't have this problem, and yo probably won't need to use `*`. + +=== "Python 3.6 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/path_params_numeric_validations/tutorial003_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="10" + {!> ../../../docs_src/path_params_numeric_validations/tutorial003_an_py39.py!} + ``` + ## Number validations: greater than or equal With `Query` and `Path` (and others you'll see later) you can declare number constraints. Here, with `ge=1`, `item_id` will need to be an integer number "`g`reater than or `e`qual" to `1`. -```Python hl_lines="8" -{!../../../docs_src/path_params_numeric_validations/tutorial004.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/path_params_numeric_validations/tutorial004_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="10" + {!> ../../../docs_src/path_params_numeric_validations/tutorial004_an_py39.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="8" + {!> ../../../docs_src/path_params_numeric_validations/tutorial004.py!} + ``` ## Number validations: greater than and less than or equal @@ -92,9 +207,26 @@ The same applies for: * `gt`: `g`reater `t`han * `le`: `l`ess than or `e`qual -```Python hl_lines="9" -{!../../../docs_src/path_params_numeric_validations/tutorial005.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/path_params_numeric_validations/tutorial005_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="10" + {!> ../../../docs_src/path_params_numeric_validations/tutorial005_an_py39.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="9" + {!> ../../../docs_src/path_params_numeric_validations/tutorial005.py!} + ``` ## Number validations: floats, greater than and less than @@ -106,9 +238,26 @@ So, `0.5` would be a valid value. But `0.0` or `0` would not. And the same for lt. -```Python hl_lines="11" -{!../../../docs_src/path_params_numeric_validations/tutorial006.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="12" + {!> ../../../docs_src/path_params_numeric_validations/tutorial006_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="13" + {!> ../../../docs_src/path_params_numeric_validations/tutorial006_an_py39.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="11" + {!> ../../../docs_src/path_params_numeric_validations/tutorial006.py!} + ``` ## Recap diff --git a/docs/en/docs/tutorial/query-params-str-validations.md b/docs/en/docs/tutorial/query-params-str-validations.md index 060e1d58a..a12b0b41a 100644 --- a/docs/en/docs/tutorial/query-params-str-validations.md +++ b/docs/en/docs/tutorial/query-params-str-validations.md @@ -27,25 +27,103 @@ The query parameter `q` is of type `Union[str, None]` (or `str | None` in Python We are going to enforce that even though `q` is optional, whenever it is provided, **its length doesn't exceed 50 characters**. -### Import `Query` +### Import `Query` and `Annotated` -To achieve that, first import `Query` from `fastapi`: +To achieve that, first import: + +* `Query` from `fastapi` +* `Annotated` from `typing` (or from `typing_extensions` in Python below 3.9) === "Python 3.6 and above" - ```Python hl_lines="3" - {!> ../../../docs_src/query_params_str_validations/tutorial002.py!} + In versions of Python below Python 3.9 you import `Annotation` from `typing_extensions`. + + It will already be installed with FastAPI. + + ```Python hl_lines="3-4" + {!> ../../../docs_src/query_params_str_validations/tutorial002_an.py!} ``` === "Python 3.10 and above" - ```Python hl_lines="1" - {!> ../../../docs_src/query_params_str_validations/tutorial002_py310.py!} + In Python 3.9 or above, `Annotated` is part of the standard library, so you can import it from `typing`. + + ```Python hl_lines="1 3" + {!> ../../../docs_src/query_params_str_validations/tutorial002_an_py310.py!} + ``` + +## Use `Annotated` in the type for the `q` parameter + +Remember I told you before that `Annotated` can be used to add metadata to your parameters in the [Python Types Intro](../python-types.md#type-hints-with-metadata-annotations){.internal-link target=_blank}? + +Now it's the time to use it with FastAPI. 🚀 + +We had this type annotation: + +=== "Python 3.6 and above" + + ```Python + q: Union[str, None] = None + ``` + +=== "Python 3.10 and above" + + ```Python + q: str | None = None + ``` + +What we will do is wrap that with `Annotated`, so it becomes: + +=== "Python 3.6 and above" + + ```Python + q: Annotated[Union[str, None]] = None + ``` + +=== "Python 3.10 and above" + + ```Python + q: Annotated[str | None] = None + ``` + +Both of those versions mean the same thing, `q` is a parameter that can be a `str` or `None`, and by default, it is `None`. + +Now let's jump to the fun stuff. 🎉 + +## Add `Query` to `Annotated` in the `q` parameter + +Now that we have this `Annotated` where we can put more metadata, add `Query` to it, and set the parameter `max_length` to 50: + +=== "Python 3.6 and above" + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial002_an.py!} ``` -## Use `Query` as the default value +=== "Python 3.10 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial002_an_py310.py!} + ``` + +Notice that the default value is still `None`, so the parameter is still optional. + +But now, having `Query(max_length=50)` inside of `Annotated`, we are telling FastAPI that we want it to extract this value from the query parameters (this would have been the default anyway 🤷) and that we want to have **additional validation** for this value (that's why we do this, to get the additional validation). 😎 + +FastAPI wll now: + +* **Validate** the data making sure that the max length is 50 characters +* Show a **clear error** for the client when the data is not valid +* **Document** the parameter in the OpenAPI schema *path operation* (so it will show up in the **automatic docs UI**) + +## Alternative (old) `Query` as the default value + +Previous versions of FastAPI (before 0.95.0) required you to use `Query` as the default value of your parameter, instead of putting it in `Annotated`, there's a high chance that you will see code using it around, so I'll explain it to you. -And now use it as the default value of your parameter, setting the parameter `max_length` to 50: +!!! tip + For new code and whenever possible, use `Annotated` as explained above. There are multiple advantages (explained below) and no disadvantages. 🍰 + +This is how you would use `Query()` as the default value of your function parameter, setting the parameter `max_length` to 50: === "Python 3.6 and above" @@ -59,7 +137,7 @@ And now use it as the default value of your parameter, setting the parameter `ma {!> ../../../docs_src/query_params_str_validations/tutorial002_py310.py!} ``` -As we have to replace the default value `None` in the function with `Query()`, we can now set the default value with the parameter `Query(default=None)`, it serves the same purpose of defining that default value. +As in this case (without using `Annotated`) we have to replace the default value `None` in the function with `Query()`, we now need to set the default value with the parameter `Query(default=None)`, it serves the same purpose of defining that default value (at least for FastAPI). So: @@ -67,7 +145,7 @@ So: q: Union[str, None] = Query(default=None) ``` -...makes the parameter optional, the same as: +...makes the parameter optional, with a default value of `None`, the same as: ```Python q: Union[str, None] = None @@ -79,7 +157,7 @@ And in Python 3.10 and above: q: str | None = Query(default=None) ``` -...makes the parameter optional, the same as: +...makes the parameter optional, with a default value of `None`, the same as: ```Python q: str | None = None @@ -112,18 +190,80 @@ q: Union[str, None] = Query(default=None, max_length=50) This will validate the data, show a clear error when the data is not valid, and document the parameter in the OpenAPI schema *path operation*. +### `Query` as the default value or in `Annotated` + +Have in mind that when using `Query` inside of `Annotated` you cannot use the `default` parameter for `Query`. + +Instead use the actual default value of the function parameter. Otherwise, it would be inconsistent. + +For example, this is not allowed: + +```Python +q: Annotated[str Query(default="rick")] = "morty" +``` + +...because it's not clear if the default value should be `"rick"` or `"morty"`. + +So, you would use (preferably): + +```Python +q: Annotated[str, Query()] = "rick" +``` + +...or in older code bases you will find: + +```Python +q: str = Query(default="rick") +``` + +### Advantages of `Annotated` + +**Using `Annotated` is recommended** instead of the default value in function parameters, it is **better** for multiple reasons. 🤓 + +The **default** value of the **function parameter** is the **actual default** value, that's more intuitive with Python in general. 😌 + +You could **call** that same function in **other places** without FastAPI, and it would **work as expected**. If there's a **required** parameter (without a default value), your **editor** will let you know with an error, **Python** will also complain if you run it without passing the required parameter. + +When you don't use `Annotated` and instead use the **(old) default value style**, if you call that function without FastAPI in **other place**, you have to **remember** to pass the arguments to the function for it to work correctly, otherwise the values will be different from what you expect (e.g. `QueryInfo` or something similar instead of `str`). And your editor won't complain, and Python won't complain running that function, only when the operations inside error out. + +Because `Annotated` can have more than one metadata annotation, you could now even use the same function with other tools, like Typer. 🚀 + ## Add more validations You can also add a parameter `min_length`: === "Python 3.6 and above" + ```Python hl_lines="11" + {!> ../../../docs_src/query_params_str_validations/tutorial003_an.py!} + ``` + +=== "Python 3.9 and above" + ```Python hl_lines="10" - {!> ../../../docs_src/query_params_str_validations/tutorial003.py!} + {!> ../../../docs_src/query_params_str_validations/tutorial003_an_py39.py!} ``` === "Python 3.10 and above" + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial003_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial003.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="7" {!> ../../../docs_src/query_params_str_validations/tutorial003_py310.py!} ``` @@ -134,12 +274,36 @@ You can define a ../../../docs_src/query_params_str_validations/tutorial004_an.py!} + ``` + +=== "Python 3.9 and above" + ```Python hl_lines="11" - {!> ../../../docs_src/query_params_str_validations/tutorial004.py!} + {!> ../../../docs_src/query_params_str_validations/tutorial004_an_py39.py!} ``` === "Python 3.10 and above" + ```Python hl_lines="11" + {!> ../../../docs_src/query_params_str_validations/tutorial004_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="11" + {!> ../../../docs_src/query_params_str_validations/tutorial004.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="9" {!> ../../../docs_src/query_params_str_validations/tutorial004_py310.py!} ``` @@ -156,16 +320,33 @@ But whenever you need them and go and learn them, know that you can already use ## Default values -The same way that you can pass `None` as the value for the `default` parameter, you can pass other values. +You can, of course, use default values other than `None`. Let's say that you want to declare the `q` query parameter to have a `min_length` of `3`, and to have a default value of `"fixedquery"`: -```Python hl_lines="7" -{!../../../docs_src/query_params_str_validations/tutorial005.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="8" + {!> ../../../docs_src/query_params_str_validations/tutorial005_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial005_an_py39.py!} + ``` + +=== "non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial005.py!} + ``` !!! note - Having a default value also makes the parameter optional. + Having a default value of any type, including `None`, makes the parameter optional (not required). ## Make it required @@ -183,23 +364,70 @@ q: Union[str, None] = None But we are now declaring it with `Query`, for example like: -```Python -q: Union[str, None] = Query(default=None, min_length=3) -``` +=== "Annotated" + + ```Python + q: Annotated[Union[str, None], Query(min_length=3)] = None + ``` + +=== "non-Annotated" + + ```Python + q: Union[str, None] = Query(default=None, min_length=3) + ``` So, when you need to declare a value as required while using `Query`, you can simply not declare a default value: -```Python hl_lines="7" -{!../../../docs_src/query_params_str_validations/tutorial006.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="8" + {!> ../../../docs_src/query_params_str_validations/tutorial006_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial006_an_py39.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial006.py!} + ``` + + !!! tip + Notice that, even though in this case the `Query()` is used as the function parameter default value, we don't pass the `default=None` to `Query()`. + + Still, probably better to use the `Annotated` version. 😉 ### Required with Ellipsis (`...`) -There's an alternative way to explicitly declare that a value is required. You can set the `default` parameter to the literal value `...`: +There's an alternative way to explicitly declare that a value is required. You can set the default to the literal value `...`: -```Python hl_lines="7" -{!../../../docs_src/query_params_str_validations/tutorial006b.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="8" + {!> ../../../docs_src/query_params_str_validations/tutorial006b_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial006b_an_py39.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial006b.py!} + ``` !!! info If you hadn't seen that `...` before: it is a special single value, it is part of Python and is called "Ellipsis". @@ -212,16 +440,40 @@ This will let **FastAPI** know that this parameter is required. You can declare that a parameter can accept `None`, but that it's still required. This would force clients to send a value, even if the value is `None`. -To do that, you can declare that `None` is a valid type but still use `default=...`: +To do that, you can declare that `None` is a valid type but still use `...` as the default: === "Python 3.6 and above" + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial006c_an.py!} + ``` + +=== "Python 3.9 and above" + ```Python hl_lines="9" - {!> ../../../docs_src/query_params_str_validations/tutorial006c.py!} + {!> ../../../docs_src/query_params_str_validations/tutorial006c_an_py39.py!} ``` === "Python 3.10 and above" + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial006c_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial006c.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="7" {!> ../../../docs_src/query_params_str_validations/tutorial006c_py310.py!} ``` @@ -233,12 +485,29 @@ To do that, you can declare that `None` is a valid type but still use `default=. If you feel uncomfortable using `...`, you can also import and use `Required` from Pydantic: -```Python hl_lines="2 8" -{!../../../docs_src/query_params_str_validations/tutorial006d.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="2 9" + {!> ../../../docs_src/query_params_str_validations/tutorial006d_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="4 10" + {!> ../../../docs_src/query_params_str_validations/tutorial006d_an_py39.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="2 8" + {!> ../../../docs_src/query_params_str_validations/tutorial006d.py!} + ``` !!! tip - Remember that in most of the cases, when something is required, you can simply omit the `default` parameter, so you normally don't have to use `...` nor `Required`. + Remember that in most of the cases, when something is required, you can simply omit the default, so you normally don't have to use `...` nor `Required`. ## Query parameter list / multiple values @@ -248,17 +517,44 @@ For example, to declare a query parameter `q` that can appear multiple times in === "Python 3.6 and above" + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial011_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial011_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial011_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="9" {!> ../../../docs_src/query_params_str_validations/tutorial011.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.9 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. ```Python hl_lines="9" {!> ../../../docs_src/query_params_str_validations/tutorial011_py39.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. ```Python hl_lines="7" {!> ../../../docs_src/query_params_str_validations/tutorial011_py310.py!} @@ -296,11 +592,29 @@ And you can also define a default `list` of values if none are provided: === "Python 3.6 and above" + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial012_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial012_an_py39.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="9" {!> ../../../docs_src/query_params_str_validations/tutorial012.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.9 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. ```Python hl_lines="7" {!> ../../../docs_src/query_params_str_validations/tutorial012_py39.py!} @@ -327,9 +641,26 @@ the default of `q` will be: `["foo", "bar"]` and your response will be: You can also use `list` directly instead of `List[str]` (or `list[str]` in Python 3.9+): -```Python hl_lines="7" -{!../../../docs_src/query_params_str_validations/tutorial013.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="8" + {!> ../../../docs_src/query_params_str_validations/tutorial013_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial013_an_py39.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial013.py!} + ``` !!! note Have in mind that in this case, FastAPI won't check the contents of the list. @@ -351,25 +682,74 @@ You can add a `title`: === "Python 3.6 and above" + ```Python hl_lines="11" + {!> ../../../docs_src/query_params_str_validations/tutorial007_an.py!} + ``` + +=== "Python 3.9 and above" + ```Python hl_lines="10" - {!> ../../../docs_src/query_params_str_validations/tutorial007.py!} + {!> ../../../docs_src/query_params_str_validations/tutorial007_an_py39.py!} ``` === "Python 3.10 and above" + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial007_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial007.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="8" {!> ../../../docs_src/query_params_str_validations/tutorial007_py310.py!} ``` + And a `description`: === "Python 3.6 and above" + ```Python hl_lines="15" + {!> ../../../docs_src/query_params_str_validations/tutorial008_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="14" + {!> ../../../docs_src/query_params_str_validations/tutorial008_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="14" + {!> ../../../docs_src/query_params_str_validations/tutorial008_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="13" {!> ../../../docs_src/query_params_str_validations/tutorial008.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. ```Python hl_lines="12" {!> ../../../docs_src/query_params_str_validations/tutorial008_py310.py!} @@ -395,12 +775,36 @@ Then you can declare an `alias`, and that alias is what will be used to find the === "Python 3.6 and above" + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial009_an.py!} + ``` + +=== "Python 3.9 and above" + ```Python hl_lines="9" - {!> ../../../docs_src/query_params_str_validations/tutorial009.py!} + {!> ../../../docs_src/query_params_str_validations/tutorial009_an_py39.py!} ``` === "Python 3.10 and above" + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial009_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial009.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="7" {!> ../../../docs_src/query_params_str_validations/tutorial009_py310.py!} ``` @@ -415,11 +819,35 @@ Then pass the parameter `deprecated=True` to `Query`: === "Python 3.6 and above" + ```Python hl_lines="20" + {!> ../../../docs_src/query_params_str_validations/tutorial010_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="19" + {!> ../../../docs_src/query_params_str_validations/tutorial010_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="19" + {!> ../../../docs_src/query_params_str_validations/tutorial010_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="18" {!> ../../../docs_src/query_params_str_validations/tutorial010.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. ```Python hl_lines="17" {!> ../../../docs_src/query_params_str_validations/tutorial010_py310.py!} @@ -435,12 +863,36 @@ To exclude a query parameter from the generated OpenAPI schema (and thus, from t === "Python 3.6 and above" + ```Python hl_lines="11" + {!> ../../../docs_src/query_params_str_validations/tutorial014_an.py!} + ``` + +=== "Python 3.9 and above" + ```Python hl_lines="10" - {!> ../../../docs_src/query_params_str_validations/tutorial014.py!} + {!> ../../../docs_src/query_params_str_validations/tutorial014_an_py39.py!} ``` === "Python 3.10 and above" + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial014_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial014.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="8" {!> ../../../docs_src/query_params_str_validations/tutorial014_py310.py!} ``` diff --git a/docs/en/docs/tutorial/request-files.md b/docs/en/docs/tutorial/request-files.md index 783783c58..666de76eb 100644 --- a/docs/en/docs/tutorial/request-files.md +++ b/docs/en/docs/tutorial/request-files.md @@ -13,17 +13,51 @@ You can define files to be uploaded by the client using `File`. Import `File` and `UploadFile` from `fastapi`: -```Python hl_lines="1" -{!../../../docs_src/request_files/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="1" + {!> ../../../docs_src/request_files/tutorial001_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="3" + {!> ../../../docs_src/request_files/tutorial001_an_py39.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="1" + {!> ../../../docs_src/request_files/tutorial001.py!} + ``` ## Define `File` Parameters Create file parameters the same way you would for `Body` or `Form`: -```Python hl_lines="7" -{!../../../docs_src/request_files/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="8" + {!> ../../../docs_src/request_files/tutorial001_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/request_files/tutorial001_an_py39.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="7" + {!> ../../../docs_src/request_files/tutorial001.py!} + ``` !!! info `File` is a class that inherits directly from `Form`. @@ -45,9 +79,26 @@ But there are several cases in which you might benefit from using `UploadFile`. Define a file parameter with a type of `UploadFile`: -```Python hl_lines="12" -{!../../../docs_src/request_files/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="13" + {!> ../../../docs_src/request_files/tutorial001_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="14" + {!> ../../../docs_src/request_files/tutorial001_an_py39.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="12" + {!> ../../../docs_src/request_files/tutorial001.py!} + ``` Using `UploadFile` has several advantages over `bytes`: @@ -120,23 +171,65 @@ You can make a file optional by using standard type annotations and setting a de === "Python 3.6 and above" + ```Python hl_lines="10 18" + {!> ../../../docs_src/request_files/tutorial001_02_an.py!} + ``` + +=== "Python 3.9 and above" + ```Python hl_lines="9 17" - {!> ../../../docs_src/request_files/tutorial001_02.py!} + {!> ../../../docs_src/request_files/tutorial001_02_an_py39.py!} ``` === "Python 3.10 and above" - ```Python hl_lines="7 14" + ```Python hl_lines="9 17" + {!> ../../../docs_src/request_files/tutorial001_02_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="9 17" + {!> ../../../docs_src/request_files/tutorial001_02.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="7 15" {!> ../../../docs_src/request_files/tutorial001_02_py310.py!} ``` + ## `UploadFile` with Additional Metadata You can also use `File()` with `UploadFile`, for example, to set additional metadata: -```Python hl_lines="13" -{!../../../docs_src/request_files/tutorial001_03.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="8 14" + {!> ../../../docs_src/request_files/tutorial001_03_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="9 15" + {!> ../../../docs_src/request_files/tutorial001_03_an_py39.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="7 13" + {!> ../../../docs_src/request_files/tutorial001_03.py!} + ``` ## Multiple File Uploads @@ -148,11 +241,29 @@ To use that, declare a list of `bytes` or `UploadFile`: === "Python 3.6 and above" + ```Python hl_lines="11 16" + {!> ../../../docs_src/request_files/tutorial002_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="10 15" + {!> ../../../docs_src/request_files/tutorial002_an_py39.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="10 15" {!> ../../../docs_src/request_files/tutorial002.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.9 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. ```Python hl_lines="8 13" {!> ../../../docs_src/request_files/tutorial002_py39.py!} @@ -171,13 +282,31 @@ And the same way as before, you can use `File()` to set additional parameters, e === "Python 3.6 and above" - ```Python hl_lines="18" - {!> ../../../docs_src/request_files/tutorial003.py!} + ```Python hl_lines="12 19-21" + {!> ../../../docs_src/request_files/tutorial003_an.py!} ``` === "Python 3.9 and above" - ```Python hl_lines="16" + ```Python hl_lines="11 18-20" + {!> ../../../docs_src/request_files/tutorial003_an_py39.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="11 18" + {!> ../../../docs_src/request_files/tutorial003.py!} + ``` + +=== "Python 3.9 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="9 16" {!> ../../../docs_src/request_files/tutorial003_py39.py!} ``` diff --git a/docs/en/docs/tutorial/request-forms-and-files.md b/docs/en/docs/tutorial/request-forms-and-files.md index 6844f8c65..9729ab160 100644 --- a/docs/en/docs/tutorial/request-forms-and-files.md +++ b/docs/en/docs/tutorial/request-forms-and-files.md @@ -9,17 +9,51 @@ You can define files and form fields at the same time using `File` and `Form`. ## Import `File` and `Form` -```Python hl_lines="1" -{!../../../docs_src/request_forms_and_files/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="1" + {!> ../../../docs_src/request_forms_and_files/tutorial001_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="3" + {!> ../../../docs_src/request_forms_and_files/tutorial001_an_py39.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="1" + {!> ../../../docs_src/request_forms_and_files/tutorial001.py!} + ``` ## Define `File` and `Form` parameters Create file and form parameters the same way you would for `Body` or `Query`: -```Python hl_lines="8" -{!../../../docs_src/request_forms_and_files/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="9-11" + {!> ../../../docs_src/request_forms_and_files/tutorial001_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="10-12" + {!> ../../../docs_src/request_forms_and_files/tutorial001_an_py39.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="8" + {!> ../../../docs_src/request_forms_and_files/tutorial001.py!} + ``` The files and form fields will be uploaded as form data and you will receive the files and form fields. diff --git a/docs/en/docs/tutorial/request-forms.md b/docs/en/docs/tutorial/request-forms.md index 26922685a..3f866410a 100644 --- a/docs/en/docs/tutorial/request-forms.md +++ b/docs/en/docs/tutorial/request-forms.md @@ -11,17 +11,51 @@ When you need to receive form fields instead of JSON, you can use `Form`. Import `Form` from `fastapi`: -```Python hl_lines="1" -{!../../../docs_src/request_forms/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="1" + {!> ../../../docs_src/request_forms/tutorial001_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="3" + {!> ../../../docs_src/request_forms/tutorial001_an_py39.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="1" + {!> ../../../docs_src/request_forms/tutorial001.py!} + ``` ## Define `Form` parameters Create form parameters the same way you would for `Body` or `Query`: -```Python hl_lines="7" -{!../../../docs_src/request_forms/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="8" + {!> ../../../docs_src/request_forms/tutorial001_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/request_forms/tutorial001_an_py39.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="7" + {!> ../../../docs_src/request_forms/tutorial001.py!} + ``` For example, in one of the ways the OAuth2 specification can be used (called "password flow") it is required to send a `username` and `password` as form fields. diff --git a/docs/en/docs/tutorial/schema-extra-example.md b/docs/en/docs/tutorial/schema-extra-example.md index 94347018d..d2aa66843 100644 --- a/docs/en/docs/tutorial/schema-extra-example.md +++ b/docs/en/docs/tutorial/schema-extra-example.md @@ -68,11 +68,32 @@ Here we pass an `example` of the data expected in `Body()`: === "Python 3.6 and above" + ```Python hl_lines="23-28" + {!> ../../../docs_src/schema_extra_example/tutorial003_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="22-27" + {!> ../../../docs_src/schema_extra_example/tutorial003_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="22-27" + {!> ../../../docs_src/schema_extra_example/tutorial003_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="20-25" {!> ../../../docs_src/schema_extra_example/tutorial003.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.10 and above - non-Annotated" ```Python hl_lines="18-23" {!> ../../../docs_src/schema_extra_example/tutorial003_py310.py!} @@ -99,11 +120,32 @@ Each specific example `dict` in the `examples` can contain: === "Python 3.6 and above" + ```Python hl_lines="24-50" + {!> ../../../docs_src/schema_extra_example/tutorial004_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="23-49" + {!> ../../../docs_src/schema_extra_example/tutorial004_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="23-49" + {!> ../../../docs_src/schema_extra_example/tutorial004_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="21-47" {!> ../../../docs_src/schema_extra_example/tutorial004.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.10 and above - non-Annotated" ```Python hl_lines="19-45" {!> ../../../docs_src/schema_extra_example/tutorial004_py310.py!} diff --git a/docs/en/docs/tutorial/security/first-steps.md b/docs/en/docs/tutorial/security/first-steps.md index 45ffaab90..9198db6a6 100644 --- a/docs/en/docs/tutorial/security/first-steps.md +++ b/docs/en/docs/tutorial/security/first-steps.md @@ -20,9 +20,27 @@ Let's first just use the code and see how it works, and then we'll come back to Copy the example in a file `main.py`: -```Python -{!../../../docs_src/security/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python + {!> ../../../docs_src/security/tutorial001_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python + {!> ../../../docs_src/security/tutorial001_an_py39.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python + {!> ../../../docs_src/security/tutorial001.py!} + ``` + ## Run it @@ -116,9 +134,26 @@ In this example we are going to use **OAuth2**, with the **Password** flow, usin When we create an instance of the `OAuth2PasswordBearer` class we pass in the `tokenUrl` parameter. This parameter contains the URL that the client (the frontend running in the user's browser) will use to send the `username` and `password` in order to get a token. -```Python hl_lines="6" -{!../../../docs_src/security/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="7" + {!> ../../../docs_src/security/tutorial001_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="8" + {!> ../../../docs_src/security/tutorial001_an_py39.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="6" + {!> ../../../docs_src/security/tutorial001.py!} + ``` !!! tip Here `tokenUrl="token"` refers to a relative URL `token` that we haven't created yet. As it's a relative URL, it's equivalent to `./token`. @@ -150,9 +185,26 @@ So, it can be used with `Depends`. Now you can pass that `oauth2_scheme` in a dependency with `Depends`. -```Python hl_lines="10" -{!../../../docs_src/security/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="11" + {!> ../../../docs_src/security/tutorial001_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="12" + {!> ../../../docs_src/security/tutorial001_an_py39.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="10" + {!> ../../../docs_src/security/tutorial001.py!} + ``` This dependency will provide a `str` that is assigned to the parameter `token` of the *path operation function*. diff --git a/docs/en/docs/tutorial/security/get-current-user.md b/docs/en/docs/tutorial/security/get-current-user.md index 13e867216..0076e7d16 100644 --- a/docs/en/docs/tutorial/security/get-current-user.md +++ b/docs/en/docs/tutorial/security/get-current-user.md @@ -2,9 +2,26 @@ In the previous chapter the security system (which is based on the dependency injection system) was giving the *path operation function* a `token` as a `str`: -```Python hl_lines="10" -{!../../../docs_src/security/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="11" + {!> ../../../docs_src/security/tutorial001_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="12" + {!> ../../../docs_src/security/tutorial001_an_py39.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="10" + {!> ../../../docs_src/security/tutorial001.py!} + ``` But that is still not that useful. @@ -18,12 +35,36 @@ The same way we use Pydantic to declare bodies, we can use it anywhere else: === "Python 3.6 and above" + ```Python hl_lines="5 13-17" + {!> ../../../docs_src/security/tutorial002_an.py!} + ``` + +=== "Python 3.9 and above" + ```Python hl_lines="5 12-16" - {!> ../../../docs_src/security/tutorial002.py!} + {!> ../../../docs_src/security/tutorial002_an_py39.py!} ``` === "Python 3.10 and above" + ```Python hl_lines="5 12-16" + {!> ../../../docs_src/security/tutorial002_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="5 12-16" + {!> ../../../docs_src/security/tutorial002.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="3 10-14" {!> ../../../docs_src/security/tutorial002_py310.py!} ``` @@ -40,12 +81,36 @@ The same as we were doing before in the *path operation* directly, our new depen === "Python 3.6 and above" + ```Python hl_lines="26" + {!> ../../../docs_src/security/tutorial002_an.py!} + ``` + +=== "Python 3.9 and above" + ```Python hl_lines="25" - {!> ../../../docs_src/security/tutorial002.py!} + {!> ../../../docs_src/security/tutorial002_an_py39.py!} ``` === "Python 3.10 and above" + ```Python hl_lines="25" + {!> ../../../docs_src/security/tutorial002_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="25" + {!> ../../../docs_src/security/tutorial002.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="23" {!> ../../../docs_src/security/tutorial002_py310.py!} ``` @@ -56,12 +121,36 @@ The same as we were doing before in the *path operation* directly, our new depen === "Python 3.6 and above" + ```Python hl_lines="20-23 27-28" + {!> ../../../docs_src/security/tutorial002_an.py!} + ``` + +=== "Python 3.9 and above" + ```Python hl_lines="19-22 26-27" - {!> ../../../docs_src/security/tutorial002.py!} + {!> ../../../docs_src/security/tutorial002_an_py39.py!} ``` === "Python 3.10 and above" + ```Python hl_lines="19-22 26-27" + {!> ../../../docs_src/security/tutorial002_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="19-22 26-27" + {!> ../../../docs_src/security/tutorial002.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="17-20 24-25" {!> ../../../docs_src/security/tutorial002_py310.py!} ``` @@ -72,12 +161,36 @@ So now we can use the same `Depends` with our `get_current_user` in the *path op === "Python 3.6 and above" + ```Python hl_lines="32" + {!> ../../../docs_src/security/tutorial002_an.py!} + ``` + +=== "Python 3.9 and above" + ```Python hl_lines="31" - {!> ../../../docs_src/security/tutorial002.py!} + {!> ../../../docs_src/security/tutorial002_an_py39.py!} ``` === "Python 3.10 and above" + ```Python hl_lines="31" + {!> ../../../docs_src/security/tutorial002_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="31" + {!> ../../../docs_src/security/tutorial002.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="29" {!> ../../../docs_src/security/tutorial002_py310.py!} ``` @@ -130,12 +243,36 @@ And all these thousands of *path operations* can be as small as 3 lines: === "Python 3.6 and above" + ```Python hl_lines="31-33" + {!> ../../../docs_src/security/tutorial002_an.py!} + ``` + +=== "Python 3.9 and above" + ```Python hl_lines="30-32" - {!> ../../../docs_src/security/tutorial002.py!} + {!> ../../../docs_src/security/tutorial002_an_py39.py!} ``` === "Python 3.10 and above" + ```Python hl_lines="30-32" + {!> ../../../docs_src/security/tutorial002_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="30-32" + {!> ../../../docs_src/security/tutorial002.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="28-30" {!> ../../../docs_src/security/tutorial002_py310.py!} ``` diff --git a/docs/en/docs/tutorial/security/oauth2-jwt.md b/docs/en/docs/tutorial/security/oauth2-jwt.md index 41f00fd41..0b6390975 100644 --- a/docs/en/docs/tutorial/security/oauth2-jwt.md +++ b/docs/en/docs/tutorial/security/oauth2-jwt.md @@ -111,12 +111,36 @@ And another one to authenticate and return a user. === "Python 3.6 and above" + ```Python hl_lines="7 49 56-57 60-61 70-76" + {!> ../../../docs_src/security/tutorial004_an.py!} + ``` + +=== "Python 3.9 and above" + ```Python hl_lines="7 48 55-56 59-60 69-75" - {!> ../../../docs_src/security/tutorial004.py!} + {!> ../../../docs_src/security/tutorial004_an_py39.py!} ``` === "Python 3.10 and above" + ```Python hl_lines="7 48 55-56 59-60 69-75" + {!> ../../../docs_src/security/tutorial004_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="7 48 55-56 59-60 69-75" + {!> ../../../docs_src/security/tutorial004.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="6 47 54-55 58-59 68-74" {!> ../../../docs_src/security/tutorial004_py310.py!} ``` @@ -154,12 +178,36 @@ Create a utility function to generate a new access token. === "Python 3.6 and above" + ```Python hl_lines="6 13-15 29-31 79-87" + {!> ../../../docs_src/security/tutorial004_an.py!} + ``` + +=== "Python 3.9 and above" + ```Python hl_lines="6 12-14 28-30 78-86" - {!> ../../../docs_src/security/tutorial004.py!} + {!> ../../../docs_src/security/tutorial004_an_py39.py!} ``` === "Python 3.10 and above" + ```Python hl_lines="6 12-14 28-30 78-86" + {!> ../../../docs_src/security/tutorial004_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="6 12-14 28-30 78-86" + {!> ../../../docs_src/security/tutorial004.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="5 11-13 27-29 77-85" {!> ../../../docs_src/security/tutorial004_py310.py!} ``` @@ -174,12 +222,36 @@ If the token is invalid, return an HTTP error right away. === "Python 3.6 and above" + ```Python hl_lines="90-107" + {!> ../../../docs_src/security/tutorial004_an.py!} + ``` + +=== "Python 3.9 and above" + ```Python hl_lines="89-106" - {!> ../../../docs_src/security/tutorial004.py!} + {!> ../../../docs_src/security/tutorial004_an_py39.py!} ``` === "Python 3.10 and above" + ```Python hl_lines="89-106" + {!> ../../../docs_src/security/tutorial004_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="89-106" + {!> ../../../docs_src/security/tutorial004.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="88-105" {!> ../../../docs_src/security/tutorial004_py310.py!} ``` @@ -192,11 +264,35 @@ Create a real JWT access token and return it. === "Python 3.6 and above" + ```Python hl_lines="118-133" + {!> ../../../docs_src/security/tutorial004_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="117-132" + {!> ../../../docs_src/security/tutorial004_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="117-132" + {!> ../../../docs_src/security/tutorial004_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="115-128" {!> ../../../docs_src/security/tutorial004.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. ```Python hl_lines="114-127" {!> ../../../docs_src/security/tutorial004_py310.py!} diff --git a/docs/en/docs/tutorial/security/simple-oauth2.md b/docs/en/docs/tutorial/security/simple-oauth2.md index 505b223b1..6abf43218 100644 --- a/docs/en/docs/tutorial/security/simple-oauth2.md +++ b/docs/en/docs/tutorial/security/simple-oauth2.md @@ -51,11 +51,35 @@ First, import `OAuth2PasswordRequestForm`, and use it as a dependency with `Depe === "Python 3.6 and above" + ```Python hl_lines="4 79" + {!> ../../../docs_src/security/tutorial003_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="4 78" + {!> ../../../docs_src/security/tutorial003_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="4 78" + {!> ../../../docs_src/security/tutorial003_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="4 76" {!> ../../../docs_src/security/tutorial003.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. ```Python hl_lines="2 74" {!> ../../../docs_src/security/tutorial003_py310.py!} @@ -100,11 +124,35 @@ For the error, we use the exception `HTTPException`: === "Python 3.6 and above" + ```Python hl_lines="3 80-82" + {!> ../../../docs_src/security/tutorial003_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="3 79-81" + {!> ../../../docs_src/security/tutorial003_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="3 79-81" + {!> ../../../docs_src/security/tutorial003_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="3 77-79" {!> ../../../docs_src/security/tutorial003.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. ```Python hl_lines="1 75-77" {!> ../../../docs_src/security/tutorial003_py310.py!} @@ -136,11 +184,35 @@ So, the thief won't be able to try to use those same passwords in another system === "Python 3.6 and above" + ```Python hl_lines="83-86" + {!> ../../../docs_src/security/tutorial003_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="82-85" + {!> ../../../docs_src/security/tutorial003_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="82-85" + {!> ../../../docs_src/security/tutorial003_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="80-83" {!> ../../../docs_src/security/tutorial003.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. ```Python hl_lines="78-81" {!> ../../../docs_src/security/tutorial003_py310.py!} @@ -182,11 +254,35 @@ For this simple example, we are going to just be completely insecure and return === "Python 3.6 and above" + ```Python hl_lines="88" + {!> ../../../docs_src/security/tutorial003_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="87" + {!> ../../../docs_src/security/tutorial003_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="87" + {!> ../../../docs_src/security/tutorial003_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="85" {!> ../../../docs_src/security/tutorial003.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. ```Python hl_lines="83" {!> ../../../docs_src/security/tutorial003_py310.py!} @@ -215,13 +311,37 @@ So, in our endpoint, we will only get a user if the user exists, was correctly a === "Python 3.6 and above" + ```Python hl_lines="59-67 70-75 95" + {!> ../../../docs_src/security/tutorial003_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="58-66 69-74 94" + {!> ../../../docs_src/security/tutorial003_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="58-66 69-74 94" + {!> ../../../docs_src/security/tutorial003_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="58-66 69-72 90" {!> ../../../docs_src/security/tutorial003.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. - ```Python hl_lines="55-64 67-70 88" + ```Python hl_lines="56-64 67-70 88" {!> ../../../docs_src/security/tutorial003_py310.py!} ``` diff --git a/docs/en/docs/tutorial/testing.md b/docs/en/docs/tutorial/testing.md index be07aab37..a932ad3f8 100644 --- a/docs/en/docs/tutorial/testing.md +++ b/docs/en/docs/tutorial/testing.md @@ -113,11 +113,35 @@ Both *path operations* require an `X-Token` header. === "Python 3.6 and above" ```Python - {!> ../../../docs_src/app_testing/app_b/main.py!} + {!> ../../../docs_src/app_testing/app_b_an/main.py!} + ``` + +=== "Python 3.9 and above" + + ```Python + {!> ../../../docs_src/app_testing/app_b_an_py39/main.py!} ``` === "Python 3.10 and above" + ```Python + {!> ../../../docs_src/app_testing/app_b_an_py310/main.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python + {!> ../../../docs_src/app_testing/app_b/main.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python {!> ../../../docs_src/app_testing/app_b_py310/main.py!} ``` diff --git a/docs_src/additional_status_codes/tutorial001_an.py b/docs_src/additional_status_codes/tutorial001_an.py new file mode 100644 index 000000000..b5ad6a16b --- /dev/null +++ b/docs_src/additional_status_codes/tutorial001_an.py @@ -0,0 +1,26 @@ +from typing import Union + +from fastapi import Body, FastAPI, status +from fastapi.responses import JSONResponse +from typing_extensions import Annotated + +app = FastAPI() + +items = {"foo": {"name": "Fighters", "size": 6}, "bar": {"name": "Tenders", "size": 3}} + + +@app.put("/items/{item_id}") +async def upsert_item( + item_id: str, + name: Annotated[Union[str, None], Body()] = None, + size: Annotated[Union[int, None], Body()] = None, +): + if item_id in items: + item = items[item_id] + item["name"] = name + item["size"] = size + return item + else: + item = {"name": name, "size": size} + items[item_id] = item + return JSONResponse(status_code=status.HTTP_201_CREATED, content=item) diff --git a/docs_src/additional_status_codes/tutorial001_an_py310.py b/docs_src/additional_status_codes/tutorial001_an_py310.py new file mode 100644 index 000000000..1865074a1 --- /dev/null +++ b/docs_src/additional_status_codes/tutorial001_an_py310.py @@ -0,0 +1,25 @@ +from typing import Annotated + +from fastapi import Body, FastAPI, status +from fastapi.responses import JSONResponse + +app = FastAPI() + +items = {"foo": {"name": "Fighters", "size": 6}, "bar": {"name": "Tenders", "size": 3}} + + +@app.put("/items/{item_id}") +async def upsert_item( + item_id: str, + name: Annotated[str | None, Body()] = None, + size: Annotated[int | None, Body()] = None, +): + if item_id in items: + item = items[item_id] + item["name"] = name + item["size"] = size + return item + else: + item = {"name": name, "size": size} + items[item_id] = item + return JSONResponse(status_code=status.HTTP_201_CREATED, content=item) diff --git a/docs_src/additional_status_codes/tutorial001_an_py39.py b/docs_src/additional_status_codes/tutorial001_an_py39.py new file mode 100644 index 000000000..89653dd8a --- /dev/null +++ b/docs_src/additional_status_codes/tutorial001_an_py39.py @@ -0,0 +1,25 @@ +from typing import Annotated, Union + +from fastapi import Body, FastAPI, status +from fastapi.responses import JSONResponse + +app = FastAPI() + +items = {"foo": {"name": "Fighters", "size": 6}, "bar": {"name": "Tenders", "size": 3}} + + +@app.put("/items/{item_id}") +async def upsert_item( + item_id: str, + name: Annotated[Union[str, None], Body()] = None, + size: Annotated[Union[int, None], Body()] = None, +): + if item_id in items: + item = items[item_id] + item["name"] = name + item["size"] = size + return item + else: + item = {"name": name, "size": size} + items[item_id] = item + return JSONResponse(status_code=status.HTTP_201_CREATED, content=item) diff --git a/docs_src/additional_status_codes/tutorial001_py310.py b/docs_src/additional_status_codes/tutorial001_py310.py new file mode 100644 index 000000000..38bfa455b --- /dev/null +++ b/docs_src/additional_status_codes/tutorial001_py310.py @@ -0,0 +1,23 @@ +from fastapi import Body, FastAPI, status +from fastapi.responses import JSONResponse + +app = FastAPI() + +items = {"foo": {"name": "Fighters", "size": 6}, "bar": {"name": "Tenders", "size": 3}} + + +@app.put("/items/{item_id}") +async def upsert_item( + item_id: str, + name: str | None = Body(default=None), + size: int | None = Body(default=None), +): + if item_id in items: + item = items[item_id] + item["name"] = name + item["size"] = size + return item + else: + item = {"name": name, "size": size} + items[item_id] = item + return JSONResponse(status_code=status.HTTP_201_CREATED, content=item) diff --git a/docs_src/annotated/tutorial001.py b/docs_src/annotated/tutorial001.py deleted file mode 100644 index 959114b3f..000000000 --- a/docs_src/annotated/tutorial001.py +++ /dev/null @@ -1,18 +0,0 @@ -from typing import Optional - -from fastapi import Depends, FastAPI -from typing_extensions import Annotated - -app = FastAPI() - - -async def common_parameters(q: Optional[str] = None, skip: int = 0, limit: int = 100): - return {"q": q, "skip": skip, "limit": limit} - - -CommonParamsDepends = Annotated[dict, Depends(common_parameters)] - - -@app.get("/items/") -async def read_items(commons: CommonParamsDepends): - return commons diff --git a/docs_src/annotated/tutorial001_py39.py b/docs_src/annotated/tutorial001_py39.py deleted file mode 100644 index b05b89c4e..000000000 --- a/docs_src/annotated/tutorial001_py39.py +++ /dev/null @@ -1,17 +0,0 @@ -from typing import Annotated, Optional - -from fastapi import Depends, FastAPI - -app = FastAPI() - - -async def common_parameters(q: Optional[str] = None, skip: int = 0, limit: int = 100): - return {"q": q, "skip": skip, "limit": limit} - - -CommonParamsDepends = Annotated[dict, Depends(common_parameters)] - - -@app.get("/items/") -async def read_items(commons: CommonParamsDepends): - return commons diff --git a/docs_src/annotated/tutorial002.py b/docs_src/annotated/tutorial002.py deleted file mode 100644 index 2293fbb1a..000000000 --- a/docs_src/annotated/tutorial002.py +++ /dev/null @@ -1,21 +0,0 @@ -from typing import Optional - -from fastapi import Depends, FastAPI -from typing_extensions import Annotated - -app = FastAPI() - - -class CommonQueryParams: - def __init__(self, q: Optional[str] = None, skip: int = 0, limit: int = 100): - self.q = q - self.skip = skip - self.limit = limit - - -CommonQueryParamsDepends = Annotated[CommonQueryParams, Depends()] - - -@app.get("/items/") -async def read_items(commons: CommonQueryParamsDepends): - return commons diff --git a/docs_src/annotated/tutorial002_py39.py b/docs_src/annotated/tutorial002_py39.py deleted file mode 100644 index 7fa1a8740..000000000 --- a/docs_src/annotated/tutorial002_py39.py +++ /dev/null @@ -1,20 +0,0 @@ -from typing import Annotated, Optional - -from fastapi import Depends, FastAPI - -app = FastAPI() - - -class CommonQueryParams: - def __init__(self, q: Optional[str] = None, skip: int = 0, limit: int = 100): - self.q = q - self.skip = skip - self.limit = limit - - -CommonQueryParamsDepends = Annotated[CommonQueryParams, Depends()] - - -@app.get("/items/") -async def read_items(commons: CommonQueryParamsDepends): - return commons diff --git a/docs_src/annotated/tutorial003.py b/docs_src/annotated/tutorial003.py deleted file mode 100644 index 353d8b806..000000000 --- a/docs_src/annotated/tutorial003.py +++ /dev/null @@ -1,15 +0,0 @@ -from fastapi import FastAPI, Path -from fastapi.param_functions import Query -from typing_extensions import Annotated - -app = FastAPI() - - -@app.get("/items/{item_id}") -async def read_items(item_id: Annotated[int, Path(gt=0)]): - return {"item_id": item_id} - - -@app.get("/users") -async def read_users(user_id: Annotated[str, Query(min_length=1)] = "me"): - return {"user_id": user_id} diff --git a/docs_src/annotated/tutorial003_py39.py b/docs_src/annotated/tutorial003_py39.py deleted file mode 100644 index 9341b7d4f..000000000 --- a/docs_src/annotated/tutorial003_py39.py +++ /dev/null @@ -1,16 +0,0 @@ -from typing import Annotated - -from fastapi import FastAPI, Path -from fastapi.param_functions import Query - -app = FastAPI() - - -@app.get("/items/{item_id}") -async def read_items(item_id: Annotated[int, Path(gt=0)]): - return {"item_id": item_id} - - -@app.get("/users") -async def read_users(user_id: Annotated[str, Query(min_length=1)] = "me"): - return {"user_id": user_id} diff --git a/tests/test_tutorial/test_annotated/__init__.py b/docs_src/app_testing/app_b_an/__init__.py similarity index 100% rename from tests/test_tutorial/test_annotated/__init__.py rename to docs_src/app_testing/app_b_an/__init__.py diff --git a/docs_src/app_testing/app_b_an/main.py b/docs_src/app_testing/app_b_an/main.py new file mode 100644 index 000000000..c63134fc9 --- /dev/null +++ b/docs_src/app_testing/app_b_an/main.py @@ -0,0 +1,39 @@ +from typing import Union + +from fastapi import FastAPI, Header, HTTPException +from pydantic import BaseModel +from typing_extensions import Annotated + +fake_secret_token = "coneofsilence" + +fake_db = { + "foo": {"id": "foo", "title": "Foo", "description": "There goes my hero"}, + "bar": {"id": "bar", "title": "Bar", "description": "The bartenders"}, +} + +app = FastAPI() + + +class Item(BaseModel): + id: str + title: str + description: Union[str, None] = None + + +@app.get("/items/{item_id}", response_model=Item) +async def read_main(item_id: str, x_token: Annotated[str, Header()]): + if x_token != fake_secret_token: + raise HTTPException(status_code=400, detail="Invalid X-Token header") + if item_id not in fake_db: + raise HTTPException(status_code=404, detail="Item not found") + return fake_db[item_id] + + +@app.post("/items/", response_model=Item) +async def create_item(item: Item, x_token: Annotated[str, Header()]): + if x_token != fake_secret_token: + raise HTTPException(status_code=400, detail="Invalid X-Token header") + if item.id in fake_db: + raise HTTPException(status_code=400, detail="Item already exists") + fake_db[item.id] = item + return item diff --git a/docs_src/app_testing/app_b_an/test_main.py b/docs_src/app_testing/app_b_an/test_main.py new file mode 100644 index 000000000..d186b8ecb --- /dev/null +++ b/docs_src/app_testing/app_b_an/test_main.py @@ -0,0 +1,65 @@ +from fastapi.testclient import TestClient + +from .main import app + +client = TestClient(app) + + +def test_read_item(): + response = client.get("/items/foo", headers={"X-Token": "coneofsilence"}) + assert response.status_code == 200 + assert response.json() == { + "id": "foo", + "title": "Foo", + "description": "There goes my hero", + } + + +def test_read_item_bad_token(): + response = client.get("/items/foo", headers={"X-Token": "hailhydra"}) + assert response.status_code == 400 + assert response.json() == {"detail": "Invalid X-Token header"} + + +def test_read_inexistent_item(): + response = client.get("/items/baz", headers={"X-Token": "coneofsilence"}) + assert response.status_code == 404 + assert response.json() == {"detail": "Item not found"} + + +def test_create_item(): + response = client.post( + "/items/", + headers={"X-Token": "coneofsilence"}, + json={"id": "foobar", "title": "Foo Bar", "description": "The Foo Barters"}, + ) + assert response.status_code == 200 + assert response.json() == { + "id": "foobar", + "title": "Foo Bar", + "description": "The Foo Barters", + } + + +def test_create_item_bad_token(): + response = client.post( + "/items/", + headers={"X-Token": "hailhydra"}, + json={"id": "bazz", "title": "Bazz", "description": "Drop the bazz"}, + ) + assert response.status_code == 400 + assert response.json() == {"detail": "Invalid X-Token header"} + + +def test_create_existing_item(): + response = client.post( + "/items/", + headers={"X-Token": "coneofsilence"}, + json={ + "id": "foo", + "title": "The Foo ID Stealers", + "description": "There goes my stealer", + }, + ) + assert response.status_code == 400 + assert response.json() == {"detail": "Item already exists"} diff --git a/docs_src/app_testing/app_b_an_py310/__init__.py b/docs_src/app_testing/app_b_an_py310/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/app_testing/app_b_an_py310/main.py b/docs_src/app_testing/app_b_an_py310/main.py new file mode 100644 index 000000000..48c27a0b8 --- /dev/null +++ b/docs_src/app_testing/app_b_an_py310/main.py @@ -0,0 +1,38 @@ +from typing import Annotated + +from fastapi import FastAPI, Header, HTTPException +from pydantic import BaseModel + +fake_secret_token = "coneofsilence" + +fake_db = { + "foo": {"id": "foo", "title": "Foo", "description": "There goes my hero"}, + "bar": {"id": "bar", "title": "Bar", "description": "The bartenders"}, +} + +app = FastAPI() + + +class Item(BaseModel): + id: str + title: str + description: str | None = None + + +@app.get("/items/{item_id}", response_model=Item) +async def read_main(item_id: str, x_token: Annotated[str, Header()]): + if x_token != fake_secret_token: + raise HTTPException(status_code=400, detail="Invalid X-Token header") + if item_id not in fake_db: + raise HTTPException(status_code=404, detail="Item not found") + return fake_db[item_id] + + +@app.post("/items/", response_model=Item) +async def create_item(item: Item, x_token: Annotated[str, Header()]): + if x_token != fake_secret_token: + raise HTTPException(status_code=400, detail="Invalid X-Token header") + if item.id in fake_db: + raise HTTPException(status_code=400, detail="Item already exists") + fake_db[item.id] = item + return item diff --git a/docs_src/app_testing/app_b_an_py310/test_main.py b/docs_src/app_testing/app_b_an_py310/test_main.py new file mode 100644 index 000000000..d186b8ecb --- /dev/null +++ b/docs_src/app_testing/app_b_an_py310/test_main.py @@ -0,0 +1,65 @@ +from fastapi.testclient import TestClient + +from .main import app + +client = TestClient(app) + + +def test_read_item(): + response = client.get("/items/foo", headers={"X-Token": "coneofsilence"}) + assert response.status_code == 200 + assert response.json() == { + "id": "foo", + "title": "Foo", + "description": "There goes my hero", + } + + +def test_read_item_bad_token(): + response = client.get("/items/foo", headers={"X-Token": "hailhydra"}) + assert response.status_code == 400 + assert response.json() == {"detail": "Invalid X-Token header"} + + +def test_read_inexistent_item(): + response = client.get("/items/baz", headers={"X-Token": "coneofsilence"}) + assert response.status_code == 404 + assert response.json() == {"detail": "Item not found"} + + +def test_create_item(): + response = client.post( + "/items/", + headers={"X-Token": "coneofsilence"}, + json={"id": "foobar", "title": "Foo Bar", "description": "The Foo Barters"}, + ) + assert response.status_code == 200 + assert response.json() == { + "id": "foobar", + "title": "Foo Bar", + "description": "The Foo Barters", + } + + +def test_create_item_bad_token(): + response = client.post( + "/items/", + headers={"X-Token": "hailhydra"}, + json={"id": "bazz", "title": "Bazz", "description": "Drop the bazz"}, + ) + assert response.status_code == 400 + assert response.json() == {"detail": "Invalid X-Token header"} + + +def test_create_existing_item(): + response = client.post( + "/items/", + headers={"X-Token": "coneofsilence"}, + json={ + "id": "foo", + "title": "The Foo ID Stealers", + "description": "There goes my stealer", + }, + ) + assert response.status_code == 400 + assert response.json() == {"detail": "Item already exists"} diff --git a/docs_src/app_testing/app_b_an_py39/__init__.py b/docs_src/app_testing/app_b_an_py39/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/app_testing/app_b_an_py39/main.py b/docs_src/app_testing/app_b_an_py39/main.py new file mode 100644 index 000000000..935a510b7 --- /dev/null +++ b/docs_src/app_testing/app_b_an_py39/main.py @@ -0,0 +1,38 @@ +from typing import Annotated, Union + +from fastapi import FastAPI, Header, HTTPException +from pydantic import BaseModel + +fake_secret_token = "coneofsilence" + +fake_db = { + "foo": {"id": "foo", "title": "Foo", "description": "There goes my hero"}, + "bar": {"id": "bar", "title": "Bar", "description": "The bartenders"}, +} + +app = FastAPI() + + +class Item(BaseModel): + id: str + title: str + description: Union[str, None] = None + + +@app.get("/items/{item_id}", response_model=Item) +async def read_main(item_id: str, x_token: Annotated[str, Header()]): + if x_token != fake_secret_token: + raise HTTPException(status_code=400, detail="Invalid X-Token header") + if item_id not in fake_db: + raise HTTPException(status_code=404, detail="Item not found") + return fake_db[item_id] + + +@app.post("/items/", response_model=Item) +async def create_item(item: Item, x_token: Annotated[str, Header()]): + if x_token != fake_secret_token: + raise HTTPException(status_code=400, detail="Invalid X-Token header") + if item.id in fake_db: + raise HTTPException(status_code=400, detail="Item already exists") + fake_db[item.id] = item + return item diff --git a/docs_src/app_testing/app_b_an_py39/test_main.py b/docs_src/app_testing/app_b_an_py39/test_main.py new file mode 100644 index 000000000..d186b8ecb --- /dev/null +++ b/docs_src/app_testing/app_b_an_py39/test_main.py @@ -0,0 +1,65 @@ +from fastapi.testclient import TestClient + +from .main import app + +client = TestClient(app) + + +def test_read_item(): + response = client.get("/items/foo", headers={"X-Token": "coneofsilence"}) + assert response.status_code == 200 + assert response.json() == { + "id": "foo", + "title": "Foo", + "description": "There goes my hero", + } + + +def test_read_item_bad_token(): + response = client.get("/items/foo", headers={"X-Token": "hailhydra"}) + assert response.status_code == 400 + assert response.json() == {"detail": "Invalid X-Token header"} + + +def test_read_inexistent_item(): + response = client.get("/items/baz", headers={"X-Token": "coneofsilence"}) + assert response.status_code == 404 + assert response.json() == {"detail": "Item not found"} + + +def test_create_item(): + response = client.post( + "/items/", + headers={"X-Token": "coneofsilence"}, + json={"id": "foobar", "title": "Foo Bar", "description": "The Foo Barters"}, + ) + assert response.status_code == 200 + assert response.json() == { + "id": "foobar", + "title": "Foo Bar", + "description": "The Foo Barters", + } + + +def test_create_item_bad_token(): + response = client.post( + "/items/", + headers={"X-Token": "hailhydra"}, + json={"id": "bazz", "title": "Bazz", "description": "Drop the bazz"}, + ) + assert response.status_code == 400 + assert response.json() == {"detail": "Invalid X-Token header"} + + +def test_create_existing_item(): + response = client.post( + "/items/", + headers={"X-Token": "coneofsilence"}, + json={ + "id": "foo", + "title": "The Foo ID Stealers", + "description": "There goes my stealer", + }, + ) + assert response.status_code == 400 + assert response.json() == {"detail": "Item already exists"} diff --git a/docs_src/background_tasks/tutorial002_an.py b/docs_src/background_tasks/tutorial002_an.py new file mode 100644 index 000000000..f63502b09 --- /dev/null +++ b/docs_src/background_tasks/tutorial002_an.py @@ -0,0 +1,27 @@ +from typing import Union + +from fastapi import BackgroundTasks, Depends, FastAPI +from typing_extensions import Annotated + +app = FastAPI() + + +def write_log(message: str): + with open("log.txt", mode="a") as log: + log.write(message) + + +def get_query(background_tasks: BackgroundTasks, q: Union[str, None] = None): + if q: + message = f"found query: {q}\n" + background_tasks.add_task(write_log, message) + return q + + +@app.post("/send-notification/{email}") +async def send_notification( + email: str, background_tasks: BackgroundTasks, q: Annotated[str, Depends(get_query)] +): + message = f"message to {email}\n" + background_tasks.add_task(write_log, message) + return {"message": "Message sent"} diff --git a/docs_src/background_tasks/tutorial002_an_py310.py b/docs_src/background_tasks/tutorial002_an_py310.py new file mode 100644 index 000000000..1fc78fbc9 --- /dev/null +++ b/docs_src/background_tasks/tutorial002_an_py310.py @@ -0,0 +1,26 @@ +from typing import Annotated + +from fastapi import BackgroundTasks, Depends, FastAPI + +app = FastAPI() + + +def write_log(message: str): + with open("log.txt", mode="a") as log: + log.write(message) + + +def get_query(background_tasks: BackgroundTasks, q: str | None = None): + if q: + message = f"found query: {q}\n" + background_tasks.add_task(write_log, message) + return q + + +@app.post("/send-notification/{email}") +async def send_notification( + email: str, background_tasks: BackgroundTasks, q: Annotated[str, Depends(get_query)] +): + message = f"message to {email}\n" + background_tasks.add_task(write_log, message) + return {"message": "Message sent"} diff --git a/docs_src/background_tasks/tutorial002_an_py39.py b/docs_src/background_tasks/tutorial002_an_py39.py new file mode 100644 index 000000000..bfdd14875 --- /dev/null +++ b/docs_src/background_tasks/tutorial002_an_py39.py @@ -0,0 +1,26 @@ +from typing import Annotated, Union + +from fastapi import BackgroundTasks, Depends, FastAPI + +app = FastAPI() + + +def write_log(message: str): + with open("log.txt", mode="a") as log: + log.write(message) + + +def get_query(background_tasks: BackgroundTasks, q: Union[str, None] = None): + if q: + message = f"found query: {q}\n" + background_tasks.add_task(write_log, message) + return q + + +@app.post("/send-notification/{email}") +async def send_notification( + email: str, background_tasks: BackgroundTasks, q: Annotated[str, Depends(get_query)] +): + message = f"message to {email}\n" + background_tasks.add_task(write_log, message) + return {"message": "Message sent"} diff --git a/docs_src/bigger_applications/app_an/__init__.py b/docs_src/bigger_applications/app_an/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/bigger_applications/app_an/dependencies.py b/docs_src/bigger_applications/app_an/dependencies.py new file mode 100644 index 000000000..1374c54b3 --- /dev/null +++ b/docs_src/bigger_applications/app_an/dependencies.py @@ -0,0 +1,12 @@ +from fastapi import Header, HTTPException +from typing_extensions import Annotated + + +async def get_token_header(x_token: Annotated[str, Header()]): + if x_token != "fake-super-secret-token": + raise HTTPException(status_code=400, detail="X-Token header invalid") + + +async def get_query_token(token: str): + if token != "jessica": + raise HTTPException(status_code=400, detail="No Jessica token provided") diff --git a/docs_src/bigger_applications/app_an/internal/__init__.py b/docs_src/bigger_applications/app_an/internal/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/bigger_applications/app_an/internal/admin.py b/docs_src/bigger_applications/app_an/internal/admin.py new file mode 100644 index 000000000..99d3da86b --- /dev/null +++ b/docs_src/bigger_applications/app_an/internal/admin.py @@ -0,0 +1,8 @@ +from fastapi import APIRouter + +router = APIRouter() + + +@router.post("/") +async def update_admin(): + return {"message": "Admin getting schwifty"} diff --git a/docs_src/bigger_applications/app_an/main.py b/docs_src/bigger_applications/app_an/main.py new file mode 100644 index 000000000..ae544a3aa --- /dev/null +++ b/docs_src/bigger_applications/app_an/main.py @@ -0,0 +1,23 @@ +from fastapi import Depends, FastAPI + +from .dependencies import get_query_token, get_token_header +from .internal import admin +from .routers import items, users + +app = FastAPI(dependencies=[Depends(get_query_token)]) + + +app.include_router(users.router) +app.include_router(items.router) +app.include_router( + admin.router, + prefix="/admin", + tags=["admin"], + dependencies=[Depends(get_token_header)], + responses={418: {"description": "I'm a teapot"}}, +) + + +@app.get("/") +async def root(): + return {"message": "Hello Bigger Applications!"} diff --git a/docs_src/bigger_applications/app_an/routers/__init__.py b/docs_src/bigger_applications/app_an/routers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/bigger_applications/app_an/routers/items.py b/docs_src/bigger_applications/app_an/routers/items.py new file mode 100644 index 000000000..bde9ff4d5 --- /dev/null +++ b/docs_src/bigger_applications/app_an/routers/items.py @@ -0,0 +1,38 @@ +from fastapi import APIRouter, Depends, HTTPException + +from ..dependencies import get_token_header + +router = APIRouter( + prefix="/items", + tags=["items"], + dependencies=[Depends(get_token_header)], + responses={404: {"description": "Not found"}}, +) + + +fake_items_db = {"plumbus": {"name": "Plumbus"}, "gun": {"name": "Portal Gun"}} + + +@router.get("/") +async def read_items(): + return fake_items_db + + +@router.get("/{item_id}") +async def read_item(item_id: str): + if item_id not in fake_items_db: + raise HTTPException(status_code=404, detail="Item not found") + return {"name": fake_items_db[item_id]["name"], "item_id": item_id} + + +@router.put( + "/{item_id}", + tags=["custom"], + responses={403: {"description": "Operation forbidden"}}, +) +async def update_item(item_id: str): + if item_id != "plumbus": + raise HTTPException( + status_code=403, detail="You can only update the item: plumbus" + ) + return {"item_id": item_id, "name": "The great Plumbus"} diff --git a/docs_src/bigger_applications/app_an/routers/users.py b/docs_src/bigger_applications/app_an/routers/users.py new file mode 100644 index 000000000..39b3d7e7c --- /dev/null +++ b/docs_src/bigger_applications/app_an/routers/users.py @@ -0,0 +1,18 @@ +from fastapi import APIRouter + +router = APIRouter() + + +@router.get("/users/", tags=["users"]) +async def read_users(): + return [{"username": "Rick"}, {"username": "Morty"}] + + +@router.get("/users/me", tags=["users"]) +async def read_user_me(): + return {"username": "fakecurrentuser"} + + +@router.get("/users/{username}", tags=["users"]) +async def read_user(username: str): + return {"username": username} diff --git a/docs_src/bigger_applications/app_an_py39/__init__.py b/docs_src/bigger_applications/app_an_py39/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/bigger_applications/app_an_py39/dependencies.py b/docs_src/bigger_applications/app_an_py39/dependencies.py new file mode 100644 index 000000000..5c7612aa0 --- /dev/null +++ b/docs_src/bigger_applications/app_an_py39/dependencies.py @@ -0,0 +1,13 @@ +from typing import Annotated + +from fastapi import Header, HTTPException + + +async def get_token_header(x_token: Annotated[str, Header()]): + if x_token != "fake-super-secret-token": + raise HTTPException(status_code=400, detail="X-Token header invalid") + + +async def get_query_token(token: str): + if token != "jessica": + raise HTTPException(status_code=400, detail="No Jessica token provided") diff --git a/docs_src/bigger_applications/app_an_py39/internal/__init__.py b/docs_src/bigger_applications/app_an_py39/internal/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/bigger_applications/app_an_py39/internal/admin.py b/docs_src/bigger_applications/app_an_py39/internal/admin.py new file mode 100644 index 000000000..99d3da86b --- /dev/null +++ b/docs_src/bigger_applications/app_an_py39/internal/admin.py @@ -0,0 +1,8 @@ +from fastapi import APIRouter + +router = APIRouter() + + +@router.post("/") +async def update_admin(): + return {"message": "Admin getting schwifty"} diff --git a/docs_src/bigger_applications/app_an_py39/main.py b/docs_src/bigger_applications/app_an_py39/main.py new file mode 100644 index 000000000..ae544a3aa --- /dev/null +++ b/docs_src/bigger_applications/app_an_py39/main.py @@ -0,0 +1,23 @@ +from fastapi import Depends, FastAPI + +from .dependencies import get_query_token, get_token_header +from .internal import admin +from .routers import items, users + +app = FastAPI(dependencies=[Depends(get_query_token)]) + + +app.include_router(users.router) +app.include_router(items.router) +app.include_router( + admin.router, + prefix="/admin", + tags=["admin"], + dependencies=[Depends(get_token_header)], + responses={418: {"description": "I'm a teapot"}}, +) + + +@app.get("/") +async def root(): + return {"message": "Hello Bigger Applications!"} diff --git a/docs_src/bigger_applications/app_an_py39/routers/__init__.py b/docs_src/bigger_applications/app_an_py39/routers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/bigger_applications/app_an_py39/routers/items.py b/docs_src/bigger_applications/app_an_py39/routers/items.py new file mode 100644 index 000000000..bde9ff4d5 --- /dev/null +++ b/docs_src/bigger_applications/app_an_py39/routers/items.py @@ -0,0 +1,38 @@ +from fastapi import APIRouter, Depends, HTTPException + +from ..dependencies import get_token_header + +router = APIRouter( + prefix="/items", + tags=["items"], + dependencies=[Depends(get_token_header)], + responses={404: {"description": "Not found"}}, +) + + +fake_items_db = {"plumbus": {"name": "Plumbus"}, "gun": {"name": "Portal Gun"}} + + +@router.get("/") +async def read_items(): + return fake_items_db + + +@router.get("/{item_id}") +async def read_item(item_id: str): + if item_id not in fake_items_db: + raise HTTPException(status_code=404, detail="Item not found") + return {"name": fake_items_db[item_id]["name"], "item_id": item_id} + + +@router.put( + "/{item_id}", + tags=["custom"], + responses={403: {"description": "Operation forbidden"}}, +) +async def update_item(item_id: str): + if item_id != "plumbus": + raise HTTPException( + status_code=403, detail="You can only update the item: plumbus" + ) + return {"item_id": item_id, "name": "The great Plumbus"} diff --git a/docs_src/bigger_applications/app_an_py39/routers/users.py b/docs_src/bigger_applications/app_an_py39/routers/users.py new file mode 100644 index 000000000..39b3d7e7c --- /dev/null +++ b/docs_src/bigger_applications/app_an_py39/routers/users.py @@ -0,0 +1,18 @@ +from fastapi import APIRouter + +router = APIRouter() + + +@router.get("/users/", tags=["users"]) +async def read_users(): + return [{"username": "Rick"}, {"username": "Morty"}] + + +@router.get("/users/me", tags=["users"]) +async def read_user_me(): + return {"username": "fakecurrentuser"} + + +@router.get("/users/{username}", tags=["users"]) +async def read_user(username: str): + return {"username": username} diff --git a/docs_src/body_fields/tutorial001_an.py b/docs_src/body_fields/tutorial001_an.py new file mode 100644 index 000000000..15ea1b53d --- /dev/null +++ b/docs_src/body_fields/tutorial001_an.py @@ -0,0 +1,22 @@ +from typing import Union + +from fastapi import Body, FastAPI +from pydantic import BaseModel, Field +from typing_extensions import Annotated + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Union[str, None] = Field( + default=None, title="The description of the item", max_length=300 + ) + price: float = Field(gt=0, description="The price must be greater than zero") + tax: Union[float, None] = None + + +@app.put("/items/{item_id}") +async def update_item(item_id: int, item: Annotated[Item, Body(embed=True)]): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/body_fields/tutorial001_an_py310.py b/docs_src/body_fields/tutorial001_an_py310.py new file mode 100644 index 000000000..c9d99e1c2 --- /dev/null +++ b/docs_src/body_fields/tutorial001_an_py310.py @@ -0,0 +1,21 @@ +from typing import Annotated + +from fastapi import Body, FastAPI +from pydantic import BaseModel, Field + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = Field( + default=None, title="The description of the item", max_length=300 + ) + price: float = Field(gt=0, description="The price must be greater than zero") + tax: float | None = None + + +@app.put("/items/{item_id}") +async def update_item(item_id: int, item: Annotated[Item, Body(embed=True)]): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/body_fields/tutorial001_an_py39.py b/docs_src/body_fields/tutorial001_an_py39.py new file mode 100644 index 000000000..6ef14470c --- /dev/null +++ b/docs_src/body_fields/tutorial001_an_py39.py @@ -0,0 +1,21 @@ +from typing import Annotated, Union + +from fastapi import Body, FastAPI +from pydantic import BaseModel, Field + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Union[str, None] = Field( + default=None, title="The description of the item", max_length=300 + ) + price: float = Field(gt=0, description="The price must be greater than zero") + tax: Union[float, None] = None + + +@app.put("/items/{item_id}") +async def update_item(item_id: int, item: Annotated[Item, Body(embed=True)]): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/body_multiple_params/tutorial001_an.py b/docs_src/body_multiple_params/tutorial001_an.py new file mode 100644 index 000000000..308eee854 --- /dev/null +++ b/docs_src/body_multiple_params/tutorial001_an.py @@ -0,0 +1,28 @@ +from typing import Union + +from fastapi import FastAPI, Path +from pydantic import BaseModel +from typing_extensions import Annotated + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + price: float + tax: Union[float, None] = None + + +@app.put("/items/{item_id}") +async def update_item( + item_id: Annotated[int, Path(title="The ID of the item to get", ge=0, le=1000)], + q: Union[str, None] = None, + item: Union[Item, None] = None, +): + results = {"item_id": item_id} + if q: + results.update({"q": q}) + if item: + results.update({"item": item}) + return results diff --git a/docs_src/body_multiple_params/tutorial001_an_py310.py b/docs_src/body_multiple_params/tutorial001_an_py310.py new file mode 100644 index 000000000..2d79c19c2 --- /dev/null +++ b/docs_src/body_multiple_params/tutorial001_an_py310.py @@ -0,0 +1,27 @@ +from typing import Annotated + +from fastapi import FastAPI, Path +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + + +@app.put("/items/{item_id}") +async def update_item( + item_id: Annotated[int, Path(title="The ID of the item to get", ge=0, le=1000)], + q: str | None = None, + item: Item | None = None, +): + results = {"item_id": item_id} + if q: + results.update({"q": q}) + if item: + results.update({"item": item}) + return results diff --git a/docs_src/body_multiple_params/tutorial001_an_py39.py b/docs_src/body_multiple_params/tutorial001_an_py39.py new file mode 100644 index 000000000..1c0ac3a7b --- /dev/null +++ b/docs_src/body_multiple_params/tutorial001_an_py39.py @@ -0,0 +1,27 @@ +from typing import Annotated, Union + +from fastapi import FastAPI, Path +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + price: float + tax: Union[float, None] = None + + +@app.put("/items/{item_id}") +async def update_item( + item_id: Annotated[int, Path(title="The ID of the item to get", ge=0, le=1000)], + q: Union[str, None] = None, + item: Union[Item, None] = None, +): + results = {"item_id": item_id} + if q: + results.update({"q": q}) + if item: + results.update({"item": item}) + return results diff --git a/docs_src/body_multiple_params/tutorial003_an.py b/docs_src/body_multiple_params/tutorial003_an.py new file mode 100644 index 000000000..39ef7340a --- /dev/null +++ b/docs_src/body_multiple_params/tutorial003_an.py @@ -0,0 +1,27 @@ +from typing import Union + +from fastapi import Body, FastAPI +from pydantic import BaseModel +from typing_extensions import Annotated + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + price: float + tax: Union[float, None] = None + + +class User(BaseModel): + username: str + full_name: Union[str, None] = None + + +@app.put("/items/{item_id}") +async def update_item( + item_id: int, item: Item, user: User, importance: Annotated[int, Body()] +): + results = {"item_id": item_id, "item": item, "user": user, "importance": importance} + return results diff --git a/docs_src/body_multiple_params/tutorial003_an_py310.py b/docs_src/body_multiple_params/tutorial003_an_py310.py new file mode 100644 index 000000000..593ff893c --- /dev/null +++ b/docs_src/body_multiple_params/tutorial003_an_py310.py @@ -0,0 +1,26 @@ +from typing import Annotated + +from fastapi import Body, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + + +class User(BaseModel): + username: str + full_name: str | None = None + + +@app.put("/items/{item_id}") +async def update_item( + item_id: int, item: Item, user: User, importance: Annotated[int, Body()] +): + results = {"item_id": item_id, "item": item, "user": user, "importance": importance} + return results diff --git a/docs_src/body_multiple_params/tutorial003_an_py39.py b/docs_src/body_multiple_params/tutorial003_an_py39.py new file mode 100644 index 000000000..042351e0b --- /dev/null +++ b/docs_src/body_multiple_params/tutorial003_an_py39.py @@ -0,0 +1,26 @@ +from typing import Annotated, Union + +from fastapi import Body, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + price: float + tax: Union[float, None] = None + + +class User(BaseModel): + username: str + full_name: Union[str, None] = None + + +@app.put("/items/{item_id}") +async def update_item( + item_id: int, item: Item, user: User, importance: Annotated[int, Body()] +): + results = {"item_id": item_id, "item": item, "user": user, "importance": importance} + return results diff --git a/docs_src/body_multiple_params/tutorial004_an.py b/docs_src/body_multiple_params/tutorial004_an.py new file mode 100644 index 000000000..f6830f392 --- /dev/null +++ b/docs_src/body_multiple_params/tutorial004_an.py @@ -0,0 +1,34 @@ +from typing import Union + +from fastapi import Body, FastAPI +from pydantic import BaseModel +from typing_extensions import Annotated + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + price: float + tax: Union[float, None] = None + + +class User(BaseModel): + username: str + full_name: Union[str, None] = None + + +@app.put("/items/{item_id}") +async def update_item( + *, + item_id: int, + item: Item, + user: User, + importance: Annotated[int, Body(gt=0)], + q: Union[str, None] = None, +): + results = {"item_id": item_id, "item": item, "user": user, "importance": importance} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/body_multiple_params/tutorial004_an_py310.py b/docs_src/body_multiple_params/tutorial004_an_py310.py new file mode 100644 index 000000000..cd5c66fef --- /dev/null +++ b/docs_src/body_multiple_params/tutorial004_an_py310.py @@ -0,0 +1,33 @@ +from typing import Annotated + +from fastapi import Body, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + + +class User(BaseModel): + username: str + full_name: str | None = None + + +@app.put("/items/{item_id}") +async def update_item( + *, + item_id: int, + item: Item, + user: User, + importance: Annotated[int, Body(gt=0)], + q: str | None = None, +): + results = {"item_id": item_id, "item": item, "user": user, "importance": importance} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/body_multiple_params/tutorial004_an_py39.py b/docs_src/body_multiple_params/tutorial004_an_py39.py new file mode 100644 index 000000000..567427c03 --- /dev/null +++ b/docs_src/body_multiple_params/tutorial004_an_py39.py @@ -0,0 +1,33 @@ +from typing import Annotated, Union + +from fastapi import Body, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + price: float + tax: Union[float, None] = None + + +class User(BaseModel): + username: str + full_name: Union[str, None] = None + + +@app.put("/items/{item_id}") +async def update_item( + *, + item_id: int, + item: Item, + user: User, + importance: Annotated[int, Body(gt=0)], + q: Union[str, None] = None, +): + results = {"item_id": item_id, "item": item, "user": user, "importance": importance} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/body_multiple_params/tutorial005_an.py b/docs_src/body_multiple_params/tutorial005_an.py new file mode 100644 index 000000000..dadde80b5 --- /dev/null +++ b/docs_src/body_multiple_params/tutorial005_an.py @@ -0,0 +1,20 @@ +from typing import Union + +from fastapi import Body, FastAPI +from pydantic import BaseModel +from typing_extensions import Annotated + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + price: float + tax: Union[float, None] = None + + +@app.put("/items/{item_id}") +async def update_item(item_id: int, item: Annotated[Item, Body(embed=True)]): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/body_multiple_params/tutorial005_an_py310.py b/docs_src/body_multiple_params/tutorial005_an_py310.py new file mode 100644 index 000000000..f97680ba0 --- /dev/null +++ b/docs_src/body_multiple_params/tutorial005_an_py310.py @@ -0,0 +1,19 @@ +from typing import Annotated + +from fastapi import Body, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + + +@app.put("/items/{item_id}") +async def update_item(item_id: int, item: Annotated[Item, Body(embed=True)]): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/body_multiple_params/tutorial005_an_py39.py b/docs_src/body_multiple_params/tutorial005_an_py39.py new file mode 100644 index 000000000..9a52425c1 --- /dev/null +++ b/docs_src/body_multiple_params/tutorial005_an_py39.py @@ -0,0 +1,19 @@ +from typing import Annotated, Union + +from fastapi import Body, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + price: float + tax: Union[float, None] = None + + +@app.put("/items/{item_id}") +async def update_item(item_id: int, item: Annotated[Item, Body(embed=True)]): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/cookie_params/tutorial001_an.py b/docs_src/cookie_params/tutorial001_an.py new file mode 100644 index 000000000..6d5931229 --- /dev/null +++ b/docs_src/cookie_params/tutorial001_an.py @@ -0,0 +1,11 @@ +from typing import Union + +from fastapi import Cookie, FastAPI +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items(ads_id: Annotated[Union[str, None], Cookie()] = None): + return {"ads_id": ads_id} diff --git a/docs_src/cookie_params/tutorial001_an_py310.py b/docs_src/cookie_params/tutorial001_an_py310.py new file mode 100644 index 000000000..69ac683fe --- /dev/null +++ b/docs_src/cookie_params/tutorial001_an_py310.py @@ -0,0 +1,10 @@ +from typing import Annotated + +from fastapi import Cookie, FastAPI + +app = FastAPI() + + +@app.get("/items/") +async def read_items(ads_id: Annotated[str | None, Cookie()] = None): + return {"ads_id": ads_id} diff --git a/docs_src/cookie_params/tutorial001_an_py39.py b/docs_src/cookie_params/tutorial001_an_py39.py new file mode 100644 index 000000000..e18d0a332 --- /dev/null +++ b/docs_src/cookie_params/tutorial001_an_py39.py @@ -0,0 +1,10 @@ +from typing import Annotated, Union + +from fastapi import Cookie, FastAPI + +app = FastAPI() + + +@app.get("/items/") +async def read_items(ads_id: Annotated[Union[str, None], Cookie()] = None): + return {"ads_id": ads_id} diff --git a/docs_src/dependencies/tutorial001_02_an.py b/docs_src/dependencies/tutorial001_02_an.py new file mode 100644 index 000000000..455d60c82 --- /dev/null +++ b/docs_src/dependencies/tutorial001_02_an.py @@ -0,0 +1,25 @@ +from typing import Union + +from fastapi import Depends, FastAPI +from typing_extensions import Annotated + +app = FastAPI() + + +async def common_parameters( + q: Union[str, None] = None, skip: int = 0, limit: int = 100 +): + return {"q": q, "skip": skip, "limit": limit} + + +CommonsDep = Annotated[dict, Depends(common_parameters)] + + +@app.get("/items/") +async def read_items(commons: CommonsDep): + return commons + + +@app.get("/users/") +async def read_users(commons: CommonsDep): + return commons diff --git a/docs_src/dependencies/tutorial001_02_an_py310.py b/docs_src/dependencies/tutorial001_02_an_py310.py new file mode 100644 index 000000000..f59637bb2 --- /dev/null +++ b/docs_src/dependencies/tutorial001_02_an_py310.py @@ -0,0 +1,22 @@ +from typing import Annotated + +from fastapi import Depends, FastAPI + +app = FastAPI() + + +async def common_parameters(q: str | None = None, skip: int = 0, limit: int = 100): + return {"q": q, "skip": skip, "limit": limit} + + +CommonsDep = Annotated[dict, Depends(common_parameters)] + + +@app.get("/items/") +async def read_items(commons: CommonsDep): + return commons + + +@app.get("/users/") +async def read_users(commons: CommonsDep): + return commons diff --git a/docs_src/dependencies/tutorial001_02_an_py39.py b/docs_src/dependencies/tutorial001_02_an_py39.py new file mode 100644 index 000000000..df969ae9c --- /dev/null +++ b/docs_src/dependencies/tutorial001_02_an_py39.py @@ -0,0 +1,24 @@ +from typing import Annotated, Union + +from fastapi import Depends, FastAPI + +app = FastAPI() + + +async def common_parameters( + q: Union[str, None] = None, skip: int = 0, limit: int = 100 +): + return {"q": q, "skip": skip, "limit": limit} + + +CommonsDep = Annotated[dict, Depends(common_parameters)] + + +@app.get("/items/") +async def read_items(commons: CommonsDep): + return commons + + +@app.get("/users/") +async def read_users(commons: CommonsDep): + return commons diff --git a/docs_src/dependencies/tutorial001_an.py b/docs_src/dependencies/tutorial001_an.py new file mode 100644 index 000000000..81e24fe86 --- /dev/null +++ b/docs_src/dependencies/tutorial001_an.py @@ -0,0 +1,22 @@ +from typing import Union + +from fastapi import Depends, FastAPI +from typing_extensions import Annotated + +app = FastAPI() + + +async def common_parameters( + q: Union[str, None] = None, skip: int = 0, limit: int = 100 +): + return {"q": q, "skip": skip, "limit": limit} + + +@app.get("/items/") +async def read_items(commons: Annotated[dict, Depends(common_parameters)]): + return commons + + +@app.get("/users/") +async def read_users(commons: Annotated[dict, Depends(common_parameters)]): + return commons diff --git a/docs_src/dependencies/tutorial001_an_py310.py b/docs_src/dependencies/tutorial001_an_py310.py new file mode 100644 index 000000000..f2e57ae7b --- /dev/null +++ b/docs_src/dependencies/tutorial001_an_py310.py @@ -0,0 +1,19 @@ +from typing import Annotated + +from fastapi import Depends, FastAPI + +app = FastAPI() + + +async def common_parameters(q: str | None = None, skip: int = 0, limit: int = 100): + return {"q": q, "skip": skip, "limit": limit} + + +@app.get("/items/") +async def read_items(commons: Annotated[dict, Depends(common_parameters)]): + return commons + + +@app.get("/users/") +async def read_users(commons: Annotated[dict, Depends(common_parameters)]): + return commons diff --git a/docs_src/dependencies/tutorial001_an_py39.py b/docs_src/dependencies/tutorial001_an_py39.py new file mode 100644 index 000000000..5d9fe6ddf --- /dev/null +++ b/docs_src/dependencies/tutorial001_an_py39.py @@ -0,0 +1,21 @@ +from typing import Annotated, Union + +from fastapi import Depends, FastAPI + +app = FastAPI() + + +async def common_parameters( + q: Union[str, None] = None, skip: int = 0, limit: int = 100 +): + return {"q": q, "skip": skip, "limit": limit} + + +@app.get("/items/") +async def read_items(commons: Annotated[dict, Depends(common_parameters)]): + return commons + + +@app.get("/users/") +async def read_users(commons: Annotated[dict, Depends(common_parameters)]): + return commons diff --git a/docs_src/dependencies/tutorial002_an.py b/docs_src/dependencies/tutorial002_an.py new file mode 100644 index 000000000..964ccf66c --- /dev/null +++ b/docs_src/dependencies/tutorial002_an.py @@ -0,0 +1,26 @@ +from typing import Union + +from fastapi import Depends, FastAPI +from typing_extensions import Annotated + +app = FastAPI() + + +fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}] + + +class CommonQueryParams: + def __init__(self, q: Union[str, None] = None, skip: int = 0, limit: int = 100): + self.q = q + self.skip = skip + self.limit = limit + + +@app.get("/items/") +async def read_items(commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)]): + response = {} + if commons.q: + response.update({"q": commons.q}) + items = fake_items_db[commons.skip : commons.skip + commons.limit] + response.update({"items": items}) + return response diff --git a/docs_src/dependencies/tutorial002_an_py310.py b/docs_src/dependencies/tutorial002_an_py310.py new file mode 100644 index 000000000..077726312 --- /dev/null +++ b/docs_src/dependencies/tutorial002_an_py310.py @@ -0,0 +1,25 @@ +from typing import Annotated + +from fastapi import Depends, FastAPI + +app = FastAPI() + + +fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}] + + +class CommonQueryParams: + def __init__(self, q: str | None = None, skip: int = 0, limit: int = 100): + self.q = q + self.skip = skip + self.limit = limit + + +@app.get("/items/") +async def read_items(commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)]): + response = {} + if commons.q: + response.update({"q": commons.q}) + items = fake_items_db[commons.skip : commons.skip + commons.limit] + response.update({"items": items}) + return response diff --git a/docs_src/dependencies/tutorial002_an_py39.py b/docs_src/dependencies/tutorial002_an_py39.py new file mode 100644 index 000000000..844a23c5a --- /dev/null +++ b/docs_src/dependencies/tutorial002_an_py39.py @@ -0,0 +1,25 @@ +from typing import Annotated, Union + +from fastapi import Depends, FastAPI + +app = FastAPI() + + +fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}] + + +class CommonQueryParams: + def __init__(self, q: Union[str, None] = None, skip: int = 0, limit: int = 100): + self.q = q + self.skip = skip + self.limit = limit + + +@app.get("/items/") +async def read_items(commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)]): + response = {} + if commons.q: + response.update({"q": commons.q}) + items = fake_items_db[commons.skip : commons.skip + commons.limit] + response.update({"items": items}) + return response diff --git a/docs_src/dependencies/tutorial003_an.py b/docs_src/dependencies/tutorial003_an.py new file mode 100644 index 000000000..ba8e9f717 --- /dev/null +++ b/docs_src/dependencies/tutorial003_an.py @@ -0,0 +1,26 @@ +from typing import Any, Union + +from fastapi import Depends, FastAPI +from typing_extensions import Annotated + +app = FastAPI() + + +fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}] + + +class CommonQueryParams: + def __init__(self, q: Union[str, None] = None, skip: int = 0, limit: int = 100): + self.q = q + self.skip = skip + self.limit = limit + + +@app.get("/items/") +async def read_items(commons: Annotated[Any, Depends(CommonQueryParams)]): + response = {} + if commons.q: + response.update({"q": commons.q}) + items = fake_items_db[commons.skip : commons.skip + commons.limit] + response.update({"items": items}) + return response diff --git a/docs_src/dependencies/tutorial003_an_py310.py b/docs_src/dependencies/tutorial003_an_py310.py new file mode 100644 index 000000000..44116989b --- /dev/null +++ b/docs_src/dependencies/tutorial003_an_py310.py @@ -0,0 +1,25 @@ +from typing import Annotated, Any + +from fastapi import Depends, FastAPI + +app = FastAPI() + + +fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}] + + +class CommonQueryParams: + def __init__(self, q: str | None = None, skip: int = 0, limit: int = 100): + self.q = q + self.skip = skip + self.limit = limit + + +@app.get("/items/") +async def read_items(commons: Annotated[Any, Depends(CommonQueryParams)]): + response = {} + if commons.q: + response.update({"q": commons.q}) + items = fake_items_db[commons.skip : commons.skip + commons.limit] + response.update({"items": items}) + return response diff --git a/docs_src/dependencies/tutorial003_an_py39.py b/docs_src/dependencies/tutorial003_an_py39.py new file mode 100644 index 000000000..9e9123ad2 --- /dev/null +++ b/docs_src/dependencies/tutorial003_an_py39.py @@ -0,0 +1,25 @@ +from typing import Annotated, Any, Union + +from fastapi import Depends, FastAPI + +app = FastAPI() + + +fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}] + + +class CommonQueryParams: + def __init__(self, q: Union[str, None] = None, skip: int = 0, limit: int = 100): + self.q = q + self.skip = skip + self.limit = limit + + +@app.get("/items/") +async def read_items(commons: Annotated[Any, Depends(CommonQueryParams)]): + response = {} + if commons.q: + response.update({"q": commons.q}) + items = fake_items_db[commons.skip : commons.skip + commons.limit] + response.update({"items": items}) + return response diff --git a/docs_src/dependencies/tutorial004_an.py b/docs_src/dependencies/tutorial004_an.py new file mode 100644 index 000000000..78881a354 --- /dev/null +++ b/docs_src/dependencies/tutorial004_an.py @@ -0,0 +1,26 @@ +from typing import Union + +from fastapi import Depends, FastAPI +from typing_extensions import Annotated + +app = FastAPI() + + +fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}] + + +class CommonQueryParams: + def __init__(self, q: Union[str, None] = None, skip: int = 0, limit: int = 100): + self.q = q + self.skip = skip + self.limit = limit + + +@app.get("/items/") +async def read_items(commons: Annotated[CommonQueryParams, Depends()]): + response = {} + if commons.q: + response.update({"q": commons.q}) + items = fake_items_db[commons.skip : commons.skip + commons.limit] + response.update({"items": items}) + return response diff --git a/docs_src/dependencies/tutorial004_an_py310.py b/docs_src/dependencies/tutorial004_an_py310.py new file mode 100644 index 000000000..2c009efcc --- /dev/null +++ b/docs_src/dependencies/tutorial004_an_py310.py @@ -0,0 +1,25 @@ +from typing import Annotated + +from fastapi import Depends, FastAPI + +app = FastAPI() + + +fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}] + + +class CommonQueryParams: + def __init__(self, q: str | None = None, skip: int = 0, limit: int = 100): + self.q = q + self.skip = skip + self.limit = limit + + +@app.get("/items/") +async def read_items(commons: Annotated[CommonQueryParams, Depends()]): + response = {} + if commons.q: + response.update({"q": commons.q}) + items = fake_items_db[commons.skip : commons.skip + commons.limit] + response.update({"items": items}) + return response diff --git a/docs_src/dependencies/tutorial004_an_py39.py b/docs_src/dependencies/tutorial004_an_py39.py new file mode 100644 index 000000000..74268626b --- /dev/null +++ b/docs_src/dependencies/tutorial004_an_py39.py @@ -0,0 +1,25 @@ +from typing import Annotated, Union + +from fastapi import Depends, FastAPI + +app = FastAPI() + + +fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}] + + +class CommonQueryParams: + def __init__(self, q: Union[str, None] = None, skip: int = 0, limit: int = 100): + self.q = q + self.skip = skip + self.limit = limit + + +@app.get("/items/") +async def read_items(commons: Annotated[CommonQueryParams, Depends()]): + response = {} + if commons.q: + response.update({"q": commons.q}) + items = fake_items_db[commons.skip : commons.skip + commons.limit] + response.update({"items": items}) + return response diff --git a/docs_src/dependencies/tutorial005_an.py b/docs_src/dependencies/tutorial005_an.py new file mode 100644 index 000000000..6785099da --- /dev/null +++ b/docs_src/dependencies/tutorial005_an.py @@ -0,0 +1,26 @@ +from typing import Union + +from fastapi import Cookie, Depends, FastAPI +from typing_extensions import Annotated + +app = FastAPI() + + +def query_extractor(q: Union[str, None] = None): + return q + + +def query_or_cookie_extractor( + q: Annotated[str, Depends(query_extractor)], + last_query: Annotated[Union[str, None], Cookie()] = None, +): + if not q: + return last_query + return q + + +@app.get("/items/") +async def read_query( + query_or_default: Annotated[str, Depends(query_or_cookie_extractor)] +): + return {"q_or_cookie": query_or_default} diff --git a/docs_src/dependencies/tutorial005_an_py310.py b/docs_src/dependencies/tutorial005_an_py310.py new file mode 100644 index 000000000..6c0aa0b36 --- /dev/null +++ b/docs_src/dependencies/tutorial005_an_py310.py @@ -0,0 +1,25 @@ +from typing import Annotated + +from fastapi import Cookie, Depends, FastAPI + +app = FastAPI() + + +def query_extractor(q: str | None = None): + return q + + +def query_or_cookie_extractor( + q: Annotated[str, Depends(query_extractor)], + last_query: Annotated[str | None, Cookie()] = None, +): + if not q: + return last_query + return q + + +@app.get("/items/") +async def read_query( + query_or_default: Annotated[str, Depends(query_or_cookie_extractor)] +): + return {"q_or_cookie": query_or_default} diff --git a/docs_src/dependencies/tutorial005_an_py39.py b/docs_src/dependencies/tutorial005_an_py39.py new file mode 100644 index 000000000..e8887e162 --- /dev/null +++ b/docs_src/dependencies/tutorial005_an_py39.py @@ -0,0 +1,25 @@ +from typing import Annotated, Union + +from fastapi import Cookie, Depends, FastAPI + +app = FastAPI() + + +def query_extractor(q: Union[str, None] = None): + return q + + +def query_or_cookie_extractor( + q: Annotated[str, Depends(query_extractor)], + last_query: Annotated[Union[str, None], Cookie()] = None, +): + if not q: + return last_query + return q + + +@app.get("/items/") +async def read_query( + query_or_default: Annotated[str, Depends(query_or_cookie_extractor)] +): + return {"q_or_cookie": query_or_default} diff --git a/docs_src/dependencies/tutorial006_an.py b/docs_src/dependencies/tutorial006_an.py new file mode 100644 index 000000000..5aaea04d1 --- /dev/null +++ b/docs_src/dependencies/tutorial006_an.py @@ -0,0 +1,20 @@ +from fastapi import Depends, FastAPI, Header, HTTPException +from typing_extensions import Annotated + +app = FastAPI() + + +async def verify_token(x_token: Annotated[str, Header()]): + if x_token != "fake-super-secret-token": + raise HTTPException(status_code=400, detail="X-Token header invalid") + + +async def verify_key(x_key: Annotated[str, Header()]): + if x_key != "fake-super-secret-key": + raise HTTPException(status_code=400, detail="X-Key header invalid") + return x_key + + +@app.get("/items/", dependencies=[Depends(verify_token), Depends(verify_key)]) +async def read_items(): + return [{"item": "Foo"}, {"item": "Bar"}] diff --git a/docs_src/dependencies/tutorial006_an_py39.py b/docs_src/dependencies/tutorial006_an_py39.py new file mode 100644 index 000000000..11976ed6a --- /dev/null +++ b/docs_src/dependencies/tutorial006_an_py39.py @@ -0,0 +1,21 @@ +from typing import Annotated + +from fastapi import Depends, FastAPI, Header, HTTPException + +app = FastAPI() + + +async def verify_token(x_token: Annotated[str, Header()]): + if x_token != "fake-super-secret-token": + raise HTTPException(status_code=400, detail="X-Token header invalid") + + +async def verify_key(x_key: Annotated[str, Header()]): + if x_key != "fake-super-secret-key": + raise HTTPException(status_code=400, detail="X-Key header invalid") + return x_key + + +@app.get("/items/", dependencies=[Depends(verify_token), Depends(verify_key)]) +async def read_items(): + return [{"item": "Foo"}, {"item": "Bar"}] diff --git a/docs_src/dependencies/tutorial008_an.py b/docs_src/dependencies/tutorial008_an.py new file mode 100644 index 000000000..2de86f042 --- /dev/null +++ b/docs_src/dependencies/tutorial008_an.py @@ -0,0 +1,26 @@ +from fastapi import Depends +from typing_extensions import Annotated + + +async def dependency_a(): + dep_a = generate_dep_a() + try: + yield dep_a + finally: + dep_a.close() + + +async def dependency_b(dep_a: Annotated[DepA, Depends(dependency_a)]): + dep_b = generate_dep_b() + try: + yield dep_b + finally: + dep_b.close(dep_a) + + +async def dependency_c(dep_b: Annotated[DepB, Depends(dependency_b)]): + dep_c = generate_dep_c() + try: + yield dep_c + finally: + dep_c.close(dep_b) diff --git a/docs_src/dependencies/tutorial008_an_py39.py b/docs_src/dependencies/tutorial008_an_py39.py new file mode 100644 index 000000000..acc804c32 --- /dev/null +++ b/docs_src/dependencies/tutorial008_an_py39.py @@ -0,0 +1,27 @@ +from typing import Annotated + +from fastapi import Depends + + +async def dependency_a(): + dep_a = generate_dep_a() + try: + yield dep_a + finally: + dep_a.close() + + +async def dependency_b(dep_a: Annotated[DepA, Depends(dependency_a)]): + dep_b = generate_dep_b() + try: + yield dep_b + finally: + dep_b.close(dep_a) + + +async def dependency_c(dep_b: Annotated[DepB, Depends(dependency_b)]): + dep_c = generate_dep_c() + try: + yield dep_c + finally: + dep_c.close(dep_b) diff --git a/docs_src/dependencies/tutorial011_an.py b/docs_src/dependencies/tutorial011_an.py new file mode 100644 index 000000000..6c13d9033 --- /dev/null +++ b/docs_src/dependencies/tutorial011_an.py @@ -0,0 +1,22 @@ +from fastapi import Depends, FastAPI +from typing_extensions import Annotated + +app = FastAPI() + + +class FixedContentQueryChecker: + def __init__(self, fixed_content: str): + self.fixed_content = fixed_content + + def __call__(self, q: str = ""): + if q: + return self.fixed_content in q + return False + + +checker = FixedContentQueryChecker("bar") + + +@app.get("/query-checker/") +async def read_query_check(fixed_content_included: Annotated[bool, Depends(checker)]): + return {"fixed_content_in_query": fixed_content_included} diff --git a/docs_src/dependencies/tutorial011_an_py39.py b/docs_src/dependencies/tutorial011_an_py39.py new file mode 100644 index 000000000..68b7434ec --- /dev/null +++ b/docs_src/dependencies/tutorial011_an_py39.py @@ -0,0 +1,23 @@ +from typing import Annotated + +from fastapi import Depends, FastAPI + +app = FastAPI() + + +class FixedContentQueryChecker: + def __init__(self, fixed_content: str): + self.fixed_content = fixed_content + + def __call__(self, q: str = ""): + if q: + return self.fixed_content in q + return False + + +checker = FixedContentQueryChecker("bar") + + +@app.get("/query-checker/") +async def read_query_check(fixed_content_included: Annotated[bool, Depends(checker)]): + return {"fixed_content_in_query": fixed_content_included} diff --git a/docs_src/dependencies/tutorial012_an.py b/docs_src/dependencies/tutorial012_an.py new file mode 100644 index 000000000..7541e6bf4 --- /dev/null +++ b/docs_src/dependencies/tutorial012_an.py @@ -0,0 +1,26 @@ +from fastapi import Depends, FastAPI, Header, HTTPException +from typing_extensions import Annotated + + +async def verify_token(x_token: Annotated[str, Header()]): + if x_token != "fake-super-secret-token": + raise HTTPException(status_code=400, detail="X-Token header invalid") + + +async def verify_key(x_key: Annotated[str, Header()]): + if x_key != "fake-super-secret-key": + raise HTTPException(status_code=400, detail="X-Key header invalid") + return x_key + + +app = FastAPI(dependencies=[Depends(verify_token), Depends(verify_key)]) + + +@app.get("/items/") +async def read_items(): + return [{"item": "Portal Gun"}, {"item": "Plumbus"}] + + +@app.get("/users/") +async def read_users(): + return [{"username": "Rick"}, {"username": "Morty"}] diff --git a/docs_src/dependencies/tutorial012_an_py39.py b/docs_src/dependencies/tutorial012_an_py39.py new file mode 100644 index 000000000..7541e6bf4 --- /dev/null +++ b/docs_src/dependencies/tutorial012_an_py39.py @@ -0,0 +1,26 @@ +from fastapi import Depends, FastAPI, Header, HTTPException +from typing_extensions import Annotated + + +async def verify_token(x_token: Annotated[str, Header()]): + if x_token != "fake-super-secret-token": + raise HTTPException(status_code=400, detail="X-Token header invalid") + + +async def verify_key(x_key: Annotated[str, Header()]): + if x_key != "fake-super-secret-key": + raise HTTPException(status_code=400, detail="X-Key header invalid") + return x_key + + +app = FastAPI(dependencies=[Depends(verify_token), Depends(verify_key)]) + + +@app.get("/items/") +async def read_items(): + return [{"item": "Portal Gun"}, {"item": "Plumbus"}] + + +@app.get("/users/") +async def read_users(): + return [{"username": "Rick"}, {"username": "Morty"}] diff --git a/docs_src/dependency_testing/tutorial001_an.py b/docs_src/dependency_testing/tutorial001_an.py new file mode 100644 index 000000000..4c76a87ff --- /dev/null +++ b/docs_src/dependency_testing/tutorial001_an.py @@ -0,0 +1,60 @@ +from typing import Union + +from fastapi import Depends, FastAPI +from fastapi.testclient import TestClient +from typing_extensions import Annotated + +app = FastAPI() + + +async def common_parameters( + q: Union[str, None] = None, skip: int = 0, limit: int = 100 +): + return {"q": q, "skip": skip, "limit": limit} + + +@app.get("/items/") +async def read_items(commons: Annotated[dict, Depends(common_parameters)]): + return {"message": "Hello Items!", "params": commons} + + +@app.get("/users/") +async def read_users(commons: Annotated[dict, Depends(common_parameters)]): + return {"message": "Hello Users!", "params": commons} + + +client = TestClient(app) + + +async def override_dependency(q: Union[str, None] = None): + return {"q": q, "skip": 5, "limit": 10} + + +app.dependency_overrides[common_parameters] = override_dependency + + +def test_override_in_items(): + response = client.get("/items/") + assert response.status_code == 200 + assert response.json() == { + "message": "Hello Items!", + "params": {"q": None, "skip": 5, "limit": 10}, + } + + +def test_override_in_items_with_q(): + response = client.get("/items/?q=foo") + assert response.status_code == 200 + assert response.json() == { + "message": "Hello Items!", + "params": {"q": "foo", "skip": 5, "limit": 10}, + } + + +def test_override_in_items_with_params(): + response = client.get("/items/?q=foo&skip=100&limit=200") + assert response.status_code == 200 + assert response.json() == { + "message": "Hello Items!", + "params": {"q": "foo", "skip": 5, "limit": 10}, + } diff --git a/docs_src/dependency_testing/tutorial001_an_py310.py b/docs_src/dependency_testing/tutorial001_an_py310.py new file mode 100644 index 000000000..f866e7a1b --- /dev/null +++ b/docs_src/dependency_testing/tutorial001_an_py310.py @@ -0,0 +1,57 @@ +from typing import Annotated + +from fastapi import Depends, FastAPI +from fastapi.testclient import TestClient + +app = FastAPI() + + +async def common_parameters(q: str | None = None, skip: int = 0, limit: int = 100): + return {"q": q, "skip": skip, "limit": limit} + + +@app.get("/items/") +async def read_items(commons: Annotated[dict, Depends(common_parameters)]): + return {"message": "Hello Items!", "params": commons} + + +@app.get("/users/") +async def read_users(commons: Annotated[dict, Depends(common_parameters)]): + return {"message": "Hello Users!", "params": commons} + + +client = TestClient(app) + + +async def override_dependency(q: str | None = None): + return {"q": q, "skip": 5, "limit": 10} + + +app.dependency_overrides[common_parameters] = override_dependency + + +def test_override_in_items(): + response = client.get("/items/") + assert response.status_code == 200 + assert response.json() == { + "message": "Hello Items!", + "params": {"q": None, "skip": 5, "limit": 10}, + } + + +def test_override_in_items_with_q(): + response = client.get("/items/?q=foo") + assert response.status_code == 200 + assert response.json() == { + "message": "Hello Items!", + "params": {"q": "foo", "skip": 5, "limit": 10}, + } + + +def test_override_in_items_with_params(): + response = client.get("/items/?q=foo&skip=100&limit=200") + assert response.status_code == 200 + assert response.json() == { + "message": "Hello Items!", + "params": {"q": "foo", "skip": 5, "limit": 10}, + } diff --git a/docs_src/dependency_testing/tutorial001_an_py39.py b/docs_src/dependency_testing/tutorial001_an_py39.py new file mode 100644 index 000000000..bccb0cdb1 --- /dev/null +++ b/docs_src/dependency_testing/tutorial001_an_py39.py @@ -0,0 +1,59 @@ +from typing import Annotated, Union + +from fastapi import Depends, FastAPI +from fastapi.testclient import TestClient + +app = FastAPI() + + +async def common_parameters( + q: Union[str, None] = None, skip: int = 0, limit: int = 100 +): + return {"q": q, "skip": skip, "limit": limit} + + +@app.get("/items/") +async def read_items(commons: Annotated[dict, Depends(common_parameters)]): + return {"message": "Hello Items!", "params": commons} + + +@app.get("/users/") +async def read_users(commons: Annotated[dict, Depends(common_parameters)]): + return {"message": "Hello Users!", "params": commons} + + +client = TestClient(app) + + +async def override_dependency(q: Union[str, None] = None): + return {"q": q, "skip": 5, "limit": 10} + + +app.dependency_overrides[common_parameters] = override_dependency + + +def test_override_in_items(): + response = client.get("/items/") + assert response.status_code == 200 + assert response.json() == { + "message": "Hello Items!", + "params": {"q": None, "skip": 5, "limit": 10}, + } + + +def test_override_in_items_with_q(): + response = client.get("/items/?q=foo") + assert response.status_code == 200 + assert response.json() == { + "message": "Hello Items!", + "params": {"q": "foo", "skip": 5, "limit": 10}, + } + + +def test_override_in_items_with_params(): + response = client.get("/items/?q=foo&skip=100&limit=200") + assert response.status_code == 200 + assert response.json() == { + "message": "Hello Items!", + "params": {"q": "foo", "skip": 5, "limit": 10}, + } diff --git a/docs_src/dependency_testing/tutorial001_py310.py b/docs_src/dependency_testing/tutorial001_py310.py new file mode 100644 index 000000000..d1f6d4374 --- /dev/null +++ b/docs_src/dependency_testing/tutorial001_py310.py @@ -0,0 +1,55 @@ +from fastapi import Depends, FastAPI +from fastapi.testclient import TestClient + +app = FastAPI() + + +async def common_parameters(q: str | None = None, skip: int = 0, limit: int = 100): + return {"q": q, "skip": skip, "limit": limit} + + +@app.get("/items/") +async def read_items(commons: dict = Depends(common_parameters)): + return {"message": "Hello Items!", "params": commons} + + +@app.get("/users/") +async def read_users(commons: dict = Depends(common_parameters)): + return {"message": "Hello Users!", "params": commons} + + +client = TestClient(app) + + +async def override_dependency(q: str | None = None): + return {"q": q, "skip": 5, "limit": 10} + + +app.dependency_overrides[common_parameters] = override_dependency + + +def test_override_in_items(): + response = client.get("/items/") + assert response.status_code == 200 + assert response.json() == { + "message": "Hello Items!", + "params": {"q": None, "skip": 5, "limit": 10}, + } + + +def test_override_in_items_with_q(): + response = client.get("/items/?q=foo") + assert response.status_code == 200 + assert response.json() == { + "message": "Hello Items!", + "params": {"q": "foo", "skip": 5, "limit": 10}, + } + + +def test_override_in_items_with_params(): + response = client.get("/items/?q=foo&skip=100&limit=200") + assert response.status_code == 200 + assert response.json() == { + "message": "Hello Items!", + "params": {"q": "foo", "skip": 5, "limit": 10}, + } diff --git a/docs_src/extra_data_types/tutorial001_an.py b/docs_src/extra_data_types/tutorial001_an.py new file mode 100644 index 000000000..a4c074241 --- /dev/null +++ b/docs_src/extra_data_types/tutorial001_an.py @@ -0,0 +1,29 @@ +from datetime import datetime, time, timedelta +from typing import Union +from uuid import UUID + +from fastapi import Body, FastAPI +from typing_extensions import Annotated + +app = FastAPI() + + +@app.put("/items/{item_id}") +async def read_items( + item_id: UUID, + start_datetime: Annotated[Union[datetime, None], Body()] = None, + end_datetime: Annotated[Union[datetime, None], Body()] = None, + repeat_at: Annotated[Union[time, None], Body()] = None, + process_after: Annotated[Union[timedelta, None], Body()] = None, +): + start_process = start_datetime + process_after + duration = end_datetime - start_process + return { + "item_id": item_id, + "start_datetime": start_datetime, + "end_datetime": end_datetime, + "repeat_at": repeat_at, + "process_after": process_after, + "start_process": start_process, + "duration": duration, + } diff --git a/docs_src/extra_data_types/tutorial001_an_py310.py b/docs_src/extra_data_types/tutorial001_an_py310.py new file mode 100644 index 000000000..4f69c40d9 --- /dev/null +++ b/docs_src/extra_data_types/tutorial001_an_py310.py @@ -0,0 +1,28 @@ +from datetime import datetime, time, timedelta +from typing import Annotated +from uuid import UUID + +from fastapi import Body, FastAPI + +app = FastAPI() + + +@app.put("/items/{item_id}") +async def read_items( + item_id: UUID, + start_datetime: Annotated[datetime | None, Body()] = None, + end_datetime: Annotated[datetime | None, Body()] = None, + repeat_at: Annotated[time | None, Body()] = None, + process_after: Annotated[timedelta | None, Body()] = None, +): + start_process = start_datetime + process_after + duration = end_datetime - start_process + return { + "item_id": item_id, + "start_datetime": start_datetime, + "end_datetime": end_datetime, + "repeat_at": repeat_at, + "process_after": process_after, + "start_process": start_process, + "duration": duration, + } diff --git a/docs_src/extra_data_types/tutorial001_an_py39.py b/docs_src/extra_data_types/tutorial001_an_py39.py new file mode 100644 index 000000000..630d36ae3 --- /dev/null +++ b/docs_src/extra_data_types/tutorial001_an_py39.py @@ -0,0 +1,28 @@ +from datetime import datetime, time, timedelta +from typing import Annotated, Union +from uuid import UUID + +from fastapi import Body, FastAPI + +app = FastAPI() + + +@app.put("/items/{item_id}") +async def read_items( + item_id: UUID, + start_datetime: Annotated[Union[datetime, None], Body()] = None, + end_datetime: Annotated[Union[datetime, None], Body()] = None, + repeat_at: Annotated[Union[time, None], Body()] = None, + process_after: Annotated[Union[timedelta, None], Body()] = None, +): + start_process = start_datetime + process_after + duration = end_datetime - start_process + return { + "item_id": item_id, + "start_datetime": start_datetime, + "end_datetime": end_datetime, + "repeat_at": repeat_at, + "process_after": process_after, + "start_process": start_process, + "duration": duration, + } diff --git a/docs_src/header_params/tutorial001_an.py b/docs_src/header_params/tutorial001_an.py new file mode 100644 index 000000000..816c00086 --- /dev/null +++ b/docs_src/header_params/tutorial001_an.py @@ -0,0 +1,11 @@ +from typing import Union + +from fastapi import FastAPI, Header +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items(user_agent: Annotated[Union[str, None], Header()] = None): + return {"User-Agent": user_agent} diff --git a/docs_src/header_params/tutorial001_an_py310.py b/docs_src/header_params/tutorial001_an_py310.py new file mode 100644 index 000000000..4ba5e1bfb --- /dev/null +++ b/docs_src/header_params/tutorial001_an_py310.py @@ -0,0 +1,10 @@ +from typing import Annotated + +from fastapi import FastAPI, Header + +app = FastAPI() + + +@app.get("/items/") +async def read_items(user_agent: Annotated[str | None, Header()] = None): + return {"User-Agent": user_agent} diff --git a/docs_src/header_params/tutorial001_an_py39.py b/docs_src/header_params/tutorial001_an_py39.py new file mode 100644 index 000000000..1fbe3bb99 --- /dev/null +++ b/docs_src/header_params/tutorial001_an_py39.py @@ -0,0 +1,10 @@ +from typing import Annotated, Union + +from fastapi import FastAPI, Header + +app = FastAPI() + + +@app.get("/items/") +async def read_items(user_agent: Annotated[Union[str, None], Header()] = None): + return {"User-Agent": user_agent} diff --git a/docs_src/header_params/tutorial002_an.py b/docs_src/header_params/tutorial002_an.py new file mode 100644 index 000000000..65d972d46 --- /dev/null +++ b/docs_src/header_params/tutorial002_an.py @@ -0,0 +1,15 @@ +from typing import Union + +from fastapi import FastAPI, Header +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + strange_header: Annotated[ + Union[str, None], Header(convert_underscores=False) + ] = None +): + return {"strange_header": strange_header} diff --git a/docs_src/header_params/tutorial002_an_py310.py b/docs_src/header_params/tutorial002_an_py310.py new file mode 100644 index 000000000..b340647b6 --- /dev/null +++ b/docs_src/header_params/tutorial002_an_py310.py @@ -0,0 +1,12 @@ +from typing import Annotated + +from fastapi import FastAPI, Header + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + strange_header: Annotated[str | None, Header(convert_underscores=False)] = None +): + return {"strange_header": strange_header} diff --git a/docs_src/header_params/tutorial002_an_py39.py b/docs_src/header_params/tutorial002_an_py39.py new file mode 100644 index 000000000..7f6a99f9c --- /dev/null +++ b/docs_src/header_params/tutorial002_an_py39.py @@ -0,0 +1,14 @@ +from typing import Annotated, Union + +from fastapi import FastAPI, Header + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + strange_header: Annotated[ + Union[str, None], Header(convert_underscores=False) + ] = None +): + return {"strange_header": strange_header} diff --git a/docs_src/header_params/tutorial003_an.py b/docs_src/header_params/tutorial003_an.py new file mode 100644 index 000000000..5406fd1f8 --- /dev/null +++ b/docs_src/header_params/tutorial003_an.py @@ -0,0 +1,11 @@ +from typing import List, Union + +from fastapi import FastAPI, Header +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items(x_token: Annotated[Union[List[str], None], Header()] = None): + return {"X-Token values": x_token} diff --git a/docs_src/header_params/tutorial003_an_py310.py b/docs_src/header_params/tutorial003_an_py310.py new file mode 100644 index 000000000..0e78cf029 --- /dev/null +++ b/docs_src/header_params/tutorial003_an_py310.py @@ -0,0 +1,10 @@ +from typing import Annotated + +from fastapi import FastAPI, Header + +app = FastAPI() + + +@app.get("/items/") +async def read_items(x_token: Annotated[list[str] | None, Header()] = None): + return {"X-Token values": x_token} diff --git a/docs_src/header_params/tutorial003_an_py39.py b/docs_src/header_params/tutorial003_an_py39.py new file mode 100644 index 000000000..c1dd49961 --- /dev/null +++ b/docs_src/header_params/tutorial003_an_py39.py @@ -0,0 +1,10 @@ +from typing import Annotated, List, Union + +from fastapi import FastAPI, Header + +app = FastAPI() + + +@app.get("/items/") +async def read_items(x_token: Annotated[Union[List[str], None], Header()] = None): + return {"X-Token values": x_token} diff --git a/docs_src/path_params_numeric_validations/tutorial001_an.py b/docs_src/path_params_numeric_validations/tutorial001_an.py new file mode 100644 index 000000000..621be7b04 --- /dev/null +++ b/docs_src/path_params_numeric_validations/tutorial001_an.py @@ -0,0 +1,17 @@ +from typing import Union + +from fastapi import FastAPI, Path, Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/{item_id}") +async def read_items( + item_id: Annotated[int, Path(title="The ID of the item to get")], + q: Annotated[Union[str, None], Query(alias="item-query")] = None, +): + results = {"item_id": item_id} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/path_params_numeric_validations/tutorial001_an_py310.py b/docs_src/path_params_numeric_validations/tutorial001_an_py310.py new file mode 100644 index 000000000..c4db263f0 --- /dev/null +++ b/docs_src/path_params_numeric_validations/tutorial001_an_py310.py @@ -0,0 +1,16 @@ +from typing import Annotated + +from fastapi import FastAPI, Path, Query + +app = FastAPI() + + +@app.get("/items/{item_id}") +async def read_items( + item_id: Annotated[int, Path(title="The ID of the item to get")], + q: Annotated[str | None, Query(alias="item-query")] = None, +): + results = {"item_id": item_id} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/path_params_numeric_validations/tutorial001_an_py39.py b/docs_src/path_params_numeric_validations/tutorial001_an_py39.py new file mode 100644 index 000000000..b36315a46 --- /dev/null +++ b/docs_src/path_params_numeric_validations/tutorial001_an_py39.py @@ -0,0 +1,16 @@ +from typing import Annotated, Union + +from fastapi import FastAPI, Path, Query + +app = FastAPI() + + +@app.get("/items/{item_id}") +async def read_items( + item_id: Annotated[int, Path(title="The ID of the item to get")], + q: Annotated[Union[str, None], Query(alias="item-query")] = None, +): + results = {"item_id": item_id} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/path_params_numeric_validations/tutorial002_an.py b/docs_src/path_params_numeric_validations/tutorial002_an.py new file mode 100644 index 000000000..322f8cf0b --- /dev/null +++ b/docs_src/path_params_numeric_validations/tutorial002_an.py @@ -0,0 +1,14 @@ +from fastapi import FastAPI, Path +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/{item_id}") +async def read_items( + q: str, item_id: Annotated[int, Path(title="The ID of the item to get")] +): + results = {"item_id": item_id} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/path_params_numeric_validations/tutorial002_an_py39.py b/docs_src/path_params_numeric_validations/tutorial002_an_py39.py new file mode 100644 index 000000000..cd882abb2 --- /dev/null +++ b/docs_src/path_params_numeric_validations/tutorial002_an_py39.py @@ -0,0 +1,15 @@ +from typing import Annotated + +from fastapi import FastAPI, Path + +app = FastAPI() + + +@app.get("/items/{item_id}") +async def read_items( + q: str, item_id: Annotated[int, Path(title="The ID of the item to get")] +): + results = {"item_id": item_id} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/path_params_numeric_validations/tutorial003_an.py b/docs_src/path_params_numeric_validations/tutorial003_an.py new file mode 100644 index 000000000..d0fa8b3db --- /dev/null +++ b/docs_src/path_params_numeric_validations/tutorial003_an.py @@ -0,0 +1,14 @@ +from fastapi import FastAPI, Path +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/{item_id}") +async def read_items( + item_id: Annotated[int, Path(title="The ID of the item to get")], q: str +): + results = {"item_id": item_id} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/path_params_numeric_validations/tutorial003_an_py39.py b/docs_src/path_params_numeric_validations/tutorial003_an_py39.py new file mode 100644 index 000000000..1588556e7 --- /dev/null +++ b/docs_src/path_params_numeric_validations/tutorial003_an_py39.py @@ -0,0 +1,15 @@ +from typing import Annotated + +from fastapi import FastAPI, Path + +app = FastAPI() + + +@app.get("/items/{item_id}") +async def read_items( + item_id: Annotated[int, Path(title="The ID of the item to get")], q: str +): + results = {"item_id": item_id} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/path_params_numeric_validations/tutorial004_an.py b/docs_src/path_params_numeric_validations/tutorial004_an.py new file mode 100644 index 000000000..ffc50f6c5 --- /dev/null +++ b/docs_src/path_params_numeric_validations/tutorial004_an.py @@ -0,0 +1,14 @@ +from fastapi import FastAPI, Path +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/{item_id}") +async def read_items( + item_id: Annotated[int, Path(title="The ID of the item to get", ge=1)], q: str +): + results = {"item_id": item_id} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/path_params_numeric_validations/tutorial004_an_py39.py b/docs_src/path_params_numeric_validations/tutorial004_an_py39.py new file mode 100644 index 000000000..f67f6450e --- /dev/null +++ b/docs_src/path_params_numeric_validations/tutorial004_an_py39.py @@ -0,0 +1,15 @@ +from typing import Annotated + +from fastapi import FastAPI, Path + +app = FastAPI() + + +@app.get("/items/{item_id}") +async def read_items( + item_id: Annotated[int, Path(title="The ID of the item to get", ge=1)], q: str +): + results = {"item_id": item_id} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/path_params_numeric_validations/tutorial005_an.py b/docs_src/path_params_numeric_validations/tutorial005_an.py new file mode 100644 index 000000000..433c69129 --- /dev/null +++ b/docs_src/path_params_numeric_validations/tutorial005_an.py @@ -0,0 +1,15 @@ +from fastapi import FastAPI, Path +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/{item_id}") +async def read_items( + item_id: Annotated[int, Path(title="The ID of the item to get", gt=0, le=1000)], + q: str, +): + results = {"item_id": item_id} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/path_params_numeric_validations/tutorial005_an_py39.py b/docs_src/path_params_numeric_validations/tutorial005_an_py39.py new file mode 100644 index 000000000..571dd583c --- /dev/null +++ b/docs_src/path_params_numeric_validations/tutorial005_an_py39.py @@ -0,0 +1,16 @@ +from typing import Annotated + +from fastapi import FastAPI, Path + +app = FastAPI() + + +@app.get("/items/{item_id}") +async def read_items( + item_id: Annotated[int, Path(title="The ID of the item to get", gt=0, le=1000)], + q: str, +): + results = {"item_id": item_id} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/path_params_numeric_validations/tutorial006_an.py b/docs_src/path_params_numeric_validations/tutorial006_an.py new file mode 100644 index 000000000..22a143623 --- /dev/null +++ b/docs_src/path_params_numeric_validations/tutorial006_an.py @@ -0,0 +1,17 @@ +from fastapi import FastAPI, Path, Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/{item_id}") +async def read_items( + *, + item_id: Annotated[int, Path(title="The ID of the item to get", ge=0, le=1000)], + q: str, + size: Annotated[float, Query(gt=0, lt=10.5)], +): + results = {"item_id": item_id} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/path_params_numeric_validations/tutorial006_an_py39.py b/docs_src/path_params_numeric_validations/tutorial006_an_py39.py new file mode 100644 index 000000000..804751893 --- /dev/null +++ b/docs_src/path_params_numeric_validations/tutorial006_an_py39.py @@ -0,0 +1,18 @@ +from typing import Annotated + +from fastapi import FastAPI, Path, Query + +app = FastAPI() + + +@app.get("/items/{item_id}") +async def read_items( + *, + item_id: Annotated[int, Path(title="The ID of the item to get", ge=0, le=1000)], + q: str, + size: Annotated[float, Query(gt=0, lt=10.5)], +): + results = {"item_id": item_id} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/python_types/tutorial013.py b/docs_src/python_types/tutorial013.py new file mode 100644 index 000000000..0ec773519 --- /dev/null +++ b/docs_src/python_types/tutorial013.py @@ -0,0 +1,5 @@ +from typing_extensions import Annotated + + +def say_hello(name: Annotated[str, "this is just metadata"]) -> str: + return f"Hello {name}" diff --git a/docs_src/python_types/tutorial013_py39.py b/docs_src/python_types/tutorial013_py39.py new file mode 100644 index 000000000..65a0eaa93 --- /dev/null +++ b/docs_src/python_types/tutorial013_py39.py @@ -0,0 +1,5 @@ +from typing import Annotated + + +def say_hello(name: Annotated[str, "this is just metadata"]) -> str: + return f"Hello {name}" diff --git a/docs_src/query_params_str_validations/tutorial002_an.py b/docs_src/query_params_str_validations/tutorial002_an.py new file mode 100644 index 000000000..cb1b38940 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial002_an.py @@ -0,0 +1,14 @@ +from typing import Union + +from fastapi import FastAPI, Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[Union[str, None], Query(max_length=50)] = None): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial002_an_py310.py b/docs_src/query_params_str_validations/tutorial002_an_py310.py new file mode 100644 index 000000000..66ee7451b --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial002_an_py310.py @@ -0,0 +1,13 @@ +from typing import Annotated + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[str | None, Query(max_length=50)] = None): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial003_an.py b/docs_src/query_params_str_validations/tutorial003_an.py new file mode 100644 index 000000000..a3665f6a8 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial003_an.py @@ -0,0 +1,16 @@ +from typing import Union + +from fastapi import FastAPI, Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: Annotated[Union[str, None], Query(min_length=3, max_length=50)] = None +): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial003_an_py310.py b/docs_src/query_params_str_validations/tutorial003_an_py310.py new file mode 100644 index 000000000..836af04de --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial003_an_py310.py @@ -0,0 +1,15 @@ +from typing import Annotated + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: Annotated[str | None, Query(min_length=3, max_length=50)] = None +): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial003_an_py39.py b/docs_src/query_params_str_validations/tutorial003_an_py39.py new file mode 100644 index 000000000..87a426839 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial003_an_py39.py @@ -0,0 +1,15 @@ +from typing import Annotated, Union + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: Annotated[Union[str, None], Query(min_length=3, max_length=50)] = None +): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial004_an.py b/docs_src/query_params_str_validations/tutorial004_an.py new file mode 100644 index 000000000..5346b997b --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial004_an.py @@ -0,0 +1,18 @@ +from typing import Union + +from fastapi import FastAPI, Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: Annotated[ + Union[str, None], Query(min_length=3, max_length=50, regex="^fixedquery$") + ] = None +): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial004_an_py310.py b/docs_src/query_params_str_validations/tutorial004_an_py310.py new file mode 100644 index 000000000..8fd375b3d --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial004_an_py310.py @@ -0,0 +1,17 @@ +from typing import Annotated + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: Annotated[ + str | None, Query(min_length=3, max_length=50, regex="^fixedquery$") + ] = None +): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial004_an_py39.py b/docs_src/query_params_str_validations/tutorial004_an_py39.py new file mode 100644 index 000000000..2fd82db75 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial004_an_py39.py @@ -0,0 +1,17 @@ +from typing import Annotated, Union + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: Annotated[ + Union[str, None], Query(min_length=3, max_length=50, regex="^fixedquery$") + ] = None +): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial005_an.py b/docs_src/query_params_str_validations/tutorial005_an.py new file mode 100644 index 000000000..452d4d38d --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial005_an.py @@ -0,0 +1,12 @@ +from fastapi import FastAPI, Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[str, Query(min_length=3)] = "fixedquery"): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial005_an_py39.py b/docs_src/query_params_str_validations/tutorial005_an_py39.py new file mode 100644 index 000000000..b1f6046b5 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial005_an_py39.py @@ -0,0 +1,13 @@ +from typing import Annotated + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[str, Query(min_length=3)] = "fixedquery"): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial006_an.py b/docs_src/query_params_str_validations/tutorial006_an.py new file mode 100644 index 000000000..559480d2b --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial006_an.py @@ -0,0 +1,12 @@ +from fastapi import FastAPI, Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[str, Query(min_length=3)]): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial006_an_py39.py b/docs_src/query_params_str_validations/tutorial006_an_py39.py new file mode 100644 index 000000000..3b4a676d2 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial006_an_py39.py @@ -0,0 +1,13 @@ +from typing import Annotated + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[str, Query(min_length=3)]): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial006b_an.py b/docs_src/query_params_str_validations/tutorial006b_an.py new file mode 100644 index 000000000..ea3b02583 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial006b_an.py @@ -0,0 +1,12 @@ +from fastapi import FastAPI, Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[str, Query(min_length=3)] = ...): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial006b_an_py39.py b/docs_src/query_params_str_validations/tutorial006b_an_py39.py new file mode 100644 index 000000000..687a9f544 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial006b_an_py39.py @@ -0,0 +1,13 @@ +from typing import Annotated + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[str, Query(min_length=3)] = ...): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial006c_an.py b/docs_src/query_params_str_validations/tutorial006c_an.py new file mode 100644 index 000000000..10bf26a57 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial006c_an.py @@ -0,0 +1,14 @@ +from typing import Union + +from fastapi import FastAPI, Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[Union[str, None], Query(min_length=3)] = ...): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial006c_an_py310.py b/docs_src/query_params_str_validations/tutorial006c_an_py310.py new file mode 100644 index 000000000..1ab0a7d53 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial006c_an_py310.py @@ -0,0 +1,13 @@ +from typing import Annotated + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[str | None, Query(min_length=3)] = ...): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial006c_an_py39.py b/docs_src/query_params_str_validations/tutorial006c_an_py39.py new file mode 100644 index 000000000..ac1273331 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial006c_an_py39.py @@ -0,0 +1,13 @@ +from typing import Annotated, Union + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[Union[str, None], Query(min_length=3)] = ...): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial006d_an.py b/docs_src/query_params_str_validations/tutorial006d_an.py new file mode 100644 index 000000000..bc8283e15 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial006d_an.py @@ -0,0 +1,13 @@ +from fastapi import FastAPI, Query +from pydantic import Required +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[str, Query(min_length=3)] = Required): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial006d_an_py39.py b/docs_src/query_params_str_validations/tutorial006d_an_py39.py new file mode 100644 index 000000000..035d9e3bd --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial006d_an_py39.py @@ -0,0 +1,14 @@ +from typing import Annotated + +from fastapi import FastAPI, Query +from pydantic import Required + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[str, Query(min_length=3)] = Required): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial007_an.py b/docs_src/query_params_str_validations/tutorial007_an.py new file mode 100644 index 000000000..3bc85cc0c --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial007_an.py @@ -0,0 +1,16 @@ +from typing import Union + +from fastapi import FastAPI, Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: Annotated[Union[str, None], Query(title="Query string", min_length=3)] = None +): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial007_an_py310.py b/docs_src/query_params_str_validations/tutorial007_an_py310.py new file mode 100644 index 000000000..5933911fd --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial007_an_py310.py @@ -0,0 +1,15 @@ +from typing import Annotated + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: Annotated[str | None, Query(title="Query string", min_length=3)] = None +): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial007_an_py39.py b/docs_src/query_params_str_validations/tutorial007_an_py39.py new file mode 100644 index 000000000..dafa1c5c9 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial007_an_py39.py @@ -0,0 +1,15 @@ +from typing import Annotated, Union + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: Annotated[Union[str, None], Query(title="Query string", min_length=3)] = None +): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial008_an.py b/docs_src/query_params_str_validations/tutorial008_an.py new file mode 100644 index 000000000..5699f1e88 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial008_an.py @@ -0,0 +1,23 @@ +from typing import Union + +from fastapi import FastAPI, Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: Annotated[ + Union[str, None], + Query( + title="Query string", + description="Query string for the items to search in the database that have a good match", + min_length=3, + ), + ] = None +): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial008_an_py310.py b/docs_src/query_params_str_validations/tutorial008_an_py310.py new file mode 100644 index 000000000..4aaadf8b4 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial008_an_py310.py @@ -0,0 +1,22 @@ +from typing import Annotated + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: Annotated[ + str | None, + Query( + title="Query string", + description="Query string for the items to search in the database that have a good match", + min_length=3, + ), + ] = None +): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial008_an_py39.py b/docs_src/query_params_str_validations/tutorial008_an_py39.py new file mode 100644 index 000000000..1c3b36176 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial008_an_py39.py @@ -0,0 +1,22 @@ +from typing import Annotated, Union + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: Annotated[ + Union[str, None], + Query( + title="Query string", + description="Query string for the items to search in the database that have a good match", + min_length=3, + ), + ] = None +): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial009_an.py b/docs_src/query_params_str_validations/tutorial009_an.py new file mode 100644 index 000000000..2894e2d51 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial009_an.py @@ -0,0 +1,14 @@ +from typing import Union + +from fastapi import FastAPI, Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[Union[str, None], Query(alias="item-query")] = None): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial009_an_py310.py b/docs_src/query_params_str_validations/tutorial009_an_py310.py new file mode 100644 index 000000000..d11753362 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial009_an_py310.py @@ -0,0 +1,13 @@ +from typing import Annotated + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[str | None, Query(alias="item-query")] = None): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial009_an_py39.py b/docs_src/query_params_str_validations/tutorial009_an_py39.py new file mode 100644 index 000000000..70a89e613 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial009_an_py39.py @@ -0,0 +1,13 @@ +from typing import Annotated, Union + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[Union[str, None], Query(alias="item-query")] = None): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial010_an.py b/docs_src/query_params_str_validations/tutorial010_an.py new file mode 100644 index 000000000..8995f3f57 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial010_an.py @@ -0,0 +1,27 @@ +from typing import Union + +from fastapi import FastAPI, Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: Annotated[ + Union[str, None], + Query( + alias="item-query", + title="Query string", + description="Query string for the items to search in the database that have a good match", + min_length=3, + max_length=50, + regex="^fixedquery$", + deprecated=True, + ), + ] = None +): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial010_an_py310.py b/docs_src/query_params_str_validations/tutorial010_an_py310.py new file mode 100644 index 000000000..cfa81926c --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial010_an_py310.py @@ -0,0 +1,26 @@ +from typing import Annotated + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: Annotated[ + str | None, + Query( + alias="item-query", + title="Query string", + description="Query string for the items to search in the database that have a good match", + min_length=3, + max_length=50, + regex="^fixedquery$", + deprecated=True, + ), + ] = None +): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial010_an_py39.py b/docs_src/query_params_str_validations/tutorial010_an_py39.py new file mode 100644 index 000000000..220eaabf4 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial010_an_py39.py @@ -0,0 +1,26 @@ +from typing import Annotated, Union + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: Annotated[ + Union[str, None], + Query( + alias="item-query", + title="Query string", + description="Query string for the items to search in the database that have a good match", + min_length=3, + max_length=50, + regex="^fixedquery$", + deprecated=True, + ), + ] = None +): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial011_an.py b/docs_src/query_params_str_validations/tutorial011_an.py new file mode 100644 index 000000000..8ed699337 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial011_an.py @@ -0,0 +1,12 @@ +from typing import List, Union + +from fastapi import FastAPI, Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[Union[List[str], None], Query()] = None): + query_items = {"q": q} + return query_items diff --git a/docs_src/query_params_str_validations/tutorial011_an_py310.py b/docs_src/query_params_str_validations/tutorial011_an_py310.py new file mode 100644 index 000000000..5dad4bfde --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial011_an_py310.py @@ -0,0 +1,11 @@ +from typing import Annotated + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[list[str] | None, Query()] = None): + query_items = {"q": q} + return query_items diff --git a/docs_src/query_params_str_validations/tutorial011_an_py39.py b/docs_src/query_params_str_validations/tutorial011_an_py39.py new file mode 100644 index 000000000..416e3990d --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial011_an_py39.py @@ -0,0 +1,11 @@ +from typing import Annotated, Union + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[Union[list[str], None], Query()] = None): + query_items = {"q": q} + return query_items diff --git a/docs_src/query_params_str_validations/tutorial012_an.py b/docs_src/query_params_str_validations/tutorial012_an.py new file mode 100644 index 000000000..261af250a --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial012_an.py @@ -0,0 +1,12 @@ +from typing import List + +from fastapi import FastAPI, Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[List[str], Query()] = ["foo", "bar"]): + query_items = {"q": q} + return query_items diff --git a/docs_src/query_params_str_validations/tutorial012_an_py39.py b/docs_src/query_params_str_validations/tutorial012_an_py39.py new file mode 100644 index 000000000..9b5a9c2fb --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial012_an_py39.py @@ -0,0 +1,11 @@ +from typing import Annotated + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[list[str], Query()] = ["foo", "bar"]): + query_items = {"q": q} + return query_items diff --git a/docs_src/query_params_str_validations/tutorial013_an.py b/docs_src/query_params_str_validations/tutorial013_an.py new file mode 100644 index 000000000..f12a25055 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial013_an.py @@ -0,0 +1,10 @@ +from fastapi import FastAPI, Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[list, Query()] = []): + query_items = {"q": q} + return query_items diff --git a/docs_src/query_params_str_validations/tutorial013_an_py39.py b/docs_src/query_params_str_validations/tutorial013_an_py39.py new file mode 100644 index 000000000..602734145 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial013_an_py39.py @@ -0,0 +1,11 @@ +from typing import Annotated + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[list, Query()] = []): + query_items = {"q": q} + return query_items diff --git a/docs_src/query_params_str_validations/tutorial014_an.py b/docs_src/query_params_str_validations/tutorial014_an.py new file mode 100644 index 000000000..a9a9c4427 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial014_an.py @@ -0,0 +1,16 @@ +from typing import Union + +from fastapi import FastAPI, Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + hidden_query: Annotated[Union[str, None], Query(include_in_schema=False)] = None +): + if hidden_query: + return {"hidden_query": hidden_query} + else: + return {"hidden_query": "Not found"} diff --git a/docs_src/query_params_str_validations/tutorial014_an_py310.py b/docs_src/query_params_str_validations/tutorial014_an_py310.py new file mode 100644 index 000000000..5fba54150 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial014_an_py310.py @@ -0,0 +1,15 @@ +from typing import Annotated + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + hidden_query: Annotated[str | None, Query(include_in_schema=False)] = None +): + if hidden_query: + return {"hidden_query": hidden_query} + else: + return {"hidden_query": "Not found"} diff --git a/docs_src/query_params_str_validations/tutorial014_an_py39.py b/docs_src/query_params_str_validations/tutorial014_an_py39.py new file mode 100644 index 000000000..b07985210 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial014_an_py39.py @@ -0,0 +1,15 @@ +from typing import Annotated, Union + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + hidden_query: Annotated[Union[str, None], Query(include_in_schema=False)] = None +): + if hidden_query: + return {"hidden_query": hidden_query} + else: + return {"hidden_query": "Not found"} diff --git a/docs_src/request_files/tutorial001_02_an.py b/docs_src/request_files/tutorial001_02_an.py new file mode 100644 index 000000000..5007fef15 --- /dev/null +++ b/docs_src/request_files/tutorial001_02_an.py @@ -0,0 +1,22 @@ +from typing import Union + +from fastapi import FastAPI, File, UploadFile +from typing_extensions import Annotated + +app = FastAPI() + + +@app.post("/files/") +async def create_file(file: Annotated[Union[bytes, None], File()] = None): + if not file: + return {"message": "No file sent"} + else: + return {"file_size": len(file)} + + +@app.post("/uploadfile/") +async def create_upload_file(file: Union[UploadFile, None] = None): + if not file: + return {"message": "No upload file sent"} + else: + return {"filename": file.filename} diff --git a/docs_src/request_files/tutorial001_02_an_py310.py b/docs_src/request_files/tutorial001_02_an_py310.py new file mode 100644 index 000000000..9b80321b4 --- /dev/null +++ b/docs_src/request_files/tutorial001_02_an_py310.py @@ -0,0 +1,21 @@ +from typing import Annotated + +from fastapi import FastAPI, File, UploadFile + +app = FastAPI() + + +@app.post("/files/") +async def create_file(file: Annotated[bytes | None, File()] = None): + if not file: + return {"message": "No file sent"} + else: + return {"file_size": len(file)} + + +@app.post("/uploadfile/") +async def create_upload_file(file: UploadFile | None = None): + if not file: + return {"message": "No upload file sent"} + else: + return {"filename": file.filename} diff --git a/docs_src/request_files/tutorial001_02_an_py39.py b/docs_src/request_files/tutorial001_02_an_py39.py new file mode 100644 index 000000000..bb090ff6c --- /dev/null +++ b/docs_src/request_files/tutorial001_02_an_py39.py @@ -0,0 +1,21 @@ +from typing import Annotated, Union + +from fastapi import FastAPI, File, UploadFile + +app = FastAPI() + + +@app.post("/files/") +async def create_file(file: Annotated[Union[bytes, None], File()] = None): + if not file: + return {"message": "No file sent"} + else: + return {"file_size": len(file)} + + +@app.post("/uploadfile/") +async def create_upload_file(file: Union[UploadFile, None] = None): + if not file: + return {"message": "No upload file sent"} + else: + return {"filename": file.filename} diff --git a/docs_src/request_files/tutorial001_03_an.py b/docs_src/request_files/tutorial001_03_an.py new file mode 100644 index 000000000..8a6b0a245 --- /dev/null +++ b/docs_src/request_files/tutorial001_03_an.py @@ -0,0 +1,16 @@ +from fastapi import FastAPI, File, UploadFile +from typing_extensions import Annotated + +app = FastAPI() + + +@app.post("/files/") +async def create_file(file: Annotated[bytes, File(description="A file read as bytes")]): + return {"file_size": len(file)} + + +@app.post("/uploadfile/") +async def create_upload_file( + file: Annotated[UploadFile, File(description="A file read as UploadFile")], +): + return {"filename": file.filename} diff --git a/docs_src/request_files/tutorial001_03_an_py39.py b/docs_src/request_files/tutorial001_03_an_py39.py new file mode 100644 index 000000000..93098a677 --- /dev/null +++ b/docs_src/request_files/tutorial001_03_an_py39.py @@ -0,0 +1,17 @@ +from typing import Annotated + +from fastapi import FastAPI, File, UploadFile + +app = FastAPI() + + +@app.post("/files/") +async def create_file(file: Annotated[bytes, File(description="A file read as bytes")]): + return {"file_size": len(file)} + + +@app.post("/uploadfile/") +async def create_upload_file( + file: Annotated[UploadFile, File(description="A file read as UploadFile")], +): + return {"filename": file.filename} diff --git a/docs_src/request_files/tutorial001_an.py b/docs_src/request_files/tutorial001_an.py new file mode 100644 index 000000000..ca2f76d5c --- /dev/null +++ b/docs_src/request_files/tutorial001_an.py @@ -0,0 +1,14 @@ +from fastapi import FastAPI, File, UploadFile +from typing_extensions import Annotated + +app = FastAPI() + + +@app.post("/files/") +async def create_file(file: Annotated[bytes, File()]): + return {"file_size": len(file)} + + +@app.post("/uploadfile/") +async def create_upload_file(file: UploadFile): + return {"filename": file.filename} diff --git a/docs_src/request_files/tutorial001_an_py39.py b/docs_src/request_files/tutorial001_an_py39.py new file mode 100644 index 000000000..26a767221 --- /dev/null +++ b/docs_src/request_files/tutorial001_an_py39.py @@ -0,0 +1,15 @@ +from typing import Annotated + +from fastapi import FastAPI, File, UploadFile + +app = FastAPI() + + +@app.post("/files/") +async def create_file(file: Annotated[bytes, File()]): + return {"file_size": len(file)} + + +@app.post("/uploadfile/") +async def create_upload_file(file: UploadFile): + return {"filename": file.filename} diff --git a/docs_src/request_files/tutorial002_an.py b/docs_src/request_files/tutorial002_an.py new file mode 100644 index 000000000..eaa90da2b --- /dev/null +++ b/docs_src/request_files/tutorial002_an.py @@ -0,0 +1,34 @@ +from typing import List + +from fastapi import FastAPI, File, UploadFile +from fastapi.responses import HTMLResponse +from typing_extensions import Annotated + +app = FastAPI() + + +@app.post("/files/") +async def create_files(files: Annotated[List[bytes], File()]): + return {"file_sizes": [len(file) for file in files]} + + +@app.post("/uploadfiles/") +async def create_upload_files(files: List[UploadFile]): + return {"filenames": [file.filename for file in files]} + + +@app.get("/") +async def main(): + content = """ + +
+ + +
+
+ + +
+ + """ + return HTMLResponse(content=content) diff --git a/docs_src/request_files/tutorial002_an_py39.py b/docs_src/request_files/tutorial002_an_py39.py new file mode 100644 index 000000000..db524ceab --- /dev/null +++ b/docs_src/request_files/tutorial002_an_py39.py @@ -0,0 +1,33 @@ +from typing import Annotated + +from fastapi import FastAPI, File, UploadFile +from fastapi.responses import HTMLResponse + +app = FastAPI() + + +@app.post("/files/") +async def create_files(files: Annotated[list[bytes], File()]): + return {"file_sizes": [len(file) for file in files]} + + +@app.post("/uploadfiles/") +async def create_upload_files(files: list[UploadFile]): + return {"filenames": [file.filename for file in files]} + + +@app.get("/") +async def main(): + content = """ + +
+ + +
+
+ + +
+ + """ + return HTMLResponse(content=content) diff --git a/docs_src/request_files/tutorial003_an.py b/docs_src/request_files/tutorial003_an.py new file mode 100644 index 000000000..2238e3c94 --- /dev/null +++ b/docs_src/request_files/tutorial003_an.py @@ -0,0 +1,40 @@ +from typing import List + +from fastapi import FastAPI, File, UploadFile +from fastapi.responses import HTMLResponse +from typing_extensions import Annotated + +app = FastAPI() + + +@app.post("/files/") +async def create_files( + files: Annotated[List[bytes], File(description="Multiple files as bytes")], +): + return {"file_sizes": [len(file) for file in files]} + + +@app.post("/uploadfiles/") +async def create_upload_files( + files: Annotated[ + List[UploadFile], File(description="Multiple files as UploadFile") + ], +): + return {"filenames": [file.filename for file in files]} + + +@app.get("/") +async def main(): + content = """ + +
+ + +
+
+ + +
+ + """ + return HTMLResponse(content=content) diff --git a/docs_src/request_files/tutorial003_an_py39.py b/docs_src/request_files/tutorial003_an_py39.py new file mode 100644 index 000000000..5a8c5dab5 --- /dev/null +++ b/docs_src/request_files/tutorial003_an_py39.py @@ -0,0 +1,39 @@ +from typing import Annotated + +from fastapi import FastAPI, File, UploadFile +from fastapi.responses import HTMLResponse + +app = FastAPI() + + +@app.post("/files/") +async def create_files( + files: Annotated[list[bytes], File(description="Multiple files as bytes")], +): + return {"file_sizes": [len(file) for file in files]} + + +@app.post("/uploadfiles/") +async def create_upload_files( + files: Annotated[ + list[UploadFile], File(description="Multiple files as UploadFile") + ], +): + return {"filenames": [file.filename for file in files]} + + +@app.get("/") +async def main(): + content = """ + +
+ + +
+
+ + +
+ + """ + return HTMLResponse(content=content) diff --git a/docs_src/request_forms/tutorial001_an.py b/docs_src/request_forms/tutorial001_an.py new file mode 100644 index 000000000..677fbf2db --- /dev/null +++ b/docs_src/request_forms/tutorial001_an.py @@ -0,0 +1,9 @@ +from fastapi import FastAPI, Form +from typing_extensions import Annotated + +app = FastAPI() + + +@app.post("/login/") +async def login(username: Annotated[str, Form()], password: Annotated[str, Form()]): + return {"username": username} diff --git a/docs_src/request_forms/tutorial001_an_py39.py b/docs_src/request_forms/tutorial001_an_py39.py new file mode 100644 index 000000000..8e9d2ea53 --- /dev/null +++ b/docs_src/request_forms/tutorial001_an_py39.py @@ -0,0 +1,10 @@ +from typing import Annotated + +from fastapi import FastAPI, Form + +app = FastAPI() + + +@app.post("/login/") +async def login(username: Annotated[str, Form()], password: Annotated[str, Form()]): + return {"username": username} diff --git a/docs_src/request_forms_and_files/tutorial001_an.py b/docs_src/request_forms_and_files/tutorial001_an.py new file mode 100644 index 000000000..0ea285ac8 --- /dev/null +++ b/docs_src/request_forms_and_files/tutorial001_an.py @@ -0,0 +1,17 @@ +from fastapi import FastAPI, File, Form, UploadFile +from typing_extensions import Annotated + +app = FastAPI() + + +@app.post("/files/") +async def create_file( + file: Annotated[bytes, File()], + fileb: Annotated[UploadFile, File()], + token: Annotated[str, Form()], +): + return { + "file_size": len(file), + "token": token, + "fileb_content_type": fileb.content_type, + } diff --git a/docs_src/request_forms_and_files/tutorial001_an_py39.py b/docs_src/request_forms_and_files/tutorial001_an_py39.py new file mode 100644 index 000000000..12cc43e50 --- /dev/null +++ b/docs_src/request_forms_and_files/tutorial001_an_py39.py @@ -0,0 +1,18 @@ +from typing import Annotated + +from fastapi import FastAPI, File, Form, UploadFile + +app = FastAPI() + + +@app.post("/files/") +async def create_file( + file: Annotated[bytes, File()], + fileb: Annotated[UploadFile, File()], + token: Annotated[str, Form()], +): + return { + "file_size": len(file), + "token": token, + "fileb_content_type": fileb.content_type, + } diff --git a/docs_src/schema_extra_example/tutorial003_an.py b/docs_src/schema_extra_example/tutorial003_an.py new file mode 100644 index 000000000..1dec555a9 --- /dev/null +++ b/docs_src/schema_extra_example/tutorial003_an.py @@ -0,0 +1,33 @@ +from typing import Union + +from fastapi import Body, FastAPI +from pydantic import BaseModel +from typing_extensions import Annotated + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + price: float + tax: Union[float, None] = None + + +@app.put("/items/{item_id}") +async def update_item( + item_id: int, + item: Annotated[ + Item, + Body( + example={ + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + ), + ], +): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/schema_extra_example/tutorial003_an_py310.py b/docs_src/schema_extra_example/tutorial003_an_py310.py new file mode 100644 index 000000000..9edaddfb8 --- /dev/null +++ b/docs_src/schema_extra_example/tutorial003_an_py310.py @@ -0,0 +1,32 @@ +from typing import Annotated + +from fastapi import Body, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + + +@app.put("/items/{item_id}") +async def update_item( + item_id: int, + item: Annotated[ + Item, + Body( + example={ + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + ), + ], +): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/schema_extra_example/tutorial003_an_py39.py b/docs_src/schema_extra_example/tutorial003_an_py39.py new file mode 100644 index 000000000..fe08847d9 --- /dev/null +++ b/docs_src/schema_extra_example/tutorial003_an_py39.py @@ -0,0 +1,32 @@ +from typing import Annotated, Union + +from fastapi import Body, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + price: float + tax: Union[float, None] = None + + +@app.put("/items/{item_id}") +async def update_item( + item_id: int, + item: Annotated[ + Item, + Body( + example={ + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + ), + ], +): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/schema_extra_example/tutorial004_an.py b/docs_src/schema_extra_example/tutorial004_an.py new file mode 100644 index 000000000..82c9a92ac --- /dev/null +++ b/docs_src/schema_extra_example/tutorial004_an.py @@ -0,0 +1,55 @@ +from typing import Union + +from fastapi import Body, FastAPI +from pydantic import BaseModel +from typing_extensions import Annotated + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + price: float + tax: Union[float, None] = None + + +@app.put("/items/{item_id}") +async def update_item( + *, + item_id: int, + item: Annotated[ + Item, + Body( + examples={ + "normal": { + "summary": "A normal example", + "description": "A **normal** item works correctly.", + "value": { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + }, + "converted": { + "summary": "An example with converted data", + "description": "FastAPI can convert price `strings` to actual `numbers` automatically", + "value": { + "name": "Bar", + "price": "35.4", + }, + }, + "invalid": { + "summary": "Invalid data is rejected with an error", + "value": { + "name": "Baz", + "price": "thirty five point four", + }, + }, + }, + ), + ], +): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/schema_extra_example/tutorial004_an_py310.py b/docs_src/schema_extra_example/tutorial004_an_py310.py new file mode 100644 index 000000000..01f1a486c --- /dev/null +++ b/docs_src/schema_extra_example/tutorial004_an_py310.py @@ -0,0 +1,54 @@ +from typing import Annotated + +from fastapi import Body, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + + +@app.put("/items/{item_id}") +async def update_item( + *, + item_id: int, + item: Annotated[ + Item, + Body( + examples={ + "normal": { + "summary": "A normal example", + "description": "A **normal** item works correctly.", + "value": { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + }, + "converted": { + "summary": "An example with converted data", + "description": "FastAPI can convert price `strings` to actual `numbers` automatically", + "value": { + "name": "Bar", + "price": "35.4", + }, + }, + "invalid": { + "summary": "Invalid data is rejected with an error", + "value": { + "name": "Baz", + "price": "thirty five point four", + }, + }, + }, + ), + ], +): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/schema_extra_example/tutorial004_an_py39.py b/docs_src/schema_extra_example/tutorial004_an_py39.py new file mode 100644 index 000000000..d50e8aa5f --- /dev/null +++ b/docs_src/schema_extra_example/tutorial004_an_py39.py @@ -0,0 +1,54 @@ +from typing import Annotated, Union + +from fastapi import Body, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + price: float + tax: Union[float, None] = None + + +@app.put("/items/{item_id}") +async def update_item( + *, + item_id: int, + item: Annotated[ + Item, + Body( + examples={ + "normal": { + "summary": "A normal example", + "description": "A **normal** item works correctly.", + "value": { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + }, + "converted": { + "summary": "An example with converted data", + "description": "FastAPI can convert price `strings` to actual `numbers` automatically", + "value": { + "name": "Bar", + "price": "35.4", + }, + }, + "invalid": { + "summary": "Invalid data is rejected with an error", + "value": { + "name": "Baz", + "price": "thirty five point four", + }, + }, + }, + ), + ], +): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/security/tutorial001_an.py b/docs_src/security/tutorial001_an.py new file mode 100644 index 000000000..dac915b7c --- /dev/null +++ b/docs_src/security/tutorial001_an.py @@ -0,0 +1,12 @@ +from fastapi import Depends, FastAPI +from fastapi.security import OAuth2PasswordBearer +from typing_extensions import Annotated + +app = FastAPI() + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + + +@app.get("/items/") +async def read_items(token: Annotated[str, Depends(oauth2_scheme)]): + return {"token": token} diff --git a/docs_src/security/tutorial001_an_py39.py b/docs_src/security/tutorial001_an_py39.py new file mode 100644 index 000000000..de110402e --- /dev/null +++ b/docs_src/security/tutorial001_an_py39.py @@ -0,0 +1,13 @@ +from typing import Annotated + +from fastapi import Depends, FastAPI +from fastapi.security import OAuth2PasswordBearer + +app = FastAPI() + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + + +@app.get("/items/") +async def read_items(token: Annotated[str, Depends(oauth2_scheme)]): + return {"token": token} diff --git a/docs_src/security/tutorial002_an.py b/docs_src/security/tutorial002_an.py new file mode 100644 index 000000000..291b3bf53 --- /dev/null +++ b/docs_src/security/tutorial002_an.py @@ -0,0 +1,33 @@ +from typing import Union + +from fastapi import Depends, FastAPI +from fastapi.security import OAuth2PasswordBearer +from pydantic import BaseModel +from typing_extensions import Annotated + +app = FastAPI() + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + + +class User(BaseModel): + username: str + email: Union[str, None] = None + full_name: Union[str, None] = None + disabled: Union[bool, None] = None + + +def fake_decode_token(token): + return User( + username=token + "fakedecoded", email="john@example.com", full_name="John Doe" + ) + + +async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]): + user = fake_decode_token(token) + return user + + +@app.get("/users/me") +async def read_users_me(current_user: Annotated[User, Depends(get_current_user)]): + return current_user diff --git a/docs_src/security/tutorial002_an_py310.py b/docs_src/security/tutorial002_an_py310.py new file mode 100644 index 000000000..c7b761e45 --- /dev/null +++ b/docs_src/security/tutorial002_an_py310.py @@ -0,0 +1,32 @@ +from typing import Annotated + +from fastapi import Depends, FastAPI +from fastapi.security import OAuth2PasswordBearer +from pydantic import BaseModel + +app = FastAPI() + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + + +class User(BaseModel): + username: str + email: str | None = None + full_name: str | None = None + disabled: bool | None = None + + +def fake_decode_token(token): + return User( + username=token + "fakedecoded", email="john@example.com", full_name="John Doe" + ) + + +async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]): + user = fake_decode_token(token) + return user + + +@app.get("/users/me") +async def read_users_me(current_user: Annotated[User, Depends(get_current_user)]): + return current_user diff --git a/docs_src/security/tutorial002_an_py39.py b/docs_src/security/tutorial002_an_py39.py new file mode 100644 index 000000000..7ff1c470b --- /dev/null +++ b/docs_src/security/tutorial002_an_py39.py @@ -0,0 +1,32 @@ +from typing import Annotated, Union + +from fastapi import Depends, FastAPI +from fastapi.security import OAuth2PasswordBearer +from pydantic import BaseModel + +app = FastAPI() + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + + +class User(BaseModel): + username: str + email: Union[str, None] = None + full_name: Union[str, None] = None + disabled: Union[bool, None] = None + + +def fake_decode_token(token): + return User( + username=token + "fakedecoded", email="john@example.com", full_name="John Doe" + ) + + +async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]): + user = fake_decode_token(token) + return user + + +@app.get("/users/me") +async def read_users_me(current_user: Annotated[User, Depends(get_current_user)]): + return current_user diff --git a/docs_src/security/tutorial003_an.py b/docs_src/security/tutorial003_an.py new file mode 100644 index 000000000..261cb4857 --- /dev/null +++ b/docs_src/security/tutorial003_an.py @@ -0,0 +1,95 @@ +from typing import Union + +from fastapi import Depends, FastAPI, HTTPException, status +from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm +from pydantic import BaseModel +from typing_extensions import Annotated + +fake_users_db = { + "johndoe": { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "hashed_password": "fakehashedsecret", + "disabled": False, + }, + "alice": { + "username": "alice", + "full_name": "Alice Wonderson", + "email": "alice@example.com", + "hashed_password": "fakehashedsecret2", + "disabled": True, + }, +} + +app = FastAPI() + + +def fake_hash_password(password: str): + return "fakehashed" + password + + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + + +class User(BaseModel): + username: str + email: Union[str, None] = None + full_name: Union[str, None] = None + disabled: Union[bool, None] = None + + +class UserInDB(User): + hashed_password: str + + +def get_user(db, username: str): + if username in db: + user_dict = db[username] + return UserInDB(**user_dict) + + +def fake_decode_token(token): + # This doesn't provide any security at all + # Check the next version + user = get_user(fake_users_db, token) + return user + + +async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]): + user = fake_decode_token(token) + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid authentication credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + return user + + +async def get_current_active_user( + current_user: Annotated[User, Depends(get_current_user)] +): + if current_user.disabled: + raise HTTPException(status_code=400, detail="Inactive user") + return current_user + + +@app.post("/token") +async def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]): + user_dict = fake_users_db.get(form_data.username) + if not user_dict: + raise HTTPException(status_code=400, detail="Incorrect username or password") + user = UserInDB(**user_dict) + hashed_password = fake_hash_password(form_data.password) + if not hashed_password == user.hashed_password: + raise HTTPException(status_code=400, detail="Incorrect username or password") + + return {"access_token": user.username, "token_type": "bearer"} + + +@app.get("/users/me") +async def read_users_me( + current_user: Annotated[User, Depends(get_current_active_user)] +): + return current_user diff --git a/docs_src/security/tutorial003_an_py310.py b/docs_src/security/tutorial003_an_py310.py new file mode 100644 index 000000000..a03f4f8bf --- /dev/null +++ b/docs_src/security/tutorial003_an_py310.py @@ -0,0 +1,94 @@ +from typing import Annotated + +from fastapi import Depends, FastAPI, HTTPException, status +from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm +from pydantic import BaseModel + +fake_users_db = { + "johndoe": { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "hashed_password": "fakehashedsecret", + "disabled": False, + }, + "alice": { + "username": "alice", + "full_name": "Alice Wonderson", + "email": "alice@example.com", + "hashed_password": "fakehashedsecret2", + "disabled": True, + }, +} + +app = FastAPI() + + +def fake_hash_password(password: str): + return "fakehashed" + password + + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + + +class User(BaseModel): + username: str + email: str | None = None + full_name: str | None = None + disabled: bool | None = None + + +class UserInDB(User): + hashed_password: str + + +def get_user(db, username: str): + if username in db: + user_dict = db[username] + return UserInDB(**user_dict) + + +def fake_decode_token(token): + # This doesn't provide any security at all + # Check the next version + user = get_user(fake_users_db, token) + return user + + +async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]): + user = fake_decode_token(token) + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid authentication credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + return user + + +async def get_current_active_user( + current_user: Annotated[User, Depends(get_current_user)] +): + if current_user.disabled: + raise HTTPException(status_code=400, detail="Inactive user") + return current_user + + +@app.post("/token") +async def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]): + user_dict = fake_users_db.get(form_data.username) + if not user_dict: + raise HTTPException(status_code=400, detail="Incorrect username or password") + user = UserInDB(**user_dict) + hashed_password = fake_hash_password(form_data.password) + if not hashed_password == user.hashed_password: + raise HTTPException(status_code=400, detail="Incorrect username or password") + + return {"access_token": user.username, "token_type": "bearer"} + + +@app.get("/users/me") +async def read_users_me( + current_user: Annotated[User, Depends(get_current_active_user)] +): + return current_user diff --git a/docs_src/security/tutorial003_an_py39.py b/docs_src/security/tutorial003_an_py39.py new file mode 100644 index 000000000..308dbe798 --- /dev/null +++ b/docs_src/security/tutorial003_an_py39.py @@ -0,0 +1,94 @@ +from typing import Annotated, Union + +from fastapi import Depends, FastAPI, HTTPException, status +from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm +from pydantic import BaseModel + +fake_users_db = { + "johndoe": { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "hashed_password": "fakehashedsecret", + "disabled": False, + }, + "alice": { + "username": "alice", + "full_name": "Alice Wonderson", + "email": "alice@example.com", + "hashed_password": "fakehashedsecret2", + "disabled": True, + }, +} + +app = FastAPI() + + +def fake_hash_password(password: str): + return "fakehashed" + password + + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + + +class User(BaseModel): + username: str + email: Union[str, None] = None + full_name: Union[str, None] = None + disabled: Union[bool, None] = None + + +class UserInDB(User): + hashed_password: str + + +def get_user(db, username: str): + if username in db: + user_dict = db[username] + return UserInDB(**user_dict) + + +def fake_decode_token(token): + # This doesn't provide any security at all + # Check the next version + user = get_user(fake_users_db, token) + return user + + +async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]): + user = fake_decode_token(token) + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid authentication credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + return user + + +async def get_current_active_user( + current_user: Annotated[User, Depends(get_current_user)] +): + if current_user.disabled: + raise HTTPException(status_code=400, detail="Inactive user") + return current_user + + +@app.post("/token") +async def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]): + user_dict = fake_users_db.get(form_data.username) + if not user_dict: + raise HTTPException(status_code=400, detail="Incorrect username or password") + user = UserInDB(**user_dict) + hashed_password = fake_hash_password(form_data.password) + if not hashed_password == user.hashed_password: + raise HTTPException(status_code=400, detail="Incorrect username or password") + + return {"access_token": user.username, "token_type": "bearer"} + + +@app.get("/users/me") +async def read_users_me( + current_user: Annotated[User, Depends(get_current_active_user)] +): + return current_user diff --git a/docs_src/security/tutorial004_an.py b/docs_src/security/tutorial004_an.py new file mode 100644 index 000000000..ca350343d --- /dev/null +++ b/docs_src/security/tutorial004_an.py @@ -0,0 +1,147 @@ +from datetime import datetime, timedelta +from typing import Union + +from fastapi import Depends, FastAPI, HTTPException, status +from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm +from jose import JWTError, jwt +from passlib.context import CryptContext +from pydantic import BaseModel +from typing_extensions import Annotated + +# to get a string like this run: +# openssl rand -hex 32 +SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7" +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 30 + + +fake_users_db = { + "johndoe": { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW", + "disabled": False, + } +} + + +class Token(BaseModel): + access_token: str + token_type: str + + +class TokenData(BaseModel): + username: Union[str, None] = None + + +class User(BaseModel): + username: str + email: Union[str, None] = None + full_name: Union[str, None] = None + disabled: Union[bool, None] = None + + +class UserInDB(User): + hashed_password: str + + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + +app = FastAPI() + + +def verify_password(plain_password, hashed_password): + return pwd_context.verify(plain_password, hashed_password) + + +def get_password_hash(password): + return pwd_context.hash(password) + + +def get_user(db, username: str): + if username in db: + user_dict = db[username] + return UserInDB(**user_dict) + + +def authenticate_user(fake_db, username: str, password: str): + user = get_user(fake_db, username) + if not user: + return False + if not verify_password(password, user.hashed_password): + return False + return user + + +def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None): + to_encode = data.copy() + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(minutes=15) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt + + +async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]): + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + username: str = payload.get("sub") + if username is None: + raise credentials_exception + token_data = TokenData(username=username) + except JWTError: + raise credentials_exception + user = get_user(fake_users_db, username=token_data.username) + if user is None: + raise credentials_exception + return user + + +async def get_current_active_user( + current_user: Annotated[User, Depends(get_current_user)] +): + if current_user.disabled: + raise HTTPException(status_code=400, detail="Inactive user") + return current_user + + +@app.post("/token", response_model=Token) +async def login_for_access_token( + form_data: Annotated[OAuth2PasswordRequestForm, Depends()] +): + user = authenticate_user(fake_users_db, form_data.username, form_data.password) + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect username or password", + headers={"WWW-Authenticate": "Bearer"}, + ) + access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + access_token = create_access_token( + data={"sub": user.username}, expires_delta=access_token_expires + ) + return {"access_token": access_token, "token_type": "bearer"} + + +@app.get("/users/me/", response_model=User) +async def read_users_me( + current_user: Annotated[User, Depends(get_current_active_user)] +): + return current_user + + +@app.get("/users/me/items/") +async def read_own_items( + current_user: Annotated[User, Depends(get_current_active_user)] +): + return [{"item_id": "Foo", "owner": current_user.username}] diff --git a/docs_src/security/tutorial004_an_py310.py b/docs_src/security/tutorial004_an_py310.py new file mode 100644 index 000000000..8bf5f3b71 --- /dev/null +++ b/docs_src/security/tutorial004_an_py310.py @@ -0,0 +1,146 @@ +from datetime import datetime, timedelta +from typing import Annotated + +from fastapi import Depends, FastAPI, HTTPException, status +from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm +from jose import JWTError, jwt +from passlib.context import CryptContext +from pydantic import BaseModel + +# to get a string like this run: +# openssl rand -hex 32 +SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7" +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 30 + + +fake_users_db = { + "johndoe": { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW", + "disabled": False, + } +} + + +class Token(BaseModel): + access_token: str + token_type: str + + +class TokenData(BaseModel): + username: str | None = None + + +class User(BaseModel): + username: str + email: str | None = None + full_name: str | None = None + disabled: bool | None = None + + +class UserInDB(User): + hashed_password: str + + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + +app = FastAPI() + + +def verify_password(plain_password, hashed_password): + return pwd_context.verify(plain_password, hashed_password) + + +def get_password_hash(password): + return pwd_context.hash(password) + + +def get_user(db, username: str): + if username in db: + user_dict = db[username] + return UserInDB(**user_dict) + + +def authenticate_user(fake_db, username: str, password: str): + user = get_user(fake_db, username) + if not user: + return False + if not verify_password(password, user.hashed_password): + return False + return user + + +def create_access_token(data: dict, expires_delta: timedelta | None = None): + to_encode = data.copy() + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(minutes=15) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt + + +async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]): + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + username: str = payload.get("sub") + if username is None: + raise credentials_exception + token_data = TokenData(username=username) + except JWTError: + raise credentials_exception + user = get_user(fake_users_db, username=token_data.username) + if user is None: + raise credentials_exception + return user + + +async def get_current_active_user( + current_user: Annotated[User, Depends(get_current_user)] +): + if current_user.disabled: + raise HTTPException(status_code=400, detail="Inactive user") + return current_user + + +@app.post("/token", response_model=Token) +async def login_for_access_token( + form_data: Annotated[OAuth2PasswordRequestForm, Depends()] +): + user = authenticate_user(fake_users_db, form_data.username, form_data.password) + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect username or password", + headers={"WWW-Authenticate": "Bearer"}, + ) + access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + access_token = create_access_token( + data={"sub": user.username}, expires_delta=access_token_expires + ) + return {"access_token": access_token, "token_type": "bearer"} + + +@app.get("/users/me/", response_model=User) +async def read_users_me( + current_user: Annotated[User, Depends(get_current_active_user)] +): + return current_user + + +@app.get("/users/me/items/") +async def read_own_items( + current_user: Annotated[User, Depends(get_current_active_user)] +): + return [{"item_id": "Foo", "owner": current_user.username}] diff --git a/docs_src/security/tutorial004_an_py39.py b/docs_src/security/tutorial004_an_py39.py new file mode 100644 index 000000000..a634e23de --- /dev/null +++ b/docs_src/security/tutorial004_an_py39.py @@ -0,0 +1,146 @@ +from datetime import datetime, timedelta +from typing import Annotated, Union + +from fastapi import Depends, FastAPI, HTTPException, status +from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm +from jose import JWTError, jwt +from passlib.context import CryptContext +from pydantic import BaseModel + +# to get a string like this run: +# openssl rand -hex 32 +SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7" +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 30 + + +fake_users_db = { + "johndoe": { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW", + "disabled": False, + } +} + + +class Token(BaseModel): + access_token: str + token_type: str + + +class TokenData(BaseModel): + username: Union[str, None] = None + + +class User(BaseModel): + username: str + email: Union[str, None] = None + full_name: Union[str, None] = None + disabled: Union[bool, None] = None + + +class UserInDB(User): + hashed_password: str + + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + +app = FastAPI() + + +def verify_password(plain_password, hashed_password): + return pwd_context.verify(plain_password, hashed_password) + + +def get_password_hash(password): + return pwd_context.hash(password) + + +def get_user(db, username: str): + if username in db: + user_dict = db[username] + return UserInDB(**user_dict) + + +def authenticate_user(fake_db, username: str, password: str): + user = get_user(fake_db, username) + if not user: + return False + if not verify_password(password, user.hashed_password): + return False + return user + + +def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None): + to_encode = data.copy() + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(minutes=15) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt + + +async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]): + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + username: str = payload.get("sub") + if username is None: + raise credentials_exception + token_data = TokenData(username=username) + except JWTError: + raise credentials_exception + user = get_user(fake_users_db, username=token_data.username) + if user is None: + raise credentials_exception + return user + + +async def get_current_active_user( + current_user: Annotated[User, Depends(get_current_user)] +): + if current_user.disabled: + raise HTTPException(status_code=400, detail="Inactive user") + return current_user + + +@app.post("/token", response_model=Token) +async def login_for_access_token( + form_data: Annotated[OAuth2PasswordRequestForm, Depends()] +): + user = authenticate_user(fake_users_db, form_data.username, form_data.password) + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect username or password", + headers={"WWW-Authenticate": "Bearer"}, + ) + access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + access_token = create_access_token( + data={"sub": user.username}, expires_delta=access_token_expires + ) + return {"access_token": access_token, "token_type": "bearer"} + + +@app.get("/users/me/", response_model=User) +async def read_users_me( + current_user: Annotated[User, Depends(get_current_active_user)] +): + return current_user + + +@app.get("/users/me/items/") +async def read_own_items( + current_user: Annotated[User, Depends(get_current_active_user)] +): + return [{"item_id": "Foo", "owner": current_user.username}] diff --git a/docs_src/security/tutorial005_an.py b/docs_src/security/tutorial005_an.py new file mode 100644 index 000000000..ec4fa1a07 --- /dev/null +++ b/docs_src/security/tutorial005_an.py @@ -0,0 +1,178 @@ +from datetime import datetime, timedelta +from typing import List, Union + +from fastapi import Depends, FastAPI, HTTPException, Security, status +from fastapi.security import ( + OAuth2PasswordBearer, + OAuth2PasswordRequestForm, + SecurityScopes, +) +from jose import JWTError, jwt +from passlib.context import CryptContext +from pydantic import BaseModel, ValidationError +from typing_extensions import Annotated + +# to get a string like this run: +# openssl rand -hex 32 +SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7" +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 30 + + +fake_users_db = { + "johndoe": { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW", + "disabled": False, + }, + "alice": { + "username": "alice", + "full_name": "Alice Chains", + "email": "alicechains@example.com", + "hashed_password": "$2b$12$gSvqqUPvlXP2tfVFaWK1Be7DlH.PKZbv5H8KnzzVgXXbVxpva.pFm", + "disabled": True, + }, +} + + +class Token(BaseModel): + access_token: str + token_type: str + + +class TokenData(BaseModel): + username: Union[str, None] = None + scopes: List[str] = [] + + +class User(BaseModel): + username: str + email: Union[str, None] = None + full_name: Union[str, None] = None + disabled: Union[bool, None] = None + + +class UserInDB(User): + hashed_password: str + + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +oauth2_scheme = OAuth2PasswordBearer( + tokenUrl="token", + scopes={"me": "Read information about the current user.", "items": "Read items."}, +) + +app = FastAPI() + + +def verify_password(plain_password, hashed_password): + return pwd_context.verify(plain_password, hashed_password) + + +def get_password_hash(password): + return pwd_context.hash(password) + + +def get_user(db, username: str): + if username in db: + user_dict = db[username] + return UserInDB(**user_dict) + + +def authenticate_user(fake_db, username: str, password: str): + user = get_user(fake_db, username) + if not user: + return False + if not verify_password(password, user.hashed_password): + return False + return user + + +def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None): + to_encode = data.copy() + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(minutes=15) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt + + +async def get_current_user( + security_scopes: SecurityScopes, token: Annotated[str, Depends(oauth2_scheme)] +): + if security_scopes.scopes: + authenticate_value = f'Bearer scope="{security_scopes.scope_str}"' + else: + authenticate_value = "Bearer" + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": authenticate_value}, + ) + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + username: str = payload.get("sub") + if username is None: + raise credentials_exception + token_scopes = payload.get("scopes", []) + token_data = TokenData(scopes=token_scopes, username=username) + except (JWTError, ValidationError): + raise credentials_exception + user = get_user(fake_users_db, username=token_data.username) + if user is None: + raise credentials_exception + for scope in security_scopes.scopes: + if scope not in token_data.scopes: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Not enough permissions", + headers={"WWW-Authenticate": authenticate_value}, + ) + return user + + +async def get_current_active_user( + current_user: Annotated[User, Security(get_current_user, scopes=["me"])] +): + if current_user.disabled: + raise HTTPException(status_code=400, detail="Inactive user") + return current_user + + +@app.post("/token", response_model=Token) +async def login_for_access_token( + form_data: Annotated[OAuth2PasswordRequestForm, Depends()] +): + user = authenticate_user(fake_users_db, form_data.username, form_data.password) + if not user: + raise HTTPException(status_code=400, detail="Incorrect username or password") + access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + access_token = create_access_token( + data={"sub": user.username, "scopes": form_data.scopes}, + expires_delta=access_token_expires, + ) + return {"access_token": access_token, "token_type": "bearer"} + + +@app.get("/users/me/", response_model=User) +async def read_users_me( + current_user: Annotated[User, Depends(get_current_active_user)] +): + return current_user + + +@app.get("/users/me/items/") +async def read_own_items( + current_user: Annotated[User, Security(get_current_active_user, scopes=["items"])] +): + return [{"item_id": "Foo", "owner": current_user.username}] + + +@app.get("/status/") +async def read_system_status(current_user: Annotated[User, Depends(get_current_user)]): + return {"status": "ok"} diff --git a/docs_src/security/tutorial005_an_py310.py b/docs_src/security/tutorial005_an_py310.py new file mode 100644 index 000000000..45f3fc0bd --- /dev/null +++ b/docs_src/security/tutorial005_an_py310.py @@ -0,0 +1,177 @@ +from datetime import datetime, timedelta +from typing import Annotated + +from fastapi import Depends, FastAPI, HTTPException, Security, status +from fastapi.security import ( + OAuth2PasswordBearer, + OAuth2PasswordRequestForm, + SecurityScopes, +) +from jose import JWTError, jwt +from passlib.context import CryptContext +from pydantic import BaseModel, ValidationError + +# to get a string like this run: +# openssl rand -hex 32 +SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7" +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 30 + + +fake_users_db = { + "johndoe": { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW", + "disabled": False, + }, + "alice": { + "username": "alice", + "full_name": "Alice Chains", + "email": "alicechains@example.com", + "hashed_password": "$2b$12$gSvqqUPvlXP2tfVFaWK1Be7DlH.PKZbv5H8KnzzVgXXbVxpva.pFm", + "disabled": True, + }, +} + + +class Token(BaseModel): + access_token: str + token_type: str + + +class TokenData(BaseModel): + username: str | None = None + scopes: list[str] = [] + + +class User(BaseModel): + username: str + email: str | None = None + full_name: str | None = None + disabled: bool | None = None + + +class UserInDB(User): + hashed_password: str + + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +oauth2_scheme = OAuth2PasswordBearer( + tokenUrl="token", + scopes={"me": "Read information about the current user.", "items": "Read items."}, +) + +app = FastAPI() + + +def verify_password(plain_password, hashed_password): + return pwd_context.verify(plain_password, hashed_password) + + +def get_password_hash(password): + return pwd_context.hash(password) + + +def get_user(db, username: str): + if username in db: + user_dict = db[username] + return UserInDB(**user_dict) + + +def authenticate_user(fake_db, username: str, password: str): + user = get_user(fake_db, username) + if not user: + return False + if not verify_password(password, user.hashed_password): + return False + return user + + +def create_access_token(data: dict, expires_delta: timedelta | None = None): + to_encode = data.copy() + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(minutes=15) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt + + +async def get_current_user( + security_scopes: SecurityScopes, token: Annotated[str, Depends(oauth2_scheme)] +): + if security_scopes.scopes: + authenticate_value = f'Bearer scope="{security_scopes.scope_str}"' + else: + authenticate_value = "Bearer" + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": authenticate_value}, + ) + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + username: str = payload.get("sub") + if username is None: + raise credentials_exception + token_scopes = payload.get("scopes", []) + token_data = TokenData(scopes=token_scopes, username=username) + except (JWTError, ValidationError): + raise credentials_exception + user = get_user(fake_users_db, username=token_data.username) + if user is None: + raise credentials_exception + for scope in security_scopes.scopes: + if scope not in token_data.scopes: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Not enough permissions", + headers={"WWW-Authenticate": authenticate_value}, + ) + return user + + +async def get_current_active_user( + current_user: Annotated[User, Security(get_current_user, scopes=["me"])] +): + if current_user.disabled: + raise HTTPException(status_code=400, detail="Inactive user") + return current_user + + +@app.post("/token", response_model=Token) +async def login_for_access_token( + form_data: Annotated[OAuth2PasswordRequestForm, Depends()] +): + user = authenticate_user(fake_users_db, form_data.username, form_data.password) + if not user: + raise HTTPException(status_code=400, detail="Incorrect username or password") + access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + access_token = create_access_token( + data={"sub": user.username, "scopes": form_data.scopes}, + expires_delta=access_token_expires, + ) + return {"access_token": access_token, "token_type": "bearer"} + + +@app.get("/users/me/", response_model=User) +async def read_users_me( + current_user: Annotated[User, Depends(get_current_active_user)] +): + return current_user + + +@app.get("/users/me/items/") +async def read_own_items( + current_user: Annotated[User, Security(get_current_active_user, scopes=["items"])] +): + return [{"item_id": "Foo", "owner": current_user.username}] + + +@app.get("/status/") +async def read_system_status(current_user: Annotated[User, Depends(get_current_user)]): + return {"status": "ok"} diff --git a/docs_src/security/tutorial005_an_py39.py b/docs_src/security/tutorial005_an_py39.py new file mode 100644 index 000000000..ecb5ed516 --- /dev/null +++ b/docs_src/security/tutorial005_an_py39.py @@ -0,0 +1,177 @@ +from datetime import datetime, timedelta +from typing import Annotated, List, Union + +from fastapi import Depends, FastAPI, HTTPException, Security, status +from fastapi.security import ( + OAuth2PasswordBearer, + OAuth2PasswordRequestForm, + SecurityScopes, +) +from jose import JWTError, jwt +from passlib.context import CryptContext +from pydantic import BaseModel, ValidationError + +# to get a string like this run: +# openssl rand -hex 32 +SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7" +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 30 + + +fake_users_db = { + "johndoe": { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW", + "disabled": False, + }, + "alice": { + "username": "alice", + "full_name": "Alice Chains", + "email": "alicechains@example.com", + "hashed_password": "$2b$12$gSvqqUPvlXP2tfVFaWK1Be7DlH.PKZbv5H8KnzzVgXXbVxpva.pFm", + "disabled": True, + }, +} + + +class Token(BaseModel): + access_token: str + token_type: str + + +class TokenData(BaseModel): + username: Union[str, None] = None + scopes: List[str] = [] + + +class User(BaseModel): + username: str + email: Union[str, None] = None + full_name: Union[str, None] = None + disabled: Union[bool, None] = None + + +class UserInDB(User): + hashed_password: str + + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +oauth2_scheme = OAuth2PasswordBearer( + tokenUrl="token", + scopes={"me": "Read information about the current user.", "items": "Read items."}, +) + +app = FastAPI() + + +def verify_password(plain_password, hashed_password): + return pwd_context.verify(plain_password, hashed_password) + + +def get_password_hash(password): + return pwd_context.hash(password) + + +def get_user(db, username: str): + if username in db: + user_dict = db[username] + return UserInDB(**user_dict) + + +def authenticate_user(fake_db, username: str, password: str): + user = get_user(fake_db, username) + if not user: + return False + if not verify_password(password, user.hashed_password): + return False + return user + + +def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None): + to_encode = data.copy() + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(minutes=15) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt + + +async def get_current_user( + security_scopes: SecurityScopes, token: Annotated[str, Depends(oauth2_scheme)] +): + if security_scopes.scopes: + authenticate_value = f'Bearer scope="{security_scopes.scope_str}"' + else: + authenticate_value = "Bearer" + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": authenticate_value}, + ) + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + username: str = payload.get("sub") + if username is None: + raise credentials_exception + token_scopes = payload.get("scopes", []) + token_data = TokenData(scopes=token_scopes, username=username) + except (JWTError, ValidationError): + raise credentials_exception + user = get_user(fake_users_db, username=token_data.username) + if user is None: + raise credentials_exception + for scope in security_scopes.scopes: + if scope not in token_data.scopes: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Not enough permissions", + headers={"WWW-Authenticate": authenticate_value}, + ) + return user + + +async def get_current_active_user( + current_user: Annotated[User, Security(get_current_user, scopes=["me"])] +): + if current_user.disabled: + raise HTTPException(status_code=400, detail="Inactive user") + return current_user + + +@app.post("/token", response_model=Token) +async def login_for_access_token( + form_data: Annotated[OAuth2PasswordRequestForm, Depends()] +): + user = authenticate_user(fake_users_db, form_data.username, form_data.password) + if not user: + raise HTTPException(status_code=400, detail="Incorrect username or password") + access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + access_token = create_access_token( + data={"sub": user.username, "scopes": form_data.scopes}, + expires_delta=access_token_expires, + ) + return {"access_token": access_token, "token_type": "bearer"} + + +@app.get("/users/me/", response_model=User) +async def read_users_me( + current_user: Annotated[User, Depends(get_current_active_user)] +): + return current_user + + +@app.get("/users/me/items/") +async def read_own_items( + current_user: Annotated[User, Security(get_current_active_user, scopes=["items"])] +): + return [{"item_id": "Foo", "owner": current_user.username}] + + +@app.get("/status/") +async def read_system_status(current_user: Annotated[User, Depends(get_current_user)]): + return {"status": "ok"} diff --git a/docs_src/security/tutorial006_an.py b/docs_src/security/tutorial006_an.py new file mode 100644 index 000000000..985e4b2ad --- /dev/null +++ b/docs_src/security/tutorial006_an.py @@ -0,0 +1,12 @@ +from fastapi import Depends, FastAPI +from fastapi.security import HTTPBasic, HTTPBasicCredentials +from typing_extensions import Annotated + +app = FastAPI() + +security = HTTPBasic() + + +@app.get("/users/me") +def read_current_user(credentials: Annotated[HTTPBasicCredentials, Depends(security)]): + return {"username": credentials.username, "password": credentials.password} diff --git a/docs_src/security/tutorial006_an_py39.py b/docs_src/security/tutorial006_an_py39.py new file mode 100644 index 000000000..03c696a4b --- /dev/null +++ b/docs_src/security/tutorial006_an_py39.py @@ -0,0 +1,13 @@ +from typing import Annotated + +from fastapi import Depends, FastAPI +from fastapi.security import HTTPBasic, HTTPBasicCredentials + +app = FastAPI() + +security = HTTPBasic() + + +@app.get("/users/me") +def read_current_user(credentials: Annotated[HTTPBasicCredentials, Depends(security)]): + return {"username": credentials.username, "password": credentials.password} diff --git a/docs_src/security/tutorial007_an.py b/docs_src/security/tutorial007_an.py new file mode 100644 index 000000000..5fb7c8e57 --- /dev/null +++ b/docs_src/security/tutorial007_an.py @@ -0,0 +1,36 @@ +import secrets + +from fastapi import Depends, FastAPI, HTTPException, status +from fastapi.security import HTTPBasic, HTTPBasicCredentials +from typing_extensions import Annotated + +app = FastAPI() + +security = HTTPBasic() + + +def get_current_username( + credentials: Annotated[HTTPBasicCredentials, Depends(security)] +): + current_username_bytes = credentials.username.encode("utf8") + correct_username_bytes = b"stanleyjobson" + is_correct_username = secrets.compare_digest( + current_username_bytes, correct_username_bytes + ) + current_password_bytes = credentials.password.encode("utf8") + correct_password_bytes = b"swordfish" + is_correct_password = secrets.compare_digest( + current_password_bytes, correct_password_bytes + ) + if not (is_correct_username and is_correct_password): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect email or password", + headers={"WWW-Authenticate": "Basic"}, + ) + return credentials.username + + +@app.get("/users/me") +def read_current_user(username: Annotated[str, Depends(get_current_username)]): + return {"username": username} diff --git a/docs_src/security/tutorial007_an_py39.py b/docs_src/security/tutorial007_an_py39.py new file mode 100644 index 000000000..17177dabf --- /dev/null +++ b/docs_src/security/tutorial007_an_py39.py @@ -0,0 +1,36 @@ +import secrets +from typing import Annotated + +from fastapi import Depends, FastAPI, HTTPException, status +from fastapi.security import HTTPBasic, HTTPBasicCredentials + +app = FastAPI() + +security = HTTPBasic() + + +def get_current_username( + credentials: Annotated[HTTPBasicCredentials, Depends(security)] +): + current_username_bytes = credentials.username.encode("utf8") + correct_username_bytes = b"stanleyjobson" + is_correct_username = secrets.compare_digest( + current_username_bytes, correct_username_bytes + ) + current_password_bytes = credentials.password.encode("utf8") + correct_password_bytes = b"swordfish" + is_correct_password = secrets.compare_digest( + current_password_bytes, correct_password_bytes + ) + if not (is_correct_username and is_correct_password): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect email or password", + headers={"WWW-Authenticate": "Basic"}, + ) + return credentials.username + + +@app.get("/users/me") +def read_current_user(username: Annotated[str, Depends(get_current_username)]): + return {"username": username} diff --git a/docs_src/settings/app02_an/__init__.py b/docs_src/settings/app02_an/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/settings/app02_an/config.py b/docs_src/settings/app02_an/config.py new file mode 100644 index 000000000..9a7829135 --- /dev/null +++ b/docs_src/settings/app02_an/config.py @@ -0,0 +1,7 @@ +from pydantic import BaseSettings + + +class Settings(BaseSettings): + app_name: str = "Awesome API" + admin_email: str + items_per_user: int = 50 diff --git a/docs_src/settings/app02_an/main.py b/docs_src/settings/app02_an/main.py new file mode 100644 index 000000000..cb679202d --- /dev/null +++ b/docs_src/settings/app02_an/main.py @@ -0,0 +1,22 @@ +from functools import lru_cache + +from fastapi import Depends, FastAPI +from typing_extensions import Annotated + +from .config import Settings + +app = FastAPI() + + +@lru_cache() +def get_settings(): + return Settings() + + +@app.get("/info") +async def info(settings: Annotated[Settings, Depends(get_settings)]): + return { + "app_name": settings.app_name, + "admin_email": settings.admin_email, + "items_per_user": settings.items_per_user, + } diff --git a/docs_src/settings/app02_an/test_main.py b/docs_src/settings/app02_an/test_main.py new file mode 100644 index 000000000..7a04d7e8e --- /dev/null +++ b/docs_src/settings/app02_an/test_main.py @@ -0,0 +1,23 @@ +from fastapi.testclient import TestClient + +from .config import Settings +from .main import app, get_settings + +client = TestClient(app) + + +def get_settings_override(): + return Settings(admin_email="testing_admin@example.com") + + +app.dependency_overrides[get_settings] = get_settings_override + + +def test_app(): + response = client.get("/info") + data = response.json() + assert data == { + "app_name": "Awesome API", + "admin_email": "testing_admin@example.com", + "items_per_user": 50, + } diff --git a/docs_src/settings/app02_an_py39/__init__.py b/docs_src/settings/app02_an_py39/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/settings/app02_an_py39/config.py b/docs_src/settings/app02_an_py39/config.py new file mode 100644 index 000000000..9a7829135 --- /dev/null +++ b/docs_src/settings/app02_an_py39/config.py @@ -0,0 +1,7 @@ +from pydantic import BaseSettings + + +class Settings(BaseSettings): + app_name: str = "Awesome API" + admin_email: str + items_per_user: int = 50 diff --git a/docs_src/settings/app02_an_py39/main.py b/docs_src/settings/app02_an_py39/main.py new file mode 100644 index 000000000..61be74fcb --- /dev/null +++ b/docs_src/settings/app02_an_py39/main.py @@ -0,0 +1,22 @@ +from functools import lru_cache +from typing import Annotated + +from fastapi import Depends, FastAPI + +from .config import Settings + +app = FastAPI() + + +@lru_cache() +def get_settings(): + return Settings() + + +@app.get("/info") +async def info(settings: Annotated[Settings, Depends(get_settings)]): + return { + "app_name": settings.app_name, + "admin_email": settings.admin_email, + "items_per_user": settings.items_per_user, + } diff --git a/docs_src/settings/app02_an_py39/test_main.py b/docs_src/settings/app02_an_py39/test_main.py new file mode 100644 index 000000000..7a04d7e8e --- /dev/null +++ b/docs_src/settings/app02_an_py39/test_main.py @@ -0,0 +1,23 @@ +from fastapi.testclient import TestClient + +from .config import Settings +from .main import app, get_settings + +client = TestClient(app) + + +def get_settings_override(): + return Settings(admin_email="testing_admin@example.com") + + +app.dependency_overrides[get_settings] = get_settings_override + + +def test_app(): + response = client.get("/info") + data = response.json() + assert data == { + "app_name": "Awesome API", + "admin_email": "testing_admin@example.com", + "items_per_user": 50, + } diff --git a/docs_src/settings/app03_an/__init__.py b/docs_src/settings/app03_an/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/settings/app03_an/config.py b/docs_src/settings/app03_an/config.py new file mode 100644 index 000000000..e1c3ee300 --- /dev/null +++ b/docs_src/settings/app03_an/config.py @@ -0,0 +1,10 @@ +from pydantic import BaseSettings + + +class Settings(BaseSettings): + app_name: str = "Awesome API" + admin_email: str + items_per_user: int = 50 + + class Config: + env_file = ".env" diff --git a/docs_src/settings/app03_an/main.py b/docs_src/settings/app03_an/main.py new file mode 100644 index 000000000..c33b98f47 --- /dev/null +++ b/docs_src/settings/app03_an/main.py @@ -0,0 +1,22 @@ +from functools import lru_cache +from typing import Annotated + +from fastapi import Depends, FastAPI + +from . import config + +app = FastAPI() + + +@lru_cache() +def get_settings(): + return config.Settings() + + +@app.get("/info") +async def info(settings: Annotated[config.Settings, Depends(get_settings)]): + return { + "app_name": settings.app_name, + "admin_email": settings.admin_email, + "items_per_user": settings.items_per_user, + } diff --git a/docs_src/settings/app03_an_py39/__init__.py b/docs_src/settings/app03_an_py39/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/settings/app03_an_py39/config.py b/docs_src/settings/app03_an_py39/config.py new file mode 100644 index 000000000..e1c3ee300 --- /dev/null +++ b/docs_src/settings/app03_an_py39/config.py @@ -0,0 +1,10 @@ +from pydantic import BaseSettings + + +class Settings(BaseSettings): + app_name: str = "Awesome API" + admin_email: str + items_per_user: int = 50 + + class Config: + env_file = ".env" diff --git a/docs_src/settings/app03_an_py39/main.py b/docs_src/settings/app03_an_py39/main.py new file mode 100644 index 000000000..b89c6b6cf --- /dev/null +++ b/docs_src/settings/app03_an_py39/main.py @@ -0,0 +1,22 @@ +from functools import lru_cache + +from fastapi import Depends, FastAPI +from typing_extensions import Annotated + +from . import config + +app = FastAPI() + + +@lru_cache() +def get_settings(): + return config.Settings() + + +@app.get("/info") +async def info(settings: Annotated[config.Settings, Depends(get_settings)]): + return { + "app_name": settings.app_name, + "admin_email": settings.admin_email, + "items_per_user": settings.items_per_user, + } diff --git a/docs_src/websockets/tutorial002_an.py b/docs_src/websockets/tutorial002_an.py new file mode 100644 index 000000000..c838fbd30 --- /dev/null +++ b/docs_src/websockets/tutorial002_an.py @@ -0,0 +1,93 @@ +from typing import Union + +from fastapi import ( + Cookie, + Depends, + FastAPI, + Query, + WebSocket, + WebSocketException, + status, +) +from fastapi.responses import HTMLResponse +from typing_extensions import Annotated + +app = FastAPI() + +html = """ + + + + Chat + + +

WebSocket Chat

+
+ + + +
+ + +
+ + + + +""" + + +@app.get("/") +async def get(): + return HTMLResponse(html) + + +async def get_cookie_or_token( + websocket: WebSocket, + session: Annotated[Union[str, None], Cookie()] = None, + token: Annotated[Union[str, None], Query()] = None, +): + if session is None and token is None: + raise WebSocketException(code=status.WS_1008_POLICY_VIOLATION) + return session or token + + +@app.websocket("/items/{item_id}/ws") +async def websocket_endpoint( + *, + websocket: WebSocket, + item_id: str, + q: Union[int, None] = None, + cookie_or_token: Annotated[str, Depends(get_cookie_or_token)], +): + await websocket.accept() + while True: + data = await websocket.receive_text() + await websocket.send_text( + f"Session cookie or query token value is: {cookie_or_token}" + ) + if q is not None: + await websocket.send_text(f"Query parameter q is: {q}") + await websocket.send_text(f"Message text was: {data}, for item ID: {item_id}") diff --git a/docs_src/websockets/tutorial002_an_py310.py b/docs_src/websockets/tutorial002_an_py310.py new file mode 100644 index 000000000..551539b32 --- /dev/null +++ b/docs_src/websockets/tutorial002_an_py310.py @@ -0,0 +1,92 @@ +from typing import Annotated + +from fastapi import ( + Cookie, + Depends, + FastAPI, + Query, + WebSocket, + WebSocketException, + status, +) +from fastapi.responses import HTMLResponse + +app = FastAPI() + +html = """ + + + + Chat + + +

WebSocket Chat

+
+ + + +
+ + +
+ + + + +""" + + +@app.get("/") +async def get(): + return HTMLResponse(html) + + +async def get_cookie_or_token( + websocket: WebSocket, + session: Annotated[str | None, Cookie()] = None, + token: Annotated[str | None, Query()] = None, +): + if session is None and token is None: + raise WebSocketException(code=status.WS_1008_POLICY_VIOLATION) + return session or token + + +@app.websocket("/items/{item_id}/ws") +async def websocket_endpoint( + *, + websocket: WebSocket, + item_id: str, + q: int | None = None, + cookie_or_token: Annotated[str, Depends(get_cookie_or_token)], +): + await websocket.accept() + while True: + data = await websocket.receive_text() + await websocket.send_text( + f"Session cookie or query token value is: {cookie_or_token}" + ) + if q is not None: + await websocket.send_text(f"Query parameter q is: {q}") + await websocket.send_text(f"Message text was: {data}, for item ID: {item_id}") diff --git a/docs_src/websockets/tutorial002_an_py39.py b/docs_src/websockets/tutorial002_an_py39.py new file mode 100644 index 000000000..606d355fe --- /dev/null +++ b/docs_src/websockets/tutorial002_an_py39.py @@ -0,0 +1,92 @@ +from typing import Annotated, Union + +from fastapi import ( + Cookie, + Depends, + FastAPI, + Query, + WebSocket, + WebSocketException, + status, +) +from fastapi.responses import HTMLResponse + +app = FastAPI() + +html = """ + + + + Chat + + +

WebSocket Chat

+
+ + + +
+ + +
+ + + + +""" + + +@app.get("/") +async def get(): + return HTMLResponse(html) + + +async def get_cookie_or_token( + websocket: WebSocket, + session: Annotated[Union[str, None], Cookie()] = None, + token: Annotated[Union[str, None], Query()] = None, +): + if session is None and token is None: + raise WebSocketException(code=status.WS_1008_POLICY_VIOLATION) + return session or token + + +@app.websocket("/items/{item_id}/ws") +async def websocket_endpoint( + *, + websocket: WebSocket, + item_id: str, + q: Union[int, None] = None, + cookie_or_token: Annotated[str, Depends(get_cookie_or_token)], +): + await websocket.accept() + while True: + data = await websocket.receive_text() + await websocket.send_text( + f"Session cookie or query token value is: {cookie_or_token}" + ) + if q is not None: + await websocket.send_text(f"Query parameter q is: {q}") + await websocket.send_text(f"Message text was: {data}, for item ID: {item_id}") diff --git a/docs_src/websockets/tutorial002_py310.py b/docs_src/websockets/tutorial002_py310.py new file mode 100644 index 000000000..51daf58e5 --- /dev/null +++ b/docs_src/websockets/tutorial002_py310.py @@ -0,0 +1,89 @@ +from fastapi import ( + Cookie, + Depends, + FastAPI, + Query, + WebSocket, + WebSocketException, + status, +) +from fastapi.responses import HTMLResponse + +app = FastAPI() + +html = """ + + + + Chat + + +

WebSocket Chat

+
+ + + +
+ + +
+ + + + +""" + + +@app.get("/") +async def get(): + return HTMLResponse(html) + + +async def get_cookie_or_token( + websocket: WebSocket, + session: str | None = Cookie(default=None), + token: str | None = Query(default=None), +): + if session is None and token is None: + raise WebSocketException(code=status.WS_1008_POLICY_VIOLATION) + return session or token + + +@app.websocket("/items/{item_id}/ws") +async def websocket_endpoint( + websocket: WebSocket, + item_id: str, + q: int | None = None, + cookie_or_token: str = Depends(get_cookie_or_token), +): + await websocket.accept() + while True: + data = await websocket.receive_text() + await websocket.send_text( + f"Session cookie or query token value is: {cookie_or_token}" + ) + if q is not None: + await websocket.send_text(f"Query parameter q is: {q}") + await websocket.send_text(f"Message text was: {data}, for item ID: {item_id}") diff --git a/docs_src/websockets/tutorial003_py39.py b/docs_src/websockets/tutorial003_py39.py new file mode 100644 index 000000000..316218088 --- /dev/null +++ b/docs_src/websockets/tutorial003_py39.py @@ -0,0 +1,81 @@ +from fastapi import FastAPI, WebSocket, WebSocketDisconnect +from fastapi.responses import HTMLResponse + +app = FastAPI() + +html = """ + + + + Chat + + +

WebSocket Chat

+

Your ID:

+
+ + +
+ + + + +""" + + +class ConnectionManager: + def __init__(self): + self.active_connections: list[WebSocket] = [] + + async def connect(self, websocket: WebSocket): + await websocket.accept() + self.active_connections.append(websocket) + + def disconnect(self, websocket: WebSocket): + self.active_connections.remove(websocket) + + async def send_personal_message(self, message: str, websocket: WebSocket): + await websocket.send_text(message) + + async def broadcast(self, message: str): + for connection in self.active_connections: + await connection.send_text(message) + + +manager = ConnectionManager() + + +@app.get("/") +async def get(): + return HTMLResponse(html) + + +@app.websocket("/ws/{client_id}") +async def websocket_endpoint(websocket: WebSocket, client_id: int): + await manager.connect(websocket) + try: + while True: + data = await websocket.receive_text() + await manager.send_personal_message(f"You wrote: {data}", websocket) + await manager.broadcast(f"Client #{client_id} says: {data}") + except WebSocketDisconnect: + manager.disconnect(websocket) + await manager.broadcast(f"Client #{client_id} left the chat") diff --git a/pyproject.toml b/pyproject.toml index b8d006359..6aa095a64 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -186,6 +186,12 @@ ignore = [ "docs_src/dataclasses/tutorial003.py" = ["I001"] "docs_src/path_operation_advanced_configuration/tutorial007.py" = ["B904"] "docs_src/custom_request_and_route/tutorial002.py" = ["B904"] +"docs_src/dependencies/tutorial008_an.py" = ["F821"] +"docs_src/dependencies/tutorial008_an_py39.py" = ["F821"] +"docs_src/query_params_str_validations/tutorial012_an.py" = ["B006"] +"docs_src/query_params_str_validations/tutorial012_an_py39.py" = ["B006"] +"docs_src/query_params_str_validations/tutorial013_an.py" = ["B006"] +"docs_src/query_params_str_validations/tutorial013_an_py39.py" = ["B006"] [tool.ruff.isort] known-third-party = ["fastapi", "pydantic", "starlette"] diff --git a/tests/test_tutorial/test_additional_status_codes/test_tutorial001_an.py b/tests/test_tutorial/test_additional_status_codes/test_tutorial001_an.py new file mode 100644 index 000000000..2cb2bb993 --- /dev/null +++ b/tests/test_tutorial/test_additional_status_codes/test_tutorial001_an.py @@ -0,0 +1,17 @@ +from fastapi.testclient import TestClient + +from docs_src.additional_status_codes.tutorial001_an import app + +client = TestClient(app) + + +def test_update(): + response = client.put("/items/foo", json={"name": "Wrestlers"}) + assert response.status_code == 200, response.text + assert response.json() == {"name": "Wrestlers", "size": None} + + +def test_create(): + response = client.put("/items/red", json={"name": "Chillies"}) + assert response.status_code == 201, response.text + assert response.json() == {"name": "Chillies", "size": None} diff --git a/tests/test_tutorial/test_additional_status_codes/test_tutorial001_an_py310.py b/tests/test_tutorial/test_additional_status_codes/test_tutorial001_an_py310.py new file mode 100644 index 000000000..c7660a392 --- /dev/null +++ b/tests/test_tutorial/test_additional_status_codes/test_tutorial001_an_py310.py @@ -0,0 +1,26 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.additional_status_codes.tutorial001_an_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_update(client: TestClient): + response = client.put("/items/foo", json={"name": "Wrestlers"}) + assert response.status_code == 200, response.text + assert response.json() == {"name": "Wrestlers", "size": None} + + +@needs_py310 +def test_create(client: TestClient): + response = client.put("/items/red", json={"name": "Chillies"}) + assert response.status_code == 201, response.text + assert response.json() == {"name": "Chillies", "size": None} diff --git a/tests/test_tutorial/test_additional_status_codes/test_tutorial001_an_py39.py b/tests/test_tutorial/test_additional_status_codes/test_tutorial001_an_py39.py new file mode 100644 index 000000000..303c5dbae --- /dev/null +++ b/tests/test_tutorial/test_additional_status_codes/test_tutorial001_an_py39.py @@ -0,0 +1,26 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.additional_status_codes.tutorial001_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_update(client: TestClient): + response = client.put("/items/foo", json={"name": "Wrestlers"}) + assert response.status_code == 200, response.text + assert response.json() == {"name": "Wrestlers", "size": None} + + +@needs_py39 +def test_create(client: TestClient): + response = client.put("/items/red", json={"name": "Chillies"}) + assert response.status_code == 201, response.text + assert response.json() == {"name": "Chillies", "size": None} diff --git a/tests/test_tutorial/test_additional_status_codes/test_tutorial001_py310.py b/tests/test_tutorial/test_additional_status_codes/test_tutorial001_py310.py new file mode 100644 index 000000000..02f2e188c --- /dev/null +++ b/tests/test_tutorial/test_additional_status_codes/test_tutorial001_py310.py @@ -0,0 +1,26 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.additional_status_codes.tutorial001_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_update(client: TestClient): + response = client.put("/items/foo", json={"name": "Wrestlers"}) + assert response.status_code == 200, response.text + assert response.json() == {"name": "Wrestlers", "size": None} + + +@needs_py310 +def test_create(client: TestClient): + response = client.put("/items/red", json={"name": "Chillies"}) + assert response.status_code == 201, response.text + assert response.json() == {"name": "Chillies", "size": None} diff --git a/tests/test_tutorial/test_background_tasks/test_tutorial002_an.py b/tests/test_tutorial/test_background_tasks/test_tutorial002_an.py new file mode 100644 index 000000000..af682ecff --- /dev/null +++ b/tests/test_tutorial/test_background_tasks/test_tutorial002_an.py @@ -0,0 +1,19 @@ +import os +from pathlib import Path + +from fastapi.testclient import TestClient + +from docs_src.background_tasks.tutorial002_an import app + +client = TestClient(app) + + +def test(): + log = Path("log.txt") + if log.is_file(): + os.remove(log) # pragma: no cover + response = client.post("/send-notification/foo@example.com?q=some-query") + assert response.status_code == 200, response.text + assert response.json() == {"message": "Message sent"} + with open("./log.txt") as f: + assert "found query: some-query\nmessage to foo@example.com" in f.read() diff --git a/tests/test_tutorial/test_background_tasks/test_tutorial002_an_py310.py b/tests/test_tutorial/test_background_tasks/test_tutorial002_an_py310.py new file mode 100644 index 000000000..067b2787e --- /dev/null +++ b/tests/test_tutorial/test_background_tasks/test_tutorial002_an_py310.py @@ -0,0 +1,21 @@ +import os +from pathlib import Path + +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@needs_py310 +def test(): + from docs_src.background_tasks.tutorial002_an_py310 import app + + client = TestClient(app) + log = Path("log.txt") + if log.is_file(): + os.remove(log) # pragma: no cover + response = client.post("/send-notification/foo@example.com?q=some-query") + assert response.status_code == 200, response.text + assert response.json() == {"message": "Message sent"} + with open("./log.txt") as f: + assert "found query: some-query\nmessage to foo@example.com" in f.read() diff --git a/tests/test_tutorial/test_background_tasks/test_tutorial002_an_py39.py b/tests/test_tutorial/test_background_tasks/test_tutorial002_an_py39.py new file mode 100644 index 000000000..06b5a2f57 --- /dev/null +++ b/tests/test_tutorial/test_background_tasks/test_tutorial002_an_py39.py @@ -0,0 +1,21 @@ +import os +from pathlib import Path + +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + + +@needs_py39 +def test(): + from docs_src.background_tasks.tutorial002_an_py39 import app + + client = TestClient(app) + log = Path("log.txt") + if log.is_file(): + os.remove(log) # pragma: no cover + response = client.post("/send-notification/foo@example.com?q=some-query") + assert response.status_code == 200, response.text + assert response.json() == {"message": "Message sent"} + with open("./log.txt") as f: + assert "found query: some-query\nmessage to foo@example.com" in f.read() diff --git a/tests/test_tutorial/test_bigger_applications/test_main_an.py b/tests/test_tutorial/test_bigger_applications/test_main_an.py new file mode 100644 index 000000000..4b84a31b5 --- /dev/null +++ b/tests/test_tutorial/test_bigger_applications/test_main_an.py @@ -0,0 +1,491 @@ +import pytest +from fastapi.testclient import TestClient + +from docs_src.bigger_applications.app_an.main import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/": { + "get": { + "tags": ["users"], + "summary": "Read Users", + "operationId": "read_users_users__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/users/me": { + "get": { + "tags": ["users"], + "summary": "Read User Me", + "operationId": "read_user_me_users_me_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/users/{username}": { + "get": { + "tags": ["users"], + "summary": "Read User", + "operationId": "read_user_users__username__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Username", "type": "string"}, + "name": "username", + "in": "path", + }, + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/items/": { + "get": { + "tags": ["items"], + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + }, + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "404": {"description": "Not found"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/items/{item_id}": { + "get": { + "tags": ["items"], + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + }, + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + }, + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "404": {"description": "Not found"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "put": { + "tags": ["items", "custom"], + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + }, + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + }, + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "404": {"description": "Not found"}, + "403": {"description": "Operation forbidden"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + "/admin/": { + "post": { + "tags": ["admin"], + "summary": "Update Admin", + "operationId": "update_admin_admin__post", + "parameters": [ + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + }, + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "418": {"description": "I'm a teapot"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/": { + "get": { + "summary": "Root", + "operationId": "root__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +no_jessica = { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] +} + + +@pytest.mark.parametrize( + "path,expected_status,expected_response,headers", + [ + ( + "/users?token=jessica", + 200, + [{"username": "Rick"}, {"username": "Morty"}], + {}, + ), + ("/users", 422, no_jessica, {}), + ("/users/foo?token=jessica", 200, {"username": "foo"}, {}), + ("/users/foo", 422, no_jessica, {}), + ("/users/me?token=jessica", 200, {"username": "fakecurrentuser"}, {}), + ("/users/me", 422, no_jessica, {}), + ( + "/users?token=monica", + 400, + {"detail": "No Jessica token provided"}, + {}, + ), + ( + "/items?token=jessica", + 200, + {"plumbus": {"name": "Plumbus"}, "gun": {"name": "Portal Gun"}}, + {"X-Token": "fake-super-secret-token"}, + ), + ("/items", 422, no_jessica, {"X-Token": "fake-super-secret-token"}), + ( + "/items/plumbus?token=jessica", + 200, + {"name": "Plumbus", "item_id": "plumbus"}, + {"X-Token": "fake-super-secret-token"}, + ), + ( + "/items/bar?token=jessica", + 404, + {"detail": "Item not found"}, + {"X-Token": "fake-super-secret-token"}, + ), + ("/items/plumbus", 422, no_jessica, {"X-Token": "fake-super-secret-token"}), + ( + "/items?token=jessica", + 400, + {"detail": "X-Token header invalid"}, + {"X-Token": "invalid"}, + ), + ( + "/items/bar?token=jessica", + 400, + {"detail": "X-Token header invalid"}, + {"X-Token": "invalid"}, + ), + ( + "/items?token=jessica", + 422, + { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + } + ] + }, + {}, + ), + ( + "/items/plumbus?token=jessica", + 422, + { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + } + ] + }, + {}, + ), + ("/?token=jessica", 200, {"message": "Hello Bigger Applications!"}, {}), + ("/", 422, no_jessica, {}), + ("/openapi.json", 200, openapi_schema, {}), + ], +) +def test_get_path(path, expected_status, expected_response, headers): + response = client.get(path, headers=headers) + assert response.status_code == expected_status + assert response.json() == expected_response + + +def test_put_no_header(): + response = client.put("/items/foo") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + + +def test_put_invalid_header(): + response = client.put("/items/foo", headers={"X-Token": "invalid"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "X-Token header invalid"} + + +def test_put(): + response = client.put( + "/items/plumbus?token=jessica", headers={"X-Token": "fake-super-secret-token"} + ) + assert response.status_code == 200, response.text + assert response.json() == {"item_id": "plumbus", "name": "The great Plumbus"} + + +def test_put_forbidden(): + response = client.put( + "/items/bar?token=jessica", headers={"X-Token": "fake-super-secret-token"} + ) + assert response.status_code == 403, response.text + assert response.json() == {"detail": "You can only update the item: plumbus"} + + +def test_admin(): + response = client.post( + "/admin/?token=jessica", headers={"X-Token": "fake-super-secret-token"} + ) + assert response.status_code == 200, response.text + assert response.json() == {"message": "Admin getting schwifty"} + + +def test_admin_invalid_header(): + response = client.post("/admin/", headers={"X-Token": "invalid"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "X-Token header invalid"} diff --git a/tests/test_tutorial/test_bigger_applications/test_main_an_py39.py b/tests/test_tutorial/test_bigger_applications/test_main_an_py39.py new file mode 100644 index 000000000..1caf5bd49 --- /dev/null +++ b/tests/test_tutorial/test_bigger_applications/test_main_an_py39.py @@ -0,0 +1,506 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/": { + "get": { + "tags": ["users"], + "summary": "Read Users", + "operationId": "read_users_users__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/users/me": { + "get": { + "tags": ["users"], + "summary": "Read User Me", + "operationId": "read_user_me_users_me_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/users/{username}": { + "get": { + "tags": ["users"], + "summary": "Read User", + "operationId": "read_user_users__username__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Username", "type": "string"}, + "name": "username", + "in": "path", + }, + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/items/": { + "get": { + "tags": ["items"], + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + }, + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "404": {"description": "Not found"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/items/{item_id}": { + "get": { + "tags": ["items"], + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + }, + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + }, + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "404": {"description": "Not found"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "put": { + "tags": ["items", "custom"], + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + }, + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + }, + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "404": {"description": "Not found"}, + "403": {"description": "Operation forbidden"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + "/admin/": { + "post": { + "tags": ["admin"], + "summary": "Update Admin", + "operationId": "update_admin_admin__post", + "parameters": [ + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + }, + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "418": {"description": "I'm a teapot"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/": { + "get": { + "summary": "Root", + "operationId": "root__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +no_jessica = { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.bigger_applications.app_an_py39.main import app + + client = TestClient(app) + return client + + +@needs_py39 +@pytest.mark.parametrize( + "path,expected_status,expected_response,headers", + [ + ( + "/users?token=jessica", + 200, + [{"username": "Rick"}, {"username": "Morty"}], + {}, + ), + ("/users", 422, no_jessica, {}), + ("/users/foo?token=jessica", 200, {"username": "foo"}, {}), + ("/users/foo", 422, no_jessica, {}), + ("/users/me?token=jessica", 200, {"username": "fakecurrentuser"}, {}), + ("/users/me", 422, no_jessica, {}), + ( + "/users?token=monica", + 400, + {"detail": "No Jessica token provided"}, + {}, + ), + ( + "/items?token=jessica", + 200, + {"plumbus": {"name": "Plumbus"}, "gun": {"name": "Portal Gun"}}, + {"X-Token": "fake-super-secret-token"}, + ), + ("/items", 422, no_jessica, {"X-Token": "fake-super-secret-token"}), + ( + "/items/plumbus?token=jessica", + 200, + {"name": "Plumbus", "item_id": "plumbus"}, + {"X-Token": "fake-super-secret-token"}, + ), + ( + "/items/bar?token=jessica", + 404, + {"detail": "Item not found"}, + {"X-Token": "fake-super-secret-token"}, + ), + ("/items/plumbus", 422, no_jessica, {"X-Token": "fake-super-secret-token"}), + ( + "/items?token=jessica", + 400, + {"detail": "X-Token header invalid"}, + {"X-Token": "invalid"}, + ), + ( + "/items/bar?token=jessica", + 400, + {"detail": "X-Token header invalid"}, + {"X-Token": "invalid"}, + ), + ( + "/items?token=jessica", + 422, + { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + } + ] + }, + {}, + ), + ( + "/items/plumbus?token=jessica", + 422, + { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + } + ] + }, + {}, + ), + ("/?token=jessica", 200, {"message": "Hello Bigger Applications!"}, {}), + ("/", 422, no_jessica, {}), + ("/openapi.json", 200, openapi_schema, {}), + ], +) +def test_get_path( + path, expected_status, expected_response, headers, client: TestClient +): + response = client.get(path, headers=headers) + assert response.status_code == expected_status + assert response.json() == expected_response + + +@needs_py39 +def test_put_no_header(client: TestClient): + response = client.put("/items/foo") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + + +@needs_py39 +def test_put_invalid_header(client: TestClient): + response = client.put("/items/foo", headers={"X-Token": "invalid"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "X-Token header invalid"} + + +@needs_py39 +def test_put(client: TestClient): + response = client.put( + "/items/plumbus?token=jessica", headers={"X-Token": "fake-super-secret-token"} + ) + assert response.status_code == 200, response.text + assert response.json() == {"item_id": "plumbus", "name": "The great Plumbus"} + + +@needs_py39 +def test_put_forbidden(client: TestClient): + response = client.put( + "/items/bar?token=jessica", headers={"X-Token": "fake-super-secret-token"} + ) + assert response.status_code == 403, response.text + assert response.json() == {"detail": "You can only update the item: plumbus"} + + +@needs_py39 +def test_admin(client: TestClient): + response = client.post( + "/admin/?token=jessica", headers={"X-Token": "fake-super-secret-token"} + ) + assert response.status_code == 200, response.text + assert response.json() == {"message": "Admin getting schwifty"} + + +@needs_py39 +def test_admin_invalid_header(client: TestClient): + response = client.post("/admin/", headers={"X-Token": "invalid"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "X-Token header invalid"} diff --git a/tests/test_tutorial/test_body_fields/test_tutorial001_an.py b/tests/test_tutorial/test_body_fields/test_tutorial001_an.py new file mode 100644 index 000000000..879769147 --- /dev/null +++ b/tests/test_tutorial/test_body_fields/test_tutorial001_an.py @@ -0,0 +1,169 @@ +import pytest +from fastapi.testclient import TestClient + +from docs_src.body_fields.tutorial001_an import app + +client = TestClient(app) + + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_update_item_items__item_id__put" + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": { + "title": "The description of the item", + "maxLength": 300, + "type": "string", + }, + "price": { + "title": "Price", + "exclusiveMinimum": 0.0, + "type": "number", + "description": "The price must be greater than zero", + }, + "tax": {"title": "Tax", "type": "number"}, + }, + }, + "Body_update_item_items__item_id__put": { + "title": "Body_update_item_items__item_id__put", + "required": ["item"], + "type": "object", + "properties": {"item": {"$ref": "#/components/schemas/Item"}}, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +price_not_greater = { + "detail": [ + { + "ctx": {"limit_value": 0}, + "loc": ["body", "item", "price"], + "msg": "ensure this value is greater than 0", + "type": "value_error.number.not_gt", + } + ] +} + + +@pytest.mark.parametrize( + "path,body,expected_status,expected_response", + [ + ( + "/items/5", + {"item": {"name": "Foo", "price": 3.0}}, + 200, + { + "item_id": 5, + "item": {"name": "Foo", "price": 3.0, "description": None, "tax": None}, + }, + ), + ( + "/items/6", + { + "item": { + "name": "Bar", + "price": 0.2, + "description": "Some bar", + "tax": "5.4", + } + }, + 200, + { + "item_id": 6, + "item": { + "name": "Bar", + "price": 0.2, + "description": "Some bar", + "tax": 5.4, + }, + }, + ), + ("/items/5", {"item": {"name": "Foo", "price": -3.0}}, 422, price_not_greater), + ], +) +def test(path, body, expected_status, expected_response): + response = client.put(path, json=body) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_body_fields/test_tutorial001_an_py310.py b/tests/test_tutorial/test_body_fields/test_tutorial001_an_py310.py new file mode 100644 index 000000000..0cd57a187 --- /dev/null +++ b/tests/test_tutorial/test_body_fields/test_tutorial001_an_py310.py @@ -0,0 +1,176 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_update_item_items__item_id__put" + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": { + "title": "The description of the item", + "maxLength": 300, + "type": "string", + }, + "price": { + "title": "Price", + "exclusiveMinimum": 0.0, + "type": "number", + "description": "The price must be greater than zero", + }, + "tax": {"title": "Tax", "type": "number"}, + }, + }, + "Body_update_item_items__item_id__put": { + "title": "Body_update_item_items__item_id__put", + "required": ["item"], + "type": "object", + "properties": {"item": {"$ref": "#/components/schemas/Item"}}, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.body_fields.tutorial001_an_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +price_not_greater = { + "detail": [ + { + "ctx": {"limit_value": 0}, + "loc": ["body", "item", "price"], + "msg": "ensure this value is greater than 0", + "type": "value_error.number.not_gt", + } + ] +} + + +@needs_py310 +@pytest.mark.parametrize( + "path,body,expected_status,expected_response", + [ + ( + "/items/5", + {"item": {"name": "Foo", "price": 3.0}}, + 200, + { + "item_id": 5, + "item": {"name": "Foo", "price": 3.0, "description": None, "tax": None}, + }, + ), + ( + "/items/6", + { + "item": { + "name": "Bar", + "price": 0.2, + "description": "Some bar", + "tax": "5.4", + } + }, + 200, + { + "item_id": 6, + "item": { + "name": "Bar", + "price": 0.2, + "description": "Some bar", + "tax": 5.4, + }, + }, + ), + ("/items/5", {"item": {"name": "Foo", "price": -3.0}}, 422, price_not_greater), + ], +) +def test(path, body, expected_status, expected_response, client: TestClient): + response = client.put(path, json=body) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_body_fields/test_tutorial001_an_py39.py b/tests/test_tutorial/test_body_fields/test_tutorial001_an_py39.py new file mode 100644 index 000000000..26ff26f50 --- /dev/null +++ b/tests/test_tutorial/test_body_fields/test_tutorial001_an_py39.py @@ -0,0 +1,176 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_update_item_items__item_id__put" + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": { + "title": "The description of the item", + "maxLength": 300, + "type": "string", + }, + "price": { + "title": "Price", + "exclusiveMinimum": 0.0, + "type": "number", + "description": "The price must be greater than zero", + }, + "tax": {"title": "Tax", "type": "number"}, + }, + }, + "Body_update_item_items__item_id__put": { + "title": "Body_update_item_items__item_id__put", + "required": ["item"], + "type": "object", + "properties": {"item": {"$ref": "#/components/schemas/Item"}}, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.body_fields.tutorial001_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +price_not_greater = { + "detail": [ + { + "ctx": {"limit_value": 0}, + "loc": ["body", "item", "price"], + "msg": "ensure this value is greater than 0", + "type": "value_error.number.not_gt", + } + ] +} + + +@needs_py39 +@pytest.mark.parametrize( + "path,body,expected_status,expected_response", + [ + ( + "/items/5", + {"item": {"name": "Foo", "price": 3.0}}, + 200, + { + "item_id": 5, + "item": {"name": "Foo", "price": 3.0, "description": None, "tax": None}, + }, + ), + ( + "/items/6", + { + "item": { + "name": "Bar", + "price": 0.2, + "description": "Some bar", + "tax": "5.4", + } + }, + 200, + { + "item_id": 6, + "item": { + "name": "Bar", + "price": 0.2, + "description": "Some bar", + "tax": 5.4, + }, + }, + ), + ("/items/5", {"item": {"name": "Foo", "price": -3.0}}, 422, price_not_greater), + ], +) +def test(path, body, expected_status, expected_response, client: TestClient): + response = client.put(path, json=body) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_annotated/test_tutorial003.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an.py similarity index 54% rename from tests/test_tutorial/test_annotated/test_tutorial003.py rename to tests/test_tutorial/test_body_multiple_params/test_tutorial001_an.py index caf7ffdf1..94ba8593a 100644 --- a/tests/test_tutorial/test_annotated/test_tutorial003.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an.py @@ -1,7 +1,7 @@ import pytest from fastapi.testclient import TestClient -from docs_src.annotated.tutorial003 import app +from docs_src.body_multiple_params.tutorial001_an import app client = TestClient(app) @@ -10,21 +10,7 @@ openapi_schema = { "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { - "get": { - "summary": "Read Items", - "operationId": "read_items_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "exclusiveMinimum": 0.0, - "type": "integer", - }, - "name": "item_id", - "in": "path", - } - ], + "put": { "responses": { "200": { "description": "Successful Response", @@ -41,55 +27,48 @@ openapi_schema = { }, }, }, - } - }, - "/users": { - "get": { - "summary": "Read Users", - "operationId": "read_users_users_get", + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", "parameters": [ { - "required": False, + "required": True, "schema": { - "title": "User Id", - "minLength": 1, - "type": "string", - "default": "me", + "title": "The ID of the item to get", + "maximum": 1000.0, + "minimum": 0.0, + "type": "integer", }, - "name": "user_id", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, + "name": "item_id", + "in": "path", }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, + { + "required": False, + "schema": {"title": "Q", "type": "string"}, + "name": "q", + "in": "query", }, + ], + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + } }, } - }, + } }, "components": { "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", + "Item": { + "title": "Item", + "required": ["name", "price"], "type": "object", "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": {"title": "Description", "type": "string"}, + "tax": {"title": "Tax", "type": "number"}, }, }, "ValidationError": { @@ -106,33 +85,63 @@ openapi_schema = { "type": {"title": "Error Type", "type": "string"}, }, }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, } }, } -item_id_negative = { + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +item_id_not_int = { "detail": [ { - "ctx": {"limit_value": 0}, "loc": ["path", "item_id"], - "msg": "ensure this value is greater than 0", - "type": "value_error.number.not_gt", + "msg": "value is not a valid integer", + "type": "type_error.integer", } ] } @pytest.mark.parametrize( - "path,expected_status,expected_response", + "path,body,expected_status,expected_response", [ - ("/items/1", 200, {"item_id": 1}), - ("/items/-1", 422, item_id_negative), - ("/users", 200, {"user_id": "me"}), - ("/users?user_id=foo", 200, {"user_id": "foo"}), - ("/openapi.json", 200, openapi_schema), + ( + "/items/5?q=bar", + {"name": "Foo", "price": 50.5}, + 200, + { + "item_id": 5, + "item": { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + }, + "q": "bar", + }, + ), + ("/items/5?q=bar", None, 200, {"item_id": 5, "q": "bar"}), + ("/items/5", None, 200, {"item_id": 5}), + ("/items/foo", None, 422, item_id_not_int), ], ) -def test_get(path, expected_status, expected_response): - response = client.get(path) - assert response.status_code == expected_status, response.text +def test_post_body(path, body, expected_status, expected_response): + response = client.put(path, json=body) + assert response.status_code == expected_status assert response.json() == expected_response diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py310.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py310.py new file mode 100644 index 000000000..cd378ec9c --- /dev/null +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py310.py @@ -0,0 +1,155 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": { + "title": "The ID of the item to get", + "maximum": 1000.0, + "minimum": 0.0, + "type": "integer", + }, + "name": "item_id", + "in": "path", + }, + { + "required": False, + "schema": {"title": "Q", "type": "string"}, + "name": "q", + "in": "query", + }, + ], + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + } + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": {"title": "Description", "type": "string"}, + "tax": {"title": "Tax", "type": "number"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.body_multiple_params.tutorial001_an_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +item_id_not_int = { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] +} + + +@needs_py310 +@pytest.mark.parametrize( + "path,body,expected_status,expected_response", + [ + ( + "/items/5?q=bar", + {"name": "Foo", "price": 50.5}, + 200, + { + "item_id": 5, + "item": { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + }, + "q": "bar", + }, + ), + ("/items/5?q=bar", None, 200, {"item_id": 5, "q": "bar"}), + ("/items/5", None, 200, {"item_id": 5}), + ("/items/foo", None, 422, item_id_not_int), + ], +) +def test_post_body(path, body, expected_status, expected_response, client: TestClient): + response = client.put(path, json=body) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_annotated/test_tutorial003_py39.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py39.py similarity index 54% rename from tests/test_tutorial/test_annotated/test_tutorial003_py39.py rename to tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py39.py index 7c828a0ce..b8fe1baaf 100644 --- a/tests/test_tutorial/test_annotated/test_tutorial003_py39.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py39.py @@ -8,21 +8,7 @@ openapi_schema = { "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { - "get": { - "summary": "Read Items", - "operationId": "read_items_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "exclusiveMinimum": 0.0, - "type": "integer", - }, - "name": "item_id", - "in": "path", - } - ], + "put": { "responses": { "200": { "description": "Successful Response", @@ -39,55 +25,48 @@ openapi_schema = { }, }, }, - } - }, - "/users": { - "get": { - "summary": "Read Users", - "operationId": "read_users_users_get", + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", "parameters": [ { - "required": False, + "required": True, "schema": { - "title": "User Id", - "minLength": 1, - "type": "string", - "default": "me", + "title": "The ID of the item to get", + "maximum": 1000.0, + "minimum": 0.0, + "type": "integer", }, - "name": "user_id", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, + "name": "item_id", + "in": "path", }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, + { + "required": False, + "schema": {"title": "Q", "type": "string"}, + "name": "q", + "in": "query", }, + ], + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + } }, } - }, + } }, "components": { "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", + "Item": { + "title": "Item", + "required": ["name", "price"], "type": "object", "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": {"title": "Description", "type": "string"}, + "tax": {"title": "Tax", "type": "number"}, }, }, "ValidationError": { @@ -104,42 +83,73 @@ openapi_schema = { "type": {"title": "Error Type", "type": "string"}, }, }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, } }, } -item_id_negative = { - "detail": [ - { - "ctx": {"limit_value": 0}, - "loc": ["path", "item_id"], - "msg": "ensure this value is greater than 0", - "type": "value_error.number.not_gt", - } - ] -} - @pytest.fixture(name="client") def get_client(): - from docs_src.annotated.tutorial003_py39 import app + from docs_src.body_multiple_params.tutorial001_an_py39 import app client = TestClient(app) return client +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +item_id_not_int = { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] +} + + @needs_py39 @pytest.mark.parametrize( - "path,expected_status,expected_response", + "path,body,expected_status,expected_response", [ - ("/items/1", 200, {"item_id": 1}), - ("/items/-1", 422, item_id_negative), - ("/users", 200, {"user_id": "me"}), - ("/users?user_id=foo", 200, {"user_id": "foo"}), - ("/openapi.json", 200, openapi_schema), + ( + "/items/5?q=bar", + {"name": "Foo", "price": 50.5}, + 200, + { + "item_id": 5, + "item": { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + }, + "q": "bar", + }, + ), + ("/items/5?q=bar", None, 200, {"item_id": 5, "q": "bar"}), + ("/items/5", None, 200, {"item_id": 5}), + ("/items/foo", None, 422, item_id_not_int), ], ) -def test_get(path, expected_status, expected_response, client): - response = client.get(path) - assert response.status_code == expected_status, response.text +def test_post_body(path, body, expected_status, expected_response, client: TestClient): + response = client.put(path, json=body) + assert response.status_code == expected_status assert response.json() == expected_response diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an.py new file mode 100644 index 000000000..788db8b30 --- /dev/null +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an.py @@ -0,0 +1,198 @@ +import pytest +from fastapi.testclient import TestClient + +from docs_src.body_multiple_params.tutorial003_an import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_update_item_items__item_id__put" + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": {"title": "Description", "type": "string"}, + "tax": {"title": "Tax", "type": "number"}, + }, + }, + "User": { + "title": "User", + "required": ["username"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "full_name": {"title": "Full Name", "type": "string"}, + }, + }, + "Body_update_item_items__item_id__put": { + "title": "Body_update_item_items__item_id__put", + "required": ["item", "user", "importance"], + "type": "object", + "properties": { + "item": {"$ref": "#/components/schemas/Item"}, + "user": {"$ref": "#/components/schemas/User"}, + "importance": {"title": "Importance", "type": "integer"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +# Test required and embedded body parameters with no bodies sent +@pytest.mark.parametrize( + "path,body,expected_status,expected_response", + [ + ( + "/items/5", + { + "importance": 2, + "item": {"name": "Foo", "price": 50.5}, + "user": {"username": "Dave"}, + }, + 200, + { + "item_id": 5, + "importance": 2, + "item": { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + }, + "user": {"username": "Dave", "full_name": None}, + }, + ), + ( + "/items/5", + None, + 422, + { + "detail": [ + { + "loc": ["body", "item"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "user"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "importance"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + }, + ), + ( + "/items/5", + [], + 422, + { + "detail": [ + { + "loc": ["body", "item"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "user"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "importance"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + }, + ), + ], +) +def test_post_body(path, body, expected_status, expected_response): + response = client.put(path, json=body) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py310.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py310.py new file mode 100644 index 000000000..9003016cd --- /dev/null +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py310.py @@ -0,0 +1,206 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_update_item_items__item_id__put" + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": {"title": "Description", "type": "string"}, + "tax": {"title": "Tax", "type": "number"}, + }, + }, + "User": { + "title": "User", + "required": ["username"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "full_name": {"title": "Full Name", "type": "string"}, + }, + }, + "Body_update_item_items__item_id__put": { + "title": "Body_update_item_items__item_id__put", + "required": ["item", "user", "importance"], + "type": "object", + "properties": { + "item": {"$ref": "#/components/schemas/Item"}, + "user": {"$ref": "#/components/schemas/User"}, + "importance": {"title": "Importance", "type": "integer"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.body_multiple_params.tutorial003_an_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +# Test required and embedded body parameters with no bodies sent +@needs_py310 +@pytest.mark.parametrize( + "path,body,expected_status,expected_response", + [ + ( + "/items/5", + { + "importance": 2, + "item": {"name": "Foo", "price": 50.5}, + "user": {"username": "Dave"}, + }, + 200, + { + "item_id": 5, + "importance": 2, + "item": { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + }, + "user": {"username": "Dave", "full_name": None}, + }, + ), + ( + "/items/5", + None, + 422, + { + "detail": [ + { + "loc": ["body", "item"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "user"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "importance"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + }, + ), + ( + "/items/5", + [], + 422, + { + "detail": [ + { + "loc": ["body", "item"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "user"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "importance"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + }, + ), + ], +) +def test_post_body(path, body, expected_status, expected_response, client: TestClient): + response = client.put(path, json=body) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py39.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py39.py new file mode 100644 index 000000000..bc014a441 --- /dev/null +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py39.py @@ -0,0 +1,206 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_update_item_items__item_id__put" + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": {"title": "Description", "type": "string"}, + "tax": {"title": "Tax", "type": "number"}, + }, + }, + "User": { + "title": "User", + "required": ["username"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "full_name": {"title": "Full Name", "type": "string"}, + }, + }, + "Body_update_item_items__item_id__put": { + "title": "Body_update_item_items__item_id__put", + "required": ["item", "user", "importance"], + "type": "object", + "properties": { + "item": {"$ref": "#/components/schemas/Item"}, + "user": {"$ref": "#/components/schemas/User"}, + "importance": {"title": "Importance", "type": "integer"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.body_multiple_params.tutorial003_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +# Test required and embedded body parameters with no bodies sent +@needs_py39 +@pytest.mark.parametrize( + "path,body,expected_status,expected_response", + [ + ( + "/items/5", + { + "importance": 2, + "item": {"name": "Foo", "price": 50.5}, + "user": {"username": "Dave"}, + }, + 200, + { + "item_id": 5, + "importance": 2, + "item": { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + }, + "user": {"username": "Dave", "full_name": None}, + }, + ), + ( + "/items/5", + None, + 422, + { + "detail": [ + { + "loc": ["body", "item"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "user"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "importance"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + }, + ), + ( + "/items/5", + [], + 422, + { + "detail": [ + { + "loc": ["body", "item"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "user"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "importance"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + }, + ), + ], +) +def test_post_body(path, body, expected_status, expected_response, client: TestClient): + response = client.put(path, json=body) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_annotated/test_tutorial002.py b/tests/test_tutorial/test_cookie_params/test_tutorial001_an.py similarity index 65% rename from tests/test_tutorial/test_annotated/test_tutorial002.py rename to tests/test_tutorial/test_cookie_params/test_tutorial001_an.py index 60c1233d8..fb60ea993 100644 --- a/tests/test_tutorial/test_annotated/test_tutorial002.py +++ b/tests/test_tutorial/test_cookie_params/test_tutorial001_an.py @@ -1,9 +1,7 @@ import pytest from fastapi.testclient import TestClient -from docs_src.annotated.tutorial002 import app - -client = TestClient(app) +from docs_src.cookie_params.tutorial001_an import app openapi_schema = { "openapi": "3.0.2", @@ -32,25 +30,13 @@ openapi_schema = { "parameters": [ { "required": False, - "schema": {"title": "Q", "type": "string"}, - "name": "q", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Skip", "type": "integer", "default": 0}, - "name": "skip", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Limit", "type": "integer", "default": 100}, - "name": "limit", - "in": "query", - }, + "schema": {"title": "Ads Id", "type": "string"}, + "name": "ads_id", + "in": "cookie", + } ], } - }, + } }, "components": { "schemas": { @@ -85,16 +71,22 @@ openapi_schema = { @pytest.mark.parametrize( - "path,expected_status,expected_response", + "path,cookies,expected_status,expected_response", [ - ("/items", 200, {"q": None, "skip": 0, "limit": 100}), - ("/items?q=foo", 200, {"q": "foo", "skip": 0, "limit": 100}), - ("/items?q=foo&skip=5", 200, {"q": "foo", "skip": 5, "limit": 100}), - ("/items?q=foo&skip=5&limit=30", 200, {"q": "foo", "skip": 5, "limit": 30}), - ("/openapi.json", 200, openapi_schema), + ("/openapi.json", None, 200, openapi_schema), + ("/items", None, 200, {"ads_id": None}), + ("/items", {"ads_id": "ads_track"}, 200, {"ads_id": "ads_track"}), + ( + "/items", + {"ads_id": "ads_track", "session": "cookiesession"}, + 200, + {"ads_id": "ads_track"}, + ), + ("/items", {"session": "cookiesession"}, 200, {"ads_id": None}), ], ) -def test_get(path, expected_status, expected_response): +def test(path, cookies, expected_status, expected_response): + client = TestClient(app, cookies=cookies) response = client.get(path) assert response.status_code == expected_status assert response.json() == expected_response diff --git a/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py310.py b/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py310.py new file mode 100644 index 000000000..308886085 --- /dev/null +++ b/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py310.py @@ -0,0 +1,95 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Ads Id", "type": "string"}, + "name": "ads_id", + "in": "cookie", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@needs_py310 +@pytest.mark.parametrize( + "path,cookies,expected_status,expected_response", + [ + ("/openapi.json", None, 200, openapi_schema), + ("/items", None, 200, {"ads_id": None}), + ("/items", {"ads_id": "ads_track"}, 200, {"ads_id": "ads_track"}), + ( + "/items", + {"ads_id": "ads_track", "session": "cookiesession"}, + 200, + {"ads_id": "ads_track"}, + ), + ("/items", {"session": "cookiesession"}, 200, {"ads_id": None}), + ], +) +def test(path, cookies, expected_status, expected_response): + from docs_src.cookie_params.tutorial001_an_py310 import app + + client = TestClient(app, cookies=cookies) + response = client.get(path) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py39.py b/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py39.py new file mode 100644 index 000000000..bbfe5ff9a --- /dev/null +++ b/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py39.py @@ -0,0 +1,95 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Ads Id", "type": "string"}, + "name": "ads_id", + "in": "cookie", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@needs_py39 +@pytest.mark.parametrize( + "path,cookies,expected_status,expected_response", + [ + ("/openapi.json", None, 200, openapi_schema), + ("/items", None, 200, {"ads_id": None}), + ("/items", {"ads_id": "ads_track"}, 200, {"ads_id": "ads_track"}), + ( + "/items", + {"ads_id": "ads_track", "session": "cookiesession"}, + 200, + {"ads_id": "ads_track"}, + ), + ("/items", {"session": "cookiesession"}, 200, {"ads_id": None}), + ], +) +def test(path, cookies, expected_status, expected_response): + from docs_src.cookie_params.tutorial001_an_py39 import app + + client = TestClient(app, cookies=cookies) + response = client.get(path) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_dependencies/test_tutorial001_an.py b/tests/test_tutorial/test_dependencies/test_tutorial001_an.py new file mode 100644 index 000000000..13960addc --- /dev/null +++ b/tests/test_tutorial/test_dependencies/test_tutorial001_an.py @@ -0,0 +1,149 @@ +import pytest +from fastapi.testclient import TestClient + +from docs_src.dependencies.tutorial001_an import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Q", "type": "string"}, + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Skip", "type": "integer", "default": 0}, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Limit", "type": "integer", "default": 100}, + "name": "limit", + "in": "query", + }, + ], + } + }, + "/users/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Users", + "operationId": "read_users_users__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Q", "type": "string"}, + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Skip", "type": "integer", "default": 0}, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Limit", "type": "integer", "default": 100}, + "name": "limit", + "in": "query", + }, + ], + } + }, + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@pytest.mark.parametrize( + "path,expected_status,expected_response", + [ + ("/items", 200, {"q": None, "skip": 0, "limit": 100}), + ("/items?q=foo", 200, {"q": "foo", "skip": 0, "limit": 100}), + ("/items?q=foo&skip=5", 200, {"q": "foo", "skip": 5, "limit": 100}), + ("/items?q=foo&skip=5&limit=30", 200, {"q": "foo", "skip": 5, "limit": 30}), + ("/users", 200, {"q": None, "skip": 0, "limit": 100}), + ("/openapi.json", 200, openapi_schema), + ], +) +def test_get(path, expected_status, expected_response): + response = client.get(path) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_dependencies/test_tutorial001_an_py310.py b/tests/test_tutorial/test_dependencies/test_tutorial001_an_py310.py new file mode 100644 index 000000000..4b093af0d --- /dev/null +++ b/tests/test_tutorial/test_dependencies/test_tutorial001_an_py310.py @@ -0,0 +1,157 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Q", "type": "string"}, + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Skip", "type": "integer", "default": 0}, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Limit", "type": "integer", "default": 100}, + "name": "limit", + "in": "query", + }, + ], + } + }, + "/users/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Users", + "operationId": "read_users_users__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Q", "type": "string"}, + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Skip", "type": "integer", "default": 0}, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Limit", "type": "integer", "default": 100}, + "name": "limit", + "in": "query", + }, + ], + } + }, + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.dependencies.tutorial001_an_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py310 +@pytest.mark.parametrize( + "path,expected_status,expected_response", + [ + ("/items", 200, {"q": None, "skip": 0, "limit": 100}), + ("/items?q=foo", 200, {"q": "foo", "skip": 0, "limit": 100}), + ("/items?q=foo&skip=5", 200, {"q": "foo", "skip": 5, "limit": 100}), + ("/items?q=foo&skip=5&limit=30", 200, {"q": "foo", "skip": 5, "limit": 30}), + ("/users", 200, {"q": None, "skip": 0, "limit": 100}), + ("/openapi.json", 200, openapi_schema), + ], +) +def test_get(path, expected_status, expected_response, client: TestClient): + response = client.get(path) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_dependencies/test_tutorial001_an_py39.py b/tests/test_tutorial/test_dependencies/test_tutorial001_an_py39.py new file mode 100644 index 000000000..6059924cc --- /dev/null +++ b/tests/test_tutorial/test_dependencies/test_tutorial001_an_py39.py @@ -0,0 +1,157 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Q", "type": "string"}, + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Skip", "type": "integer", "default": 0}, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Limit", "type": "integer", "default": 100}, + "name": "limit", + "in": "query", + }, + ], + } + }, + "/users/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Users", + "operationId": "read_users_users__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Q", "type": "string"}, + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Skip", "type": "integer", "default": 0}, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Limit", "type": "integer", "default": 100}, + "name": "limit", + "in": "query", + }, + ], + } + }, + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.dependencies.tutorial001_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py39 +@pytest.mark.parametrize( + "path,expected_status,expected_response", + [ + ("/items", 200, {"q": None, "skip": 0, "limit": 100}), + ("/items?q=foo", 200, {"q": "foo", "skip": 0, "limit": 100}), + ("/items?q=foo&skip=5", 200, {"q": "foo", "skip": 5, "limit": 100}), + ("/items?q=foo&skip=5&limit=30", 200, {"q": "foo", "skip": 5, "limit": 30}), + ("/users", 200, {"q": None, "skip": 0, "limit": 100}), + ("/openapi.json", 200, openapi_schema), + ], +) +def test_get(path, expected_status, expected_response, client: TestClient): + response = client.get(path) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_annotated/test_tutorial001.py b/tests/test_tutorial/test_dependencies/test_tutorial004_an.py similarity index 69% rename from tests/test_tutorial/test_annotated/test_tutorial001.py rename to tests/test_tutorial/test_dependencies/test_tutorial004_an.py index 50c9caca2..ef6199b04 100644 --- a/tests/test_tutorial/test_annotated/test_tutorial001.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial004_an.py @@ -1,7 +1,7 @@ import pytest from fastapi.testclient import TestClient -from docs_src.annotated.tutorial001 import app +from docs_src.dependencies.tutorial004_an import app client = TestClient(app) @@ -50,7 +50,7 @@ openapi_schema = { }, ], } - }, + } }, "components": { "schemas": { @@ -84,14 +84,58 @@ openapi_schema = { } +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + @pytest.mark.parametrize( "path,expected_status,expected_response", [ - ("/items", 200, {"q": None, "skip": 0, "limit": 100}), - ("/items?q=foo", 200, {"q": "foo", "skip": 0, "limit": 100}), - ("/items?q=foo&skip=5", 200, {"q": "foo", "skip": 5, "limit": 100}), - ("/items?q=foo&skip=5&limit=30", 200, {"q": "foo", "skip": 5, "limit": 30}), - ("/openapi.json", 200, openapi_schema), + ( + "/items", + 200, + { + "items": [ + {"item_name": "Foo"}, + {"item_name": "Bar"}, + {"item_name": "Baz"}, + ] + }, + ), + ( + "/items?q=foo", + 200, + { + "items": [ + {"item_name": "Foo"}, + {"item_name": "Bar"}, + {"item_name": "Baz"}, + ], + "q": "foo", + }, + ), + ( + "/items?q=foo&skip=1", + 200, + {"items": [{"item_name": "Bar"}, {"item_name": "Baz"}], "q": "foo"}, + ), + ( + "/items?q=bar&limit=2", + 200, + {"items": [{"item_name": "Foo"}, {"item_name": "Bar"}], "q": "bar"}, + ), + ( + "/items?q=bar&skip=1&limit=1", + 200, + {"items": [{"item_name": "Bar"}], "q": "bar"}, + ), + ( + "/items?limit=1&q=bar&skip=1", + 200, + {"items": [{"item_name": "Bar"}], "q": "bar"}, + ), ], ) def test_get(path, expected_status, expected_response): diff --git a/tests/test_tutorial/test_annotated/test_tutorial002_py39.py b/tests/test_tutorial/test_dependencies/test_tutorial004_an_py310.py similarity index 67% rename from tests/test_tutorial/test_annotated/test_tutorial002_py39.py rename to tests/test_tutorial/test_dependencies/test_tutorial004_an_py310.py index 77a1f36a0..e9736780c 100644 --- a/tests/test_tutorial/test_annotated/test_tutorial002_py39.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial004_an_py310.py @@ -1,7 +1,7 @@ import pytest from fastapi.testclient import TestClient -from ...utils import needs_py39 +from ...utils import needs_py310 openapi_schema = { "openapi": "3.0.2", @@ -48,7 +48,7 @@ openapi_schema = { }, ], } - }, + } }, "components": { "schemas": { @@ -84,24 +84,69 @@ openapi_schema = { @pytest.fixture(name="client") def get_client(): - from docs_src.annotated.tutorial002_py39 import app + from docs_src.dependencies.tutorial004_an_py310 import app client = TestClient(app) return client -@needs_py39 +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py310 @pytest.mark.parametrize( "path,expected_status,expected_response", [ - ("/items", 200, {"q": None, "skip": 0, "limit": 100}), - ("/items?q=foo", 200, {"q": "foo", "skip": 0, "limit": 100}), - ("/items?q=foo&skip=5", 200, {"q": "foo", "skip": 5, "limit": 100}), - ("/items?q=foo&skip=5&limit=30", 200, {"q": "foo", "skip": 5, "limit": 30}), - ("/openapi.json", 200, openapi_schema), + ( + "/items", + 200, + { + "items": [ + {"item_name": "Foo"}, + {"item_name": "Bar"}, + {"item_name": "Baz"}, + ] + }, + ), + ( + "/items?q=foo", + 200, + { + "items": [ + {"item_name": "Foo"}, + {"item_name": "Bar"}, + {"item_name": "Baz"}, + ], + "q": "foo", + }, + ), + ( + "/items?q=foo&skip=1", + 200, + {"items": [{"item_name": "Bar"}, {"item_name": "Baz"}], "q": "foo"}, + ), + ( + "/items?q=bar&limit=2", + 200, + {"items": [{"item_name": "Foo"}, {"item_name": "Bar"}], "q": "bar"}, + ), + ( + "/items?q=bar&skip=1&limit=1", + 200, + {"items": [{"item_name": "Bar"}], "q": "bar"}, + ), + ( + "/items?limit=1&q=bar&skip=1", + 200, + {"items": [{"item_name": "Bar"}], "q": "bar"}, + ), ], ) -def test_get(path, expected_status, expected_response, client): +def test_get(path, expected_status, expected_response, client: TestClient): response = client.get(path) assert response.status_code == expected_status assert response.json() == expected_response diff --git a/tests/test_tutorial/test_annotated/test_tutorial001_py39.py b/tests/test_tutorial/test_dependencies/test_tutorial004_an_py39.py similarity index 68% rename from tests/test_tutorial/test_annotated/test_tutorial001_py39.py rename to tests/test_tutorial/test_dependencies/test_tutorial004_an_py39.py index 576f55702..2b346f3b2 100644 --- a/tests/test_tutorial/test_annotated/test_tutorial001_py39.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial004_an_py39.py @@ -48,7 +48,7 @@ openapi_schema = { }, ], } - }, + } }, "components": { "schemas": { @@ -84,24 +84,69 @@ openapi_schema = { @pytest.fixture(name="client") def get_client(): - from docs_src.annotated.tutorial001_py39 import app + from docs_src.dependencies.tutorial004_an_py39 import app client = TestClient(app) return client +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + @needs_py39 @pytest.mark.parametrize( "path,expected_status,expected_response", [ - ("/items", 200, {"q": None, "skip": 0, "limit": 100}), - ("/items?q=foo", 200, {"q": "foo", "skip": 0, "limit": 100}), - ("/items?q=foo&skip=5", 200, {"q": "foo", "skip": 5, "limit": 100}), - ("/items?q=foo&skip=5&limit=30", 200, {"q": "foo", "skip": 5, "limit": 30}), - ("/openapi.json", 200, openapi_schema), + ( + "/items", + 200, + { + "items": [ + {"item_name": "Foo"}, + {"item_name": "Bar"}, + {"item_name": "Baz"}, + ] + }, + ), + ( + "/items?q=foo", + 200, + { + "items": [ + {"item_name": "Foo"}, + {"item_name": "Bar"}, + {"item_name": "Baz"}, + ], + "q": "foo", + }, + ), + ( + "/items?q=foo&skip=1", + 200, + {"items": [{"item_name": "Bar"}, {"item_name": "Baz"}], "q": "foo"}, + ), + ( + "/items?q=bar&limit=2", + 200, + {"items": [{"item_name": "Foo"}, {"item_name": "Bar"}], "q": "bar"}, + ), + ( + "/items?q=bar&skip=1&limit=1", + 200, + {"items": [{"item_name": "Bar"}], "q": "bar"}, + ), + ( + "/items?limit=1&q=bar&skip=1", + 200, + {"items": [{"item_name": "Bar"}], "q": "bar"}, + ), ], ) -def test_get(path, expected_status, expected_response, client): +def test_get(path, expected_status, expected_response, client: TestClient): response = client.get(path) assert response.status_code == expected_status assert response.json() == expected_response diff --git a/tests/test_tutorial/test_dependencies/test_tutorial006_an.py b/tests/test_tutorial/test_dependencies/test_tutorial006_an.py new file mode 100644 index 000000000..f33b67d58 --- /dev/null +++ b/tests/test_tutorial/test_dependencies/test_tutorial006_an.py @@ -0,0 +1,128 @@ +from fastapi.testclient import TestClient + +from docs_src.dependencies.tutorial006_an import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + { + "required": True, + "schema": {"title": "X-Key", "type": "string"}, + "name": "x-key", + "in": "header", + }, + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def test_get_no_headers(): + response = client.get("/items/") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["header", "x-key"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + + +def test_get_invalid_one_header(): + response = client.get("/items/", headers={"X-Token": "invalid"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "X-Token header invalid"} + + +def test_get_invalid_second_header(): + response = client.get( + "/items/", headers={"X-Token": "fake-super-secret-token", "X-Key": "invalid"} + ) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "X-Key header invalid"} + + +def test_get_valid_headers(): + response = client.get( + "/items/", + headers={ + "X-Token": "fake-super-secret-token", + "X-Key": "fake-super-secret-key", + }, + ) + assert response.status_code == 200, response.text + assert response.json() == [{"item": "Foo"}, {"item": "Bar"}] diff --git a/tests/test_tutorial/test_dependencies/test_tutorial006_an_py39.py b/tests/test_tutorial/test_dependencies/test_tutorial006_an_py39.py new file mode 100644 index 000000000..171e39a96 --- /dev/null +++ b/tests/test_tutorial/test_dependencies/test_tutorial006_an_py39.py @@ -0,0 +1,140 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + { + "required": True, + "schema": {"title": "X-Key", "type": "string"}, + "name": "x-key", + "in": "header", + }, + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.dependencies.tutorial006_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py39 +def test_get_no_headers(client: TestClient): + response = client.get("/items/") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["header", "x-key"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + + +@needs_py39 +def test_get_invalid_one_header(client: TestClient): + response = client.get("/items/", headers={"X-Token": "invalid"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "X-Token header invalid"} + + +@needs_py39 +def test_get_invalid_second_header(client: TestClient): + response = client.get( + "/items/", headers={"X-Token": "fake-super-secret-token", "X-Key": "invalid"} + ) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "X-Key header invalid"} + + +@needs_py39 +def test_get_valid_headers(client: TestClient): + response = client.get( + "/items/", + headers={ + "X-Token": "fake-super-secret-token", + "X-Key": "fake-super-secret-key", + }, + ) + assert response.status_code == 200, response.text + assert response.json() == [{"item": "Foo"}, {"item": "Bar"}] diff --git a/tests/test_tutorial/test_dependencies/test_tutorial012_an.py b/tests/test_tutorial/test_dependencies/test_tutorial012_an.py new file mode 100644 index 000000000..0a6908f72 --- /dev/null +++ b/tests/test_tutorial/test_dependencies/test_tutorial012_an.py @@ -0,0 +1,209 @@ +from fastapi.testclient import TestClient + +from docs_src.dependencies.tutorial012_an import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + { + "required": True, + "schema": {"title": "X-Key", "type": "string"}, + "name": "x-key", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/users/": { + "get": { + "summary": "Read Users", + "operationId": "read_users_users__get", + "parameters": [ + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + { + "required": True, + "schema": {"title": "X-Key", "type": "string"}, + "name": "x-key", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def test_get_no_headers_items(): + response = client.get("/items/") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["header", "x-key"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + + +def test_get_no_headers_users(): + response = client.get("/users/") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["header", "x-key"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + + +def test_get_invalid_one_header_items(): + response = client.get("/items/", headers={"X-Token": "invalid"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "X-Token header invalid"} + + +def test_get_invalid_one_users(): + response = client.get("/users/", headers={"X-Token": "invalid"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "X-Token header invalid"} + + +def test_get_invalid_second_header_items(): + response = client.get( + "/items/", headers={"X-Token": "fake-super-secret-token", "X-Key": "invalid"} + ) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "X-Key header invalid"} + + +def test_get_invalid_second_header_users(): + response = client.get( + "/users/", headers={"X-Token": "fake-super-secret-token", "X-Key": "invalid"} + ) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "X-Key header invalid"} + + +def test_get_valid_headers_items(): + response = client.get( + "/items/", + headers={ + "X-Token": "fake-super-secret-token", + "X-Key": "fake-super-secret-key", + }, + ) + assert response.status_code == 200, response.text + assert response.json() == [{"item": "Portal Gun"}, {"item": "Plumbus"}] + + +def test_get_valid_headers_users(): + response = client.get( + "/users/", + headers={ + "X-Token": "fake-super-secret-token", + "X-Key": "fake-super-secret-key", + }, + ) + assert response.status_code == 200, response.text + assert response.json() == [{"username": "Rick"}, {"username": "Morty"}] diff --git a/tests/test_tutorial/test_dependencies/test_tutorial012_an_py39.py b/tests/test_tutorial/test_dependencies/test_tutorial012_an_py39.py new file mode 100644 index 000000000..25f54f4c9 --- /dev/null +++ b/tests/test_tutorial/test_dependencies/test_tutorial012_an_py39.py @@ -0,0 +1,225 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + { + "required": True, + "schema": {"title": "X-Key", "type": "string"}, + "name": "x-key", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/users/": { + "get": { + "summary": "Read Users", + "operationId": "read_users_users__get", + "parameters": [ + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + { + "required": True, + "schema": {"title": "X-Key", "type": "string"}, + "name": "x-key", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.dependencies.tutorial012_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py39 +def test_get_no_headers_items(client: TestClient): + response = client.get("/items/") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["header", "x-key"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + + +@needs_py39 +def test_get_no_headers_users(client: TestClient): + response = client.get("/users/") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["header", "x-key"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + + +@needs_py39 +def test_get_invalid_one_header_items(client: TestClient): + response = client.get("/items/", headers={"X-Token": "invalid"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "X-Token header invalid"} + + +@needs_py39 +def test_get_invalid_one_users(client: TestClient): + response = client.get("/users/", headers={"X-Token": "invalid"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "X-Token header invalid"} + + +@needs_py39 +def test_get_invalid_second_header_items(client: TestClient): + response = client.get( + "/items/", headers={"X-Token": "fake-super-secret-token", "X-Key": "invalid"} + ) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "X-Key header invalid"} + + +@needs_py39 +def test_get_invalid_second_header_users(client: TestClient): + response = client.get( + "/users/", headers={"X-Token": "fake-super-secret-token", "X-Key": "invalid"} + ) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "X-Key header invalid"} + + +@needs_py39 +def test_get_valid_headers_items(client: TestClient): + response = client.get( + "/items/", + headers={ + "X-Token": "fake-super-secret-token", + "X-Key": "fake-super-secret-key", + }, + ) + assert response.status_code == 200, response.text + assert response.json() == [{"item": "Portal Gun"}, {"item": "Plumbus"}] + + +@needs_py39 +def test_get_valid_headers_users(client: TestClient): + response = client.get( + "/users/", + headers={ + "X-Token": "fake-super-secret-token", + "X-Key": "fake-super-secret-key", + }, + ) + assert response.status_code == 200, response.text + assert response.json() == [{"username": "Rick"}, {"username": "Morty"}] diff --git a/tests/test_tutorial/test_extra_data_types/test_tutorial001_an.py b/tests/test_tutorial/test_extra_data_types/test_tutorial001_an.py new file mode 100644 index 000000000..d5be16dfb --- /dev/null +++ b/tests/test_tutorial/test_extra_data_types/test_tutorial001_an.py @@ -0,0 +1,138 @@ +from fastapi.testclient import TestClient + +from docs_src.extra_data_types.tutorial001_an import app + +client = TestClient(app) + + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "type": "string", + "format": "uuid", + }, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_read_items_items__item_id__put" + } + } + } + }, + } + } + }, + "components": { + "schemas": { + "Body_read_items_items__item_id__put": { + "title": "Body_read_items_items__item_id__put", + "type": "object", + "properties": { + "start_datetime": { + "title": "Start Datetime", + "type": "string", + "format": "date-time", + }, + "end_datetime": { + "title": "End Datetime", + "type": "string", + "format": "date-time", + }, + "repeat_at": { + "title": "Repeat At", + "type": "string", + "format": "time", + }, + "process_after": { + "title": "Process After", + "type": "number", + "format": "time-delta", + }, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def test_extra_types(): + item_id = "ff97dd87-a4a5-4a12-b412-cde99f33e00e" + data = { + "start_datetime": "2018-12-22T14:00:00+00:00", + "end_datetime": "2018-12-24T15:00:00+00:00", + "repeat_at": "15:30:00", + "process_after": 300, + } + expected_response = data.copy() + expected_response.update( + { + "start_process": "2018-12-22T14:05:00+00:00", + "duration": 176_100, + "item_id": item_id, + } + ) + response = client.put(f"/items/{item_id}", json=data) + assert response.status_code == 200, response.text + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py310.py b/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py310.py new file mode 100644 index 000000000..80806b694 --- /dev/null +++ b/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py310.py @@ -0,0 +1,146 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "type": "string", + "format": "uuid", + }, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_read_items_items__item_id__put" + } + } + } + }, + } + } + }, + "components": { + "schemas": { + "Body_read_items_items__item_id__put": { + "title": "Body_read_items_items__item_id__put", + "type": "object", + "properties": { + "start_datetime": { + "title": "Start Datetime", + "type": "string", + "format": "date-time", + }, + "end_datetime": { + "title": "End Datetime", + "type": "string", + "format": "date-time", + }, + "repeat_at": { + "title": "Repeat At", + "type": "string", + "format": "time", + }, + "process_after": { + "title": "Process After", + "type": "number", + "format": "time-delta", + }, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.extra_data_types.tutorial001_an_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py310 +def test_extra_types(client: TestClient): + item_id = "ff97dd87-a4a5-4a12-b412-cde99f33e00e" + data = { + "start_datetime": "2018-12-22T14:00:00+00:00", + "end_datetime": "2018-12-24T15:00:00+00:00", + "repeat_at": "15:30:00", + "process_after": 300, + } + expected_response = data.copy() + expected_response.update( + { + "start_process": "2018-12-22T14:05:00+00:00", + "duration": 176_100, + "item_id": item_id, + } + ) + response = client.put(f"/items/{item_id}", json=data) + assert response.status_code == 200, response.text + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py39.py b/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py39.py new file mode 100644 index 000000000..5c7d43394 --- /dev/null +++ b/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py39.py @@ -0,0 +1,146 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "type": "string", + "format": "uuid", + }, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_read_items_items__item_id__put" + } + } + } + }, + } + } + }, + "components": { + "schemas": { + "Body_read_items_items__item_id__put": { + "title": "Body_read_items_items__item_id__put", + "type": "object", + "properties": { + "start_datetime": { + "title": "Start Datetime", + "type": "string", + "format": "date-time", + }, + "end_datetime": { + "title": "End Datetime", + "type": "string", + "format": "date-time", + }, + "repeat_at": { + "title": "Repeat At", + "type": "string", + "format": "time", + }, + "process_after": { + "title": "Process After", + "type": "number", + "format": "time-delta", + }, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.extra_data_types.tutorial001_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py39 +def test_extra_types(client: TestClient): + item_id = "ff97dd87-a4a5-4a12-b412-cde99f33e00e" + data = { + "start_datetime": "2018-12-22T14:00:00+00:00", + "end_datetime": "2018-12-24T15:00:00+00:00", + "repeat_at": "15:30:00", + "process_after": 300, + } + expected_response = data.copy() + expected_response.update( + { + "start_process": "2018-12-22T14:05:00+00:00", + "duration": 176_100, + "item_id": item_id, + } + ) + response = client.put(f"/items/{item_id}", json=data) + assert response.status_code == 200, response.text + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_header_params/test_tutorial001_an.py b/tests/test_tutorial/test_header_params/test_tutorial001_an.py new file mode 100644 index 000000000..3c155f786 --- /dev/null +++ b/tests/test_tutorial/test_header_params/test_tutorial001_an.py @@ -0,0 +1,88 @@ +import pytest +from fastapi.testclient import TestClient + +from docs_src.header_params.tutorial001_an import app + +client = TestClient(app) + + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "User-Agent", "type": "string"}, + "name": "user-agent", + "in": "header", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.mark.parametrize( + "path,headers,expected_status,expected_response", + [ + ("/openapi.json", None, 200, openapi_schema), + ("/items", None, 200, {"User-Agent": "testclient"}), + ("/items", {"X-Header": "notvalid"}, 200, {"User-Agent": "testclient"}), + ("/items", {"User-Agent": "FastAPI test"}, 200, {"User-Agent": "FastAPI test"}), + ], +) +def test(path, headers, expected_status, expected_response): + response = client.get(path, headers=headers) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_header_params/test_tutorial001_an_py310.py b/tests/test_tutorial/test_header_params/test_tutorial001_an_py310.py new file mode 100644 index 000000000..1f86f2a5d --- /dev/null +++ b/tests/test_tutorial/test_header_params/test_tutorial001_an_py310.py @@ -0,0 +1,94 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "User-Agent", "type": "string"}, + "name": "user-agent", + "in": "header", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.header_params.tutorial001_an_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +@pytest.mark.parametrize( + "path,headers,expected_status,expected_response", + [ + ("/openapi.json", None, 200, openapi_schema), + ("/items", None, 200, {"User-Agent": "testclient"}), + ("/items", {"X-Header": "notvalid"}, 200, {"User-Agent": "testclient"}), + ("/items", {"User-Agent": "FastAPI test"}, 200, {"User-Agent": "FastAPI test"}), + ], +) +def test(path, headers, expected_status, expected_response, client: TestClient): + response = client.get(path, headers=headers) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_header_params/test_tutorial002.py b/tests/test_tutorial/test_header_params/test_tutorial002.py new file mode 100644 index 000000000..b23398287 --- /dev/null +++ b/tests/test_tutorial/test_header_params/test_tutorial002.py @@ -0,0 +1,99 @@ +import pytest +from fastapi.testclient import TestClient + +from docs_src.header_params.tutorial002 import app + +client = TestClient(app) + + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Strange Header", "type": "string"}, + "name": "strange_header", + "in": "header", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.mark.parametrize( + "path,headers,expected_status,expected_response", + [ + ("/openapi.json", None, 200, openapi_schema), + ("/items", None, 200, {"strange_header": None}), + ("/items", {"X-Header": "notvalid"}, 200, {"strange_header": None}), + ( + "/items", + {"strange_header": "FastAPI test"}, + 200, + {"strange_header": "FastAPI test"}, + ), + ( + "/items", + {"strange-header": "Not really underscore"}, + 200, + {"strange_header": None}, + ), + ], +) +def test(path, headers, expected_status, expected_response): + response = client.get(path, headers=headers) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_header_params/test_tutorial002_an.py b/tests/test_tutorial/test_header_params/test_tutorial002_an.py new file mode 100644 index 000000000..77d236e09 --- /dev/null +++ b/tests/test_tutorial/test_header_params/test_tutorial002_an.py @@ -0,0 +1,99 @@ +import pytest +from fastapi.testclient import TestClient + +from docs_src.header_params.tutorial002_an import app + +client = TestClient(app) + + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Strange Header", "type": "string"}, + "name": "strange_header", + "in": "header", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.mark.parametrize( + "path,headers,expected_status,expected_response", + [ + ("/openapi.json", None, 200, openapi_schema), + ("/items", None, 200, {"strange_header": None}), + ("/items", {"X-Header": "notvalid"}, 200, {"strange_header": None}), + ( + "/items", + {"strange_header": "FastAPI test"}, + 200, + {"strange_header": "FastAPI test"}, + ), + ( + "/items", + {"strange-header": "Not really underscore"}, + 200, + {"strange_header": None}, + ), + ], +) +def test(path, headers, expected_status, expected_response): + response = client.get(path, headers=headers) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_header_params/test_tutorial002_an_py310.py b/tests/test_tutorial/test_header_params/test_tutorial002_an_py310.py new file mode 100644 index 000000000..49b0ef462 --- /dev/null +++ b/tests/test_tutorial/test_header_params/test_tutorial002_an_py310.py @@ -0,0 +1,105 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Strange Header", "type": "string"}, + "name": "strange_header", + "in": "header", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.header_params.tutorial002_an_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +@pytest.mark.parametrize( + "path,headers,expected_status,expected_response", + [ + ("/openapi.json", None, 200, openapi_schema), + ("/items", None, 200, {"strange_header": None}), + ("/items", {"X-Header": "notvalid"}, 200, {"strange_header": None}), + ( + "/items", + {"strange_header": "FastAPI test"}, + 200, + {"strange_header": "FastAPI test"}, + ), + ( + "/items", + {"strange-header": "Not really underscore"}, + 200, + {"strange_header": None}, + ), + ], +) +def test(path, headers, expected_status, expected_response, client: TestClient): + response = client.get(path, headers=headers) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_header_params/test_tutorial002_an_py39.py b/tests/test_tutorial/test_header_params/test_tutorial002_an_py39.py new file mode 100644 index 000000000..13aaabeb8 --- /dev/null +++ b/tests/test_tutorial/test_header_params/test_tutorial002_an_py39.py @@ -0,0 +1,105 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Strange Header", "type": "string"}, + "name": "strange_header", + "in": "header", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.header_params.tutorial002_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +@pytest.mark.parametrize( + "path,headers,expected_status,expected_response", + [ + ("/openapi.json", None, 200, openapi_schema), + ("/items", None, 200, {"strange_header": None}), + ("/items", {"X-Header": "notvalid"}, 200, {"strange_header": None}), + ( + "/items", + {"strange_header": "FastAPI test"}, + 200, + {"strange_header": "FastAPI test"}, + ), + ( + "/items", + {"strange-header": "Not really underscore"}, + 200, + {"strange_header": None}, + ), + ], +) +def test(path, headers, expected_status, expected_response, client: TestClient): + response = client.get(path, headers=headers) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_header_params/test_tutorial002_py310.py b/tests/test_tutorial/test_header_params/test_tutorial002_py310.py new file mode 100644 index 000000000..6cae3d338 --- /dev/null +++ b/tests/test_tutorial/test_header_params/test_tutorial002_py310.py @@ -0,0 +1,105 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Strange Header", "type": "string"}, + "name": "strange_header", + "in": "header", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.header_params.tutorial002_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +@pytest.mark.parametrize( + "path,headers,expected_status,expected_response", + [ + ("/openapi.json", None, 200, openapi_schema), + ("/items", None, 200, {"strange_header": None}), + ("/items", {"X-Header": "notvalid"}, 200, {"strange_header": None}), + ( + "/items", + {"strange_header": "FastAPI test"}, + 200, + {"strange_header": "FastAPI test"}, + ), + ( + "/items", + {"strange-header": "Not really underscore"}, + 200, + {"strange_header": None}, + ), + ], +) +def test(path, headers, expected_status, expected_response, client: TestClient): + response = client.get(path, headers=headers) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_header_params/test_tutorial003.py b/tests/test_tutorial/test_header_params/test_tutorial003.py new file mode 100644 index 000000000..99dd9e25f --- /dev/null +++ b/tests/test_tutorial/test_header_params/test_tutorial003.py @@ -0,0 +1,98 @@ +import pytest +from fastapi.testclient import TestClient + +from docs_src.header_params.tutorial003 import app + +client = TestClient(app) + + +@pytest.mark.parametrize( + "path,headers,expected_status,expected_response", + [ + ("/items", None, 200, {"X-Token values": None}), + ("/items", {"x-token": "foo"}, 200, {"X-Token values": ["foo"]}), + # TODO: fix this, is it a bug? + # ("/items", [("x-token", "foo"), ("x-token", "bar")], 200, {"X-Token values": ["foo", "bar"]}), + ], +) +def test(path, headers, expected_status, expected_response): + response = client.get(path, headers=headers) + assert response.status_code == expected_status + assert response.json() == expected_response + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200 + # insert_assert(response.json()) + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "X-Token", + "type": "array", + "items": {"type": "string"}, + }, + "name": "x-token", + "in": "header", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_header_params/test_tutorial003_an.py b/tests/test_tutorial/test_header_params/test_tutorial003_an.py new file mode 100644 index 000000000..4477da7a8 --- /dev/null +++ b/tests/test_tutorial/test_header_params/test_tutorial003_an.py @@ -0,0 +1,98 @@ +import pytest +from fastapi.testclient import TestClient + +from docs_src.header_params.tutorial003_an import app + +client = TestClient(app) + + +@pytest.mark.parametrize( + "path,headers,expected_status,expected_response", + [ + ("/items", None, 200, {"X-Token values": None}), + ("/items", {"x-token": "foo"}, 200, {"X-Token values": ["foo"]}), + # TODO: fix this, is it a bug? + # ("/items", [("x-token", "foo"), ("x-token", "bar")], 200, {"X-Token values": ["foo", "bar"]}), + ], +) +def test(path, headers, expected_status, expected_response): + response = client.get(path, headers=headers) + assert response.status_code == expected_status + assert response.json() == expected_response + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200 + # insert_assert(response.json()) + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "X-Token", + "type": "array", + "items": {"type": "string"}, + }, + "name": "x-token", + "in": "header", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_header_params/test_tutorial003_an_py310.py b/tests/test_tutorial/test_header_params/test_tutorial003_an_py310.py new file mode 100644 index 000000000..b52304a2b --- /dev/null +++ b/tests/test_tutorial/test_header_params/test_tutorial003_an_py310.py @@ -0,0 +1,106 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.header_params.tutorial003_an_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +@pytest.mark.parametrize( + "path,headers,expected_status,expected_response", + [ + ("/items", None, 200, {"X-Token values": None}), + ("/items", {"x-token": "foo"}, 200, {"X-Token values": ["foo"]}), + # TODO: fix this, is it a bug? + # ("/items", [("x-token", "foo"), ("x-token", "bar")], 200, {"X-Token values": ["foo", "bar"]}), + ], +) +def test(path, headers, expected_status, expected_response, client: TestClient): + response = client.get(path, headers=headers) + assert response.status_code == expected_status + assert response.json() == expected_response + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200 + # insert_assert(response.json()) + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "X-Token", + "type": "array", + "items": {"type": "string"}, + }, + "name": "x-token", + "in": "header", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_header_params/test_tutorial003_an_py39.py b/tests/test_tutorial/test_header_params/test_tutorial003_an_py39.py new file mode 100644 index 000000000..dffdd1622 --- /dev/null +++ b/tests/test_tutorial/test_header_params/test_tutorial003_an_py39.py @@ -0,0 +1,106 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.header_params.tutorial003_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py310 +@pytest.mark.parametrize( + "path,headers,expected_status,expected_response", + [ + ("/items", None, 200, {"X-Token values": None}), + ("/items", {"x-token": "foo"}, 200, {"X-Token values": ["foo"]}), + # TODO: fix this, is it a bug? + # ("/items", [("x-token", "foo"), ("x-token", "bar")], 200, {"X-Token values": ["foo", "bar"]}), + ], +) +def test(path, headers, expected_status, expected_response, client: TestClient): + response = client.get(path, headers=headers) + assert response.status_code == expected_status + assert response.json() == expected_response + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200 + # insert_assert(response.json()) + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "X-Token", + "type": "array", + "items": {"type": "string"}, + }, + "name": "x-token", + "in": "header", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_header_params/test_tutorial003_py310.py b/tests/test_tutorial/test_header_params/test_tutorial003_py310.py new file mode 100644 index 000000000..64ef7b22a --- /dev/null +++ b/tests/test_tutorial/test_header_params/test_tutorial003_py310.py @@ -0,0 +1,106 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.header_params.tutorial003_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +@pytest.mark.parametrize( + "path,headers,expected_status,expected_response", + [ + ("/items", None, 200, {"X-Token values": None}), + ("/items", {"x-token": "foo"}, 200, {"X-Token values": ["foo"]}), + # TODO: fix this, is it a bug? + # ("/items", [("x-token", "foo"), ("x-token", "bar")], 200, {"X-Token values": ["foo", "bar"]}), + ], +) +def test(path, headers, expected_status, expected_response, client: TestClient): + response = client.get(path, headers=headers) + assert response.status_code == expected_status + assert response.json() == expected_response + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200 + # insert_assert(response.json()) + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "X-Token", + "type": "array", + "items": {"type": "string"}, + }, + "name": "x-token", + "in": "header", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial001.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010.py similarity index 100% rename from tests/test_tutorial/test_query_params_str_validations/test_tutorial001.py rename to tests/test_tutorial/test_query_params_str_validations/test_tutorial010.py diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an.py new file mode 100644 index 000000000..b2b9b5018 --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an.py @@ -0,0 +1,122 @@ +import pytest +from fastapi.testclient import TestClient + +from docs_src.query_params_str_validations.tutorial010_an import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "description": "Query string for the items to search in the database that have a good match", + "required": False, + "deprecated": True, + "schema": { + "title": "Query string", + "maxLength": 50, + "minLength": 3, + "pattern": "^fixedquery$", + "type": "string", + "description": "Query string for the items to search in the database that have a good match", + }, + "name": "item-query", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +regex_error = { + "detail": [ + { + "ctx": {"pattern": "^fixedquery$"}, + "loc": ["query", "item-query"], + "msg": 'string does not match regex "^fixedquery$"', + "type": "value_error.str.regex", + } + ] +} + + +@pytest.mark.parametrize( + "q_name,q,expected_status,expected_response", + [ + (None, None, 200, {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}), + ( + "item-query", + "fixedquery", + 200, + {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}], "q": "fixedquery"}, + ), + ("q", "fixedquery", 200, {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}), + ("item-query", "nonregexquery", 422, regex_error), + ], +) +def test_query_params_str_validations(q_name, q, expected_status, expected_response): + url = "/items/" + if q_name and q: + url = f"{url}?{q_name}={q}" + response = client.get(url) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py310.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py310.py new file mode 100644 index 000000000..edbe4d009 --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py310.py @@ -0,0 +1,132 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "description": "Query string for the items to search in the database that have a good match", + "required": False, + "deprecated": True, + "schema": { + "title": "Query string", + "maxLength": 50, + "minLength": 3, + "pattern": "^fixedquery$", + "type": "string", + "description": "Query string for the items to search in the database that have a good match", + }, + "name": "item-query", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.query_params_str_validations.tutorial010_an_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +regex_error = { + "detail": [ + { + "ctx": {"pattern": "^fixedquery$"}, + "loc": ["query", "item-query"], + "msg": 'string does not match regex "^fixedquery$"', + "type": "value_error.str.regex", + } + ] +} + + +@needs_py310 +@pytest.mark.parametrize( + "q_name,q,expected_status,expected_response", + [ + (None, None, 200, {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}), + ( + "item-query", + "fixedquery", + 200, + {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}], "q": "fixedquery"}, + ), + ("q", "fixedquery", 200, {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}), + ("item-query", "nonregexquery", 422, regex_error), + ], +) +def test_query_params_str_validations( + q_name, q, expected_status, expected_response, client: TestClient +): + url = "/items/" + if q_name and q: + url = f"{url}?{q_name}={q}" + response = client.get(url) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py39.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py39.py new file mode 100644 index 000000000..f51e90247 --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py39.py @@ -0,0 +1,132 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "description": "Query string for the items to search in the database that have a good match", + "required": False, + "deprecated": True, + "schema": { + "title": "Query string", + "maxLength": 50, + "minLength": 3, + "pattern": "^fixedquery$", + "type": "string", + "description": "Query string for the items to search in the database that have a good match", + }, + "name": "item-query", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.query_params_str_validations.tutorial010_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +regex_error = { + "detail": [ + { + "ctx": {"pattern": "^fixedquery$"}, + "loc": ["query", "item-query"], + "msg": 'string does not match regex "^fixedquery$"', + "type": "value_error.str.regex", + } + ] +} + + +@needs_py39 +@pytest.mark.parametrize( + "q_name,q,expected_status,expected_response", + [ + (None, None, 200, {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}), + ( + "item-query", + "fixedquery", + 200, + {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}], "q": "fixedquery"}, + ), + ("q", "fixedquery", 200, {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}), + ("item-query", "nonregexquery", 422, regex_error), + ], +) +def test_query_params_str_validations( + q_name, q, expected_status, expected_response, client: TestClient +): + url = "/items/" + if q_name and q: + url = f"{url}?{q_name}={q}" + response = client.get(url) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial001_py310.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_py310.py similarity index 100% rename from tests/test_tutorial/test_query_params_str_validations/test_tutorial001_py310.py rename to tests/test_tutorial/test_query_params_str_validations/test_tutorial010_py310.py diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an.py new file mode 100644 index 000000000..25a11f2ca --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an.py @@ -0,0 +1,95 @@ +from fastapi.testclient import TestClient + +from docs_src.query_params_str_validations.tutorial011_an import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Q", + "type": "array", + "items": {"type": "string"}, + }, + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def test_multi_query_values(): + url = "/items/?q=foo&q=bar" + response = client.get(url) + assert response.status_code == 200, response.text + assert response.json() == {"q": ["foo", "bar"]} + + +def test_query_no_values(): + url = "/items/" + response = client.get(url) + assert response.status_code == 200, response.text + assert response.json() == {"q": None} diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py310.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py310.py new file mode 100644 index 000000000..99aaf3948 --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py310.py @@ -0,0 +1,105 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Q", + "type": "array", + "items": {"type": "string"}, + }, + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.query_params_str_validations.tutorial011_an_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py310 +def test_multi_query_values(client: TestClient): + url = "/items/?q=foo&q=bar" + response = client.get(url) + assert response.status_code == 200, response.text + assert response.json() == {"q": ["foo", "bar"]} + + +@needs_py310 +def test_query_no_values(client: TestClient): + url = "/items/" + response = client.get(url) + assert response.status_code == 200, response.text + assert response.json() == {"q": None} diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py39.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py39.py new file mode 100644 index 000000000..902add851 --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py39.py @@ -0,0 +1,105 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Q", + "type": "array", + "items": {"type": "string"}, + }, + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.query_params_str_validations.tutorial011_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py39 +def test_multi_query_values(client: TestClient): + url = "/items/?q=foo&q=bar" + response = client.get(url) + assert response.status_code == 200, response.text + assert response.json() == {"q": ["foo", "bar"]} + + +@needs_py39 +def test_query_no_values(client: TestClient): + url = "/items/" + response = client.get(url) + assert response.status_code == 200, response.text + assert response.json() == {"q": None} diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_an.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_an.py new file mode 100644 index 000000000..e57a2178f --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_an.py @@ -0,0 +1,96 @@ +from fastapi.testclient import TestClient + +from docs_src.query_params_str_validations.tutorial012_an import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Q", + "type": "array", + "items": {"type": "string"}, + "default": ["foo", "bar"], + }, + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def test_default_query_values(): + url = "/items/" + response = client.get(url) + assert response.status_code == 200, response.text + assert response.json() == {"q": ["foo", "bar"]} + + +def test_multi_query_values(): + url = "/items/?q=baz&q=foobar" + response = client.get(url) + assert response.status_code == 200, response.text + assert response.json() == {"q": ["baz", "foobar"]} diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_an_py39.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_an_py39.py new file mode 100644 index 000000000..140b74790 --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_an_py39.py @@ -0,0 +1,106 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Q", + "type": "array", + "items": {"type": "string"}, + "default": ["foo", "bar"], + }, + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.query_params_str_validations.tutorial012_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py39 +def test_default_query_values(client: TestClient): + url = "/items/" + response = client.get(url) + assert response.status_code == 200, response.text + assert response.json() == {"q": ["foo", "bar"]} + + +@needs_py39 +def test_multi_query_values(client: TestClient): + url = "/items/?q=baz&q=foobar" + response = client.get(url) + assert response.status_code == 200, response.text + assert response.json() == {"q": ["baz", "foobar"]} diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial013_an.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial013_an.py new file mode 100644 index 000000000..fc684b557 --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial013_an.py @@ -0,0 +1,96 @@ +from fastapi.testclient import TestClient + +from docs_src.query_params_str_validations.tutorial013_an import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Q", + "type": "array", + "items": {}, + "default": [], + }, + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def test_multi_query_values(): + url = "/items/?q=foo&q=bar" + response = client.get(url) + assert response.status_code == 200, response.text + assert response.json() == {"q": ["foo", "bar"]} + + +def test_query_no_values(): + url = "/items/" + response = client.get(url) + assert response.status_code == 200, response.text + assert response.json() == {"q": []} diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial013_an_py39.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial013_an_py39.py new file mode 100644 index 000000000..9d3f255e0 --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial013_an_py39.py @@ -0,0 +1,106 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Q", + "type": "array", + "items": {}, + "default": [], + }, + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.query_params_str_validations.tutorial013_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py39 +def test_multi_query_values(client: TestClient): + url = "/items/?q=foo&q=bar" + response = client.get(url) + assert response.status_code == 200, response.text + assert response.json() == {"q": ["foo", "bar"]} + + +@needs_py39 +def test_query_no_values(client: TestClient): + url = "/items/" + response = client.get(url) + assert response.status_code == 200, response.text + assert response.json() == {"q": []} diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an.py new file mode 100644 index 000000000..ba5bf7c50 --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an.py @@ -0,0 +1,82 @@ +from fastapi.testclient import TestClient + +from docs_src.query_params_str_validations.tutorial014_an import app + +client = TestClient(app) + + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def test_hidden_query(): + response = client.get("/items?hidden_query=somevalue") + assert response.status_code == 200, response.text + assert response.json() == {"hidden_query": "somevalue"} + + +def test_no_hidden_query(): + response = client.get("/items") + assert response.status_code == 200, response.text + assert response.json() == {"hidden_query": "Not found"} diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an_py310.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an_py310.py new file mode 100644 index 000000000..69e176bab --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an_py310.py @@ -0,0 +1,91 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.query_params_str_validations.tutorial014_an_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py310 +def test_hidden_query(client: TestClient): + response = client.get("/items?hidden_query=somevalue") + assert response.status_code == 200, response.text + assert response.json() == {"hidden_query": "somevalue"} + + +@needs_py310 +def test_no_hidden_query(client: TestClient): + response = client.get("/items") + assert response.status_code == 200, response.text + assert response.json() == {"hidden_query": "Not found"} diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an_py39.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an_py39.py new file mode 100644 index 000000000..2adfddfef --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an_py39.py @@ -0,0 +1,91 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.query_params_str_validations.tutorial014_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py310 +def test_hidden_query(client: TestClient): + response = client.get("/items?hidden_query=somevalue") + assert response.status_code == 200, response.text + assert response.json() == {"hidden_query": "somevalue"} + + +@needs_py310 +def test_no_hidden_query(client: TestClient): + response = client.get("/items") + assert response.status_code == 200, response.text + assert response.json() == {"hidden_query": "Not found"} diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_02_an.py b/tests/test_tutorial/test_request_files/test_tutorial001_02_an.py new file mode 100644 index 000000000..50b05fa4b --- /dev/null +++ b/tests/test_tutorial/test_request_files/test_tutorial001_02_an.py @@ -0,0 +1,157 @@ +from fastapi.testclient import TestClient + +from docs_src.request_files.tutorial001_02_an import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "summary": "Create File", + "operationId": "create_file_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/uploadfile/": { + "post": { + "summary": "Create Upload File", + "operationId": "create_upload_file_uploadfile__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "Body_create_file_files__post": { + "title": "Body_create_file_files__post", + "type": "object", + "properties": { + "file": {"title": "File", "type": "string", "format": "binary"} + }, + }, + "Body_create_upload_file_uploadfile__post": { + "title": "Body_create_upload_file_uploadfile__post", + "type": "object", + "properties": { + "file": {"title": "File", "type": "string", "format": "binary"} + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def test_post_form_no_body(): + response = client.post("/files/") + assert response.status_code == 200, response.text + assert response.json() == {"message": "No file sent"} + + +def test_post_uploadfile_no_body(): + response = client.post("/uploadfile/") + assert response.status_code == 200, response.text + assert response.json() == {"message": "No upload file sent"} + + +def test_post_file(tmp_path): + path = tmp_path / "test.txt" + path.write_bytes(b"") + + client = TestClient(app) + with path.open("rb") as file: + response = client.post("/files/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": 14} + + +def test_post_upload_file(tmp_path): + path = tmp_path / "test.txt" + path.write_bytes(b"") + + client = TestClient(app) + with path.open("rb") as file: + response = client.post("/uploadfile/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"filename": "test.txt"} diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py310.py b/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py310.py new file mode 100644 index 000000000..a5796b74c --- /dev/null +++ b/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py310.py @@ -0,0 +1,169 @@ +from pathlib import Path + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "summary": "Create File", + "operationId": "create_file_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/uploadfile/": { + "post": { + "summary": "Create Upload File", + "operationId": "create_upload_file_uploadfile__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "Body_create_file_files__post": { + "title": "Body_create_file_files__post", + "type": "object", + "properties": { + "file": {"title": "File", "type": "string", "format": "binary"} + }, + }, + "Body_create_upload_file_uploadfile__post": { + "title": "Body_create_upload_file_uploadfile__post", + "type": "object", + "properties": { + "file": {"title": "File", "type": "string", "format": "binary"} + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.request_files.tutorial001_02_an_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py310 +def test_post_form_no_body(client: TestClient): + response = client.post("/files/") + assert response.status_code == 200, response.text + assert response.json() == {"message": "No file sent"} + + +@needs_py310 +def test_post_uploadfile_no_body(client: TestClient): + response = client.post("/uploadfile/") + assert response.status_code == 200, response.text + assert response.json() == {"message": "No upload file sent"} + + +@needs_py310 +def test_post_file(tmp_path: Path, client: TestClient): + path = tmp_path / "test.txt" + path.write_bytes(b"") + + with path.open("rb") as file: + response = client.post("/files/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": 14} + + +@needs_py310 +def test_post_upload_file(tmp_path: Path, client: TestClient): + path = tmp_path / "test.txt" + path.write_bytes(b"") + + with path.open("rb") as file: + response = client.post("/uploadfile/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"filename": "test.txt"} diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py39.py b/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py39.py new file mode 100644 index 000000000..57175f736 --- /dev/null +++ b/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py39.py @@ -0,0 +1,169 @@ +from pathlib import Path + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "summary": "Create File", + "operationId": "create_file_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/uploadfile/": { + "post": { + "summary": "Create Upload File", + "operationId": "create_upload_file_uploadfile__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "Body_create_file_files__post": { + "title": "Body_create_file_files__post", + "type": "object", + "properties": { + "file": {"title": "File", "type": "string", "format": "binary"} + }, + }, + "Body_create_upload_file_uploadfile__post": { + "title": "Body_create_upload_file_uploadfile__post", + "type": "object", + "properties": { + "file": {"title": "File", "type": "string", "format": "binary"} + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.request_files.tutorial001_02_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py39 +def test_post_form_no_body(client: TestClient): + response = client.post("/files/") + assert response.status_code == 200, response.text + assert response.json() == {"message": "No file sent"} + + +@needs_py39 +def test_post_uploadfile_no_body(client: TestClient): + response = client.post("/uploadfile/") + assert response.status_code == 200, response.text + assert response.json() == {"message": "No upload file sent"} + + +@needs_py39 +def test_post_file(tmp_path: Path, client: TestClient): + path = tmp_path / "test.txt" + path.write_bytes(b"") + + with path.open("rb") as file: + response = client.post("/files/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": 14} + + +@needs_py39 +def test_post_upload_file(tmp_path: Path, client: TestClient): + path = tmp_path / "test.txt" + path.write_bytes(b"") + + with path.open("rb") as file: + response = client.post("/uploadfile/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"filename": "test.txt"} diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_03_an.py b/tests/test_tutorial/test_request_files/test_tutorial001_03_an.py new file mode 100644 index 000000000..e83fc68bb --- /dev/null +++ b/tests/test_tutorial/test_request_files/test_tutorial001_03_an.py @@ -0,0 +1,159 @@ +from fastapi.testclient import TestClient + +from docs_src.request_files.tutorial001_03_an import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "summary": "Create File", + "operationId": "create_file_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/uploadfile/": { + "post": { + "summary": "Create Upload File", + "operationId": "create_upload_file_uploadfile__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "Body_create_file_files__post": { + "title": "Body_create_file_files__post", + "required": ["file"], + "type": "object", + "properties": { + "file": { + "title": "File", + "type": "string", + "description": "A file read as bytes", + "format": "binary", + } + }, + }, + "Body_create_upload_file_uploadfile__post": { + "title": "Body_create_upload_file_uploadfile__post", + "required": ["file"], + "type": "object", + "properties": { + "file": { + "title": "File", + "type": "string", + "description": "A file read as UploadFile", + "format": "binary", + } + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def test_post_file(tmp_path): + path = tmp_path / "test.txt" + path.write_bytes(b"") + + client = TestClient(app) + with path.open("rb") as file: + response = client.post("/files/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": 14} + + +def test_post_upload_file(tmp_path): + path = tmp_path / "test.txt" + path.write_bytes(b"") + + client = TestClient(app) + with path.open("rb") as file: + response = client.post("/uploadfile/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"filename": "test.txt"} diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_03_an_py39.py b/tests/test_tutorial/test_request_files/test_tutorial001_03_an_py39.py new file mode 100644 index 000000000..7808262a7 --- /dev/null +++ b/tests/test_tutorial/test_request_files/test_tutorial001_03_an_py39.py @@ -0,0 +1,167 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "summary": "Create File", + "operationId": "create_file_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/uploadfile/": { + "post": { + "summary": "Create Upload File", + "operationId": "create_upload_file_uploadfile__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "Body_create_file_files__post": { + "title": "Body_create_file_files__post", + "required": ["file"], + "type": "object", + "properties": { + "file": { + "title": "File", + "type": "string", + "description": "A file read as bytes", + "format": "binary", + } + }, + }, + "Body_create_upload_file_uploadfile__post": { + "title": "Body_create_upload_file_uploadfile__post", + "required": ["file"], + "type": "object", + "properties": { + "file": { + "title": "File", + "type": "string", + "description": "A file read as UploadFile", + "format": "binary", + } + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.request_files.tutorial001_03_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py39 +def test_post_file(tmp_path, client: TestClient): + path = tmp_path / "test.txt" + path.write_bytes(b"") + + with path.open("rb") as file: + response = client.post("/files/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": 14} + + +@needs_py39 +def test_post_upload_file(tmp_path, client: TestClient): + path = tmp_path / "test.txt" + path.write_bytes(b"") + + with path.open("rb") as file: + response = client.post("/uploadfile/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"filename": "test.txt"} diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_an.py b/tests/test_tutorial/test_request_files/test_tutorial001_an.py new file mode 100644 index 000000000..739f04b43 --- /dev/null +++ b/tests/test_tutorial/test_request_files/test_tutorial001_an.py @@ -0,0 +1,184 @@ +from fastapi.testclient import TestClient + +from docs_src.request_files.tutorial001_an import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create File", + "operationId": "create_file_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + } + }, + "required": True, + }, + } + }, + "/uploadfile/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create Upload File", + "operationId": "create_upload_file_uploadfile__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + } + }, + "required": True, + }, + } + }, + }, + "components": { + "schemas": { + "Body_create_upload_file_uploadfile__post": { + "title": "Body_create_upload_file_uploadfile__post", + "required": ["file"], + "type": "object", + "properties": { + "file": {"title": "File", "type": "string", "format": "binary"} + }, + }, + "Body_create_file_files__post": { + "title": "Body_create_file_files__post", + "required": ["file"], + "type": "object", + "properties": { + "file": {"title": "File", "type": "string", "format": "binary"} + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +file_required = { + "detail": [ + { + "loc": ["body", "file"], + "msg": "field required", + "type": "value_error.missing", + } + ] +} + + +def test_post_form_no_body(): + response = client.post("/files/") + assert response.status_code == 422, response.text + assert response.json() == file_required + + +def test_post_body_json(): + response = client.post("/files/", json={"file": "Foo"}) + assert response.status_code == 422, response.text + assert response.json() == file_required + + +def test_post_file(tmp_path): + path = tmp_path / "test.txt" + path.write_bytes(b"") + + client = TestClient(app) + with path.open("rb") as file: + response = client.post("/files/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": 14} + + +def test_post_large_file(tmp_path): + default_pydantic_max_size = 2**16 + path = tmp_path / "test.txt" + path.write_bytes(b"x" * (default_pydantic_max_size + 1)) + + client = TestClient(app) + with path.open("rb") as file: + response = client.post("/files/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": default_pydantic_max_size + 1} + + +def test_post_upload_file(tmp_path): + path = tmp_path / "test.txt" + path.write_bytes(b"") + + client = TestClient(app) + with path.open("rb") as file: + response = client.post("/uploadfile/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"filename": "test.txt"} diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_an_py39.py b/tests/test_tutorial/test_request_files/test_tutorial001_an_py39.py new file mode 100644 index 000000000..091a9362b --- /dev/null +++ b/tests/test_tutorial/test_request_files/test_tutorial001_an_py39.py @@ -0,0 +1,194 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create File", + "operationId": "create_file_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + } + }, + "required": True, + }, + } + }, + "/uploadfile/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create Upload File", + "operationId": "create_upload_file_uploadfile__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + } + }, + "required": True, + }, + } + }, + }, + "components": { + "schemas": { + "Body_create_upload_file_uploadfile__post": { + "title": "Body_create_upload_file_uploadfile__post", + "required": ["file"], + "type": "object", + "properties": { + "file": {"title": "File", "type": "string", "format": "binary"} + }, + }, + "Body_create_file_files__post": { + "title": "Body_create_file_files__post", + "required": ["file"], + "type": "object", + "properties": { + "file": {"title": "File", "type": "string", "format": "binary"} + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.request_files.tutorial001_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +file_required = { + "detail": [ + { + "loc": ["body", "file"], + "msg": "field required", + "type": "value_error.missing", + } + ] +} + + +@needs_py39 +def test_post_form_no_body(client: TestClient): + response = client.post("/files/") + assert response.status_code == 422, response.text + assert response.json() == file_required + + +@needs_py39 +def test_post_body_json(client: TestClient): + response = client.post("/files/", json={"file": "Foo"}) + assert response.status_code == 422, response.text + assert response.json() == file_required + + +@needs_py39 +def test_post_file(tmp_path, client: TestClient): + path = tmp_path / "test.txt" + path.write_bytes(b"") + + with path.open("rb") as file: + response = client.post("/files/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": 14} + + +@needs_py39 +def test_post_large_file(tmp_path, client: TestClient): + default_pydantic_max_size = 2**16 + path = tmp_path / "test.txt" + path.write_bytes(b"x" * (default_pydantic_max_size + 1)) + + with path.open("rb") as file: + response = client.post("/files/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": default_pydantic_max_size + 1} + + +@needs_py39 +def test_post_upload_file(tmp_path, client: TestClient): + path = tmp_path / "test.txt" + path.write_bytes(b"") + + with path.open("rb") as file: + response = client.post("/uploadfile/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"filename": "test.txt"} diff --git a/tests/test_tutorial/test_request_files/test_tutorial002_an.py b/tests/test_tutorial/test_request_files/test_tutorial002_an.py new file mode 100644 index 000000000..8a99c657c --- /dev/null +++ b/tests/test_tutorial/test_request_files/test_tutorial002_an.py @@ -0,0 +1,215 @@ +from fastapi.testclient import TestClient + +from docs_src.request_files.tutorial002_an import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create Files", + "operationId": "create_files_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_files_files__post" + } + } + }, + "required": True, + }, + } + }, + "/uploadfiles/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create Upload Files", + "operationId": "create_upload_files_uploadfiles__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_upload_files_uploadfiles__post" + } + } + }, + "required": True, + }, + } + }, + "/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Main", + "operationId": "main__get", + } + }, + }, + "components": { + "schemas": { + "Body_create_upload_files_uploadfiles__post": { + "title": "Body_create_upload_files_uploadfiles__post", + "required": ["files"], + "type": "object", + "properties": { + "files": { + "title": "Files", + "type": "array", + "items": {"type": "string", "format": "binary"}, + } + }, + }, + "Body_create_files_files__post": { + "title": "Body_create_files_files__post", + "required": ["files"], + "type": "object", + "properties": { + "files": { + "title": "Files", + "type": "array", + "items": {"type": "string", "format": "binary"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +file_required = { + "detail": [ + { + "loc": ["body", "files"], + "msg": "field required", + "type": "value_error.missing", + } + ] +} + + +def test_post_form_no_body(): + response = client.post("/files/") + assert response.status_code == 422, response.text + assert response.json() == file_required + + +def test_post_body_json(): + response = client.post("/files/", json={"file": "Foo"}) + assert response.status_code == 422, response.text + assert response.json() == file_required + + +def test_post_files(tmp_path): + path = tmp_path / "test.txt" + path.write_bytes(b"") + path2 = tmp_path / "test2.txt" + path2.write_bytes(b"") + + client = TestClient(app) + with path.open("rb") as file, path2.open("rb") as file2: + response = client.post( + "/files/", + files=( + ("files", ("test.txt", file)), + ("files", ("test2.txt", file2)), + ), + ) + assert response.status_code == 200, response.text + assert response.json() == {"file_sizes": [14, 15]} + + +def test_post_upload_file(tmp_path): + path = tmp_path / "test.txt" + path.write_bytes(b"") + path2 = tmp_path / "test2.txt" + path2.write_bytes(b"") + + client = TestClient(app) + with path.open("rb") as file, path2.open("rb") as file2: + response = client.post( + "/uploadfiles/", + files=( + ("files", ("test.txt", file)), + ("files", ("test2.txt", file2)), + ), + ) + assert response.status_code == 200, response.text + assert response.json() == {"filenames": ["test.txt", "test2.txt"]} + + +def test_get_root(): + client = TestClient(app) + response = client.get("/") + assert response.status_code == 200, response.text + assert b"") + path2 = tmp_path / "test2.txt" + path2.write_bytes(b"") + + client = TestClient(app) + with path.open("rb") as file, path2.open("rb") as file2: + response = client.post( + "/files/", + files=( + ("files", ("test.txt", file)), + ("files", ("test2.txt", file2)), + ), + ) + assert response.status_code == 200, response.text + assert response.json() == {"file_sizes": [14, 15]} + + +@needs_py39 +def test_post_upload_file(tmp_path, app: FastAPI): + path = tmp_path / "test.txt" + path.write_bytes(b"") + path2 = tmp_path / "test2.txt" + path2.write_bytes(b"") + + client = TestClient(app) + with path.open("rb") as file, path2.open("rb") as file2: + response = client.post( + "/uploadfiles/", + files=( + ("files", ("test.txt", file)), + ("files", ("test2.txt", file2)), + ), + ) + assert response.status_code == 200, response.text + assert response.json() == {"filenames": ["test.txt", "test2.txt"]} + + +@needs_py39 +def test_get_root(app: FastAPI): + client = TestClient(app) + response = client.get("/") + assert response.status_code == 200, response.text + assert b"") + path2 = tmp_path / "test2.txt" + path2.write_bytes(b"") + + client = TestClient(app) + with path.open("rb") as file, path2.open("rb") as file2: + response = client.post( + "/files/", + files=( + ("files", ("test.txt", file)), + ("files", ("test2.txt", file2)), + ), + ) + assert response.status_code == 200, response.text + assert response.json() == {"file_sizes": [14, 15]} + + +def test_post_upload_file(tmp_path): + path = tmp_path / "test.txt" + path.write_bytes(b"") + path2 = tmp_path / "test2.txt" + path2.write_bytes(b"") + + client = TestClient(app) + with path.open("rb") as file, path2.open("rb") as file2: + response = client.post( + "/uploadfiles/", + files=( + ("files", ("test.txt", file)), + ("files", ("test2.txt", file2)), + ), + ) + assert response.status_code == 200, response.text + assert response.json() == {"filenames": ["test.txt", "test2.txt"]} + + +def test_get_root(): + client = TestClient(app) + response = client.get("/") + assert response.status_code == 200, response.text + assert b"") + path2 = tmp_path / "test2.txt" + path2.write_bytes(b"") + + client = TestClient(app) + with path.open("rb") as file, path2.open("rb") as file2: + response = client.post( + "/files/", + files=( + ("files", ("test.txt", file)), + ("files", ("test2.txt", file2)), + ), + ) + assert response.status_code == 200, response.text + assert response.json() == {"file_sizes": [14, 15]} + + +@needs_py39 +def test_post_upload_file(tmp_path, app: FastAPI): + path = tmp_path / "test.txt" + path.write_bytes(b"") + path2 = tmp_path / "test2.txt" + path2.write_bytes(b"") + + client = TestClient(app) + with path.open("rb") as file, path2.open("rb") as file2: + response = client.post( + "/uploadfiles/", + files=( + ("files", ("test.txt", file)), + ("files", ("test2.txt", file2)), + ), + ) + assert response.status_code == 200, response.text + assert response.json() == {"filenames": ["test.txt", "test2.txt"]} + + +@needs_py39 +def test_get_root(app: FastAPI): + client = TestClient(app) + response = client.get("/") + assert response.status_code == 200, response.text + assert b"") + + client = TestClient(app) + with path.open("rb") as file: + response = client.post("/files/", files={"file": file}) + assert response.status_code == 422, response.text + assert response.json() == token_required + + +def test_post_files_and_token(tmp_path): + patha = tmp_path / "test.txt" + pathb = tmp_path / "testb.txt" + patha.write_text("") + pathb.write_text("") + + client = TestClient(app) + with patha.open("rb") as filea, pathb.open("rb") as fileb: + response = client.post( + "/files/", + data={"token": "foo"}, + files={"file": filea, "fileb": ("testb.txt", fileb, "text/plain")}, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "file_size": 14, + "token": "foo", + "fileb_content_type": "text/plain", + } diff --git a/tests/test_tutorial/test_request_forms_and_files/test_tutorial001_an_py39.py b/tests/test_tutorial/test_request_forms_and_files/test_tutorial001_an_py39.py new file mode 100644 index 000000000..fa2ebc77d --- /dev/null +++ b/tests/test_tutorial/test_request_forms_and_files/test_tutorial001_an_py39.py @@ -0,0 +1,211 @@ +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create File", + "operationId": "create_file_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Body_create_file_files__post": { + "title": "Body_create_file_files__post", + "required": ["file", "fileb", "token"], + "type": "object", + "properties": { + "file": {"title": "File", "type": "string", "format": "binary"}, + "fileb": {"title": "Fileb", "type": "string", "format": "binary"}, + "token": {"title": "Token", "type": "string"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="app") +def get_app(): + from docs_src.request_forms_and_files.tutorial001_an_py39 import app + + return app + + +@pytest.fixture(name="client") +def get_client(app: FastAPI): + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +file_required = { + "detail": [ + { + "loc": ["body", "file"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "fileb"], + "msg": "field required", + "type": "value_error.missing", + }, + ] +} + +token_required = { + "detail": [ + { + "loc": ["body", "fileb"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] +} + +# {'detail': [, {'loc': ['body', 'token'], 'msg': 'field required', 'type': 'value_error.missing'}]} + +file_and_token_required = { + "detail": [ + { + "loc": ["body", "file"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "fileb"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] +} + + +@needs_py39 +def test_post_form_no_body(client: TestClient): + response = client.post("/files/") + assert response.status_code == 422, response.text + assert response.json() == file_and_token_required + + +@needs_py39 +def test_post_form_no_file(client: TestClient): + response = client.post("/files/", data={"token": "foo"}) + assert response.status_code == 422, response.text + assert response.json() == file_required + + +@needs_py39 +def test_post_body_json(client: TestClient): + response = client.post("/files/", json={"file": "Foo", "token": "Bar"}) + assert response.status_code == 422, response.text + assert response.json() == file_and_token_required + + +@needs_py39 +def test_post_file_no_token(tmp_path, app: FastAPI): + path = tmp_path / "test.txt" + path.write_bytes(b"") + + client = TestClient(app) + with path.open("rb") as file: + response = client.post("/files/", files={"file": file}) + assert response.status_code == 422, response.text + assert response.json() == token_required + + +@needs_py39 +def test_post_files_and_token(tmp_path, app: FastAPI): + patha = tmp_path / "test.txt" + pathb = tmp_path / "testb.txt" + patha.write_text("") + pathb.write_text("") + + client = TestClient(app) + with patha.open("rb") as filea, pathb.open("rb") as fileb: + response = client.post( + "/files/", + data={"token": "foo"}, + files={"file": filea, "fileb": ("testb.txt", fileb, "text/plain")}, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "file_size": 14, + "token": "foo", + "fileb_content_type": "text/plain", + } diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an.py new file mode 100644 index 000000000..7694669ce --- /dev/null +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an.py @@ -0,0 +1,134 @@ +from fastapi.testclient import TestClient + +from docs_src.schema_extra_example.tutorial004_an import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"}, + "examples": { + "normal": { + "summary": "A normal example", + "description": "A **normal** item works correctly.", + "value": { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + }, + "converted": { + "summary": "An example with converted data", + "description": "FastAPI can convert price `strings` to actual `numbers` automatically", + "value": {"name": "Bar", "price": "35.4"}, + }, + "invalid": { + "summary": "Invalid data is rejected with an error", + "value": { + "name": "Baz", + "price": "thirty five point four", + }, + }, + }, + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": {"title": "Description", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "tax": {"title": "Tax", "type": "number"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +# Test required and embedded body parameters with no bodies sent +def test_post_body_example(): + response = client.put( + "/items/5", + json={ + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + ) + assert response.status_code == 200 diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py310.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py310.py new file mode 100644 index 000000000..c81fbcf52 --- /dev/null +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py310.py @@ -0,0 +1,143 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"}, + "examples": { + "normal": { + "summary": "A normal example", + "description": "A **normal** item works correctly.", + "value": { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + }, + "converted": { + "summary": "An example with converted data", + "description": "FastAPI can convert price `strings` to actual `numbers` automatically", + "value": {"name": "Bar", "price": "35.4"}, + }, + "invalid": { + "summary": "Invalid data is rejected with an error", + "value": { + "name": "Baz", + "price": "thirty five point four", + }, + }, + }, + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": {"title": "Description", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "tax": {"title": "Tax", "type": "number"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.schema_extra_example.tutorial004_an_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +# Test required and embedded body parameters with no bodies sent +@needs_py310 +def test_post_body_example(client: TestClient): + response = client.put( + "/items/5", + json={ + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + ) + assert response.status_code == 200 diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py39.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py39.py new file mode 100644 index 000000000..395c27b0e --- /dev/null +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py39.py @@ -0,0 +1,143 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"}, + "examples": { + "normal": { + "summary": "A normal example", + "description": "A **normal** item works correctly.", + "value": { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + }, + "converted": { + "summary": "An example with converted data", + "description": "FastAPI can convert price `strings` to actual `numbers` automatically", + "value": {"name": "Bar", "price": "35.4"}, + }, + "invalid": { + "summary": "Invalid data is rejected with an error", + "value": { + "name": "Baz", + "price": "thirty five point four", + }, + }, + }, + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": {"title": "Description", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "tax": {"title": "Tax", "type": "number"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.schema_extra_example.tutorial004_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +# Test required and embedded body parameters with no bodies sent +@needs_py39 +def test_post_body_example(client: TestClient): + response = client.put( + "/items/5", + json={ + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + ) + assert response.status_code == 200 diff --git a/tests/test_tutorial/test_security/test_tutorial001_an.py b/tests/test_tutorial/test_security/test_tutorial001_an.py new file mode 100644 index 000000000..fdcb9bfb8 --- /dev/null +++ b/tests/test_tutorial/test_security/test_tutorial001_an.py @@ -0,0 +1,59 @@ +from fastapi.testclient import TestClient + +from docs_src.security.tutorial001_an import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "security": [{"OAuth2PasswordBearer": []}], + } + } + }, + "components": { + "securitySchemes": { + "OAuth2PasswordBearer": { + "type": "oauth2", + "flows": {"password": {"scopes": {}, "tokenUrl": "token"}}, + } + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def test_no_token(): + response = client.get("/items") + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +def test_token(): + response = client.get("/items", headers={"Authorization": "Bearer testtoken"}) + assert response.status_code == 200, response.text + assert response.json() == {"token": "testtoken"} + + +def test_incorrect_token(): + response = client.get("/items", headers={"Authorization": "Notexistent testtoken"}) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" diff --git a/tests/test_tutorial/test_security/test_tutorial001_an_py39.py b/tests/test_tutorial/test_security/test_tutorial001_an_py39.py new file mode 100644 index 000000000..4f8947b90 --- /dev/null +++ b/tests/test_tutorial/test_security/test_tutorial001_an_py39.py @@ -0,0 +1,70 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "security": [{"OAuth2PasswordBearer": []}], + } + } + }, + "components": { + "securitySchemes": { + "OAuth2PasswordBearer": { + "type": "oauth2", + "flows": {"password": {"scopes": {}, "tokenUrl": "token"}}, + } + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.security.tutorial001_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py39 +def test_no_token(client: TestClient): + response = client.get("/items") + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +@needs_py39 +def test_token(client: TestClient): + response = client.get("/items", headers={"Authorization": "Bearer testtoken"}) + assert response.status_code == 200, response.text + assert response.json() == {"token": "testtoken"} + + +@needs_py39 +def test_incorrect_token(client: TestClient): + response = client.get("/items", headers={"Authorization": "Notexistent testtoken"}) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" diff --git a/tests/test_tutorial/test_security/test_tutorial003_an.py b/tests/test_tutorial/test_security/test_tutorial003_an.py new file mode 100644 index 000000000..b1b9c0fc1 --- /dev/null +++ b/tests/test_tutorial/test_security/test_tutorial003_an.py @@ -0,0 +1,176 @@ +from fastapi.testclient import TestClient + +from docs_src.security.tutorial003_an import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/token": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Login", + "operationId": "login_token_post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Body_login_token_post" + } + } + }, + "required": True, + }, + } + }, + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Users Me", + "operationId": "read_users_me_users_me_get", + "security": [{"OAuth2PasswordBearer": []}], + } + }, + }, + "components": { + "schemas": { + "Body_login_token_post": { + "title": "Body_login_token_post", + "required": ["username", "password"], + "type": "object", + "properties": { + "grant_type": { + "title": "Grant Type", + "pattern": "password", + "type": "string", + }, + "username": {"title": "Username", "type": "string"}, + "password": {"title": "Password", "type": "string"}, + "scope": {"title": "Scope", "type": "string", "default": ""}, + "client_id": {"title": "Client Id", "type": "string"}, + "client_secret": {"title": "Client Secret", "type": "string"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + }, + "securitySchemes": { + "OAuth2PasswordBearer": { + "type": "oauth2", + "flows": {"password": {"scopes": {}, "tokenUrl": "token"}}, + } + }, + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def test_login(): + response = client.post("/token", data={"username": "johndoe", "password": "secret"}) + assert response.status_code == 200, response.text + assert response.json() == {"access_token": "johndoe", "token_type": "bearer"} + + +def test_login_incorrect_password(): + response = client.post( + "/token", data={"username": "johndoe", "password": "incorrect"} + ) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Incorrect username or password"} + + +def test_login_incorrect_username(): + response = client.post("/token", data={"username": "foo", "password": "secret"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Incorrect username or password"} + + +def test_no_token(): + response = client.get("/users/me") + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +def test_token(): + response = client.get("/users/me", headers={"Authorization": "Bearer johndoe"}) + assert response.status_code == 200, response.text + assert response.json() == { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "hashed_password": "fakehashedsecret", + "disabled": False, + } + + +def test_incorrect_token(): + 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.headers["WWW-Authenticate"] == "Bearer" + + +def test_incorrect_token_type(): + response = client.get( + "/users/me", headers={"Authorization": "Notexistent testtoken"} + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +def test_inactive_user(): + response = client.get("/users/me", headers={"Authorization": "Bearer alice"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Inactive user"} diff --git a/tests/test_tutorial/test_security/test_tutorial003_an_py310.py b/tests/test_tutorial/test_security/test_tutorial003_an_py310.py new file mode 100644 index 000000000..486f0c4ce --- /dev/null +++ b/tests/test_tutorial/test_security/test_tutorial003_an_py310.py @@ -0,0 +1,192 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/token": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Login", + "operationId": "login_token_post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Body_login_token_post" + } + } + }, + "required": True, + }, + } + }, + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Users Me", + "operationId": "read_users_me_users_me_get", + "security": [{"OAuth2PasswordBearer": []}], + } + }, + }, + "components": { + "schemas": { + "Body_login_token_post": { + "title": "Body_login_token_post", + "required": ["username", "password"], + "type": "object", + "properties": { + "grant_type": { + "title": "Grant Type", + "pattern": "password", + "type": "string", + }, + "username": {"title": "Username", "type": "string"}, + "password": {"title": "Password", "type": "string"}, + "scope": {"title": "Scope", "type": "string", "default": ""}, + "client_id": {"title": "Client Id", "type": "string"}, + "client_secret": {"title": "Client Secret", "type": "string"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + }, + "securitySchemes": { + "OAuth2PasswordBearer": { + "type": "oauth2", + "flows": {"password": {"scopes": {}, "tokenUrl": "token"}}, + } + }, + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.security.tutorial003_an_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py310 +def test_login(client: TestClient): + response = client.post("/token", data={"username": "johndoe", "password": "secret"}) + assert response.status_code == 200, response.text + assert response.json() == {"access_token": "johndoe", "token_type": "bearer"} + + +@needs_py310 +def test_login_incorrect_password(client: TestClient): + response = client.post( + "/token", data={"username": "johndoe", "password": "incorrect"} + ) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Incorrect username or password"} + + +@needs_py310 +def test_login_incorrect_username(client: TestClient): + response = client.post("/token", data={"username": "foo", "password": "secret"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Incorrect username or password"} + + +@needs_py310 +def test_no_token(client: TestClient): + response = client.get("/users/me") + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +@needs_py310 +def test_token(client: TestClient): + response = client.get("/users/me", headers={"Authorization": "Bearer johndoe"}) + assert response.status_code == 200, response.text + assert response.json() == { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "hashed_password": "fakehashedsecret", + "disabled": False, + } + + +@needs_py310 +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.headers["WWW-Authenticate"] == "Bearer" + + +@needs_py310 +def test_incorrect_token_type(client: TestClient): + response = client.get( + "/users/me", headers={"Authorization": "Notexistent testtoken"} + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +@needs_py310 +def test_inactive_user(client: TestClient): + response = client.get("/users/me", headers={"Authorization": "Bearer alice"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Inactive user"} diff --git a/tests/test_tutorial/test_security/test_tutorial003_an_py39.py b/tests/test_tutorial/test_security/test_tutorial003_an_py39.py new file mode 100644 index 000000000..b6709e2fb --- /dev/null +++ b/tests/test_tutorial/test_security/test_tutorial003_an_py39.py @@ -0,0 +1,192 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/token": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Login", + "operationId": "login_token_post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Body_login_token_post" + } + } + }, + "required": True, + }, + } + }, + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Users Me", + "operationId": "read_users_me_users_me_get", + "security": [{"OAuth2PasswordBearer": []}], + } + }, + }, + "components": { + "schemas": { + "Body_login_token_post": { + "title": "Body_login_token_post", + "required": ["username", "password"], + "type": "object", + "properties": { + "grant_type": { + "title": "Grant Type", + "pattern": "password", + "type": "string", + }, + "username": {"title": "Username", "type": "string"}, + "password": {"title": "Password", "type": "string"}, + "scope": {"title": "Scope", "type": "string", "default": ""}, + "client_id": {"title": "Client Id", "type": "string"}, + "client_secret": {"title": "Client Secret", "type": "string"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + }, + "securitySchemes": { + "OAuth2PasswordBearer": { + "type": "oauth2", + "flows": {"password": {"scopes": {}, "tokenUrl": "token"}}, + } + }, + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.security.tutorial003_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py39 +def test_login(client: TestClient): + response = client.post("/token", data={"username": "johndoe", "password": "secret"}) + assert response.status_code == 200, response.text + assert response.json() == {"access_token": "johndoe", "token_type": "bearer"} + + +@needs_py39 +def test_login_incorrect_password(client: TestClient): + response = client.post( + "/token", data={"username": "johndoe", "password": "incorrect"} + ) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Incorrect username or password"} + + +@needs_py39 +def test_login_incorrect_username(client: TestClient): + response = client.post("/token", data={"username": "foo", "password": "secret"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Incorrect username or password"} + + +@needs_py39 +def test_no_token(client: TestClient): + response = client.get("/users/me") + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +@needs_py39 +def test_token(client: TestClient): + response = client.get("/users/me", headers={"Authorization": "Bearer johndoe"}) + assert response.status_code == 200, response.text + assert response.json() == { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "hashed_password": "fakehashedsecret", + "disabled": False, + } + + +@needs_py39 +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.headers["WWW-Authenticate"] == "Bearer" + + +@needs_py39 +def test_incorrect_token_type(client: TestClient): + response = client.get( + "/users/me", headers={"Authorization": "Notexistent testtoken"} + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +@needs_py39 +def test_inactive_user(client: TestClient): + response = client.get("/users/me", headers={"Authorization": "Bearer alice"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Inactive user"} diff --git a/tests/test_tutorial/test_security/test_tutorial005_an.py b/tests/test_tutorial/test_security/test_tutorial005_an.py new file mode 100644 index 000000000..b6c2708f0 --- /dev/null +++ b/tests/test_tutorial/test_security/test_tutorial005_an.py @@ -0,0 +1,347 @@ +from fastapi.testclient import TestClient + +from docs_src.security.tutorial005_an import ( + app, + create_access_token, + fake_users_db, + get_password_hash, + verify_password, +) + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/token": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Token"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Login For Access Token", + "operationId": "login_for_access_token_token_post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Body_login_for_access_token_token_post" + } + } + }, + "required": True, + }, + } + }, + "/users/me/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + "summary": "Read Users Me", + "operationId": "read_users_me_users_me__get", + "security": [{"OAuth2PasswordBearer": ["me"]}], + } + }, + "/users/me/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Own Items", + "operationId": "read_own_items_users_me_items__get", + "security": [{"OAuth2PasswordBearer": ["items", "me"]}], + } + }, + "/status/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read System Status", + "operationId": "read_system_status_status__get", + "security": [{"OAuth2PasswordBearer": []}], + } + }, + }, + "components": { + "schemas": { + "User": { + "title": "User", + "required": ["username"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "email": {"title": "Email", "type": "string"}, + "full_name": {"title": "Full Name", "type": "string"}, + "disabled": {"title": "Disabled", "type": "boolean"}, + }, + }, + "Token": { + "title": "Token", + "required": ["access_token", "token_type"], + "type": "object", + "properties": { + "access_token": {"title": "Access Token", "type": "string"}, + "token_type": {"title": "Token Type", "type": "string"}, + }, + }, + "Body_login_for_access_token_token_post": { + "title": "Body_login_for_access_token_token_post", + "required": ["username", "password"], + "type": "object", + "properties": { + "grant_type": { + "title": "Grant Type", + "pattern": "password", + "type": "string", + }, + "username": {"title": "Username", "type": "string"}, + "password": {"title": "Password", "type": "string"}, + "scope": {"title": "Scope", "type": "string", "default": ""}, + "client_id": {"title": "Client Id", "type": "string"}, + "client_secret": {"title": "Client Secret", "type": "string"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + }, + "securitySchemes": { + "OAuth2PasswordBearer": { + "type": "oauth2", + "flows": { + "password": { + "scopes": { + "me": "Read information about the current user.", + "items": "Read items.", + }, + "tokenUrl": "token", + } + }, + } + }, + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def get_access_token(username="johndoe", password="secret", scope=None): + data = {"username": username, "password": password} + if scope: + data["scope"] = scope + response = client.post("/token", data=data) + content = response.json() + access_token = content.get("access_token") + return access_token + + +def test_login(): + response = client.post("/token", data={"username": "johndoe", "password": "secret"}) + assert response.status_code == 200, response.text + content = response.json() + assert "access_token" in content + assert content["token_type"] == "bearer" + + +def test_login_incorrect_password(): + response = client.post( + "/token", data={"username": "johndoe", "password": "incorrect"} + ) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Incorrect username or password"} + + +def test_login_incorrect_username(): + response = client.post("/token", data={"username": "foo", "password": "secret"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Incorrect username or password"} + + +def test_no_token(): + response = client.get("/users/me") + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +def test_token(): + access_token = get_access_token(scope="me") + response = client.get( + "/users/me", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 200, response.text + assert response.json() == { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "disabled": False, + } + + +def test_incorrect_token(): + response = client.get("/users/me", headers={"Authorization": "Bearer nonexistent"}) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Could not validate credentials"} + assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' + + +def test_incorrect_token_type(): + response = client.get( + "/users/me", headers={"Authorization": "Notexistent testtoken"} + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +def test_verify_password(): + assert verify_password("secret", fake_users_db["johndoe"]["hashed_password"]) + + +def test_get_password_hash(): + assert get_password_hash("secretalice") + + +def test_create_access_token(): + access_token = create_access_token(data={"data": "foo"}) + assert access_token + + +def test_token_no_sub(): + response = client.get( + "/users/me", + headers={ + "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjoiZm9vIn0.9ynBhuYb4e6aW3oJr_K_TBgwcMTDpRToQIE25L57rOE" + }, + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Could not validate credentials"} + assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' + + +def test_token_no_username(): + response = client.get( + "/users/me", + headers={ + "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmb28ifQ.NnExK_dlNAYyzACrXtXDrcWOgGY2JuPbI4eDaHdfK5Y" + }, + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Could not validate credentials"} + assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' + + +def test_token_no_scope(): + access_token = get_access_token() + response = client.get( + "/users/me", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not enough permissions"} + assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' + + +def test_token_inexistent_user(): + response = client.get( + "/users/me", + headers={ + "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VybmFtZTpib2IifQ.HcfCW67Uda-0gz54ZWTqmtgJnZeNem0Q757eTa9EZuw" + }, + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Could not validate credentials"} + assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' + + +def test_token_inactive_user(): + access_token = get_access_token( + username="alice", password="secretalice", scope="me" + ) + response = client.get( + "/users/me", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Inactive user"} + + +def test_read_items(): + access_token = get_access_token(scope="me items") + response = client.get( + "/users/me/items/", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 200, response.text + assert response.json() == [{"item_id": "Foo", "owner": "johndoe"}] + + +def test_read_system_status(): + access_token = get_access_token() + response = client.get( + "/status/", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 200, response.text + assert response.json() == {"status": "ok"} + + +def test_read_system_status_no_token(): + response = client.get("/status/") + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" diff --git a/tests/test_tutorial/test_security/test_tutorial005_an_py310.py b/tests/test_tutorial/test_security/test_tutorial005_an_py310.py new file mode 100644 index 000000000..15a9445b9 --- /dev/null +++ b/tests/test_tutorial/test_security/test_tutorial005_an_py310.py @@ -0,0 +1,375 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/token": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Token"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Login For Access Token", + "operationId": "login_for_access_token_token_post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Body_login_for_access_token_token_post" + } + } + }, + "required": True, + }, + } + }, + "/users/me/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + "summary": "Read Users Me", + "operationId": "read_users_me_users_me__get", + "security": [{"OAuth2PasswordBearer": ["me"]}], + } + }, + "/users/me/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Own Items", + "operationId": "read_own_items_users_me_items__get", + "security": [{"OAuth2PasswordBearer": ["items", "me"]}], + } + }, + "/status/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read System Status", + "operationId": "read_system_status_status__get", + "security": [{"OAuth2PasswordBearer": []}], + } + }, + }, + "components": { + "schemas": { + "User": { + "title": "User", + "required": ["username"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "email": {"title": "Email", "type": "string"}, + "full_name": {"title": "Full Name", "type": "string"}, + "disabled": {"title": "Disabled", "type": "boolean"}, + }, + }, + "Token": { + "title": "Token", + "required": ["access_token", "token_type"], + "type": "object", + "properties": { + "access_token": {"title": "Access Token", "type": "string"}, + "token_type": {"title": "Token Type", "type": "string"}, + }, + }, + "Body_login_for_access_token_token_post": { + "title": "Body_login_for_access_token_token_post", + "required": ["username", "password"], + "type": "object", + "properties": { + "grant_type": { + "title": "Grant Type", + "pattern": "password", + "type": "string", + }, + "username": {"title": "Username", "type": "string"}, + "password": {"title": "Password", "type": "string"}, + "scope": {"title": "Scope", "type": "string", "default": ""}, + "client_id": {"title": "Client Id", "type": "string"}, + "client_secret": {"title": "Client Secret", "type": "string"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + }, + "securitySchemes": { + "OAuth2PasswordBearer": { + "type": "oauth2", + "flows": { + "password": { + "scopes": { + "me": "Read information about the current user.", + "items": "Read items.", + }, + "tokenUrl": "token", + } + }, + } + }, + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.security.tutorial005_an_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def get_access_token( + *, username="johndoe", password="secret", scope=None, client: TestClient +): + data = {"username": username, "password": password} + if scope: + data["scope"] = scope + response = client.post("/token", data=data) + content = response.json() + access_token = content.get("access_token") + return access_token + + +@needs_py310 +def test_login(client: TestClient): + response = client.post("/token", data={"username": "johndoe", "password": "secret"}) + assert response.status_code == 200, response.text + content = response.json() + assert "access_token" in content + assert content["token_type"] == "bearer" + + +@needs_py310 +def test_login_incorrect_password(client: TestClient): + response = client.post( + "/token", data={"username": "johndoe", "password": "incorrect"} + ) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Incorrect username or password"} + + +@needs_py310 +def test_login_incorrect_username(client: TestClient): + response = client.post("/token", data={"username": "foo", "password": "secret"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Incorrect username or password"} + + +@needs_py310 +def test_no_token(client: TestClient): + response = client.get("/users/me") + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +@needs_py310 +def test_token(client: TestClient): + access_token = get_access_token(scope="me", client=client) + response = client.get( + "/users/me", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 200, response.text + assert response.json() == { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "disabled": False, + } + + +@needs_py310 +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": "Could not validate credentials"} + assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' + + +@needs_py310 +def test_incorrect_token_type(client: TestClient): + response = client.get( + "/users/me", headers={"Authorization": "Notexistent testtoken"} + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +@needs_py310 +def test_verify_password(): + from docs_src.security.tutorial005_an_py310 import fake_users_db, verify_password + + assert verify_password("secret", fake_users_db["johndoe"]["hashed_password"]) + + +@needs_py310 +def test_get_password_hash(): + from docs_src.security.tutorial005_an_py310 import get_password_hash + + assert get_password_hash("secretalice") + + +@needs_py310 +def test_create_access_token(): + from docs_src.security.tutorial005_an_py310 import create_access_token + + access_token = create_access_token(data={"data": "foo"}) + assert access_token + + +@needs_py310 +def test_token_no_sub(client: TestClient): + response = client.get( + "/users/me", + headers={ + "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjoiZm9vIn0.9ynBhuYb4e6aW3oJr_K_TBgwcMTDpRToQIE25L57rOE" + }, + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Could not validate credentials"} + assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' + + +@needs_py310 +def test_token_no_username(client: TestClient): + response = client.get( + "/users/me", + headers={ + "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmb28ifQ.NnExK_dlNAYyzACrXtXDrcWOgGY2JuPbI4eDaHdfK5Y" + }, + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Could not validate credentials"} + assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' + + +@needs_py310 +def test_token_no_scope(client: TestClient): + access_token = get_access_token(client=client) + response = client.get( + "/users/me", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not enough permissions"} + assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' + + +@needs_py310 +def test_token_inexistent_user(client: TestClient): + response = client.get( + "/users/me", + headers={ + "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VybmFtZTpib2IifQ.HcfCW67Uda-0gz54ZWTqmtgJnZeNem0Q757eTa9EZuw" + }, + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Could not validate credentials"} + assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' + + +@needs_py310 +def test_token_inactive_user(client: TestClient): + access_token = get_access_token( + username="alice", password="secretalice", scope="me", client=client + ) + response = client.get( + "/users/me", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Inactive user"} + + +@needs_py310 +def test_read_items(client: TestClient): + access_token = get_access_token(scope="me items", client=client) + response = client.get( + "/users/me/items/", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 200, response.text + assert response.json() == [{"item_id": "Foo", "owner": "johndoe"}] + + +@needs_py310 +def test_read_system_status(client: TestClient): + access_token = get_access_token(client=client) + response = client.get( + "/status/", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 200, response.text + assert response.json() == {"status": "ok"} + + +@needs_py310 +def test_read_system_status_no_token(client: TestClient): + response = client.get("/status/") + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" diff --git a/tests/test_tutorial/test_security/test_tutorial005_an_py39.py b/tests/test_tutorial/test_security/test_tutorial005_an_py39.py new file mode 100644 index 000000000..989424dd3 --- /dev/null +++ b/tests/test_tutorial/test_security/test_tutorial005_an_py39.py @@ -0,0 +1,375 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/token": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Token"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Login For Access Token", + "operationId": "login_for_access_token_token_post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Body_login_for_access_token_token_post" + } + } + }, + "required": True, + }, + } + }, + "/users/me/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + "summary": "Read Users Me", + "operationId": "read_users_me_users_me__get", + "security": [{"OAuth2PasswordBearer": ["me"]}], + } + }, + "/users/me/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Own Items", + "operationId": "read_own_items_users_me_items__get", + "security": [{"OAuth2PasswordBearer": ["items", "me"]}], + } + }, + "/status/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read System Status", + "operationId": "read_system_status_status__get", + "security": [{"OAuth2PasswordBearer": []}], + } + }, + }, + "components": { + "schemas": { + "User": { + "title": "User", + "required": ["username"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "email": {"title": "Email", "type": "string"}, + "full_name": {"title": "Full Name", "type": "string"}, + "disabled": {"title": "Disabled", "type": "boolean"}, + }, + }, + "Token": { + "title": "Token", + "required": ["access_token", "token_type"], + "type": "object", + "properties": { + "access_token": {"title": "Access Token", "type": "string"}, + "token_type": {"title": "Token Type", "type": "string"}, + }, + }, + "Body_login_for_access_token_token_post": { + "title": "Body_login_for_access_token_token_post", + "required": ["username", "password"], + "type": "object", + "properties": { + "grant_type": { + "title": "Grant Type", + "pattern": "password", + "type": "string", + }, + "username": {"title": "Username", "type": "string"}, + "password": {"title": "Password", "type": "string"}, + "scope": {"title": "Scope", "type": "string", "default": ""}, + "client_id": {"title": "Client Id", "type": "string"}, + "client_secret": {"title": "Client Secret", "type": "string"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + }, + "securitySchemes": { + "OAuth2PasswordBearer": { + "type": "oauth2", + "flows": { + "password": { + "scopes": { + "me": "Read information about the current user.", + "items": "Read items.", + }, + "tokenUrl": "token", + } + }, + } + }, + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.security.tutorial005_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def get_access_token( + *, username="johndoe", password="secret", scope=None, client: TestClient +): + data = {"username": username, "password": password} + if scope: + data["scope"] = scope + response = client.post("/token", data=data) + content = response.json() + access_token = content.get("access_token") + return access_token + + +@needs_py39 +def test_login(client: TestClient): + response = client.post("/token", data={"username": "johndoe", "password": "secret"}) + assert response.status_code == 200, response.text + content = response.json() + assert "access_token" in content + assert content["token_type"] == "bearer" + + +@needs_py39 +def test_login_incorrect_password(client: TestClient): + response = client.post( + "/token", data={"username": "johndoe", "password": "incorrect"} + ) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Incorrect username or password"} + + +@needs_py39 +def test_login_incorrect_username(client: TestClient): + response = client.post("/token", data={"username": "foo", "password": "secret"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Incorrect username or password"} + + +@needs_py39 +def test_no_token(client: TestClient): + response = client.get("/users/me") + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +@needs_py39 +def test_token(client: TestClient): + access_token = get_access_token(scope="me", client=client) + response = client.get( + "/users/me", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 200, response.text + assert response.json() == { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "disabled": False, + } + + +@needs_py39 +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": "Could not validate credentials"} + assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' + + +@needs_py39 +def test_incorrect_token_type(client: TestClient): + response = client.get( + "/users/me", headers={"Authorization": "Notexistent testtoken"} + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +@needs_py39 +def test_verify_password(): + from docs_src.security.tutorial005_an_py39 import fake_users_db, verify_password + + assert verify_password("secret", fake_users_db["johndoe"]["hashed_password"]) + + +@needs_py39 +def test_get_password_hash(): + from docs_src.security.tutorial005_an_py39 import get_password_hash + + assert get_password_hash("secretalice") + + +@needs_py39 +def test_create_access_token(): + from docs_src.security.tutorial005_an_py39 import create_access_token + + access_token = create_access_token(data={"data": "foo"}) + assert access_token + + +@needs_py39 +def test_token_no_sub(client: TestClient): + response = client.get( + "/users/me", + headers={ + "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjoiZm9vIn0.9ynBhuYb4e6aW3oJr_K_TBgwcMTDpRToQIE25L57rOE" + }, + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Could not validate credentials"} + assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' + + +@needs_py39 +def test_token_no_username(client: TestClient): + response = client.get( + "/users/me", + headers={ + "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmb28ifQ.NnExK_dlNAYyzACrXtXDrcWOgGY2JuPbI4eDaHdfK5Y" + }, + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Could not validate credentials"} + assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' + + +@needs_py39 +def test_token_no_scope(client: TestClient): + access_token = get_access_token(client=client) + response = client.get( + "/users/me", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not enough permissions"} + assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' + + +@needs_py39 +def test_token_inexistent_user(client: TestClient): + response = client.get( + "/users/me", + headers={ + "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VybmFtZTpib2IifQ.HcfCW67Uda-0gz54ZWTqmtgJnZeNem0Q757eTa9EZuw" + }, + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Could not validate credentials"} + assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' + + +@needs_py39 +def test_token_inactive_user(client: TestClient): + access_token = get_access_token( + username="alice", password="secretalice", scope="me", client=client + ) + response = client.get( + "/users/me", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Inactive user"} + + +@needs_py39 +def test_read_items(client: TestClient): + access_token = get_access_token(scope="me items", client=client) + response = client.get( + "/users/me/items/", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 200, response.text + assert response.json() == [{"item_id": "Foo", "owner": "johndoe"}] + + +@needs_py39 +def test_read_system_status(client: TestClient): + access_token = get_access_token(client=client) + response = client.get( + "/status/", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 200, response.text + assert response.json() == {"status": "ok"} + + +@needs_py39 +def test_read_system_status_no_token(client: TestClient): + response = client.get("/status/") + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" diff --git a/tests/test_tutorial/test_security/test_tutorial006_an.py b/tests/test_tutorial/test_security/test_tutorial006_an.py new file mode 100644 index 000000000..1d1668fec --- /dev/null +++ b/tests/test_tutorial/test_security/test_tutorial006_an.py @@ -0,0 +1,67 @@ +from base64 import b64encode + +from fastapi.testclient import TestClient + +from docs_src.security.tutorial006_an import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Current User", + "operationId": "read_current_user_users_me_get", + "security": [{"HTTPBasic": []}], + } + } + }, + "components": { + "securitySchemes": {"HTTPBasic": {"type": "http", "scheme": "basic"}} + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def test_security_http_basic(): + response = client.get("/users/me", auth=("john", "secret")) + assert response.status_code == 200, response.text + assert response.json() == {"username": "john", "password": "secret"} + + +def test_security_http_basic_no_credentials(): + response = client.get("/users/me") + assert response.json() == {"detail": "Not authenticated"} + assert response.status_code == 401, response.text + assert response.headers["WWW-Authenticate"] == "Basic" + + +def test_security_http_basic_invalid_credentials(): + response = client.get( + "/users/me", headers={"Authorization": "Basic notabase64token"} + ) + assert response.status_code == 401, response.text + assert response.headers["WWW-Authenticate"] == "Basic" + assert response.json() == {"detail": "Invalid authentication credentials"} + + +def test_security_http_basic_non_basic_credentials(): + payload = b64encode(b"johnsecret").decode("ascii") + auth_header = f"Basic {payload}" + 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"} diff --git a/tests/test_tutorial/test_security/test_tutorial006_an_py39.py b/tests/test_tutorial/test_security/test_tutorial006_an_py39.py new file mode 100644 index 000000000..b72b5d864 --- /dev/null +++ b/tests/test_tutorial/test_security/test_tutorial006_an_py39.py @@ -0,0 +1,79 @@ +from base64 import b64encode + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Current User", + "operationId": "read_current_user_users_me_get", + "security": [{"HTTPBasic": []}], + } + } + }, + "components": { + "securitySchemes": {"HTTPBasic": {"type": "http", "scheme": "basic"}} + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.security.tutorial006_an import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py39 +def test_security_http_basic(client: TestClient): + response = client.get("/users/me", auth=("john", "secret")) + assert response.status_code == 200, response.text + assert response.json() == {"username": "john", "password": "secret"} + + +@needs_py39 +def test_security_http_basic_no_credentials(client: TestClient): + response = client.get("/users/me") + assert response.json() == {"detail": "Not authenticated"} + assert response.status_code == 401, response.text + assert response.headers["WWW-Authenticate"] == "Basic" + + +@needs_py39 +def test_security_http_basic_invalid_credentials(client: TestClient): + response = client.get( + "/users/me", headers={"Authorization": "Basic notabase64token"} + ) + assert response.status_code == 401, response.text + assert response.headers["WWW-Authenticate"] == "Basic" + assert response.json() == {"detail": "Invalid authentication credentials"} + + +@needs_py39 +def test_security_http_basic_non_basic_credentials(client: TestClient): + payload = b64encode(b"johnsecret").decode("ascii") + auth_header = f"Basic {payload}" + 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"} diff --git a/tests/test_tutorial/test_testing/test_main_b_an.py b/tests/test_tutorial/test_testing/test_main_b_an.py new file mode 100644 index 000000000..b64c5f710 --- /dev/null +++ b/tests/test_tutorial/test_testing/test_main_b_an.py @@ -0,0 +1,10 @@ +from docs_src.app_testing.app_b_an import test_main + + +def test_app(): + test_main.test_create_existing_item() + test_main.test_create_item() + test_main.test_create_item_bad_token() + test_main.test_read_inexistent_item() + test_main.test_read_item() + test_main.test_read_item_bad_token() diff --git a/tests/test_tutorial/test_testing/test_main_b_an_py310.py b/tests/test_tutorial/test_testing/test_main_b_an_py310.py new file mode 100644 index 000000000..194700b6d --- /dev/null +++ b/tests/test_tutorial/test_testing/test_main_b_an_py310.py @@ -0,0 +1,13 @@ +from ...utils import needs_py310 + + +@needs_py310 +def test_app(): + from docs_src.app_testing.app_b_an_py310 import test_main + + test_main.test_create_existing_item() + test_main.test_create_item() + test_main.test_create_item_bad_token() + test_main.test_read_inexistent_item() + test_main.test_read_item() + test_main.test_read_item_bad_token() diff --git a/tests/test_tutorial/test_testing/test_main_b_an_py39.py b/tests/test_tutorial/test_testing/test_main_b_an_py39.py new file mode 100644 index 000000000..2f8a13623 --- /dev/null +++ b/tests/test_tutorial/test_testing/test_main_b_an_py39.py @@ -0,0 +1,13 @@ +from ...utils import needs_py39 + + +@needs_py39 +def test_app(): + from docs_src.app_testing.app_b_an_py39 import test_main + + test_main.test_create_existing_item() + test_main.test_create_item() + test_main.test_create_item_bad_token() + test_main.test_read_inexistent_item() + test_main.test_read_item() + test_main.test_read_item_bad_token() diff --git a/tests/test_tutorial/test_testing_dependencies/test_tutorial001_an.py b/tests/test_tutorial/test_testing_dependencies/test_tutorial001_an.py new file mode 100644 index 000000000..fc1f9149a --- /dev/null +++ b/tests/test_tutorial/test_testing_dependencies/test_tutorial001_an.py @@ -0,0 +1,56 @@ +from docs_src.dependency_testing.tutorial001_an import ( + app, + client, + test_override_in_items, + test_override_in_items_with_params, + test_override_in_items_with_q, +) + + +def test_override_in_items_run(): + test_override_in_items() + + +def test_override_in_items_with_q_run(): + test_override_in_items_with_q() + + +def test_override_in_items_with_params_run(): + test_override_in_items_with_params() + + +def test_override_in_users(): + response = client.get("/users/") + assert response.status_code == 200, response.text + assert response.json() == { + "message": "Hello Users!", + "params": {"q": None, "skip": 5, "limit": 10}, + } + + +def test_override_in_users_with_q(): + response = client.get("/users/?q=foo") + assert response.status_code == 200, response.text + assert response.json() == { + "message": "Hello Users!", + "params": {"q": "foo", "skip": 5, "limit": 10}, + } + + +def test_override_in_users_with_params(): + response = client.get("/users/?q=foo&skip=100&limit=200") + assert response.status_code == 200, response.text + assert response.json() == { + "message": "Hello Users!", + "params": {"q": "foo", "skip": 5, "limit": 10}, + } + + +def test_normal_app(): + app.dependency_overrides = None + response = client.get("/items/?q=foo&skip=100&limit=200") + assert response.status_code == 200, response.text + assert response.json() == { + "message": "Hello Items!", + "params": {"q": "foo", "skip": 100, "limit": 200}, + } diff --git a/tests/test_tutorial/test_testing_dependencies/test_tutorial001_an_py310.py b/tests/test_tutorial/test_testing_dependencies/test_tutorial001_an_py310.py new file mode 100644 index 000000000..a3d27f47f --- /dev/null +++ b/tests/test_tutorial/test_testing_dependencies/test_tutorial001_an_py310.py @@ -0,0 +1,75 @@ +from ...utils import needs_py310 + + +@needs_py310 +def test_override_in_items_run(): + from docs_src.dependency_testing.tutorial001_an_py310 import test_override_in_items + + test_override_in_items() + + +@needs_py310 +def test_override_in_items_with_q_run(): + from docs_src.dependency_testing.tutorial001_an_py310 import ( + test_override_in_items_with_q, + ) + + test_override_in_items_with_q() + + +@needs_py310 +def test_override_in_items_with_params_run(): + from docs_src.dependency_testing.tutorial001_an_py310 import ( + test_override_in_items_with_params, + ) + + test_override_in_items_with_params() + + +@needs_py310 +def test_override_in_users(): + from docs_src.dependency_testing.tutorial001_an_py310 import client + + response = client.get("/users/") + assert response.status_code == 200, response.text + assert response.json() == { + "message": "Hello Users!", + "params": {"q": None, "skip": 5, "limit": 10}, + } + + +@needs_py310 +def test_override_in_users_with_q(): + from docs_src.dependency_testing.tutorial001_an_py310 import client + + response = client.get("/users/?q=foo") + assert response.status_code == 200, response.text + assert response.json() == { + "message": "Hello Users!", + "params": {"q": "foo", "skip": 5, "limit": 10}, + } + + +@needs_py310 +def test_override_in_users_with_params(): + from docs_src.dependency_testing.tutorial001_an_py310 import client + + response = client.get("/users/?q=foo&skip=100&limit=200") + assert response.status_code == 200, response.text + assert response.json() == { + "message": "Hello Users!", + "params": {"q": "foo", "skip": 5, "limit": 10}, + } + + +@needs_py310 +def test_normal_app(): + from docs_src.dependency_testing.tutorial001_an_py310 import app, client + + app.dependency_overrides = None + response = client.get("/items/?q=foo&skip=100&limit=200") + assert response.status_code == 200, response.text + assert response.json() == { + "message": "Hello Items!", + "params": {"q": "foo", "skip": 100, "limit": 200}, + } diff --git a/tests/test_tutorial/test_testing_dependencies/test_tutorial001_an_py39.py b/tests/test_tutorial/test_testing_dependencies/test_tutorial001_an_py39.py new file mode 100644 index 000000000..f03ed5e07 --- /dev/null +++ b/tests/test_tutorial/test_testing_dependencies/test_tutorial001_an_py39.py @@ -0,0 +1,75 @@ +from ...utils import needs_py39 + + +@needs_py39 +def test_override_in_items_run(): + from docs_src.dependency_testing.tutorial001_an_py39 import test_override_in_items + + test_override_in_items() + + +@needs_py39 +def test_override_in_items_with_q_run(): + from docs_src.dependency_testing.tutorial001_an_py39 import ( + test_override_in_items_with_q, + ) + + test_override_in_items_with_q() + + +@needs_py39 +def test_override_in_items_with_params_run(): + from docs_src.dependency_testing.tutorial001_an_py39 import ( + test_override_in_items_with_params, + ) + + test_override_in_items_with_params() + + +@needs_py39 +def test_override_in_users(): + from docs_src.dependency_testing.tutorial001_an_py39 import client + + response = client.get("/users/") + assert response.status_code == 200, response.text + assert response.json() == { + "message": "Hello Users!", + "params": {"q": None, "skip": 5, "limit": 10}, + } + + +@needs_py39 +def test_override_in_users_with_q(): + from docs_src.dependency_testing.tutorial001_an_py39 import client + + response = client.get("/users/?q=foo") + assert response.status_code == 200, response.text + assert response.json() == { + "message": "Hello Users!", + "params": {"q": "foo", "skip": 5, "limit": 10}, + } + + +@needs_py39 +def test_override_in_users_with_params(): + from docs_src.dependency_testing.tutorial001_an_py39 import client + + response = client.get("/users/?q=foo&skip=100&limit=200") + assert response.status_code == 200, response.text + assert response.json() == { + "message": "Hello Users!", + "params": {"q": "foo", "skip": 5, "limit": 10}, + } + + +@needs_py39 +def test_normal_app(): + from docs_src.dependency_testing.tutorial001_an_py39 import app, client + + app.dependency_overrides = None + response = client.get("/items/?q=foo&skip=100&limit=200") + assert response.status_code == 200, response.text + assert response.json() == { + "message": "Hello Items!", + "params": {"q": "foo", "skip": 100, "limit": 200}, + } diff --git a/tests/test_tutorial/test_testing_dependencies/test_tutorial001_py310.py b/tests/test_tutorial/test_testing_dependencies/test_tutorial001_py310.py new file mode 100644 index 000000000..776b916ff --- /dev/null +++ b/tests/test_tutorial/test_testing_dependencies/test_tutorial001_py310.py @@ -0,0 +1,75 @@ +from ...utils import needs_py310 + + +@needs_py310 +def test_override_in_items_run(): + from docs_src.dependency_testing.tutorial001_py310 import test_override_in_items + + test_override_in_items() + + +@needs_py310 +def test_override_in_items_with_q_run(): + from docs_src.dependency_testing.tutorial001_py310 import ( + test_override_in_items_with_q, + ) + + test_override_in_items_with_q() + + +@needs_py310 +def test_override_in_items_with_params_run(): + from docs_src.dependency_testing.tutorial001_py310 import ( + test_override_in_items_with_params, + ) + + test_override_in_items_with_params() + + +@needs_py310 +def test_override_in_users(): + from docs_src.dependency_testing.tutorial001_py310 import client + + response = client.get("/users/") + assert response.status_code == 200, response.text + assert response.json() == { + "message": "Hello Users!", + "params": {"q": None, "skip": 5, "limit": 10}, + } + + +@needs_py310 +def test_override_in_users_with_q(): + from docs_src.dependency_testing.tutorial001_py310 import client + + response = client.get("/users/?q=foo") + assert response.status_code == 200, response.text + assert response.json() == { + "message": "Hello Users!", + "params": {"q": "foo", "skip": 5, "limit": 10}, + } + + +@needs_py310 +def test_override_in_users_with_params(): + from docs_src.dependency_testing.tutorial001_py310 import client + + response = client.get("/users/?q=foo&skip=100&limit=200") + assert response.status_code == 200, response.text + assert response.json() == { + "message": "Hello Users!", + "params": {"q": "foo", "skip": 5, "limit": 10}, + } + + +@needs_py310 +def test_normal_app(): + from docs_src.dependency_testing.tutorial001_py310 import app, client + + app.dependency_overrides = None + response = client.get("/items/?q=foo&skip=100&limit=200") + assert response.status_code == 200, response.text + assert response.json() == { + "message": "Hello Items!", + "params": {"q": "foo", "skip": 100, "limit": 200}, + } diff --git a/tests/test_tutorial/test_websockets/test_tutorial002_an.py b/tests/test_tutorial/test_websockets/test_tutorial002_an.py new file mode 100644 index 000000000..ec78d70d3 --- /dev/null +++ b/tests/test_tutorial/test_websockets/test_tutorial002_an.py @@ -0,0 +1,88 @@ +import pytest +from fastapi.testclient import TestClient +from fastapi.websockets import WebSocketDisconnect + +from docs_src.websockets.tutorial002_an import app + + +def test_main(): + client = TestClient(app) + response = client.get("/") + assert response.status_code == 200, response.text + assert b"" in response.content + + +def test_websocket_with_cookie(): + client = TestClient(app, cookies={"session": "fakesession"}) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/foo/ws") as websocket: + message = "Message one" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: fakesession" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: foo" + message = "Message two" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: fakesession" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: foo" + + +def test_websocket_with_header(): + client = TestClient(app) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/bar/ws?token=some-token") as websocket: + message = "Message one" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: some-token" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: bar" + message = "Message two" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: some-token" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: bar" + + +def test_websocket_with_header_and_query(): + client = TestClient(app) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/2/ws?q=3&token=some-token") as websocket: + message = "Message one" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: some-token" + data = websocket.receive_text() + assert data == "Query parameter q is: 3" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: 2" + message = "Message two" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: some-token" + data = websocket.receive_text() + assert data == "Query parameter q is: 3" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: 2" + + +def test_websocket_no_credentials(): + client = TestClient(app) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/foo/ws"): + pytest.fail( + "did not raise WebSocketDisconnect on __enter__" + ) # pragma: no cover + + +def test_websocket_invalid_data(): + client = TestClient(app) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/foo/ws?q=bar&token=some-token"): + pytest.fail( + "did not raise WebSocketDisconnect on __enter__" + ) # pragma: no cover diff --git a/tests/test_tutorial/test_websockets/test_tutorial002_an_py310.py b/tests/test_tutorial/test_websockets/test_tutorial002_an_py310.py new file mode 100644 index 000000000..23b4bcb78 --- /dev/null +++ b/tests/test_tutorial/test_websockets/test_tutorial002_an_py310.py @@ -0,0 +1,102 @@ +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient +from fastapi.websockets import WebSocketDisconnect + +from ...utils import needs_py310 + + +@pytest.fixture(name="app") +def get_app(): + from docs_src.websockets.tutorial002_an_py310 import app + + return app + + +@needs_py310 +def test_main(app: FastAPI): + client = TestClient(app) + response = client.get("/") + assert response.status_code == 200, response.text + assert b"" in response.content + + +@needs_py310 +def test_websocket_with_cookie(app: FastAPI): + client = TestClient(app, cookies={"session": "fakesession"}) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/foo/ws") as websocket: + message = "Message one" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: fakesession" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: foo" + message = "Message two" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: fakesession" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: foo" + + +@needs_py310 +def test_websocket_with_header(app: FastAPI): + client = TestClient(app) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/bar/ws?token=some-token") as websocket: + message = "Message one" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: some-token" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: bar" + message = "Message two" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: some-token" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: bar" + + +@needs_py310 +def test_websocket_with_header_and_query(app: FastAPI): + client = TestClient(app) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/2/ws?q=3&token=some-token") as websocket: + message = "Message one" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: some-token" + data = websocket.receive_text() + assert data == "Query parameter q is: 3" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: 2" + message = "Message two" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: some-token" + data = websocket.receive_text() + assert data == "Query parameter q is: 3" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: 2" + + +@needs_py310 +def test_websocket_no_credentials(app: FastAPI): + client = TestClient(app) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/foo/ws"): + pytest.fail( + "did not raise WebSocketDisconnect on __enter__" + ) # pragma: no cover + + +@needs_py310 +def test_websocket_invalid_data(app: FastAPI): + client = TestClient(app) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/foo/ws?q=bar&token=some-token"): + pytest.fail( + "did not raise WebSocketDisconnect on __enter__" + ) # pragma: no cover diff --git a/tests/test_tutorial/test_websockets/test_tutorial002_an_py39.py b/tests/test_tutorial/test_websockets/test_tutorial002_an_py39.py new file mode 100644 index 000000000..2d77f05b3 --- /dev/null +++ b/tests/test_tutorial/test_websockets/test_tutorial002_an_py39.py @@ -0,0 +1,102 @@ +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient +from fastapi.websockets import WebSocketDisconnect + +from ...utils import needs_py39 + + +@pytest.fixture(name="app") +def get_app(): + from docs_src.websockets.tutorial002_an_py39 import app + + return app + + +@needs_py39 +def test_main(app: FastAPI): + client = TestClient(app) + response = client.get("/") + assert response.status_code == 200, response.text + assert b"" in response.content + + +@needs_py39 +def test_websocket_with_cookie(app: FastAPI): + client = TestClient(app, cookies={"session": "fakesession"}) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/foo/ws") as websocket: + message = "Message one" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: fakesession" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: foo" + message = "Message two" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: fakesession" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: foo" + + +@needs_py39 +def test_websocket_with_header(app: FastAPI): + client = TestClient(app) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/bar/ws?token=some-token") as websocket: + message = "Message one" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: some-token" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: bar" + message = "Message two" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: some-token" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: bar" + + +@needs_py39 +def test_websocket_with_header_and_query(app: FastAPI): + client = TestClient(app) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/2/ws?q=3&token=some-token") as websocket: + message = "Message one" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: some-token" + data = websocket.receive_text() + assert data == "Query parameter q is: 3" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: 2" + message = "Message two" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: some-token" + data = websocket.receive_text() + assert data == "Query parameter q is: 3" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: 2" + + +@needs_py39 +def test_websocket_no_credentials(app: FastAPI): + client = TestClient(app) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/foo/ws"): + pytest.fail( + "did not raise WebSocketDisconnect on __enter__" + ) # pragma: no cover + + +@needs_py39 +def test_websocket_invalid_data(app: FastAPI): + client = TestClient(app) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/foo/ws?q=bar&token=some-token"): + pytest.fail( + "did not raise WebSocketDisconnect on __enter__" + ) # pragma: no cover diff --git a/tests/test_tutorial/test_websockets/test_tutorial002_py310.py b/tests/test_tutorial/test_websockets/test_tutorial002_py310.py new file mode 100644 index 000000000..03bc27bdf --- /dev/null +++ b/tests/test_tutorial/test_websockets/test_tutorial002_py310.py @@ -0,0 +1,102 @@ +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient +from fastapi.websockets import WebSocketDisconnect + +from ...utils import needs_py310 + + +@pytest.fixture(name="app") +def get_app(): + from docs_src.websockets.tutorial002_py310 import app + + return app + + +@needs_py310 +def test_main(app: FastAPI): + client = TestClient(app) + response = client.get("/") + assert response.status_code == 200, response.text + assert b"" in response.content + + +@needs_py310 +def test_websocket_with_cookie(app: FastAPI): + client = TestClient(app, cookies={"session": "fakesession"}) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/foo/ws") as websocket: + message = "Message one" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: fakesession" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: foo" + message = "Message two" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: fakesession" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: foo" + + +@needs_py310 +def test_websocket_with_header(app: FastAPI): + client = TestClient(app) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/bar/ws?token=some-token") as websocket: + message = "Message one" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: some-token" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: bar" + message = "Message two" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: some-token" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: bar" + + +@needs_py310 +def test_websocket_with_header_and_query(app: FastAPI): + client = TestClient(app) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/2/ws?q=3&token=some-token") as websocket: + message = "Message one" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: some-token" + data = websocket.receive_text() + assert data == "Query parameter q is: 3" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: 2" + message = "Message two" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: some-token" + data = websocket.receive_text() + assert data == "Query parameter q is: 3" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: 2" + + +@needs_py310 +def test_websocket_no_credentials(app: FastAPI): + client = TestClient(app) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/foo/ws"): + pytest.fail( + "did not raise WebSocketDisconnect on __enter__" + ) # pragma: no cover + + +@needs_py310 +def test_websocket_invalid_data(app: FastAPI): + client = TestClient(app) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/foo/ws?q=bar&token=some-token"): + pytest.fail( + "did not raise WebSocketDisconnect on __enter__" + ) # pragma: no cover diff --git a/tests/test_tutorial/test_websockets/test_tutorial003_py39.py b/tests/test_tutorial/test_websockets/test_tutorial003_py39.py new file mode 100644 index 000000000..06c4a9279 --- /dev/null +++ b/tests/test_tutorial/test_websockets/test_tutorial003_py39.py @@ -0,0 +1,50 @@ +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + + +@pytest.fixture(name="app") +def get_app(): + from docs_src.websockets.tutorial003_py39 import app + + return app + + +@pytest.fixture(name="html") +def get_html(): + from docs_src.websockets.tutorial003_py39 import html + + return html + + +@pytest.fixture(name="client") +def get_client(app: FastAPI): + client = TestClient(app) + + return client + + +@needs_py39 +def test_get(client: TestClient, html: str): + response = client.get("/") + assert response.text == html + + +@needs_py39 +def test_websocket_handle_disconnection(client: TestClient): + with client.websocket_connect("/ws/1234") as connection, client.websocket_connect( + "/ws/5678" + ) as connection_two: + connection.send_text("Hello from 1234") + data1 = connection.receive_text() + assert data1 == "You wrote: Hello from 1234" + data2 = connection_two.receive_text() + client1_says = "Client #1234 says: Hello from 1234" + assert data2 == client1_says + data1 = connection.receive_text() + assert data1 == client1_says + connection_two.close() + data1 = connection.receive_text() + assert data1 == "Client #5678 left the chat" diff --git a/tests/test_typing_python39.py b/tests/test_typing_python39.py index b1ea635e7..26775efd4 100644 --- a/tests/test_typing_python39.py +++ b/tests/test_typing_python39.py @@ -1,10 +1,10 @@ from fastapi import FastAPI from fastapi.testclient import TestClient -from .utils import needs_py39 +from .utils import needs_py310 -@needs_py39 +@needs_py310 def test_typing(): types = { list[int]: [1, 2, 3],