diff --git a/.github/labeler.yml b/.github/labeler.yml index 1d49a2411..c5b1f84f3 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -7,6 +7,8 @@ docs: - all-globs-to-all-files: - '!fastapi/**' - '!pyproject.toml' + - '!docs/en/data/sponsors.yml' + - '!docs/en/overrides/main.html' lang-all: - all: @@ -28,6 +30,8 @@ internal: - .pre-commit-config.yaml - pdm_build.py - requirements*.txt + - docs/en/data/sponsors.yml + - docs/en/overrides/main.html - all-globs-to-all-files: - '!docs/*/docs/**' - '!fastapi/**' diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index e46629e9b..52c34a49e 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -113,6 +113,7 @@ jobs: with: name: docs-site-${{ matrix.lang }} path: ./site/** + include-hidden-files: true # https://github.com/marketplace/actions/alls-green#why docs-all-green: # This job does nothing and is only used for the branch protection diff --git a/.github/workflows/issue-manager.yml b/.github/workflows/issue-manager.yml index d5b947a9c..439084434 100644 --- a/.github/workflows/issue-manager.yml +++ b/.github/workflows/issue-manager.yml @@ -2,7 +2,7 @@ name: Issue Manager on: schedule: - - cron: "10 3 * * *" + - cron: "13 22 * * *" issue_comment: types: - created @@ -16,6 +16,7 @@ on: permissions: issues: write + pull-requests: write jobs: issue-manager: @@ -26,7 +27,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: tiangolo/issue-manager@0.5.0 + - uses: tiangolo/issue-manager@0.5.1 with: token: ${{ secrets.GITHUB_TOKEN }} config: > @@ -35,8 +36,8 @@ jobs: "delay": 864000, "message": "Assuming the original need was handled, this will be automatically closed now. But feel free to add more comments or create new issues or PRs." }, - "changes-requested": { + "waiting": { "delay": 2628000, - "message": "As this PR had requested changes to be applied but has been inactive for a while, it's now going to be closed. But if there's anyone interested, feel free to create a new PR." + "message": "As this PR has been waiting for the original user for a while but seems to be inactive, it's now going to be closed. But if there's anyone interested, feel free to create a new PR." } } diff --git a/.github/workflows/latest-changes.yml b/.github/workflows/latest-changes.yml index 27e062d09..16da3bc63 100644 --- a/.github/workflows/latest-changes.yml +++ b/.github/workflows/latest-changes.yml @@ -34,8 +34,7 @@ jobs: if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled == 'true' }} with: limit-access-to-actor: true - - uses: docker://tiangolo/latest-changes:0.3.0 - # - uses: tiangolo/latest-changes@main + - uses: tiangolo/latest-changes@0.3.1 with: token: ${{ secrets.GITHUB_TOKEN }} latest_changes_file: docs/en/docs/release-notes.md diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 591df634b..5004b94dd 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -35,7 +35,7 @@ jobs: TIANGOLO_BUILD_PACKAGE: ${{ matrix.package }} run: python -m build - name: Publish - uses: pypa/gh-action-pypi-publish@v1.9.0 + uses: pypa/gh-action-pypi-publish@v1.10.1 - name: Dump GitHub context env: GITHUB_CONTEXT: ${{ toJson(github) }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0458f83ff..fb4b083c4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -37,7 +37,7 @@ jobs: if: steps.cache.outputs.cache-hit != 'true' run: pip install -r requirements-tests.txt - name: Install Pydantic v2 - run: pip install "pydantic>=2.0.2,<3.0.0" + run: pip install --upgrade "pydantic>=2.0.2,<3.0.0" - name: Lint run: bash scripts/lint.sh @@ -79,7 +79,7 @@ jobs: run: pip install "pydantic>=1.10.0,<2.0.0" - name: Install Pydantic v2 if: matrix.pydantic-version == 'pydantic-v2' - run: pip install "pydantic>=2.0.2,<3.0.0" + run: pip install --upgrade "pydantic>=2.0.2,<3.0.0" - run: mkdir coverage - name: Test run: bash scripts/test.sh @@ -91,6 +91,7 @@ jobs: with: name: coverage-${{ matrix.python-version }}-${{ matrix.pydantic-version }} path: coverage + include-hidden-files: true coverage-combine: needs: [test] @@ -123,6 +124,7 @@ jobs: with: name: coverage-html path: htmlcov + include-hidden-files: true # https://github.com/marketplace/actions/alls-green#why check: # This job does nothing and is only used for the branch protection diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7532f21b5..4b1b10a68 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.6.1 + rev: v0.6.5 hooks: - id: ruff args: diff --git a/README.md b/README.md index b00ef6ba9..3b01b713a 100644 --- a/README.md +++ b/README.md @@ -52,9 +52,8 @@ The key features are: - + - @@ -132,6 +131,8 @@ FastAPI stands on the shoulders of giants: ## Installation +Create and activate a virtual environment and then install FastAPI: +
```console @@ -392,7 +393,7 @@ Coming back to the previous code example, **FastAPI** will: * Check if there is an optional query parameter named `q` (as in `http://127.0.0.1:8000/items/foo?q=somequery`) for `GET` requests. * As the `q` parameter is declared with `= None`, it is optional. * Without the `None` it would be required (as is the body in the case with `PUT`). -* For `PUT` requests to `/items/{item_id}`, Read the body as JSON: +* For `PUT` requests to `/items/{item_id}`, read the body as JSON: * Check that it has a required attribute `name` that should be a `str`. * Check that it has a required attribute `price` that has to be a `float`. * Check that it has an optional attribute `is_offer`, that should be a `bool`, if present. diff --git a/docs/de/docs/advanced/async-tests.md b/docs/de/docs/advanced/async-tests.md index 9f0bd4aa2..e56841faa 100644 --- a/docs/de/docs/advanced/async-tests.md +++ b/docs/de/docs/advanced/async-tests.md @@ -72,7 +72,7 @@ Beachten Sie, dass die Testfunktion jetzt `async def` ist und nicht nur `def` wi Dann kรถnnen wir einen `AsyncClient` mit der App erstellen und mit `await` asynchrone Requests an ihn senden. -```Python hl_lines="9-10" +```Python hl_lines="9-12" {!../../../docs_src/async_tests/test_main.py!} ``` diff --git a/docs/em/docs/advanced/async-tests.md b/docs/em/docs/advanced/async-tests.md index 324b4f68a..11f885fe6 100644 --- a/docs/em/docs/advanced/async-tests.md +++ b/docs/em/docs/advanced/async-tests.md @@ -72,7 +72,7 @@ $ pytest โคด๏ธ ๐Ÿ‘ฅ ๐Ÿ’ช โœ `AsyncClient` โฎ๏ธ ๐Ÿ“ฑ, & ๐Ÿ“จ ๐Ÿ” ๐Ÿ“จ โšซ๏ธ, โš™๏ธ `await`. -```Python hl_lines="9-10" +```Python hl_lines="9-12" {!../../../docs_src/async_tests/test_main.py!} ``` diff --git a/docs/en/data/external_links.yml b/docs/en/data/external_links.yml index 15f6169ee..63fd3d0cf 100644 --- a/docs/en/data/external_links.yml +++ b/docs/en/data/external_links.yml @@ -264,6 +264,14 @@ Articles: author_link: https://devonray.com link: https://devonray.com/blog/deploying-a-fastapi-project-using-aws-lambda-aurora-cdk title: Deployment using Docker, Lambda, Aurora, CDK & GH Actions + - author: Shubhendra Kushwaha + author_link: https://www.linkedin.com/in/theshubhendra/ + link: https://theshubhendra.medium.com/mastering-soft-delete-advanced-sqlalchemy-techniques-4678f4738947 + title: 'Mastering Soft Delete: Advanced SQLAlchemy Techniques' + - author: Shubhendra Kushwaha + author_link: https://www.linkedin.com/in/theshubhendra/ + link: https://theshubhendra.medium.com/role-based-row-filtering-advanced-sqlalchemy-techniques-733e6b1328f6 + title: 'Role based row filtering: Advanced SQLAlchemy Techniques' German: - author: Marcel Sander (actidoo) author_link: https://www.actidoo.com diff --git a/docs/en/data/sponsors.yml b/docs/en/data/sponsors.yml index 8c0956ac5..d96646fb3 100644 --- a/docs/en/data/sponsors.yml +++ b/docs/en/data/sponsors.yml @@ -17,15 +17,12 @@ gold: - url: https://www.propelauth.com/?utm_source=fastapi&utm_campaign=1223&utm_medium=mainbadge title: Auth, user management and more for your B2B product img: https://fastapi.tiangolo.com/img/sponsors/propelauth.png - - url: https://docs.withcoherence.com/configuration/frameworks/?utm_medium=advertising&utm_source=fastapi&utm_campaign=docs#fastapi-example + - url: https://www.withcoherence.com/?utm_medium=advertising&utm_source=fastapi&utm_campaign=website title: Coherence img: https://fastapi.tiangolo.com/img/sponsors/coherence.png - url: https://www.mongodb.com/developer/languages/python/python-quickstart-fastapi/?utm_campaign=fastapi_framework&utm_source=fastapi_sponsorship&utm_medium=web_referral title: Simplify Full Stack Development with FastAPI & MongoDB img: https://fastapi.tiangolo.com/img/sponsors/mongodb.png - - url: https://konghq.com/products/kong-konnect?utm_medium=referral&utm_source=github&utm_campaign=platform&utm_content=fast-api - title: Kong Konnect - API management platform - img: https://fastapi.tiangolo.com/img/sponsors/kong.png - url: https://zuplo.link/fastapi-gh title: 'Zuplo: Scale, Protect, Document, and Monetize your FastAPI' img: https://fastapi.tiangolo.com/img/sponsors/zuplo.png diff --git a/docs/en/docs/advanced/additional-responses.md b/docs/en/docs/advanced/additional-responses.md index 674f0672c..4fec41213 100644 --- a/docs/en/docs/advanced/additional-responses.md +++ b/docs/en/docs/advanced/additional-responses.md @@ -18,7 +18,7 @@ But for those additional responses you have to make sure you return a `Response` You can pass to your *path operation decorators* a parameter `responses`. -It receives a `dict`, the keys are status codes for each response, like `200`, and the values are other `dict`s with the information for each of them. +It receives a `dict`: the keys are status codes for each response (like `200`), and the values are other `dict`s with the information for each of them. Each of those response `dict`s can have a key `model`, containing a Pydantic model, just like `response_model`. diff --git a/docs/en/docs/advanced/async-tests.md b/docs/en/docs/advanced/async-tests.md index ac459ff0c..a528c80fe 100644 --- a/docs/en/docs/advanced/async-tests.md +++ b/docs/en/docs/advanced/async-tests.md @@ -72,7 +72,7 @@ Note that the test function is now `async def` instead of just `def` as before w Then we can create an `AsyncClient` with the app, and send async requests to it, using `await`. -```Python hl_lines="9-10" +```Python hl_lines="9-12" {!../../../docs_src/async_tests/test_main.py!} ``` @@ -102,6 +102,6 @@ As the testing function is now asynchronous, you can now also call (and `await`) /// tip -If you encounter a `RuntimeError: Task attached to a different loop` when integrating asynchronous function calls in your tests (e.g. when using MongoDB's MotorClient) Remember to instantiate objects that need an event loop only within async functions, e.g. an `'@app.on_event("startup")` callback. +If you encounter a `RuntimeError: Task attached to a different loop` when integrating asynchronous function calls in your tests (e.g. when using MongoDB's MotorClient), remember to instantiate objects that need an event loop only within async functions, e.g. an `'@app.on_event("startup")` callback. /// diff --git a/docs/en/docs/advanced/behind-a-proxy.md b/docs/en/docs/advanced/behind-a-proxy.md index 5ff64016c..e642b1910 100644 --- a/docs/en/docs/advanced/behind-a-proxy.md +++ b/docs/en/docs/advanced/behind-a-proxy.md @@ -303,7 +303,7 @@ This is a more advanced use case. Feel free to skip it. By default, **FastAPI** will create a `server` in the OpenAPI schema with the URL for the `root_path`. -But you can also provide other alternative `servers`, for example if you want *the same* docs UI to interact with a staging and production environments. +But you can also provide other alternative `servers`, for example if you want *the same* docs UI to interact with both a staging and a production environment. If you pass a custom list of `servers` and there's a `root_path` (because your API lives behind a proxy), **FastAPI** will insert a "server" with this `root_path` at the beginning of the list. diff --git a/docs/en/docs/advanced/custom-response.md b/docs/en/docs/advanced/custom-response.md index 8a6555dba..1cefe979f 100644 --- a/docs/en/docs/advanced/custom-response.md +++ b/docs/en/docs/advanced/custom-response.md @@ -4,9 +4,9 @@ By default, **FastAPI** will return the responses using `JSONResponse`. You can override it by returning a `Response` directly as seen in [Return a Response directly](response-directly.md){.internal-link target=_blank}. -But if you return a `Response` directly, the data won't be automatically converted, and the documentation won't be automatically generated (for example, including the specific "media type", in the HTTP header `Content-Type` as part of the generated OpenAPI). +But if you return a `Response` directly (or any subclass, like `JSONResponse`), the data won't be automatically converted (even if you declare a `response_model`), and the documentation won't be automatically generated (for example, including the specific "media type", in the HTTP header `Content-Type` as part of the generated OpenAPI). -But you can also declare the `Response` that you want to be used, in the *path operation decorator*. +But you can also declare the `Response` that you want to be used (e.g. any `Response` subclass), in the *path operation decorator* using the `response_class` parameter. The contents that you return from your *path operation function* will be put inside of that `Response`. @@ -142,7 +142,7 @@ It accepts the following parameters: * `headers` - A `dict` of strings. * `media_type` - A `str` giving the media type. E.g. `"text/html"`. -FastAPI (actually Starlette) will automatically include a Content-Length header. It will also include a Content-Type header, based on the media_type and appending a charset for text types. +FastAPI (actually Starlette) will automatically include a Content-Length header. It will also include a Content-Type header, based on the `media_type` and appending a charset for text types. ```Python hl_lines="1 18" {!../../../docs_src/response_directly/tutorial002.py!} @@ -154,7 +154,7 @@ Takes some text or bytes and returns an HTML response, as you read above. ### `PlainTextResponse` -Takes some text or bytes and returns an plain text response. +Takes some text or bytes and returns a plain text response. ```Python hl_lines="2 7 9" {!../../../docs_src/custom_response/tutorial005.py!} @@ -273,7 +273,7 @@ Asynchronously streams a file as the response. Takes a different set of arguments to instantiate than the other response types: -* `path` - The filepath to the file to stream. +* `path` - The file path to the file to stream. * `headers` - Any custom headers to include, as a dictionary. * `media_type` - A string giving the media type. If unset, the filename or path will be used to infer a media type. * `filename` - If set, this will be included in the response `Content-Disposition`. diff --git a/docs/en/docs/advanced/middleware.md b/docs/en/docs/advanced/middleware.md index 70415adca..ab7778db0 100644 --- a/docs/en/docs/advanced/middleware.md +++ b/docs/en/docs/advanced/middleware.md @@ -24,7 +24,7 @@ app = SomeASGIApp() new_app = UnicornMiddleware(app, some_config="rainbow") ``` -But FastAPI (actually Starlette) provides a simpler way to do it that makes sure that the internal middlewares to handle server errors and custom exception handlers work properly. +But FastAPI (actually Starlette) provides a simpler way to do it that makes sure that the internal middlewares handle server errors and custom exception handlers work properly. For that, you use `app.add_middleware()` (as in the example for CORS). @@ -55,7 +55,7 @@ For the next examples, you could also use `from starlette.middleware.something i Enforces that all incoming requests must either be `https` or `wss`. -Any incoming requests to `http` or `ws` will be redirected to the secure scheme instead. +Any incoming request to `http` or `ws` will be redirected to the secure scheme instead. ```Python hl_lines="2 6" {!../../../docs_src/advanced_middleware/tutorial001.py!} diff --git a/docs/en/docs/advanced/path-operation-advanced-configuration.md b/docs/en/docs/advanced/path-operation-advanced-configuration.md index c8874bad9..d599006d2 100644 --- a/docs/en/docs/advanced/path-operation-advanced-configuration.md +++ b/docs/en/docs/advanced/path-operation-advanced-configuration.md @@ -149,7 +149,7 @@ For example, you could decide to read and validate the request with your own cod You could do that with `openapi_extra`: -```Python hl_lines="20-37 39-40" +```Python hl_lines="19-36 39-40" {!../../../docs_src/path_operation_advanced_configuration/tutorial006.py!} ``` diff --git a/docs/en/docs/advanced/response-directly.md b/docs/en/docs/advanced/response-directly.md index 73071ed1b..092aeceb1 100644 --- a/docs/en/docs/advanced/response-directly.md +++ b/docs/en/docs/advanced/response-directly.md @@ -28,7 +28,7 @@ This gives you a lot of flexibility. You can return any data type, override any ## Using the `jsonable_encoder` in a `Response` -Because **FastAPI** doesn't do any change to a `Response` you return, you have to make sure it's contents are ready for it. +Because **FastAPI** doesn't make any changes to a `Response` you return, you have to make sure its contents are ready for it. For example, you cannot put a Pydantic model in a `JSONResponse` without first converting it to a `dict` with all the data types (like `datetime`, `UUID`, etc) converted to JSON-compatible types. diff --git a/docs/en/docs/advanced/security/http-basic-auth.md b/docs/en/docs/advanced/security/http-basic-auth.md index c302bf8dc..e669d10d8 100644 --- a/docs/en/docs/advanced/security/http-basic-auth.md +++ b/docs/en/docs/advanced/security/http-basic-auth.md @@ -144,7 +144,7 @@ And then they can try again knowing that it's probably something more similar to #### A "professional" attack -Of course, the attackers would not try all this by hand, they would write a program to do it, possibly with thousands or millions of tests per second. And would get just one extra correct letter at a time. +Of course, the attackers would not try all this by hand, they would write a program to do it, possibly with thousands or millions of tests per second. And they would get just one extra correct letter at a time. But doing that, in some minutes or hours the attackers would have guessed the correct username and password, with the "help" of our application, just using the time taken to answer. diff --git a/docs/en/docs/advanced/security/oauth2-scopes.md b/docs/en/docs/advanced/security/oauth2-scopes.md index ff52d7bb8..fdd8db7b9 100644 --- a/docs/en/docs/advanced/security/oauth2-scopes.md +++ b/docs/en/docs/advanced/security/oauth2-scopes.md @@ -769,7 +769,7 @@ But if you are building an OAuth2 application that others would connect to (i.e. The most common is the implicit flow. -The most secure is the code flow, but is more complex to implement as it requires more steps. As it is more complex, many providers end up suggesting the implicit flow. +The most secure is the code flow, but it's more complex to implement as it requires more steps. As it is more complex, many providers end up suggesting the implicit flow. /// note diff --git a/docs/en/docs/advanced/settings.md b/docs/en/docs/advanced/settings.md index b78f83953..8c04d2507 100644 --- a/docs/en/docs/advanced/settings.md +++ b/docs/en/docs/advanced/settings.md @@ -6,135 +6,17 @@ Most of these settings are variable (can change), like database URLs. And many c For this reason it's common to provide them in environment variables that are read by the application. -## Environment Variables - -/// tip - -If you already know what "environment variables" are and how to use them, feel free to skip to the next section below. - -/// - -An environment variable (also known as "env var") is a variable that lives outside of the Python code, in the operating system, and could be read by your Python code (or by other programs as well). - -You can create and use environment variables in the shell, without needing Python: - -//// tab | Linux, macOS, Windows Bash - -
- -```console -// You could create an env var MY_NAME with -$ export MY_NAME="Wade Wilson" - -// Then you could use it with other programs, like -$ echo "Hello $MY_NAME" - -Hello Wade Wilson -``` - -
- -//// - -//// tab | Windows PowerShell - -
- -```console -// Create an env var MY_NAME -$ $Env:MY_NAME = "Wade Wilson" - -// Use it with other programs, like -$ echo "Hello $Env:MY_NAME" - -Hello Wade Wilson -``` - -
- -//// - -### Read env vars in Python - -You could also create environment variables outside of Python, in the terminal (or with any other method), and then read them in Python. - -For example you could have a file `main.py` with: - -```Python hl_lines="3" -import os - -name = os.getenv("MY_NAME", "World") -print(f"Hello {name} from Python") -``` - -/// tip - -The second argument to `os.getenv()` is the default value to return. - -If not provided, it's `None` by default, here we provide `"World"` as the default value to use. - -/// - -Then you could call that Python program: - -
- -```console -// Here we don't set the env var yet -$ python main.py - -// As we didn't set the env var, we get the default value - -Hello World from Python - -// But if we create an environment variable first -$ export MY_NAME="Wade Wilson" - -// And then call the program again -$ python main.py - -// Now it can read the environment variable - -Hello Wade Wilson from Python -``` - -
- -As environment variables can be set outside of the code, but can be read by the code, and don't have to be stored (committed to `git`) with the rest of the files, it's common to use them for configurations or settings. - -You can also create an environment variable only for a specific program invocation, that is only available to that program, and only for its duration. - -To do that, create it right before the program itself, on the same line: - -
- -```console -// Create an env var MY_NAME in line for this program call -$ MY_NAME="Wade Wilson" python main.py - -// Now it can read the environment variable - -Hello Wade Wilson from Python - -// The env var no longer exists afterwards -$ python main.py - -Hello World from Python -``` - -
- /// tip -You can read more about it at The Twelve-Factor App: Config. +To understand environment variables you can read [Environment Variables](../environment-variables.md){.internal-link target=_blank}. /// -### Types and validation +## Types and validation These environment variables can only handle text strings, as they are external to Python and have to be compatible with other programs and the rest of the system (and even with different operating systems, as Linux, Windows, macOS). -That means that any value read in Python from an environment variable will be a `str`, and any conversion to a different type or validation has to be done in code. +That means that any value read in Python from an environment variable will be a `str`, and any conversion to a different type or any validation has to be done in code. ## Pydantic `Settings` @@ -142,7 +24,7 @@ Fortunately, Pydantic provides a great utility to handle these settings coming f ### Install `pydantic-settings` -First, install the `pydantic-settings` package: +First, make sure you create your [virtual environment](../virtual-environments.md){.internal-link target=_blank}, activate it, and then install the `pydantic-settings` package:
@@ -492,7 +374,7 @@ Prefer to use the `Annotated` version if possible. //// -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. +Then for any subsequent call 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. #### `lru_cache` Technical Details diff --git a/docs/en/docs/advanced/sub-applications.md b/docs/en/docs/advanced/sub-applications.md index 8c52e091f..568a9deca 100644 --- a/docs/en/docs/advanced/sub-applications.md +++ b/docs/en/docs/advanced/sub-applications.md @@ -36,12 +36,12 @@ In this case, it will be mounted at the path `/subapi`: ### Check the automatic API docs -Now, run `uvicorn` with the main app, if your file is `main.py`, it would be: +Now, run the `fastapi` command with your file:
```console -$ uvicorn main:app --reload +$ fastapi dev main.py INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) ``` diff --git a/docs/en/docs/advanced/templates.md b/docs/en/docs/advanced/templates.md index 43731ec36..416540ba4 100644 --- a/docs/en/docs/advanced/templates.md +++ b/docs/en/docs/advanced/templates.md @@ -8,7 +8,7 @@ There are utilities to configure it easily that you can use directly in your **F ## Install dependencies -Install `jinja2`: +Make sure you create a [virtual environment](../virtual-environments.md){.internal-link target=_blank}, activate it, and install `jinja2`:
diff --git a/docs/en/docs/advanced/websockets.md b/docs/en/docs/advanced/websockets.md index 9655714b0..44c6c7428 100644 --- a/docs/en/docs/advanced/websockets.md +++ b/docs/en/docs/advanced/websockets.md @@ -4,7 +4,7 @@ You can use diff --git a/docs/en/docs/alternatives.md b/docs/en/docs/alternatives.md index 2ad584c95..e98c0475a 100644 --- a/docs/en/docs/alternatives.md +++ b/docs/en/docs/alternatives.md @@ -474,7 +474,7 @@ It is the recommended server for Starlette and **FastAPI**. The main web server to run **FastAPI** applications. -You can combine it with Gunicorn, to have an asynchronous multi-process server. +You can also use the `--workers` command line option to have an asynchronous multi-process server. Check more details in the [Deployment](deployment/index.md){.internal-link target=_blank} section. diff --git a/docs/en/docs/async.md b/docs/en/docs/async.md index 7cf4af627..63bd8ca68 100644 --- a/docs/en/docs/async.md +++ b/docs/en/docs/async.md @@ -292,7 +292,7 @@ For example: ### Concurrency + Parallelism: Web + Machine Learning -With **FastAPI** you can take the advantage of concurrency that is very common for web development (the same main attraction of NodeJS). +With **FastAPI** you can take advantage of concurrency that is very common for web development (the same main attraction of NodeJS). But you can also exploit the benefits of parallelism and multiprocessing (having multiple processes running in parallel) for **CPU bound** workloads like those in Machine Learning systems. @@ -387,7 +387,7 @@ In previous versions of NodeJS / Browser JavaScript, you would have used "callba ## Coroutines -**Coroutine** is just the very fancy term for the thing returned by an `async def` function. Python knows that it is something like a function that it can start and that it will end at some point, but that it might be paused โธ internally too, whenever there is an `await` inside of it. +**Coroutine** is just the very fancy term for the thing returned by an `async def` function. Python knows that it is something like a function, that it can start and that it will end at some point, but that it might be paused โธ internally too, whenever there is an `await` inside of it. But all this functionality of using asynchronous code with `async` and `await` is many times summarized as using "coroutines". It is comparable to the main key feature of Go, the "Goroutines". diff --git a/docs/en/docs/contributing.md b/docs/en/docs/contributing.md index 63e1f359a..0dc07b89b 100644 --- a/docs/en/docs/contributing.md +++ b/docs/en/docs/contributing.md @@ -6,117 +6,13 @@ First, you might want to see the basic ways to [help FastAPI and get help](help- If you already cloned the fastapi repository and you want to deep dive in the code, here are some guidelines to set up your environment. -### Virtual environment with `venv` +### Virtual environment -You can create an isolated virtual local environment in a directory using Python's `venv` module. Let's do this in the cloned repository (where the `requirements.txt` is): - -
- -```console -$ python -m venv env -``` - -
- -That will create a directory `./env/` with the Python binaries, and then you will be able to install packages for that local environment. - -### Activate the environment - -Activate the new environment with: - -//// tab | Linux, macOS - -
- -```console -$ source ./env/bin/activate -``` - -
- -//// - -//// tab | Windows PowerShell - -
- -```console -$ .\env\Scripts\Activate.ps1 -``` - -
- -//// - -//// tab | Windows Bash - -Or if you use Bash for Windows (e.g. Git Bash): - -
- -```console -$ source ./env/Scripts/activate -``` - -
- -//// - -To check it worked, use: - -//// tab | Linux, macOS, Windows Bash - -
- -```console -$ which pip - -some/directory/fastapi/env/bin/pip -``` - -
- -//// - -//// tab | Windows PowerShell - -
- -```console -$ Get-Command pip - -some/directory/fastapi/env/bin/pip -``` - -
- -//// - -If it shows the `pip` binary at `env/bin/pip` then it worked. ๐ŸŽ‰ - -Make sure you have the latest pip version on your local environment to avoid errors on the next steps: - -
- -```console -$ python -m pip install --upgrade pip - ----> 100% -``` - -
- -/// tip - -Every time you install a new package with `pip` under that environment, activate the environment again. - -This makes sure that if you use a terminal program installed by that package, you use the one from your local environment and not any other that could be installed globally. - -/// +Follow the instructions to create and activate a [virtual environment](virtual-environments.md){.internal-link target=_blank} for the internal code of `fastapi`. ### Install requirements using pip -After activating the environment as described above: +After activating the environment, install the required packages:
@@ -160,7 +56,19 @@ $ bash scripts/format.sh It will also auto-sort all your imports. -For it to sort them correctly, you need to have FastAPI installed locally in your environment, with the command in the section above using `-e`. +## Tests + +There is a script that you can run locally to test all the code and generate coverage reports in HTML: + +
+ +```console +$ bash scripts/test-cov-html.sh +``` + +
+ +This command generates a directory `./htmlcov/`, if you open the file `./htmlcov/index.html` in your browser, you can explore interactively the regions of code that are covered by the tests, and notice if there is any region missing. ## Docs @@ -262,7 +170,7 @@ If you run the examples with, e.g.:
```console -$ uvicorn tutorial001:app --reload +$ fastapi dev tutorial001.py INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) ``` @@ -482,17 +390,3 @@ Serving at: http://127.0.0.1:8008 * Search for such links in the translated document using the regex `#[^# ]`. * Search in all documents already translated into your language for `your-translated-document.md`. For example VS Code has an option "Edit" -> "Find in Files". * When translating a document, do not "pre-translate" `#hash-parts` that link to headings in untranslated documents. - -## Tests - -There is a script that you can run locally to test all the code and generate coverage reports in HTML: - -
- -```console -$ bash scripts/test-cov-html.sh -``` - -
- -This command generates a directory `./htmlcov/`, if you open the file `./htmlcov/index.html` in your browser, you can explore interactively the regions of code that are covered by the tests, and notice if there is any region missing. diff --git a/docs/en/docs/deployment/cloud.md b/docs/en/docs/deployment/cloud.md index d34fbe2f7..41ada859d 100644 --- a/docs/en/docs/deployment/cloud.md +++ b/docs/en/docs/deployment/cloud.md @@ -14,4 +14,4 @@ You might want to try their services and follow their guides: * Platform.sh * Porter -* Coherence +* Coherence diff --git a/docs/en/docs/deployment/concepts.md b/docs/en/docs/deployment/concepts.md index f917d18b3..e71a7487a 100644 --- a/docs/en/docs/deployment/concepts.md +++ b/docs/en/docs/deployment/concepts.md @@ -94,7 +94,7 @@ In most cases, when you create a web API, you want it to be **always running**, ### In a Remote Server -When you set up a remote server (a cloud server, a virtual machine, etc.) the simplest thing you can do is to use `fastapi run`, Uvicorn (or similar) manually, the same way you do when developing locally. +When you set up a remote server (a cloud server, a virtual machine, etc.) the simplest thing you can do is use `fastapi run` (which uses Uvicorn) or something similar, manually, the same way you do when developing locally. And it will work and will be useful **during development**. @@ -178,7 +178,7 @@ For example, this could be handled by: ## Replication - Processes and Memory -With a FastAPI application, using a server program like Uvicorn, running it once in **one process** can serve multiple clients concurrently. +With a FastAPI application, using a server program like the `fastapi` command that runs Uvicorn, running it once in **one process** can serve multiple clients concurrently. But in many cases, you will want to run several worker processes at the same time. @@ -232,9 +232,7 @@ The main constraint to consider is that there has to be a **single** component h Here are some possible combinations and strategies: -* **Gunicorn** managing **Uvicorn workers** - * Gunicorn would be the **process manager** listening on the **IP** and **port**, the replication would be by having **multiple Uvicorn worker processes**. -* **Uvicorn** managing **Uvicorn workers** +* **Uvicorn** with `--workers` * One Uvicorn **process manager** would listen on the **IP** and **port**, and it would start **multiple Uvicorn worker processes**. * **Kubernetes** and other distributed **container systems** * Something in the **Kubernetes** layer would listen on the **IP** and **port**. The replication would be by having **multiple containers**, each with **one Uvicorn process** running. @@ -259,7 +257,7 @@ But in most cases, you will want to perform these steps only **once**. So, you will want to have a **single process** to perform those **previous steps**, before starting the application. -And you will have to make sure that it's a single process running those previous steps *even* if afterwards, you start **multiple processes** (multiple workers) for the application itself. If those steps were run by **multiple processes**, they would **duplicate** the work by running it on **parallel**, and if the steps were something delicate like a database migration, they could cause conflicts with each other. +And you will have to make sure that it's a single process running those previous steps *even* if afterwards, you start **multiple processes** (multiple workers) for the application itself. If those steps were run by **multiple processes**, they would **duplicate** the work by running it in **parallel**, and if the steps were something delicate like a database migration, they could cause conflicts with each other. Of course, there are some cases where there's no problem in running the previous steps multiple times, in that case, it's a lot easier to handle. diff --git a/docs/en/docs/deployment/docker.md b/docs/en/docs/deployment/docker.md index 253e25fe5..b106f7ac3 100644 --- a/docs/en/docs/deployment/docker.md +++ b/docs/en/docs/deployment/docker.md @@ -167,22 +167,22 @@ def read_item(item_id: int, q: Union[str, None] = None): Now in the same project directory create a file `Dockerfile` with: ```{ .dockerfile .annotate } -# (1) +# (1)! FROM python:3.9 -# (2) +# (2)! WORKDIR /code -# (3) +# (3)! COPY ./requirements.txt /code/requirements.txt -# (4) +# (4)! RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt -# (5) +# (5)! COPY ./app /code/app -# (6) +# (6)! CMD ["fastapi", "run", "app/main.py", "--port", "80"] ``` @@ -232,6 +232,38 @@ Review what each line does by clicking each number bubble in the code. ๐Ÿ‘† /// +/// warning + +Make sure to **always** use the **exec form** of the `CMD` instruction, as explained below. + +/// + +#### Use `CMD` - Exec Form + +The `CMD` Docker instruction can be written using two forms: + +โœ… **Exec** form: + +```Dockerfile +# โœ… Do this +CMD ["fastapi", "run", "app/main.py", "--port", "80"] +``` + +โ›”๏ธ **Shell** form: + +```Dockerfile +# โ›”๏ธ Don't do this +CMD fastapi run app/main.py --port 80 +``` + +Make sure to always use the **exec** form to ensure that FastAPI can shutdown gracefully and [lifespan events](../advanced/events.md){.internal-link target=_blank} are triggered. + +You can read more about it in the Docker docs for shell and exec form. + +This can be quite noticeable when using `docker compose`. See this Docker Compose FAQ section for more technical details: Why do my services take 10 seconds to recreate or stop?. + +#### Directory Structure + You should now have a directory structure like: ``` @@ -368,10 +400,10 @@ COPY ./requirements.txt /code/requirements.txt RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt -# (1) +# (1)! COPY ./main.py /code/ -# (2) +# (2)! CMD ["fastapi", "run", "main.py", "--port", "80"] ``` @@ -424,11 +456,11 @@ Without using containers, making applications run on startup and with restarts c ## Replication - Number of Processes -If you have a cluster of machines with **Kubernetes**, Docker Swarm Mode, Nomad, or another similar complex system to manage distributed containers on multiple machines, then you will probably want to **handle replication** at the **cluster level** instead of using a **process manager** (like Gunicorn with workers) in each container. +If you have a cluster of machines with **Kubernetes**, Docker Swarm Mode, Nomad, or another similar complex system to manage distributed containers on multiple machines, then you will probably want to **handle replication** at the **cluster level** instead of using a **process manager** (like Uvicorn with workers) in each container. One of those distributed container management systems like Kubernetes normally has some integrated way of handling **replication of containers** while still supporting **load balancing** for the incoming requests. All at the **cluster level**. -In those cases, you would probably want to build a **Docker image from scratch** as [explained above](#dockerfile), installing your dependencies, and running **a single Uvicorn process** instead of running something like Gunicorn with Uvicorn workers. +In those cases, you would probably want to build a **Docker image from scratch** as [explained above](#dockerfile), installing your dependencies, and running **a single Uvicorn process** instead of using multiple Uvicorn workers. ### Load Balancer @@ -458,37 +490,44 @@ And normally this **load balancer** would be able to handle requests that go to In this type of scenario, you probably would want to have **a single (Uvicorn) process per container**, as you would already be handling replication at the cluster level. -So, in this case, you **would not** want to have a process manager like Gunicorn with Uvicorn workers, or Uvicorn using its own Uvicorn workers. You would want to have just a **single Uvicorn process** per container (but probably multiple containers). +So, in this case, you **would not** want to have a multiple workers in the container, for example with the `--workers` command line option.You would want to have just a **single Uvicorn process** per container (but probably multiple containers). -Having another process manager inside the container (as would be with Gunicorn or Uvicorn managing Uvicorn workers) would only add **unnecessary complexity** that you are most probably already taking care of with your cluster system. +Having another process manager inside the container (as would be with multiple workers) would only add **unnecessary complexity** that you are most probably already taking care of with your cluster system. ### Containers with Multiple Processes and Special Cases -Of course, there are **special cases** where you could want to have **a container** with a **Gunicorn process manager** starting several **Uvicorn worker processes** inside. +Of course, there are **special cases** where you could want to have **a container** with several **Uvicorn worker processes** inside. -In those cases, you can use the **official Docker image** that includes **Gunicorn** as a process manager running multiple **Uvicorn worker processes**, and some default settings to adjust the number of workers based on the current CPU cores automatically. I'll tell you more about it below in [Official Docker Image with Gunicorn - Uvicorn](#official-docker-image-with-gunicorn-uvicorn). +In those cases, you can use the `--workers` command line option to set the number of workers that you want to run: -Here are some examples of when that could make sense: +```{ .dockerfile .annotate } +FROM python:3.9 -#### A Simple App +WORKDIR /code -You could want a process manager in the container if your application is **simple enough** that you don't need (at least not yet) to fine-tune the number of processes too much, and you can just use an automated default (with the official Docker image), and you are running it on a **single server**, not a cluster. +COPY ./requirements.txt /code/requirements.txt -#### Docker Compose +RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt -You could be deploying to a **single server** (not a cluster) with **Docker Compose**, so you wouldn't have an easy way to manage replication of containers (with Docker Compose) while preserving the shared network and **load balancing**. +COPY ./app /code/app -Then you could want to have **a single container** with a **process manager** starting **several worker processes** inside. +# (1)! +CMD ["fastapi", "run", "app/main.py", "--port", "80", "--workers", "4"] +``` -#### Prometheus and Other Reasons +1. Here we use the `--workers` command line option to set the number of workers to 4. + +Here are some examples of when that could make sense: -You could also have **other reasons** that would make it easier to have a **single container** with **multiple processes** instead of having **multiple containers** with **a single process** in each of them. +#### A Simple App -For example (depending on your setup) you could have some tool like a Prometheus exporter in the same container that should have access to **each of the requests** that come. +You could want a process manager in the container if your application is **simple enough** that can run it on a **single server**, not a cluster. -In this case, if you had **multiple containers**, by default, when Prometheus came to **read the metrics**, it would get the ones for **a single container each time** (for the container that handled that particular request), instead of getting the **accumulated metrics** for all the replicated containers. +#### Docker Compose -Then, in that case, it could be simpler to have **one container** with **multiple processes**, and a local tool (e.g. a Prometheus exporter) on the same container collecting Prometheus metrics for all the internal processes and exposing those metrics on that single container. +You could be deploying to a **single server** (not a cluster) with **Docker Compose**, so you wouldn't have an easy way to manage replication of containers (with Docker Compose) while preserving the shared network and **load balancing**. + +Then you could want to have **a single container** with a **process manager** starting **several worker processes** inside. --- @@ -509,7 +548,7 @@ And then you can set those same memory limits and requirements in your configura If your application is **simple**, this will probably **not be a problem**, and you might not need to specify hard memory limits. But if you are **using a lot of memory** (for example with **machine learning** models), you should check how much memory you are consuming and adjust the **number of containers** that runs in **each machine** (and maybe add more machines to your cluster). -If you run **multiple processes per container** (for example with the official Docker image) you will have to make sure that the number of processes started doesn't **consume more memory** than what is available. +If you run **multiple processes per container** you will have to make sure that the number of processes started doesn't **consume more memory** than what is available. ## Previous Steps Before Starting and Containers @@ -529,80 +568,26 @@ If in your use case there's no problem in running those previous steps **multipl ### Single Container -If you have a simple setup, with a **single container** that then starts multiple **worker processes** (or also just one process), then you could run those previous steps in the same container, right before starting the process with the app. The official Docker image supports this internally. - -## Official Docker Image with Gunicorn - Uvicorn +If you have a simple setup, with a **single container** that then starts multiple **worker processes** (or also just one process), then you could run those previous steps in the same container, right before starting the process with the app. -There is an official Docker image that includes Gunicorn running with Uvicorn workers, as detailed in a previous chapter: [Server Workers - Gunicorn with Uvicorn](server-workers.md){.internal-link target=_blank}. +### Base Docker Image -This image would be useful mainly in the situations described above in: [Containers with Multiple Processes and Special Cases](#containers-with-multiple-processes-and-special-cases). +There used to be an official FastAPI Docker image: tiangolo/uvicorn-gunicorn-fastapi. But it is now deprecated. โ›”๏ธ -* tiangolo/uvicorn-gunicorn-fastapi. - -/// warning - -There's a high chance that you **don't** need this base image or any other similar one, and would be better off by building the image from scratch as [described above in: Build a Docker Image for FastAPI](#build-a-docker-image-for-fastapi). - -/// +You should probably **not** use this base Docker image (or any other similar one). -This image has an **auto-tuning** mechanism included to set the **number of worker processes** based on the CPU cores available. +If you are using **Kubernetes** (or others) and you are already setting **replication** at the cluster level, with multiple **containers**. In those cases, you are better off **building an image from scratch** as described above: [Build a Docker Image for FastAPI](#build-a-docker-image-for-fastapi). -It has **sensible defaults**, but you can still change and update all the configurations with **environment variables** or configuration files. +And if you need to have multiple workers, you can simply use the `--workers` command line option. -It also supports running **previous steps before starting** with a script. +/// note | Technical Details -/// tip +The Docker image was created when Uvicorn didn't support managing and restarting dead workers, so it was needed to use Gunicorn with Uvicorn, which added quite some complexity, just to have Gunicorn manage and restart the Uvicorn worker processes. -To see all the configurations and options, go to the Docker image page: tiangolo/uvicorn-gunicorn-fastapi. +But now that Uvicorn (and the `fastapi` command) support using `--workers`, there's no reason to use a base Docker image instead of building your own (it's pretty much the same amount of code ๐Ÿ˜…). /// -### Number of Processes on the Official Docker Image - -The **number of processes** on this image is **computed automatically** from the CPU **cores** available. - -This means that it will try to **squeeze** as much **performance** from the CPU as possible. - -You can also adjust it with the configurations using **environment variables**, etc. - -But it also means that as the number of processes depends on the CPU the container is running, the **amount of memory consumed** will also depend on that. - -So, if your application consumes a lot of memory (for example with machine learning models), and your server has a lot of CPU cores **but little memory**, then your container could end up trying to use more memory than what is available, and degrading performance a lot (or even crashing). ๐Ÿšจ - -### Create a `Dockerfile` - -Here's how you would create a `Dockerfile` based on this image: - -```Dockerfile -FROM tiangolo/uvicorn-gunicorn-fastapi:python3.9 - -COPY ./requirements.txt /app/requirements.txt - -RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt - -COPY ./app /app -``` - -### Bigger Applications - -If you followed the section about creating [Bigger Applications with Multiple Files](../tutorial/bigger-applications.md){.internal-link target=_blank}, your `Dockerfile` might instead look like: - -```Dockerfile hl_lines="7" -FROM tiangolo/uvicorn-gunicorn-fastapi:python3.9 - -COPY ./requirements.txt /app/requirements.txt - -RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt - -COPY ./app /app/app -``` - -### When to Use - -You should probably **not** use this official base image (or any other similar one) if you are using **Kubernetes** (or others) and you are already setting **replication** at the cluster level, with multiple **containers**. In those cases, you are better off **building an image from scratch** as described above: [Build a Docker Image for FastAPI](#build-a-docker-image-for-fastapi). - -This image would be useful mainly in the special cases described above in [Containers with Multiple Processes and Special Cases](#containers-with-multiple-processes-and-special-cases). For example, if your application is **simple enough** that setting a default number of processes based on the CPU works well, you don't want to bother with manually configuring the replication at the cluster level, and you are not running more than one container with your app. Or if you are deploying with **Docker Compose**, running on a single server, etc. - ## Deploy the Container Image After having a Container (Docker) Image there are several ways to deploy it. @@ -615,98 +600,9 @@ For example: * With another tool like Nomad * With a cloud service that takes your container image and deploys it -## Docker Image with Poetry - -If you use Poetry to manage your project's dependencies, you could use Docker multi-stage building: - -```{ .dockerfile .annotate } -# (1) -FROM python:3.9 as requirements-stage - -# (2) -WORKDIR /tmp - -# (3) -RUN pip install poetry - -# (4) -COPY ./pyproject.toml ./poetry.lock* /tmp/ - -# (5) -RUN poetry export -f requirements.txt --output requirements.txt --without-hashes - -# (6) -FROM python:3.9 - -# (7) -WORKDIR /code - -# (8) -COPY --from=requirements-stage /tmp/requirements.txt /code/requirements.txt - -# (9) -RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt - -# (10) -COPY ./app /code/app - -# (11) -CMD ["fastapi", "run", "app/main.py", "--port", "80"] -``` - -1. This is the first stage, it is named `requirements-stage`. +## Docker Image with `uv` -2. Set `/tmp` as the current working directory. - - Here's where we will generate the file `requirements.txt` - -3. Install Poetry in this Docker stage. - -4. Copy the `pyproject.toml` and `poetry.lock` files to the `/tmp` directory. - - Because it uses `./poetry.lock*` (ending with a `*`), it won't crash if that file is not available yet. - -5. Generate the `requirements.txt` file. - -6. This is the final stage, anything here will be preserved in the final container image. - -7. Set the current working directory to `/code`. - -8. Copy the `requirements.txt` file to the `/code` directory. - - This file only lives in the previous Docker stage, that's why we use `--from-requirements-stage` to copy it. - -9. Install the package dependencies in the generated `requirements.txt` file. - -10. Copy the `app` directory to the `/code` directory. - -11. Use the `fastapi run` command to run your app. - -/// tip - -Click the bubble numbers to see what each line does. - -/// - -A **Docker stage** is a part of a `Dockerfile` that works as a **temporary container image** that is only used to generate some files to be used later. - -The first stage will only be used to **install Poetry** and to **generate the `requirements.txt`** with your project dependencies from Poetry's `pyproject.toml` file. - -This `requirements.txt` file will be used with `pip` later in the **next stage**. - -In the final container image **only the final stage** is preserved. The previous stage(s) will be discarded. - -When using Poetry, it would make sense to use **Docker multi-stage builds** because you don't really need to have Poetry and its dependencies installed in the final container image, you **only need** to have the generated `requirements.txt` file to install your project dependencies. - -Then in the next (and final) stage you would build the image more or less in the same way as described before. - -### Behind a TLS Termination Proxy - Poetry - -Again, if you are running your container behind a TLS Termination Proxy (load balancer) like Nginx or Traefik, add the option `--proxy-headers` to the command: - -```Dockerfile -CMD ["fastapi", "run", "app/main.py", "--proxy-headers", "--port", "80"] -``` +If you are using uv to install and manage your project, you can follow their uv Docker guide. ## Recap @@ -719,8 +615,6 @@ Using container systems (e.g. with **Docker** and **Kubernetes**) it becomes fai * Memory * Previous steps before starting -In most cases, you probably won't want to use any base image, and instead **build a container image from scratch** one based on the official Python Docker image. +In most cases, you probably won't want to use any base image, and instead **build a container image from scratch** based on the official Python Docker image. Taking care of the **order** of instructions in the `Dockerfile` and the **Docker cache** you can **minimize build times**, to maximize your productivity (and avoid boredom). ๐Ÿ˜Ž - -In certain special cases, you might want to use the official Docker image for FastAPI. ๐Ÿค“ diff --git a/docs/en/docs/deployment/manually.md b/docs/en/docs/deployment/manually.md index d70c5e48b..3f7c7a008 100644 --- a/docs/en/docs/deployment/manually.md +++ b/docs/en/docs/deployment/manually.md @@ -67,6 +67,8 @@ There are several alternatives, including: * Uvicorn: a high performance ASGI server. * Hypercorn: an ASGI server compatible with HTTP/2 and Trio among other features. * Daphne: the ASGI server built for Django Channels. +* Granian: A Rust HTTP server for Python applications. +* NGINX Unit: NGINX Unit is a lightweight and versatile web application runtime. ## Server Machine and Server Program @@ -82,11 +84,11 @@ When referring to the remote machine, it's common to call it **server**, but als When you install FastAPI, it comes with a production server, Uvicorn, and you can start it with the `fastapi run` command. -But you can also install an ASGI server manually: +But you can also install an ASGI server manually. -//// tab | Uvicorn +Make sure you create a [virtual environment](../virtual-environments.md){.internal-link target=_blank}, activate it, and then you can install the server application. -* Uvicorn, a lightning-fast ASGI server, built on uvloop and httptools. +For example, to install Uvicorn:
@@ -98,6 +100,8 @@ $ pip install "uvicorn[standard]"
+A similar process would apply to any other ASGI server program. + /// tip By adding the `standard`, Uvicorn will install and use some recommended extra dependencies. @@ -108,32 +112,10 @@ When you install FastAPI with something like `pip install "fastapi[standard]"` y /// -//// - -//// tab | Hypercorn - -* Hypercorn, an ASGI server also compatible with HTTP/2. - -
- -```console -$ pip install hypercorn - ----> 100% -``` - -
- -...or any other ASGI server. - -//// - ## Run the Server Program If you installed an ASGI server manually, you would normally need to pass an import string in a special format for it to import your FastAPI application: -//// tab | Uvicorn -
```console @@ -144,22 +126,6 @@ $ uvicorn main:app --host 0.0.0.0 --port 80
-//// - -//// tab | Hypercorn - -
- -```console -$ hypercorn main:app --bind 0.0.0.0:80 - -Running on 0.0.0.0:8080 over http (CTRL + C to quit) -``` - -
- -//// - /// note The command `uvicorn main:app` refers to: @@ -175,9 +141,11 @@ from main import app /// +Each alternative ASGI server program would have a similar command, you can read more in their respective documentation. + /// warning -Uvicorn and others support a `--reload` option that is useful during development. +Uvicorn and other servers support a `--reload` option that is useful during development. The `--reload` option consumes much more resources, is more unstable, etc. @@ -185,43 +153,6 @@ It helps a lot during **development**, but you **shouldn't** use it in **product /// -## Hypercorn with Trio - -Starlette and **FastAPI** are based on AnyIO, which makes them compatible with both Python's standard library asyncio and Trio. - -Nevertheless, Uvicorn is currently only compatible with asyncio, and it normally uses `uvloop`, the high-performance drop-in replacement for `asyncio`. - -But if you want to directly use **Trio**, then you can use **Hypercorn** as it supports it. โœจ - -### Install Hypercorn with Trio - -First you need to install Hypercorn with Trio support: - -
- -```console -$ pip install "hypercorn[trio]" ----> 100% -``` - -
- -### Run with Trio - -Then you can pass the command line option `--worker-class` with the value `trio`: - -
- -```console -$ hypercorn main:app --worker-class trio -``` - -
- -And that will start Hypercorn with your app using Trio as the backend. - -Now you can use Trio internally in your app. Or even better, you can use AnyIO, to keep your code compatible with both Trio and asyncio. ๐ŸŽ‰ - ## Deployment Concepts These examples run the server program (e.g Uvicorn), starting **a single process**, listening on all the IPs (`0.0.0.0`) on a predefined port (e.g. `80`). diff --git a/docs/en/docs/deployment/server-workers.md b/docs/en/docs/deployment/server-workers.md index 433371b9d..622c10a30 100644 --- a/docs/en/docs/deployment/server-workers.md +++ b/docs/en/docs/deployment/server-workers.md @@ -1,4 +1,4 @@ -# Server Workers - Gunicorn with Uvicorn +# Server Workers - Uvicorn with Workers Let's check back those deployment concepts from before: @@ -9,123 +9,92 @@ Let's check back those deployment concepts from before: * Memory * Previous steps before starting -Up to this point, with all the tutorials in the docs, you have probably been running a **server program** like Uvicorn, running a **single process**. +Up to this point, with all the tutorials in the docs, you have probably been running a **server program**, for example, using the `fastapi` command, that runs Uvicorn, running a **single process**. When deploying applications you will probably want to have some **replication of processes** to take advantage of **multiple cores** and to be able to handle more requests. As you saw in the previous chapter about [Deployment Concepts](concepts.md){.internal-link target=_blank}, there are multiple strategies you can use. -Here I'll show you how to use **Gunicorn** with **Uvicorn worker processes**. +Here I'll show you how to use **Uvicorn** with **worker processes** using the `fastapi` command or the `uvicorn` command directly. /// info If you are using containers, for example with Docker or Kubernetes, I'll tell you more about that in the next chapter: [FastAPI in Containers - Docker](docker.md){.internal-link target=_blank}. -In particular, when running on **Kubernetes** you will probably **not** want to use Gunicorn and instead run **a single Uvicorn process per container**, but I'll tell you about it later in that chapter. +In particular, when running on **Kubernetes** you will probably **not** want to use workers and instead run **a single Uvicorn process per container**, but I'll tell you about it later in that chapter. /// -## Gunicorn with Uvicorn Workers +## Multiple Workers -**Gunicorn** is mainly an application server using the **WSGI standard**. That means that Gunicorn can serve applications like Flask and Django. Gunicorn by itself is not compatible with **FastAPI**, as FastAPI uses the newest **ASGI standard**. +You can start multiple workers with the `--workers` command line option: -But Gunicorn supports working as a **process manager** and allowing users to tell it which specific **worker process class** to use. Then Gunicorn would start one or more **worker processes** using that class. +//// tab | `fastapi` -And **Uvicorn** has a **Gunicorn-compatible worker class**. - -Using that combination, Gunicorn would act as a **process manager**, listening on the **port** and the **IP**. And it would **transmit** the communication to the worker processes running the **Uvicorn class**. - -And then the Gunicorn-compatible **Uvicorn worker** class would be in charge of converting the data sent by Gunicorn to the ASGI standard for FastAPI to use it. - -## Install Gunicorn and Uvicorn +If you use the `fastapi` command:
```console -$ pip install "uvicorn[standard]" gunicorn - ----> 100% +$
 fastapi run --workers 4 main.py
+INFO     Using path main.py
+INFO     Resolved absolute path /home/user/code/awesomeapp/main.py
+INFO     Searching for package file structure from directories with __init__.py files
+INFO     Importing from /home/user/code/awesomeapp
+
+ โ•ญโ”€ Python module file โ”€โ•ฎ
+ โ”‚                      โ”‚
+ โ”‚  ๐Ÿ main.py          โ”‚
+ โ”‚                      โ”‚
+ โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
+
+INFO     Importing module main
+INFO     Found importable FastAPI app
+
+ โ•ญโ”€ Importable FastAPI app โ”€โ•ฎ
+ โ”‚                          โ”‚
+ โ”‚  from main import app    โ”‚
+ โ”‚                          โ”‚
+ โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
+
+INFO     Using import string main:app
+
+ โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ FastAPI CLI - Production mode โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
+ โ”‚                                                     โ”‚
+ โ”‚  Serving at: http://0.0.0.0:8000                    โ”‚
+ โ”‚                                                     โ”‚
+ โ”‚  API docs: http://0.0.0.0:8000/docs                 โ”‚
+ โ”‚                                                     โ”‚
+ โ”‚  Running in production mode, for development use:   โ”‚
+ โ”‚                                                     โ”‚
+ โ”‚  fastapi dev                                        โ”‚
+ โ”‚                                                     โ”‚
+ โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
+
+INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
+INFO:     Started parent process [27365]
+INFO:     Started server process [27368]
+INFO:     Waiting for application startup.
+INFO:     Application startup complete.
+INFO:     Started server process [27369]
+INFO:     Waiting for application startup.
+INFO:     Application startup complete.
+INFO:     Started server process [27370]
+INFO:     Waiting for application startup.
+INFO:     Application startup complete.
+INFO:     Started server process [27367]
+INFO:     Waiting for application startup.
+INFO:     Application startup complete.
+
```
-That will install both Uvicorn with the `standard` extra packages (to get high performance) and Gunicorn. - -## Run Gunicorn with Uvicorn Workers - -Then you can run Gunicorn with: - -
- -```console -$ gunicorn main:app --workers 4 --worker-class uvicorn.workers.UvicornWorker --bind 0.0.0.0:80 - -[19499] [INFO] Starting gunicorn 20.1.0 -[19499] [INFO] Listening at: http://0.0.0.0:80 (19499) -[19499] [INFO] Using worker: uvicorn.workers.UvicornWorker -[19511] [INFO] Booting worker with pid: 19511 -[19513] [INFO] Booting worker with pid: 19513 -[19514] [INFO] Booting worker with pid: 19514 -[19515] [INFO] Booting worker with pid: 19515 -[19511] [INFO] Started server process [19511] -[19511] [INFO] Waiting for application startup. -[19511] [INFO] Application startup complete. -[19513] [INFO] Started server process [19513] -[19513] [INFO] Waiting for application startup. -[19513] [INFO] Application startup complete. -[19514] [INFO] Started server process [19514] -[19514] [INFO] Waiting for application startup. -[19514] [INFO] Application startup complete. -[19515] [INFO] Started server process [19515] -[19515] [INFO] Waiting for application startup. -[19515] [INFO] Application startup complete. -``` - -
- -Let's see what each of those options mean: - -* `main:app`: This is the same syntax used by Uvicorn, `main` means the Python module named "`main`", so, a file `main.py`. And `app` is the name of the variable that is the **FastAPI** application. - * You can imagine that `main:app` is equivalent to a Python `import` statement like: +//// - ```Python - from main import app - ``` +//// tab | `uvicorn` - * So, the colon in `main:app` would be equivalent to the Python `import` part in `from main import app`. - -* `--workers`: The number of worker processes to use, each will run a Uvicorn worker, in this case, 4 workers. - -* `--worker-class`: The Gunicorn-compatible worker class to use in the worker processes. - * Here we pass the class that Gunicorn can import and use with: - - ```Python - import uvicorn.workers.UvicornWorker - ``` - -* `--bind`: This tells Gunicorn the IP and the port to listen to, using a colon (`:`) to separate the IP and the port. - * If you were running Uvicorn directly, instead of `--bind 0.0.0.0:80` (the Gunicorn option) you would use `--host 0.0.0.0` and `--port 80`. - -In the output, you can see that it shows the **PID** (process ID) of each process (it's just a number). - -You can see that: - -* The Gunicorn **process manager** starts with PID `19499` (in your case it will be a different number). -* Then it starts `Listening at: http://0.0.0.0:80`. -* Then it detects that it has to use the worker class at `uvicorn.workers.UvicornWorker`. -* And then it starts **4 workers**, each with its own PID: `19511`, `19513`, `19514`, and `19515`. - -Gunicorn would also take care of managing **dead processes** and **restarting** new ones if needed to keep the number of workers. So that helps in part with the **restart** concept from the list above. - -Nevertheless, you would probably also want to have something outside making sure to **restart Gunicorn** if necessary, and also to **run it on startup**, etc. - -## Uvicorn with Workers - -Uvicorn also has an option to start and run several **worker processes**. - -Nevertheless, as of now, Uvicorn's capabilities for handling worker processes are more limited than Gunicorn's. So, if you want to have a process manager at this level (at the Python level), then it might be better to try with Gunicorn as the process manager. - -In any case, you would run it like this: +If you prefer to use the `uvicorn` command directly:
@@ -149,13 +118,15 @@ $ uvicorn main:app --host 0.0.0.0 --port 8080 --workers 4
+//// + The only new option here is `--workers` telling Uvicorn to start 4 worker processes. You can also see that it shows the **PID** of each process, `27365` for the parent process (this is the **process manager**) and one for each worker process: `27368`, `27369`, `27370`, and `27367`. ## Deployment Concepts -Here you saw how to use **Gunicorn** (or Uvicorn) managing **Uvicorn worker processes** to **parallelize** the execution of the application, take advantage of **multiple cores** in the CPU, and be able to serve **more requests**. +Here you saw how to use multiple **workers** to **parallelize** the execution of the application, take advantage of **multiple cores** in the CPU, and be able to serve **more requests**. From the list of deployment concepts from above, using workers would mainly help with the **replication** part, and a little bit with the **restarts**, but you still need to take care of the others: @@ -168,15 +139,13 @@ From the list of deployment concepts from above, using workers would mainly help ## Containers and Docker -In the next chapter about [FastAPI in Containers - Docker](docker.md){.internal-link target=_blank} I'll tell some strategies you could use to handle the other **deployment concepts**. - -I'll also show you the **official Docker image** that includes **Gunicorn with Uvicorn workers** and some default configurations that can be useful for simple cases. +In the next chapter about [FastAPI in Containers - Docker](docker.md){.internal-link target=_blank} I'll explain some strategies you could use to handle the other **deployment concepts**. -There I'll also show you how to **build your own image from scratch** to run a single Uvicorn process (without Gunicorn). It is a simple process and is probably what you would want to do when using a distributed container management system like **Kubernetes**. +I'll show you how to **build your own image from scratch** to run a single Uvicorn process. It is a simple process and is probably what you would want to do when using a distributed container management system like **Kubernetes**. ## Recap -You can use **Gunicorn** (or also Uvicorn) as a process manager with Uvicorn workers to take advantage of **multi-core CPUs**, to run **multiple processes in parallel**. +You can use multiple worker processes with the `--workers` CLI option with the `fastapi` or `uvicorn` commands to take advantage of **multi-core CPUs**, to run **multiple processes in parallel**. You could use these tools and ideas if you are setting up **your own deployment system** while taking care of the other deployment concepts yourself. diff --git a/docs/en/docs/deployment/versions.md b/docs/en/docs/deployment/versions.md index e387d5712..23f49cf99 100644 --- a/docs/en/docs/deployment/versions.md +++ b/docs/en/docs/deployment/versions.md @@ -30,7 +30,7 @@ fastapi[standard]>=0.112.0,<0.113.0 that would mean that you would use the versions `0.112.0` or above, but less than `0.113.0`, for example, a version `0.112.2` would still be accepted. -If you use any other tool to manage your installations, like Poetry, Pipenv, or others, they all have a way that you can use to define specific versions for your packages. +If you use any other tool to manage your installations, like `uv`, Poetry, Pipenv, or others, they all have a way that you can use to define specific versions for your packages. ## Available versions diff --git a/docs/en/docs/environment-variables.md b/docs/en/docs/environment-variables.md new file mode 100644 index 000000000..43dd06add --- /dev/null +++ b/docs/en/docs/environment-variables.md @@ -0,0 +1,298 @@ +# Environment Variables + +/// tip + +If you already know what "environment variables" are and how to use them, feel free to skip this. + +/// + +An environment variable (also known as "**env var**") is a variable that lives **outside** of the Python code, in the **operating system**, and could be read by your Python code (or by other programs as well). + +Environment variables could be useful for handling application **settings**, as part of the **installation** of Python, etc. + +## Create and Use Env Vars + +You can **create** and use environment variables in the **shell (terminal)**, without needing Python: + +//// tab | Linux, macOS, Windows Bash + +
+ +```console +// You could create an env var MY_NAME with +$ export MY_NAME="Wade Wilson" + +// Then you could use it with other programs, like +$ echo "Hello $MY_NAME" + +Hello Wade Wilson +``` + +
+ +//// + +//// tab | Windows PowerShell + +
+ +```console +// Create an env var MY_NAME +$ $Env:MY_NAME = "Wade Wilson" + +// Use it with other programs, like +$ echo "Hello $Env:MY_NAME" + +Hello Wade Wilson +``` + +
+ +//// + +## Read env vars in Python + +You could also create environment variables **outside** of Python, in the terminal (or with any other method), and then **read them in Python**. + +For example you could have a file `main.py` with: + +```Python hl_lines="3" +import os + +name = os.getenv("MY_NAME", "World") +print(f"Hello {name} from Python") +``` + +/// tip + +The second argument to `os.getenv()` is the default value to return. + +If not provided, it's `None` by default, here we provide `"World"` as the default value to use. + +/// + +Then you could call that Python program: + +//// tab | Linux, macOS, Windows Bash + +
+ +```console +// Here we don't set the env var yet +$ python main.py + +// As we didn't set the env var, we get the default value + +Hello World from Python + +// But if we create an environment variable first +$ export MY_NAME="Wade Wilson" + +// And then call the program again +$ python main.py + +// Now it can read the environment variable + +Hello Wade Wilson from Python +``` + +
+ +//// + +//// tab | Windows PowerShell + +
+ +```console +// Here we don't set the env var yet +$ python main.py + +// As we didn't set the env var, we get the default value + +Hello World from Python + +// But if we create an environment variable first +$ $Env:MY_NAME = "Wade Wilson" + +// And then call the program again +$ python main.py + +// Now it can read the environment variable + +Hello Wade Wilson from Python +``` + +
+ +//// + +As environment variables can be set outside of the code, but can be read by the code, and don't have to be stored (committed to `git`) with the rest of the files, it's common to use them for configurations or **settings**. + +You can also create an environment variable only for a **specific program invocation**, that is only available to that program, and only for its duration. + +To do that, create it right before the program itself, on the same line: + +
+ +```console +// Create an env var MY_NAME in line for this program call +$ MY_NAME="Wade Wilson" python main.py + +// Now it can read the environment variable + +Hello Wade Wilson from Python + +// The env var no longer exists afterwards +$ python main.py + +Hello World from Python +``` + +
+ +/// tip + +You can read more about it at The Twelve-Factor App: Config. + +/// + +## Types and Validation + +These environment variables can only handle **text strings**, as they are external to Python and have to be compatible with other programs and the rest of the system (and even with different operating systems, as Linux, Windows, macOS). + +That means that **any value** read in Python from an environment variable **will be a `str`**, and any conversion to a different type or any validation has to be done in code. + +You will learn more about using environment variables for handling **application settings** in the [Advanced User Guide - Settings and Environment Variables](./advanced/settings.md){.internal-link target=_blank}. + +## `PATH` Environment Variable + +There is a **special** environment variable called **`PATH`** that is used by the operating systems (Linux, macOS, Windows) to find programs to run. + +The value of the variable `PATH` is a long string that is made of directories separated by a colon `:` on Linux and macOS, and by a semicolon `;` on Windows. + +For example, the `PATH` environment variable could look like this: + +//// tab | Linux, macOS + +```plaintext +/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin +``` + +This means that the system should look for programs in the directories: + +* `/usr/local/bin` +* `/usr/bin` +* `/bin` +* `/usr/sbin` +* `/sbin` + +//// + +//// tab | Windows + +```plaintext +C:\Program Files\Python312\Scripts;C:\Program Files\Python312;C:\Windows\System32 +``` + +This means that the system should look for programs in the directories: + +* `C:\Program Files\Python312\Scripts` +* `C:\Program Files\Python312` +* `C:\Windows\System32` + +//// + +When you type a **command** in the terminal, the operating system **looks for** the program in **each of those directories** listed in the `PATH` environment variable. + +For example, when you type `python` in the terminal, the operating system looks for a program called `python` in the **first directory** in that list. + +If it finds it, then it will **use it**. Otherwise it keeps looking in the **other directories**. + +### Installing Python and Updating the `PATH` + +When you install Python, you might be asked if you want to update the `PATH` environment variable. + +//// tab | Linux, macOS + +Let's say you install Python and it ends up in a directory `/opt/custompython/bin`. + +If you say yes to update the `PATH` environment variable, then the installer will add `/opt/custompython/bin` to the `PATH` environment variable. + +It could look like this: + +```plaintext +/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/custompython/bin +``` + +This way, when you type `python` in the terminal, the system will find the Python program in `/opt/custompython/bin` (the last directory) and use that one. + +//// + +//// tab | Windows + +Let's say you install Python and it ends up in a directory `C:\opt\custompython\bin`. + +If you say yes to update the `PATH` environment variable, then the installer will add `C:\opt\custompython\bin` to the `PATH` environment variable. + +```plaintext +C:\Program Files\Python312\Scripts;C:\Program Files\Python312;C:\Windows\System32;C:\opt\custompython\bin +``` + +This way, when you type `python` in the terminal, the system will find the Python program in `C:\opt\custompython\bin` (the last directory) and use that one. + +//// + +So, if you type: + +
+ +```console +$ python +``` + +
+ +//// tab | Linux, macOS + +The system will **find** the `python` program in `/opt/custompython/bin` and run it. + +It would be roughly equivalent to typing: + +
+ +```console +$ /opt/custompython/bin/python +``` + +
+ +//// + +//// tab | Windows + +The system will **find** the `python` program in `C:\opt\custompython\bin\python` and run it. + +It would be roughly equivalent to typing: + +
+ +```console +$ C:\opt\custompython\bin\python +``` + +
+ +//// + +This information will be useful when learning about [Virtual Environments](virtual-environments.md){.internal-link target=_blank}. + +## Conclusion + +With this you should have a basic understanding of what **environment variables** are and how to use them in Python. + +You can also read more about them in the Wikipedia for Environment Variable. + +In many cases it's not very obvious how environment variables would be useful and applicable right away. But they keep showing up in many different scenarios when you are developing, so it's good to know about them. + +For example, you will need this information in the next section, about [Virtual Environments](virtual-environments.md). diff --git a/docs/en/docs/img/tutorial/cookie-param-models/image01.png b/docs/en/docs/img/tutorial/cookie-param-models/image01.png new file mode 100644 index 000000000..85c370f80 Binary files /dev/null and b/docs/en/docs/img/tutorial/cookie-param-models/image01.png differ diff --git a/docs/en/docs/img/tutorial/header-param-models/image01.png b/docs/en/docs/img/tutorial/header-param-models/image01.png new file mode 100644 index 000000000..849dea3d8 Binary files /dev/null and b/docs/en/docs/img/tutorial/header-param-models/image01.png differ diff --git a/docs/en/docs/img/tutorial/query-param-models/image01.png b/docs/en/docs/img/tutorial/query-param-models/image01.png new file mode 100644 index 000000000..e7a61b61f Binary files /dev/null and b/docs/en/docs/img/tutorial/query-param-models/image01.png differ diff --git a/docs/en/docs/img/tutorial/request-form-models/image01.png b/docs/en/docs/img/tutorial/request-form-models/image01.png new file mode 100644 index 000000000..3fe32c03d Binary files /dev/null and b/docs/en/docs/img/tutorial/request-form-models/image01.png differ diff --git a/docs/en/docs/index.md b/docs/en/docs/index.md index d76ef498b..3ed3d7bf6 100644 --- a/docs/en/docs/index.md +++ b/docs/en/docs/index.md @@ -128,6 +128,8 @@ FastAPI stands on the shoulders of giants: ## Installation +Create and activate a virtual environment and then install FastAPI: +
```console @@ -388,7 +390,7 @@ Coming back to the previous code example, **FastAPI** will: * Check if there is an optional query parameter named `q` (as in `http://127.0.0.1:8000/items/foo?q=somequery`) for `GET` requests. * As the `q` parameter is declared with `= None`, it is optional. * Without the `None` it would be required (as is the body in the case with `PUT`). -* For `PUT` requests to `/items/{item_id}`, Read the body as JSON: +* For `PUT` requests to `/items/{item_id}`, read the body as JSON: * Check that it has a required attribute `name` that should be a `str`. * Check that it has a required attribute `price` that has to be a `float`. * Check that it has an optional attribute `is_offer`, that should be a `bool`, if present. diff --git a/docs/en/docs/python-types.md b/docs/en/docs/python-types.md index 4ed1fc680..6994adb5f 100644 --- a/docs/en/docs/python-types.md +++ b/docs/en/docs/python-types.md @@ -324,7 +324,7 @@ In Python 3.6 and above (including Python 3.10) you can declare it by importing {!../../../docs_src/python_types/tutorial009.py!} ``` -Using `Optional[str]` instead of just `str` will let the editor help you detecting errors where you could be assuming that a value is always a `str`, when it could actually be `None` too. +Using `Optional[str]` instead of just `str` will let the editor help you detect errors where you could be assuming that a value is always a `str`, when it could actually be `None` too. `Optional[Something]` is actually a shortcut for `Union[Something, None]`, they are equivalent. diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index f562ba06e..0411bbdda 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,326 @@ hide: ## Latest Changes +### Docs + +* ๐Ÿ“ Fix small typos in the documentation. PR [#12213](https://github.com/fastapi/fastapi/pull/12213) by [@svlandeg](https://github.com/svlandeg). + +## 0.115.0 + +### Highlights + +Now you can declare `Query`, `Header`, and `Cookie` parameters with Pydantic models. ๐ŸŽ‰ + +#### `Query` Parameter Models + +Use Pydantic models for `Query` parameters: + +```python +from typing import Annotated, Literal + +from fastapi import FastAPI, Query +from pydantic import BaseModel, Field + +app = FastAPI() + + +class FilterParams(BaseModel): + limit: int = Field(100, gt=0, le=100) + offset: int = Field(0, ge=0) + order_by: Literal["created_at", "updated_at"] = "created_at" + tags: list[str] = [] + + +@app.get("/items/") +async def read_items(filter_query: Annotated[FilterParams, Query()]): + return filter_query +``` + +Read the new docs: [Query Parameter Models](https://fastapi.tiangolo.com/tutorial/query-param-models/). + +#### `Header` Parameter Models + +Use Pydantic models for `Header` parameters: + +```python +from typing import Annotated + +from fastapi import FastAPI, Header +from pydantic import BaseModel + +app = FastAPI() + + +class CommonHeaders(BaseModel): + host: str + save_data: bool + if_modified_since: str | None = None + traceparent: str | None = None + x_tag: list[str] = [] + + +@app.get("/items/") +async def read_items(headers: Annotated[CommonHeaders, Header()]): + return headers +``` + +Read the new docs: [Header Parameter Models](https://fastapi.tiangolo.com/tutorial/header-param-models/). + +#### `Cookie` Parameter Models + +Use Pydantic models for `Cookie` parameters: + +```python +from typing import Annotated + +from fastapi import Cookie, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Cookies(BaseModel): + session_id: str + fatebook_tracker: str | None = None + googall_tracker: str | None = None + + +@app.get("/items/") +async def read_items(cookies: Annotated[Cookies, Cookie()]): + return cookies +``` + +Read the new docs: [Cookie Parameter Models](https://fastapi.tiangolo.com/tutorial/cookie-param-models/). + +#### Forbid Extra Query (Cookie, Header) Parameters + +Use Pydantic models to restrict extra values for `Query` parameters (also applies to `Header` and `Cookie` parameters). + +To achieve it, use Pydantic's `model_config = {"extra": "forbid"}`: + +```python +from typing import Annotated, Literal + +from fastapi import FastAPI, Query +from pydantic import BaseModel, Field + +app = FastAPI() + + +class FilterParams(BaseModel): + model_config = {"extra": "forbid"} + + limit: int = Field(100, gt=0, le=100) + offset: int = Field(0, ge=0) + order_by: Literal["created_at", "updated_at"] = "created_at" + tags: list[str] = [] + + +@app.get("/items/") +async def read_items(filter_query: Annotated[FilterParams, Query()]): + return filter_query +``` + +This applies to `Query`, `Header`, and `Cookie` parameters, read the new docs: + +* [Forbid Extra Query Parameters](https://fastapi.tiangolo.com/tutorial/query-param-models/#forbid-extra-query-parameters) +* [Forbid Extra Headers](https://fastapi.tiangolo.com/tutorial/header-param-models/#forbid-extra-headers) +* [Forbid Extra Cookies](https://fastapi.tiangolo.com/tutorial/cookie-param-models/#forbid-extra-cookies) + +### Features + +* โœจ Add support for Pydantic models for parameters using `Query`, `Cookie`, `Header`. PR [#12199](https://github.com/fastapi/fastapi/pull/12199) by [@tiangolo](https://github.com/tiangolo). + +### Translations + +* ๐ŸŒ Add Portuguese translation for `docs/pt/docs/advanced/security/http-basic-auth.md`. PR [#12195](https://github.com/fastapi/fastapi/pull/12195) by [@ceb10n](https://github.com/ceb10n). + +### Internal + +* โฌ† [pre-commit.ci] pre-commit autoupdate. PR [#12204](https://github.com/fastapi/fastapi/pull/12204) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). + +## 0.114.2 + +### Fixes + +* ๐Ÿ› Fix form field regression with `alias`. PR [#12194](https://github.com/fastapi/fastapi/pull/12194) by [@Wurstnase](https://github.com/Wurstnase). + +### Translations + +* ๐ŸŒ Add Portuguese translation for `docs/pt/docs/tutorial/request-form-models.md`. PR [#12175](https://github.com/fastapi/fastapi/pull/12175) by [@ceb10n](https://github.com/ceb10n). +* ๐ŸŒ Add Chinese translation for `docs/zh/docs/project-generation.md`. PR [#12170](https://github.com/fastapi/fastapi/pull/12170) by [@waketzheng](https://github.com/waketzheng). +* ๐ŸŒ Add Dutch translation for `docs/nl/docs/python-types.md`. PR [#12158](https://github.com/fastapi/fastapi/pull/12158) by [@maxscheijen](https://github.com/maxscheijen). + +### Internal + +* ๐Ÿ’ก Add comments with instructions for Playwright screenshot scripts. PR [#12193](https://github.com/fastapi/fastapi/pull/12193) by [@tiangolo](https://github.com/tiangolo). +* โž• Add inline-snapshot for tests. PR [#12189](https://github.com/fastapi/fastapi/pull/12189) by [@tiangolo](https://github.com/tiangolo). + +## 0.114.1 + +### Refactors + +* โšก๏ธ Improve performance in request body parsing with a cache for internal model fields. PR [#12184](https://github.com/fastapi/fastapi/pull/12184) by [@tiangolo](https://github.com/tiangolo). + +### Docs + +* ๐Ÿ“ Remove duplicate line in docs for `docs/en/docs/environment-variables.md`. PR [#12169](https://github.com/fastapi/fastapi/pull/12169) by [@prometek](https://github.com/prometek). + +### Translations + +* ๐ŸŒ Add Portuguese translation for `docs/pt/docs/virtual-environments.md`. PR [#12163](https://github.com/fastapi/fastapi/pull/12163) by [@marcelomarkus](https://github.com/marcelomarkus). +* ๐ŸŒ Add Portuguese translation for `docs/pt/docs/environment-variables.md`. PR [#12162](https://github.com/fastapi/fastapi/pull/12162) by [@marcelomarkus](https://github.com/marcelomarkus). +* ๐ŸŒ Add Portuguese translation for `docs/pt/docs/tutorial/testing.md`. PR [#12164](https://github.com/fastapi/fastapi/pull/12164) by [@marcelomarkus](https://github.com/marcelomarkus). +* ๐ŸŒ Add Portuguese translation for `docs/pt/docs/tutorial/debugging.md`. PR [#12165](https://github.com/fastapi/fastapi/pull/12165) by [@marcelomarkus](https://github.com/marcelomarkus). +* ๐ŸŒ Add Korean translation for `docs/ko/docs/project-generation.md`. PR [#12157](https://github.com/fastapi/fastapi/pull/12157) by [@BORA040126](https://github.com/BORA040126). + +### Internal + +* โฌ† Bump tiangolo/issue-manager from 0.5.0 to 0.5.1. PR [#12173](https://github.com/fastapi/fastapi/pull/12173) by [@dependabot[bot]](https://github.com/apps/dependabot). +* โฌ† [pre-commit.ci] pre-commit autoupdate. PR [#12176](https://github.com/fastapi/fastapi/pull/12176) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ๐Ÿ‘ท Update `issue-manager.yml`. PR [#12159](https://github.com/fastapi/fastapi/pull/12159) by [@tiangolo](https://github.com/tiangolo). +* โœ๏ธ Fix typo in `fastapi/params.py`. PR [#12143](https://github.com/fastapi/fastapi/pull/12143) by [@surreal30](https://github.com/surreal30). + +## 0.114.0 + +You can restrict form fields to only include those declared in a Pydantic model and forbid any extra field sent in the request using Pydantic's `model_config = {"extra": "forbid"}`: + +```python +from typing import Annotated + +from fastapi import FastAPI, Form +from pydantic import BaseModel + +app = FastAPI() + + +class FormData(BaseModel): + username: str + password: str + model_config = {"extra": "forbid"} + + +@app.post("/login/") +async def login(data: Annotated[FormData, Form()]): + return data +``` + +Read the new docs: [Form Models - Forbid Extra Form Fields](https://fastapi.tiangolo.com/tutorial/request-form-models/#forbid-extra-form-fields). + +### Features + +* โœจ Add support for forbidding extra form fields with Pydantic models. PR [#12134](https://github.com/fastapi/fastapi/pull/12134) by [@tiangolo](https://github.com/tiangolo). + +### Docs + +* ๐Ÿ“ Update docs, Form Models section title, to match config name. PR [#12152](https://github.com/fastapi/fastapi/pull/12152) by [@tiangolo](https://github.com/tiangolo). + +### Internal + +* โœ… Update internal tests for latest Pydantic, including CI tweaks to install the latest Pydantic. PR [#12147](https://github.com/fastapi/fastapi/pull/12147) by [@tiangolo](https://github.com/tiangolo). + +## 0.113.0 + +Now you can declare form fields with Pydantic models: + +```python +from typing import Annotated + +from fastapi import FastAPI, Form +from pydantic import BaseModel + +app = FastAPI() + + +class FormData(BaseModel): + username: str + password: str + + +@app.post("/login/") +async def login(data: Annotated[FormData, Form()]): + return data +``` + +Read the new docs: [Form Models](https://fastapi.tiangolo.com/tutorial/request-form-models/). + +### Features + +* โœจ Add support for Pydantic models in `Form` parameters. PR [#12129](https://github.com/fastapi/fastapi/pull/12129) by [@tiangolo](https://github.com/tiangolo). + +### Internal + +* ๐Ÿ”ง Update sponsors: Coherence link. PR [#12130](https://github.com/fastapi/fastapi/pull/12130) by [@tiangolo](https://github.com/tiangolo). + +## 0.112.4 + +This release is mainly a big internal refactor to enable adding support for Pydantic models for `Form` fields, but that feature comes in the next release. + +This release shouldn't affect apps using FastAPI in any way. You don't even have to upgrade to this version yet. It's just a checkpoint. ๐Ÿค“ + +### Refactors + +* โ™ป๏ธ Refactor deciding if `embed` body fields, do not overwrite fields, compute once per router, refactor internals in preparation for Pydantic models in `Form`, `Query` and others. PR [#12117](https://github.com/fastapi/fastapi/pull/12117) by [@tiangolo](https://github.com/tiangolo). + +### Internal + +* โช๏ธ Temporarily revert "โœจ Add support for Pydantic models in `Form` parameters" to make a checkpoint release. PR [#12128](https://github.com/fastapi/fastapi/pull/12128) by [@tiangolo](https://github.com/tiangolo). Restored by PR [#12129](https://github.com/fastapi/fastapi/pull/12129). +* โœจ Add support for Pydantic models in `Form` parameters. PR [#12127](https://github.com/fastapi/fastapi/pull/12127) by [@tiangolo](https://github.com/tiangolo). Reverted by PR [#12128](https://github.com/fastapi/fastapi/pull/12128) to make a checkpoint release with only refactors. Restored by PR [#12129](https://github.com/fastapi/fastapi/pull/12129). + +## 0.112.3 + +This release is mainly internal refactors, it shouldn't affect apps using FastAPI in any way. You don't even have to upgrade to this version yet. There are a few bigger releases coming right after. ๐Ÿš€ + +### Refactors + +* โ™ป๏ธ Refactor internal `check_file_field()`, rename to `ensure_multipart_is_installed()` to clarify its purpose. PR [#12106](https://github.com/fastapi/fastapi/pull/12106) by [@tiangolo](https://github.com/tiangolo). +* โ™ป๏ธ Rename internal `create_response_field()` to `create_model_field()` as it's used for more than response models. PR [#12103](https://github.com/fastapi/fastapi/pull/12103) by [@tiangolo](https://github.com/tiangolo). +* โ™ป๏ธ Refactor and simplify internal data from `solve_dependencies()` using dataclasses. PR [#12100](https://github.com/fastapi/fastapi/pull/12100) by [@tiangolo](https://github.com/tiangolo). +* โ™ป๏ธ Refactor and simplify internal `analyze_param()` to structure data with dataclasses instead of tuple. PR [#12099](https://github.com/fastapi/fastapi/pull/12099) by [@tiangolo](https://github.com/tiangolo). +* โ™ป๏ธ Refactor and simplify dependencies data structures with dataclasses. PR [#12098](https://github.com/fastapi/fastapi/pull/12098) by [@tiangolo](https://github.com/tiangolo). + +### Docs + +* ๐Ÿ“ Add External Link: Techniques and applications of SQLAlchemy global filters in FastAPI. PR [#12109](https://github.com/fastapi/fastapi/pull/12109) by [@TheShubhendra](https://github.com/TheShubhendra). +* ๐Ÿ“ Add note about `time.perf_counter()` in middlewares. PR [#12095](https://github.com/fastapi/fastapi/pull/12095) by [@tiangolo](https://github.com/tiangolo). +* ๐Ÿ“ Tweak middleware code sample `time.time()` to `time.perf_counter()`. PR [#11957](https://github.com/fastapi/fastapi/pull/11957) by [@domdent](https://github.com/domdent). +* ๐Ÿ”ง Update sponsors: Coherence. PR [#12093](https://github.com/fastapi/fastapi/pull/12093) by [@tiangolo](https://github.com/tiangolo). +* ๐Ÿ“ Fix async test example not to trigger DeprecationWarning. PR [#12084](https://github.com/fastapi/fastapi/pull/12084) by [@marcinsulikowski](https://github.com/marcinsulikowski). +* ๐Ÿ“ Update `docs_src/path_params_numeric_validations/tutorial006.py`. PR [#11478](https://github.com/fastapi/fastapi/pull/11478) by [@MuhammadAshiqAmeer](https://github.com/MuhammadAshiqAmeer). +* ๐Ÿ“ Update comma in `docs/en/docs/async.md`. PR [#12062](https://github.com/fastapi/fastapi/pull/12062) by [@Alec-Gillis](https://github.com/Alec-Gillis). +* ๐Ÿ“ Update docs about serving FastAPI: ASGI servers, Docker containers, etc.. PR [#12069](https://github.com/fastapi/fastapi/pull/12069) by [@tiangolo](https://github.com/tiangolo). +* ๐Ÿ“ Clarify `response_class` parameter, validations, and returning a response directly. PR [#12067](https://github.com/fastapi/fastapi/pull/12067) by [@tiangolo](https://github.com/tiangolo). +* ๐Ÿ“ Fix minor typos and issues in the documentation. PR [#12063](https://github.com/fastapi/fastapi/pull/12063) by [@svlandeg](https://github.com/svlandeg). +* ๐Ÿ“ Add note in Docker docs about ensuring graceful shutdowns and lifespan events with `CMD` exec form. PR [#11960](https://github.com/fastapi/fastapi/pull/11960) by [@GPla](https://github.com/GPla). + +### Translations + +* ๐ŸŒ Add Dutch translation for `docs/nl/docs/features.md`. PR [#12101](https://github.com/fastapi/fastapi/pull/12101) by [@maxscheijen](https://github.com/maxscheijen). +* ๐ŸŒ Add Portuguese translation for `docs/pt/docs/advanced/testing-events.md`. PR [#12108](https://github.com/fastapi/fastapi/pull/12108) by [@ceb10n](https://github.com/ceb10n). +* ๐ŸŒ Add Portuguese translation for `docs/pt/docs/advanced/security/index.md`. PR [#12114](https://github.com/fastapi/fastapi/pull/12114) by [@ceb10n](https://github.com/ceb10n). +* ๐ŸŒ Add Dutch translation for `docs/nl/docs/index.md`. PR [#12042](https://github.com/fastapi/fastapi/pull/12042) by [@svlandeg](https://github.com/svlandeg). +* ๐ŸŒ Update Chinese translation for `docs/zh/docs/how-to/index.md`. PR [#12070](https://github.com/fastapi/fastapi/pull/12070) by [@synthpop123](https://github.com/synthpop123). + +### Internal + +* โฌ† [pre-commit.ci] pre-commit autoupdate. PR [#12115](https://github.com/fastapi/fastapi/pull/12115) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* โฌ† Bump pypa/gh-action-pypi-publish from 1.10.0 to 1.10.1. PR [#12120](https://github.com/fastapi/fastapi/pull/12120) by [@dependabot[bot]](https://github.com/apps/dependabot). +* โฌ† Bump pillow from 10.3.0 to 10.4.0. PR [#12105](https://github.com/fastapi/fastapi/pull/12105) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ๐Ÿ’š Set `include-hidden-files` to `True` when using the `upload-artifact` GH action. PR [#12118](https://github.com/fastapi/fastapi/pull/12118) by [@svlandeg](https://github.com/svlandeg). +* โฌ† Bump pypa/gh-action-pypi-publish from 1.9.0 to 1.10.0. PR [#12112](https://github.com/fastapi/fastapi/pull/12112) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ๐Ÿ”ง Update sponsors link: Coherence. PR [#12097](https://github.com/fastapi/fastapi/pull/12097) by [@tiangolo](https://github.com/tiangolo). +* ๐Ÿ”ง Update labeler config to handle sponsorships data. PR [#12096](https://github.com/fastapi/fastapi/pull/12096) by [@tiangolo](https://github.com/tiangolo). +* ๐Ÿ”ง Update sponsors, remove Kong. PR [#12085](https://github.com/fastapi/fastapi/pull/12085) by [@tiangolo](https://github.com/tiangolo). +* โฌ† [pre-commit.ci] pre-commit autoupdate. PR [#12076](https://github.com/fastapi/fastapi/pull/12076) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ๐Ÿ‘ท Update `latest-changes` GitHub Action. PR [#12073](https://github.com/fastapi/fastapi/pull/12073) by [@tiangolo](https://github.com/tiangolo). + +## 0.112.2 + +### Fixes + +* ๐Ÿ› Fix `allow_inf_nan` option for Param and Body classes. PR [#11867](https://github.com/fastapi/fastapi/pull/11867) by [@giunio-prc](https://github.com/giunio-prc). +* ๐Ÿ› Ensure that `app.include_router` merges nested lifespans. PR [#9630](https://github.com/fastapi/fastapi/pull/9630) by [@Lancetnik](https://github.com/Lancetnik). + ### Refactors * ๐ŸŽจ Fix typing annotation for semi-internal `FastAPI.add_api_route()`. PR [#10240](https://github.com/fastapi/fastapi/pull/10240) by [@ordinary-jamie](https://github.com/ordinary-jamie). @@ -14,6 +334,8 @@ hide: ### Docs +* ๐Ÿ“ Fix a typo in `docs/en/docs/virtual-environments.md`. PR [#12064](https://github.com/fastapi/fastapi/pull/12064) by [@aymenkrifa](https://github.com/aymenkrifa). +* ๐Ÿ“ Add docs about Environment Variables and Virtual Environments. PR [#12054](https://github.com/fastapi/fastapi/pull/12054) by [@tiangolo](https://github.com/tiangolo). * ๐Ÿ“ Add Asyncer mention in async docs. PR [#12037](https://github.com/fastapi/fastapi/pull/12037) by [@tiangolo](https://github.com/tiangolo). * ๐Ÿ“ Move the Features docs to the top level to improve the main page menu. PR [#12036](https://github.com/fastapi/fastapi/pull/12036) by [@tiangolo](https://github.com/tiangolo). * โœ๏ธ Fix import typo in reference example for `Security`. PR [#11168](https://github.com/fastapi/fastapi/pull/11168) by [@0shah0](https://github.com/0shah0). @@ -31,8 +353,10 @@ hide: ### Translations +* ๐ŸŒ Add Portuguese translation for `docs/pt/docs/tutorial/request_file.md`. PR [#12018](https://github.com/fastapi/fastapi/pull/12018) by [@Joao-Pedro-P-Holanda](https://github.com/Joao-Pedro-P-Holanda). +* ๐ŸŒ Add Japanese translation for `docs/ja/docs/learn/index.md`. PR [#11592](https://github.com/fastapi/fastapi/pull/11592) by [@ukwhatn](https://github.com/ukwhatn). * ๐Ÿ“ Update Spanish translation docs for consistency. PR [#12044](https://github.com/fastapi/fastapi/pull/12044) by [@alejsdev](https://github.com/alejsdev). -* ๐ŸŒ Update docs about dependencies with yield. PR [#12028](https://github.com/fastapi/fastapi/pull/12028) by [@xuvjso](https://github.com/xuvjso). +* ๐ŸŒ Update Chinese translation for `docs/zh/docs/tutorial/dependencies/dependencies-with-yield.md`. PR [#12028](https://github.com/fastapi/fastapi/pull/12028) by [@xuvjso](https://github.com/xuvjso). * ๐Ÿ“ Update FastAPI People, do not translate to have the most recent info. PR [#12034](https://github.com/fastapi/fastapi/pull/12034) by [@tiangolo](https://github.com/tiangolo). * ๐ŸŒ Update Urdu translation for `docs/ur/docs/benchmarks.md`. PR [#10046](https://github.com/fastapi/fastapi/pull/10046) by [@AhsanSheraz](https://github.com/AhsanSheraz). @@ -174,7 +498,7 @@ Discussed here: [#11522](https://github.com/fastapi/fastapi/pull/11522) and here ### Upgrades * โž– Remove `orjson` and `ujson` from default dependencies. PR [#11842](https://github.com/tiangolo/fastapi/pull/11842) by [@tiangolo](https://github.com/tiangolo). - * These dependencies are still installed when you install with `pip install "fastapi[all]"`. But they not included in `pip install fastapi`. + * These dependencies are still installed when you install with `pip install "fastapi[all]"`. But they are not included in `pip install fastapi`. * ๐Ÿ“ Restored Swagger-UI links to use the latest version possible. PR [#11459](https://github.com/tiangolo/fastapi/pull/11459) by [@UltimateLobster](https://github.com/UltimateLobster). ### Docs diff --git a/docs/en/docs/tutorial/bigger-applications.md b/docs/en/docs/tutorial/bigger-applications.md index 97f6b205b..1c33fd051 100644 --- a/docs/en/docs/tutorial/bigger-applications.md +++ b/docs/en/docs/tutorial/bigger-applications.md @@ -1,6 +1,6 @@ # Bigger Applications - Multiple Files -If you are building an application or a web API, it's rarely the case that you can put everything on a single file. +If you are building an application or a web API, it's rarely the case that you can put everything in a single file. **FastAPI** provides a convenience tool to structure your application while keeping all the flexibility. @@ -478,7 +478,7 @@ We can declare all that without having to modify the original `APIRouter` by pas {!../../../docs_src/bigger_applications/app/main.py!} ``` -That way, the original `APIRouter` will keep unmodified, so we can still share that same `app/internal/admin.py` file with other projects in the organization. +That way, the original `APIRouter` will stay unmodified, so we can still share that same `app/internal/admin.py` file with other projects in the organization. The result is that in our app, each of the *path operations* from the `admin` module will have: @@ -519,12 +519,12 @@ As we cannot just isolate them and "mount" them independently of the rest, the * ## Check the automatic API docs -Now, run `uvicorn`, using the module `app.main` and the variable `app`: +Now, run your app:
```console -$ uvicorn app.main:app --reload +$ fastapi dev app/main.py INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) ``` diff --git a/docs/en/docs/tutorial/body-multiple-params.md b/docs/en/docs/tutorial/body-multiple-params.md index 511fb358e..d63cd2529 100644 --- a/docs/en/docs/tutorial/body-multiple-params.md +++ b/docs/en/docs/tutorial/body-multiple-params.md @@ -97,7 +97,7 @@ But you can also declare multiple body parameters, e.g. `item` and `user`: //// -In this case, **FastAPI** will notice that there are more than one body parameters in the function (two parameters that are Pydantic models). +In this case, **FastAPI** will notice that there is more than one body parameter in the function (there are two parameters that are Pydantic models). So, it will then use the parameter names as keys (field names) in the body, and expect a body like: diff --git a/docs/en/docs/tutorial/body-updates.md b/docs/en/docs/tutorial/body-updates.md index 261f44d33..3257f9a08 100644 --- a/docs/en/docs/tutorial/body-updates.md +++ b/docs/en/docs/tutorial/body-updates.md @@ -155,7 +155,7 @@ In summary, to apply partial updates you would: * Put that data in a Pydantic model. * Generate a `dict` without default values from the input model (using `exclude_unset`). * This way you can update only the values actually set by the user, instead of overriding values already stored with default values in your model. -* Create a copy of the stored model, updating it's attributes with the received partial updates (using the `update` parameter). +* Create a copy of the stored model, updating its attributes with the received partial updates (using the `update` parameter). * Convert the copied model to something that can be stored in your DB (for example, using the `jsonable_encoder`). * This is comparable to using the model's `.model_dump()` method again, but it makes sure (and converts) the values to data types that can be converted to JSON, for example, `datetime` to `str`. * Save the data to your DB. diff --git a/docs/en/docs/tutorial/body.md b/docs/en/docs/tutorial/body.md index 608b50dbb..83f08ce84 100644 --- a/docs/en/docs/tutorial/body.md +++ b/docs/en/docs/tutorial/body.md @@ -123,7 +123,7 @@ The JSON Schemas of your models will be part of your OpenAPI generated schema, a -And will be also used in the API docs inside each *path operation* that needs them: +And will also be used in the API docs inside each *path operation* that needs them: diff --git a/docs/en/docs/tutorial/cookie-param-models.md b/docs/en/docs/tutorial/cookie-param-models.md new file mode 100644 index 000000000..2aa3a1f59 --- /dev/null +++ b/docs/en/docs/tutorial/cookie-param-models.md @@ -0,0 +1,154 @@ +# Cookie Parameter Models + +If you have a group of **cookies** that are related, you can create a **Pydantic model** to declare them. ๐Ÿช + +This would allow you to **re-use the model** in **multiple places** and also to declare validations and metadata for all the parameters at once. ๐Ÿ˜Ž + +/// note + +This is supported since FastAPI version `0.115.0`. ๐Ÿค“ + +/// + +/// tip + +This same technique applies to `Query`, `Cookie`, and `Header`. ๐Ÿ˜Ž + +/// + +## Cookies with a Pydantic Model + +Declare the **cookie** parameters that you need in a **Pydantic model**, and then declare the parameter as `Cookie`: + +//// tab | Python 3.10+ + +```Python hl_lines="9-12 16" +{!> ../../../docs_src/cookie_param_models/tutorial001_an_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="9-12 16" +{!> ../../../docs_src/cookie_param_models/tutorial001_an_py39.py!} +``` + +//// + +//// tab | Python 3.8+ + +```Python hl_lines="10-13 17" +{!> ../../../docs_src/cookie_param_models/tutorial001_an.py!} +``` + +//// + +//// tab | Python 3.10+ non-Annotated + +/// tip + +Prefer to use the `Annotated` version if possible. + +/// + +```Python hl_lines="7-10 14" +{!> ../../../docs_src/cookie_param_models/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.8+ non-Annotated + +/// tip + +Prefer to use the `Annotated` version if possible. + +/// + +```Python hl_lines="9-12 16" +{!> ../../../docs_src/cookie_param_models/tutorial001.py!} +``` + +//// + +**FastAPI** will **extract** the data for **each field** from the **cookies** received in the request and give you the Pydantic model you defined. + +## Check the Docs + +You can see the defined cookies in the docs UI at `/docs`: + +
+ +
+ +/// info + +Have in mind that, as **browsers handle cookies** in special ways and behind the scenes, they **don't** easily allow **JavaScript** to touch them. + +If you go to the **API docs UI** at `/docs` you will be able to see the **documentation** for cookies for your *path operations*. + +But even if you **fill the data** and click "Execute", because the docs UI works with **JavaScript**, the cookies won't be sent, and you will see an **error** message as if you didn't write any values. + +/// + +## Forbid Extra Cookies + +In some special use cases (probably not very common), you might want to **restrict** the cookies that you want to receive. + +Your API now has the power to control its own cookie consent. ๐Ÿคช๐Ÿช + +You can use Pydantic's model configuration to `forbid` any `extra` fields: + +//// tab | Python 3.9+ + +```Python hl_lines="10" +{!> ../../../docs_src/cookie_param_models/tutorial002_an_py39.py!} +``` + +//// + +//// tab | Python 3.8+ + +```Python hl_lines="11" +{!> ../../../docs_src/cookie_param_models/tutorial002_an.py!} +``` + +//// + +//// tab | Python 3.8+ non-Annotated + +/// tip + +Prefer to use the `Annotated` version if possible. + +/// + +```Python hl_lines="10" +{!> ../../../docs_src/cookie_param_models/tutorial002.py!} +``` + +//// + +If a client tries to send some **extra cookies**, they will receive an **error** response. + +Poor cookie banners with all their effort to get your consent for the API to reject it. ๐Ÿช + +For example, if the client tries to send a `santa_tracker` cookie with a value of `good-list-please`, the client will receive an **error** response telling them that the `santa_tracker` cookie is not allowed: + +```json +{ + "detail": [ + { + "type": "extra_forbidden", + "loc": ["cookie", "santa_tracker"], + "msg": "Extra inputs are not permitted", + "input": "good-list-please", + } + ] +} +``` + +## Summary + +You can use **Pydantic models** to declare **cookies** in **FastAPI**. ๐Ÿ˜Ž diff --git a/docs/en/docs/tutorial/cors.md b/docs/en/docs/tutorial/cors.md index fd329e138..7dd0a5df5 100644 --- a/docs/en/docs/tutorial/cors.md +++ b/docs/en/docs/tutorial/cors.md @@ -40,7 +40,7 @@ You can configure it in your **FastAPI** application using the `CORSMiddleware`. * Create a list of allowed origins (as strings). * Add it as a "middleware" to your **FastAPI** application. -You can also specify if your backend allows: +You can also specify whether your backend allows: * Credentials (Authorization headers, Cookies, etc). * Specific HTTP methods (`POST`, `PUT`) or all of them with the wildcard `"*"`. diff --git a/docs/en/docs/tutorial/dependencies/classes-as-dependencies.md b/docs/en/docs/tutorial/dependencies/classes-as-dependencies.md index a392672bb..b3394f14e 100644 --- a/docs/en/docs/tutorial/dependencies/classes-as-dependencies.md +++ b/docs/en/docs/tutorial/dependencies/classes-as-dependencies.md @@ -381,7 +381,7 @@ The last `CommonQueryParams`, in: ...is what **FastAPI** will actually use to know what is the dependency. -From it is that FastAPI will extract the declared parameters and that is what FastAPI will actually call. +It is from this one that FastAPI will extract the declared parameters and that is what FastAPI will actually call. --- diff --git a/docs/en/docs/tutorial/extra-data-types.md b/docs/en/docs/tutorial/extra-data-types.md index 849dee41f..3009acaf3 100644 --- a/docs/en/docs/tutorial/extra-data-types.md +++ b/docs/en/docs/tutorial/extra-data-types.md @@ -49,7 +49,7 @@ Here are some of the additional data types you can use: * `Decimal`: * Standard Python `Decimal`. * In requests and responses, handled the same as a `float`. -* You can check all the valid pydantic data types here: Pydantic data types. +* You can check all the valid Pydantic data types here: Pydantic data types. ## Example diff --git a/docs/en/docs/tutorial/handling-errors.md b/docs/en/docs/tutorial/handling-errors.md index ca3cff661..14a3cf998 100644 --- a/docs/en/docs/tutorial/handling-errors.md +++ b/docs/en/docs/tutorial/handling-errors.md @@ -176,7 +176,7 @@ These are technical details that you might skip if it's not important for you no **FastAPI** uses it so that, if you use a Pydantic model in `response_model`, and your data has an error, you will see the error in your log. -But the client/user will not see it. Instead, the client will receive an "Internal Server Error" with a HTTP status code `500`. +But the client/user will not see it. Instead, the client will receive an "Internal Server Error" with an HTTP status code `500`. It should be this way because if you have a Pydantic `ValidationError` in your *response* or anywhere in your code (not in the client's *request*), it's actually a bug in your code. @@ -262,7 +262,7 @@ from starlette.exceptions import HTTPException as StarletteHTTPException ### Reuse **FastAPI**'s exception handlers -If you want to use the exception along with the same default exception handlers from **FastAPI**, You can import and reuse the default exception handlers from `fastapi.exception_handlers`: +If you want to use the exception along with the same default exception handlers from **FastAPI**, you can import and reuse the default exception handlers from `fastapi.exception_handlers`: ```Python hl_lines="2-5 15 21" {!../../../docs_src/handling_errors/tutorial006.py!} diff --git a/docs/en/docs/tutorial/header-param-models.md b/docs/en/docs/tutorial/header-param-models.md new file mode 100644 index 000000000..8deb0a455 --- /dev/null +++ b/docs/en/docs/tutorial/header-param-models.md @@ -0,0 +1,184 @@ +# Header Parameter Models + +If you have a group of related **header parameters**, you can create a **Pydantic model** to declare them. + +This would allow you to **re-use the model** in **multiple places** and also to declare validations and metadata for all the parameters at once. ๐Ÿ˜Ž + +/// note + +This is supported since FastAPI version `0.115.0`. ๐Ÿค“ + +/// + +## Header Parameters with a Pydantic Model + +Declare the **header parameters** that you need in a **Pydantic model**, and then declare the parameter as `Header`: + +//// tab | Python 3.10+ + +```Python hl_lines="9-14 18" +{!> ../../../docs_src/header_param_models/tutorial001_an_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="9-14 18" +{!> ../../../docs_src/header_param_models/tutorial001_an_py39.py!} +``` + +//// + +//// tab | Python 3.8+ + +```Python hl_lines="10-15 19" +{!> ../../../docs_src/header_param_models/tutorial001_an.py!} +``` + +//// + +//// tab | Python 3.10+ non-Annotated + +/// tip + +Prefer to use the `Annotated` version if possible. + +/// + +```Python hl_lines="7-12 16" +{!> ../../../docs_src/header_param_models/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.9+ non-Annotated + +/// tip + +Prefer to use the `Annotated` version if possible. + +/// + +```Python hl_lines="9-14 18" +{!> ../../../docs_src/header_param_models/tutorial001_py39.py!} +``` + +//// + +//// tab | Python 3.8+ non-Annotated + +/// tip + +Prefer to use the `Annotated` version if possible. + +/// + +```Python hl_lines="7-12 16" +{!> ../../../docs_src/header_param_models/tutorial001_py310.py!} +``` + +//// + +**FastAPI** will **extract** the data for **each field** from the **headers** in the request and give you the Pydantic model you defined. + +## Check the Docs + +You can see the required headers in the docs UI at `/docs`: + +
+ +
+ +## Forbid Extra Headers + +In some special use cases (probably not very common), you might want to **restrict** the headers that you want to receive. + +You can use Pydantic's model configuration to `forbid` any `extra` fields: + +//// tab | Python 3.10+ + +```Python hl_lines="10" +{!> ../../../docs_src/header_param_models/tutorial002_an_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="10" +{!> ../../../docs_src/header_param_models/tutorial002_an_py39.py!} +``` + +//// + +//// tab | Python 3.8+ + +```Python hl_lines="11" +{!> ../../../docs_src/header_param_models/tutorial002_an.py!} +``` + +//// + +//// tab | Python 3.10+ non-Annotated + +/// tip + +Prefer to use the `Annotated` version if possible. + +/// + +```Python hl_lines="8" +{!> ../../../docs_src/header_param_models/tutorial002_py310.py!} +``` + +//// + +//// tab | Python 3.9+ non-Annotated + +/// tip + +Prefer to use the `Annotated` version if possible. + +/// + +```Python hl_lines="10" +{!> ../../../docs_src/header_param_models/tutorial002_py39.py!} +``` + +//// + +//// tab | Python 3.8+ non-Annotated + +/// tip + +Prefer to use the `Annotated` version if possible. + +/// + +```Python hl_lines="10" +{!> ../../../docs_src/header_param_models/tutorial002.py!} +``` + +//// + +If a client tries to send some **extra headers**, they will receive an **error** response. + +For example, if the client tries to send a `tool` header with a value of `plumbus`, they will receive an **error** response telling them that the header parameter `tool` is not allowed: + +```json +{ + "detail": [ + { + "type": "extra_forbidden", + "loc": ["header", "tool"], + "msg": "Extra inputs are not permitted", + "input": "plumbus", + } + ] +} +``` + +## Summary + +You can use **Pydantic models** to declare **headers** in **FastAPI**. ๐Ÿ˜Ž diff --git a/docs/en/docs/tutorial/index.md b/docs/en/docs/tutorial/index.md index 5f8c51c4c..bf613aace 100644 --- a/docs/en/docs/tutorial/index.md +++ b/docs/en/docs/tutorial/index.md @@ -4,9 +4,7 @@ This tutorial shows you how to use **FastAPI** with most of its features, step b Each section gradually builds on the previous ones, but it's structured to separate topics, so that you can go directly to any specific one to solve your specific API needs. -It is also built to work as a future reference. - -So you can come back and see exactly what you need. +It is also built to work as a future reference so you can come back and see exactly what you need. ## Run the code @@ -71,7 +69,9 @@ Using it in your editor is what really shows you the benefits of FastAPI, seeing ## Install FastAPI -The first step is to install FastAPI: +The first step is to install FastAPI. + +Make sure you create a [virtual environment](../virtual-environments.md){.internal-link target=_blank}, activate it, and then **install FastAPI**:
@@ -95,7 +95,7 @@ If you don't want to have those optional dependencies, you can instead install ` There is also an **Advanced User Guide** that you can read later after this **Tutorial - User guide**. -The **Advanced User Guide**, builds on this, uses the same concepts, and teaches you some extra features. +The **Advanced User Guide** builds on this one, uses the same concepts, and teaches you some extra features. But you should first read the **Tutorial - User Guide** (what you are reading right now). diff --git a/docs/en/docs/tutorial/middleware.md b/docs/en/docs/tutorial/middleware.md index f0b3faf3b..199b593d3 100644 --- a/docs/en/docs/tutorial/middleware.md +++ b/docs/en/docs/tutorial/middleware.md @@ -29,7 +29,7 @@ The middleware function receives: * A function `call_next` that will receive the `request` as a parameter. * This function will pass the `request` to the corresponding *path operation*. * Then it returns the `response` generated by the corresponding *path operation*. -* You can then modify further the `response` before returning it. +* You can then further modify the `response` before returning it. ```Python hl_lines="8-9 11 14" {!../../../docs_src/middleware/tutorial001.py!} @@ -63,6 +63,12 @@ For example, you could add a custom header `X-Process-Time` containing the time {!../../../docs_src/middleware/tutorial001.py!} ``` +/// tip + +Here we use `time.perf_counter()` instead of `time.time()` because it can be more precise for these use cases. ๐Ÿค“ + +/// + ## Other middlewares You can later read more about other middlewares in the [Advanced User Guide: Advanced Middleware](../advanced/middleware.md){.internal-link target=_blank}. diff --git a/docs/en/docs/tutorial/query-param-models.md b/docs/en/docs/tutorial/query-param-models.md new file mode 100644 index 000000000..02e36dc0f --- /dev/null +++ b/docs/en/docs/tutorial/query-param-models.md @@ -0,0 +1,196 @@ +# Query Parameter Models + +If you have a group of **query parameters** that are related, you can create a **Pydantic model** to declare them. + +This would allow you to **re-use the model** in **multiple places** and also to declare validations and metadata for all the parameters at once. ๐Ÿ˜Ž + +/// note + +This is supported since FastAPI version `0.115.0`. ๐Ÿค“ + +/// + +## Query Parameters with a Pydantic Model + +Declare the **query parameters** that you need in a **Pydantic model**, and then declare the parameter as `Query`: + +//// tab | Python 3.10+ + +```Python hl_lines="9-13 17" +{!> ../../../docs_src/query_param_models/tutorial001_an_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="8-12 16" +{!> ../../../docs_src/query_param_models/tutorial001_an_py39.py!} +``` + +//// + +//// tab | Python 3.8+ + +```Python hl_lines="10-14 18" +{!> ../../../docs_src/query_param_models/tutorial001_an.py!} +``` + +//// + +//// tab | Python 3.10+ non-Annotated + +/// tip + +Prefer to use the `Annotated` version if possible. + +/// + +```Python hl_lines="9-13 17" +{!> ../../../docs_src/query_param_models/tutorial001_py310.py!} +``` + +//// + +//// tab | Python 3.9+ non-Annotated + +/// tip + +Prefer to use the `Annotated` version if possible. + +/// + +```Python hl_lines="8-12 16" +{!> ../../../docs_src/query_param_models/tutorial001_py39.py!} +``` + +//// + +//// tab | Python 3.8+ non-Annotated + +/// tip + +Prefer to use the `Annotated` version if possible. + +/// + +```Python hl_lines="9-13 17" +{!> ../../../docs_src/query_param_models/tutorial001_py310.py!} +``` + +//// + +**FastAPI** will **extract** the data for **each field** from the **query parameters** in the request and give you the Pydantic model you defined. + +## Check the Docs + +You can see the query parameters in the docs UI at `/docs`: + +
+ +
+ +## Forbid Extra Query Parameters + +In some special use cases (probably not very common), you might want to **restrict** the query parameters that you want to receive. + +You can use Pydantic's model configuration to `forbid` any `extra` fields: + +//// tab | Python 3.10+ + +```Python hl_lines="10" +{!> ../../../docs_src/query_param_models/tutorial002_an_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="9" +{!> ../../../docs_src/query_param_models/tutorial002_an_py39.py!} +``` + +//// + +//// tab | Python 3.8+ + +```Python hl_lines="11" +{!> ../../../docs_src/query_param_models/tutorial002_an.py!} +``` + +//// + +//// tab | Python 3.10+ non-Annotated + +/// tip + +Prefer to use the `Annotated` version if possible. + +/// + +```Python hl_lines="10" +{!> ../../../docs_src/query_param_models/tutorial002_py310.py!} +``` + +//// + +//// tab | Python 3.9+ non-Annotated + +/// tip + +Prefer to use the `Annotated` version if possible. + +/// + +```Python hl_lines="9" +{!> ../../../docs_src/query_param_models/tutorial002_py39.py!} +``` + +//// + +//// tab | Python 3.8+ non-Annotated + +/// tip + +Prefer to use the `Annotated` version if possible. + +/// + +```Python hl_lines="11" +{!> ../../../docs_src/query_param_models/tutorial002.py!} +``` + +//// + +If a client tries to send some **extra** data in the **query parameters**, they will receive an **error** response. + +For example, if the client tries to send a `tool` query parameter with a value of `plumbus`, like: + +```http +https://example.com/items/?limit=10&tool=plumbus +``` + +They will receive an **error** response telling them that the query parameter `tool` is not allowed: + +```json +{ + "detail": [ + { + "type": "extra_forbidden", + "loc": ["query", "tool"], + "msg": "Extra inputs are not permitted", + "input": "plumbus" + } + ] +} +``` + +## Summary + +You can use **Pydantic models** to declare **query parameters** in **FastAPI**. ๐Ÿ˜Ž + +/// tip + +Spoiler alert: you can also use Pydantic models to declare cookies and headers, but you will read about that later in the tutorial. ๐Ÿคซ + +/// diff --git a/docs/en/docs/tutorial/query-params-str-validations.md b/docs/en/docs/tutorial/query-params-str-validations.md index 859242d93..dd101a2a6 100644 --- a/docs/en/docs/tutorial/query-params-str-validations.md +++ b/docs/en/docs/tutorial/query-params-str-validations.md @@ -273,7 +273,7 @@ The **default** value of the **function parameter** is the **actual default** va 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. +When you don't use `Annotated` and instead use the **(old) default value style**, if you call that function without FastAPI in **other places**, 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. ๐Ÿš€ @@ -645,7 +645,7 @@ Remember that in most of the cases, when something is required, you can simply o ## Query parameter list / multiple values -When you define a query parameter explicitly with `Query` you can also declare it to receive a list of values, or said in other way, to receive multiple values. +When you define a query parameter explicitly with `Query` you can also declare it to receive a list of values, or said in another way, to receive multiple values. For example, to declare a query parameter `q` that can appear multiple times in the URL, you can write: @@ -1182,4 +1182,4 @@ Validations specific for strings: In these examples you saw how to declare validations for `str` values. -See the next chapters to see how to declare validations for other types, like numbers. +See the next chapters to learn how to declare validations for other types, like numbers. diff --git a/docs/en/docs/tutorial/request-files.md b/docs/en/docs/tutorial/request-files.md index 53d9eefde..9f19596a8 100644 --- a/docs/en/docs/tutorial/request-files.md +++ b/docs/en/docs/tutorial/request-files.md @@ -6,7 +6,11 @@ You can define files to be uploaded by the client using `File`. To receive uploaded files, first install `python-multipart`. -E.g. `pip install python-multipart`. +Make sure you create a [virtual environment](../virtual-environments.md){.internal-link target=_blank}, activate it, and then install it, for example: + +```console +$ pip install python-multipart +``` This is because uploaded files are sent as "form data". diff --git a/docs/en/docs/tutorial/request-form-models.md b/docs/en/docs/tutorial/request-form-models.md new file mode 100644 index 000000000..1440d17b8 --- /dev/null +++ b/docs/en/docs/tutorial/request-form-models.md @@ -0,0 +1,134 @@ +# Form Models + +You can use **Pydantic models** to declare **form fields** in FastAPI. + +/// info + +To use forms, first install `python-multipart`. + +Make sure you create a [virtual environment](../virtual-environments.md){.internal-link target=_blank}, activate it, and then install it, for example: + +```console +$ pip install python-multipart +``` + +/// + +/// note + +This is supported since FastAPI version `0.113.0`. ๐Ÿค“ + +/// + +## Pydantic Models for Forms + +You just need to declare a **Pydantic model** with the fields you want to receive as **form fields**, and then declare the parameter as `Form`: + +//// tab | Python 3.9+ + +```Python hl_lines="9-11 15" +{!> ../../../docs_src/request_form_models/tutorial001_an_py39.py!} +``` + +//// + +//// tab | Python 3.8+ + +```Python hl_lines="8-10 14" +{!> ../../../docs_src/request_form_models/tutorial001_an.py!} +``` + +//// + +//// tab | Python 3.8+ non-Annotated + +/// tip + +Prefer to use the `Annotated` version if possible. + +/// + +```Python hl_lines="7-9 13" +{!> ../../../docs_src/request_form_models/tutorial001.py!} +``` + +//// + +**FastAPI** will **extract** the data for **each field** from the **form data** in the request and give you the Pydantic model you defined. + +## Check the Docs + +You can verify it in the docs UI at `/docs`: + +
+ +
+ +## Forbid Extra Form Fields + +In some special use cases (probably not very common), you might want to **restrict** the form fields to only those declared in the Pydantic model. And **forbid** any **extra** fields. + +/// note + +This is supported since FastAPI version `0.114.0`. ๐Ÿค“ + +/// + +You can use Pydantic's model configuration to `forbid` any `extra` fields: + +//// tab | Python 3.9+ + +```Python hl_lines="12" +{!> ../../../docs_src/request_form_models/tutorial002_an_py39.py!} +``` + +//// + +//// tab | Python 3.8+ + +```Python hl_lines="11" +{!> ../../../docs_src/request_form_models/tutorial002_an.py!} +``` + +//// + +//// tab | Python 3.8+ non-Annotated + +/// tip + +Prefer to use the `Annotated` version if possible. + +/// + +```Python hl_lines="10" +{!> ../../../docs_src/request_form_models/tutorial002.py!} +``` + +//// + +If a client tries to send some extra data, they will receive an **error** response. + +For example, if the client tries to send the form fields: + +* `username`: `Rick` +* `password`: `Portal Gun` +* `extra`: `Mr. Poopybutthole` + +They will receive an error response telling them that the field `extra` is not allowed: + +```json +{ + "detail": [ + { + "type": "extra_forbidden", + "loc": ["body", "extra"], + "msg": "Extra inputs are not permitted", + "input": "Mr. Poopybutthole" + } + ] +} +``` + +## Summary + +You can use Pydantic models to declare form fields in FastAPI. ๐Ÿ˜Ž diff --git a/docs/en/docs/tutorial/request-forms-and-files.md b/docs/en/docs/tutorial/request-forms-and-files.md index 9b4342652..7830a2ba4 100644 --- a/docs/en/docs/tutorial/request-forms-and-files.md +++ b/docs/en/docs/tutorial/request-forms-and-files.md @@ -6,7 +6,11 @@ You can define files and form fields at the same time using `File` and `Form`. To receive uploaded files and/or form data, first install `python-multipart`. -E.g. `pip install python-multipart`. +Make sure you create a [virtual environment](../virtual-environments.md){.internal-link target=_blank}, activate it, and then install it, for example: + +```console +$ pip install python-multipart +``` /// diff --git a/docs/en/docs/tutorial/request-forms.md b/docs/en/docs/tutorial/request-forms.md index 88b5b86b6..87cfdefbc 100644 --- a/docs/en/docs/tutorial/request-forms.md +++ b/docs/en/docs/tutorial/request-forms.md @@ -6,7 +6,11 @@ When you need to receive form fields instead of JSON, you can use `Form`. To use forms, first install `python-multipart`. -E.g. `pip install python-multipart`. +Make sure you create a [virtual environment](../virtual-environments.md){.internal-link target=_blank}, activate it, and then install it, for example: + +```console +$ pip install python-multipart +``` /// diff --git a/docs/en/docs/tutorial/response-model.md b/docs/en/docs/tutorial/response-model.md index c17e32f90..6a2093e6d 100644 --- a/docs/en/docs/tutorial/response-model.md +++ b/docs/en/docs/tutorial/response-model.md @@ -133,8 +133,17 @@ Here we are declaring a `UserIn` model, it will contain a plaintext password: To use `EmailStr`, first install `email-validator`. -E.g. `pip install email-validator` -or `pip install pydantic[email]`. +Make sure you create a [virtual environment](../virtual-environments.md){.internal-link target=_blank}, activate it, and then install it, for example: + +```console +$ pip install email-validator +``` + +or with: + +```console +$ pip install "pydantic[email]" +``` /// diff --git a/docs/en/docs/tutorial/schema-extra-example.md b/docs/en/docs/tutorial/schema-extra-example.md index 70745b048..20dee3a4f 100644 --- a/docs/en/docs/tutorial/schema-extra-example.md +++ b/docs/en/docs/tutorial/schema-extra-example.md @@ -347,7 +347,7 @@ If the ideas above already work for you, that might be enough, and you probably Before OpenAPI 3.1.0, OpenAPI used an older and modified version of **JSON Schema**. -JSON Schema didn't have `examples`, so OpenAPI added it's own `example` field to its own modified version. +JSON Schema didn't have `examples`, so OpenAPI added its own `example` field to its own modified version. OpenAPI also added `example` and `examples` fields to other parts of the specification: diff --git a/docs/en/docs/tutorial/security/first-steps.md b/docs/en/docs/tutorial/security/first-steps.md index ed427a282..d1fe0163e 100644 --- a/docs/en/docs/tutorial/security/first-steps.md +++ b/docs/en/docs/tutorial/security/first-steps.md @@ -56,9 +56,13 @@ Prefer to use the `Annotated` version if possible. The `python-multipart` package is automatically installed with **FastAPI** when you run the `pip install "fastapi[standard]"` command. -However, if you use the `pip install fastapi` command, the `python-multipart` package is not included by default. To install it manually, use the following command: +However, if you use the `pip install fastapi` command, the `python-multipart` package is not included by default. -`pip install python-multipart` +To install it manually, make sure you create a [virtual environment](../../virtual-environments.md){.internal-link target=_blank}, activate it, and then install it with: + +```console +$ pip install python-multipart +``` This is because **OAuth2** uses "form data" for sending the `username` and `password`. @@ -148,7 +152,7 @@ A "bearer" token is not the only option. But it's the best one for our use case. -And it might be the best for most use cases, unless you are an OAuth2 expert and know exactly why there's another option that suits better your needs. +And it might be the best for most use cases, unless you are an OAuth2 expert and know exactly why there's another option that better suits your needs. In that case, **FastAPI** also provides you with the tools to build it. diff --git a/docs/en/docs/tutorial/security/get-current-user.md b/docs/en/docs/tutorial/security/get-current-user.md index 6f3bf3944..7faaa3e13 100644 --- a/docs/en/docs/tutorial/security/get-current-user.md +++ b/docs/en/docs/tutorial/security/get-current-user.md @@ -316,7 +316,7 @@ And you can make it as complex as you want. And still, have it written only once But you can have thousands of endpoints (*path operations*) using the same security system. -And all of them (or any portion of them that you want) can take the advantage of re-using these dependencies or any other dependencies you create. +And all of them (or any portion of them that you want) can take advantage of re-using these dependencies or any other dependencies you create. And all these thousands of *path operations* can be as small as 3 lines: diff --git a/docs/en/docs/tutorial/security/oauth2-jwt.md b/docs/en/docs/tutorial/security/oauth2-jwt.md index 52877b916..ba2bfef29 100644 --- a/docs/en/docs/tutorial/security/oauth2-jwt.md +++ b/docs/en/docs/tutorial/security/oauth2-jwt.md @@ -28,7 +28,9 @@ If you want to play with JWT tokens and see how they work, check @@ -70,7 +72,7 @@ It supports many secure hashing algorithms and utilities to work with them. The recommended algorithm is "Bcrypt". -So, install PassLib with Bcrypt: +Make sure you create a [virtual environment](../virtual-environments.md){.internal-link target=_blank}, activate it, and then install PassLib with Bcrypt:
diff --git a/docs/en/docs/tutorial/sql-databases.md b/docs/en/docs/tutorial/sql-databases.md index 0645cc9f1..56971bf9d 100644 --- a/docs/en/docs/tutorial/sql-databases.md +++ b/docs/en/docs/tutorial/sql-databases.md @@ -101,7 +101,9 @@ Now let's see what each file/module does. ## Install `SQLAlchemy` -First you need to install `SQLAlchemy`: +First you need to install `SQLAlchemy`. + +Make sure you create a [virtual environment](../virtual-environments.md){.internal-link target=_blank}, activate it, and then install it, for example:
diff --git a/docs/en/docs/tutorial/testing.md b/docs/en/docs/tutorial/testing.md index 95c8c5bef..06a87e92e 100644 --- a/docs/en/docs/tutorial/testing.md +++ b/docs/en/docs/tutorial/testing.md @@ -12,7 +12,11 @@ With it, you can use `httpx`. -E.g. `pip install httpx`. +Make sure you create a [virtual environment](../virtual-environments.md){.internal-link target=_blank}, activate it, and then install it, for example: + +```console +$ pip install httpx +``` /// @@ -206,7 +210,9 @@ If you have a Pydantic model in your test and you want to send its data to the a ## Run it -After that, you just need to install `pytest`: +After that, you just need to install `pytest`. + +Make sure you create a [virtual environment](../virtual-environments.md){.internal-link target=_blank}, activate it, and then install it, for example:
diff --git a/docs/en/docs/virtual-environments.md b/docs/en/docs/virtual-environments.md new file mode 100644 index 000000000..fcc72fbe7 --- /dev/null +++ b/docs/en/docs/virtual-environments.md @@ -0,0 +1,844 @@ +# Virtual Environments + +When you work in Python projects you probably should use a **virtual environment** (or a similar mechanism) to isolate the packages you install for each project. + +/// info + +If you already know about virtual environments, how to create them and use them, you might want to skip this section. ๐Ÿค“ + +/// + +/// tip + +A **virtual environment** is different than an **environment variable**. + +An **environment variable** is a variable in the system that can be used by programs. + +A **virtual environment** is a directory with some files in it. + +/// + +/// info + +This page will teach you how to use **virtual environments** and how they work. + +If you are ready to adopt a **tool that manages everything** for you (including installing Python), try uv. + +/// + +## Create a Project + +First, create a directory for your project. + +What I normally do is that I create a directory named `code` inside my home/user directory. + +And inside of that I create one directory per project. + +
+ +```console +// Go to the home directory +$ cd +// Create a directory for all your code projects +$ mkdir code +// Enter into that code directory +$ cd code +// Create a directory for this project +$ mkdir awesome-project +// Enter into that project directory +$ cd awesome-project +``` + +
+ +## Create a Virtual Environment + +When you start working on a Python project **for the first time**, create a virtual environment **inside your project**. + +/// tip + +You only need to do this **once per project**, not every time you work. + +/// + +//// tab | `venv` + +To create a virtual environment, you can use the `venv` module that comes with Python. + +
+ +```console +$ python -m venv .venv +``` + +
+ +/// details | What that command means + +* `python`: use the program called `python` +* `-m`: call a module as a script, we'll tell it which module next +* `venv`: use the module called `venv` that normally comes installed with Python +* `.venv`: create the virtual environment in the new directory `.venv` + +/// + +//// + +//// tab | `uv` + +If you have `uv` installed, you can use it to create a virtual environment. + +
+ +```console +$ uv venv +``` + +
+ +/// tip + +By default, `uv` will create a virtual environment in a directory called `.venv`. + +But you could customize it passing an additional argument with the directory name. + +/// + +//// + +That command creates a new virtual environment in a directory called `.venv`. + +/// details | `.venv` or other name + +You could create the virtual environment in a different directory, but there's a convention of calling it `.venv`. + +/// + +## Activate the Virtual Environment + +Activate the new virtual environment so that any Python command you run or package you install uses it. + +/// tip + +Do this **every time** you start a **new terminal session** to work on the project. + +/// + +//// tab | Linux, macOS + +
+ +```console +$ source .venv/bin/activate +``` + +
+ +//// + +//// tab | Windows PowerShell + +
+ +```console +$ .venv\Scripts\Activate.ps1 +``` + +
+ +//// + +//// tab | Windows Bash + +Or if you use Bash for Windows (e.g. Git Bash): + +
+ +```console +$ source .venv/Scripts/activate +``` + +
+ +//// + +/// tip + +Every time you install a **new package** in that environment, **activate** the environment again. + +This makes sure that if you use a **terminal (CLI) program** installed by that package, you use the one from your virtual environment and not any other that could be installed globally, probably with a different version than what you need. + +/// + +## Check the Virtual Environment is Active + +Check that the virtual environment is active (the previous command worked). + +/// tip + +This is **optional**, but it's a good way to **check** that everything is working as expected and you are using the virtual environment you intended. + +/// + +//// tab | Linux, macOS, Windows Bash + +
+ +```console +$ which python + +/home/user/code/awesome-project/.venv/bin/python +``` + +
+ +If it shows the `python` binary at `.venv/bin/python`, inside of your project (in this case `awesome-project`), then it worked. ๐ŸŽ‰ + +//// + +//// tab | Windows PowerShell + +
+ +```console +$ Get-Command python + +C:\Users\user\code\awesome-project\.venv\Scripts\python +``` + +
+ +If it shows the `python` binary at `.venv\Scripts\python`, inside of your project (in this case `awesome-project`), then it worked. ๐ŸŽ‰ + +//// + +## Upgrade `pip` + +/// tip + +If you use `uv` you would use it to install things instead of `pip`, so you don't need to upgrade `pip`. ๐Ÿ˜Ž + +/// + +If you are using `pip` to install packages (it comes by default with Python), you should **upgrade** it to the latest version. + +Many exotic errors while installing a package are solved by just upgrading `pip` first. + +/// tip + +You would normally do this **once**, right after you create the virtual environment. + +/// + +Make sure the virtual environment is active (with the command above) and then run: + +
+ +```console +$ python -m pip install --upgrade pip + +---> 100% +``` + +
+ +## Add `.gitignore` + +If you are using **Git** (you should), add a `.gitignore` file to exclude everything in your `.venv` from Git. + +/// tip + +If you used `uv` to create the virtual environment, it already did this for you, you can skip this step. ๐Ÿ˜Ž + +/// + +/// tip + +Do this **once**, right after you create the virtual environment. + +/// + +
+ +```console +$ echo "*" > .venv/.gitignore +``` + +
+ +/// details | What that command means + +* `echo "*"`: will "print" the text `*` in the terminal (the next part changes that a bit) +* `>`: anything printed to the terminal by the command to the left of `>` should not be printed but instead written to the file that goes to the right of `>` +* `.gitignore`: the name of the file where the text should be written + +And `*` for Git means "everything". So, it will ignore everything in the `.venv` directory. + +That command will create a file `.gitignore` with the content: + +```gitignore +* +``` + +/// + +## Install Packages + +After activating the environment, you can install packages in it. + +/// tip + +Do this **once** when installing or upgrading the packages your project needs. + +If you need to upgrade a version or add a new package you would **do this again**. + +/// + +### Install Packages Directly + +If you're in a hurry and don't want to use a file to declare your project's package requirements, you can install them directly. + +/// tip + +It's a (very) good idea to put the packages and versions your program needs in a file (for example `requirements.txt` or `pyproject.toml`). + +/// + +//// tab | `pip` + +
+ +```console +$ pip install "fastapi[standard]" + +---> 100% +``` + +
+ +//// + +//// tab | `uv` + +If you have `uv`: + +
+ +```console +$ uv pip install "fastapi[standard]" +---> 100% +``` + +
+ +//// + +### Install from `requirements.txt` + +If you have a `requirements.txt`, you can now use it to install its packages. + +//// tab | `pip` + +
+ +```console +$ pip install -r requirements.txt +---> 100% +``` + +
+ +//// + +//// tab | `uv` + +If you have `uv`: + +
+ +```console +$ uv pip install -r requirements.txt +---> 100% +``` + +
+ +//// + +/// details | `requirements.txt` + +A `requirements.txt` with some packages could look like: + +```requirements.txt +fastapi[standard]==0.113.0 +pydantic==2.8.0 +``` + +/// + +## Run Your Program + +After you activated the virtual environment, you can run your program, and it will use the Python inside of your virtual environment with the packages you installed there. + +
+ +```console +$ python main.py + +Hello World +``` + +
+ +## Configure Your Editor + +You would probably use an editor, make sure you configure it to use the same virtual environment you created (it will probably autodetect it) so that you can get autocompletion and inline errors. + +For example: + +* VS Code +* PyCharm + +/// tip + +You normally have to do this only **once**, when you create the virtual environment. + +/// + +## Deactivate the Virtual Environment + +Once you are done working on your project you can **deactivate** the virtual environment. + +
+ +```console +$ deactivate +``` + +
+ +This way, when you run `python` it won't try to run it from that virtual environment with the packages installed there. + +## Ready to Work + +Now you're ready to start working on your project. + + + +/// tip + +Do you want to understand what's all that above? + +Continue reading. ๐Ÿ‘‡๐Ÿค“ + +/// + +## Why Virtual Environments + +To work with FastAPI you need to install Python. + +After that, you would need to **install** FastAPI and any other **packages** you want to use. + +To install packages you would normally use the `pip` command that comes with Python (or similar alternatives). + +Nevertheless, if you just use `pip` directly, the packages would be installed in your **global Python environment** (the global installation of Python). + +### The Problem + +So, what's the problem with installing packages in the global Python environment? + +At some point, you will probably end up writing many different programs that depend on **different packages**. And some of these projects you work on will depend on **different versions** of the same package. ๐Ÿ˜ฑ + +For example, you could create a project called `philosophers-stone`, this program depends on another package called **`harry`, using the version `1`**. So, you need to install `harry`. + +```mermaid +flowchart LR + stone(philosophers-stone) -->|requires| harry-1[harry v1] +``` + +Then, at some point later, you create another project called `prisoner-of-azkaban`, and this project also depends on `harry`, but this project needs **`harry` version `3`**. + +```mermaid +flowchart LR + azkaban(prisoner-of-azkaban) --> |requires| harry-3[harry v3] +``` + +But now the problem is, if you install the packages globally (in the global environment) instead of in a local **virtual environment**, you will have to choose which version of `harry` to install. + +If you want to run `philosophers-stone` you will need to first install `harry` version `1`, for example with: + +
+ +```console +$ pip install "harry==1" +``` + +
+ +And then you would end up with `harry` version `1` installed in your global Python environment. + +```mermaid +flowchart LR + subgraph global[global env] + harry-1[harry v1] + end + subgraph stone-project[philosophers-stone project] + stone(philosophers-stone) -->|requires| harry-1 + end +``` + +But then if you want to run `prisoner-of-azkaban`, you will need to uninstall `harry` version `1` and install `harry` version `3` (or just installing version `3` would automatically uninstall version `1`). + +
+ +```console +$ pip install "harry==3" +``` + +
+ +And then you would end up with `harry` version `3` installed in your global Python environment. + +And if you try to run `philosophers-stone` again, there's a chance it would **not work** because it needs `harry` version `1`. + +```mermaid +flowchart LR + subgraph global[global env] + harry-1[harry v1] + style harry-1 fill:#ccc,stroke-dasharray: 5 5 + harry-3[harry v3] + end + subgraph stone-project[philosophers-stone project] + stone(philosophers-stone) -.-x|โ›”๏ธ| harry-1 + end + subgraph azkaban-project[prisoner-of-azkaban project] + azkaban(prisoner-of-azkaban) --> |requires| harry-3 + end +``` + +/// tip + +It's very common in Python packages to try the best to **avoid breaking changes** in **new versions**, but it's better to be safe, and install newer versions intentionally and when you can run the tests to check everything is working correctly. + +/// + +Now, imagine that with **many** other **packages** that all your **projects depend on**. That's very difficult to manage. And you would probably end up running some projects with some **incompatible versions** of the packages, and not knowing why something isn't working. + +Also, depending on your operating system (e.g. Linux, Windows, macOS), it could have come with Python already installed. And in that case it probably had some packages pre-installed with some specific versions **needed by your system**. If you install packages in the global Python environment, you could end up **breaking** some of the programs that came with your operating system. + +## Where are Packages Installed + +When you install Python, it creates some directories with some files in your computer. + +Some of these directories are the ones in charge of having all the packages you install. + +When you run: + +
+ +```console +// Don't run this now, it's just an example ๐Ÿค“ +$ pip install "fastapi[standard]" +---> 100% +``` + +
+ +That will download a compressed file with the FastAPI code, normally from PyPI. + +It will also **download** files for other packages that FastAPI depends on. + +Then it will **extract** all those files and put them in a directory in your computer. + +By default, it will put those files downloaded and extracted in the directory that comes with your Python installation, that's the **global environment**. + +## What are Virtual Environments + +The solution to the problems of having all the packages in the global environment is to use a **virtual environment for each project** you work on. + +A virtual environment is a **directory**, very similar to the global one, where you can install the packages for a project. + +This way, each project will have its own virtual environment (`.venv` directory) with its own packages. + +```mermaid +flowchart TB + subgraph stone-project[philosophers-stone project] + stone(philosophers-stone) --->|requires| harry-1 + subgraph venv1[.venv] + harry-1[harry v1] + end + end + subgraph azkaban-project[prisoner-of-azkaban project] + azkaban(prisoner-of-azkaban) --->|requires| harry-3 + subgraph venv2[.venv] + harry-3[harry v3] + end + end + stone-project ~~~ azkaban-project +``` + +## What Does Activating a Virtual Environment Mean + +When you activate a virtual environment, for example with: + +//// tab | Linux, macOS + +
+ +```console +$ source .venv/bin/activate +``` + +
+ +//// + +//// tab | Windows PowerShell + +
+ +```console +$ .venv\Scripts\Activate.ps1 +``` + +
+ +//// + +//// tab | Windows Bash + +Or if you use Bash for Windows (e.g. Git Bash): + +
+ +```console +$ source .venv/Scripts/activate +``` + +
+ +//// + +That command will create or modify some [environment variables](environment-variables.md){.internal-link target=_blank} that will be available for the next commands. + +One of those variables is the `PATH` variable. + +/// tip + +You can learn more about the `PATH` environment variable in the [Environment Variables](environment-variables.md#path-environment-variable){.internal-link target=_blank} section. + +/// + +Activating a virtual environment adds its path `.venv/bin` (on Linux and macOS) or `.venv\Scripts` (on Windows) to the `PATH` environment variable. + +Let's say that before activating the environment, the `PATH` variable looked like this: + +//// tab | Linux, macOS + +```plaintext +/usr/bin:/bin:/usr/sbin:/sbin +``` + +That means that the system would look for programs in: + +* `/usr/bin` +* `/bin` +* `/usr/sbin` +* `/sbin` + +//// + +//// tab | Windows + +```plaintext +C:\Windows\System32 +``` + +That means that the system would look for programs in: + +* `C:\Windows\System32` + +//// + +After activating the virtual environment, the `PATH` variable would look something like this: + +//// tab | Linux, macOS + +```plaintext +/home/user/code/awesome-project/.venv/bin:/usr/bin:/bin:/usr/sbin:/sbin +``` + +That means that the system will now start looking first look for programs in: + +```plaintext +/home/user/code/awesome-project/.venv/bin +``` + +before looking in the other directories. + +So, when you type `python` in the terminal, the system will find the Python program in + +```plaintext +/home/user/code/awesome-project/.venv/bin/python +``` + +and use that one. + +//// + +//// tab | Windows + +```plaintext +C:\Users\user\code\awesome-project\.venv\Scripts;C:\Windows\System32 +``` + +That means that the system will now start looking first look for programs in: + +```plaintext +C:\Users\user\code\awesome-project\.venv\Scripts +``` + +before looking in the other directories. + +So, when you type `python` in the terminal, the system will find the Python program in + +```plaintext +C:\Users\user\code\awesome-project\.venv\Scripts\python +``` + +and use that one. + +//// + +An important detail is that it will put the virtual environment path at the **beginning** of the `PATH` variable. The system will find it **before** finding any other Python available. This way, when you run `python`, it will use the Python **from the virtual environment** instead of any other `python` (for example, a `python` from a global environment). + +Activating a virtual environment also changes a couple of other things, but this is one of the most important things it does. + +## Checking a Virtual Environment + +When you check if a virtual environment is active, for example with: + +//// tab | Linux, macOS, Windows Bash + +
+ +```console +$ which python + +/home/user/code/awesome-project/.venv/bin/python +``` + +
+ +//// + +//// tab | Windows PowerShell + +
+ +```console +$ Get-Command python + +C:\Users\user\code\awesome-project\.venv\Scripts\python +``` + +
+ +//// + +That means that the `python` program that will be used is the one **in the virtual environment**. + +you use `which` in Linux and macOS and `Get-Command` in Windows PowerShell. + +The way that command works is that it will go and check in the `PATH` environment variable, going through **each path in order**, looking for the program called `python`. Once it finds it, it will **show you the path** to that program. + +The most important part is that when you call `python`, that is the exact "`python`" that will be executed. + +So, you can confirm if you are in the correct virtual environment. + +/// tip + +It's easy to activate one virtual environment, get one Python, and then **go to another project**. + +And the second project **wouldn't work** because you are using the **incorrect Python**, from a virtual environment for another project. + +It's useful being able to check what `python` is being used. ๐Ÿค“ + +/// + +## Why Deactivate a Virtual Environment + +For example, you could be working on a project `philosophers-stone`, **activate that virtual environment**, install packages and work with that environment. + +And then you want to work on **another project** `prisoner-of-azkaban`. + +You go to that project: + +
+ +```console +$ cd ~/code/prisoner-of-azkaban +``` + +
+ +If you don't deactivate the virtual environment for `philosophers-stone`, when you run `python` in the terminal, it will try to use the Python from `philosophers-stone`. + +
+ +```console +$ cd ~/code/prisoner-of-azkaban + +$ python main.py + +// Error importing sirius, it's not installed ๐Ÿ˜ฑ +Traceback (most recent call last): + File "main.py", line 1, in + import sirius +``` + +
+ +But if you deactivate the virtual environment and activate the new one for `prisoner-of-askaban` then when you run `python` it will use the Python from the virtual environment in `prisoner-of-azkaban`. + +
+ +```console +$ cd ~/code/prisoner-of-azkaban + +// You don't need to be in the old directory to deactivate, you can do it wherever you are, even after going to the other project ๐Ÿ˜Ž +$ deactivate + +// Activate the virtual environment in prisoner-of-azkaban/.venv ๐Ÿš€ +$ source .venv/bin/activate + +// Now when you run python, it will find the package sirius installed in this virtual environment โœจ +$ python main.py + +I solemnly swear ๐Ÿบ +``` + +
+ +## Alternatives + +This is a simple guide to get you started and teach you how everything works **underneath**. + +There are many **alternatives** to managing virtual environments, package dependencies (requirements), projects. + +Once you are ready and want to use a tool to **manage the entire project**, packages dependencies, virtual environments, etc. I would suggest you try uv. + +`uv` can do a lot of things, it can: + +* **Install Python** for you, including different versions +* Manage the **virtual environment** for your projects +* Install **packages** +* Manage package **dependencies and versions** for your project +* Make sure you have an **exact** set of packages and versions to install, including their dependencies, so that you can be sure that you can run your project in production exactly the same as in your computer while developing, this is called **locking** +* And many other things + +## Conclusion + +If you read and understood all this, now **you know much more** about virtual environments than many developers out there. ๐Ÿค“ + +Knowing these details will most probably be useful in a future time when you are debugging something that seems complex, but you will know **how it all works underneath**. ๐Ÿ˜Ž diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index d0527ca3c..5161b891b 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -108,6 +108,8 @@ nav: - learn/index.md - python-types.md - async.md + - environment-variables.md + - virtual-environments.md - Tutorial - User Guide: - tutorial/index.md - tutorial/first-steps.md @@ -116,6 +118,7 @@ nav: - tutorial/body.md - tutorial/query-params-str-validations.md - tutorial/path-params-numeric-validations.md + - tutorial/query-param-models.md - tutorial/body-multiple-params.md - tutorial/body-fields.md - tutorial/body-nested-models.md @@ -123,10 +126,13 @@ nav: - tutorial/extra-data-types.md - tutorial/cookie-params.md - tutorial/header-params.md + - tutorial/cookie-param-models.md + - tutorial/header-param-models.md - tutorial/response-model.md - tutorial/extra-models.md - tutorial/response-status-code.md - tutorial/request-forms.md + - tutorial/request-form-models.md - tutorial/request-files.md - tutorial/request-forms-and-files.md - tutorial/handling-errors.md @@ -361,6 +367,8 @@ extra: name: ja - ๆ—ฅๆœฌ่ชž - link: /ko/ name: ko - ํ•œ๊ตญ์–ด + - link: /nl/ + name: nl - Nederlands - link: /pl/ name: pl - Polski - link: /pt/ diff --git a/docs/en/overrides/main.html b/docs/en/overrides/main.html index 229cbca71..463c5af3b 100644 --- a/docs/en/overrides/main.html +++ b/docs/en/overrides/main.html @@ -59,7 +59,7 @@
- + @@ -70,12 +70,6 @@
-
diff --git a/docs/ja/docs/learn/index.md b/docs/ja/docs/learn/index.md new file mode 100644 index 000000000..2f24c670a --- /dev/null +++ b/docs/ja/docs/learn/index.md @@ -0,0 +1,5 @@ +# ๅญฆ็ฟ’ + +ใ“ใ“ใงใฏใ€**FastAPI** ใ‚’ๅญฆ็ฟ’ใ™ใ‚‹ใŸใ‚ใฎๅ…ฅ้–€ใ‚ปใ‚ฏใ‚ทใƒงใƒณใจใƒใƒฅใƒผใƒˆใƒชใ‚ขใƒซใ‚’็ดนไป‹ใ—ใพใ™ใ€‚ + +ใ“ใ‚Œใฏใ€FastAPIใ‚’ๅญฆ็ฟ’ใ™ใ‚‹ใซใ‚ใŸใฃใฆใฎ**ๆ›ธ็ฑ**ใ‚„**ใ‚ณใƒผใ‚น**ใงใ‚ใ‚Šใ€**ๅ…ฌๅผ**ใ‹ใคๆŽจๅฅจใ•ใ‚Œใ‚‹ๆ–นๆณ•ใจใฟใชใ™ใ“ใจใŒใงใใพใ™ ๐Ÿ˜Ž diff --git a/docs/ko/docs/project-generation.md b/docs/ko/docs/project-generation.md new file mode 100644 index 000000000..019919f77 --- /dev/null +++ b/docs/ko/docs/project-generation.md @@ -0,0 +1,28 @@ +# Full Stack FastAPI ํ…œํ”Œ๋ฆฟ + +ํ…œํ”Œ๋ฆฟ์€ ์ผ๋ฐ˜์ ์œผ๋กœ ํŠน์ • ์„ค์ •๊ณผ ํ•จ๊ป˜ ์ œ๊ณต๋˜์ง€๋งŒ, ์œ ์—ฐํ•˜๊ณ  ์ปค์Šคํ„ฐ๋งˆ์ด์ง•์ด ๊ฐ€๋Šฅํ•˜๊ฒŒ ๋””์ž์ธ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ด ํŠน์„ฑ๋“ค์€ ์—ฌ๋Ÿฌ๋ถ„์ด ํ”„๋กœ์ ํŠธ์˜ ์š”๊ตฌ์‚ฌํ•ญ์— ๋งž์ถฐ ์ˆ˜์ •, ์ ์šฉ์„ ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๊ณ , ํ…œํ”Œ๋ฆฟ์ด ์™„๋ฒฝํ•œ ์‹œ์ž‘์ ์ด ๋˜๊ฒŒ ํ•ด์ค๋‹ˆ๋‹ค. ๐Ÿ + +๋งŽ์€ ์ดˆ๊ธฐ ์„ค์ •, ๋ณด์•ˆ, ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ฐ ์ผ๋ถ€ API ์—”๋“œํฌ์ธํŠธ๊ฐ€ ์ด๋ฏธ ์ค€๋น„๋˜์–ด ์žˆ์œผ๋ฏ€๋กœ, ์—ฌ๋Ÿฌ๋ถ„์€ ์ด ํ…œํ”Œ๋ฆฟ์„ (ํ”„๋กœ์ ํŠธ๋ฅผ) ์‹œ์ž‘ํ•˜๋Š” ๋ฐ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. + +GitHub ์ €์žฅ์†Œ: Full Stack FastAPI ํ…œํ”Œ๋ฆฟ + +## Full Stack FastAPI ํ…œํ”Œ๋ฆฟ - ๊ธฐ์ˆ  ์Šคํƒ๊ณผ ๊ธฐ๋Šฅ๋“ค + +- โšก [**FastAPI**](https://fastapi.tiangolo.com): Python ๋ฐฑ์—”๋“œ API. + - ๐Ÿงฐ [SQLModel](https://sqlmodel.tiangolo.com): Python SQL ๋ฐ์ดํ„ฐ ์ƒํ˜ธ์ž‘์šฉ์„ ์œ„ํ•œ (ORM). + - ๐Ÿ” [Pydantic](https://docs.pydantic.dev): FastAPI์— ์˜ํ•ด ์‚ฌ์šฉ๋˜๋Š”, ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ๊ณผ ์„ค์ •๊ด€๋ฆฌ. + - ๐Ÿ’พ [PostgreSQL](https://www.postgresql.org): SQL ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค. +- ๐Ÿš€ [React](https://react.dev): ํ”„๋ก ํŠธ์—”๋“œ. + - ๐Ÿ’ƒ TypeScript, hooks, Vite ๋ฐ ๊ธฐํƒ€ ํ˜„๋Œ€์ ์ธ ํ”„๋ก ํŠธ์—”๋“œ ์Šคํƒ์„ ์‚ฌ์šฉ. + - ๐ŸŽจ [Chakra UI](https://chakra-ui.com): ํ”„๋ก ํŠธ์—”๋“œ ์ปดํฌ๋„ŒํŠธ. + - ๐Ÿค– ์ž๋™์œผ๋กœ ์ƒ์„ฑ๋œ ํ”„๋ก ํŠธ์—”๋“œ ํด๋ผ์ด์–ธํŠธ. + - ๐Ÿงช E2E ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•œ Playwright. + - ๐Ÿฆ‡ ๋‹คํฌ ๋ชจ๋“œ ์ง€์›. +- ๐Ÿ‹ [Docker Compose](https://www.docker.com): ๊ฐœ๋ฐœ ํ™˜๊ฒฝ๊ณผ ํ”„๋กœ๋•์…˜(์šด์˜). +- ๐Ÿ”’ ๊ธฐ๋ณธ์œผ๋กœ ์ง€์›๋˜๋Š” ์•ˆ์ „ํ•œ ๋น„๋ฐ€๋ฒˆํ˜ธ ํ•ด์‹ฑ. +- ๐Ÿ”‘ JWT ํ† ํฐ ์ธ์ฆ. +- ๐Ÿ“ซ ์ด๋ฉ”์ผ ๊ธฐ๋ฐ˜ ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณต๊ตฌ. +- โœ… [Pytest]๋ฅผ ์ด์šฉํ•œ ํ…Œ์ŠคํŠธ(https://pytest.org). +- ๐Ÿ“ž [Traefik](https://traefik.io): ๋ฆฌ๋ฒ„์Šค ํ”„๋ก์‹œ / ๋กœ๋“œ ๋ฐธ๋Ÿฐ์„œ. +- ๐Ÿšข Docker Compose๋ฅผ ์ด์šฉํ•œ ๋ฐฐํฌ ์ง€์นจ: ์ž๋™ HTTPS ์ธ์ฆ์„œ๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์œ„ํ•œ ํ”„๋ก ํŠธ์—”๋“œ Traefik ํ”„๋ก์‹œ ์„ค์ • ๋ฐฉ๋ฒ•์„ ํฌํ•จ. +- ๐Ÿญ GitHub Actions๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ CI (์ง€์†์ ์ธ ํ†ตํ•ฉ) ๋ฐ CD (์ง€์†์ ์ธ ๋ฐฐํฌ). diff --git a/docs/nl/docs/features.md b/docs/nl/docs/features.md new file mode 100644 index 000000000..848b155ec --- /dev/null +++ b/docs/nl/docs/features.md @@ -0,0 +1,201 @@ +# Functionaliteit + +## FastAPI functionaliteit + +**FastAPI** biedt je het volgende: + +### Gebaseerd op open standaarden + +* OpenAPI voor het maken van API's, inclusief declaraties van padbewerkingen, parameters, request bodies, beveiliging, enz. +* Automatische datamodel documentatie met JSON Schema (aangezien OpenAPI zelf is gebaseerd op JSON Schema). +* Ontworpen op basis van deze standaarden, na zorgvuldig onderzoek. In plaats van achteraf deze laag er bovenop te bouwen. +* Dit maakt het ook mogelijk om automatisch **clientcode te genereren** in verschillende programmeertalen. + +### Automatische documentatie + +Interactieve API-documentatie en verkenning van webgebruikersinterfaces. Aangezien dit framework is gebaseerd op OpenAPI, zijn er meerdere documentatie opties mogelijk, waarvan er standaard 2 zijn inbegrepen. + +* Swagger UI, met interactieve interface, maakt het mogelijk je API rechtstreeks vanuit de browser aan te roepen en te testen. + +![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) + +* Alternatieve API-documentatie met ReDoc. + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) + +### Gewoon Moderne Python + +Het is allemaal gebaseerd op standaard **Python type** declaraties (dankzij Pydantic). Je hoeft dus geen nieuwe syntax te leren. Het is gewoon standaard moderne Python. + +Als je een opfriscursus van 2 minuten nodig hebt over het gebruik van Python types (zelfs als je FastAPI niet gebruikt), bekijk dan deze korte tutorial: [Python Types](python-types.md){.internal-link target=_blank}. + +Je schrijft gewoon standaard Python met types: + +```Python +from datetime import date + +from pydantic import BaseModel + +# Declareer een variabele als een str +# en krijg editorondersteuning in de functie +def main(user_id: str): + return user_id + + +# Een Pydantic model +class User(BaseModel): + id: int + name: str + joined: date +``` + +Vervolgens kan je het op deze manier gebruiken: + +```Python +my_user: User = User(id=3, name="John Doe", joined="2018-07-19") + +second_user_data = { + "id": 4, + "name": "Mary", + "joined": "2018-11-30", +} + +my_second_user: User = User(**second_user_data) +``` + +/// info + +`**second_user_data` betekent: + +Geef de sleutels (keys) en waarden (values) van de `second_user_data` dict direct door als sleutel-waarden argumenten, gelijk aan: `User(id=4, name=โ€œMaryโ€, joined=โ€œ2018-11-30โ€)` + +/// + +### Editor-ondersteuning + +Het gehele framework is ontworpen om eenvoudig en intuรฏtief te zijn in gebruik. Alle beslissingen zijn getest op meerdere code-editors nog voordat het daadwerkelijke ontwikkelen begon, om zo de beste ontwikkelervaring te garanderen. + +Uit enquรชtes onder Python ontwikkelaars blijkt maar al te duidelijk dat "(automatische) code aanvulling" een van de meest gebruikte functionaliteiten is. + +Het hele **FastAPI** framework is daarop gebaseerd. Automatische code aanvulling werkt overal. + +Je hoeft zelden terug te vallen op de documentatie. + +Zo kan je editor je helpen: + +* in Visual Studio Code: + +![editor ondersteuning](https://fastapi.tiangolo.com/img/vscode-completion.png) + +* in PyCharm: + +![editor ondersteuning](https://fastapi.tiangolo.com/img/pycharm-completion.png) + +Je krijgt autocomletion die je voorheen misschien zelfs voor onmogelijk had gehouden. Zoals bijvoorbeeld de `price` key in een JSON body (die genest had kunnen zijn) die afkomstig is van een request. + +Je hoeft niet langer de verkeerde keys in te typen, op en neer te gaan tussen de documentatie, of heen en weer te scrollen om te checken of je `username` of toch `user_name` had gebruikt. + +### Kort + +Dit framework heeft voor alles verstandige **standaardinstellingen**, met overal optionele configuraties. Alle parameters kunnen worden verfijnd zodat het past bij wat je nodig hebt, om zo de API te kunnen definiรซren die jij nodig hebt. + +Maar standaard werkt alles **โ€œgewoonโ€**. + +### Validatie + +* Validatie voor de meeste (of misschien wel alle?) Python **datatypes**, inclusief: + * JSON objecten (`dict`). + * JSON array (`list`) die itemtypes definiรซren. + * String (`str`) velden, die min en max lengtes hebben. + * Getallen (`int`, `float`) met min en max waarden, enz. + +* Validatie voor meer exotische typen, zoals: + * URL. + * E-mail. + * UUID. + * ...en anderen. + +Alle validatie wordt uitgevoerd door het beproefde en robuuste **Pydantic**. + +### Beveiliging en authenticatie + +Beveiliging en authenticatie is geรฏntegreerd. Zonder compromissen te doen naar databases of datamodellen. + +Alle beveiligingsschema's gedefinieerd in OpenAPI, inclusief: + +* HTTP Basic. +* **OAuth2** (ook met **JWT tokens**). Bekijk de tutorial over [OAuth2 with JWT](tutorial/security/oauth2-jwt.md){.internal-link target=_blank}. +* API keys in: + * Headers. + * Query parameters. + * Cookies, enz. + +Plus alle beveiligingsfuncties van Starlette (inclusief **sessiecookies**). + +Gebouwd als een herbruikbare tool met componenten die makkelijk te integreren zijn in en met je systemen, datastores, relationele en NoSQL databases, enz. + +### Dependency Injection + +FastAPI bevat een uiterst eenvoudig, maar uiterst krachtig Dependency Injection systeem. + +* Zelfs dependencies kunnen dependencies hebben, waardoor een hiรซrarchie of **โ€œgraphโ€ van dependencies** ontstaat. +* Allemaal **automatisch afgehandeld** door het framework. +* Alle dependencies kunnen data nodig hebben van request, de vereiste **padoperaties veranderen** en automatische documentatie verstrekken. +* **Automatische validatie** zelfs voor *padoperatie* parameters gedefinieerd in dependencies. +* Ondersteuning voor complexe gebruikersauthenticatiesystemen, **databaseverbindingen**, enz. +* **Geen compromisen** met databases, gebruikersinterfaces, enz. Maar eenvoudige integratie met ze allemaal. + +### Ongelimiteerde "plug-ins" + +Of anders gezegd, je hebt ze niet nodig, importeer en gebruik de code die je nodig hebt. + +Elke integratie is ontworpen om eenvoudig te gebruiken (met afhankelijkheden), zodat je een โ€œplug-in" kunt maken in 2 regels code, met dezelfde structuur en syntax die wordt gebruikt voor je *padbewerkingen*. + +### Getest + +* 100% van de code is getest. +* 100% type geannoteerde codebase. +* Wordt gebruikt in productietoepassingen. + +## Starlette functies + +**FastAPI** is volledig verenigbaar met (en gebaseerd op) Starlette. + +`FastAPI` is eigenlijk een subklasse van `Starlette`. Dus als je Starlette al kent of gebruikt, zal de meeste functionaliteit op dezelfde manier werken. + +Met **FastAPI** krijg je alle functies van **Starlette** (FastAPI is gewoon Starlette op steroรฏden): + +* Zeer indrukwekkende prestaties. Het is een van de snelste Python frameworks, vergelijkbaar met **NodeJS** en **Go**. +* **WebSocket** ondersteuning. +* Taken in de achtergrond tijdens het proces. +* Opstart- en afsluit events. +* Test client gebouwd op HTTPX. +* **CORS**, GZip, Statische bestanden, Streaming reacties. +* **Sessie en Cookie** ondersteuning. +* 100% van de code is getest. +* 100% type geannoteerde codebase. + +## Pydantic functionaliteit + +**FastAPI** is volledig verenigbaar met (en gebaseerd op) Pydantic. Dus alle extra Pydantic code die je nog hebt werkt ook. + +Inclusief externe pakketten die ook gebaseerd zijn op Pydantic, zoals ORMs, ODMs voor databases. + +Dit betekent ook dat je in veel gevallen het object dat je van een request krijgt **direct naar je database** kunt sturen, omdat alles automatisch wordt gevalideerd. + +Hetzelfde geldt ook andersom, in veel gevallen kun je dus het object dat je krijgt van de database **direct doorgeven aan de client**. + +Met **FastAPI** krijg je alle functionaliteit van **Pydantic** (omdat FastAPI is gebaseerd op Pydantic voor alle dataverwerking): + +* **Geen brainfucks**: + * Je hoeft geen nieuwe microtaal voor schemadefinities te leren. + * Als je bekend bent Python types, weet je hoe je Pydantic moet gebruiken. +* Werkt goed samen met je **IDE/linter/hersenen**: + * Doordat pydantic's datastructuren enkel instanties zijn van klassen, die je definieert, werkt automatische aanvulling, linting, mypy en je intuรฏtie allemaal goed met je gevalideerde data. +* Valideer **complexe structuren**: + * Gebruik van hiรซrarchische Pydantic modellen, Python `typing`'s `List` en `Dict`, enz. + * Met validators kunnen complexe dataschema's duidelijk en eenvoudig worden gedefinieerd, gecontroleerd en gedocumenteerd als JSON Schema. + * Je kunt diep **geneste JSON** objecten laten valideren en annoteren. +* **Uitbreidbaar**: + * Met Pydantic kunnen op maat gemaakte datatypen worden gedefinieerd of je kunt validatie uitbreiden met methoden op een model dat is ingericht met de decorator validator. +* 100% van de code is getest. diff --git a/docs/nl/docs/index.md b/docs/nl/docs/index.md new file mode 100644 index 000000000..8edc3ba0c --- /dev/null +++ b/docs/nl/docs/index.md @@ -0,0 +1,494 @@ +# FastAPI + + + +

+ FastAPI +

+

+ FastAPI framework, zeer goede prestaties, eenvoudig te leren, snel te programmeren, klaar voor productie +

+

+ + Test + + + Coverage + + + Package version + + + Supported Python versions + +

+ +--- + +**Documentatie**: https://fastapi.tiangolo.com + +**Broncode**: https://github.com/tiangolo/fastapi + +--- + +FastAPI is een modern, snel (zeer goede prestaties), web framework voor het bouwen van API's in Python, gebruikmakend van standaard Python type-hints. + +De belangrijkste kenmerken zijn: + +* **Snel**: Zeer goede prestaties, vergelijkbaar met **NodeJS** en **Go** (dankzij Starlette en Pydantic). [Een van de snelste beschikbare Python frameworks](#prestaties). +* **Snel te programmeren**: Verhoog de snelheid om functionaliteit te ontwikkelen met ongeveer 200% tot 300%. * +* **Minder bugs**: Verminder ongeveer 40% van de door mensen (ontwikkelaars) veroorzaakte fouten. * +* **Intuรฏtief**: Buitengewoon goede ondersteuning voor editors. Overal automische code aanvulling. Minder tijd kwijt aan debuggen. +* **Eenvoudig**: Ontworpen om gemakkelijk te gebruiken en te leren. Minder tijd nodig om documentatie te lezen. +* **Kort**: Minimaliseer codeduplicatie. Elke parameterdeclaratie ondersteunt meerdere functionaliteiten. Minder bugs. +* **Robust**: Code gereed voor productie. Met automatische interactieve documentatie. +* **Standards-based**: Gebaseerd op (en volledig verenigbaar met) open standaarden voor API's: OpenAPI (voorheen bekend als Swagger) en JSON Schema. + +* schatting op basis van testen met een intern ontwikkelteam en bouwen van productieapplicaties. + +## Sponsors + + + +{% if sponsors %} +{% for sponsor in sponsors.gold -%} + +{% endfor -%} +{%- for sponsor in sponsors.silver -%} + +{% endfor %} +{% endif %} + + + +Overige sponsoren + +## Meningen + +"_[...] Ik gebruik **FastAPI** heel vaak tegenwoordig. [...] Ik ben van plan om het te gebruiken voor alle **ML-services van mijn team bij Microsoft**. Sommige van deze worden geรฏntegreerd in het kernproduct van **Windows** en sommige **Office**-producten._" + +
Kabir Khan - Microsoft (ref)
+ +--- + +"_We hebben de **FastAPI** library gebruikt om een **REST** server te maken die bevraagd kan worden om **voorspellingen** te maken. [voor Ludwig]_" + +
Piero Molino, Yaroslav Dudin en Sai Sumanth Miryala - Uber (ref)
+ +--- + +"_**Netflix** is verheugd om een open-source release aan te kondigen van ons **crisismanagement**-orkestratieframework: **Dispatch**! [gebouwd met **FastAPI**]_" + +
Kevin Glisson, Marc Vilanova, Forest Monsen - Netflix (ref)
+ +--- + +"_Ik ben super enthousiast over **FastAPI**. Het is zo leuk!_" + +
Brian Okken - Python Bytes podcast presentator (ref)
+ +--- + +"_Wat je hebt gebouwd ziet er echt super solide en gepolijst uit. In veel opzichten is het wat ik wilde dat **Hug** kon zijn - het is echt inspirerend om iemand dit te zien bouwen._" + +
Timothy Crosley - Hug creator (ref)
+ +--- + +"Wie geรฏnteresseerd is in een **modern framework** voor het bouwen van REST API's, bekijkt best eens **FastAPI** [...] Het is snel, gebruiksvriendelijk en gemakkelijk te leren [...]_" + +"_We zijn overgestapt naar **FastAPI** voor onze **API's** [...] Het gaat jou vast ook bevallen [...]_" + +
Ines Montani - Matthew Honnibal - Explosion AI oprichters - spaCy ontwikkelaars (ref) - (ref)
+ +--- + +"_Wie een Python API wil bouwen voor productie, kan ik ten stelligste **FastAPI** aanraden. Het is **prachtig ontworpen**, **eenvoudig te gebruiken** en **gemakkelijk schaalbaar**, het is een **cruciale component** geworden in onze strategie om API's centraal te zetten, en het vereenvoudigt automatisering en diensten zoals onze Virtual TAC Engineer._" + +
Deon Pillsbury - Cisco (ref)
+ +--- + +## **Typer**, de FastAPI van CLIs + + + +Als je een CLI-app bouwt die in de terminal moet worden gebruikt in plaats van een web-API, gebruik dan **Typer**. + +**Typer** is het kleine broertje van FastAPI. En het is bedoeld als de **FastAPI van CLI's**. ๏ธ + +## Vereisten + +FastAPI staat op de schouders van reuzen: + +* Starlette voor de webonderdelen. +* Pydantic voor de datadelen. + +## Installatie + +
+ +```console +$ pip install "fastapi[standard]" + +---> 100% +``` + +
+ +**Opmerking**: Zet `"fastapi[standard]"` tussen aanhalingstekens om ervoor te zorgen dat het werkt in alle terminals. + +## Voorbeeld + +### Creรซer het + +* Maak het bestand `main.py` aan met daarin: + +```Python +from typing import Union + +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} +``` + +
+Of maak gebruik van async def... + +Als je code gebruik maakt van `async` / `await`, gebruik dan `async def`: + +```Python hl_lines="9 14" +from typing import Union + +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +async def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +async def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} +``` + +**Opmerking**: + +Als je het niet weet, kijk dan in het gedeelte _"Heb je haast?"_ over `async` en `await` in de documentatie. + +
+ +### Voer het uit + +Run de server met: + +
+ +```console +$ fastapi dev main.py + + โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ FastAPI CLI - Development mode โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ + โ”‚ โ”‚ + โ”‚ Serving at: http://127.0.0.1:8000 โ”‚ + โ”‚ โ”‚ + โ”‚ API docs: http://127.0.0.1:8000/docs โ”‚ + โ”‚ โ”‚ + โ”‚ Running in development mode, for production use: โ”‚ + โ”‚ โ”‚ + โ”‚ fastapi run โ”‚ + โ”‚ โ”‚ + โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ + +INFO: Will watch for changes in these directories: ['/home/user/code/awesomeapp'] +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +INFO: Started reloader process [2248755] using WatchFiles +INFO: Started server process [2248757] +INFO: Waiting for application startup. +INFO: Application startup complete. +``` + +
+ +
+Over het commando fastapi dev main.py... + +Het commando `fastapi dev` leest het `main.py` bestand, detecteert de **FastAPI** app, en start een server met Uvicorn. + +Standaard zal dit commando `fastapi dev` starten met "auto-reload" geactiveerd voor ontwikkeling op het lokale systeem. + +Je kan hier meer over lezen in de FastAPI CLI documentatie. + +
+ +### Controleer het + +Open je browser op http://127.0.0.1:8000/items/5?q=somequery. + +Je zult een JSON response zien: + +```JSON +{"item_id": 5, "q": "somequery"} +``` + +Je hebt een API gemaakt die: + +* HTTP verzoeken kan ontvangen op de _paden_ `/` en `/items/{item_id}`. +* Beide _paden_ hebben `GET` operaties (ook bekend als HTTP _methoden_). +* Het _pad_ `/items/{item_id}` heeft een _pad parameter_ `item_id` dat een `int` moet zijn. +* Het _pad_ `/items/{item_id}` heeft een optionele `str` _query parameter_ `q`. + +### Interactieve API documentatie + +Ga naar http://127.0.0.1:8000/docs. + +Je ziet de automatische interactieve API documentatie (verstrekt door Swagger UI): + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) + +### Alternatieve API documentatie + +Ga vervolgens naar http://127.0.0.1:8000/redoc. + +Je ziet de automatische interactieve API documentatie (verstrekt door ReDoc): + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) + +## Voorbeeld upgrade + +Pas nu het bestand `main.py` aan om de body van een `PUT` request te ontvangen. + +Dankzij Pydantic kunnen we de body declareren met standaard Python types. + +```Python hl_lines="4 9-12 25-27" +from typing import Union + +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + price: float + is_offer: Union[bool, None] = None + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} + + +@app.put("/items/{item_id}") +def update_item(item_id: int, item: Item): + return {"item_name": item.name, "item_id": item_id} +``` + +De `fastapi dev` server zou automatisch moeten herladen. + +### Interactieve API documentatie upgrade + +Ga nu naar http://127.0.0.1:8000/docs. + +* De interactieve API-documentatie wordt automatisch bijgewerkt, inclusief de nieuwe body: + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) + +* Klik op de knop "Try it out", hiermee kan je de parameters invullen en direct met de API interacteren: + +![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) + +* Klik vervolgens op de knop "Execute", de gebruikersinterface zal communiceren met jouw API, de parameters verzenden, de resultaten ophalen en deze op het scherm tonen: + +![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) + +### Alternatieve API documentatie upgrade + +Ga vervolgens naar http://127.0.0.1:8000/redoc. + +* De alternatieve documentatie zal ook de nieuwe queryparameter en body weergeven: + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) + +### Samenvatting + +Samengevat declareer je **eenmalig** de types van parameters, body, etc. als functieparameters. + +Dat doe je met standaard moderne Python types. + +Je hoeft geen nieuwe syntax te leren, de methods of klassen van een specifieke bibliotheek, etc. + +Gewoon standaard **Python**. + +Bijvoorbeeld, voor een `int`: + +```Python +item_id: int +``` + +of voor een complexer `Item` model: + +```Python +item: Item +``` + +...en met die ene verklaring krijg je: + +* Editor ondersteuning, inclusief: + * Code aanvulling. + * Type validatie. +* Validatie van data: + * Automatische en duidelijke foutboodschappen wanneer de data ongeldig is. + * Validatie zelfs voor diep geneste JSON objecten. +* Conversie van invoergegevens: afkomstig van het netwerk naar Python-data en -types. Zoals: + * JSON. + * Pad parameters. + * Query parameters. + * Cookies. + * Headers. + * Formulieren. + * Bestanden. +* Conversie van uitvoergegevens: converstie van Python-data en -types naar netwerkgegevens (zoals JSON): + * Converteer Python types (`str`, `int`, `float`, `bool`, `list`, etc). + * `datetime` objecten. + * `UUID` objecten. + * Database modellen. + * ...en nog veel meer. +* Automatische interactieve API-documentatie, inclusief 2 alternatieve gebruikersinterfaces: + * Swagger UI. + * ReDoc. + +--- + +Terugkomend op het vorige code voorbeeld, **FastAPI** zal: + +* Valideren dat er een `item_id` bestaat in het pad voor `GET` en `PUT` verzoeken. +* Valideren dat het `item_id` van het type `int` is voor `GET` en `PUT` verzoeken. + * Wanneer dat niet het geval is, krijgt de cliรซnt een nuttige, duidelijke foutmelding. +* Controleren of er een optionele query parameter is met de naam `q` (zoals in `http://127.0.0.1:8000/items/foo?q=somequery`) voor `GET` verzoeken. + * Aangezien de `q` parameter werd gedeclareerd met `= None`, is deze optioneel. + * Zonder de `None` declaratie zou deze verplicht zijn (net als bij de body in het geval met `PUT`). +* Voor `PUT` verzoeken naar `/items/{item_id}`, lees de body als JSON: + * Controleer of het een verplicht attribuut `naam` heeft en dat dat een `str` is. + * Controleer of het een verplicht attribuut `price` heeft en dat dat een`float` is. + * Controleer of het een optioneel attribuut `is_offer` heeft, dat een `bool` is wanneer het aanwezig is. + * Dit alles werkt ook voor diep geneste JSON objecten. +* Converteer automatisch van en naar JSON. +* Documenteer alles met OpenAPI, dat gebruikt kan worden door: + * Interactieve documentatiesystemen. + * Automatische client code generatie systemen, voor vele talen. +* Biedt 2 interactieve documentatie-webinterfaces aan. + +--- + +Dit was nog maar een snel overzicht, maar je zou nu toch al een idee moeten hebben over hoe het allemaal werkt. + +Probeer deze regel te veranderen: + +```Python + return {"item_name": item.name, "item_id": item_id} +``` + +...van: + +```Python + ... "item_name": item.name ... +``` + +...naar: + +```Python + ... "item_price": item.price ... +``` + +...en zie hoe je editor de attributen automatisch invult en hun types herkent: + +![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) + +Voor een vollediger voorbeeld met meer mogelijkheden, zie de Tutorial - Gebruikershandleiding. + +**Spoiler alert**: de tutorial - gebruikershandleiding bevat: + +* Declaratie van **parameters** op andere plaatsen zoals: **headers**, **cookies**, **formuliervelden** en **bestanden**. +* Hoe stel je **validatie restricties** in zoals `maximum_length` of een `regex`. +* Een zeer krachtig en eenvoudig te gebruiken **Dependency Injection** systeem. +* Beveiliging en authenticatie, inclusief ondersteuning voor **OAuth2** met **JWT-tokens** en **HTTP Basic** auth. +* Meer geavanceerde (maar even eenvoudige) technieken voor het declareren van **diep geneste JSON modellen** (dankzij Pydantic). +* **GraphQL** integratie met Strawberry en andere packages. +* Veel extra functies (dankzij Starlette) zoals: + * **WebSockets** + * uiterst gemakkelijke tests gebaseerd op HTTPX en `pytest` + * **CORS** + * **Cookie Sessions** + * ...en meer. + +## Prestaties + +Onafhankelijke TechEmpower benchmarks tonen **FastAPI** applicaties draaiend onder Uvicorn aan als een van de snelste Python frameworks beschikbaar, alleen onder Starlette en Uvicorn zelf (intern gebruikt door FastAPI). (*) + +Zie de sectie Benchmarks om hier meer over te lezen. + +## Afhankelijkheden + +FastAPI maakt gebruik van Pydantic en Starlette. + +### `standard` Afhankelijkheden + +Wanneer je FastAPI installeert met `pip install "fastapi[standard]"`, worden de volgende `standard` optionele afhankelijkheden geรฏnstalleerd: + +Gebruikt door Pydantic: + +* email_validator - voor email validatie. + +Gebruikt door Starlette: + +* httpx - Vereist indien je de `TestClient` wil gebruiken. +* jinja2 - Vereist als je de standaard templateconfiguratie wil gebruiken. +* python-multipart - Vereist indien je "parsen" van formulieren wil ondersteunen met `requests.form()`. + +Gebruikt door FastAPI / Starlette: + +* uvicorn - voor de server die jouw applicatie laadt en bedient. +* `fastapi-cli` - om het `fastapi` commando te voorzien. + +### Zonder `standard` Afhankelijkheden + +Indien je de optionele `standard` afhankelijkheden niet wenst te installeren, kan je installeren met `pip install fastapi` in plaats van `pip install "fastapi[standard]"`. + +### Bijkomende Optionele Afhankelijkheden + +Er zijn nog een aantal bijkomende afhankelijkheden die je eventueel kan installeren. + +Bijkomende optionele afhankelijkheden voor Pydantic: + +* pydantic-settings - voor het beheren van settings. +* pydantic-extra-types - voor extra data types die gebruikt kunnen worden met Pydantic. + +Bijkomende optionele afhankelijkheden voor FastAPI: + +* orjson - Vereist indien je `ORJSONResponse` wil gebruiken. +* ujson - Vereist indien je `UJSONResponse` wil gebruiken. + +## Licentie + +Dit project is gelicenseerd onder de voorwaarden van de MIT licentie. diff --git a/docs/nl/docs/python-types.md b/docs/nl/docs/python-types.md new file mode 100644 index 000000000..a5562b795 --- /dev/null +++ b/docs/nl/docs/python-types.md @@ -0,0 +1,597 @@ +# Introductie tot Python Types + +Python biedt ondersteuning voor optionele "type hints" (ook wel "type annotaties" genoemd). + +Deze **"type hints"** of annotaties zijn een speciale syntax waarmee het type van een variabele kan worden gedeclareerd. + +Door types voor je variabelen te declareren, kunnen editors en hulpmiddelen je beter ondersteunen. + +Dit is slechts een **korte tutorial/opfrisser** over Python type hints. Het behandelt enkel het minimum dat nodig is om ze te gebruiken met **FastAPI**... en dat is relatief weinig. + +**FastAPI** is helemaal gebaseerd op deze type hints, ze geven veel voordelen. + +Maar zelfs als je **FastAPI** nooit gebruikt, heb je er baat bij om er iets over te leren. + +/// note + +Als je een Python expert bent en alles al weet over type hints, sla dan dit hoofdstuk over. + +/// + +## Motivatie + +Laten we beginnen met een eenvoudig voorbeeld: + +```Python +{!../../../docs_src/python_types/tutorial001.py!} +``` + +Het aanroepen van dit programma leidt tot het volgende resultaat: + +``` +John Doe +``` + +De functie voert het volgende uit: + +* Neem een `first_name` en een `last_name` +* Converteer de eerste letter van elk naar een hoofdletter met `title()`. +`` +* Voeg samen met een spatie in het midden. + +```Python hl_lines="2" +{!../../../docs_src/python_types/tutorial001.py!} +``` + +### Bewerk het + +Dit is een heel eenvoudig programma. + +Maar stel je nu voor dat je het vanaf nul zou moeten maken. + +Op een gegeven moment zou je aan de definitie van de functie zijn begonnen, je had de parameters klaar... + +Maar dan moet je โ€œdie methode die de eerste letter naar hoofdletters converteertโ€ aanroepen. + +Was het `upper`? Was het `uppercase`? `first_uppercase`? `capitalize`? + +Dan roep je de hulp in van je oude programmeursvriend, (automatische) code aanvulling in je editor. + +Je typt de eerste parameter van de functie, `first_name`, dan een punt (`.`) en drukt dan op `Ctrl+Spatie` om de aanvulling te activeren. + +Maar helaas krijg je niets bruikbaars: + + + +### Types toevoegen + +Laten we een enkele regel uit de vorige versie aanpassen. + +We zullen precies dit fragment, de parameters van de functie, wijzigen van: + +```Python + first_name, last_name +``` + +naar: + +```Python + first_name: str, last_name: str +``` + +Dat is alles. + +Dat zijn de "type hints": + +```Python hl_lines="1" +{!../../../docs_src/python_types/tutorial002.py!} +``` + +Dit is niet hetzelfde als het declareren van standaardwaarden zoals bij: + +```Python + first_name="john", last_name="doe" +``` + +Het is iets anders. + +We gebruiken dubbele punten (`:`), geen gelijkheidstekens (`=`). + +Het toevoegen van type hints verandert normaal gesproken niet wat er gebeurt in je programma t.o.v. wat er zonder type hints zou gebeuren. + +Maar stel je voor dat je weer bezig bent met het maken van een functie, maar deze keer met type hints. + +Op hetzelfde moment probeer je de automatische aanvulling te activeren met `Ctrl+Spatie` en je ziet: + + + +Nu kun je de opties bekijken en er doorheen scrollen totdat je de optie vindt die โ€œeen belletje doet rinkelenโ€: + + + +### Meer motivatie + +Bekijk deze functie, deze heeft al type hints: + +```Python hl_lines="1" +{!../../../docs_src/python_types/tutorial003.py!} +``` + +Omdat de editor de types van de variabelen kent, krijgt u niet alleen aanvulling, maar ook controles op fouten: + + + +Nu weet je hoe je het moet oplossen, converteer `age` naar een string met `str(age)`: + +```Python hl_lines="2" +{!../../../docs_src/python_types/tutorial004.py!} +``` + +## Types declareren + +Je hebt net de belangrijkste plek om type hints te declareren gezien. Namelijk als functieparameters. + +Dit is ook de belangrijkste plek waar je ze gebruikt met **FastAPI**. + +### Eenvoudige types + +Je kunt alle standaard Python types declareren, niet alleen `str`. + +Je kunt bijvoorbeeld het volgende gebruiken: + +* `int` +* `float` +* `bool` +* `bytes` + +```Python hl_lines="1" +{!../../../docs_src/python_types/tutorial005.py!} +``` + +### Generieke types met typeparameters + +Er zijn enkele datastructuren die andere waarden kunnen bevatten, zoals `dict`, `list`, `set` en `tuple` en waar ook de interne waarden hun eigen type kunnen hebben. + +Deze types die interne types hebben worden โ€œ**generieke**โ€ types genoemd. Het is mogelijk om ze te declareren, zelfs met hun interne types. + +Om deze types en de interne types te declareren, kun je de standaard Python module `typing` gebruiken. Deze module is speciaal gemaakt om deze type hints te ondersteunen. + +#### Nieuwere versies van Python + +De syntax met `typing` is **verenigbaar** met alle versies, van Python 3.6 tot aan de nieuwste, inclusief Python 3.9, Python 3.10, enz. + +Naarmate Python zich ontwikkelt, worden **nieuwere versies**, met verbeterde ondersteuning voor deze type annotaties, beschikbaar. In veel gevallen hoef je niet eens de `typing` module te importeren en te gebruiken om de type annotaties te declareren. + +Als je een recentere versie van Python kunt kiezen voor je project, kun je profiteren van die extra eenvoud. + +In alle documentatie staan voorbeelden die compatibel zijn met elke versie van Python (als er een verschil is). + +Bijvoorbeeld โ€œ**Python 3.6+**โ€ betekent dat het compatibel is met Python 3.6 of hoger (inclusief 3.7, 3.8, 3.9, 3.10, etc). En โ€œ**Python 3.9+**โ€ betekent dat het compatibel is met Python 3.9 of hoger (inclusief 3.10, etc). + +Als je de **laatste versies van Python** kunt gebruiken, gebruik dan de voorbeelden voor de laatste versie, die hebben de **beste en eenvoudigste syntax**, bijvoorbeeld โ€œ**Python 3.10+**โ€. + +#### List + +Laten we bijvoorbeeld een variabele definiรซren als een `list` van `str`. + +//// tab | Python 3.9+ + +Declareer de variabele met dezelfde dubbele punt (`:`) syntax. + +Als type, vul `list` in. + +Doordat de list een type is dat enkele interne types bevat, zet je ze tussen vierkante haakjes: + +```Python hl_lines="1" +{!> ../../../docs_src/python_types/tutorial006_py39.py!} +``` + +//// + +//// tab | Python 3.8+ + +Van `typing`, importeer `List` (met een hoofdletter `L`): + +```Python hl_lines="1" +{!> ../../../docs_src/python_types/tutorial006.py!} +``` + +Declareer de variabele met dezelfde dubbele punt (`:`) syntax. + +Zet als type de `List` die je hebt geรฏmporteerd uit `typing`. + +Doordat de list een type is dat enkele interne types bevat, zet je ze tussen vierkante haakjes: + +```Python hl_lines="4" +{!> ../../../docs_src/python_types/tutorial006.py!} +``` + +//// + +/// info + +De interne types tussen vierkante haakjes worden โ€œtypeparametersโ€ genoemd. + +In dit geval is `str` de typeparameter die wordt doorgegeven aan `List` (of `list` in Python 3.9 en hoger). + +/// + +Dat betekent: โ€œde variabele `items` is een `list`, en elk van de items in deze list is een `str`โ€. + +/// tip + +Als je Python 3.9 of hoger gebruikt, hoef je `List` niet te importeren uit `typing`, je kunt in plaats daarvan hetzelfde reguliere `list` type gebruiken. + +/// + +Door dat te doen, kan je editor ondersteuning bieden, zelfs tijdens het verwerken van items uit de list: + + + +Zonder types is dat bijna onmogelijk om te bereiken. + +Merk op dat de variabele `item` een van de elementen is in de lijst `items`. + +Toch weet de editor dat het een `str` is, en biedt daar vervolgens ondersteuning voor aan. + +#### Tuple en Set + +Je kunt hetzelfde doen om `tuple`s en `set`s te declareren: + +//// tab | Python 3.9+ + +```Python hl_lines="1" +{!> ../../../docs_src/python_types/tutorial007_py39.py!} +``` + +//// + +//// tab | Python 3.8+ + +```Python hl_lines="1 4" +{!> ../../../docs_src/python_types/tutorial007.py!} +``` + +//// + +Dit betekent: + +* De variabele `items_t` is een `tuple` met 3 items, een `int`, nog een `int`, en een `str`. +* De variabele `items_s` is een `set`, en elk van de items is van het type `bytes`. + +#### Dict + +Om een `dict` te definiรซren, geef je 2 typeparameters door, gescheiden door komma's. + +De eerste typeparameter is voor de sleutels (keys) van de `dict`. + +De tweede typeparameter is voor de waarden (values) van het `dict`: + +//// tab | Python 3.9+ + +```Python hl_lines="1" +{!> ../../../docs_src/python_types/tutorial008_py39.py!} +``` + +//// + +//// tab | Python 3.8+ + +```Python hl_lines="1 4" +{!> ../../../docs_src/python_types/tutorial008.py!} +``` + +//// + +Dit betekent: + +* De variabele `prices` is een `dict`: + * De sleutels van dit `dict` zijn van het type `str` (bijvoorbeeld de naam van elk item). + * De waarden van dit `dict` zijn van het type `float` (bijvoorbeeld de prijs van elk item). + +#### Union + +Je kunt een variable declareren die van **verschillende types** kan zijn, bijvoorbeeld een `int` of een `str`. + +In Python 3.6 en hoger (inclusief Python 3.10) kun je het `Union`-type van `typing` gebruiken en de mogelijke types die je wilt accepteren, tussen de vierkante haakjes zetten. + +In Python 3.10 is er ook een **nieuwe syntax** waarin je de mogelijke types kunt scheiden door een verticale balk (`|`). + +//// tab | Python 3.10+ + +```Python hl_lines="1" +{!> ../../../docs_src/python_types/tutorial008b_py310.py!} +``` + +//// + +//// tab | Python 3.8+ + +```Python hl_lines="1 4" +{!> ../../../docs_src/python_types/tutorial008b.py!} +``` + +//// + +In beide gevallen betekent dit dat `item` een `int` of een `str` kan zijn. + +#### Mogelijk `None` + +Je kunt declareren dat een waarde een type kan hebben, zoals `str`, maar dat het ook `None` kan zijn. + +In Python 3.6 en hoger (inclusief Python 3.10) kun je het declareren door `Optional` te importeren en te gebruiken vanuit de `typing`-module. + +```Python hl_lines="1 4" +{!../../../docs_src/python_types/tutorial009.py!} +``` + +Door `Optional[str]` te gebruiken in plaats van alleen `str`, kan de editor je helpen fouten te detecteren waarbij je ervan uit zou kunnen gaan dat een waarde altijd een `str` is, terwijl het in werkelijkheid ook `None` zou kunnen zijn. + +`Optional[EenType]` is eigenlijk een snelkoppeling voor `Union[EenType, None]`, ze zijn equivalent. + +Dit betekent ook dat je in Python 3.10 `EenType | None` kunt gebruiken: + +//// tab | Python 3.10+ + +```Python hl_lines="1" +{!> ../../../docs_src/python_types/tutorial009_py310.py!} +``` + +//// + +//// tab | Python 3.8+ + +```Python hl_lines="1 4" +{!> ../../../docs_src/python_types/tutorial009.py!} +``` + +//// + +//// tab | Python 3.8+ alternative + +```Python hl_lines="1 4" +{!> ../../../docs_src/python_types/tutorial009b.py!} +``` + +//// + +#### Gebruik van `Union` of `Optional` + +Als je een Python versie lager dan 3.10 gebruikt, is dit een tip vanuit mijn **subjectieve** standpunt: + +* ๐Ÿšจ Vermijd het gebruik van `Optional[EenType]`. +* Gebruik in plaats daarvan **`Union[EenType, None]`** โœจ. + +Beide zijn gelijkwaardig en onderliggend zijn ze hetzelfde, maar ik zou `Union` aanraden in plaats van `Optional` omdat het woord โ€œ**optional**โ€ lijkt te impliceren dat de waarde optioneel is, en het eigenlijk betekent โ€œhet kan `None` zijnโ€, zelfs als het niet optioneel is en nog steeds vereist is. + +Ik denk dat `Union[SomeType, None]` explicieter is over wat het betekent. + +Het gaat alleen om de woorden en naamgeving. Maar die naamgeving kan invloed hebben op hoe jij en je teamgenoten over de code denken. + +Laten we als voorbeeld deze functie nemen: + +```Python hl_lines="1 4" +{!../../../docs_src/python_types/tutorial009c.py!} +``` + +De parameter `name` is gedefinieerd als `Optional[str]`, maar is **niet optioneel**, je kunt de functie niet aanroepen zonder de parameter: + +```Python +say_hi() # Oh, nee, dit geeft een foutmelding! ๐Ÿ˜ฑ +``` + +De `name` parameter is **nog steeds vereist** (niet *optioneel*) omdat het geen standaardwaarde heeft. Toch accepteert `name` `None` als waarde: + +```Python +say_hi(name=None) # Dit werkt, None is geldig ๐ŸŽ‰ +``` + +Het goede nieuws is dat als je eenmaal Python 3.10 gebruikt, je je daar geen zorgen meer over hoeft te maken, omdat je dan gewoon `|` kunt gebruiken om unions van types te definiรซren: + +```Python hl_lines="1 4" +{!../../../docs_src/python_types/tutorial009c_py310.py!} +``` + +Dan hoef je je geen zorgen te maken over namen als `Optional` en `Union`. ๐Ÿ˜Ž + +#### Generieke typen + +De types die typeparameters in vierkante haakjes gebruiken, worden **Generieke types** of **Generics** genoemd, bijvoorbeeld: + +//// tab | Python 3.10+ + +Je kunt dezelfde ingebouwde types gebruiken als generics (met vierkante haakjes en types erin): + +* `list` +* `tuple` +* `set` +* `dict` + +Hetzelfde als bij Python 3.8, uit de `typing`-module: + +* `Union` +* `Optional` (hetzelfde als bij Python 3.8) +* ...en anderen. + +In Python 3.10 kun je , als alternatief voor de generieke `Union` en `Optional`, de verticale lijn (`|`) gebruiken om unions van typen te voorzien, dat is veel beter en eenvoudiger. + +//// + +//// tab | Python 3.9+ + +Je kunt dezelfde ingebouwde types gebruiken als generieke types (met vierkante haakjes en types erin): + +* `list` +* `tuple` +* `set` +* `dict` + +En hetzelfde als met Python 3.8, vanuit de `typing`-module: + +* `Union` +* `Optional` +* ...en anderen. + +//// + +//// tab | Python 3.8+ + +* `List` +* `Tuple` +* `Set` +* `Dict` +* `Union` +* `Optional` +* ...en anderen. + +//// + +### Klassen als types + +Je kunt een klasse ook declareren als het type van een variabele. + +Stel dat je een klasse `Person` hebt, met een naam: + +```Python hl_lines="1-3" +{!../../../docs_src/python_types/tutorial010.py!} +``` + +Vervolgens kun je een variabele van het type `Persoon` declareren: + +```Python hl_lines="6" +{!../../../docs_src/python_types/tutorial010.py!} +``` + +Dan krijg je ook nog eens volledige editorondersteuning: + + + +Merk op dat dit betekent dat "`one_person` een **instantie** is van de klasse `Person`". + +Dit betekent niet dat `one_person` de **klasse** is met de naam `Person`. + +## Pydantic modellen + +Pydantic is een Python-pakket voor het uitvoeren van datavalidatie. + +Je declareert de "vorm" van de data als klassen met attributen. + +Elk attribuut heeft een type. + +Vervolgens maak je een instantie van die klasse met een aantal waarden en het valideert de waarden, converteert ze naar het juiste type (als dat het geval is) en geeft je een object met alle data terug. + +Daarnaast krijg je volledige editorondersteuning met dat resulterende object. + +Een voorbeeld uit de officiรซle Pydantic-documentatie: + +//// tab | Python 3.10+ + +```Python +{!> ../../../docs_src/python_types/tutorial011_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!> ../../../docs_src/python_types/tutorial011_py39.py!} +``` + +//// + +//// tab | Python 3.8+ + +```Python +{!> ../../../docs_src/python_types/tutorial011.py!} +``` + +//// + +/// info + +Om meer te leren over Pydantic, bekijk de documentatie. + +/// + +**FastAPI** is volledig gebaseerd op Pydantic. + +Je zult veel meer van dit alles in de praktijk zien in de [Tutorial - Gebruikershandleiding](tutorial/index.md){.internal-link target=_blank}. + +/// tip + +Pydantic heeft een speciaal gedrag wanneer je `Optional` of `Union[EenType, None]` gebruikt zonder een standaardwaarde, je kunt er meer over lezen in de Pydantic-documentatie over Verplichte optionele velden. + +/// + +## Type Hints met Metadata Annotaties + +Python heeft ook een functie waarmee je **extra metadata** in deze type hints kunt toevoegen met behulp van `Annotated`. + +//// tab | Python 3.9+ + +In Python 3.9 is `Annotated` onderdeel van de standaardpakket, dus je kunt het importeren vanuit `typing`. + +```Python hl_lines="1 4" +{!> ../../../docs_src/python_types/tutorial013_py39.py!} +``` + +//// + +//// tab | Python 3.8+ + +In versies lager dan Python 3.9 importeer je `Annotated` vanuit `typing_extensions`. + +Het wordt al geรฏnstalleerd met **FastAPI**. + +```Python hl_lines="1 4" +{!> ../../../docs_src/python_types/tutorial013.py!} +``` + +//// + +Python zelf doet niets met deze `Annotated` en voor editors en andere hulpmiddelen is het type nog steeds een `str`. + +Maar je kunt deze ruimte in `Annotated` gebruiken om **FastAPI** te voorzien van extra metadata over hoe je wilt dat je applicatie zich gedraagt. + +Het belangrijkste om te onthouden is dat **de eerste *typeparameter*** die je doorgeeft aan `Annotated` het **werkelijke type** is. De rest is gewoon metadata voor andere hulpmiddelen. + +Voor nu hoef je alleen te weten dat `Annotated` bestaat en dat het standaard Python is. ๐Ÿ˜Ž + +Later zul je zien hoe **krachtig** het kan zijn. + +/// tip + +Het feit dat dit **standaard Python** is, betekent dat je nog steeds de **best mogelijke ontwikkelaarservaring** krijgt in je editor, met de hulpmiddelen die je gebruikt om je code te analyseren en te refactoren, enz. โœจ + +Daarnaast betekent het ook dat je code zeer verenigbaar zal zijn met veel andere Python-hulpmiddelen en -pakketten. ๐Ÿš€ + +/// + +## Type hints in **FastAPI** + +**FastAPI** maakt gebruik van type hints om verschillende dingen te doen. + +Met **FastAPI** declareer je parameters met type hints en krijg je: + +* **Editor ondersteuning**. +* **Type checks**. + +...en **FastAPI** gebruikt dezelfde declaraties om: + +* **Vereisten te definรฏeren **: van request pad parameters, query parameters, headers, bodies, dependencies, enz. +* **Data te converteren**: van de request naar het vereiste type. +* **Data te valideren**: afkomstig van elke request: + * **Automatische foutmeldingen** te genereren die naar de client worden geretourneerd wanneer de data ongeldig is. +* De API met OpenAPI te **documenteren**: + * die vervolgens wordt gebruikt door de automatische interactieve documentatie gebruikersinterfaces. + +Dit klinkt misschien allemaal abstract. Maak je geen zorgen. Je ziet dit allemaal in actie in de [Tutorial - Gebruikershandleiding](tutorial/index.md){.internal-link target=_blank}. + +Het belangrijkste is dat door standaard Python types te gebruiken, op รฉรฉn plek (in plaats van meer klassen, decorators, enz. toe te voegen), **FastAPI** een groot deel van het werk voor je doet. + +/// info + +Als je de hele tutorial al hebt doorgenomen en terug bent gekomen om meer te weten te komen over types, is een goede bron het "cheat sheet" van `mypy`. + +/// diff --git a/docs/nl/mkdocs.yml b/docs/nl/mkdocs.yml new file mode 100644 index 000000000..de18856f4 --- /dev/null +++ b/docs/nl/mkdocs.yml @@ -0,0 +1 @@ +INHERIT: ../en/mkdocs.yml diff --git a/docs/pt/docs/advanced/async-tests.md b/docs/pt/docs/advanced/async-tests.md index ab5bfa648..7cac26262 100644 --- a/docs/pt/docs/advanced/async-tests.md +++ b/docs/pt/docs/advanced/async-tests.md @@ -72,7 +72,7 @@ Note que a funรงรฃo de teste รฉ `async def` agora, no lugar de apenas `def` como Entรฃo podemos criar um `AsyncClient` com a aplicaรงรฃo, e enviar requisiรงรตes assรญncronas para ela utilizando `await`. -```Python hl_lines="9-10" +```Python hl_lines="9-12" {!../../../docs_src/async_tests/test_main.py!} ``` diff --git a/docs/pt/docs/advanced/security/http-basic-auth.md b/docs/pt/docs/advanced/security/http-basic-auth.md new file mode 100644 index 000000000..12b8ab01c --- /dev/null +++ b/docs/pt/docs/advanced/security/http-basic-auth.md @@ -0,0 +1,192 @@ +# HTTP Basic Auth + +Para os casos mais simples, vocรช pode utilizar o HTTP Basic Auth. + +No HTTP Basic Auth, a aplicaรงรฃo espera um cabeรงalho que contรฉm um usuรกrio e uma senha. + +Caso ela nรฃo receba, ela retorna um erro HTTP 401 "Unauthorized" (*Nรฃo Autorizado*). + +E retorna um cabeรงalho `WWW-Authenticate` com o valor `Basic`, e um parรขmetro opcional `realm`. + +Isso sinaliza ao navegador para mostrar o prompt integrado para um usuรกrio e senha. + +Entรฃo, quando vocรช digitar o usuรกrio e senha, o navegador os envia automaticamente no cabeรงalho. + +## HTTP Basic Auth Simples + +* Importe `HTTPBasic` e `HTTPBasicCredentials`. +* Crie um "esquema `security`" utilizando `HTTPBasic`. +* Utilize o `security` com uma dependรชncia em sua *operaรงรฃo de rota*. +* Isso retorna um objeto do tipo `HTTPBasicCredentials`: + * Isto contรฉm o `username` e o `password` enviado. + +//// tab | Python 3.9+ + +```Python hl_lines="4 8 12" +{!> ../../../docs_src/security/tutorial006_an_py39.py!} +``` + +//// + +//// tab | Python 3.8+ + +```Python hl_lines="2 7 11" +{!> ../../../docs_src/security/tutorial006_an.py!} +``` + +//// + +//// tab | Python 3.8+ non-Annotated + +/// tip | "Dica" + +Prefira utilizar a versรฃo `Annotated` se possรญvel. + +/// + +```Python hl_lines="2 6 10" +{!> ../../../docs_src/security/tutorial006.py!} +``` + +//// + +Quando vocรช tentar abrir a URL pela primeira vez (ou clicar no botรฃo "Executar" nos documentos) o navegador vai pedir pelo seu usuรกrio e senha: + + + +## Verifique o usuรกrio + +Aqui estรก um exemplo mais completo. + +Utilize uma dependรชncia para verificar se o usuรกrio e a senha estรฃo corretos. + +Para isso, utilize o mรณdulo padrรฃo do Python `secrets` para verificar o usuรกrio e senha. + +O `secrets.compare_digest()` necessita receber `bytes` ou `str` que possuem apenas caracteres ASCII (os em Inglรชs). Isso significa que nรฃo funcionaria com caracteres como o `รก`, como em `Sebastiรกn`. + +Para lidar com isso, primeiramente nรณs convertemos o `username` e o `password` para `bytes`, codificando-os com UTF-8. + +Entรฃo nรณs podemos utilizar o `secrets.compare_digest()` para garantir que o `credentials.username` รฉ `"stanleyjobson"`, e que o `credentials.password` รฉ `"swordfish"`. + +//// tab | Python 3.9+ + +```Python hl_lines="1 12-24" +{!> ../../../docs_src/security/tutorial007_an_py39.py!} +``` + +//// + +//// tab | Python 3.8+ + +```Python hl_lines="1 12-24" +{!> ../../../docs_src/security/tutorial007_an.py!} +``` + +//// + +//// tab | Python 3.8+ non-Annotated + +/// tip | "Dica" + +Prefira utilizar a versรฃo `Annotated` se possรญvel. + +/// + +```Python hl_lines="1 11-21" +{!> ../../../docs_src/security/tutorial007.py!} +``` + +//// + +Isso seria parecido com: + +```Python +if not (credentials.username == "stanleyjobson") or not (credentials.password == "swordfish"): + # Return some error + ... +``` + +Porรฉm ao utilizar o `secrets.compare_digest()`, isso estarรก seguro contra um tipo de ataque chamado "ataque de temporizaรงรฃo (timing attacks)". + +### Ataques de Temporizaรงรฃo + +Mas o que รฉ um "ataque de temporizaรงรฃo"? + +Vamos imaginar que alguns invasores estรฃo tentando adivinhar o usuรกrio e a senha. + +E eles enviam uma requisiรงรฃo com um usuรกrio `johndoe` e uma senha `love123`. + +Entรฃo o cรณdigo Python em sua aplicaรงรฃo seria equivalente a algo como: + +```Python +if "johndoe" == "stanleyjobson" and "love123" == "swordfish": + ... +``` + +Mas no exato momento que o Python compara o primeiro `j` em `johndoe` contra o primeiro `s` em `stanleyjobson`, ele retornarรก `False`, porque ele jรก sabe que aquelas duas strings nรฃo sรฃo a mesma, pensando que "nรฃo existe a necessidade de desperdiรงar mais poder computacional comparando o resto das letras". E a sua aplicaรงรฃo dirรก "Usuรกrio ou senha incorretos". + +Mas entรฃo os invasores vรฃo tentar com o usuรกrio `stanleyjobsox` e a senha `love123`. + +E a sua aplicaรงรฃo faz algo como: + +```Python +if "stanleyjobsox" == "stanleyjobson" and "love123" == "swordfish": + ... +``` + +O Python terรก que comparar todo o `stanleyjobso` tanto em `stanleyjobsox` como em `stanleyjobson` antes de perceber que as strings nรฃo sรฃo a mesma. Entรฃo isso levarรก alguns microsegundos a mais para retornar "Usuรกrio ou senha incorretos". + +#### O tempo para responder ajuda os invasores + +Neste ponto, ao perceber que o servidor demorou alguns microsegundos a mais para enviar o retorno "Usuรกrio ou senha incorretos", os invasores irรฃo saber que eles acertaram _alguma coisa_, algumas das letras iniciais estavam certas. + +E eles podem tentar de novo sabendo que provavelmente รฉ algo mais parecido com `stanleyjobsox` do que com `johndoe`. + +#### Um ataque "profissional" + +Claro, os invasores nรฃo tentariam tudo isso de forma manual, eles escreveriam um programa para fazer isso, possivelmente com milhares ou milhรตes de testes por segundo. E obteriam apenas uma letra a mais por vez. + +Mas fazendo isso, em alguns minutos ou horas os invasores teriam adivinhado o usuรกrio e senha corretos, com a "ajuda" da nossa aplicaรงรฃo, apenas usando o tempo levado para responder. + +#### Corrija com o `secrets.compare_digest()` + +Mas em nosso cรณdigo nรณs estamos utilizando o `secrets.compare_digest()`. + +Resumindo, levarรก o mesmo tempo para comparar `stanleyjobsox` com `stanleyjobson` do que comparar `johndoe` com `stanleyjobson`. E o mesmo para a senha. + +Deste modo, ao utilizar `secrets.compare_digest()` no cรณdigo de sua aplicaรงรฃo, ela esterรก a salvo contra toda essa gama de ataques de seguranรงa. + + +### Retorne o erro + +Depois de detectar que as credenciais estรฃo incorretas, retorne um `HTTPException` com o status 401 (o mesmo retornado quando nenhuma credencial foi informada) e adicione o cabeรงalho `WWW-Authenticate` para fazer com que o navegador mostre o prompt de login novamente: + +//// tab | Python 3.9+ + +```Python hl_lines="26-30" +{!> ../../../docs_src/security/tutorial007_an_py39.py!} +``` + +//// + +//// tab | Python 3.8+ + +```Python hl_lines="26-30" +{!> ../../../docs_src/security/tutorial007_an.py!} +``` + +//// + +//// tab | Python 3.8+ non-Annotated + +/// tip | "Dica" + +Prefira utilizar a versรฃo `Annotated` se possรญvel. + +/// + +```Python hl_lines="23-27" +{!> ../../../docs_src/security/tutorial007.py!} +``` + +//// diff --git a/docs/pt/docs/advanced/security/index.md b/docs/pt/docs/advanced/security/index.md new file mode 100644 index 000000000..ae63f1c96 --- /dev/null +++ b/docs/pt/docs/advanced/security/index.md @@ -0,0 +1,19 @@ +# Seguranรงa Avanรงada + +## Funcionalidades Adicionais + +Existem algumas funcionalidades adicionais para lidar com seguranรงa alรฉm das cobertas em [Tutorial - Guia de Usuรกrio: Seguranรงa](../../tutorial/security/index.md){.internal-link target=_blank}. + +/// tip | "Dica" + +As prรณximas seรงรตes **nรฃo sรฃo necessariamente "avanรงadas"**. + +E รฉ possรญvel que para o seu caso de uso, a soluรงรฃo estรก em uma delas. + +/// + +## Leia o Tutorial primeiro + +As prรณximas seรงรตes pressupรตem que vocรช jรก leu o principal [Tutorial - Guia de Usuรกrio: Seguranรงa](../../tutorial/security/index.md){.internal-link target=_blank}. + +Todas elas sรฃo baseadas nos mesmos conceitos, mas permitem algumas funcionalidades extras. diff --git a/docs/pt/docs/advanced/testing-events.md b/docs/pt/docs/advanced/testing-events.md new file mode 100644 index 000000000..392fb741c --- /dev/null +++ b/docs/pt/docs/advanced/testing-events.md @@ -0,0 +1,7 @@ +# Testando Eventos: inicializaรงรฃo - encerramento + +Quando vocรช precisa que os seus manipuladores de eventos (`startup` e `shutdown`) sejam executados em seus testes, vocรช pode utilizar o `TestClient` usando a instruรงรฃo `with`: + +```Python hl_lines="9-12 20-24" +{!../../../docs_src/app_testing/tutorial003.py!} +``` diff --git a/docs/pt/docs/environment-variables.md b/docs/pt/docs/environment-variables.md new file mode 100644 index 000000000..360d1c496 --- /dev/null +++ b/docs/pt/docs/environment-variables.md @@ -0,0 +1,298 @@ +# Variรกveis de Ambiente + +/// tip | "Dica" + +Se vocรช jรก sabe o que sรฃo "variรกveis de ambiente" e como usรก-las, pode pular esta seรงรฃo. + +/// + +Uma variรกvel de ambiente (tambรฉm conhecida como "**env var**") รฉ uma variรกvel que existe **fora** do cรณdigo Python, no **sistema operacional**, e pode ser lida pelo seu cรณdigo Python (ou por outros programas tambรฉm). + +Variรกveis de ambiente podem ser รบteis para lidar com **configuraรงรตes** do aplicativo, como parte da **instalaรงรฃo** do Python, etc. + +## Criar e Usar Variรกveis de Ambiente + +Vocรช pode **criar** e usar variรกveis de ambiente no **shell (terminal)**, sem precisar do Python: + +//// tab | Linux, macOS, Windows Bash + +
+ +```console +// Vocรช pode criar uma variรกvel de ambiente MY_NAME com +$ export MY_NAME="Wade Wilson" + +// Entรฃo vocรช pode usรก-la com outros programas, como +$ echo "Hello $MY_NAME" + +Hello Wade Wilson +``` + +
+ +//// + +//// tab | Windows PowerShell + +
+ +```console +// Criar uma variรกvel de ambiente MY_NAME +$ $Env:MY_NAME = "Wade Wilson" + +// Usรก-la com outros programas, como +$ echo "Hello $Env:MY_NAME" + +Hello Wade Wilson +``` + +
+ +//// + +## Ler Variรกveis de Ambiente no Python + +Vocรช tambรฉm pode criar variรกveis de ambiente **fora** do Python, no terminal (ou com qualquer outro mรฉtodo) e depois **lรช-las no Python**. + +Por exemplo, vocรช poderia ter um arquivo `main.py` com: + +```Python hl_lines="3" +import os + +name = os.getenv("MY_NAME", "World") +print(f"Hello {name} from Python") +``` + +/// tip | "Dica" + +O segundo argumento para `os.getenv()` รฉ o valor padrรฃo a ser retornado. + +Se nรฃo for fornecido, รฉ `None` por padrรฃo, Aqui fornecemos `"World"` como o valor padrรฃo a ser usado. + +/// + +Entรฃo vocรช poderia chamar esse programa Python: + +//// tab | Linux, macOS, Windows Bash + +
+ +```console +// Aqui ainda nรฃo definimos a variรกvel de ambiente +$ python main.py + +// Como nรฃo definimos a variรกvel de ambiente, obtemos o valor padrรฃo + +Hello World from Python + +// Mas se criarmos uma variรกvel de ambiente primeiro +$ export MY_NAME="Wade Wilson" + +// E entรฃo chamar o programa novamente +$ python main.py + +// Agora ele pode ler a variรกvel de ambiente + +Hello Wade Wilson from Python +``` + +
+ +//// + +//// tab | Windows PowerShell + +
+ +```console +// Aqui ainda nรฃo definimos a variรกvel de ambiente +$ python main.py + +// Como nรฃo definimos a variรกvel de ambiente, obtemos o valor padrรฃo + +Hello World from Python + +// Mas se criarmos uma variรกvel de ambiente primeiro +$ $Env:MY_NAME = "Wade Wilson" + +// E entรฃo chamar o programa novamente +$ python main.py + +// Agora ele pode ler a variรกvel de ambiente + +Hello Wade Wilson from Python +``` + +
+ +//// + +Como as variรกveis de ambiente podem ser definidas fora do cรณdigo, mas podem ser lidas pelo cรณdigo e nรฃo precisam ser armazenadas (com versรฃo no `git`) com o restante dos arquivos, รฉ comum usรก-las para configuraรงรตes ou **definiรงรตes**. + +Vocรช tambรฉm pode criar uma variรกvel de ambiente apenas para uma **invocaรงรฃo especรญfica do programa**, que sรณ estรก disponรญvel para aquele programa e apenas pela duraรงรฃo dele. + +Para fazer isso, crie-a na mesma linha, antes do prรณprio programa: + +
+ +```console +// Criar uma variรกvel de ambiente MY_NAME para esta chamada de programa +$ MY_NAME="Wade Wilson" python main.py + +// Agora ele pode ler a variรกvel de ambiente + +Hello Wade Wilson from Python + +// A variรกvel de ambiente nรฃo existe mais depois +$ python main.py + +Hello World from Python +``` + +
+ +/// tip | "Dica" + +Vocรช pode ler mais sobre isso em The Twelve-Factor App: Config. + +/// + +## Tipos e Validaรงรฃo + +Essas variรกveis de ambiente sรณ podem lidar com **strings de texto**, pois sรฃo externas ao Python e precisam ser compatรญveis com outros programas e com o resto do sistema (e atรฉ mesmo com diferentes sistemas operacionais, como Linux, Windows, macOS). + +Isso significa que **qualquer valor** lido em Python de uma variรกvel de ambiente **serรก uma `str`**, e qualquer conversรฃo para um tipo diferente ou qualquer validaรงรฃo precisa ser feita no cรณdigo. + +Vocรช aprenderรก mais sobre como usar variรกveis de ambiente para lidar com **configuraรงรตes do aplicativo** no [Guia do Usuรกrio Avanรงado - Configuraรงรตes e Variรกveis de Ambiente](./advanced/settings.md){.internal-link target=_blank}. + +## Variรกvel de Ambiente `PATH` + +Existe uma variรกvel de ambiente **especial** chamada **`PATH`** que รฉ usada pelos sistemas operacionais (Linux, macOS, Windows) para encontrar programas para executar. + +O valor da variรกvel `PATH` รฉ uma longa string composta por diretรณrios separados por dois pontos `:` no Linux e macOS, e por ponto e vรญrgula `;` no Windows. + +Por exemplo, a variรกvel de ambiente `PATH` poderia ter esta aparรชncia: + +//// tab | Linux, macOS + +```plaintext +/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin +``` + +Isso significa que o sistema deve procurar programas nos diretรณrios: + +* `/usr/local/bin` +* `/usr/bin` +* `/bin` +* `/usr/sbin` +* `/sbin` + +//// + +//// tab | Windows + +```plaintext +C:\Program Files\Python312\Scripts;C:\Program Files\Python312;C:\Windows\System32 +``` + +Isso significa que o sistema deve procurar programas nos diretรณrios: + +* `C:\Program Files\Python312\Scripts` +* `C:\Program Files\Python312` +* `C:\Windows\System32` + +//// + +Quando vocรช digita um **comando** no terminal, o sistema operacional **procura** o programa em **cada um dos diretรณrios** listados na variรกvel de ambiente `PATH`. + +Por exemplo, quando vocรช digita `python` no terminal, o sistema operacional procura um programa chamado `python` no **primeiro diretรณrio** dessa lista. + +Se ele o encontrar, entรฃo ele o **usarรก**. Caso contrรกrio, ele continua procurando nos **outros diretรณrios**. + +### Instalando o Python e Atualizando o `PATH` + +Durante a instalaรงรฃo do Python, vocรช pode ser questionado sobre a atualizaรงรฃo da variรกvel de ambiente `PATH`. + +//// tab | Linux, macOS + +Vamos supor que vocรช instale o Python e ele fique em um diretรณrio `/opt/custompython/bin`. + +Se vocรช concordar em atualizar a variรกvel de ambiente `PATH`, o instalador adicionarรก `/opt/custompython/bin` para a variรกvel de ambiente `PATH`. + +Poderia parecer assim: + +```plaintext +/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/custompython/bin +``` + +Dessa forma, ao digitar `python` no terminal, o sistema encontrarรก o programa Python em `/opt/custompython/bin` (รบltimo diretรณrio) e o utilizarรก. + +//// + +//// tab | Windows + +Digamos que vocรช instala o Python e ele acaba em um diretรณrio `C:\opt\custompython\bin`. + +Se vocรช disser sim para atualizar a variรกvel de ambiente `PATH`, o instalador adicionarรก `C:\opt\custompython\bin` ร  variรกvel de ambiente `PATH`. + +```plaintext +C:\Program Files\Python312\Scripts;C:\Program Files\Python312;C:\Windows\System32;C:\opt\custompython\bin +``` + +Dessa forma, quando vocรช digitar `python` no terminal, o sistema encontrarรก o programa Python em `C:\opt\custompython\bin` (o รบltimo diretรณrio) e o utilizarรก. + +//// + +Entรฃo, se vocรช digitar: + +
+ +```console +$ python +``` + +
+ +//// tab | Linux, macOS + +O sistema **encontrarรก** o programa `python` em `/opt/custompython/bin` e o executarรก. + +Seria aproximadamente equivalente a digitar: + +
+ +```console +$ /opt/custompython/bin/python +``` + +
+ +//// + +//// tab | Windows + +O sistema **encontrarรก** o programa `python` em `C:\opt\custompython\bin\python` e o executarรก. + +Seria aproximadamente equivalente a digitar: + +
+ +```console +$ C:\opt\custompython\bin\python +``` + +
+ +//// + +Essas informaรงรตes serรฃo รบteis ao aprender sobre [Ambientes Virtuais](virtual-environments.md){.internal-link target=_blank}. + +## Conclusรฃo + +Com isso, vocรช deve ter uma compreensรฃo bรกsica do que sรฃo **variรกveis โ€‹โ€‹de ambiente** e como usรก-las em Python. + +Vocรช tambรฉm pode ler mais sobre elas na Wikipedia para Variรกveis โ€‹โ€‹de Ambiente. + +Em muitos casos, nรฃo รฉ muito รณbvio como as variรกveis โ€‹โ€‹de ambiente seriam รบteis e aplicรกveis โ€‹โ€‹imediatamente. Mas elas continuam aparecendo em muitos cenรกrios diferentes quando vocรช estรก desenvolvendo, entรฃo รฉ bom saber sobre elas. + +Por exemplo, vocรช precisarรก dessas informaรงรตes na prรณxima seรงรฃo, sobre [Ambientes Virtuais](virtual-environments.md). diff --git a/docs/pt/docs/tutorial/debugging.md b/docs/pt/docs/tutorial/debugging.md new file mode 100644 index 000000000..54582fcbc --- /dev/null +++ b/docs/pt/docs/tutorial/debugging.md @@ -0,0 +1,115 @@ +# Depuraรงรฃo + +Vocรช pode conectar o depurador no seu editor, por exemplo, com o Visual Studio Code ou PyCharm. + +## Chamar `uvicorn` + +Em seu aplicativo FastAPI, importe e execute `uvicorn` diretamente: + +```Python hl_lines="1 15" +{!../../../docs_src/debugging/tutorial001.py!} +``` + +### Sobre `__name__ == "__main__"` + +O objetivo principal de `__name__ == "__main__"` รฉ ter algum cรณdigo que seja executado quando seu arquivo for chamado com: + +
+ +```console +$ python myapp.py +``` + +
+ +mas nรฃo รฉ chamado quando outro arquivo o importa, como em: + +```Python +from myapp import app +``` + +#### Mais detalhes + +Digamos que seu arquivo se chama `myapp.py`. + +Se vocรช executรก-lo com: + +
+ +```console +$ python myapp.py +``` + +
+ +entรฃo a variรกvel interna `__name__` no seu arquivo, criada automaticamente pelo Python, terรก como valor a string `"__main__"`. + +Entรฃo, a seรงรฃo: + +```Python + uvicorn.run(app, host="0.0.0.0", port=8000) +``` + +vai executar. + +--- + +Isso nรฃo acontecerรก se vocรช importar esse mรณdulo (arquivo). + +Entรฃo, se vocรช tiver outro arquivo `importer.py` com: + +```Python +from myapp import app + +# Mais um pouco de cรณdigo +``` + +nesse caso, a variรกvel criada automaticamente dentro de `myapp.py` nรฃo terรก a variรกvel `__name__` com o valor `"__main__"`. + +Entรฃo, a linha: + +```Python + uvicorn.run(app, host="0.0.0.0", port=8000) +``` + +nรฃo serรก executada. + +/// info | "Informaรงรฃo" + +Para mais informaรงรตes, consulte a documentaรงรฃo oficial do Python. + +/// + +## Execute seu cรณdigo com seu depurador + +Como vocรช estรก executando o servidor Uvicorn diretamente do seu cรณdigo, vocรช pode chamar seu programa Python (seu aplicativo FastAPI) diretamente do depurador. + +--- + +Por exemplo, no Visual Studio Code, vocรช pode: + +* Ir para o painel "Debug". +* "Add configuration...". +* Selecionar "Python" +* Executar o depurador com a opรงรฃo "`Python: Current File (Integrated Terminal)`". + +Em seguida, ele iniciarรก o servidor com seu cรณdigo **FastAPI**, pararรก em seus pontos de interrupรงรฃo, etc. + +Veja como pode parecer: + + + +--- + +Se vocรช usar o Pycharm, vocรช pode: + +* Abrir o menu "Executar". +* Selecionar a opรงรฃo "Depurar...". +* Entรฃo um menu de contexto aparece. +* Selecionar o arquivo para depurar (neste caso, `main.py`). + +Em seguida, ele iniciarรก o servidor com seu cรณdigo **FastAPI**, pararรก em seus pontos de interrupรงรฃo, etc. + +Veja como pode parecer: + + diff --git a/docs/pt/docs/tutorial/request-form-models.md b/docs/pt/docs/tutorial/request-form-models.md new file mode 100644 index 000000000..a9db18e9d --- /dev/null +++ b/docs/pt/docs/tutorial/request-form-models.md @@ -0,0 +1,134 @@ +# Modelos de Formulรกrios + +Vocรช pode utilizar **Modelos Pydantic** para declarar **campos de formulรกrios** no FastAPI. + +/// info | "Informaรงรฃo" + +Para utilizar formulรกrios, instale primeiramente o `python-multipart`. + +Certifique-se de criar um [ambiente virtual](../virtual-environments.md){.internal-link target=_blank}, ativรก-lo, e entรฃo instalar. Por exemplo: + +```console +$ pip install python-multipart +``` + +/// + +/// note | "Nota" + +Isto รฉ suportado desde a versรฃo `0.113.0` do FastAPI. ๐Ÿค“ + +/// + +## Modelos Pydantic para Formulรกrios + +Vocรช precisa apenas declarar um **modelo Pydantic** com os campos que deseja receber como **campos de formulรกrios**, e entรฃo declarar o parรขmetro como um `Form`: + +//// tab | Python 3.9+ + +```Python hl_lines="9-11 15" +{!> ../../../docs_src/request_form_models/tutorial001_an_py39.py!} +``` + +//// + +//// tab | Python 3.8+ + +```Python hl_lines="8-10 14" +{!> ../../../docs_src/request_form_models/tutorial001_an.py!} +``` + +//// + +//// tab | Python 3.8+ non-Annotated + +/// tip | "Dica" + +Prefira utilizar a versรฃo `Annotated` se possรญvel. + +/// + +```Python hl_lines="7-9 13" +{!> ../../../docs_src/request_form_models/tutorial001.py!} +``` + +//// + +O **FastAPI** irรก **extrair** as informaรงรตes para **cada campo** dos **dados do formulรกrio** na requisiรงรฃo e dar para vocรช o modelo Pydantic que vocรช definiu. + +## Confira os Documentos + +Vocรช pode verificar na UI de documentaรงรฃo em `/docs`: + +
+ +
+ +## Proibir Campos Extras de Formulรกrios + +Em alguns casos de uso especiais (provavelmente nรฃo muito comum), vocรช pode desejar **restringir** os campos do formulรกrio para aceitar apenas os declarados no modelo Pydantic. E **proibir** qualquer campo **extra**. + +/// note | "Nota" + +Isso รฉ suportado deste a versรฃo `0.114.0` do FastAPI. ๐Ÿค“ + +/// + +Vocรช pode utilizar a configuraรงรฃo de modelo do Pydantic para `proibir` qualquer campo `extra`: + +//// tab | Python 3.9+ + +```Python hl_lines="12" +{!> ../../../docs_src/request_form_models/tutorial002_an_py39.py!} +``` + +//// + +//// tab | Python 3.8+ + +```Python hl_lines="11" +{!> ../../../docs_src/request_form_models/tutorial002_an.py!} +``` + +//// + +//// tab | Python 3.8+ non-Annotated + +/// tip + +Prefira utilizar a versรฃo `Annotated` se possรญvel. + +/// + +```Python hl_lines="10" +{!> ../../../docs_src/request_form_models/tutorial002.py!} +``` + +//// + +Caso um cliente tente enviar informaรงรตes adicionais, ele receberรก um retorno de **erro**. + +Por exemplo, se o cliente tentar enviar os campos de formulรกrio: + +* `username`: `Rick` +* `password`: `Portal Gun` +* `extra`: `Mr. Poopybutthole` + +Ele receberรก um retorno de erro informando-o que o campo `extra` nรฃo รฉ permitido: + +```json +{ + "detail": [ + { + "type": "extra_forbidden", + "loc": ["body", "extra"], + "msg": "Extra inputs are not permitted", + "input": "Mr. Poopybutthole" + } + ] +} +``` + +## Resumo + +Vocรช pode utilizar modelos Pydantic para declarar campos de formulรกrios no FastAPI. ๐Ÿ˜Ž diff --git a/docs/pt/docs/tutorial/request_files.md b/docs/pt/docs/tutorial/request_files.md new file mode 100644 index 000000000..60e4ecb26 --- /dev/null +++ b/docs/pt/docs/tutorial/request_files.md @@ -0,0 +1,418 @@ +# Arquivos de Requisiรงรฃo + +Vocรช pode definir arquivos para serem enviados para o cliente utilizando `File`. + +/// info + +Para receber arquivos compartilhados, primeiro instale `python-multipart`. + +E.g. `pip install python-multipart`. + +Isso se deve por que arquivos enviados sรฃo enviados como "dados de formulรกrio". + +/// + +## Importe `File` + +Importe `File` e `UploadFile` do `fastapi`: + +//// tab | Python 3.9+ + +```Python hl_lines="3" +{!> ../../../docs_src/request_files/tutorial001_an_py39.py!} +``` + +//// + +//// tab | Python 3.8+ + +```Python hl_lines="1" +{!> ../../../docs_src/request_files/tutorial001_an.py!} +``` + +//// + +//// tab | Python 3.8+ non-Annotated + +/// tip | Dica + +Utilize a versรฃo com `Annotated` se possรญvel. + +/// + +```Python hl_lines="1" +{!> ../../../docs_src/request_files/tutorial001.py!} +``` + +//// + +## Defina os parรขmetros de `File` + +Cria os parรขmetros do arquivo da mesma forma que vocรช faria para `Body` ou `Form`: + +//// tab | Python 3.9+ + +```Python hl_lines="9" +{!> ../../../docs_src/request_files/tutorial001_an_py39.py!} +``` + +//// + +//// tab | Python 3.8+ + +```Python hl_lines="8" +{!> ../../../docs_src/request_files/tutorial001_an.py!} +``` + +//// + +//// tab | Python 3.8+ non-Annotated + +/// tip | Dica + +Utilize a versรฃo com `Annotated` se possรญvel. + +/// + +```Python hl_lines="7" +{!> ../../../docs_src/request_files/tutorial001.py!} +``` + +//// + +/// info | Informaรงรฃo + +`File` รฉ uma classe que herda diretamente de `Form`. + +Mas lembre-se que quando vocรช importa `Query`,`Path`, `File`, entre outros, do `fastapi`, essas sรฃo na verdade funรงรตes que retornam classes especiais. + +/// + +/// tip | Dica + +Para declarar o corpo de arquivos, vocรช precisa utilizar `File`, do contrรกrio os parรขmetros seriam interpretados como parรขmetros de consulta ou corpo (JSON) da requisiรงรฃo. + +/// + +Os arquivos serรฃo enviados como "form data". + +Se vocรช declarar o tipo do seu parรขmetro na sua *funรงรฃo de operaรงรฃo de rota* como `bytes`, o **FastAPI** irรก ler o arquivo para vocรช e vocรช receberรก o conteรบdo como `bytes`. + +Lembre-se que isso significa que o conteรบdo inteiro serรก armazenado em memรณria. Isso funciona bem para arquivos pequenos. + +Mas existem vรกrios casos em que vocรช pode se beneficiar ao usar `UploadFile`. + +## Parรขmetros de arquivo com `UploadFile` + +Defina um parรขmetro de arquivo com o tipo `UploadFile` + +//// tab | Python 3.9+ + +```Python hl_lines="14" +{!> ../../../docs_src/request_files/tutorial001_an_py39.py!} +``` + +//// + +//// tab | Python 3.8+ + +```Python hl_lines="13" +{!> ../../../docs_src/request_files/tutorial001_an.py!} +``` + +//// + +//// tab | Python 3.8+ non-Annotated + +/// tip | Dica + +Utilize a versรฃo com `Annotated` se possรญvel. + +/// + +```Python hl_lines="12" +{!> ../../../docs_src/request_files/tutorial001.py!} +``` + +//// + +Utilizando `UploadFile` tem vรกrias vantagens sobre `bytes`: + +* Vocรช nรฃo precisa utilizar `File()` como o valor padrรฃo do parรขmetro. +* A classe utiliza um arquivo em "spool": + * Um arquivo guardado em memรณria atรฉ um tamanho mรกximo, depois desse limite ele รฉ guardado em disco. +* Isso significa que a classe funciona bem com arquivos grandes como imagens, vรญdeos, binรกrios extensos, etc. Sem consumir toda a memรณria. +* Vocรช pode obter metadados do arquivo enviado. +* Ela possui uma interface semelhante a arquivos `async`. +* Ela expรตe um objeto python `SpooledTemporaryFile` que vocรช pode repassar para bibliotecas que esperam um objeto com comportamento de arquivo. + +### `UploadFile` + +`UploadFile` tem os seguintes atributos: + +* `filename`: Uma string (`str`) com o nome original do arquivo enviado (e.g. `myimage.jpg`). +* `content-type`: Uma `str` com o tipo do conteรบdo (tipo MIME / media) (e.g. `image/jpeg`). +* `file`: Um objeto do tipo `SpooledTemporaryFile` (um objeto file-like). O arquivo propriamente dito que vocรช pode passar diretamente para outras funรงรตes ou bibliotecas que esperam um objeto "file-like". + +`UploadFile` tem os seguintes mรฉtodos `async`. Todos eles chamam os mรฉtodos de arquivos por baixo dos panos (usando o objeto `SpooledTemporaryFile` interno). + +* `write(data)`: escreve dados (`data`) em `str` ou `bytes` no arquivo. +* `read(size)`: Lรช um nรบmero de bytes/caracteres de acordo com a quantidade `size` (`int`). +* `seek(offset)`: Navega para o byte na posiรงรฃo `offset` (`int`) do arquivo. + * E.g., `await myfile.seek(0)` navegaria para o รญnicio do arquivo. + * Isso รฉ especialmente รบtil se vocรช executar `await myfile.read()` uma vez e depois precisar ler os conteรบdos do arquivo de novo. +* `close()`: Fecha o arquivo. + +Como todos esses mรฉtodos sรฃo assรญncronos (`async`) vocรช precisa esperar ("await") por eles. + +Por exemplo, dentro de uma *funรงรฃo de operaรงรฃo de rota* assรญncrona vocรช pode obter os conteรบdos com: + +```Python +contents = await myfile.read() +``` + +Se vocรช estiver dentro de uma *funรงรฃo de operaรงรฃo de rota* definida normalmente com `def`, vocรช pode acessar `UploadFile.file` diretamente, por exemplo: + +```Python +contents = myfile.file.read() +``` + +/// note | Detalhes tรฉcnicos do `async` + +Quando vocรช utiliza mรฉtodos assรญncronos, o **FastAPI** executa os mรฉtodos do arquivo em uma threadpool e espera por eles. + +/// + +/// note | Detalhes tรฉcnicos do Starlette + +O `UploadFile` do **FastAPI** herda diretamente do `UploadFile` do **Starlette**, mas adiciona algumas funcionalidades necessรกrias para ser compatรญvel com o **Pydantic** + +/// + +## O que รฉ "Form Data" + +A forma como formulรกrios HTML(`
`) enviam dados para o servidor normalmente utilizam uma codificaรงรฃo "especial" para esses dados, que รฉ diferente do JSON. + +O **FastAPI** garante que os dados serรฃo lidos da forma correta, em vez do JSON. + +/// note | Detalhes Tรฉcnicos + +Dados vindos de formulรกrios geralmente tem a codificaรงรฃo com o "media type" `application/x-www-form-urlencoded` quando estes nรฃo incluem arquivos. + +Mas quando os dados incluem arquivos, eles sรฃo codificados como `multipart/form-data`. Se vocรช utilizar `File`, **FastAPI** saberรก que deve receber os arquivos da parte correta do corpo da requisiรงรฃo. + +Se vocรช quer ler mais sobre essas codificaรงรตes e campos de formulรกrio, veja a documentaรงรฃo online da MDN sobre POST . + +/// + +/// warning | Aviso + +Vocรช pode declarar mรบltiplos parรขmetros `File` e `Form` em uma *operaรงรฃo de rota*, mas vocรช nรฃo pode declarar campos `Body`que seriam recebidos como JSON junto desses parรขmetros, por que a codificaรงรฃo do corpo da requisiรงรฃo serรก `multipart/form-data` em vez de `application/json`. + +Isso nรฃo รฉ uma limitaรงรฃo do **FastAPI**, รฉ uma parte do protocolo HTTP. + +/// + +## Arquivo de upload opcional + +Vocรช pode definir um arquivo como opcional utilizando as anotaรงรตes de tipo padrรฃo e definindo o valor padrรฃo como `None`: + +//// tab | Python 3.10+ + +```Python hl_lines="9 17" +{!> ../../../docs_src/request_files/tutorial001_02_an_py310.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python hl_lines="9 17" +{!> ../../../docs_src/request_files/tutorial001_02_an_py39.py!} +``` + +//// + +//// tab | Python 3.8+ + +```Python hl_lines="10 18" +{!> ../../../docs_src/request_files/tutorial001_02_an.py!} +``` + +//// + +//// tab | Python 3.10+ non-Annotated + +/// tip | Dica + +Utilize a versรฃo com `Annotated`, se possรญvel + +/// + +```Python hl_lines="7 15" +{!> ../../../docs_src/request_files/tutorial001_02_py310.py!} +``` + +//// + +//// tab | Python 3.8+ non-Annotated + +/// tip | Dica + +Utilize a versรฃo com `Annotated`, se possรญvel + +/// + +```Python hl_lines="9 17" +{!> ../../../docs_src/request_files/tutorial001_02.py!} +``` + +//// + +## `UploadFile` com Metadados Adicionais + +Vocรช tambรฉm pode utilizar `File()` com `UploadFile`, por exemplo, para definir metadados adicionais: + +//// tab | Python 3.9+ + +```Python hl_lines="9 15" +{!> ../../../docs_src/request_files/tutorial001_03_an_py39.py!} +``` + +//// + +//// tab | Python 3.8+ + +```Python hl_lines="8 14" +{!> ../../../docs_src/request_files/tutorial001_03_an.py!} +``` + +//// + +//// tab | Python 3.8+ non-Annotated + +/// tip | Dica + +Utilize a versรฃo com `Annotated` se possรญvel + +/// + +```Python hl_lines="7 13" +{!> ../../../docs_src/request_files/tutorial001_03.py!} +``` + +//// + +## Envio de Mรบltiplos Arquivos + +ร‰ possรญvel enviar mรบltiplos arquivos ao mesmo tmepo. + +Ele ficam associados ao mesmo "campo do formulรกrio" enviado com "form data". + +Para usar isso, declare uma lista de `bytes` ou `UploadFile`: + +//// tab | Python 3.9+ + +```Python hl_lines="10 15" +{!> ../../../docs_src/request_files/tutorial002_an_py39.py!} +``` + +//// + +//// tab | Python 3.8+ + +```Python hl_lines="11 16" +{!> ../../../docs_src/request_files/tutorial002_an.py!} +``` + +//// + +//// tab | Python 3.9+ non-Annotated + +/// tip | Dica + +Utilize a versรฃo com `Annotated` se possรญvel + +/// + +```Python hl_lines="8 13" +{!> ../../../docs_src/request_files/tutorial002_py39.py!} +``` + +//// + +//// tab | Python 3.8+ non-Annotated + +/// tip | Dica + +Utilize a versรฃo com `Annotated` se possรญvel + +/// + +```Python hl_lines="10 15" +{!> ../../../docs_src/request_files/tutorial002.py!} +``` + +//// + +Vocรช irรก receber, como delcarado uma lista (`list`) de `bytes` ou `UploadFile`s, + +/// note | Detalhes Tรฉcnicos + +Vocรช tambรฉm poderia utilizar `from starlette.responses import HTMLResponse`. + +O **FastAPI** fornece as mesmas `starlette.responses` como `fastapi.responses` apenas como um facilitador para vocรช, desenvolvedor. Mas a maior parte das respostas vem diretamente do Starlette. + +/// + +### Enviando Mรบltiplos Arquivos com Metadados Adicionais + +E da mesma forma que antes, vocรช pode utilizar `File()` para definir parรขmetros adicionais, atรฉ mesmo para `UploadFile`: + +//// tab | Python 3.9+ + +```Python hl_lines="11 18-20" +{!> ../../../docs_src/request_files/tutorial003_an_py39.py!} +``` + +//// + +//// tab | Python 3.8+ + +```Python hl_lines="12 19-21" +{!> ../../../docs_src/request_files/tutorial003_an.py!} +``` + +//// + +//// tab | Python 3.9+ non-Annotated + +/// tip | Dica + +Utilize a versรฃo com `Annotated` se possรญvel. + +/// + +```Python hl_lines="9 16" +{!> ../../../docs_src/request_files/tutorial003_py39.py!} +``` + +//// + +//// tab | Python 3.8+ non-Annotated + +/// tip | Dica + +Utilize a versรฃo com `Annotated` se possรญvel. + +/// + +```Python hl_lines="11 18" +{!> ../../../docs_src/request_files/tutorial003.py!} +``` + +//// + +## Recapitulando + +Use `File`, `bytes` e `UploadFile` para declarar arquivos que serรฃo enviados na requisiรงรฃo, enviados como dados do formulรกrio. diff --git a/docs/pt/docs/tutorial/testing.md b/docs/pt/docs/tutorial/testing.md new file mode 100644 index 000000000..f734a7d9a --- /dev/null +++ b/docs/pt/docs/tutorial/testing.md @@ -0,0 +1,249 @@ +# Testando + +Graรงas ao Starlette, testar aplicativos **FastAPI** รฉ fรกcil e agradรกvel. + +Ele รฉ baseado no HTTPX, que por sua vez รฉ projetado com base em Requests, por isso รฉ muito familiar e intuitivo. + +Com ele, vocรช pode usar o pytest diretamente com **FastAPI**. + +## Usando `TestClient` + +/// info | "Informaรงรฃo" + +Para usar o `TestClient`, primeiro instale o `httpx`. + +Certifique-se de criar um [ambiente virtual](../virtual-environments.md){.internal-link target=_blank}, ativรก-lo e instalรก-lo, por exemplo: + +```console +$ pip install httpx +``` + +/// + +Importe `TestClient`. + +Crie um `TestClient` passando seu aplicativo **FastAPI** para ele. + +Crie funรงรตes com um nome que comece com `test_` (essa รฉ a convenรงรฃo padrรฃo do `pytest`). + +Use o objeto `TestClient` da mesma forma que vocรช faz com `httpx`. + +Escreva instruรงรตes `assert` simples com as expressรตes Python padrรฃo que vocรช precisa verificar (novamente, `pytest` padrรฃo). + +```Python hl_lines="2 12 15-18" +{!../../../docs_src/app_testing/tutorial001.py!} +``` + +/// tip | "Dica" + +Observe que as funรงรตes de teste sรฃo `def` normais, nรฃo `async def`. + +E as chamadas para o cliente tambรฉm sรฃo chamadas normais, nรฃo usando `await`. + +Isso permite que vocรช use `pytest` diretamente sem complicaรงรตes. + +/// + +/// note | "Detalhes tรฉcnicos" + +Vocรช tambรฉm pode usar `from starlette.testclient import TestClient`. + +**FastAPI** fornece o mesmo `starlette.testclient` que `fastapi.testclient` apenas como uma conveniรชncia para vocรช, o desenvolvedor. Mas ele vem diretamente da Starlette. + +/// + +/// tip | "Dica" + +Se vocรช quiser chamar funรงรตes `async` em seus testes alรฉm de enviar solicitaรงรตes ao seu aplicativo FastAPI (por exemplo, funรงรตes de banco de dados assรญncronas), dรช uma olhada em [Testes assรญncronos](../advanced/async-tests.md){.internal-link target=_blank} no tutorial avanรงado. + +/// + +## Separando testes + +Em uma aplicaรงรฃo real, vocรช provavelmente teria seus testes em um arquivo diferente. + +E seu aplicativo **FastAPI** tambรฉm pode ser composto de vรกrios arquivos/mรณdulos, etc. + +### Arquivo do aplicativo **FastAPI** + +Digamos que vocรช tenha uma estrutura de arquivo conforme descrito em [Aplicativos maiores](bigger-applications.md){.internal-link target=_blank}: + +``` +. +โ”œโ”€โ”€ app +โ”‚ย ย  โ”œโ”€โ”€ __init__.py +โ”‚ย ย  โ””โ”€โ”€ main.py +``` + +No arquivo `main.py` vocรช tem seu aplicativo **FastAPI**: + + +```Python +{!../../../docs_src/app_testing/main.py!} +``` + +### Arquivo de teste + +Entรฃo vocรช poderia ter um arquivo `test_main.py` com seus testes. Ele poderia estar no mesmo pacote Python (o mesmo diretรณrio com um arquivo `__init__.py`): + +``` hl_lines="5" +. +โ”œโ”€โ”€ app +โ”‚ย ย  โ”œโ”€โ”€ __init__.py +โ”‚ย ย  โ”œโ”€โ”€ main.py +โ”‚ย ย  โ””โ”€โ”€ test_main.py +``` + +Como esse arquivo estรก no mesmo pacote, vocรช pode usar importaรงรตes relativas para importar o objeto `app` do mรณdulo `main` (`main.py`): + +```Python hl_lines="3" +{!../../../docs_src/app_testing/test_main.py!} +``` + +...e ter o cรณdigo para os testes como antes. + +## Testando: exemplo estendido + +Agora vamos estender este exemplo e adicionar mais detalhes para ver como testar diferentes partes. + +### Arquivo de aplicativo **FastAPI** estendido + +Vamos continuar com a mesma estrutura de arquivo de antes: + +``` +. +โ”œโ”€โ”€ app +โ”‚ย ย  โ”œโ”€โ”€ __init__.py +โ”‚ย ย  โ”œโ”€โ”€ main.py +โ”‚ย ย  โ””โ”€โ”€ test_main.py +``` + +Digamos que agora o arquivo `main.py` com seu aplicativo **FastAPI** tenha algumas outras **operaรงรตes de rotas**. + +Ele tem uma operaรงรฃo `GET` que pode retornar um erro. + +Ele tem uma operaรงรฃo `POST` que pode retornar vรกrios erros. + +Ambas as *operaรงรตes de rotas* requerem um cabeรงalho `X-Token`. + +//// tab | Python 3.10+ + +```Python +{!> ../../../docs_src/app_testing/app_b_an_py310/main.py!} +``` + +//// + +//// tab | Python 3.9+ + +```Python +{!> ../../../docs_src/app_testing/app_b_an_py39/main.py!} +``` + +//// + +//// tab | Python 3.8+ + +```Python +{!> ../../../docs_src/app_testing/app_b_an/main.py!} +``` + +//// + +//// tab | Python 3.10+ non-Annotated + +/// tip | "Dica" + +Prefira usar a versรฃo `Annotated` se possรญvel. + +/// + +```Python +{!> ../../../docs_src/app_testing/app_b_py310/main.py!} +``` + +//// + +//// tab | Python 3.8+ non-Annotated + +/// tip | "Dica" + +Prefira usar a versรฃo `Annotated` se possรญvel. + +/// + +```Python +{!> ../../../docs_src/app_testing/app_b/main.py!} +``` + +//// + +### Arquivo de teste estendido + +Vocรช pode entรฃo atualizar `test_main.py` com os testes estendidos: + +```Python +{!> ../../../docs_src/app_testing/app_b/test_main.py!} +``` + +Sempre que vocรช precisar que o cliente passe informaรงรตes na requisiรงรฃo e nรฃo souber como, vocรช pode pesquisar (no Google) como fazer isso no `httpx`, ou atรฉ mesmo como fazer isso com `requests`, jรก que o design do HTTPX รฉ baseado no design do Requests. + +Depois รฉ sรณ fazer o mesmo nos seus testes. + +Por exemplo: + +* Para passar um parรขmetro *path* ou *query*, adicione-o ร  prรณpria URL. +* Para passar um corpo JSON, passe um objeto Python (por exemplo, um `dict`) para o parรขmetro `json`. +* Se vocรช precisar enviar *Dados de Formulรกrio* em vez de JSON, use o parรขmetro `data`. +* Para passar *headers*, use um `dict` no parรขmetro `headers`. +* Para *cookies*, um `dict` no parรขmetro `cookies`. + +Para mais informaรงรตes sobre como passar dados para o backend (usando `httpx` ou `TestClient`), consulte a documentaรงรฃo do HTTPX. + +/// info | "Informaรงรฃo" + +Observe que o `TestClient` recebe dados que podem ser convertidos para JSON, nรฃo para modelos Pydantic. + +Se vocรช tiver um modelo Pydantic em seu teste e quiser enviar seus dados para o aplicativo durante o teste, poderรก usar o `jsonable_encoder` descrito em [Codificador compatรญvel com JSON](encoder.md){.internal-link target=_blank}. + +/// + +## Execute-o + +Depois disso, vocรช sรณ precisa instalar o `pytest`. + +Certifique-se de criar um [ambiente virtual](../virtual-environments.md){.internal-link target=_blank}, ativรก-lo e instalรก-lo, por exemplo: + +
+ +```console +$ pip install pytest + +---> 100% +``` + +
+ +Ele detectarรก os arquivos e os testes automaticamente, os executarรก e informarรก os resultados para vocรช. + +Execute os testes com: + +
+ +```console +$ pytest + +================ test session starts ================ +platform linux -- Python 3.6.9, pytest-5.3.5, py-1.8.1, pluggy-0.13.1 +rootdir: /home/user/code/superawesome-cli/app +plugins: forked-1.1.3, xdist-1.31.0, cov-2.8.1 +collected 6 items + +---> 100% + +test_main.py ...... [100%] + +================= 1 passed in 0.03s ================= +``` + +
diff --git a/docs/pt/docs/virtual-environments.md b/docs/pt/docs/virtual-environments.md new file mode 100644 index 000000000..863c8d65e --- /dev/null +++ b/docs/pt/docs/virtual-environments.md @@ -0,0 +1,844 @@ +# Ambientes Virtuais + +Ao trabalhar em projetos Python, vocรช provavelmente deve usar um **ambiente virtual** (ou um mecanismo similar) para isolar os pacotes que vocรช instala para cada projeto. + +/// info | "Informaรงรฃo" + +Se vocรช jรก sabe sobre ambientes virtuais, como criรก-los e usรก-los, talvez seja melhor pular esta seรงรฃo. ๐Ÿค“ + +/// + +/// tip | "Dica" + +Um **ambiente virtual** รฉ diferente de uma **variรกvel de ambiente**. + +Uma **variรกvel de ambiente** รฉ uma variรกvel no sistema que pode ser usada por programas. + +Um **ambiente virtual** รฉ um diretรณrio com alguns arquivos. + +/// + +/// info | "Informaรงรฃo" + +Esta pรกgina lhe ensinarรก como usar **ambientes virtuais** e como eles funcionam. + +Se vocรช estiver pronto para adotar uma **ferramenta que gerencia tudo** para vocรช (incluindo a instalaรงรฃo do Python), experimente uv. + +/// + +## Criar um Projeto + +Primeiro, crie um diretรณrio para seu projeto. + +O que normalmente faรงo รฉ criar um diretรณrio chamado `code` dentro do meu diretรณrio home/user. + +E dentro disso eu crio um diretรณrio por projeto. + +
+ +```console +// Vรก para o diretรณrio inicial +$ cd +// Crie um diretรณrio para todos os seus projetos de cรณdigo +$ mkdir code +// Entre nesse diretรณrio de cรณdigo +$ cd code +// Crie um diretรณrio para este projeto +$ mkdir awesome-project +// Entre no diretรณrio do projeto +$ cd awesome-project +``` + +
+ +## Crie um ambiente virtual + +Ao comeรงar a trabalhar em um projeto Python **pela primeira vez**, crie um ambiente virtual **dentro do seu projeto**. + +/// tip | "Dica" + +Vocรช sรณ precisa fazer isso **uma vez por projeto**, nรฃo toda vez que trabalhar. + +/// + +//// tab | `venv` + +Para criar um ambiente virtual, vocรช pode usar o mรณdulo `venv` que vem com o Python. + +
+ +```console +$ python -m venv .venv +``` + +
+ +/// details | O que esse comando significa + +* `python`: usa o programa chamado `python` +* `-m`: chama um mรณdulo como um script, nรณs diremos a ele qual mรณdulo vem em seguida +* `venv`: usa o mรณdulo chamado `venv` que normalmente vem instalado com o Python +* `.venv`: cria o ambiente virtual no novo diretรณrio `.venv` + +/// + +//// + +//// tab | `uv` + +Se vocรช tiver o `uv` instalado, poderรก usรก-lo para criar um ambiente virtual. + +
+ +```console +$ uv venv +``` + +
+ +/// tip | "Dica" + +Por padrรฃo, `uv` criarรก um ambiente virtual em um diretรณrio chamado `.venv`. + +Mas vocรช pode personalizรก-lo passando um argumento adicional com o nome do diretรณrio. + +/// + +//// + +Esse comando cria um novo ambiente virtual em um diretรณrio chamado `.venv`. + +/// details | `.venv` ou outro nome + +Vocรช pode criar o ambiente virtual em um diretรณrio diferente, mas hรก uma convenรงรฃo para chamรก-lo de `.venv`. + +/// + +## Ative o ambiente virtual + +Ative o novo ambiente virtual para que qualquer comando Python que vocรช executar ou pacote que vocรช instalar o utilize. + +/// tip | "Dica" + +Faรงa isso **toda vez** que iniciar uma **nova sessรฃo de terminal** para trabalhar no projeto. + +/// + +//// tab | Linux, macOS + +
+ +```console +$ source .venv/bin/activate +``` + +
+ +//// + +//// tab | Windows PowerShell + +
+ +```console +$ .venv\Scripts\Activate.ps1 +``` + +
+ +//// + +//// tab | Windows Bash + +Ou se vocรช usa o Bash para Windows (por exemplo, Git Bash): + +
+ +```console +$ source .venv/Scripts/activate +``` + +
+ +//// + +/// tip | "Dica" + +Toda vez que vocรช instalar um **novo pacote** naquele ambiente, **ative** o ambiente novamente. + +Isso garante que, se vocรช usar um **programa de terminal (CLI)** instalado por esse pacote, vocรช usarรก aquele do seu ambiente virtual e nรฃo qualquer outro que possa ser instalado globalmente, provavelmente com uma versรฃo diferente do que vocรช precisa. + +/// + +## Verifique se o ambiente virtual estรก ativo + +Verifique se o ambiente virtual estรก ativo (o comando anterior funcionou). + +/// tip | "Dica" + +Isso รฉ **opcional**, mas รฉ uma boa maneira de **verificar** se tudo estรก funcionando conforme o esperado e se vocรช estรก usando o ambiente virtual pretendido. + +/// + +//// tab | Linux, macOS, Windows Bash + +
+ +```console +$ which python + +/home/user/code/awesome-project/.venv/bin/python +``` + +
+ +Se ele mostrar o binรกrio `python` em `.venv/bin/python`, dentro do seu projeto (neste caso `awesome-project`), entรฃo funcionou. ๐ŸŽ‰ + +//// + +//// tab | Windows PowerShell + +
+ +```console +$ Get-Command python + +C:\Users\user\code\awesome-project\.venv\Scripts\python +``` + +
+ +Se ele mostrar o binรกrio `python` em `.venv\Scripts\python`, dentro do seu projeto (neste caso `awesome-project`), entรฃo funcionou. ๐ŸŽ‰ + +//// + +## Atualizar `pip` + +/// tip | "Dica" + +Se vocรช usar `uv`, vocรช o usarรก para instalar coisas em vez do `pip`, entรฃo nรฃo precisarรก atualizar o `pip`. ๐Ÿ˜Ž + +/// + +Se vocรช estiver usando `pip` para instalar pacotes (ele vem por padrรฃo com o Python), vocรช deve **atualizรก-lo** para a versรฃo mais recente. + +Muitos erros exรณticos durante a instalaรงรฃo de um pacote sรฃo resolvidos apenas atualizando o `pip` primeiro. + +/// tip | "Dica" + +Normalmente, vocรช faria isso **uma vez**, logo apรณs criar o ambiente virtual. + +/// + +Certifique-se de que o ambiente virtual esteja ativo (com o comando acima) e execute: + +
+ +```console +$ python -m pip install --upgrade pip + +---> 100% +``` + +
+ +## Adicionar `.gitignore` + +Se vocรช estiver usando **Git** (vocรช deveria), adicione um arquivo `.gitignore` para excluir tudo em seu `.venv` do Git. + +/// tip | "Dica" + +Se vocรช usou `uv` para criar o ambiente virtual, ele jรก fez isso para vocรช, vocรช pode pular esta etapa. ๐Ÿ˜Ž + +/// + +/// tip | "Dica" + +Faรงa isso **uma vez**, logo apรณs criar o ambiente virtual. + +/// + +
+ +```console +$ echo "*" > .venv/.gitignore +``` + +
+ +/// details | O que esse comando significa + +* `echo "*"`: irรก "imprimir" o texto `*` no terminal (a prรณxima parte muda isso um pouco) +* `>`: qualquer coisa impressa no terminal pelo comando ร  esquerda de `>` nรฃo deve ser impressa, mas sim escrita no arquivo que vai ร  direita de `>` +* `.gitignore`: o nome do arquivo onde o texto deve ser escrito + +E `*` para Git significa "tudo". Entรฃo, ele ignorarรก tudo no diretรณrio `.venv`. + +Esse comando criarรก um arquivo `.gitignore` com o conteรบdo: + +```gitignore +* +``` + +/// + +## Instalar Pacotes + +Apรณs ativar o ambiente, vocรช pode instalar pacotes nele. + +/// tip | "Dica" + +Faรงa isso **uma vez** ao instalar ou atualizar os pacotes que seu projeto precisa. + +Se precisar atualizar uma versรฃo ou adicionar um novo pacote, vocรช **farรก isso novamente**. + +/// + +### Instalar pacotes diretamente + +Se estiver com pressa e nรฃo quiser usar um arquivo para declarar os requisitos de pacote do seu projeto, vocรช pode instalรก-los diretamente. + +/// tip | "Dica" + +ร‰ uma (muito) boa ideia colocar os pacotes e versรตes que seu programa precisa em um arquivo (por exemplo `requirements.txt` ou `pyproject.toml`). + +/// + +//// tab | `pip` + +
+ +```console +$ pip install "fastapi[standard]" + +---> 100% +``` + +
+ +//// + +//// tab | `uv` + +Se vocรช tem o `uv`: + +
+ +```console +$ uv pip install "fastapi[standard]" +---> 100% +``` + +
+ +//// + +### Instalar a partir de `requirements.txt` + +Se vocรช tiver um `requirements.txt`, agora poderรก usรก-lo para instalar seus pacotes. + +//// tab | `pip` + +
+ +```console +$ pip install -r requirements.txt +---> 100% +``` + +
+ +//// + +//// tab | `uv` + +Se vocรช tem o `uv`: + +
+ +```console +$ uv pip install -r requirements.txt +---> 100% +``` + +
+ +//// + +/// details | `requirements.txt` + +Um `requirements.txt` com alguns pacotes poderia se parecer com: + +```requirements.txt +fastapi[standard]==0.113.0 +pydantic==2.8.0 +``` + +/// + +## Execute seu programa + +Depois de ativar o ambiente virtual, vocรช pode executar seu programa, e ele usarรก o Python dentro do seu ambiente virtual com os pacotes que vocรช instalou lรก. + +
+ +```console +$ python main.py + +Hello World +``` + +
+ +## Configure seu editor + +Vocรช provavelmente usaria um editor. Certifique-se de configurรก-lo para usar o mesmo ambiente virtual que vocรช criou (ele provavelmente o detectarรก automaticamente) para que vocรช possa obter erros de preenchimento automรกtico e em linha. + +Por exemplo: + +* VS Code +* PyCharm + +/// tip | "Dica" + +Normalmente, vocรช sรณ precisa fazer isso **uma vez**, ao criar o ambiente virtual. + +/// + +## Desativar o ambiente virtual + +Quando terminar de trabalhar no seu projeto, vocรช pode **desativar** o ambiente virtual. + +
+ +```console +$ deactivate +``` + +
+ +Dessa forma, quando vocรช executar `python`, ele nรฃo tentarรก executรก-lo naquele ambiente virtual com os pacotes instalados nele. + +## Pronto para trabalhar + +Agora vocรช estรก pronto para comeรงar a trabalhar no seu projeto. + + + +/// tip | "Dica" + +Vocรช quer entender o que รฉ tudo isso acima? + +Continue lendo. ๐Ÿ‘‡๐Ÿค“ + +/// + +## Por que ambientes virtuais + +Para trabalhar com o FastAPI, vocรช precisa instalar o Python. + +Depois disso, vocรช precisarรก **instalar** o FastAPI e quaisquer outros **pacotes** que queira usar. + +Para instalar pacotes, vocรช normalmente usaria o comando `pip` que vem com o Python (ou alternativas semelhantes). + +No entanto, se vocรช usar `pip` diretamente, os pacotes serรฃo instalados no seu **ambiente Python global** (a instalaรงรฃo global do Python). + +### O Problema + +Entรฃo, qual รฉ o problema em instalar pacotes no ambiente global do Python? + +Em algum momento, vocรช provavelmente acabarรก escrevendo muitos programas diferentes que dependem de **pacotes diferentes**. E alguns desses projetos em que vocรช trabalha dependerรฃo de **versรตes diferentes** do mesmo pacote. ๐Ÿ˜ฑ + +Por exemplo, vocรช pode criar um projeto chamado `philosophers-stone`, este programa depende de outro pacote chamado **`harry`, usando a versรฃo `1`**. Entรฃo, vocรช precisa instalar `harry`. + +```mermaid +flowchart LR + stone(philosophers-stone) -->|requires| harry-1[harry v1] +``` + +Entรฃo, em algum momento depois, vocรช cria outro projeto chamado `prisoner-of-azkaban`, e esse projeto tambรฉm depende de `harry`, mas esse projeto precisa do **`harry` versรฃo `3`**. + +```mermaid +flowchart LR + azkaban(prisoner-of-azkaban) --> |requires| harry-3[harry v3] +``` + +Mas agora o problema รฉ que, se vocรช instalar os pacotes globalmente (no ambiente global) em vez de em um **ambiente virtual** local, vocรช terรก que escolher qual versรฃo do `harry` instalar. + +Se vocรช quiser executar `philosophers-stone`, precisarรก primeiro instalar `harry` versรฃo `1`, por exemplo com: + +
+ +```console +$ pip install "harry==1" +``` + +
+ +E entรฃo vocรช acabaria com `harry` versรฃo `1` instalado em seu ambiente Python global. + +```mermaid +flowchart LR + subgraph global[global env] + harry-1[harry v1] + end + subgraph stone-project[philosophers-stone project] + stone(philosophers-stone) -->|requires| harry-1 + end +``` + +Mas se vocรช quiser executar `prisoner-of-azkaban`, vocรช precisarรก desinstalar `harry` versรฃo `1` e instalar `harry` versรฃo `3` (ou apenas instalar a versรฃo `3` desinstalaria automaticamente a versรฃo `1`). + +
+ +```console +$ pip install "harry==3" +``` + +
+ +E entรฃo vocรช acabaria com `harry` versรฃo `3` instalado em seu ambiente Python global. + +E se vocรช tentar executar `philosophers-stone` novamente, hรก uma chance de que **nรฃo funcione** porque ele precisa de `harry` versรฃo `1`. + +```mermaid +flowchart LR + subgraph global[global env] + harry-1[harry v1] + style harry-1 fill:#ccc,stroke-dasharray: 5 5 + harry-3[harry v3] + end + subgraph stone-project[philosophers-stone project] + stone(philosophers-stone) -.-x|โ›”๏ธ| harry-1 + end + subgraph azkaban-project[prisoner-of-azkaban project] + azkaban(prisoner-of-azkaban) --> |requires| harry-3 + end +``` + +/// tip | "Dica" + +ร‰ muito comum em pacotes Python tentar ao mรกximo **evitar alteraรงรตes drรกsticas** em **novas versรตes**, mas รฉ melhor prevenir do que remediar e instalar versรตes mais recentes intencionalmente e, quando possรญvel, executar os testes para verificar se tudo estรก funcionando corretamente. + +/// + +Agora, imagine isso com **muitos** outros **pacotes** dos quais todos os seus **projetos dependem**. Isso รฉ muito difรญcil de gerenciar. E vocรช provavelmente acabaria executando alguns projetos com algumas **versรตes incompatรญveis** dos pacotes, e nรฃo saberia por que algo nรฃo estรก funcionando. + +Alรฉm disso, dependendo do seu sistema operacional (por exemplo, Linux, Windows, macOS), ele pode ter vindo com o Python jรก instalado. E, nesse caso, provavelmente tinha alguns pacotes prรฉ-instalados com algumas versรตes especรญficas **necessรกrias para o seu sistema**. Se vocรช instalar pacotes no ambiente global do Python, poderรก acabar **quebrando** alguns dos programas que vieram com seu sistema operacional. + +## Onde os pacotes sรฃo instalados + +Quando vocรช instala o Python, ele cria alguns diretรณrios com alguns arquivos no seu computador. + +Alguns desses diretรณrios sรฃo os responsรกveis โ€‹โ€‹por ter todos os pacotes que vocรช instala. + +Quando vocรช executa: + +
+ +```console +// Nรฃo execute isso agora, รฉ apenas um exemplo ๐Ÿค“ +$ pip install "fastapi[standard]" +---> 100% +``` + +
+ +Isso farรก o download de um arquivo compactado com o cรณdigo FastAPI, normalmente do PyPI. + +Ele tambรฉm farรก o **download** de arquivos para outros pacotes dos quais o FastAPI depende. + +Em seguida, ele **extrairรก** todos esses arquivos e os colocarรก em um diretรณrio no seu computador. + +Por padrรฃo, ele colocarรก os arquivos baixados e extraรญdos no diretรณrio que vem com a instalaรงรฃo do Python, que รฉ o **ambiente global**. + +## O que sรฃo ambientes virtuais + +A soluรงรฃo para os problemas de ter todos os pacotes no ambiente global รฉ usar um **ambiente virtual para cada projeto** em que vocรช trabalha. + +Um ambiente virtual รฉ um **diretรณrio**, muito semelhante ao global, onde vocรช pode instalar os pacotes para um projeto. + +Dessa forma, cada projeto terรก seu prรณprio ambiente virtual (diretรณrio `.venv`) com seus prรณprios pacotes. + +```mermaid +flowchart TB + subgraph stone-project[philosophers-stone project] + stone(philosophers-stone) --->|requires| harry-1 + subgraph venv1[.venv] + harry-1[harry v1] + end + end + subgraph azkaban-project[prisoner-of-azkaban project] + azkaban(prisoner-of-azkaban) --->|requires| harry-3 + subgraph venv2[.venv] + harry-3[harry v3] + end + end + stone-project ~~~ azkaban-project +``` + +## O que significa ativar um ambiente virtual + +Quando vocรช ativa um ambiente virtual, por exemplo com: + +//// tab | Linux, macOS + +
+ +```console +$ source .venv/bin/activate +``` + +
+ +//// + +//// tab | Windows PowerShell + +
+ +```console +$ .venv\Scripts\Activate.ps1 +``` + +
+ +//// + +//// tab | Windows Bash + +Ou se vocรช usa o Bash para Windows (por exemplo, Git Bash): + +
+ +```console +$ source .venv/Scripts/activate +``` + +
+ +//// + +Esse comando criarรก ou modificarรก algumas [variรกveis โ€‹โ€‹de ambiente](environment-variables.md){.internal-link target=_blank} que estarรฃo disponรญveis para os prรณximos comandos. + +Uma dessas variรกveis โ€‹โ€‹รฉ a variรกvel `PATH`. + +/// tip | "Dica" + +Vocรช pode aprender mais sobre a variรกvel de ambiente `PATH` na seรงรฃo [Variรกveis โ€‹โ€‹de ambiente](environment-variables.md#path-environment-variable){.internal-link target=_blank}. + +/// + +A ativaรงรฃo de um ambiente virtual adiciona seu caminho `.venv/bin` (no Linux e macOS) ou `.venv\Scripts` (no Windows) ร  variรกvel de ambiente `PATH`. + +Digamos que antes de ativar o ambiente, a variรกvel `PATH` estava assim: + +//// tab | Linux, macOS + +```plaintext +/usr/bin:/bin:/usr/sbin:/sbin +``` + +Isso significa que o sistema procuraria programas em: + +* `/usr/bin` +* `/bin` +* `/usr/sbin` +* `/sbin` + +//// + +//// tab | Windows + +```plaintext +C:\Windows\System32 +``` + +Isso significa que o sistema procuraria programas em: + +* `C:\Windows\System32` + +//// + +Apรณs ativar o ambiente virtual, a variรกvel `PATH` ficaria mais ou menos assim: + +//// tab | Linux, macOS + +```plaintext +/home/user/code/awesome-project/.venv/bin:/usr/bin:/bin:/usr/sbin:/sbin +``` + +Isso significa que o sistema agora comeรงarรก a procurar primeiro por programas em: + +```plaintext +/home/user/code/awesome-project/.venv/bin +``` + +antes de procurar nos outros diretรณrios. + +Entรฃo, quando vocรช digita `python` no terminal, o sistema encontrarรก o programa Python em + +```plaintext +/home/user/code/awesome-project/.venv/bin/python +``` + +e usa esse. + +//// + +//// tab | Windows + +```plaintext +C:\Users\user\code\awesome-project\.venv\Scripts;C:\Windows\System32 +``` + +Isso significa que o sistema agora comeรงarรก a procurar primeiro por programas em: + +```plaintext +C:\Users\user\code\awesome-project\.venv\Scripts +``` + +antes de procurar nos outros diretรณrios. + +Entรฃo, quando vocรช digita `python` no terminal, o sistema encontrarรก o programa Python em + +```plaintext +C:\Users\user\code\awesome-project\.venv\Scripts\python +``` + +e usa esse. + +//// + +Um detalhe importante รฉ que ele colocarรก o caminho do ambiente virtual no **inรญcio** da variรกvel `PATH`. O sistema o encontrarรก **antes** de encontrar qualquer outro Python disponรญvel. Dessa forma, quando vocรช executar `python`, ele usarรก o Python **do ambiente virtual** em vez de qualquer outro `python` (por exemplo, um `python` de um ambiente global). + +Ativar um ambiente virtual tambรฉm muda algumas outras coisas, mas esta รฉ uma das mais importantes. + +## Verificando um ambiente virtual + +Ao verificar se um ambiente virtual estรก ativo, por exemplo com: + +//// tab | Linux, macOS, Windows Bash + +
+ +```console +$ which python + +/home/user/code/awesome-project/.venv/bin/python +``` + +
+ +//// + +//// tab | Windows PowerShell + +
+ +```console +$ Get-Command python + +C:\Users\user\code\awesome-project\.venv\Scripts\python +``` + +
+ +//// + +Isso significa que o programa `python` que serรก usado รฉ aquele **no ambiente virtual**. + +vocรช usa `which` no Linux e macOS e `Get-Command` no Windows PowerShell. + +A maneira como esse comando funciona รฉ que ele vai e verifica na variรกvel de ambiente `PATH`, passando por **cada caminho em ordem**, procurando pelo programa chamado `python`. Uma vez que ele o encontre, ele **mostrarรก o caminho** para esse programa. + +A parte mais importante รฉ que quando vocรช chama ``python`, esse รฉ exatamente o "`python`" que serรก executado. + +Assim, vocรช pode confirmar se estรก no ambiente virtual correto. + +/// tip | "Dica" + +ร‰ fรกcil ativar um ambiente virtual, obter um Python e entรฃo **ir para outro projeto**. + +E o segundo projeto **nรฃo funcionaria** porque vocรช estรก usando o **Python incorreto**, de um ambiente virtual para outro projeto. + +ร‰ รบtil poder verificar qual `python` estรก sendo usado. ๐Ÿค“ + +/// + +## Por que desativar um ambiente virtual + +Por exemplo, vocรช pode estar trabalhando em um projeto `philosophers-stone`, **ativar esse ambiente virtual**, instalar pacotes e trabalhar com esse ambiente. + +E entรฃo vocรช quer trabalhar em **outro projeto** `prisoner-of-azkaban`. + +Vocรช vai para aquele projeto: + +
+ +```console +$ cd ~/code/prisoner-of-azkaban +``` + +
+ +Se vocรช nรฃo desativar o ambiente virtual para `philosophers-stone`, quando vocรช executar `python` no terminal, ele tentarรก usar o Python de `philosophers-stone`. + +
+ +```console +$ cd ~/code/prisoner-of-azkaban + +$ python main.py + +// Erro ao importar o Sirius, ele nรฃo estรก instalado ๐Ÿ˜ฑ +Traceback (most recent call last): + File "main.py", line 1, in + import sirius +``` + +
+ +Mas se vocรช desativar o ambiente virtual e ativar o novo para `prisoner-of-askaban`, quando vocรช executar `python`, ele usarรก o Python do ambiente virtual em `prisoner-of-azkaban`. + +
+ +```console +$ cd ~/code/prisoner-of-azkaban + +// Vocรช nรฃo precisa estar no diretรณrio antigo para desativar, vocรช pode fazer isso de onde estiver, mesmo depois de ir para o outro projeto ๐Ÿ˜Ž +$ deactivate + +// Ative o ambiente virtual em prisoner-of-azkaban/.venv ๐Ÿš€ +$ source .venv/bin/activate + +// Agora, quando vocรช executar o python, ele encontrarรก o pacote sirius instalado neste ambiente virtual โœจ +$ python main.py + +Eu juro solenemente ๐Ÿบ +``` + +
+ +## Alternativas + +Este รฉ um guia simples para vocรช comeรงar e lhe ensinar como tudo funciona **por baixo**. + +Existem muitas **alternativas** para gerenciar ambientes virtuais, dependรชncias de pacotes (requisitos) e projetos. + +Quando estiver pronto e quiser usar uma ferramenta para **gerenciar todo o projeto**, dependรชncias de pacotes, ambientes virtuais, etc., sugiro que vocรช experimente o uv. + +`uv` pode fazer muitas coisas, ele pode: + +* **Instalar o Python** para vocรช, incluindo versรตes diferentes +* Gerenciar o **ambiente virtual** para seus projetos +* Instalar **pacotes** +* Gerenciar **dependรชncias e versรตes** de pacotes para seu projeto +* Certifique-se de ter um conjunto **exato** de pacotes e versรตes para instalar, incluindo suas dependรชncias, para que vocรช possa ter certeza de que pode executar seu projeto em produรงรฃo exatamente da mesma forma que em seu computador durante o desenvolvimento, isso รฉ chamado de **bloqueio** +* E muitas outras coisas + +## Conclusรฃo + +Se vocรช leu e entendeu tudo isso, agora **vocรช sabe muito mais** sobre ambientes virtuais do que muitos desenvolvedores por aรญ. ๐Ÿค“ + +Saber esses detalhes provavelmente serรก รบtil no futuro, quando vocรช estiver depurando algo que parece complexo, mas vocรช saberรก **como tudo funciona**. ๐Ÿ˜Ž diff --git a/docs/zh/docs/how-to/index.md b/docs/zh/docs/how-to/index.md index 262dcfaee..ac097618b 100644 --- a/docs/zh/docs/how-to/index.md +++ b/docs/zh/docs/how-to/index.md @@ -6,7 +6,7 @@ ๅฆ‚ๆžœๆŸไบ›ๅ†…ๅฎน็œ‹่ตทๆฅๅฏนไฝ ็š„้กน็›ฎๆœ‰็”จ๏ผŒ่ฏท็ปง็ปญๆŸฅ้˜…๏ผŒๅฆๅˆ™่ฏท็›ดๆŽฅ่ทณ่ฟ‡ๅฎƒไปฌใ€‚ -/// ๅฐๆŠ€ๅทง +/// tip | ๅฐๆŠ€ๅทง ๅฆ‚ๆžœไฝ ๆƒณไปฅ็ณป็ปŸ็š„ๆ–นๅผ**ๅญฆไน  FastAPI**๏ผˆๆŽจ่๏ผ‰๏ผŒ่ฏท้˜…่ฏป [ๆ•™็จ‹ - ็”จๆˆทๆŒ‡ๅ—](../tutorial/index.md){.internal-link target=_blank} ็š„ๆฏไธ€็ซ ่Š‚ใ€‚ diff --git a/docs/zh/docs/project-generation.md b/docs/zh/docs/project-generation.md index 0655cb0a9..9b3735539 100644 --- a/docs/zh/docs/project-generation.md +++ b/docs/zh/docs/project-generation.md @@ -1,84 +1,28 @@ -# ้กน็›ฎ็”Ÿๆˆ - ๆจกๆฟ - -้กน็›ฎ็”Ÿๆˆๅ™จไธ€่ˆฌ้ƒฝไผšๆไพ›ๅพˆๅคšๅˆๅง‹่ฎพ็ฝฎใ€ๅฎ‰ๅ…จๆŽชๆ–ฝใ€ๆ•ฐๆฎๅบ“๏ผŒ็”š่‡ณ่ฟ˜ๅ‡†ๅค‡ๅฅฝไบ†็ฌฌไธ€ไธช API ็ซฏ็‚น๏ผŒ่ƒฝๅธฎๅŠฉๆ‚จๅฟซ้€ŸไธŠๆ‰‹ใ€‚ - -้กน็›ฎ็”Ÿๆˆๅ™จ็š„่ฎพ็ฝฎ้€šๅธธ้ƒฝๅพˆไธป่ง‚๏ผŒๆ‚จๅฏไปฅๆŒ‰้œ€ๆ›ดๆ–ฐๆˆ–ไฟฎๆ”น๏ผŒไฝ†ๅฏนไบŽๆ‚จ็š„้กน็›ฎๆฅ่ฏด๏ผŒๅฎƒๆ˜ฏ้žๅธธๅฅฝ็š„่ตท็‚นใ€‚ - -## ๅ…จๆ ˆ FastAPI + PostgreSQL - -GitHub๏ผšhttps://github.com/tiangolo/full-stack-fastapi-postgresql - -### ๅ…จๆ ˆ FastAPI + PostgreSQL - ๅŠŸ่ƒฝ - -* ๅฎŒๆ•ด็š„ **Docker** ้›†ๆˆ๏ผˆๅŸบไบŽ Docker๏ผ‰ -* Docker Swarm ๅผ€ๅ‘ๆจกๅผ -* **Docker Compose** ๆœฌๅœฐๅผ€ๅ‘้›†ๆˆไธŽไผ˜ๅŒ– -* **็”Ÿไบงๅฏ็”จ**็š„ Python ็ฝ‘็ปœๆœๅŠกๅ™จ๏ผŒไฝฟ็”จ Uvicorn ๆˆ– Gunicorn -* Python **FastAPI** ๅŽ็ซฏ๏ผš -* * **้€Ÿๅบฆๅฟซ**๏ผšๅฏไธŽ **NodeJS** ๅ’Œ **Go** ๆฏ”่‚ฉ็š„ๆž้ซ˜ๆ€ง่ƒฝ๏ผˆๅฝ’ๅŠŸไบŽ Starlette ๅ’Œ Pydantic๏ผ‰ - * **็›ด่ง‚**๏ผšๅผบๅคง็š„็ผ–่พ‘ๅ™จๆ”ฏๆŒ๏ผŒๅค„ๅค„็š†ๅฏ่‡ชๅŠจ่กฅๅ…จ๏ผŒๅ‡ๅฐ‘่ฐƒ่ฏ•ๆ—ถ้—ด - * **็ฎ€ๅ•**๏ผšๆ˜“ๅญฆใ€ๆ˜“็”จ๏ผŒ้˜…่ฏปๆ–‡ๆกฃๆ‰€้œ€ๆ—ถ้—ดๆ›ด็Ÿญ - * **็ฎ€็Ÿญ**๏ผšไปฃ็ ้‡ๅคๆœ€ๅฐๅŒ–๏ผŒๆฏๆฌกๅ‚ๆ•ฐๅฃฐๆ˜Ž้ƒฝๅฏไปฅๅฎž็ŽฐๅคšไธชๅŠŸ่ƒฝ - * **ๅฅๅฃฎ**๏ผš ็”Ÿไบง็บงๅˆซ็š„ไปฃ็ ๏ผŒ่ฟ˜ๆœ‰่‡ชๅŠจไบคไบ’ๆ–‡ๆกฃ - * **ๅŸบไบŽๆ ‡ๅ‡†**๏ผšๅฎŒๅ…จๅ…ผๅฎนๅนถๅŸบไบŽ API ๅผ€ๆ”พๆ ‡ๅ‡†๏ผšOpenAPI ๅ’Œ JSON Schema - * **ๆ›ดๅคšๅŠŸ่ƒฝ**ๅŒ…ๆ‹ฌ่‡ชๅŠจ้ชŒ่ฏใ€ๅบๅˆ—ๅŒ–ใ€ไบคไบ’ๆ–‡ๆกฃใ€OAuth2 JWT ไปค็‰Œ่บซไปฝ้ชŒ่ฏ็ญ‰ -* **ๅฎ‰ๅ…จๅฏ†็ **๏ผŒ้ป˜่ฎคไฝฟ็”จๅฏ†็ ๅ“ˆๅธŒ -* **JWT ไปค็‰Œ**่บซไปฝ้ชŒ่ฏ -* **SQLAlchemy** ๆจกๅž‹๏ผˆ็‹ฌ็ซ‹ไบŽ Flask ๆ‰ฉๅฑ•๏ผŒๅฏ็›ดๆŽฅ็”จไบŽ Celery Worker๏ผ‰ -* ๅŸบ็ก€็š„็”จๆˆทๆจกๅž‹๏ผˆๅฏๆŒ‰้œ€ไฟฎๆ”นๆˆ–ๅˆ ้™ค๏ผ‰ -* **Alembic** ่ฟ็งป -* **CORS**๏ผˆ่ทจๅŸŸ่ต„ๆบๅ…ฑไบซ๏ผ‰ -* **Celery** Worker ๅฏไปŽๅŽ็ซฏๅ…ถๅฎƒ้ƒจๅˆ†ๆœ‰้€‰ๆ‹ฉๅœฐๅฏผๅ…ฅๅนถไฝฟ็”จๆจกๅž‹ๅ’Œไปฃ็  -* REST ๅŽ็ซฏๆต‹่ฏ•ๅŸบไบŽ Pytest๏ผŒๅนถไธŽ Docker ้›†ๆˆ๏ผŒๅฏ็‹ฌ็ซ‹ไบŽๆ•ฐๆฎๅบ“ๅฎž็ŽฐๅฎŒๆ•ด็š„ API ไบคไบ’ๆต‹่ฏ•ใ€‚ๅ› ไธบๆ˜ฏๅœจ Docker ไธญ่ฟ่กŒ๏ผŒๆฏๆฌก้ƒฝๅฏไปŽๅคดๆž„ๅปบๆ–ฐ็š„ๆ•ฐๆฎๅญ˜ๅ‚จ๏ผˆไฝฟ็”จ ElasticSearchใ€MongoDBใ€CouchDB ็ญ‰ๆ•ฐๆฎๅบ“๏ผŒไป…ๆต‹่ฏ• API ่ฟ่กŒ๏ผ‰ -* Python ไธŽ **Jupyter Kernels** ้›†ๆˆ๏ผŒ็”จไบŽ่ฟœ็จ‹ๆˆ– Docker ๅฎนๅ™จๅ†…้ƒจๅผ€ๅ‘๏ผŒไฝฟ็”จ Atom Hydrogen ๆˆ– Visual Studio Code ็š„ Jupyter ๆ’ไปถ -* **Vue** ๅ‰็ซฏ๏ผš - * ็”ฑ Vue CLI ็”Ÿๆˆ - * **JWT ่บซไปฝ้ชŒ่ฏ**ๅค„็† - * ็™ปๅฝ•่ง†ๅ›พ - * ็™ปๅฝ•ๅŽๆ˜พ็คบไธปไปช่กจ็›˜่ง†ๅ›พ - * ไธปไปช่กจ็›˜ๆ”ฏๆŒ็”จๆˆทๅˆ›ๅปบไธŽ็ผ–่พ‘ - * ็”จๆˆทไฟกๆฏ็ผ–่พ‘ - * **Vuex** - * **Vue-router** - * **Vuetify** ็พŽๅŒ–็ป„ไปถ - * **TypeScript** - * ๅŸบไบŽ **Nginx** ็š„ Docker ๆœๅŠกๅ™จ๏ผˆไผ˜ๅŒ–ไบ† Vue-router ้…็ฝฎ๏ผ‰ - * Docker ๅคš้˜ถๆฎตๆž„ๅปบ๏ผŒๆ— ้œ€ไฟๅญ˜ๆˆ–ๆไบค็ผ–่ฏ‘็š„ไปฃ็  - * ๅœจๆž„ๅปบๆ—ถ่ฟ่กŒๅ‰็ซฏๆต‹่ฏ•๏ผˆๅฏ็ฆ็”จ๏ผ‰ - * ๅฐฝ้‡ๆจกๅ—ๅŒ–๏ผŒๅผ€็ฎฑๅณ็”จ๏ผŒไฝ†ไปๅฏไฝฟ็”จ Vue CLI ้‡ๆ–ฐ็”Ÿๆˆๆˆ–ๅˆ›ๅปบๆ‰€้œ€้กน็›ฎ๏ผŒๆˆ–ๅค็”จๆ‰€้œ€ๅ†…ๅฎน -* ไฝฟ็”จ **PGAdmin** ็ฎก็† PostgreSQL ๆ•ฐๆฎๅบ“๏ผŒๅฏ่ฝปๆพๆ›ฟๆขไธบ PHPMyAdmin ๆˆ– MySQL -* ไฝฟ็”จ **Flower** ็›‘ๆŽง Celery ไปปๅŠก -* ไฝฟ็”จ **Traefik** ๅค„็†ๅ‰ๅŽ็ซฏ่ดŸ่ฝฝๅนณ่กก๏ผŒๅฏๆŠŠๅ‰ๅŽ็ซฏๆ”พๅœจๅŒไธ€ไธชๅŸŸไธ‹๏ผŒๆŒ‰่ทฏๅพ„ๅˆ†้š”๏ผŒไฝ†ๅœจไธๅŒๅฎนๅ™จไธญๆไพ›ๆœๅŠก -* Traefik ้›†ๆˆ๏ผŒๅŒ…ๆ‹ฌ่‡ชๅŠจ็”Ÿๆˆ Let's Encrypt **HTTPS** ๅ‡ญ่ฏ -* GitLab **CI**๏ผˆๆŒ็ปญ้›†ๆˆ๏ผ‰๏ผŒๅŒ…ๆ‹ฌๅ‰ๅŽ็ซฏๆต‹่ฏ• - -## ๅ…จๆ ˆ FastAPI + Couchbase - -GitHub๏ผšhttps://github.com/tiangolo/full-stack-fastapi-couchbase - -โš ๏ธ **่ญฆๅ‘Š** โš ๏ธ - -ๅฆ‚ๆžœๆ‚จๆƒณไปŽๅคดๅผ€ๅง‹ๅˆ›ๅปบๆ–ฐ้กน็›ฎ๏ผŒๅปบ่ฎฎไฝฟ็”จไปฅไธ‹ๅค‡้€‰ๆ–นๆกˆใ€‚ - -ไพ‹ๅฆ‚๏ผŒ้กน็›ฎ็”Ÿๆˆๅ™จๅ…จๆ ˆ FastAPI + PostgreSQLย ไผšๆ›ด้€‚็”จ๏ผŒ่ฟ™ไธช้กน็›ฎ็š„็ปดๆŠค็งฏๆž๏ผŒ็”จ็š„ไบบไนŸๅคš๏ผŒ่ฟ˜ๅŒ…ๆ‹ฌไบ†ๆ‰€ๆœ‰ๆ–ฐๅŠŸ่ƒฝๅ’Œๆ”น่ฟ›ๅ†…ๅฎนใ€‚ - -ๅฝ“็„ถ๏ผŒๆ‚จไนŸๅฏไปฅๆ”พๅฟƒไฝฟ็”จ่ฟ™ไธชๅŸบไบŽ Couchbase ็š„็”Ÿๆˆๅ™จ๏ผŒๅฎƒไนŸ่ƒฝๆญฃๅธธไฝฟ็”จใ€‚ๅฐฑ็ฎ—็”จๅฎƒ็”Ÿๆˆ้กน็›ฎไนŸๆฒกๆœ‰ไปปไฝ•้—ฎ้ข˜๏ผˆไธบไบ†ๆ›ดๅฅฝๅœฐๆปก่ถณ้œ€ๆฑ‚๏ผŒๆ‚จๅฏไปฅ่‡ช่กŒๆ›ดๆ–ฐ่ฟ™ไธช้กน็›ฎ๏ผ‰ใ€‚ - -่ฏฆ่ง่ต„ๆบไป“ๅบ“ไธญ็š„ๆ–‡ๆกฃใ€‚ - -## ๅ…จๆ ˆ FastAPI + MongoDB - -โ€ฆโ€ฆๆ•ฌ่ฏทๆœŸๅพ…๏ผŒๅพ—็œ‹ๆˆ‘ๆœ‰ๆฒกๆœ‰ๆ—ถ้—ดๅš่ฟ™ไธช้กน็›ฎใ€‚๐Ÿ˜… ๐ŸŽ‰ - -## FastAPI + spaCy ๆœบๅ™จๅญฆไน ๆจกๅž‹ - -GitHub๏ผšhttps://github.com/microsoft/cookiecutter-spacy-fastapi - -### FastAPI + spaCy ๆœบๅ™จๅญฆไน ๆจกๅž‹ - ๅŠŸ่ƒฝ - -* ้›†ๆˆ **spaCy** NER ๆจกๅž‹ -* ๅ†…็ฝฎ **Azure ่ฎค็Ÿฅๆœ็ดข**่ฏทๆฑ‚ๆ ผๅผ -* **็”Ÿไบงๅฏ็”จ**็š„ Python ็ฝ‘็ปœๆœๅŠกๅ™จ๏ผŒไฝฟ็”จ Uvicorn ไธŽ Gunicorn -* ๅ†…็ฝฎ **Azure DevOps** Kubernetes (AKS) CI/CD ๅผ€ๅ‘ -* **ๅคš่ฏญ**ๆ”ฏๆŒ๏ผŒๅฏๅœจ้กน็›ฎ่ฎพ็ฝฎๆ—ถ้€‰ๆ‹ฉ spaCy ๅ†…็ฝฎ็š„่ฏญ่จ€ -* ไธไป…ๅฑ€้™ไบŽ spaCy๏ผŒๅฏ**่ฝปๆพๆ‰ฉๅฑ•**่‡ณๅ…ถๅฎƒๆจกๅž‹ๆก†ๆžถ๏ผˆPytorchใ€TensorFlow๏ผ‰ +# FastAPIๅ…จๆ ˆๆจกๆฟ + +ๆจกๆฟ้€šๅธธๅธฆๆœ‰็‰นๅฎš็š„่ฎพ็ฝฎ๏ผŒ่€Œไธ”่ขซ่ฎพ่ฎกไธบ็ตๆดปๅ’Œๅฏๅฎšๅˆถ็š„ใ€‚่ฟ™ๅ…่ฎธๆ‚จๆ นๆฎ้กน็›ฎ็š„้œ€ๆฑ‚ไฟฎๆ”นๅ’Œ่ฐƒๆ•ดๅฎƒไปฌ๏ผŒไฝฟๅฎƒไปฌๆˆไธบไธ€ไธชๅพˆๅฅฝ็š„่ตท็‚นใ€‚๐Ÿ + +ๆ‚จๅฏไปฅไฝฟ็”จๆญคๆจกๆฟๅผ€ๅง‹๏ผŒๅ› ไธบๅฎƒๅŒ…ๅซไบ†่ฎธๅคšๅทฒ็ปไธบๆ‚จๅฎŒๆˆ็š„ๅˆๅง‹่ฎพ็ฝฎใ€ๅฎ‰ๅ…จๆ€งใ€ๆ•ฐๆฎๅบ“ๅ’Œไธ€ไบ›API็ซฏ็‚นใ€‚ + +ไปฃ็ ไป“๏ผš Full Stack FastAPI Template + +## FastAPIๅ…จๆ ˆๆจกๆฟ - ๆŠ€ๆœฏๆ ˆๅ’Œ็‰นๆ€ง + +- โšก [**FastAPI**](https://fastapi.tiangolo.com) ็”จไบŽPythonๅŽ็ซฏAPI. + - ๐Ÿงฐ [SQLModel](https://sqlmodel.tiangolo.com) ็”จไบŽPythonๅ’ŒSQLๆ•ฐๆฎๅบ“็š„้›†ๆˆ๏ผˆORM๏ผ‰ใ€‚ + - ๐Ÿ” [Pydantic](https://docs.pydantic.dev) FastAPI็š„ไพ่ต–้กนไน‹ไธ€๏ผŒ็”จไบŽๆ•ฐๆฎ้ชŒ่ฏๅ’Œ้…็ฝฎ็ฎก็†ใ€‚ + - ๐Ÿ’พ [PostgreSQL](https://www.postgresql.org) ไฝœไธบSQLๆ•ฐๆฎๅบ“ใ€‚ +- ๐Ÿš€ [React](https://react.dev) ็”จไบŽๅ‰็ซฏใ€‚ + - ๐Ÿ’ƒ ไฝฟ็”จไบ†TypeScriptใ€hooksใ€Viteๅ’Œๅ…ถไป–ไธ€ไบ›็ŽฐไปฃๅŒ–็š„ๅ‰็ซฏๆŠ€ๆœฏๆ ˆใ€‚ + - ๐ŸŽจ [Chakra UI](https://chakra-ui.com) ็”จไบŽๅ‰็ซฏ็ป„ไปถใ€‚ + - ๐Ÿค– ไธ€ไธช่‡ชๅŠจๅŒ–็”Ÿๆˆ็š„ๅ‰็ซฏๅฎขๆˆท็ซฏใ€‚ + - ๐Ÿงช Playwright็”จไบŽ็ซฏๅˆฐ็ซฏๆต‹่ฏ•ใ€‚ + - ๐Ÿฆ‡ ๆ”ฏๆŒๆš—้ป‘ไธป้ข˜๏ผˆDark mode๏ผ‰ใ€‚ +- ๐Ÿ‹ [Docker Compose](https://www.docker.com) ็”จไบŽๅผ€ๅ‘็Žฏๅขƒๅ’Œ็”Ÿไบง็Žฏๅขƒใ€‚ +- ๐Ÿ”’ ้ป˜่ฎคไฝฟ็”จๅฏ†็ ๅ“ˆๅธŒๆฅไฟ่ฏๅฎ‰ๅ…จใ€‚ +- ๐Ÿ”‘ JWTไปค็‰Œ็”จไบŽๆƒ้™้ชŒ่ฏใ€‚ +- ๐Ÿ“ซ ไฝฟ็”จ้‚ฎ็ฎฑๆฅ่ฟ›่กŒๅฏ†็ ๆขๅคใ€‚ +- โœ… ๅ•ๅ…ƒๆต‹่ฏ•็”จไบ†[Pytest](https://pytest.org). +- ๐Ÿ“ž [Traefik](https://traefik.io) ็”จไบŽๅๅ‘ไปฃ็†ๅ’Œ่ดŸ่ฝฝๅ‡่กกใ€‚ +- ๐Ÿšข ้ƒจ็ฝฒๆŒ‡ๅ—๏ผˆDocker Compose๏ผ‰ๅŒ…ๅซไบ†ๅฆ‚ไฝ•่ตทไธ€ไธชTraefikๅ‰็ซฏไปฃ็†ๆฅ่‡ชๅŠจๅŒ–HTTPS่ฎค่ฏใ€‚ +- ๐Ÿญ CI๏ผˆๆŒ็ปญ้›†ๆˆ๏ผ‰ๅ’Œ CD๏ผˆๆŒ็ปญ้ƒจ็ฝฒ๏ผ‰ๅŸบไบŽGitHub Actionsใ€‚ diff --git a/docs_src/async_tests/test_main.py b/docs_src/async_tests/test_main.py index 9f1527d5f..a57a31f7d 100644 --- a/docs_src/async_tests/test_main.py +++ b/docs_src/async_tests/test_main.py @@ -1,12 +1,14 @@ import pytest -from httpx import AsyncClient +from httpx import ASGITransport, AsyncClient from .main import app @pytest.mark.anyio async def test_root(): - async with AsyncClient(app=app, base_url="http://test") as ac: + async with AsyncClient( + transport=ASGITransport(app=app), base_url="http://test" + ) as ac: response = await ac.get("/") assert response.status_code == 200 assert response.json() == {"message": "Tomato"} diff --git a/docs_src/cookie_param_models/tutorial001.py b/docs_src/cookie_param_models/tutorial001.py new file mode 100644 index 000000000..cc65c43e1 --- /dev/null +++ b/docs_src/cookie_param_models/tutorial001.py @@ -0,0 +1,17 @@ +from typing import Union + +from fastapi import Cookie, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Cookies(BaseModel): + session_id: str + fatebook_tracker: Union[str, None] = None + googall_tracker: Union[str, None] = None + + +@app.get("/items/") +async def read_items(cookies: Cookies = Cookie()): + return cookies diff --git a/docs_src/cookie_param_models/tutorial001_an.py b/docs_src/cookie_param_models/tutorial001_an.py new file mode 100644 index 000000000..e5839ffd5 --- /dev/null +++ b/docs_src/cookie_param_models/tutorial001_an.py @@ -0,0 +1,18 @@ +from typing import Union + +from fastapi import Cookie, FastAPI +from pydantic import BaseModel +from typing_extensions import Annotated + +app = FastAPI() + + +class Cookies(BaseModel): + session_id: str + fatebook_tracker: Union[str, None] = None + googall_tracker: Union[str, None] = None + + +@app.get("/items/") +async def read_items(cookies: Annotated[Cookies, Cookie()]): + return cookies diff --git a/docs_src/cookie_param_models/tutorial001_an_py310.py b/docs_src/cookie_param_models/tutorial001_an_py310.py new file mode 100644 index 000000000..24cc889a9 --- /dev/null +++ b/docs_src/cookie_param_models/tutorial001_an_py310.py @@ -0,0 +1,17 @@ +from typing import Annotated + +from fastapi import Cookie, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Cookies(BaseModel): + session_id: str + fatebook_tracker: str | None = None + googall_tracker: str | None = None + + +@app.get("/items/") +async def read_items(cookies: Annotated[Cookies, Cookie()]): + return cookies diff --git a/docs_src/cookie_param_models/tutorial001_an_py39.py b/docs_src/cookie_param_models/tutorial001_an_py39.py new file mode 100644 index 000000000..3d90c2007 --- /dev/null +++ b/docs_src/cookie_param_models/tutorial001_an_py39.py @@ -0,0 +1,17 @@ +from typing import Annotated, Union + +from fastapi import Cookie, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Cookies(BaseModel): + session_id: str + fatebook_tracker: Union[str, None] = None + googall_tracker: Union[str, None] = None + + +@app.get("/items/") +async def read_items(cookies: Annotated[Cookies, Cookie()]): + return cookies diff --git a/docs_src/cookie_param_models/tutorial001_py310.py b/docs_src/cookie_param_models/tutorial001_py310.py new file mode 100644 index 000000000..7cdee5a92 --- /dev/null +++ b/docs_src/cookie_param_models/tutorial001_py310.py @@ -0,0 +1,15 @@ +from fastapi import Cookie, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Cookies(BaseModel): + session_id: str + fatebook_tracker: str | None = None + googall_tracker: str | None = None + + +@app.get("/items/") +async def read_items(cookies: Cookies = Cookie()): + return cookies diff --git a/docs_src/cookie_param_models/tutorial002.py b/docs_src/cookie_param_models/tutorial002.py new file mode 100644 index 000000000..9679e890f --- /dev/null +++ b/docs_src/cookie_param_models/tutorial002.py @@ -0,0 +1,19 @@ +from typing import Union + +from fastapi import Cookie, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Cookies(BaseModel): + model_config = {"extra": "forbid"} + + session_id: str + fatebook_tracker: Union[str, None] = None + googall_tracker: Union[str, None] = None + + +@app.get("/items/") +async def read_items(cookies: Cookies = Cookie()): + return cookies diff --git a/docs_src/cookie_param_models/tutorial002_an.py b/docs_src/cookie_param_models/tutorial002_an.py new file mode 100644 index 000000000..ce5644b7b --- /dev/null +++ b/docs_src/cookie_param_models/tutorial002_an.py @@ -0,0 +1,20 @@ +from typing import Union + +from fastapi import Cookie, FastAPI +from pydantic import BaseModel +from typing_extensions import Annotated + +app = FastAPI() + + +class Cookies(BaseModel): + model_config = {"extra": "forbid"} + + session_id: str + fatebook_tracker: Union[str, None] = None + googall_tracker: Union[str, None] = None + + +@app.get("/items/") +async def read_items(cookies: Annotated[Cookies, Cookie()]): + return cookies diff --git a/docs_src/cookie_param_models/tutorial002_an_py310.py b/docs_src/cookie_param_models/tutorial002_an_py310.py new file mode 100644 index 000000000..7fa70fe92 --- /dev/null +++ b/docs_src/cookie_param_models/tutorial002_an_py310.py @@ -0,0 +1,19 @@ +from typing import Annotated + +from fastapi import Cookie, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Cookies(BaseModel): + model_config = {"extra": "forbid"} + + session_id: str + fatebook_tracker: str | None = None + googall_tracker: str | None = None + + +@app.get("/items/") +async def read_items(cookies: Annotated[Cookies, Cookie()]): + return cookies diff --git a/docs_src/cookie_param_models/tutorial002_an_py39.py b/docs_src/cookie_param_models/tutorial002_an_py39.py new file mode 100644 index 000000000..a906ce6a1 --- /dev/null +++ b/docs_src/cookie_param_models/tutorial002_an_py39.py @@ -0,0 +1,19 @@ +from typing import Annotated, Union + +from fastapi import Cookie, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Cookies(BaseModel): + model_config = {"extra": "forbid"} + + session_id: str + fatebook_tracker: Union[str, None] = None + googall_tracker: Union[str, None] = None + + +@app.get("/items/") +async def read_items(cookies: Annotated[Cookies, Cookie()]): + return cookies diff --git a/docs_src/cookie_param_models/tutorial002_pv1.py b/docs_src/cookie_param_models/tutorial002_pv1.py new file mode 100644 index 000000000..13f78b850 --- /dev/null +++ b/docs_src/cookie_param_models/tutorial002_pv1.py @@ -0,0 +1,20 @@ +from typing import Union + +from fastapi import Cookie, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Cookies(BaseModel): + class Config: + extra = "forbid" + + session_id: str + fatebook_tracker: Union[str, None] = None + googall_tracker: Union[str, None] = None + + +@app.get("/items/") +async def read_items(cookies: Cookies = Cookie()): + return cookies diff --git a/docs_src/cookie_param_models/tutorial002_pv1_an.py b/docs_src/cookie_param_models/tutorial002_pv1_an.py new file mode 100644 index 000000000..ddfda9b6f --- /dev/null +++ b/docs_src/cookie_param_models/tutorial002_pv1_an.py @@ -0,0 +1,21 @@ +from typing import Union + +from fastapi import Cookie, FastAPI +from pydantic import BaseModel +from typing_extensions import Annotated + +app = FastAPI() + + +class Cookies(BaseModel): + class Config: + extra = "forbid" + + session_id: str + fatebook_tracker: Union[str, None] = None + googall_tracker: Union[str, None] = None + + +@app.get("/items/") +async def read_items(cookies: Annotated[Cookies, Cookie()]): + return cookies diff --git a/docs_src/cookie_param_models/tutorial002_pv1_an_py310.py b/docs_src/cookie_param_models/tutorial002_pv1_an_py310.py new file mode 100644 index 000000000..ac00360b6 --- /dev/null +++ b/docs_src/cookie_param_models/tutorial002_pv1_an_py310.py @@ -0,0 +1,20 @@ +from typing import Annotated + +from fastapi import Cookie, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Cookies(BaseModel): + class Config: + extra = "forbid" + + session_id: str + fatebook_tracker: str | None = None + googall_tracker: str | None = None + + +@app.get("/items/") +async def read_items(cookies: Annotated[Cookies, Cookie()]): + return cookies diff --git a/docs_src/cookie_param_models/tutorial002_pv1_an_py39.py b/docs_src/cookie_param_models/tutorial002_pv1_an_py39.py new file mode 100644 index 000000000..573caea4b --- /dev/null +++ b/docs_src/cookie_param_models/tutorial002_pv1_an_py39.py @@ -0,0 +1,20 @@ +from typing import Annotated, Union + +from fastapi import Cookie, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Cookies(BaseModel): + class Config: + extra = "forbid" + + session_id: str + fatebook_tracker: Union[str, None] = None + googall_tracker: Union[str, None] = None + + +@app.get("/items/") +async def read_items(cookies: Annotated[Cookies, Cookie()]): + return cookies diff --git a/docs_src/cookie_param_models/tutorial002_pv1_py310.py b/docs_src/cookie_param_models/tutorial002_pv1_py310.py new file mode 100644 index 000000000..2c59aad12 --- /dev/null +++ b/docs_src/cookie_param_models/tutorial002_pv1_py310.py @@ -0,0 +1,18 @@ +from fastapi import Cookie, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Cookies(BaseModel): + class Config: + extra = "forbid" + + session_id: str + fatebook_tracker: str | None = None + googall_tracker: str | None = None + + +@app.get("/items/") +async def read_items(cookies: Cookies = Cookie()): + return cookies diff --git a/docs_src/cookie_param_models/tutorial002_py310.py b/docs_src/cookie_param_models/tutorial002_py310.py new file mode 100644 index 000000000..f011aa1af --- /dev/null +++ b/docs_src/cookie_param_models/tutorial002_py310.py @@ -0,0 +1,17 @@ +from fastapi import Cookie, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Cookies(BaseModel): + model_config = {"extra": "forbid"} + + session_id: str + fatebook_tracker: str | None = None + googall_tracker: str | None = None + + +@app.get("/items/") +async def read_items(cookies: Cookies = Cookie()): + return cookies diff --git a/docs_src/header_param_models/tutorial001.py b/docs_src/header_param_models/tutorial001.py new file mode 100644 index 000000000..4caaba87b --- /dev/null +++ b/docs_src/header_param_models/tutorial001.py @@ -0,0 +1,19 @@ +from typing import List, Union + +from fastapi import FastAPI, Header +from pydantic import BaseModel + +app = FastAPI() + + +class CommonHeaders(BaseModel): + host: str + save_data: bool + if_modified_since: Union[str, None] = None + traceparent: Union[str, None] = None + x_tag: List[str] = [] + + +@app.get("/items/") +async def read_items(headers: CommonHeaders = Header()): + return headers diff --git a/docs_src/header_param_models/tutorial001_an.py b/docs_src/header_param_models/tutorial001_an.py new file mode 100644 index 000000000..b55c6b56b --- /dev/null +++ b/docs_src/header_param_models/tutorial001_an.py @@ -0,0 +1,20 @@ +from typing import List, Union + +from fastapi import FastAPI, Header +from pydantic import BaseModel +from typing_extensions import Annotated + +app = FastAPI() + + +class CommonHeaders(BaseModel): + host: str + save_data: bool + if_modified_since: Union[str, None] = None + traceparent: Union[str, None] = None + x_tag: List[str] = [] + + +@app.get("/items/") +async def read_items(headers: Annotated[CommonHeaders, Header()]): + return headers diff --git a/docs_src/header_param_models/tutorial001_an_py310.py b/docs_src/header_param_models/tutorial001_an_py310.py new file mode 100644 index 000000000..acfb6b9bf --- /dev/null +++ b/docs_src/header_param_models/tutorial001_an_py310.py @@ -0,0 +1,19 @@ +from typing import Annotated + +from fastapi import FastAPI, Header +from pydantic import BaseModel + +app = FastAPI() + + +class CommonHeaders(BaseModel): + host: str + save_data: bool + if_modified_since: str | None = None + traceparent: str | None = None + x_tag: list[str] = [] + + +@app.get("/items/") +async def read_items(headers: Annotated[CommonHeaders, Header()]): + return headers diff --git a/docs_src/header_param_models/tutorial001_an_py39.py b/docs_src/header_param_models/tutorial001_an_py39.py new file mode 100644 index 000000000..51a5f94fc --- /dev/null +++ b/docs_src/header_param_models/tutorial001_an_py39.py @@ -0,0 +1,19 @@ +from typing import Annotated, Union + +from fastapi import FastAPI, Header +from pydantic import BaseModel + +app = FastAPI() + + +class CommonHeaders(BaseModel): + host: str + save_data: bool + if_modified_since: Union[str, None] = None + traceparent: Union[str, None] = None + x_tag: list[str] = [] + + +@app.get("/items/") +async def read_items(headers: Annotated[CommonHeaders, Header()]): + return headers diff --git a/docs_src/header_param_models/tutorial001_py310.py b/docs_src/header_param_models/tutorial001_py310.py new file mode 100644 index 000000000..7239c64ce --- /dev/null +++ b/docs_src/header_param_models/tutorial001_py310.py @@ -0,0 +1,17 @@ +from fastapi import FastAPI, Header +from pydantic import BaseModel + +app = FastAPI() + + +class CommonHeaders(BaseModel): + host: str + save_data: bool + if_modified_since: str | None = None + traceparent: str | None = None + x_tag: list[str] = [] + + +@app.get("/items/") +async def read_items(headers: CommonHeaders = Header()): + return headers diff --git a/docs_src/header_param_models/tutorial001_py39.py b/docs_src/header_param_models/tutorial001_py39.py new file mode 100644 index 000000000..4c1137813 --- /dev/null +++ b/docs_src/header_param_models/tutorial001_py39.py @@ -0,0 +1,19 @@ +from typing import Union + +from fastapi import FastAPI, Header +from pydantic import BaseModel + +app = FastAPI() + + +class CommonHeaders(BaseModel): + host: str + save_data: bool + if_modified_since: Union[str, None] = None + traceparent: Union[str, None] = None + x_tag: list[str] = [] + + +@app.get("/items/") +async def read_items(headers: CommonHeaders = Header()): + return headers diff --git a/docs_src/header_param_models/tutorial002.py b/docs_src/header_param_models/tutorial002.py new file mode 100644 index 000000000..3f9aac58d --- /dev/null +++ b/docs_src/header_param_models/tutorial002.py @@ -0,0 +1,21 @@ +from typing import List, Union + +from fastapi import FastAPI, Header +from pydantic import BaseModel + +app = FastAPI() + + +class CommonHeaders(BaseModel): + model_config = {"extra": "forbid"} + + host: str + save_data: bool + if_modified_since: Union[str, None] = None + traceparent: Union[str, None] = None + x_tag: List[str] = [] + + +@app.get("/items/") +async def read_items(headers: CommonHeaders = Header()): + return headers diff --git a/docs_src/header_param_models/tutorial002_an.py b/docs_src/header_param_models/tutorial002_an.py new file mode 100644 index 000000000..771135d77 --- /dev/null +++ b/docs_src/header_param_models/tutorial002_an.py @@ -0,0 +1,22 @@ +from typing import List, Union + +from fastapi import FastAPI, Header +from pydantic import BaseModel +from typing_extensions import Annotated + +app = FastAPI() + + +class CommonHeaders(BaseModel): + model_config = {"extra": "forbid"} + + host: str + save_data: bool + if_modified_since: Union[str, None] = None + traceparent: Union[str, None] = None + x_tag: List[str] = [] + + +@app.get("/items/") +async def read_items(headers: Annotated[CommonHeaders, Header()]): + return headers diff --git a/docs_src/header_param_models/tutorial002_an_py310.py b/docs_src/header_param_models/tutorial002_an_py310.py new file mode 100644 index 000000000..e9535f045 --- /dev/null +++ b/docs_src/header_param_models/tutorial002_an_py310.py @@ -0,0 +1,21 @@ +from typing import Annotated + +from fastapi import FastAPI, Header +from pydantic import BaseModel + +app = FastAPI() + + +class CommonHeaders(BaseModel): + model_config = {"extra": "forbid"} + + host: str + save_data: bool + if_modified_since: str | None = None + traceparent: str | None = None + x_tag: list[str] = [] + + +@app.get("/items/") +async def read_items(headers: Annotated[CommonHeaders, Header()]): + return headers diff --git a/docs_src/header_param_models/tutorial002_an_py39.py b/docs_src/header_param_models/tutorial002_an_py39.py new file mode 100644 index 000000000..ca5208c9d --- /dev/null +++ b/docs_src/header_param_models/tutorial002_an_py39.py @@ -0,0 +1,21 @@ +from typing import Annotated, Union + +from fastapi import FastAPI, Header +from pydantic import BaseModel + +app = FastAPI() + + +class CommonHeaders(BaseModel): + model_config = {"extra": "forbid"} + + host: str + save_data: bool + if_modified_since: Union[str, None] = None + traceparent: Union[str, None] = None + x_tag: list[str] = [] + + +@app.get("/items/") +async def read_items(headers: Annotated[CommonHeaders, Header()]): + return headers diff --git a/docs_src/header_param_models/tutorial002_pv1.py b/docs_src/header_param_models/tutorial002_pv1.py new file mode 100644 index 000000000..7e56cd993 --- /dev/null +++ b/docs_src/header_param_models/tutorial002_pv1.py @@ -0,0 +1,22 @@ +from typing import List, Union + +from fastapi import FastAPI, Header +from pydantic import BaseModel + +app = FastAPI() + + +class CommonHeaders(BaseModel): + class Config: + extra = "forbid" + + host: str + save_data: bool + if_modified_since: Union[str, None] = None + traceparent: Union[str, None] = None + x_tag: List[str] = [] + + +@app.get("/items/") +async def read_items(headers: CommonHeaders = Header()): + return headers diff --git a/docs_src/header_param_models/tutorial002_pv1_an.py b/docs_src/header_param_models/tutorial002_pv1_an.py new file mode 100644 index 000000000..236778231 --- /dev/null +++ b/docs_src/header_param_models/tutorial002_pv1_an.py @@ -0,0 +1,23 @@ +from typing import List, Union + +from fastapi import FastAPI, Header +from pydantic import BaseModel +from typing_extensions import Annotated + +app = FastAPI() + + +class CommonHeaders(BaseModel): + class Config: + extra = "forbid" + + host: str + save_data: bool + if_modified_since: Union[str, None] = None + traceparent: Union[str, None] = None + x_tag: List[str] = [] + + +@app.get("/items/") +async def read_items(headers: Annotated[CommonHeaders, Header()]): + return headers diff --git a/docs_src/header_param_models/tutorial002_pv1_an_py310.py b/docs_src/header_param_models/tutorial002_pv1_an_py310.py new file mode 100644 index 000000000..e99e24ea5 --- /dev/null +++ b/docs_src/header_param_models/tutorial002_pv1_an_py310.py @@ -0,0 +1,22 @@ +from typing import Annotated + +from fastapi import FastAPI, Header +from pydantic import BaseModel + +app = FastAPI() + + +class CommonHeaders(BaseModel): + class Config: + extra = "forbid" + + host: str + save_data: bool + if_modified_since: str | None = None + traceparent: str | None = None + x_tag: list[str] = [] + + +@app.get("/items/") +async def read_items(headers: Annotated[CommonHeaders, Header()]): + return headers diff --git a/docs_src/header_param_models/tutorial002_pv1_an_py39.py b/docs_src/header_param_models/tutorial002_pv1_an_py39.py new file mode 100644 index 000000000..18398b726 --- /dev/null +++ b/docs_src/header_param_models/tutorial002_pv1_an_py39.py @@ -0,0 +1,22 @@ +from typing import Annotated, Union + +from fastapi import FastAPI, Header +from pydantic import BaseModel + +app = FastAPI() + + +class CommonHeaders(BaseModel): + class Config: + extra = "forbid" + + host: str + save_data: bool + if_modified_since: Union[str, None] = None + traceparent: Union[str, None] = None + x_tag: list[str] = [] + + +@app.get("/items/") +async def read_items(headers: Annotated[CommonHeaders, Header()]): + return headers diff --git a/docs_src/header_param_models/tutorial002_pv1_py310.py b/docs_src/header_param_models/tutorial002_pv1_py310.py new file mode 100644 index 000000000..3dbff9d7b --- /dev/null +++ b/docs_src/header_param_models/tutorial002_pv1_py310.py @@ -0,0 +1,20 @@ +from fastapi import FastAPI, Header +from pydantic import BaseModel + +app = FastAPI() + + +class CommonHeaders(BaseModel): + class Config: + extra = "forbid" + + host: str + save_data: bool + if_modified_since: str | None = None + traceparent: str | None = None + x_tag: list[str] = [] + + +@app.get("/items/") +async def read_items(headers: CommonHeaders = Header()): + return headers diff --git a/docs_src/header_param_models/tutorial002_pv1_py39.py b/docs_src/header_param_models/tutorial002_pv1_py39.py new file mode 100644 index 000000000..86e19be0d --- /dev/null +++ b/docs_src/header_param_models/tutorial002_pv1_py39.py @@ -0,0 +1,22 @@ +from typing import Union + +from fastapi import FastAPI, Header +from pydantic import BaseModel + +app = FastAPI() + + +class CommonHeaders(BaseModel): + class Config: + extra = "forbid" + + host: str + save_data: bool + if_modified_since: Union[str, None] = None + traceparent: Union[str, None] = None + x_tag: list[str] = [] + + +@app.get("/items/") +async def read_items(headers: CommonHeaders = Header()): + return headers diff --git a/docs_src/header_param_models/tutorial002_py310.py b/docs_src/header_param_models/tutorial002_py310.py new file mode 100644 index 000000000..3d2296345 --- /dev/null +++ b/docs_src/header_param_models/tutorial002_py310.py @@ -0,0 +1,19 @@ +from fastapi import FastAPI, Header +from pydantic import BaseModel + +app = FastAPI() + + +class CommonHeaders(BaseModel): + model_config = {"extra": "forbid"} + + host: str + save_data: bool + if_modified_since: str | None = None + traceparent: str | None = None + x_tag: list[str] = [] + + +@app.get("/items/") +async def read_items(headers: CommonHeaders = Header()): + return headers diff --git a/docs_src/header_param_models/tutorial002_py39.py b/docs_src/header_param_models/tutorial002_py39.py new file mode 100644 index 000000000..f8ce559a7 --- /dev/null +++ b/docs_src/header_param_models/tutorial002_py39.py @@ -0,0 +1,21 @@ +from typing import Union + +from fastapi import FastAPI, Header +from pydantic import BaseModel + +app = FastAPI() + + +class CommonHeaders(BaseModel): + model_config = {"extra": "forbid"} + + host: str + save_data: bool + if_modified_since: Union[str, None] = None + traceparent: Union[str, None] = None + x_tag: list[str] = [] + + +@app.get("/items/") +async def read_items(headers: CommonHeaders = Header()): + return headers diff --git a/docs_src/middleware/tutorial001.py b/docs_src/middleware/tutorial001.py index 6bab3410a..e65a7dade 100644 --- a/docs_src/middleware/tutorial001.py +++ b/docs_src/middleware/tutorial001.py @@ -7,8 +7,8 @@ app = FastAPI() @app.middleware("http") async def add_process_time_header(request: Request, call_next): - start_time = time.time() + start_time = time.perf_counter() response = await call_next(request) - process_time = time.time() - start_time + process_time = time.perf_counter() - start_time response.headers["X-Process-Time"] = str(process_time) return response diff --git a/docs_src/path_params_numeric_validations/tutorial006.py b/docs_src/path_params_numeric_validations/tutorial006.py index 0ea32694a..f07629aa0 100644 --- a/docs_src/path_params_numeric_validations/tutorial006.py +++ b/docs_src/path_params_numeric_validations/tutorial006.py @@ -13,4 +13,6 @@ async def read_items( results = {"item_id": item_id} if q: results.update({"q": q}) + if size: + results.update({"size": size}) return results diff --git a/docs_src/path_params_numeric_validations/tutorial006_an.py b/docs_src/path_params_numeric_validations/tutorial006_an.py index 22a143623..ac4732573 100644 --- a/docs_src/path_params_numeric_validations/tutorial006_an.py +++ b/docs_src/path_params_numeric_validations/tutorial006_an.py @@ -14,4 +14,6 @@ async def read_items( results = {"item_id": item_id} if q: results.update({"q": q}) + if size: + results.update({"size": size}) return results diff --git a/docs_src/path_params_numeric_validations/tutorial006_an_py39.py b/docs_src/path_params_numeric_validations/tutorial006_an_py39.py index 804751893..426ec3776 100644 --- a/docs_src/path_params_numeric_validations/tutorial006_an_py39.py +++ b/docs_src/path_params_numeric_validations/tutorial006_an_py39.py @@ -15,4 +15,6 @@ async def read_items( results = {"item_id": item_id} if q: results.update({"q": q}) + if size: + results.update({"size": size}) return results diff --git a/docs_src/query_param_models/tutorial001.py b/docs_src/query_param_models/tutorial001.py new file mode 100644 index 000000000..0c0ab315e --- /dev/null +++ b/docs_src/query_param_models/tutorial001.py @@ -0,0 +1,19 @@ +from typing import List + +from fastapi import FastAPI, Query +from pydantic import BaseModel, Field +from typing_extensions import Literal + +app = FastAPI() + + +class FilterParams(BaseModel): + limit: int = Field(100, gt=0, le=100) + offset: int = Field(0, ge=0) + order_by: Literal["created_at", "updated_at"] = "created_at" + tags: List[str] = [] + + +@app.get("/items/") +async def read_items(filter_query: FilterParams = Query()): + return filter_query diff --git a/docs_src/query_param_models/tutorial001_an.py b/docs_src/query_param_models/tutorial001_an.py new file mode 100644 index 000000000..28375057c --- /dev/null +++ b/docs_src/query_param_models/tutorial001_an.py @@ -0,0 +1,19 @@ +from typing import List + +from fastapi import FastAPI, Query +from pydantic import BaseModel, Field +from typing_extensions import Annotated, Literal + +app = FastAPI() + + +class FilterParams(BaseModel): + limit: int = Field(100, gt=0, le=100) + offset: int = Field(0, ge=0) + order_by: Literal["created_at", "updated_at"] = "created_at" + tags: List[str] = [] + + +@app.get("/items/") +async def read_items(filter_query: Annotated[FilterParams, Query()]): + return filter_query diff --git a/docs_src/query_param_models/tutorial001_an_py310.py b/docs_src/query_param_models/tutorial001_an_py310.py new file mode 100644 index 000000000..71427acae --- /dev/null +++ b/docs_src/query_param_models/tutorial001_an_py310.py @@ -0,0 +1,18 @@ +from typing import Annotated, Literal + +from fastapi import FastAPI, Query +from pydantic import BaseModel, Field + +app = FastAPI() + + +class FilterParams(BaseModel): + limit: int = Field(100, gt=0, le=100) + offset: int = Field(0, ge=0) + order_by: Literal["created_at", "updated_at"] = "created_at" + tags: list[str] = [] + + +@app.get("/items/") +async def read_items(filter_query: Annotated[FilterParams, Query()]): + return filter_query diff --git a/docs_src/query_param_models/tutorial001_an_py39.py b/docs_src/query_param_models/tutorial001_an_py39.py new file mode 100644 index 000000000..ba690d3e3 --- /dev/null +++ b/docs_src/query_param_models/tutorial001_an_py39.py @@ -0,0 +1,17 @@ +from fastapi import FastAPI, Query +from pydantic import BaseModel, Field +from typing_extensions import Annotated, Literal + +app = FastAPI() + + +class FilterParams(BaseModel): + limit: int = Field(100, gt=0, le=100) + offset: int = Field(0, ge=0) + order_by: Literal["created_at", "updated_at"] = "created_at" + tags: list[str] = [] + + +@app.get("/items/") +async def read_items(filter_query: Annotated[FilterParams, Query()]): + return filter_query diff --git a/docs_src/query_param_models/tutorial001_py310.py b/docs_src/query_param_models/tutorial001_py310.py new file mode 100644 index 000000000..3ebf9f4d7 --- /dev/null +++ b/docs_src/query_param_models/tutorial001_py310.py @@ -0,0 +1,18 @@ +from typing import Literal + +from fastapi import FastAPI, Query +from pydantic import BaseModel, Field + +app = FastAPI() + + +class FilterParams(BaseModel): + limit: int = Field(100, gt=0, le=100) + offset: int = Field(0, ge=0) + order_by: Literal["created_at", "updated_at"] = "created_at" + tags: list[str] = [] + + +@app.get("/items/") +async def read_items(filter_query: FilterParams = Query()): + return filter_query diff --git a/docs_src/query_param_models/tutorial001_py39.py b/docs_src/query_param_models/tutorial001_py39.py new file mode 100644 index 000000000..54b52a054 --- /dev/null +++ b/docs_src/query_param_models/tutorial001_py39.py @@ -0,0 +1,17 @@ +from fastapi import FastAPI, Query +from pydantic import BaseModel, Field +from typing_extensions import Literal + +app = FastAPI() + + +class FilterParams(BaseModel): + limit: int = Field(100, gt=0, le=100) + offset: int = Field(0, ge=0) + order_by: Literal["created_at", "updated_at"] = "created_at" + tags: list[str] = [] + + +@app.get("/items/") +async def read_items(filter_query: FilterParams = Query()): + return filter_query diff --git a/docs_src/query_param_models/tutorial002.py b/docs_src/query_param_models/tutorial002.py new file mode 100644 index 000000000..1633bc464 --- /dev/null +++ b/docs_src/query_param_models/tutorial002.py @@ -0,0 +1,21 @@ +from typing import List + +from fastapi import FastAPI, Query +from pydantic import BaseModel, Field +from typing_extensions import Literal + +app = FastAPI() + + +class FilterParams(BaseModel): + model_config = {"extra": "forbid"} + + limit: int = Field(100, gt=0, le=100) + offset: int = Field(0, ge=0) + order_by: Literal["created_at", "updated_at"] = "created_at" + tags: List[str] = [] + + +@app.get("/items/") +async def read_items(filter_query: FilterParams = Query()): + return filter_query diff --git a/docs_src/query_param_models/tutorial002_an.py b/docs_src/query_param_models/tutorial002_an.py new file mode 100644 index 000000000..69705d4b4 --- /dev/null +++ b/docs_src/query_param_models/tutorial002_an.py @@ -0,0 +1,21 @@ +from typing import List + +from fastapi import FastAPI, Query +from pydantic import BaseModel, Field +from typing_extensions import Annotated, Literal + +app = FastAPI() + + +class FilterParams(BaseModel): + model_config = {"extra": "forbid"} + + limit: int = Field(100, gt=0, le=100) + offset: int = Field(0, ge=0) + order_by: Literal["created_at", "updated_at"] = "created_at" + tags: List[str] = [] + + +@app.get("/items/") +async def read_items(filter_query: Annotated[FilterParams, Query()]): + return filter_query diff --git a/docs_src/query_param_models/tutorial002_an_py310.py b/docs_src/query_param_models/tutorial002_an_py310.py new file mode 100644 index 000000000..975956502 --- /dev/null +++ b/docs_src/query_param_models/tutorial002_an_py310.py @@ -0,0 +1,20 @@ +from typing import Annotated, Literal + +from fastapi import FastAPI, Query +from pydantic import BaseModel, Field + +app = FastAPI() + + +class FilterParams(BaseModel): + model_config = {"extra": "forbid"} + + limit: int = Field(100, gt=0, le=100) + offset: int = Field(0, ge=0) + order_by: Literal["created_at", "updated_at"] = "created_at" + tags: list[str] = [] + + +@app.get("/items/") +async def read_items(filter_query: Annotated[FilterParams, Query()]): + return filter_query diff --git a/docs_src/query_param_models/tutorial002_an_py39.py b/docs_src/query_param_models/tutorial002_an_py39.py new file mode 100644 index 000000000..2d4c1a62b --- /dev/null +++ b/docs_src/query_param_models/tutorial002_an_py39.py @@ -0,0 +1,19 @@ +from fastapi import FastAPI, Query +from pydantic import BaseModel, Field +from typing_extensions import Annotated, Literal + +app = FastAPI() + + +class FilterParams(BaseModel): + model_config = {"extra": "forbid"} + + limit: int = Field(100, gt=0, le=100) + offset: int = Field(0, ge=0) + order_by: Literal["created_at", "updated_at"] = "created_at" + tags: list[str] = [] + + +@app.get("/items/") +async def read_items(filter_query: Annotated[FilterParams, Query()]): + return filter_query diff --git a/docs_src/query_param_models/tutorial002_pv1.py b/docs_src/query_param_models/tutorial002_pv1.py new file mode 100644 index 000000000..71ccd961d --- /dev/null +++ b/docs_src/query_param_models/tutorial002_pv1.py @@ -0,0 +1,22 @@ +from typing import List + +from fastapi import FastAPI, Query +from pydantic import BaseModel, Field +from typing_extensions import Literal + +app = FastAPI() + + +class FilterParams(BaseModel): + class Config: + extra = "forbid" + + limit: int = Field(100, gt=0, le=100) + offset: int = Field(0, ge=0) + order_by: Literal["created_at", "updated_at"] = "created_at" + tags: List[str] = [] + + +@app.get("/items/") +async def read_items(filter_query: FilterParams = Query()): + return filter_query diff --git a/docs_src/query_param_models/tutorial002_pv1_an.py b/docs_src/query_param_models/tutorial002_pv1_an.py new file mode 100644 index 000000000..1dd29157a --- /dev/null +++ b/docs_src/query_param_models/tutorial002_pv1_an.py @@ -0,0 +1,22 @@ +from typing import List + +from fastapi import FastAPI, Query +from pydantic import BaseModel, Field +from typing_extensions import Annotated, Literal + +app = FastAPI() + + +class FilterParams(BaseModel): + class Config: + extra = "forbid" + + limit: int = Field(100, gt=0, le=100) + offset: int = Field(0, ge=0) + order_by: Literal["created_at", "updated_at"] = "created_at" + tags: List[str] = [] + + +@app.get("/items/") +async def read_items(filter_query: Annotated[FilterParams, Query()]): + return filter_query diff --git a/docs_src/query_param_models/tutorial002_pv1_an_py310.py b/docs_src/query_param_models/tutorial002_pv1_an_py310.py new file mode 100644 index 000000000..d635aae88 --- /dev/null +++ b/docs_src/query_param_models/tutorial002_pv1_an_py310.py @@ -0,0 +1,21 @@ +from typing import Annotated, Literal + +from fastapi import FastAPI, Query +from pydantic import BaseModel, Field + +app = FastAPI() + + +class FilterParams(BaseModel): + class Config: + extra = "forbid" + + limit: int = Field(100, gt=0, le=100) + offset: int = Field(0, ge=0) + order_by: Literal["created_at", "updated_at"] = "created_at" + tags: list[str] = [] + + +@app.get("/items/") +async def read_items(filter_query: Annotated[FilterParams, Query()]): + return filter_query diff --git a/docs_src/query_param_models/tutorial002_pv1_an_py39.py b/docs_src/query_param_models/tutorial002_pv1_an_py39.py new file mode 100644 index 000000000..494fef11f --- /dev/null +++ b/docs_src/query_param_models/tutorial002_pv1_an_py39.py @@ -0,0 +1,20 @@ +from fastapi import FastAPI, Query +from pydantic import BaseModel, Field +from typing_extensions import Annotated, Literal + +app = FastAPI() + + +class FilterParams(BaseModel): + class Config: + extra = "forbid" + + limit: int = Field(100, gt=0, le=100) + offset: int = Field(0, ge=0) + order_by: Literal["created_at", "updated_at"] = "created_at" + tags: list[str] = [] + + +@app.get("/items/") +async def read_items(filter_query: Annotated[FilterParams, Query()]): + return filter_query diff --git a/docs_src/query_param_models/tutorial002_pv1_py310.py b/docs_src/query_param_models/tutorial002_pv1_py310.py new file mode 100644 index 000000000..9ffdeefc0 --- /dev/null +++ b/docs_src/query_param_models/tutorial002_pv1_py310.py @@ -0,0 +1,21 @@ +from typing import Literal + +from fastapi import FastAPI, Query +from pydantic import BaseModel, Field + +app = FastAPI() + + +class FilterParams(BaseModel): + class Config: + extra = "forbid" + + limit: int = Field(100, gt=0, le=100) + offset: int = Field(0, ge=0) + order_by: Literal["created_at", "updated_at"] = "created_at" + tags: list[str] = [] + + +@app.get("/items/") +async def read_items(filter_query: FilterParams = Query()): + return filter_query diff --git a/docs_src/query_param_models/tutorial002_pv1_py39.py b/docs_src/query_param_models/tutorial002_pv1_py39.py new file mode 100644 index 000000000..7fa456a79 --- /dev/null +++ b/docs_src/query_param_models/tutorial002_pv1_py39.py @@ -0,0 +1,20 @@ +from fastapi import FastAPI, Query +from pydantic import BaseModel, Field +from typing_extensions import Literal + +app = FastAPI() + + +class FilterParams(BaseModel): + class Config: + extra = "forbid" + + limit: int = Field(100, gt=0, le=100) + offset: int = Field(0, ge=0) + order_by: Literal["created_at", "updated_at"] = "created_at" + tags: list[str] = [] + + +@app.get("/items/") +async def read_items(filter_query: FilterParams = Query()): + return filter_query diff --git a/docs_src/query_param_models/tutorial002_py310.py b/docs_src/query_param_models/tutorial002_py310.py new file mode 100644 index 000000000..6ec418499 --- /dev/null +++ b/docs_src/query_param_models/tutorial002_py310.py @@ -0,0 +1,20 @@ +from typing import Literal + +from fastapi import FastAPI, Query +from pydantic import BaseModel, Field + +app = FastAPI() + + +class FilterParams(BaseModel): + model_config = {"extra": "forbid"} + + limit: int = Field(100, gt=0, le=100) + offset: int = Field(0, ge=0) + order_by: Literal["created_at", "updated_at"] = "created_at" + tags: list[str] = [] + + +@app.get("/items/") +async def read_items(filter_query: FilterParams = Query()): + return filter_query diff --git a/docs_src/query_param_models/tutorial002_py39.py b/docs_src/query_param_models/tutorial002_py39.py new file mode 100644 index 000000000..f9bba028c --- /dev/null +++ b/docs_src/query_param_models/tutorial002_py39.py @@ -0,0 +1,19 @@ +from fastapi import FastAPI, Query +from pydantic import BaseModel, Field +from typing_extensions import Literal + +app = FastAPI() + + +class FilterParams(BaseModel): + model_config = {"extra": "forbid"} + + limit: int = Field(100, gt=0, le=100) + offset: int = Field(0, ge=0) + order_by: Literal["created_at", "updated_at"] = "created_at" + tags: list[str] = [] + + +@app.get("/items/") +async def read_items(filter_query: FilterParams = Query()): + return filter_query diff --git a/docs_src/request_form_models/tutorial001.py b/docs_src/request_form_models/tutorial001.py new file mode 100644 index 000000000..98feff0b9 --- /dev/null +++ b/docs_src/request_form_models/tutorial001.py @@ -0,0 +1,14 @@ +from fastapi import FastAPI, Form +from pydantic import BaseModel + +app = FastAPI() + + +class FormData(BaseModel): + username: str + password: str + + +@app.post("/login/") +async def login(data: FormData = Form()): + return data diff --git a/docs_src/request_form_models/tutorial001_an.py b/docs_src/request_form_models/tutorial001_an.py new file mode 100644 index 000000000..30483d445 --- /dev/null +++ b/docs_src/request_form_models/tutorial001_an.py @@ -0,0 +1,15 @@ +from fastapi import FastAPI, Form +from pydantic import BaseModel +from typing_extensions import Annotated + +app = FastAPI() + + +class FormData(BaseModel): + username: str + password: str + + +@app.post("/login/") +async def login(data: Annotated[FormData, Form()]): + return data diff --git a/docs_src/request_form_models/tutorial001_an_py39.py b/docs_src/request_form_models/tutorial001_an_py39.py new file mode 100644 index 000000000..7cc81aae9 --- /dev/null +++ b/docs_src/request_form_models/tutorial001_an_py39.py @@ -0,0 +1,16 @@ +from typing import Annotated + +from fastapi import FastAPI, Form +from pydantic import BaseModel + +app = FastAPI() + + +class FormData(BaseModel): + username: str + password: str + + +@app.post("/login/") +async def login(data: Annotated[FormData, Form()]): + return data diff --git a/docs_src/request_form_models/tutorial002.py b/docs_src/request_form_models/tutorial002.py new file mode 100644 index 000000000..59b329e8d --- /dev/null +++ b/docs_src/request_form_models/tutorial002.py @@ -0,0 +1,15 @@ +from fastapi import FastAPI, Form +from pydantic import BaseModel + +app = FastAPI() + + +class FormData(BaseModel): + username: str + password: str + model_config = {"extra": "forbid"} + + +@app.post("/login/") +async def login(data: FormData = Form()): + return data diff --git a/docs_src/request_form_models/tutorial002_an.py b/docs_src/request_form_models/tutorial002_an.py new file mode 100644 index 000000000..bcb022795 --- /dev/null +++ b/docs_src/request_form_models/tutorial002_an.py @@ -0,0 +1,16 @@ +from fastapi import FastAPI, Form +from pydantic import BaseModel +from typing_extensions import Annotated + +app = FastAPI() + + +class FormData(BaseModel): + username: str + password: str + model_config = {"extra": "forbid"} + + +@app.post("/login/") +async def login(data: Annotated[FormData, Form()]): + return data diff --git a/docs_src/request_form_models/tutorial002_an_py39.py b/docs_src/request_form_models/tutorial002_an_py39.py new file mode 100644 index 000000000..3004e0852 --- /dev/null +++ b/docs_src/request_form_models/tutorial002_an_py39.py @@ -0,0 +1,17 @@ +from typing import Annotated + +from fastapi import FastAPI, Form +from pydantic import BaseModel + +app = FastAPI() + + +class FormData(BaseModel): + username: str + password: str + model_config = {"extra": "forbid"} + + +@app.post("/login/") +async def login(data: Annotated[FormData, Form()]): + return data diff --git a/docs_src/request_form_models/tutorial002_pv1.py b/docs_src/request_form_models/tutorial002_pv1.py new file mode 100644 index 000000000..d5f7db2a6 --- /dev/null +++ b/docs_src/request_form_models/tutorial002_pv1.py @@ -0,0 +1,17 @@ +from fastapi import FastAPI, Form +from pydantic import BaseModel + +app = FastAPI() + + +class FormData(BaseModel): + username: str + password: str + + class Config: + extra = "forbid" + + +@app.post("/login/") +async def login(data: FormData = Form()): + return data diff --git a/docs_src/request_form_models/tutorial002_pv1_an.py b/docs_src/request_form_models/tutorial002_pv1_an.py new file mode 100644 index 000000000..fe9dbc344 --- /dev/null +++ b/docs_src/request_form_models/tutorial002_pv1_an.py @@ -0,0 +1,18 @@ +from fastapi import FastAPI, Form +from pydantic import BaseModel +from typing_extensions import Annotated + +app = FastAPI() + + +class FormData(BaseModel): + username: str + password: str + + class Config: + extra = "forbid" + + +@app.post("/login/") +async def login(data: Annotated[FormData, Form()]): + return data diff --git a/docs_src/request_form_models/tutorial002_pv1_an_py39.py b/docs_src/request_form_models/tutorial002_pv1_an_py39.py new file mode 100644 index 000000000..942d5d411 --- /dev/null +++ b/docs_src/request_form_models/tutorial002_pv1_an_py39.py @@ -0,0 +1,19 @@ +from typing import Annotated + +from fastapi import FastAPI, Form +from pydantic import BaseModel + +app = FastAPI() + + +class FormData(BaseModel): + username: str + password: str + + class Config: + extra = "forbid" + + +@app.post("/login/") +async def login(data: Annotated[FormData, Form()]): + return data diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 0b79d45ef..7dd74c28f 100644 --- a/fastapi/__init__.py +++ b/fastapi/__init__.py @@ -1,6 +1,6 @@ """FastAPI framework, high performance, easy to learn, fast to code, ready for production""" -__version__ = "0.112.1" +__version__ = "0.115.0" from starlette import status as status diff --git a/fastapi/_compat.py b/fastapi/_compat.py index 06b847b4f..4b07b44fa 100644 --- a/fastapi/_compat.py +++ b/fastapi/_compat.py @@ -2,6 +2,7 @@ from collections import deque from copy import copy from dataclasses import dataclass, is_dataclass from enum import Enum +from functools import lru_cache from typing import ( Any, Callable, @@ -279,6 +280,12 @@ if PYDANTIC_V2: BodyModel: Type[BaseModel] = create_model(model_name, **field_params) # type: ignore[call-overload] return BodyModel + def get_model_fields(model: Type[BaseModel]) -> List[ModelField]: + return [ + ModelField(field_info=field_info, name=name) + for name, field_info in model.model_fields.items() + ] + else: from fastapi.openapi.constants import REF_PREFIX as REF_PREFIX from pydantic import AnyUrl as Url # noqa: F401 @@ -513,6 +520,9 @@ else: BodyModel.__fields__[f.name] = f # type: ignore[index] return BodyModel + def get_model_fields(model: Type[BaseModel]) -> List[ModelField]: + return list(model.__fields__.values()) # type: ignore[attr-defined] + def _regenerate_error_with_loc( *, errors: Sequence[Any], loc_prefix: Tuple[Union[str, int], ...] @@ -532,6 +542,12 @@ def _annotation_is_sequence(annotation: Union[Type[Any], None]) -> bool: def field_annotation_is_sequence(annotation: Union[Type[Any], None]) -> bool: + origin = get_origin(annotation) + if origin is Union or origin is UnionType: + for arg in get_args(annotation): + if field_annotation_is_sequence(arg): + return True + return False return _annotation_is_sequence(annotation) or _annotation_is_sequence( get_origin(annotation) ) @@ -634,3 +650,8 @@ def is_uploadfile_sequence_annotation(annotation: Any) -> bool: is_uploadfile_or_nonable_uploadfile_annotation(sub_annotation) for sub_annotation in get_args(annotation) ) + + +@lru_cache +def get_cached_model_fields(model: Type[BaseModel]) -> List[ModelField]: + return get_model_fields(model) diff --git a/fastapi/dependencies/models.py b/fastapi/dependencies/models.py index 61ef00638..418c11725 100644 --- a/fastapi/dependencies/models.py +++ b/fastapi/dependencies/models.py @@ -1,58 +1,37 @@ -from typing import Any, Callable, List, Optional, Sequence +from dataclasses import dataclass, field +from typing import Any, Callable, List, Optional, Sequence, Tuple from fastapi._compat import ModelField from fastapi.security.base import SecurityBase +@dataclass class SecurityRequirement: - def __init__( - self, security_scheme: SecurityBase, scopes: Optional[Sequence[str]] = None - ): - self.security_scheme = security_scheme - self.scopes = scopes + security_scheme: SecurityBase + scopes: Optional[Sequence[str]] = None +@dataclass class Dependant: - def __init__( - self, - *, - path_params: Optional[List[ModelField]] = None, - query_params: Optional[List[ModelField]] = None, - header_params: Optional[List[ModelField]] = None, - cookie_params: Optional[List[ModelField]] = None, - body_params: Optional[List[ModelField]] = None, - dependencies: Optional[List["Dependant"]] = None, - security_schemes: Optional[List[SecurityRequirement]] = None, - name: Optional[str] = None, - call: Optional[Callable[..., Any]] = None, - request_param_name: Optional[str] = None, - websocket_param_name: Optional[str] = None, - http_connection_param_name: Optional[str] = None, - response_param_name: Optional[str] = None, - background_tasks_param_name: Optional[str] = None, - security_scopes_param_name: Optional[str] = None, - security_scopes: Optional[List[str]] = None, - use_cache: bool = True, - path: Optional[str] = None, - ) -> None: - self.path_params = path_params or [] - self.query_params = query_params or [] - self.header_params = header_params or [] - self.cookie_params = cookie_params or [] - self.body_params = body_params or [] - self.dependencies = dependencies or [] - self.security_requirements = security_schemes or [] - self.request_param_name = request_param_name - self.websocket_param_name = websocket_param_name - self.http_connection_param_name = http_connection_param_name - self.response_param_name = response_param_name - self.background_tasks_param_name = background_tasks_param_name - self.security_scopes = security_scopes - self.security_scopes_param_name = security_scopes_param_name - self.name = name - self.call = call - self.use_cache = use_cache - # Store the path to be able to re-generate a dependable from it in overrides - self.path = path - # Save the cache key at creation to optimize performance + path_params: List[ModelField] = field(default_factory=list) + query_params: List[ModelField] = field(default_factory=list) + header_params: List[ModelField] = field(default_factory=list) + cookie_params: List[ModelField] = field(default_factory=list) + body_params: List[ModelField] = field(default_factory=list) + dependencies: List["Dependant"] = field(default_factory=list) + security_requirements: List[SecurityRequirement] = field(default_factory=list) + name: Optional[str] = None + call: Optional[Callable[..., Any]] = None + request_param_name: Optional[str] = None + websocket_param_name: Optional[str] = None + http_connection_param_name: Optional[str] = None + response_param_name: Optional[str] = None + background_tasks_param_name: Optional[str] = None + security_scopes_param_name: Optional[str] = None + security_scopes: Optional[List[str]] = None + use_cache: bool = True + path: Optional[str] = None + cache_key: Tuple[Optional[Callable[..., Any]], Tuple[str, ...]] = field(init=False) + + def __post_init__(self) -> None: self.cache_key = (self.call, tuple(sorted(set(self.security_scopes or [])))) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 3e8e7b410..5cebbf00f 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -1,6 +1,7 @@ import inspect from contextlib import AsyncExitStack, contextmanager from copy import copy, deepcopy +from dataclasses import dataclass from typing import ( Any, Callable, @@ -31,6 +32,7 @@ from fastapi._compat import ( evaluate_forwardref, field_annotation_is_scalar, get_annotation_from_field_info, + get_cached_model_fields, get_missing_field_error, is_bytes_field, is_bytes_sequence_field, @@ -54,11 +56,18 @@ from fastapi.logger import logger from fastapi.security.base import SecurityBase from fastapi.security.oauth2 import OAuth2, SecurityScopes from fastapi.security.open_id_connect_url import OpenIdConnect -from fastapi.utils import create_response_field, get_path_param_names +from fastapi.utils import create_model_field, get_path_param_names +from pydantic import BaseModel from pydantic.fields import FieldInfo from starlette.background import BackgroundTasks as StarletteBackgroundTasks from starlette.concurrency import run_in_threadpool -from starlette.datastructures import FormData, Headers, QueryParams, UploadFile +from starlette.datastructures import ( + FormData, + Headers, + ImmutableMultiDict, + QueryParams, + UploadFile, +) from starlette.requests import HTTPConnection, Request from starlette.responses import Response from starlette.websockets import WebSocket @@ -79,25 +88,23 @@ multipart_incorrect_install_error = ( ) -def check_file_field(field: ModelField) -> None: - field_info = field.field_info - if isinstance(field_info, params.Form): - try: - # __version__ is available in both multiparts, and can be mocked - from multipart import __version__ # type: ignore +def ensure_multipart_is_installed() -> None: + try: + # __version__ is available in both multiparts, and can be mocked + from multipart import __version__ # type: ignore - assert __version__ - try: - # parse_options_header is only available in the right multipart - from multipart.multipart import parse_options_header # type: ignore + assert __version__ + try: + # parse_options_header is only available in the right multipart + from multipart.multipart import parse_options_header # type: ignore - assert parse_options_header - except ImportError: - logger.error(multipart_incorrect_install_error) - raise RuntimeError(multipart_incorrect_install_error) from None + assert parse_options_header except ImportError: - logger.error(multipart_not_installed_error) - raise RuntimeError(multipart_not_installed_error) from None + logger.error(multipart_incorrect_install_error) + raise RuntimeError(multipart_incorrect_install_error) from None + except ImportError: + logger.error(multipart_not_installed_error) + raise RuntimeError(multipart_not_installed_error) from None def get_param_sub_dependant( @@ -175,7 +182,7 @@ def get_flat_dependant( header_params=dependant.header_params.copy(), cookie_params=dependant.cookie_params.copy(), body_params=dependant.body_params.copy(), - security_schemes=dependant.security_requirements.copy(), + security_requirements=dependant.security_requirements.copy(), use_cache=dependant.use_cache, path=dependant.path, ) @@ -194,14 +201,23 @@ def get_flat_dependant( return flat_dependant +def _get_flat_fields_from_params(fields: List[ModelField]) -> List[ModelField]: + if not fields: + return fields + first_field = fields[0] + if len(fields) == 1 and lenient_issubclass(first_field.type_, BaseModel): + fields_to_extract = get_cached_model_fields(first_field.type_) + return fields_to_extract + return fields + + def get_flat_params(dependant: Dependant) -> List[ModelField]: flat_dependant = get_flat_dependant(dependant, skip_repeats=True) - return ( - flat_dependant.path_params - + flat_dependant.query_params - + flat_dependant.header_params - + flat_dependant.cookie_params - ) + path_params = _get_flat_fields_from_params(flat_dependant.path_params) + query_params = _get_flat_fields_from_params(flat_dependant.query_params) + header_params = _get_flat_fields_from_params(flat_dependant.header_params) + cookie_params = _get_flat_fields_from_params(flat_dependant.cookie_params) + return path_params + query_params + header_params + cookie_params def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature: @@ -258,16 +274,16 @@ def get_dependant( ) for param_name, param in signature_params.items(): is_path_param = param_name in path_param_names - type_annotation, depends, param_field = analyze_param( + param_details = analyze_param( param_name=param_name, annotation=param.annotation, value=param.default, is_path_param=is_path_param, ) - if depends is not None: + if param_details.depends is not None: sub_dependant = get_param_sub_dependant( param_name=param_name, - depends=depends, + depends=param_details.depends, path=path, security_scopes=security_scopes, ) @@ -275,18 +291,18 @@ def get_dependant( continue if add_non_field_param_to_dependency( param_name=param_name, - type_annotation=type_annotation, + type_annotation=param_details.type_annotation, dependant=dependant, ): assert ( - param_field is None + param_details.field is None ), f"Cannot specify multiple FastAPI annotations for {param_name!r}" continue - assert param_field is not None - if is_body_param(param_field=param_field, is_path_param=is_path_param): - dependant.body_params.append(param_field) + assert param_details.field is not None + if isinstance(param_details.field.field_info, params.Body): + dependant.body_params.append(param_details.field) else: - add_param_to_fields(field=param_field, dependant=dependant) + add_param_to_fields(field=param_details.field, dependant=dependant) return dependant @@ -314,13 +330,20 @@ def add_non_field_param_to_dependency( return None +@dataclass +class ParamDetails: + type_annotation: Any + depends: Optional[params.Depends] + field: Optional[ModelField] + + def analyze_param( *, param_name: str, annotation: Any, value: Any, is_path_param: bool, -) -> Tuple[Any, Optional[params.Depends], Optional[ModelField]]: +) -> ParamDetails: field_info = None depends = None type_annotation: Any = Any @@ -328,6 +351,7 @@ def analyze_param( if annotation is not inspect.Signature.empty: use_annotation = annotation type_annotation = annotation + # Extract Annotated info if get_origin(use_annotation) is Annotated: annotated_args = get_args(annotation) type_annotation = annotated_args[0] @@ -347,6 +371,7 @@ def analyze_param( ) else: fastapi_annotation = None + # Set default for Annotated FieldInfo if isinstance(fastapi_annotation, FieldInfo): # Copy `field_info` because we mutate `field_info.default` below. field_info = copy_field_info( @@ -361,9 +386,10 @@ def analyze_param( field_info.default = value else: field_info.default = Required + # Get Annotated Depends elif isinstance(fastapi_annotation, params.Depends): depends = fastapi_annotation - + # Get Depends from default value if isinstance(value, params.Depends): assert depends is None, ( "Cannot specify `Depends` in `Annotated` and default value" @@ -374,6 +400,7 @@ def analyze_param( f" default value together for {param_name!r}" ) depends = value + # Get FieldInfo from default value elif isinstance(value, FieldInfo): assert field_info is None, ( "Cannot specify FastAPI annotations in `Annotated` and default value" @@ -383,11 +410,13 @@ def analyze_param( if PYDANTIC_V2: field_info.annotation = type_annotation + # Get Depends from type annotation if depends is not None and depends.dependency is None: # Copy `depends` before mutating it depends = copy(depends) depends.dependency = type_annotation + # Handle non-param type annotations like Request if lenient_issubclass( type_annotation, ( @@ -403,6 +432,7 @@ def analyze_param( assert ( field_info is None ), f"Cannot specify FastAPI annotation for type {type_annotation!r}" + # Handle default assignations, neither field_info nor depends was not found in Annotated nor default value elif field_info is None and depends is None: default_value = value if value is not inspect.Signature.empty else Required if is_path_param: @@ -420,7 +450,9 @@ def analyze_param( field_info = params.Query(annotation=use_annotation, default=default_value) field = None + # It's a field_info, not a dependency if field_info is not None: + # Handle field_info.in_ if is_path_param: assert isinstance(field_info, params.Path), ( f"Cannot use `{field_info.__class__.__name__}` for path param" @@ -436,12 +468,14 @@ def analyze_param( field_info, param_name, ) + if isinstance(field_info, params.Form): + ensure_multipart_is_installed() if not field_info.alias and getattr(field_info, "convert_underscores", None): alias = param_name.replace("_", "-") else: alias = field_info.alias or param_name field_info.alias = alias - field = create_response_field( + field = create_model_field( name=param_name, type_=use_annotation_from_field_info, default=field_info.default, @@ -449,27 +483,22 @@ def analyze_param( required=field_info.default in (Required, Undefined), field_info=field_info, ) + if is_path_param: + assert is_scalar_field( + field=field + ), "Path params must be of one of the supported types" + elif isinstance(field_info, params.Query): + assert ( + is_scalar_field(field) + or is_scalar_sequence_field(field) + or ( + lenient_issubclass(field.type_, BaseModel) + # For Pydantic v1 + and getattr(field, "shape", 1) == 1 + ) + ) - return type_annotation, depends, field - - -def is_body_param(*, param_field: ModelField, is_path_param: bool) -> bool: - if is_path_param: - assert is_scalar_field( - field=param_field - ), "Path params must be of one of the supported types" - return False - elif is_scalar_field(field=param_field): - return False - elif isinstance( - param_field.field_info, (params.Query, params.Header) - ) and is_scalar_sequence_field(param_field): - return False - else: - assert isinstance( - param_field.field_info, params.Body - ), f"Param: {param_field.name} can only be a request body, using Body()" - return True + return ParamDetails(type_annotation=type_annotation, depends=depends, field=field) def add_param_to_fields(*, field: ModelField, dependant: Dependant) -> None: @@ -521,6 +550,15 @@ async def solve_generator( return await stack.enter_async_context(cm) +@dataclass +class SolvedDependency: + values: Dict[str, Any] + errors: List[Any] + background_tasks: Optional[StarletteBackgroundTasks] + response: Response + dependency_cache: Dict[Tuple[Callable[..., Any], Tuple[str]], Any] + + async def solve_dependencies( *, request: Union[Request, WebSocket], @@ -531,13 +569,8 @@ async def solve_dependencies( dependency_overrides_provider: Optional[Any] = None, dependency_cache: Optional[Dict[Tuple[Callable[..., Any], Tuple[str]], Any]] = None, async_exit_stack: AsyncExitStack, -) -> Tuple[ - Dict[str, Any], - List[Any], - Optional[StarletteBackgroundTasks], - Response, - Dict[Tuple[Callable[..., Any], Tuple[str]], Any], -]: + embed_body_fields: bool, +) -> SolvedDependency: values: Dict[str, Any] = {} errors: List[Any] = [] if response is None: @@ -578,28 +611,23 @@ async def solve_dependencies( dependency_overrides_provider=dependency_overrides_provider, dependency_cache=dependency_cache, async_exit_stack=async_exit_stack, + embed_body_fields=embed_body_fields, ) - ( - sub_values, - sub_errors, - background_tasks, - _, # the subdependency returns the same response we have - sub_dependency_cache, - ) = solved_result - dependency_cache.update(sub_dependency_cache) - if sub_errors: - errors.extend(sub_errors) + background_tasks = solved_result.background_tasks + dependency_cache.update(solved_result.dependency_cache) + if solved_result.errors: + errors.extend(solved_result.errors) continue if sub_dependant.use_cache and sub_dependant.cache_key in dependency_cache: solved = dependency_cache[sub_dependant.cache_key] elif is_gen_callable(call) or is_async_gen_callable(call): solved = await solve_generator( - call=call, stack=async_exit_stack, sub_values=sub_values + call=call, stack=async_exit_stack, sub_values=solved_result.values ) elif is_coroutine_callable(call): - solved = await call(**sub_values) + solved = await call(**solved_result.values) else: - solved = await run_in_threadpool(call, **sub_values) + solved = await run_in_threadpool(call, **solved_result.values) if sub_dependant.name is not None: values[sub_dependant.name] = solved if sub_dependant.cache_key not in dependency_cache: @@ -626,7 +654,9 @@ async def solve_dependencies( body_values, body_errors, ) = await request_body_to_args( # body_params checked above - required_params=dependant.body_params, received_body=body + body_fields=dependant.body_params, + received_body=body, + embed_body_fields=embed_body_fields, ) values.update(body_values) errors.extend(body_errors) @@ -646,142 +676,257 @@ async def solve_dependencies( values[dependant.security_scopes_param_name] = SecurityScopes( scopes=dependant.security_scopes ) - return values, errors, background_tasks, response, dependency_cache + return SolvedDependency( + values=values, + errors=errors, + background_tasks=background_tasks, + response=response, + dependency_cache=dependency_cache, + ) + + +def _validate_value_with_model_field( + *, field: ModelField, value: Any, values: Dict[str, Any], loc: Tuple[str, ...] +) -> Tuple[Any, List[Any]]: + if value is None: + if field.required: + return None, [get_missing_field_error(loc=loc)] + else: + return deepcopy(field.default), [] + v_, errors_ = field.validate(value, values, loc=loc) + if isinstance(errors_, ErrorWrapper): + return None, [errors_] + elif isinstance(errors_, list): + new_errors = _regenerate_error_with_loc(errors=errors_, loc_prefix=()) + return None, new_errors + else: + return v_, [] + + +def _get_multidict_value( + field: ModelField, values: Mapping[str, Any], alias: Union[str, None] = None +) -> Any: + alias = alias or field.alias + if is_sequence_field(field) and isinstance(values, (ImmutableMultiDict, Headers)): + value = values.getlist(alias) + else: + value = values.get(alias, None) + if ( + value is None + or ( + isinstance(field.field_info, params.Form) + and isinstance(value, str) # For type checks + and value == "" + ) + or (is_sequence_field(field) and len(value) == 0) + ): + if field.required: + return + else: + return deepcopy(field.default) + return value def request_params_to_args( - required_params: Sequence[ModelField], + fields: Sequence[ModelField], received_params: Union[Mapping[str, Any], QueryParams, Headers], ) -> Tuple[Dict[str, Any], List[Any]]: - values = {} - errors = [] - for field in required_params: - if is_scalar_sequence_field(field) and isinstance( - received_params, (QueryParams, Headers) - ): - value = received_params.getlist(field.alias) or field.default - else: - value = received_params.get(field.alias) + values: Dict[str, Any] = {} + errors: List[Dict[str, Any]] = [] + + if not fields: + return values, errors + + first_field = fields[0] + fields_to_extract = fields + single_not_embedded_field = False + if len(fields) == 1 and lenient_issubclass(first_field.type_, BaseModel): + fields_to_extract = get_cached_model_fields(first_field.type_) + single_not_embedded_field = True + + params_to_process: Dict[str, Any] = {} + + processed_keys = set() + + for field in fields_to_extract: + alias = None + if isinstance(received_params, Headers): + # Handle fields extracted from a Pydantic Model for a header, each field + # doesn't have a FieldInfo of type Header with the default convert_underscores=True + convert_underscores = getattr(field.field_info, "convert_underscores", True) + if convert_underscores: + alias = ( + field.alias + if field.alias != field.name + else field.name.replace("_", "-") + ) + value = _get_multidict_value(field, received_params, alias=alias) + if value is not None: + params_to_process[field.name] = value + processed_keys.add(alias or field.alias) + processed_keys.add(field.name) + + for key, value in received_params.items(): + if key not in processed_keys: + params_to_process[key] = value + + if single_not_embedded_field: + field_info = first_field.field_info + assert isinstance( + field_info, params.Param + ), "Params must be subclasses of Param" + loc: Tuple[str, ...] = (field_info.in_.value,) + v_, errors_ = _validate_value_with_model_field( + field=first_field, value=params_to_process, values=values, loc=loc + ) + return {first_field.name: v_}, errors_ + + for field in fields: + value = _get_multidict_value(field, received_params) field_info = field.field_info assert isinstance( field_info, params.Param ), "Params must be subclasses of Param" loc = (field_info.in_.value, field.alias) - if value is None: - if field.required: - errors.append(get_missing_field_error(loc=loc)) - else: - values[field.name] = deepcopy(field.default) - continue - v_, errors_ = field.validate(value, values, loc=loc) - if isinstance(errors_, ErrorWrapper): - errors.append(errors_) - elif isinstance(errors_, list): - new_errors = _regenerate_error_with_loc(errors=errors_, loc_prefix=()) - errors.extend(new_errors) + v_, errors_ = _validate_value_with_model_field( + field=field, value=value, values=values, loc=loc + ) + if errors_: + errors.extend(errors_) else: values[field.name] = v_ return values, errors +def _should_embed_body_fields(fields: List[ModelField]) -> bool: + if not fields: + return False + # More than one dependency could have the same field, it would show up as multiple + # fields but it's the same one, so count them by name + body_param_names_set = {field.name for field in fields} + # A top level field has to be a single field, not multiple + if len(body_param_names_set) > 1: + return True + first_field = fields[0] + # If it explicitly specifies it is embedded, it has to be embedded + if getattr(first_field.field_info, "embed", None): + return True + # If it's a Form (or File) field, it has to be a BaseModel to be top level + # otherwise it has to be embedded, so that the key value pair can be extracted + if isinstance(first_field.field_info, params.Form) and not lenient_issubclass( + first_field.type_, BaseModel + ): + return True + return False + + +async def _extract_form_body( + body_fields: List[ModelField], + received_body: FormData, +) -> Dict[str, Any]: + values = {} + first_field = body_fields[0] + first_field_info = first_field.field_info + + for field in body_fields: + value = _get_multidict_value(field, received_body) + if ( + isinstance(first_field_info, params.File) + and is_bytes_field(field) + and isinstance(value, UploadFile) + ): + value = await value.read() + elif ( + is_bytes_sequence_field(field) + and isinstance(first_field_info, params.File) + and value_is_sequence(value) + ): + # For types + assert isinstance(value, sequence_types) # type: ignore[arg-type] + results: List[Union[bytes, str]] = [] + + async def process_fn( + fn: Callable[[], Coroutine[Any, Any, Any]], + ) -> None: + result = await fn() + results.append(result) # noqa: B023 + + async with anyio.create_task_group() as tg: + for sub_value in value: + tg.start_soon(process_fn, sub_value.read) + value = serialize_sequence_value(field=field, value=results) + if value is not None: + values[field.alias] = value + for key, value in received_body.items(): + if key not in values: + values[key] = value + return values + + async def request_body_to_args( - required_params: List[ModelField], + body_fields: List[ModelField], received_body: Optional[Union[Dict[str, Any], FormData]], + embed_body_fields: bool, ) -> Tuple[Dict[str, Any], List[Dict[str, Any]]]: - values = {} + values: Dict[str, Any] = {} errors: List[Dict[str, Any]] = [] - if required_params: - field = required_params[0] - field_info = field.field_info - embed = getattr(field_info, "embed", None) - field_alias_omitted = len(required_params) == 1 and not embed - if field_alias_omitted: - received_body = {field.alias: received_body} - - for field in required_params: - loc: Tuple[str, ...] - if field_alias_omitted: - loc = ("body",) - else: - loc = ("body", field.alias) - - value: Optional[Any] = None - if received_body is not None: - if (is_sequence_field(field)) and isinstance(received_body, FormData): - value = received_body.getlist(field.alias) - else: - try: - value = received_body.get(field.alias) - except AttributeError: - errors.append(get_missing_field_error(loc)) - continue - if ( - value is None - or (isinstance(field_info, params.Form) and value == "") - or ( - isinstance(field_info, params.Form) - and is_sequence_field(field) - and len(value) == 0 - ) - ): - if field.required: - errors.append(get_missing_field_error(loc)) - else: - values[field.name] = deepcopy(field.default) + assert body_fields, "request_body_to_args() should be called with fields" + single_not_embedded_field = len(body_fields) == 1 and not embed_body_fields + first_field = body_fields[0] + body_to_process = received_body + + fields_to_extract: List[ModelField] = body_fields + + if single_not_embedded_field and lenient_issubclass(first_field.type_, BaseModel): + fields_to_extract = get_cached_model_fields(first_field.type_) + + if isinstance(received_body, FormData): + body_to_process = await _extract_form_body(fields_to_extract, received_body) + + if single_not_embedded_field: + loc: Tuple[str, ...] = ("body",) + v_, errors_ = _validate_value_with_model_field( + field=first_field, value=body_to_process, values=values, loc=loc + ) + return {first_field.name: v_}, errors_ + for field in body_fields: + loc = ("body", field.alias) + value: Optional[Any] = None + if body_to_process is not None: + try: + value = body_to_process.get(field.alias) + # If the received body is a list, not a dict + except AttributeError: + errors.append(get_missing_field_error(loc)) continue - if ( - isinstance(field_info, params.File) - and is_bytes_field(field) - and isinstance(value, UploadFile) - ): - value = await value.read() - elif ( - is_bytes_sequence_field(field) - and isinstance(field_info, params.File) - and value_is_sequence(value) - ): - # For types - assert isinstance(value, sequence_types) # type: ignore[arg-type] - results: List[Union[bytes, str]] = [] - - async def process_fn( - fn: Callable[[], Coroutine[Any, Any, Any]], - ) -> None: - result = await fn() - results.append(result) # noqa: B023 - - async with anyio.create_task_group() as tg: - for sub_value in value: - tg.start_soon(process_fn, sub_value.read) - value = serialize_sequence_value(field=field, value=results) - - v_, errors_ = field.validate(value, values, loc=loc) - - if isinstance(errors_, list): - errors.extend(errors_) - elif errors_: - errors.append(errors_) - else: - values[field.name] = v_ + v_, errors_ = _validate_value_with_model_field( + field=field, value=value, values=values, loc=loc + ) + if errors_: + errors.extend(errors_) + else: + values[field.name] = v_ return values, errors -def get_body_field(*, dependant: Dependant, name: str) -> Optional[ModelField]: - flat_dependant = get_flat_dependant(dependant) +def get_body_field( + *, flat_dependant: Dependant, name: str, embed_body_fields: bool +) -> Optional[ModelField]: + """ + Get a ModelField representing the request body for a path operation, combining + all body parameters into a single field if necessary. + + Used to check if it's form data (with `isinstance(body_field, params.Form)`) + or JSON and to generate the JSON Schema for a request body. + + This is **not** used to validate/parse the request body, that's done with each + individual body parameter. + """ if not flat_dependant.body_params: return None first_param = flat_dependant.body_params[0] - field_info = first_param.field_info - embed = getattr(field_info, "embed", None) - body_param_names_set = {param.name for param in flat_dependant.body_params} - if len(body_param_names_set) == 1 and not embed: - check_file_field(first_param) + if not embed_body_fields: return first_param - # If one field requires to embed, all have to be embedded - # in case a sub-dependency is evaluated with a single unique body field - # That is combined (embedded) with other body fields - for param in flat_dependant.body_params: - setattr(param.field_info, "embed", True) # noqa: B010 model_name = "Body_" + name BodyModel = create_body_model( fields=flat_dependant.body_params, model_name=model_name @@ -807,12 +952,11 @@ def get_body_field(*, dependant: Dependant, name: str) -> Optional[ModelField]: ] if len(set(body_param_media_types)) == 1: BodyFieldInfo_kwargs["media_type"] = body_param_media_types[0] - final_field = create_response_field( + final_field = create_model_field( name="body", type_=BodyModel, required=required, alias="body", field_info=BodyFieldInfo(**BodyFieldInfo_kwargs), ) - check_file_field(final_field) return final_field diff --git a/fastapi/openapi/utils.py b/fastapi/openapi/utils.py index 79ad9f83f..947eca948 100644 --- a/fastapi/openapi/utils.py +++ b/fastapi/openapi/utils.py @@ -16,11 +16,15 @@ from fastapi._compat import ( ) from fastapi.datastructures import DefaultPlaceholder from fastapi.dependencies.models import Dependant -from fastapi.dependencies.utils import get_flat_dependant, get_flat_params +from fastapi.dependencies.utils import ( + _get_flat_fields_from_params, + get_flat_dependant, + get_flat_params, +) from fastapi.encoders import jsonable_encoder from fastapi.openapi.constants import METHODS_WITH_BODY, REF_PREFIX, REF_TEMPLATE from fastapi.openapi.models import OpenAPI -from fastapi.params import Body, Param +from fastapi.params import Body, ParamTypes from fastapi.responses import Response from fastapi.types import ModelNameMap from fastapi.utils import ( @@ -87,9 +91,9 @@ def get_openapi_security_definitions( return security_definitions, operation_security -def get_openapi_operation_parameters( +def _get_openapi_operation_parameters( *, - all_route_params: Sequence[ModelField], + dependant: Dependant, schema_generator: GenerateJsonSchema, model_name_map: ModelNameMap, field_mapping: Dict[ @@ -98,33 +102,47 @@ def get_openapi_operation_parameters( separate_input_output_schemas: bool = True, ) -> List[Dict[str, Any]]: parameters = [] - for param in all_route_params: - field_info = param.field_info - field_info = cast(Param, field_info) - if not field_info.include_in_schema: - continue - param_schema = get_schema_from_model_field( - field=param, - schema_generator=schema_generator, - model_name_map=model_name_map, - field_mapping=field_mapping, - separate_input_output_schemas=separate_input_output_schemas, - ) - parameter = { - "name": param.alias, - "in": field_info.in_.value, - "required": param.required, - "schema": param_schema, - } - if field_info.description: - parameter["description"] = field_info.description - if field_info.openapi_examples: - parameter["examples"] = jsonable_encoder(field_info.openapi_examples) - elif field_info.example != Undefined: - parameter["example"] = jsonable_encoder(field_info.example) - if field_info.deprecated: - parameter["deprecated"] = True - parameters.append(parameter) + flat_dependant = get_flat_dependant(dependant, skip_repeats=True) + path_params = _get_flat_fields_from_params(flat_dependant.path_params) + query_params = _get_flat_fields_from_params(flat_dependant.query_params) + header_params = _get_flat_fields_from_params(flat_dependant.header_params) + cookie_params = _get_flat_fields_from_params(flat_dependant.cookie_params) + parameter_groups = [ + (ParamTypes.path, path_params), + (ParamTypes.query, query_params), + (ParamTypes.header, header_params), + (ParamTypes.cookie, cookie_params), + ] + for param_type, param_group in parameter_groups: + for param in param_group: + field_info = param.field_info + # field_info = cast(Param, field_info) + if not getattr(field_info, "include_in_schema", True): + continue + param_schema = get_schema_from_model_field( + field=param, + schema_generator=schema_generator, + model_name_map=model_name_map, + field_mapping=field_mapping, + separate_input_output_schemas=separate_input_output_schemas, + ) + parameter = { + "name": param.alias, + "in": param_type.value, + "required": param.required, + "schema": param_schema, + } + if field_info.description: + parameter["description"] = field_info.description + openapi_examples = getattr(field_info, "openapi_examples", None) + example = getattr(field_info, "example", None) + if openapi_examples: + parameter["examples"] = jsonable_encoder(openapi_examples) + elif example != Undefined: + parameter["example"] = jsonable_encoder(example) + if getattr(field_info, "deprecated", None): + parameter["deprecated"] = True + parameters.append(parameter) return parameters @@ -247,9 +265,8 @@ def get_openapi_path( operation.setdefault("security", []).extend(operation_security) if security_definitions: security_schemes.update(security_definitions) - all_route_params = get_flat_params(route.dependant) - operation_parameters = get_openapi_operation_parameters( - all_route_params=all_route_params, + operation_parameters = _get_openapi_operation_parameters( + dependant=route.dependant, schema_generator=schema_generator, model_name_map=model_name_map, field_mapping=field_mapping, @@ -379,6 +396,7 @@ def get_openapi_path( deep_dict_update(openapi_response, process_response) openapi_response["description"] = description http422 = str(HTTP_422_UNPROCESSABLE_ENTITY) + all_route_params = get_flat_params(route.dependant) if (all_route_params or route.body_field) and not any( status in operation["responses"] for status in [http422, "4XX", "default"] diff --git a/fastapi/param_functions.py b/fastapi/param_functions.py index 0d5f27af4..7ddaace25 100644 --- a/fastapi/param_functions.py +++ b/fastapi/param_functions.py @@ -1282,7 +1282,7 @@ def Body( # noqa: N802 ), ] = _Unset, embed: Annotated[ - bool, + Union[bool, None], Doc( """ When `embed` is `True`, the parameter will be expected in a JSON body as a @@ -1294,7 +1294,7 @@ def Body( # noqa: N802 [FastAPI docs for Body - Multiple Parameters](https://fastapi.tiangolo.com/tutorial/body-multiple-params/#embed-a-single-body-parameter). """ ), - ] = False, + ] = None, media_type: Annotated[ str, Doc( diff --git a/fastapi/params.py b/fastapi/params.py index 860146531..90ca7cb01 100644 --- a/fastapi/params.py +++ b/fastapi/params.py @@ -91,7 +91,7 @@ class Param(FieldInfo): max_length=max_length, discriminator=discriminator, multiple_of=multiple_of, - allow_nan=allow_inf_nan, + allow_inf_nan=allow_inf_nan, max_digits=max_digits, decimal_places=decimal_places, **extra, @@ -479,7 +479,7 @@ class Body(FieldInfo): *, default_factory: Union[Callable[[], Any], None] = _Unset, annotation: Optional[Any] = None, - embed: bool = False, + embed: Union[bool, None] = None, media_type: str = "application/json", alias: Optional[str] = None, alias_priority: Union[int, None] = _Unset, @@ -547,7 +547,7 @@ class Body(FieldInfo): max_length=max_length, discriminator=discriminator, multiple_of=multiple_of, - allow_nan=allow_inf_nan, + allow_inf_nan=allow_inf_nan, max_digits=max_digits, decimal_places=decimal_places, **extra, @@ -556,7 +556,7 @@ class Body(FieldInfo): kwargs["examples"] = examples if regex is not None: warnings.warn( - "`regex` has been depreacated, please use `pattern` instead", + "`regex` has been deprecated, please use `pattern` instead", category=DeprecationWarning, stacklevel=4, ) @@ -642,7 +642,6 @@ class Form(Body): default=default, default_factory=default_factory, annotation=annotation, - embed=True, media_type=media_type, alias=alias, alias_priority=alias_priority, diff --git a/fastapi/routing.py b/fastapi/routing.py index 2e7959f3d..86e303602 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -3,14 +3,16 @@ import dataclasses import email.message import inspect import json -from contextlib import AsyncExitStack +from contextlib import AsyncExitStack, asynccontextmanager from enum import Enum, IntEnum from typing import ( Any, + AsyncIterator, Callable, Coroutine, Dict, List, + Mapping, Optional, Sequence, Set, @@ -31,8 +33,10 @@ from fastapi._compat import ( from fastapi.datastructures import Default, DefaultPlaceholder from fastapi.dependencies.models import Dependant from fastapi.dependencies.utils import ( + _should_embed_body_fields, get_body_field, get_dependant, + get_flat_dependant, get_parameterless_sub_dependant, get_typed_return_annotation, solve_dependencies, @@ -47,7 +51,7 @@ from fastapi.exceptions import ( from fastapi.types import DecoratedCallable, IncEx from fastapi.utils import ( create_cloned_field, - create_response_field, + create_model_field, generate_unique_id, get_value_or_default, is_body_allowed_for_status_code, @@ -67,7 +71,7 @@ from starlette.routing import ( websocket_session, ) from starlette.routing import Mount as Mount # noqa -from starlette.types import ASGIApp, Lifespan, Scope +from starlette.types import AppType, ASGIApp, Lifespan, Scope from starlette.websockets import WebSocket from typing_extensions import Annotated, Doc, deprecated @@ -119,6 +123,23 @@ def _prepare_response_content( return res +def _merge_lifespan_context( + original_context: Lifespan[Any], nested_context: Lifespan[Any] +) -> Lifespan[Any]: + @asynccontextmanager + async def merged_lifespan( + app: AppType, + ) -> AsyncIterator[Optional[Mapping[str, Any]]]: + async with original_context(app) as maybe_original_state: + async with nested_context(app) as maybe_nested_state: + if maybe_nested_state is None and maybe_original_state is None: + yield None # old ASGI compatibility + else: + yield {**(maybe_nested_state or {}), **(maybe_original_state or {})} + + return merged_lifespan # type: ignore[return-value] + + async def serialize_response( *, field: Optional[ModelField] = None, @@ -206,6 +227,7 @@ def get_request_handler( response_model_exclude_defaults: bool = False, response_model_exclude_none: bool = False, dependency_overrides_provider: Optional[Any] = None, + embed_body_fields: bool = False, ) -> Callable[[Request], Coroutine[Any, Any, Response]]: assert dependant.call is not None, "dependant.call must be a function" is_coroutine = asyncio.iscoroutinefunction(dependant.call) @@ -272,27 +294,36 @@ def get_request_handler( body=body, dependency_overrides_provider=dependency_overrides_provider, async_exit_stack=async_exit_stack, + embed_body_fields=embed_body_fields, ) - values, errors, background_tasks, sub_response, _ = solved_result + errors = solved_result.errors if not errors: raw_response = await run_endpoint_function( - dependant=dependant, values=values, is_coroutine=is_coroutine + dependant=dependant, + values=solved_result.values, + is_coroutine=is_coroutine, ) if isinstance(raw_response, Response): if raw_response.background is None: - raw_response.background = background_tasks + raw_response.background = solved_result.background_tasks response = raw_response else: - response_args: Dict[str, Any] = {"background": background_tasks} + response_args: Dict[str, Any] = { + "background": solved_result.background_tasks + } # If status_code was set, use it, otherwise use the default from the # response class, in the case of redirect it's 307 current_status_code = ( - status_code if status_code else sub_response.status_code + status_code + if status_code + else solved_result.response.status_code ) if current_status_code is not None: response_args["status_code"] = current_status_code - if sub_response.status_code: - response_args["status_code"] = sub_response.status_code + if solved_result.response.status_code: + response_args["status_code"] = ( + solved_result.response.status_code + ) content = await serialize_response( field=response_field, response_content=raw_response, @@ -307,7 +338,7 @@ def get_request_handler( response = actual_response_class(content, **response_args) if not is_body_allowed_for_status_code(response.status_code): response.body = b"" - response.headers.raw.extend(sub_response.headers.raw) + response.headers.raw.extend(solved_result.response.headers.raw) if errors: validation_error = RequestValidationError( _normalize_errors(errors), body=body @@ -327,7 +358,9 @@ def get_request_handler( def get_websocket_app( - dependant: Dependant, dependency_overrides_provider: Optional[Any] = None + dependant: Dependant, + dependency_overrides_provider: Optional[Any] = None, + embed_body_fields: bool = False, ) -> Callable[[WebSocket], Coroutine[Any, Any, Any]]: async def app(websocket: WebSocket) -> None: async with AsyncExitStack() as async_exit_stack: @@ -340,12 +373,14 @@ def get_websocket_app( dependant=dependant, dependency_overrides_provider=dependency_overrides_provider, async_exit_stack=async_exit_stack, + embed_body_fields=embed_body_fields, ) - values, errors, _, _2, _3 = solved_result - if errors: - raise WebSocketRequestValidationError(_normalize_errors(errors)) + if solved_result.errors: + raise WebSocketRequestValidationError( + _normalize_errors(solved_result.errors) + ) assert dependant.call is not None, "dependant.call must be a function" - await dependant.call(**values) + await dependant.call(**solved_result.values) return app @@ -371,11 +406,15 @@ class APIWebSocketRoute(routing.WebSocketRoute): 0, get_parameterless_sub_dependant(depends=depends, path=self.path_format), ) - + self._flat_dependant = get_flat_dependant(self.dependant) + self._embed_body_fields = _should_embed_body_fields( + self._flat_dependant.body_params + ) self.app = websocket_session( get_websocket_app( dependant=self.dependant, dependency_overrides_provider=dependency_overrides_provider, + embed_body_fields=self._embed_body_fields, ) ) @@ -469,7 +508,7 @@ class APIRoute(routing.Route): status_code ), f"Status code {status_code} must not have a response body" response_name = "Response_" + self.unique_id - self.response_field = create_response_field( + self.response_field = create_model_field( name=response_name, type_=self.response_model, mode="serialization", @@ -502,7 +541,7 @@ class APIRoute(routing.Route): additional_status_code ), f"Status code {additional_status_code} must not have a response body" response_name = f"Response_{additional_status_code}_{self.unique_id}" - response_field = create_response_field(name=response_name, type_=model) + response_field = create_model_field(name=response_name, type_=model) response_fields[additional_status_code] = response_field if response_fields: self.response_fields: Dict[Union[int, str], ModelField] = response_fields @@ -516,7 +555,15 @@ class APIRoute(routing.Route): 0, get_parameterless_sub_dependant(depends=depends, path=self.path_format), ) - self.body_field = get_body_field(dependant=self.dependant, name=self.unique_id) + self._flat_dependant = get_flat_dependant(self.dependant) + self._embed_body_fields = _should_embed_body_fields( + self._flat_dependant.body_params + ) + self.body_field = get_body_field( + flat_dependant=self._flat_dependant, + name=self.unique_id, + embed_body_fields=self._embed_body_fields, + ) self.app = request_response(self.get_route_handler()) def get_route_handler(self) -> Callable[[Request], Coroutine[Any, Any, Response]]: @@ -533,6 +580,7 @@ class APIRoute(routing.Route): response_model_exclude_defaults=self.response_model_exclude_defaults, response_model_exclude_none=self.response_model_exclude_none, dependency_overrides_provider=self.dependency_overrides_provider, + embed_body_fields=self._embed_body_fields, ) def matches(self, scope: Scope) -> Tuple[Match, Scope]: @@ -1308,6 +1356,10 @@ class APIRouter(routing.Router): self.add_event_handler("startup", handler) for handler in router.on_shutdown: self.add_event_handler("shutdown", handler) + self.lifespan_context = _merge_lifespan_context( + self.lifespan_context, + router.lifespan_context, + ) def get( self, diff --git a/fastapi/utils.py b/fastapi/utils.py index fb44d729c..46f2c98c3 100644 --- a/fastapi/utils.py +++ b/fastapi/utils.py @@ -60,9 +60,9 @@ def get_path_param_names(path: str) -> Set[str]: return set(re.findall("{(.*?)}", path)) -def create_response_field( +def create_model_field( name: str, - type_: Type[Any], + type_: Any, class_validators: Optional[Dict[str, Validator]] = None, default: Optional[Any] = Undefined, required: Union[bool, UndefinedType] = Undefined, @@ -71,9 +71,6 @@ def create_response_field( alias: Optional[str] = None, mode: Literal["validation", "serialization"] = "validation", ) -> ModelField: - """ - Create a new response field. Raises if type_ is invalid. - """ class_validators = class_validators or {} if PYDANTIC_V2: field_info = field_info or FieldInfo( @@ -135,7 +132,7 @@ def create_cloned_field( use_type.__fields__[f.name] = create_cloned_field( f, cloned_types=cloned_types ) - new_field = create_response_field(name=field.name, type_=use_type) + new_field = create_model_field(name=field.name, type_=use_type) new_field.has_alias = field.has_alias # type: ignore[attr-defined] new_field.alias = field.alias # type: ignore[misc] new_field.class_validators = field.class_validators # type: ignore[attr-defined] diff --git a/pyproject.toml b/pyproject.toml index bb87be470..1be2817a1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -241,3 +241,7 @@ known-third-party = ["fastapi", "pydantic", "starlette"] [tool.ruff.lint.pyupgrade] # Preserve types, even if a file imports `from __future__ import annotations`. keep-runtime-typing = true + +[tool.inline-snapshot] +# default-flags=["fix"] +# default-flags=["create"] diff --git a/requirements-docs.txt b/requirements-docs.txt index ab2b0165b..332fd1857 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -8,7 +8,7 @@ pyyaml >=5.3.1,<7.0.0 # For Material for MkDocs, Chinese search jieba==0.42.1 # For image processing by Material for MkDocs -pillow==10.3.0 +pillow==10.4.0 # For image processing by Material for MkDocs cairosvg==2.7.1 mkdocstrings[python]==0.25.1 diff --git a/requirements-tests.txt b/requirements-tests.txt index 08561d23a..2f2576dd5 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -3,7 +3,7 @@ pytest >=7.1.3,<8.0.0 coverage[toml] >= 6.5.0,< 8.0 mypy ==1.8.0 -ruff ==0.6.1 +ruff ==0.6.4 dirty-equals ==0.6.0 # TODO: once removing databases from tutorial, upgrade SQLAlchemy # probably when including SQLModel @@ -14,7 +14,7 @@ anyio[trio] >=3.2.1,<4.0.0 PyJWT==2.8.0 pyyaml >=5.3.1,<7.0.0 passlib[bcrypt] >=1.7.2,<2.0.0 - +inline-snapshot==0.13.0 # types types-ujson ==5.7.0.1 types-orjson ==3.6.2 diff --git a/scripts/playwright/cookie_param_models/image01.py b/scripts/playwright/cookie_param_models/image01.py new file mode 100644 index 000000000..77c91bfe2 --- /dev/null +++ b/scripts/playwright/cookie_param_models/image01.py @@ -0,0 +1,39 @@ +import subprocess +import time + +import httpx +from playwright.sync_api import Playwright, sync_playwright + + +# Run playwright codegen to generate the code below, copy paste the sections in run() +def run(playwright: Playwright) -> None: + browser = playwright.chromium.launch(headless=False) + # Update the viewport manually + context = browser.new_context(viewport={"width": 960, "height": 1080}) + browser = playwright.chromium.launch(headless=False) + context = browser.new_context() + page = context.new_page() + page.goto("http://localhost:8000/docs") + page.get_by_role("link", name="/items/").click() + # Manually add the screenshot + page.screenshot(path="docs/en/docs/img/tutorial/cookie-param-models/image01.png") + + # --------------------- + context.close() + browser.close() + + +process = subprocess.Popen( + ["fastapi", "run", "docs_src/cookie_param_models/tutorial001.py"] +) +try: + for _ in range(3): + try: + response = httpx.get("http://localhost:8000/docs") + except httpx.ConnectError: + time.sleep(1) + break + with sync_playwright() as playwright: + run(playwright) +finally: + process.terminate() diff --git a/scripts/playwright/header_param_models/image01.py b/scripts/playwright/header_param_models/image01.py new file mode 100644 index 000000000..53914251e --- /dev/null +++ b/scripts/playwright/header_param_models/image01.py @@ -0,0 +1,38 @@ +import subprocess +import time + +import httpx +from playwright.sync_api import Playwright, sync_playwright + + +# Run playwright codegen to generate the code below, copy paste the sections in run() +def run(playwright: Playwright) -> None: + browser = playwright.chromium.launch(headless=False) + # Update the viewport manually + context = browser.new_context(viewport={"width": 960, "height": 1080}) + page = context.new_page() + page.goto("http://localhost:8000/docs") + page.get_by_role("button", name="GET /items/ Read Items").click() + page.get_by_role("button", name="Try it out").click() + # Manually add the screenshot + page.screenshot(path="docs/en/docs/img/tutorial/header-param-models/image01.png") + + # --------------------- + context.close() + browser.close() + + +process = subprocess.Popen( + ["fastapi", "run", "docs_src/header_param_models/tutorial001.py"] +) +try: + for _ in range(3): + try: + response = httpx.get("http://localhost:8000/docs") + except httpx.ConnectError: + time.sleep(1) + break + with sync_playwright() as playwright: + run(playwright) +finally: + process.terminate() diff --git a/scripts/playwright/query_param_models/image01.py b/scripts/playwright/query_param_models/image01.py new file mode 100644 index 000000000..0ea1d0df4 --- /dev/null +++ b/scripts/playwright/query_param_models/image01.py @@ -0,0 +1,41 @@ +import subprocess +import time + +import httpx +from playwright.sync_api import Playwright, sync_playwright + + +# Run playwright codegen to generate the code below, copy paste the sections in run() +def run(playwright: Playwright) -> None: + browser = playwright.chromium.launch(headless=False) + # Update the viewport manually + context = browser.new_context(viewport={"width": 960, "height": 1080}) + browser = playwright.chromium.launch(headless=False) + context = browser.new_context() + page = context.new_page() + page.goto("http://localhost:8000/docs") + page.get_by_role("button", name="GET /items/ Read Items").click() + page.get_by_role("button", name="Try it out").click() + page.get_by_role("heading", name="Servers").click() + # Manually add the screenshot + page.screenshot(path="docs/en/docs/img/tutorial/query-param-models/image01.png") + + # --------------------- + context.close() + browser.close() + + +process = subprocess.Popen( + ["fastapi", "run", "docs_src/query_param_models/tutorial001.py"] +) +try: + for _ in range(3): + try: + response = httpx.get("http://localhost:8000/docs") + except httpx.ConnectError: + time.sleep(1) + break + with sync_playwright() as playwright: + run(playwright) +finally: + process.terminate() diff --git a/scripts/playwright/request_form_models/image01.py b/scripts/playwright/request_form_models/image01.py new file mode 100644 index 000000000..fe4da32fc --- /dev/null +++ b/scripts/playwright/request_form_models/image01.py @@ -0,0 +1,38 @@ +import subprocess +import time + +import httpx +from playwright.sync_api import Playwright, sync_playwright + + +# Run playwright codegen to generate the code below, copy paste the sections in run() +def run(playwright: Playwright) -> None: + browser = playwright.chromium.launch(headless=False) + # Update the viewport manually + context = browser.new_context(viewport={"width": 960, "height": 1080}) + page = context.new_page() + page.goto("http://localhost:8000/docs") + page.get_by_role("button", name="POST /login/ Login").click() + page.get_by_role("button", name="Try it out").click() + # Manually add the screenshot + page.screenshot(path="docs/en/docs/img/tutorial/request-form-models/image01.png") + + # --------------------- + context.close() + browser.close() + + +process = subprocess.Popen( + ["fastapi", "run", "docs_src/request_form_models/tutorial001.py"] +) +try: + for _ in range(3): + try: + response = httpx.get("http://localhost:8000/docs") + except httpx.ConnectError: + time.sleep(1) + break + with sync_playwright() as playwright: + run(playwright) +finally: + process.terminate() diff --git a/scripts/playwright/separate_openapi_schemas/image01.py b/scripts/playwright/separate_openapi_schemas/image01.py index 0b40f3bbc..0eb55fb73 100644 --- a/scripts/playwright/separate_openapi_schemas/image01.py +++ b/scripts/playwright/separate_openapi_schemas/image01.py @@ -3,13 +3,16 @@ import subprocess from playwright.sync_api import Playwright, sync_playwright +# Run playwright codegen to generate the code below, copy paste the sections in run() def run(playwright: Playwright) -> None: browser = playwright.chromium.launch(headless=False) + # Update the viewport manually context = browser.new_context(viewport={"width": 960, "height": 1080}) page = context.new_page() page.goto("http://localhost:8000/docs") page.get_by_text("POST/items/Create Item").click() page.get_by_role("tab", name="Schema").first.click() + # Manually add the screenshot page.screenshot( path="docs/en/docs/img/tutorial/separate-openapi-schemas/image01.png" ) diff --git a/scripts/playwright/separate_openapi_schemas/image02.py b/scripts/playwright/separate_openapi_schemas/image02.py index f76af7ee2..0eb6c3c79 100644 --- a/scripts/playwright/separate_openapi_schemas/image02.py +++ b/scripts/playwright/separate_openapi_schemas/image02.py @@ -3,14 +3,17 @@ import subprocess from playwright.sync_api import Playwright, sync_playwright +# Run playwright codegen to generate the code below, copy paste the sections in run() def run(playwright: Playwright) -> None: browser = playwright.chromium.launch(headless=False) + # Update the viewport manually context = browser.new_context(viewport={"width": 960, "height": 1080}) page = context.new_page() page.goto("http://localhost:8000/docs") page.get_by_text("GET/items/Read Items").click() page.get_by_role("button", name="Try it out").click() page.get_by_role("button", name="Execute").click() + # Manually add the screenshot page.screenshot( path="docs/en/docs/img/tutorial/separate-openapi-schemas/image02.png" ) diff --git a/scripts/playwright/separate_openapi_schemas/image03.py b/scripts/playwright/separate_openapi_schemas/image03.py index 127f5c428..b68e9d7db 100644 --- a/scripts/playwright/separate_openapi_schemas/image03.py +++ b/scripts/playwright/separate_openapi_schemas/image03.py @@ -3,14 +3,17 @@ import subprocess from playwright.sync_api import Playwright, sync_playwright +# Run playwright codegen to generate the code below, copy paste the sections in run() def run(playwright: Playwright) -> None: browser = playwright.chromium.launch(headless=False) + # Update the viewport manually context = browser.new_context(viewport={"width": 960, "height": 1080}) page = context.new_page() page.goto("http://localhost:8000/docs") page.get_by_text("GET/items/Read Items").click() page.get_by_role("tab", name="Schema").click() page.get_by_label("Schema").get_by_role("button", name="Expand all").click() + # Manually add the screenshot page.screenshot( path="docs/en/docs/img/tutorial/separate-openapi-schemas/image03.png" ) diff --git a/scripts/playwright/separate_openapi_schemas/image04.py b/scripts/playwright/separate_openapi_schemas/image04.py index 208eaf8a0..a36c2f6b2 100644 --- a/scripts/playwright/separate_openapi_schemas/image04.py +++ b/scripts/playwright/separate_openapi_schemas/image04.py @@ -3,14 +3,17 @@ import subprocess from playwright.sync_api import Playwright, sync_playwright +# Run playwright codegen to generate the code below, copy paste the sections in run() def run(playwright: Playwright) -> None: browser = playwright.chromium.launch(headless=False) + # Update the viewport manually context = browser.new_context(viewport={"width": 960, "height": 1080}) page = context.new_page() page.goto("http://localhost:8000/docs") page.get_by_role("button", name="Item-Input").click() page.get_by_role("button", name="Item-Output").click() page.set_viewport_size({"width": 960, "height": 820}) + # Manually add the screenshot page.screenshot( path="docs/en/docs/img/tutorial/separate-openapi-schemas/image04.png" ) diff --git a/scripts/playwright/separate_openapi_schemas/image05.py b/scripts/playwright/separate_openapi_schemas/image05.py index 83966b449..0da5db0cf 100644 --- a/scripts/playwright/separate_openapi_schemas/image05.py +++ b/scripts/playwright/separate_openapi_schemas/image05.py @@ -3,13 +3,16 @@ import subprocess from playwright.sync_api import Playwright, sync_playwright +# Run playwright codegen to generate the code below, copy paste the sections in run() def run(playwright: Playwright) -> None: browser = playwright.chromium.launch(headless=False) + # Update the viewport manually context = browser.new_context(viewport={"width": 960, "height": 1080}) page = context.new_page() page.goto("http://localhost:8000/docs") page.get_by_role("button", name="Item", exact=True).click() page.set_viewport_size({"width": 960, "height": 700}) + # Manually add the screenshot page.screenshot( path="docs/en/docs/img/tutorial/separate-openapi-schemas/image05.png" ) diff --git a/tests/test_allow_inf_nan_in_enforcing.py b/tests/test_allow_inf_nan_in_enforcing.py new file mode 100644 index 000000000..9e855fdf8 --- /dev/null +++ b/tests/test_allow_inf_nan_in_enforcing.py @@ -0,0 +1,83 @@ +import pytest +from fastapi import Body, FastAPI, Query +from fastapi.testclient import TestClient +from typing_extensions import Annotated + +app = FastAPI() + + +@app.post("/") +async def get( + x: Annotated[float, Query(allow_inf_nan=True)] = 0, + y: Annotated[float, Query(allow_inf_nan=False)] = 0, + z: Annotated[float, Query()] = 0, + b: Annotated[float, Body(allow_inf_nan=False)] = 0, +) -> str: + return "OK" + + +client = TestClient(app) + + +@pytest.mark.parametrize( + "value,code", + [ + ("-1", 200), + ("inf", 200), + ("-inf", 200), + ("nan", 200), + ("0", 200), + ("342", 200), + ], +) +def test_allow_inf_nan_param_true(value: str, code: int): + response = client.post(f"/?x={value}") + assert response.status_code == code, response.text + + +@pytest.mark.parametrize( + "value,code", + [ + ("-1", 200), + ("inf", 422), + ("-inf", 422), + ("nan", 422), + ("0", 200), + ("342", 200), + ], +) +def test_allow_inf_nan_param_false(value: str, code: int): + response = client.post(f"/?y={value}") + assert response.status_code == code, response.text + + +@pytest.mark.parametrize( + "value,code", + [ + ("-1", 200), + ("inf", 200), + ("-inf", 200), + ("nan", 200), + ("0", 200), + ("342", 200), + ], +) +def test_allow_inf_nan_param_default(value: str, code: int): + response = client.post(f"/?z={value}") + assert response.status_code == code, response.text + + +@pytest.mark.parametrize( + "value,code", + [ + ("-1", 200), + ("inf", 422), + ("-inf", 422), + ("nan", 422), + ("0", 200), + ("342", 200), + ], +) +def test_allow_inf_nan_body(value: str, code: int): + response = client.post("/", json=value) + assert response.status_code == code, response.text diff --git a/tests/test_compat.py b/tests/test_compat.py index bf268b860..f4a3093c5 100644 --- a/tests/test_compat.py +++ b/tests/test_compat.py @@ -1,11 +1,14 @@ -from typing import List, Union +from typing import Any, Dict, List, Union from fastapi import FastAPI, UploadFile from fastapi._compat import ( ModelField, Undefined, _get_model_config, + get_cached_model_fields, + get_model_fields, is_bytes_sequence_annotation, + is_scalar_field, is_uploadfile_sequence_annotation, ) from fastapi.testclient import TestClient @@ -91,3 +94,27 @@ def test_is_uploadfile_sequence_annotation(): # and other types, but I'm not even sure it's a good idea to support it as a first # class "feature" assert is_uploadfile_sequence_annotation(Union[List[str], List[UploadFile]]) + + +def test_is_pv1_scalar_field(): + # For coverage + class Model(BaseModel): + foo: Union[str, Dict[str, Any]] + + fields = get_model_fields(Model) + assert not is_scalar_field(fields[0]) + + +def test_get_model_fields_cached(): + class Model(BaseModel): + foo: str + + non_cached_fields = get_model_fields(Model) + non_cached_fields2 = get_model_fields(Model) + cached_fields = get_cached_model_fields(Model) + cached_fields2 = get_cached_model_fields(Model) + for f1, f2 in zip(cached_fields, cached_fields2): + assert f1 is f2 + + assert non_cached_fields is not non_cached_fields2 + assert cached_fields is cached_fields2 diff --git a/tests/test_forms_single_model.py b/tests/test_forms_single_model.py new file mode 100644 index 000000000..880ab3820 --- /dev/null +++ b/tests/test_forms_single_model.py @@ -0,0 +1,133 @@ +from typing import List, Optional + +from dirty_equals import IsDict +from fastapi import FastAPI, Form +from fastapi.testclient import TestClient +from pydantic import BaseModel, Field +from typing_extensions import Annotated + +app = FastAPI() + + +class FormModel(BaseModel): + username: str + lastname: str + age: Optional[int] = None + tags: List[str] = ["foo", "bar"] + alias_with: str = Field(alias="with", default="nothing") + + +@app.post("/form/") +def post_form(user: Annotated[FormModel, Form()]): + return user + + +client = TestClient(app) + + +def test_send_all_data(): + response = client.post( + "/form/", + data={ + "username": "Rick", + "lastname": "Sanchez", + "age": "70", + "tags": ["plumbus", "citadel"], + "with": "something", + }, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "username": "Rick", + "lastname": "Sanchez", + "age": 70, + "tags": ["plumbus", "citadel"], + "with": "something", + } + + +def test_defaults(): + response = client.post("/form/", data={"username": "Rick", "lastname": "Sanchez"}) + assert response.status_code == 200, response.text + assert response.json() == { + "username": "Rick", + "lastname": "Sanchez", + "age": None, + "tags": ["foo", "bar"], + "with": "nothing", + } + + +def test_invalid_data(): + response = client.post( + "/form/", + data={ + "username": "Rick", + "lastname": "Sanchez", + "age": "seventy", + "tags": ["plumbus", "citadel"], + }, + ) + assert response.status_code == 422, response.text + assert response.json() == IsDict( + { + "detail": [ + { + "type": "int_parsing", + "loc": ["body", "age"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "seventy", + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "age"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] + } + ) + + +def test_no_data(): + response = client.post("/form/") + assert response.status_code == 422, response.text + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {"tags": ["foo", "bar"], "with": "nothing"}, + }, + { + "type": "missing", + "loc": ["body", "lastname"], + "msg": "Field required", + "input": {"tags": ["foo", "bar"], "with": "nothing"}, + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "lastname"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) diff --git a/tests/test_forms_single_param.py b/tests/test_forms_single_param.py new file mode 100644 index 000000000..3bb951441 --- /dev/null +++ b/tests/test_forms_single_param.py @@ -0,0 +1,99 @@ +from fastapi import FastAPI, Form +from fastapi.testclient import TestClient +from typing_extensions import Annotated + +app = FastAPI() + + +@app.post("/form/") +def post_form(username: Annotated[str, Form()]): + return username + + +client = TestClient(app) + + +def test_single_form_field(): + response = client.post("/form/", data={"username": "Rick"}) + assert response.status_code == 200, response.text + assert response.json() == "Rick" + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/form/": { + "post": { + "summary": "Post Form", + "operationId": "post_form_form__post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Body_post_form_form__post" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "Body_post_form_form__post": { + "properties": {"username": {"type": "string", "title": "Username"}}, + "type": "object", + "required": ["username"], + "title": "Body_post_form_form__post", + }, + "HTTPValidationError": { + "properties": { + "detail": { + "items": {"$ref": "#/components/schemas/ValidationError"}, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } diff --git a/tests/test_openapi_examples.py b/tests/test_openapi_examples.py index 6597e5058..b3f83ae23 100644 --- a/tests/test_openapi_examples.py +++ b/tests/test_openapi_examples.py @@ -155,13 +155,26 @@ def test_openapi_schema(): "requestBody": { "content": { "application/json": { - "schema": { - "allOf": [{"$ref": "#/components/schemas/Item"}], - "title": "Item", - "examples": [ - {"data": "Data in Body examples, example1"} - ], - }, + "schema": IsDict( + { + "$ref": "#/components/schemas/Item", + "examples": [ + {"data": "Data in Body examples, example1"} + ], + } + ) + | IsDict( + { + # TODO: remove when deprecating Pydantic v1 + "allOf": [ + {"$ref": "#/components/schemas/Item"} + ], + "title": "Item", + "examples": [ + {"data": "Data in Body examples, example1"} + ], + } + ), "examples": { "Example One": { "summary": "Example One Summary", diff --git a/tests/test_router_events.py b/tests/test_router_events.py index 1b9de18ae..dd7ff3314 100644 --- a/tests/test_router_events.py +++ b/tests/test_router_events.py @@ -1,8 +1,8 @@ from contextlib import asynccontextmanager -from typing import AsyncGenerator, Dict +from typing import AsyncGenerator, Dict, Union import pytest -from fastapi import APIRouter, FastAPI +from fastapi import APIRouter, FastAPI, Request from fastapi.testclient import TestClient from pydantic import BaseModel @@ -109,3 +109,134 @@ def test_app_lifespan_state(state: State) -> None: assert response.json() == {"message": "Hello World"} assert state.app_startup is True assert state.app_shutdown is True + + +def test_router_nested_lifespan_state(state: State) -> None: + @asynccontextmanager + async def lifespan(app: FastAPI) -> AsyncGenerator[Dict[str, bool], None]: + state.app_startup = True + yield {"app": True} + state.app_shutdown = True + + @asynccontextmanager + async def router_lifespan(app: FastAPI) -> AsyncGenerator[Dict[str, bool], None]: + state.router_startup = True + yield {"router": True} + state.router_shutdown = True + + @asynccontextmanager + async def subrouter_lifespan(app: FastAPI) -> AsyncGenerator[Dict[str, bool], None]: + state.sub_router_startup = True + yield {"sub_router": True} + state.sub_router_shutdown = True + + sub_router = APIRouter(lifespan=subrouter_lifespan) + + router = APIRouter(lifespan=router_lifespan) + router.include_router(sub_router) + + app = FastAPI(lifespan=lifespan) + app.include_router(router) + + @app.get("/") + def main(request: Request) -> Dict[str, str]: + assert request.state.app + assert request.state.router + assert request.state.sub_router + return {"message": "Hello World"} + + assert state.app_startup is False + assert state.router_startup is False + assert state.sub_router_startup is False + assert state.app_shutdown is False + assert state.router_shutdown is False + assert state.sub_router_shutdown is False + + with TestClient(app) as client: + assert state.app_startup is True + assert state.router_startup is True + assert state.sub_router_startup is True + assert state.app_shutdown is False + assert state.router_shutdown is False + assert state.sub_router_shutdown is False + response = client.get("/") + assert response.status_code == 200, response.text + assert response.json() == {"message": "Hello World"} + + assert state.app_startup is True + assert state.router_startup is True + assert state.sub_router_startup is True + assert state.app_shutdown is True + assert state.router_shutdown is True + assert state.sub_router_shutdown is True + + +def test_router_nested_lifespan_state_overriding_by_parent() -> None: + @asynccontextmanager + async def lifespan( + app: FastAPI, + ) -> AsyncGenerator[Dict[str, Union[str, bool]], None]: + yield { + "app_specific": True, + "overridden": "app", + } + + @asynccontextmanager + async def router_lifespan( + app: FastAPI, + ) -> AsyncGenerator[Dict[str, Union[str, bool]], None]: + yield { + "router_specific": True, + "overridden": "router", # should override parent + } + + router = APIRouter(lifespan=router_lifespan) + app = FastAPI(lifespan=lifespan) + app.include_router(router) + + with TestClient(app) as client: + assert client.app_state == { + "app_specific": True, + "router_specific": True, + "overridden": "app", + } + + +def test_merged_no_return_lifespans_return_none() -> None: + @asynccontextmanager + async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: + yield + + @asynccontextmanager + async def router_lifespan(app: FastAPI) -> AsyncGenerator[None, None]: + yield + + router = APIRouter(lifespan=router_lifespan) + app = FastAPI(lifespan=lifespan) + app.include_router(router) + + with TestClient(app) as client: + assert not client.app_state + + +def test_merged_mixed_state_lifespans() -> None: + @asynccontextmanager + async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: + yield + + @asynccontextmanager + async def router_lifespan(app: FastAPI) -> AsyncGenerator[Dict[str, bool], None]: + yield {"router": True} + + @asynccontextmanager + async def sub_router_lifespan(app: FastAPI) -> AsyncGenerator[None, None]: + yield + + sub_router = APIRouter(lifespan=sub_router_lifespan) + router = APIRouter(lifespan=router_lifespan) + app = FastAPI(lifespan=lifespan) + router.include_router(sub_router) + app.include_router(router) + + with TestClient(app) as client: + assert client.app_state == {"router": True} diff --git a/tests/test_tutorial/test_cookie_param_models/__init__.py b/tests/test_tutorial/test_cookie_param_models/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_tutorial/test_cookie_param_models/test_tutorial001.py b/tests/test_tutorial/test_cookie_param_models/test_tutorial001.py new file mode 100644 index 000000000..60643185a --- /dev/null +++ b/tests/test_tutorial/test_cookie_param_models/test_tutorial001.py @@ -0,0 +1,205 @@ +import importlib + +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient +from inline_snapshot import snapshot + +from tests.utils import needs_py39, needs_py310 + + +@pytest.fixture( + name="client", + params=[ + "tutorial001", + pytest.param("tutorial001_py310", marks=needs_py310), + "tutorial001_an", + pytest.param("tutorial001_an_py39", marks=needs_py39), + pytest.param("tutorial001_an_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.cookie_param_models.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_cookie_param_model(client: TestClient): + with client as c: + c.cookies.set("session_id", "123") + c.cookies.set("fatebook_tracker", "456") + c.cookies.set("googall_tracker", "789") + response = c.get("/items/") + assert response.status_code == 200 + assert response.json() == { + "session_id": "123", + "fatebook_tracker": "456", + "googall_tracker": "789", + } + + +def test_cookie_param_model_defaults(client: TestClient): + with client as c: + c.cookies.set("session_id", "123") + response = c.get("/items/") + assert response.status_code == 200 + assert response.json() == { + "session_id": "123", + "fatebook_tracker": None, + "googall_tracker": None, + } + + +def test_cookie_param_model_invalid(client: TestClient): + response = client.get("/items/") + assert response.status_code == 422 + assert response.json() == snapshot( + IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["cookie", "session_id"], + "msg": "Field required", + "input": {}, + } + ] + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "type": "value_error.missing", + "loc": ["cookie", "session_id"], + "msg": "field required", + } + ] + } + ) + ) + + +def test_cookie_param_model_extra(client: TestClient): + with client as c: + c.cookies.set("session_id", "123") + c.cookies.set("extra", "track-me-here-too") + response = c.get("/items/") + assert response.status_code == 200 + assert response.json() == snapshot( + {"session_id": "123", "fatebook_tracker": None, "googall_tracker": None} + ) + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "name": "session_id", + "in": "cookie", + "required": True, + "schema": {"type": "string", "title": "Session Id"}, + }, + { + "name": "fatebook_tracker", + "in": "cookie", + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Fatebook Tracker", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "type": "string", + "title": "Fatebook Tracker", + } + ), + }, + { + "name": "googall_tracker", + "in": "cookie", + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Googall Tracker", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "type": "string", + "title": "Googall Tracker", + } + ), + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } + ) diff --git a/tests/test_tutorial/test_cookie_param_models/test_tutorial002.py b/tests/test_tutorial/test_cookie_param_models/test_tutorial002.py new file mode 100644 index 000000000..30adadc8a --- /dev/null +++ b/tests/test_tutorial/test_cookie_param_models/test_tutorial002.py @@ -0,0 +1,233 @@ +import importlib + +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient +from inline_snapshot import snapshot + +from tests.utils import needs_py39, needs_py310, needs_pydanticv1, needs_pydanticv2 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial002", marks=needs_pydanticv2), + pytest.param("tutorial002_py310", marks=[needs_py310, needs_pydanticv2]), + pytest.param("tutorial002_an", marks=needs_pydanticv2), + pytest.param("tutorial002_an_py39", marks=[needs_py39, needs_pydanticv2]), + pytest.param("tutorial002_an_py310", marks=[needs_py310, needs_pydanticv2]), + pytest.param("tutorial002_pv1", marks=[needs_pydanticv1, needs_pydanticv1]), + pytest.param("tutorial002_pv1_py310", marks=[needs_py310, needs_pydanticv1]), + pytest.param("tutorial002_pv1_an", marks=[needs_pydanticv1]), + pytest.param("tutorial002_pv1_an_py39", marks=[needs_py39, needs_pydanticv1]), + pytest.param("tutorial002_pv1_an_py310", marks=[needs_py310, needs_pydanticv1]), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.cookie_param_models.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_cookie_param_model(client: TestClient): + with client as c: + c.cookies.set("session_id", "123") + c.cookies.set("fatebook_tracker", "456") + c.cookies.set("googall_tracker", "789") + response = c.get("/items/") + assert response.status_code == 200 + assert response.json() == { + "session_id": "123", + "fatebook_tracker": "456", + "googall_tracker": "789", + } + + +def test_cookie_param_model_defaults(client: TestClient): + with client as c: + c.cookies.set("session_id", "123") + response = c.get("/items/") + assert response.status_code == 200 + assert response.json() == { + "session_id": "123", + "fatebook_tracker": None, + "googall_tracker": None, + } + + +def test_cookie_param_model_invalid(client: TestClient): + response = client.get("/items/") + assert response.status_code == 422 + assert response.json() == snapshot( + IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["cookie", "session_id"], + "msg": "Field required", + "input": {}, + } + ] + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "type": "value_error.missing", + "loc": ["cookie", "session_id"], + "msg": "field required", + } + ] + } + ) + ) + + +def test_cookie_param_model_extra(client: TestClient): + with client as c: + c.cookies.set("session_id", "123") + c.cookies.set("extra", "track-me-here-too") + response = c.get("/items/") + assert response.status_code == 422 + assert response.json() == snapshot( + IsDict( + { + "detail": [ + { + "type": "extra_forbidden", + "loc": ["cookie", "extra"], + "msg": "Extra inputs are not permitted", + "input": "track-me-here-too", + } + ] + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "type": "value_error.extra", + "loc": ["cookie", "extra"], + "msg": "extra fields not permitted", + } + ] + } + ) + ) + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "name": "session_id", + "in": "cookie", + "required": True, + "schema": {"type": "string", "title": "Session Id"}, + }, + { + "name": "fatebook_tracker", + "in": "cookie", + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Fatebook Tracker", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "type": "string", + "title": "Fatebook Tracker", + } + ), + }, + { + "name": "googall_tracker", + "in": "cookie", + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Googall Tracker", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "type": "string", + "title": "Googall Tracker", + } + ), + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } + ) diff --git a/tests/test_tutorial/test_header_param_models/__init__.py b/tests/test_tutorial/test_header_param_models/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_tutorial/test_header_param_models/test_tutorial001.py b/tests/test_tutorial/test_header_param_models/test_tutorial001.py new file mode 100644 index 000000000..06b2404cf --- /dev/null +++ b/tests/test_tutorial/test_header_param_models/test_tutorial001.py @@ -0,0 +1,238 @@ +import importlib + +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient +from inline_snapshot import snapshot + +from tests.utils import needs_py39, needs_py310 + + +@pytest.fixture( + name="client", + params=[ + "tutorial001", + pytest.param("tutorial001_py39", marks=needs_py39), + pytest.param("tutorial001_py310", marks=needs_py310), + "tutorial001_an", + pytest.param("tutorial001_an_py39", marks=needs_py39), + pytest.param("tutorial001_an_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.header_param_models.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_header_param_model(client: TestClient): + response = client.get( + "/items/", + headers=[ + ("save-data", "true"), + ("if-modified-since", "yesterday"), + ("traceparent", "123"), + ("x-tag", "one"), + ("x-tag", "two"), + ], + ) + assert response.status_code == 200 + assert response.json() == { + "host": "testserver", + "save_data": True, + "if_modified_since": "yesterday", + "traceparent": "123", + "x_tag": ["one", "two"], + } + + +def test_header_param_model_defaults(client: TestClient): + response = client.get("/items/", headers=[("save-data", "true")]) + assert response.status_code == 200 + assert response.json() == { + "host": "testserver", + "save_data": True, + "if_modified_since": None, + "traceparent": None, + "x_tag": [], + } + + +def test_header_param_model_invalid(client: TestClient): + response = client.get("/items/") + assert response.status_code == 422 + assert response.json() == snapshot( + { + "detail": [ + IsDict( + { + "type": "missing", + "loc": ["header", "save_data"], + "msg": "Field required", + "input": { + "x_tag": [], + "host": "testserver", + "accept": "*/*", + "accept-encoding": "gzip, deflate", + "connection": "keep-alive", + "user-agent": "testclient", + }, + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "type": "value_error.missing", + "loc": ["header", "save_data"], + "msg": "field required", + } + ) + ] + } + ) + + +def test_header_param_model_extra(client: TestClient): + response = client.get( + "/items/", headers=[("save-data", "true"), ("tool", "plumbus")] + ) + assert response.status_code == 200, response.text + assert response.json() == snapshot( + { + "host": "testserver", + "save_data": True, + "if_modified_since": None, + "traceparent": None, + "x_tag": [], + } + ) + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "name": "host", + "in": "header", + "required": True, + "schema": {"type": "string", "title": "Host"}, + }, + { + "name": "save_data", + "in": "header", + "required": True, + "schema": {"type": "boolean", "title": "Save Data"}, + }, + { + "name": "if_modified_since", + "in": "header", + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "If Modified Since", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "type": "string", + "title": "If Modified Since", + } + ), + }, + { + "name": "traceparent", + "in": "header", + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Traceparent", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "type": "string", + "title": "Traceparent", + } + ), + }, + { + "name": "x_tag", + "in": "header", + "required": False, + "schema": { + "type": "array", + "items": {"type": "string"}, + "default": [], + "title": "X Tag", + }, + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } + ) diff --git a/tests/test_tutorial/test_header_param_models/test_tutorial002.py b/tests/test_tutorial/test_header_param_models/test_tutorial002.py new file mode 100644 index 000000000..e07655a0c --- /dev/null +++ b/tests/test_tutorial/test_header_param_models/test_tutorial002.py @@ -0,0 +1,249 @@ +import importlib + +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient +from inline_snapshot import snapshot + +from tests.utils import needs_py39, needs_py310, needs_pydanticv1, needs_pydanticv2 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial002", marks=needs_pydanticv2), + pytest.param("tutorial002_py310", marks=[needs_py310, needs_pydanticv2]), + pytest.param("tutorial002_an", marks=needs_pydanticv2), + pytest.param("tutorial002_an_py39", marks=[needs_py39, needs_pydanticv2]), + pytest.param("tutorial002_an_py310", marks=[needs_py310, needs_pydanticv2]), + pytest.param("tutorial002_pv1", marks=[needs_pydanticv1, needs_pydanticv1]), + pytest.param("tutorial002_pv1_py310", marks=[needs_py310, needs_pydanticv1]), + pytest.param("tutorial002_pv1_an", marks=[needs_pydanticv1]), + pytest.param("tutorial002_pv1_an_py39", marks=[needs_py39, needs_pydanticv1]), + pytest.param("tutorial002_pv1_an_py310", marks=[needs_py310, needs_pydanticv1]), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.header_param_models.{request.param}") + + client = TestClient(mod.app) + client.headers.clear() + return client + + +def test_header_param_model(client: TestClient): + response = client.get( + "/items/", + headers=[ + ("save-data", "true"), + ("if-modified-since", "yesterday"), + ("traceparent", "123"), + ("x-tag", "one"), + ("x-tag", "two"), + ], + ) + assert response.status_code == 200, response.text + assert response.json() == { + "host": "testserver", + "save_data": True, + "if_modified_since": "yesterday", + "traceparent": "123", + "x_tag": ["one", "two"], + } + + +def test_header_param_model_defaults(client: TestClient): + response = client.get("/items/", headers=[("save-data", "true")]) + assert response.status_code == 200 + assert response.json() == { + "host": "testserver", + "save_data": True, + "if_modified_since": None, + "traceparent": None, + "x_tag": [], + } + + +def test_header_param_model_invalid(client: TestClient): + response = client.get("/items/") + assert response.status_code == 422 + assert response.json() == snapshot( + { + "detail": [ + IsDict( + { + "type": "missing", + "loc": ["header", "save_data"], + "msg": "Field required", + "input": {"x_tag": [], "host": "testserver"}, + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "type": "value_error.missing", + "loc": ["header", "save_data"], + "msg": "field required", + } + ) + ] + } + ) + + +def test_header_param_model_extra(client: TestClient): + response = client.get( + "/items/", headers=[("save-data", "true"), ("tool", "plumbus")] + ) + assert response.status_code == 422, response.text + assert response.json() == snapshot( + { + "detail": [ + IsDict( + { + "type": "extra_forbidden", + "loc": ["header", "tool"], + "msg": "Extra inputs are not permitted", + "input": "plumbus", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "type": "value_error.extra", + "loc": ["header", "tool"], + "msg": "extra fields not permitted", + } + ) + ] + } + ) + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "name": "host", + "in": "header", + "required": True, + "schema": {"type": "string", "title": "Host"}, + }, + { + "name": "save_data", + "in": "header", + "required": True, + "schema": {"type": "boolean", "title": "Save Data"}, + }, + { + "name": "if_modified_since", + "in": "header", + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "If Modified Since", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "type": "string", + "title": "If Modified Since", + } + ), + }, + { + "name": "traceparent", + "in": "header", + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Traceparent", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "type": "string", + "title": "Traceparent", + } + ), + }, + { + "name": "x_tag", + "in": "header", + "required": False, + "schema": { + "type": "array", + "items": {"type": "string"}, + "default": [], + "title": "X Tag", + }, + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } + ) diff --git a/tests/test_tutorial/test_query_param_models/__init__.py b/tests/test_tutorial/test_query_param_models/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_tutorial/test_query_param_models/test_tutorial001.py b/tests/test_tutorial/test_query_param_models/test_tutorial001.py new file mode 100644 index 000000000..5b7bc7b42 --- /dev/null +++ b/tests/test_tutorial/test_query_param_models/test_tutorial001.py @@ -0,0 +1,260 @@ +import importlib + +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient +from inline_snapshot import snapshot + +from tests.utils import needs_py39, needs_py310 + + +@pytest.fixture( + name="client", + params=[ + "tutorial001", + pytest.param("tutorial001_py39", marks=needs_py39), + pytest.param("tutorial001_py310", marks=needs_py310), + "tutorial001_an", + pytest.param("tutorial001_an_py39", marks=needs_py39), + pytest.param("tutorial001_an_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.query_param_models.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_query_param_model(client: TestClient): + response = client.get( + "/items/", + params={ + "limit": 10, + "offset": 5, + "order_by": "updated_at", + "tags": ["tag1", "tag2"], + }, + ) + assert response.status_code == 200 + assert response.json() == { + "limit": 10, + "offset": 5, + "order_by": "updated_at", + "tags": ["tag1", "tag2"], + } + + +def test_query_param_model_defaults(client: TestClient): + response = client.get("/items/") + assert response.status_code == 200 + assert response.json() == { + "limit": 100, + "offset": 0, + "order_by": "created_at", + "tags": [], + } + + +def test_query_param_model_invalid(client: TestClient): + response = client.get( + "/items/", + params={ + "limit": 150, + "offset": -1, + "order_by": "invalid", + }, + ) + assert response.status_code == 422 + assert response.json() == snapshot( + IsDict( + { + "detail": [ + { + "type": "less_than_equal", + "loc": ["query", "limit"], + "msg": "Input should be less than or equal to 100", + "input": "150", + "ctx": {"le": 100}, + }, + { + "type": "greater_than_equal", + "loc": ["query", "offset"], + "msg": "Input should be greater than or equal to 0", + "input": "-1", + "ctx": {"ge": 0}, + }, + { + "type": "literal_error", + "loc": ["query", "order_by"], + "msg": "Input should be 'created_at' or 'updated_at'", + "input": "invalid", + "ctx": {"expected": "'created_at' or 'updated_at'"}, + }, + ] + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "type": "value_error.number.not_le", + "loc": ["query", "limit"], + "msg": "ensure this value is less than or equal to 100", + "ctx": {"limit_value": 100}, + }, + { + "type": "value_error.number.not_ge", + "loc": ["query", "offset"], + "msg": "ensure this value is greater than or equal to 0", + "ctx": {"limit_value": 0}, + }, + { + "type": "value_error.const", + "loc": ["query", "order_by"], + "msg": "unexpected value; permitted: 'created_at', 'updated_at'", + "ctx": { + "given": "invalid", + "permitted": ["created_at", "updated_at"], + }, + }, + ] + } + ) + ) + + +def test_query_param_model_extra(client: TestClient): + response = client.get( + "/items/", + params={ + "limit": 10, + "offset": 5, + "order_by": "updated_at", + "tags": ["tag1", "tag2"], + "tool": "plumbus", + }, + ) + assert response.status_code == 200 + assert response.json() == { + "limit": 10, + "offset": 5, + "order_by": "updated_at", + "tags": ["tag1", "tag2"], + } + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "name": "limit", + "in": "query", + "required": False, + "schema": { + "type": "integer", + "maximum": 100, + "exclusiveMinimum": 0, + "default": 100, + "title": "Limit", + }, + }, + { + "name": "offset", + "in": "query", + "required": False, + "schema": { + "type": "integer", + "minimum": 0, + "default": 0, + "title": "Offset", + }, + }, + { + "name": "order_by", + "in": "query", + "required": False, + "schema": { + "enum": ["created_at", "updated_at"], + "type": "string", + "default": "created_at", + "title": "Order By", + }, + }, + { + "name": "tags", + "in": "query", + "required": False, + "schema": { + "type": "array", + "items": {"type": "string"}, + "default": [], + "title": "Tags", + }, + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } + ) diff --git a/tests/test_tutorial/test_query_param_models/test_tutorial002.py b/tests/test_tutorial/test_query_param_models/test_tutorial002.py new file mode 100644 index 000000000..4432c9d8a --- /dev/null +++ b/tests/test_tutorial/test_query_param_models/test_tutorial002.py @@ -0,0 +1,282 @@ +import importlib + +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient +from inline_snapshot import snapshot + +from tests.utils import needs_py39, needs_py310, needs_pydanticv1, needs_pydanticv2 + + +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial002", marks=needs_pydanticv2), + pytest.param("tutorial002_py39", marks=[needs_py39, needs_pydanticv2]), + pytest.param("tutorial002_py310", marks=[needs_py310, needs_pydanticv2]), + pytest.param("tutorial002_an", marks=needs_pydanticv2), + pytest.param("tutorial002_an_py39", marks=[needs_py39, needs_pydanticv2]), + pytest.param("tutorial002_an_py310", marks=[needs_py310, needs_pydanticv2]), + pytest.param("tutorial002_pv1", marks=[needs_pydanticv1, needs_pydanticv1]), + pytest.param("tutorial002_pv1_py39", marks=[needs_py39, needs_pydanticv1]), + pytest.param("tutorial002_pv1_py310", marks=[needs_py310, needs_pydanticv1]), + pytest.param("tutorial002_pv1_an", marks=[needs_pydanticv1]), + pytest.param("tutorial002_pv1_an_py39", marks=[needs_py39, needs_pydanticv1]), + pytest.param("tutorial002_pv1_an_py310", marks=[needs_py310, needs_pydanticv1]), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.query_param_models.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_query_param_model(client: TestClient): + response = client.get( + "/items/", + params={ + "limit": 10, + "offset": 5, + "order_by": "updated_at", + "tags": ["tag1", "tag2"], + }, + ) + assert response.status_code == 200 + assert response.json() == { + "limit": 10, + "offset": 5, + "order_by": "updated_at", + "tags": ["tag1", "tag2"], + } + + +def test_query_param_model_defaults(client: TestClient): + response = client.get("/items/") + assert response.status_code == 200 + assert response.json() == { + "limit": 100, + "offset": 0, + "order_by": "created_at", + "tags": [], + } + + +def test_query_param_model_invalid(client: TestClient): + response = client.get( + "/items/", + params={ + "limit": 150, + "offset": -1, + "order_by": "invalid", + }, + ) + assert response.status_code == 422 + assert response.json() == snapshot( + IsDict( + { + "detail": [ + { + "type": "less_than_equal", + "loc": ["query", "limit"], + "msg": "Input should be less than or equal to 100", + "input": "150", + "ctx": {"le": 100}, + }, + { + "type": "greater_than_equal", + "loc": ["query", "offset"], + "msg": "Input should be greater than or equal to 0", + "input": "-1", + "ctx": {"ge": 0}, + }, + { + "type": "literal_error", + "loc": ["query", "order_by"], + "msg": "Input should be 'created_at' or 'updated_at'", + "input": "invalid", + "ctx": {"expected": "'created_at' or 'updated_at'"}, + }, + ] + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "type": "value_error.number.not_le", + "loc": ["query", "limit"], + "msg": "ensure this value is less than or equal to 100", + "ctx": {"limit_value": 100}, + }, + { + "type": "value_error.number.not_ge", + "loc": ["query", "offset"], + "msg": "ensure this value is greater than or equal to 0", + "ctx": {"limit_value": 0}, + }, + { + "type": "value_error.const", + "loc": ["query", "order_by"], + "msg": "unexpected value; permitted: 'created_at', 'updated_at'", + "ctx": { + "given": "invalid", + "permitted": ["created_at", "updated_at"], + }, + }, + ] + } + ) + ) + + +def test_query_param_model_extra(client: TestClient): + response = client.get( + "/items/", + params={ + "limit": 10, + "offset": 5, + "order_by": "updated_at", + "tags": ["tag1", "tag2"], + "tool": "plumbus", + }, + ) + assert response.status_code == 422 + assert response.json() == snapshot( + { + "detail": [ + IsDict( + { + "type": "extra_forbidden", + "loc": ["query", "tool"], + "msg": "Extra inputs are not permitted", + "input": "plumbus", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "type": "value_error.extra", + "loc": ["query", "tool"], + "msg": "extra fields not permitted", + } + ) + ] + } + ) + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "name": "limit", + "in": "query", + "required": False, + "schema": { + "type": "integer", + "maximum": 100, + "exclusiveMinimum": 0, + "default": 100, + "title": "Limit", + }, + }, + { + "name": "offset", + "in": "query", + "required": False, + "schema": { + "type": "integer", + "minimum": 0, + "default": 0, + "title": "Offset", + }, + }, + { + "name": "order_by", + "in": "query", + "required": False, + "schema": { + "enum": ["created_at", "updated_at"], + "type": "string", + "default": "created_at", + "title": "Order By", + }, + }, + { + "name": "tags", + "in": "query", + "required": False, + "schema": { + "type": "array", + "items": {"type": "string"}, + "default": [], + "title": "Tags", + }, + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } + ) diff --git a/tests/test_tutorial/test_request_form_models/__init__.py b/tests/test_tutorial/test_request_form_models/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_tutorial/test_request_form_models/test_tutorial001.py b/tests/test_tutorial/test_request_form_models/test_tutorial001.py new file mode 100644 index 000000000..46c130ee8 --- /dev/null +++ b/tests/test_tutorial/test_request_form_models/test_tutorial001.py @@ -0,0 +1,232 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.request_form_models.tutorial001 import app + + client = TestClient(app) + return client + + +def test_post_body_form(client: TestClient): + response = client.post("/login/", data={"username": "Foo", "password": "secret"}) + assert response.status_code == 200 + assert response.json() == {"username": "Foo", "password": "secret"} + + +def test_post_body_form_no_password(client: TestClient): + response = client.post("/login/", data={"username": "Foo"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": {"username": "Foo"}, + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_post_body_form_no_username(client: TestClient): + response = client.post("/login/", data={"password": "secret"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {"password": "secret"}, + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_post_body_form_no_data(client: TestClient): + response = client.post("/login/") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {}, + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": {}, + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_post_body_json(client: TestClient): + response = client.post("/login/", json={"username": "Foo", "password": "secret"}) + assert response.status_code == 422, response.text + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {}, + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": {}, + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/login/": { + "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_login__post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": {"$ref": "#/components/schemas/FormData"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "FormData": { + "properties": { + "username": {"type": "string", "title": "Username"}, + "password": {"type": "string", "title": "Password"}, + }, + "type": "object", + "required": ["username", "password"], + "title": "FormData", + }, + "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"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_request_form_models/test_tutorial001_an.py b/tests/test_tutorial/test_request_form_models/test_tutorial001_an.py new file mode 100644 index 000000000..4e14d89c8 --- /dev/null +++ b/tests/test_tutorial/test_request_form_models/test_tutorial001_an.py @@ -0,0 +1,232 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.request_form_models.tutorial001_an import app + + client = TestClient(app) + return client + + +def test_post_body_form(client: TestClient): + response = client.post("/login/", data={"username": "Foo", "password": "secret"}) + assert response.status_code == 200 + assert response.json() == {"username": "Foo", "password": "secret"} + + +def test_post_body_form_no_password(client: TestClient): + response = client.post("/login/", data={"username": "Foo"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": {"username": "Foo"}, + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_post_body_form_no_username(client: TestClient): + response = client.post("/login/", data={"password": "secret"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {"password": "secret"}, + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_post_body_form_no_data(client: TestClient): + response = client.post("/login/") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {}, + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": {}, + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_post_body_json(client: TestClient): + response = client.post("/login/", json={"username": "Foo", "password": "secret"}) + assert response.status_code == 422, response.text + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {}, + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": {}, + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/login/": { + "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_login__post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": {"$ref": "#/components/schemas/FormData"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "FormData": { + "properties": { + "username": {"type": "string", "title": "Username"}, + "password": {"type": "string", "title": "Password"}, + }, + "type": "object", + "required": ["username", "password"], + "title": "FormData", + }, + "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"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_request_form_models/test_tutorial001_an_py39.py b/tests/test_tutorial/test_request_form_models/test_tutorial001_an_py39.py new file mode 100644 index 000000000..2e6426aa7 --- /dev/null +++ b/tests/test_tutorial/test_request_form_models/test_tutorial001_an_py39.py @@ -0,0 +1,240 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient + +from tests.utils import needs_py39 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.request_form_models.tutorial001_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_post_body_form(client: TestClient): + response = client.post("/login/", data={"username": "Foo", "password": "secret"}) + assert response.status_code == 200 + assert response.json() == {"username": "Foo", "password": "secret"} + + +@needs_py39 +def test_post_body_form_no_password(client: TestClient): + response = client.post("/login/", data={"username": "Foo"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": {"username": "Foo"}, + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +@needs_py39 +def test_post_body_form_no_username(client: TestClient): + response = client.post("/login/", data={"password": "secret"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {"password": "secret"}, + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +@needs_py39 +def test_post_body_form_no_data(client: TestClient): + response = client.post("/login/") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {}, + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": {}, + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +@needs_py39 +def test_post_body_json(client: TestClient): + response = client.post("/login/", json={"username": "Foo", "password": "secret"}) + assert response.status_code == 422, response.text + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {}, + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": {}, + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/login/": { + "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_login__post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": {"$ref": "#/components/schemas/FormData"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "FormData": { + "properties": { + "username": {"type": "string", "title": "Username"}, + "password": {"type": "string", "title": "Password"}, + }, + "type": "object", + "required": ["username", "password"], + "title": "FormData", + }, + "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"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_request_form_models/test_tutorial002.py b/tests/test_tutorial/test_request_form_models/test_tutorial002.py new file mode 100644 index 000000000..76f480001 --- /dev/null +++ b/tests/test_tutorial/test_request_form_models/test_tutorial002.py @@ -0,0 +1,196 @@ +import pytest +from fastapi.testclient import TestClient + +from tests.utils import needs_pydanticv2 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.request_form_models.tutorial002 import app + + client = TestClient(app) + return client + + +@needs_pydanticv2 +def test_post_body_form(client: TestClient): + response = client.post("/login/", data={"username": "Foo", "password": "secret"}) + assert response.status_code == 200 + assert response.json() == {"username": "Foo", "password": "secret"} + + +@needs_pydanticv2 +def test_post_body_extra_form(client: TestClient): + response = client.post( + "/login/", data={"username": "Foo", "password": "secret", "extra": "extra"} + ) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "extra_forbidden", + "loc": ["body", "extra"], + "msg": "Extra inputs are not permitted", + "input": "extra", + } + ] + } + + +@needs_pydanticv2 +def test_post_body_form_no_password(client: TestClient): + response = client.post("/login/", data={"username": "Foo"}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": {"username": "Foo"}, + } + ] + } + + +@needs_pydanticv2 +def test_post_body_form_no_username(client: TestClient): + response = client.post("/login/", data={"password": "secret"}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {"password": "secret"}, + } + ] + } + + +@needs_pydanticv2 +def test_post_body_form_no_data(client: TestClient): + response = client.post("/login/") + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {}, + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": {}, + }, + ] + } + + +@needs_pydanticv2 +def test_post_body_json(client: TestClient): + response = client.post("/login/", json={"username": "Foo", "password": "secret"}) + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {}, + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": {}, + }, + ] + } + + +@needs_pydanticv2 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/login/": { + "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_login__post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": {"$ref": "#/components/schemas/FormData"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "FormData": { + "properties": { + "username": {"type": "string", "title": "Username"}, + "password": {"type": "string", "title": "Password"}, + }, + "additionalProperties": False, + "type": "object", + "required": ["username", "password"], + "title": "FormData", + }, + "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"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_request_form_models/test_tutorial002_an.py b/tests/test_tutorial/test_request_form_models/test_tutorial002_an.py new file mode 100644 index 000000000..179b2977d --- /dev/null +++ b/tests/test_tutorial/test_request_form_models/test_tutorial002_an.py @@ -0,0 +1,196 @@ +import pytest +from fastapi.testclient import TestClient + +from tests.utils import needs_pydanticv2 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.request_form_models.tutorial002_an import app + + client = TestClient(app) + return client + + +@needs_pydanticv2 +def test_post_body_form(client: TestClient): + response = client.post("/login/", data={"username": "Foo", "password": "secret"}) + assert response.status_code == 200 + assert response.json() == {"username": "Foo", "password": "secret"} + + +@needs_pydanticv2 +def test_post_body_extra_form(client: TestClient): + response = client.post( + "/login/", data={"username": "Foo", "password": "secret", "extra": "extra"} + ) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "extra_forbidden", + "loc": ["body", "extra"], + "msg": "Extra inputs are not permitted", + "input": "extra", + } + ] + } + + +@needs_pydanticv2 +def test_post_body_form_no_password(client: TestClient): + response = client.post("/login/", data={"username": "Foo"}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": {"username": "Foo"}, + } + ] + } + + +@needs_pydanticv2 +def test_post_body_form_no_username(client: TestClient): + response = client.post("/login/", data={"password": "secret"}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {"password": "secret"}, + } + ] + } + + +@needs_pydanticv2 +def test_post_body_form_no_data(client: TestClient): + response = client.post("/login/") + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {}, + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": {}, + }, + ] + } + + +@needs_pydanticv2 +def test_post_body_json(client: TestClient): + response = client.post("/login/", json={"username": "Foo", "password": "secret"}) + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {}, + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": {}, + }, + ] + } + + +@needs_pydanticv2 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/login/": { + "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_login__post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": {"$ref": "#/components/schemas/FormData"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "FormData": { + "properties": { + "username": {"type": "string", "title": "Username"}, + "password": {"type": "string", "title": "Password"}, + }, + "additionalProperties": False, + "type": "object", + "required": ["username", "password"], + "title": "FormData", + }, + "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"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_request_form_models/test_tutorial002_an_py39.py b/tests/test_tutorial/test_request_form_models/test_tutorial002_an_py39.py new file mode 100644 index 000000000..510ad9d7c --- /dev/null +++ b/tests/test_tutorial/test_request_form_models/test_tutorial002_an_py39.py @@ -0,0 +1,203 @@ +import pytest +from fastapi.testclient import TestClient + +from tests.utils import needs_py39, needs_pydanticv2 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.request_form_models.tutorial002_an_py39 import app + + client = TestClient(app) + return client + + +@needs_pydanticv2 +@needs_py39 +def test_post_body_form(client: TestClient): + response = client.post("/login/", data={"username": "Foo", "password": "secret"}) + assert response.status_code == 200 + assert response.json() == {"username": "Foo", "password": "secret"} + + +@needs_pydanticv2 +@needs_py39 +def test_post_body_extra_form(client: TestClient): + response = client.post( + "/login/", data={"username": "Foo", "password": "secret", "extra": "extra"} + ) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "extra_forbidden", + "loc": ["body", "extra"], + "msg": "Extra inputs are not permitted", + "input": "extra", + } + ] + } + + +@needs_pydanticv2 +@needs_py39 +def test_post_body_form_no_password(client: TestClient): + response = client.post("/login/", data={"username": "Foo"}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": {"username": "Foo"}, + } + ] + } + + +@needs_pydanticv2 +@needs_py39 +def test_post_body_form_no_username(client: TestClient): + response = client.post("/login/", data={"password": "secret"}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {"password": "secret"}, + } + ] + } + + +@needs_pydanticv2 +@needs_py39 +def test_post_body_form_no_data(client: TestClient): + response = client.post("/login/") + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {}, + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": {}, + }, + ] + } + + +@needs_pydanticv2 +@needs_py39 +def test_post_body_json(client: TestClient): + response = client.post("/login/", json={"username": "Foo", "password": "secret"}) + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": {}, + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": {}, + }, + ] + } + + +@needs_pydanticv2 +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/login/": { + "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_login__post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": {"$ref": "#/components/schemas/FormData"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "FormData": { + "properties": { + "username": {"type": "string", "title": "Username"}, + "password": {"type": "string", "title": "Password"}, + }, + "additionalProperties": False, + "type": "object", + "required": ["username", "password"], + "title": "FormData", + }, + "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"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_request_form_models/test_tutorial002_pv1.py b/tests/test_tutorial/test_request_form_models/test_tutorial002_pv1.py new file mode 100644 index 000000000..249b9379d --- /dev/null +++ b/tests/test_tutorial/test_request_form_models/test_tutorial002_pv1.py @@ -0,0 +1,189 @@ +import pytest +from fastapi.testclient import TestClient + +from tests.utils import needs_pydanticv1 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.request_form_models.tutorial002_pv1 import app + + client = TestClient(app) + return client + + +@needs_pydanticv1 +def test_post_body_form(client: TestClient): + response = client.post("/login/", data={"username": "Foo", "password": "secret"}) + assert response.status_code == 200 + assert response.json() == {"username": "Foo", "password": "secret"} + + +@needs_pydanticv1 +def test_post_body_extra_form(client: TestClient): + response = client.post( + "/login/", data={"username": "Foo", "password": "secret", "extra": "extra"} + ) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "value_error.extra", + "loc": ["body", "extra"], + "msg": "extra fields not permitted", + } + ] + } + + +@needs_pydanticv1 +def test_post_body_form_no_password(client: TestClient): + response = client.post("/login/", data={"username": "Foo"}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "value_error.missing", + "loc": ["body", "password"], + "msg": "field required", + } + ] + } + + +@needs_pydanticv1 +def test_post_body_form_no_username(client: TestClient): + response = client.post("/login/", data={"password": "secret"}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "value_error.missing", + "loc": ["body", "username"], + "msg": "field required", + } + ] + } + + +@needs_pydanticv1 +def test_post_body_form_no_data(client: TestClient): + response = client.post("/login/") + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "value_error.missing", + "loc": ["body", "username"], + "msg": "field required", + }, + { + "type": "value_error.missing", + "loc": ["body", "password"], + "msg": "field required", + }, + ] + } + + +@needs_pydanticv1 +def test_post_body_json(client: TestClient): + response = client.post("/login/", json={"username": "Foo", "password": "secret"}) + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "type": "value_error.missing", + "loc": ["body", "username"], + "msg": "field required", + }, + { + "type": "value_error.missing", + "loc": ["body", "password"], + "msg": "field required", + }, + ] + } + + +@needs_pydanticv1 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/login/": { + "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_login__post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": {"$ref": "#/components/schemas/FormData"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "FormData": { + "properties": { + "username": {"type": "string", "title": "Username"}, + "password": {"type": "string", "title": "Password"}, + }, + "additionalProperties": False, + "type": "object", + "required": ["username", "password"], + "title": "FormData", + }, + "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"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_request_form_models/test_tutorial002_pv1_an.py b/tests/test_tutorial/test_request_form_models/test_tutorial002_pv1_an.py new file mode 100644 index 000000000..44cb3c32b --- /dev/null +++ b/tests/test_tutorial/test_request_form_models/test_tutorial002_pv1_an.py @@ -0,0 +1,196 @@ +import pytest +from fastapi.testclient import TestClient + +from tests.utils import needs_pydanticv1 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.request_form_models.tutorial002_pv1_an import app + + client = TestClient(app) + return client + + +# TODO: remove when deprecating Pydantic v1 +@needs_pydanticv1 +def test_post_body_form(client: TestClient): + response = client.post("/login/", data={"username": "Foo", "password": "secret"}) + assert response.status_code == 200 + assert response.json() == {"username": "Foo", "password": "secret"} + + +# TODO: remove when deprecating Pydantic v1 +@needs_pydanticv1 +def test_post_body_extra_form(client: TestClient): + response = client.post( + "/login/", data={"username": "Foo", "password": "secret", "extra": "extra"} + ) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "value_error.extra", + "loc": ["body", "extra"], + "msg": "extra fields not permitted", + } + ] + } + + +# TODO: remove when deprecating Pydantic v1 +@needs_pydanticv1 +def test_post_body_form_no_password(client: TestClient): + response = client.post("/login/", data={"username": "Foo"}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "value_error.missing", + "loc": ["body", "password"], + "msg": "field required", + } + ] + } + + +# TODO: remove when deprecating Pydantic v1 +@needs_pydanticv1 +def test_post_body_form_no_username(client: TestClient): + response = client.post("/login/", data={"password": "secret"}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "value_error.missing", + "loc": ["body", "username"], + "msg": "field required", + } + ] + } + + +# TODO: remove when deprecating Pydantic v1 +@needs_pydanticv1 +def test_post_body_form_no_data(client: TestClient): + response = client.post("/login/") + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "value_error.missing", + "loc": ["body", "username"], + "msg": "field required", + }, + { + "type": "value_error.missing", + "loc": ["body", "password"], + "msg": "field required", + }, + ] + } + + +# TODO: remove when deprecating Pydantic v1 +@needs_pydanticv1 +def test_post_body_json(client: TestClient): + response = client.post("/login/", json={"username": "Foo", "password": "secret"}) + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "type": "value_error.missing", + "loc": ["body", "username"], + "msg": "field required", + }, + { + "type": "value_error.missing", + "loc": ["body", "password"], + "msg": "field required", + }, + ] + } + + +# TODO: remove when deprecating Pydantic v1 +@needs_pydanticv1 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/login/": { + "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_login__post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": {"$ref": "#/components/schemas/FormData"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "FormData": { + "properties": { + "username": {"type": "string", "title": "Username"}, + "password": {"type": "string", "title": "Password"}, + }, + "additionalProperties": False, + "type": "object", + "required": ["username", "password"], + "title": "FormData", + }, + "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"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_request_form_models/test_tutorial002_pv1_an_p39.py b/tests/test_tutorial/test_request_form_models/test_tutorial002_pv1_an_p39.py new file mode 100644 index 000000000..899549e40 --- /dev/null +++ b/tests/test_tutorial/test_request_form_models/test_tutorial002_pv1_an_p39.py @@ -0,0 +1,203 @@ +import pytest +from fastapi.testclient import TestClient + +from tests.utils import needs_py39, needs_pydanticv1 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.request_form_models.tutorial002_pv1_an_py39 import app + + client = TestClient(app) + return client + + +# TODO: remove when deprecating Pydantic v1 +@needs_pydanticv1 +@needs_py39 +def test_post_body_form(client: TestClient): + response = client.post("/login/", data={"username": "Foo", "password": "secret"}) + assert response.status_code == 200 + assert response.json() == {"username": "Foo", "password": "secret"} + + +# TODO: remove when deprecating Pydantic v1 +@needs_pydanticv1 +@needs_py39 +def test_post_body_extra_form(client: TestClient): + response = client.post( + "/login/", data={"username": "Foo", "password": "secret", "extra": "extra"} + ) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "value_error.extra", + "loc": ["body", "extra"], + "msg": "extra fields not permitted", + } + ] + } + + +# TODO: remove when deprecating Pydantic v1 +@needs_pydanticv1 +@needs_py39 +def test_post_body_form_no_password(client: TestClient): + response = client.post("/login/", data={"username": "Foo"}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "value_error.missing", + "loc": ["body", "password"], + "msg": "field required", + } + ] + } + + +# TODO: remove when deprecating Pydantic v1 +@needs_pydanticv1 +@needs_py39 +def test_post_body_form_no_username(client: TestClient): + response = client.post("/login/", data={"password": "secret"}) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "value_error.missing", + "loc": ["body", "username"], + "msg": "field required", + } + ] + } + + +# TODO: remove when deprecating Pydantic v1 +@needs_pydanticv1 +@needs_py39 +def test_post_body_form_no_data(client: TestClient): + response = client.post("/login/") + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "type": "value_error.missing", + "loc": ["body", "username"], + "msg": "field required", + }, + { + "type": "value_error.missing", + "loc": ["body", "password"], + "msg": "field required", + }, + ] + } + + +# TODO: remove when deprecating Pydantic v1 +@needs_pydanticv1 +@needs_py39 +def test_post_body_json(client: TestClient): + response = client.post("/login/", json={"username": "Foo", "password": "secret"}) + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "type": "value_error.missing", + "loc": ["body", "username"], + "msg": "field required", + }, + { + "type": "value_error.missing", + "loc": ["body", "password"], + "msg": "field required", + }, + ] + } + + +# TODO: remove when deprecating Pydantic v1 +@needs_pydanticv1 +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/login/": { + "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_login__post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": {"$ref": "#/components/schemas/FormData"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "FormData": { + "properties": { + "username": {"type": "string", "title": "Username"}, + "password": {"type": "string", "title": "Password"}, + }, + "additionalProperties": False, + "type": "object", + "required": ["username", "password"], + "title": "FormData", + }, + "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"}, + } + }, + }, + } + }, + }