From bfd46e562b91e560ff2446eac38320aa2bd6e82b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 5 Jun 2020 17:34:43 +0200 Subject: [PATCH 01/82] =?UTF-8?q?=F0=9F=94=A7=20Update=20issue-manager=20G?= =?UTF-8?q?itHub=20action=20(#1520)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../workflows/{main.yml => issue-manager.yml} | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) rename .github/workflows/{main.yml => issue-manager.yml} (60%) diff --git a/.github/workflows/main.yml b/.github/workflows/issue-manager.yml similarity index 60% rename from .github/workflows/main.yml rename to .github/workflows/issue-manager.yml index 5e0a49684..712930e0b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/issue-manager.yml @@ -1,15 +1,24 @@ +name: Issue Manager + on: schedule: - - cron: "0 0 * * *" + - cron: "0 0 * * *" + issue_comment: + types: + - created + - edited + issues: + types: + - labeled jobs: issue-manager: runs-on: ubuntu-latest steps: - - uses: tiangolo/issue-manager@master - with: - token: ${{ secrets.GITHUB_TOKEN }} - config: > + - uses: tiangolo/issue-manager@0.2.0 + with: + token: ${{ secrets.GITHUB_TOKEN }} + config: > { "answered": { "users": ["tiangolo", "dmontagu"], From 8cfe254400a92c1184c354a92541b401932d24a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 5 Jun 2020 17:35:39 +0200 Subject: [PATCH 02/82] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 09f6b2a83..d1ee4584c 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Update GitHub action issue-manager. PR [#1520](https://github.com/tiangolo/fastapi/pull/1520). * Add new links: * **English articles**: * [Real-time Notifications with Python and Postgres](https://wuilly.com/2019/10/real-time-notifications-with-python-and-postgres/) by [Guillermo Cruz](https://wuilly.com/). From 88a887329eaca29e5a78071e182285b042e0e54a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 7 Jun 2020 22:00:15 +0200 Subject: [PATCH 03/82] =?UTF-8?q?=F0=9F=93=9D=20Update=20help=20and=20issu?= =?UTF-8?q?e=20templates=20(#1531)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 📝 Update help docs: Gitter, issues, links also fix Gitter tab padding * 📝 Update new GitHub issue templates * 📝 Add note about extra help required for new issues --- .github/ISSUE_TEMPLATE/bug_report.md | 62 --------------- .github/ISSUE_TEMPLATE/feature_request.md | 92 +++++++++++++++++++++-- .github/ISSUE_TEMPLATE/question.md | 71 +++++++++++++++-- docs/en/docs/css/custom.css | 5 ++ docs/en/docs/help-fastapi.md | 75 ++++++++++-------- 5 files changed, 197 insertions(+), 108 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/bug_report.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index d64ca6b40..000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: "[BUG]" -labels: bug -assignees: '' - ---- - -### Describe the bug - -Write here a clear and concise description of what the bug is. - -### To Reproduce - -Steps to reproduce the behavior with a minimum self-contained file. - -Replace each part with your own scenario: - -1. Create a file with: - -```Python -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -def read_root(): - return {"Hello": "World"} -``` - -3. Open the browser and call the endpoint `/`. -4. It returns a JSON with `{"Hello": "World"}`. -5. But I expected it to return `{"Hello": "Sara"}`. - -### Expected behavior - -Add a clear and concise description of what you expected to happen. - -### Screenshots - -If applicable, add screenshots to help explain your problem. - -### Environment - -- OS: [e.g. Linux / Windows / macOS] -- FastAPI Version [e.g. 0.3.0], get it with: - -```bash -python -c "import fastapi; print(fastapi.__version__)" -``` - -- Python version, get it with: - -```bash -python --version -``` - -### Additional context - -Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 15b94028e..75c02cc1f 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,26 +1,104 @@ --- name: Feature request about: Suggest an idea for this project -title: "[FEATURE]" +title: "" labels: enhancement assignees: '' --- -### Is your feature request related to a problem +### First check -Is your feature request related to a problem? +* [ ] I added a very descriptive title to this issue. +* [ ] I used the GitHub search to find a similar issue and didn't find it. +* [ ] I searched the FastAPI documentation, with the integrated search. +* [ ] I already searched in Google "How to X in FastAPI" and didn't find any information. +* [ ] I already read and followed all the tutorial in the docs and didn't find an answer. +* [ ] I already checked if it is not related to FastAPI but to [Pydantic](https://github.com/samuelcolvin/pydantic). +* [ ] I already checked if it is not related to FastAPI but to [Swagger UI](https://github.com/swagger-api/swagger-ui). +* [ ] I already checked if it is not related to FastAPI but to [ReDoc](https://github.com/Redocly/redoc). +* [ ] After submitting this, I commit to: + * Read open issues with questions until I find 2 issues where I can help someone and add a comment to help there. + * Or, I already hit the "watch" button in this repository to receive notifications and I commit to help at least 2 people that ask questions in the future. + * Implement a Pull Request for a confirmed bug. -Add a clear and concise description of what the problem is. Ex. I want to be able to [...] but I can't because [...] + + +### Example + +Here's a self-contained [minimal, reproducible, example](https://stackoverflow.com/help/minimal-reproducible-example) with my use case: + + + +```Python +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +def read_root(): + return {"Hello": "World"} +``` + +### Description + + + +* Open the browser and call the endpoint `/`. +* It returns a JSON with `{"Hello": "World"}`. +* I would like it to have an extra parameter to teleport me to the moon and back. ### The solution you would like -Add a clear and concise description of what you want to happen. + + +I would like it to have a `teleport_to_moon` parameter that defaults to `False`, and can be set to `True` to teleport me: + +```Python +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/", teleport_to_moon=True) +def read_root(): + return {"Hello": "World"} +``` ### Describe alternatives you've considered -Add a clear and concise description of any alternative solutions or features you've considered. + + +To wait for Space X moon travel plans to drop down long after they release them. But I would rather teleport. + +### Environment + +* OS: [e.g. Linux / Windows / macOS]: +* FastAPI Version [e.g. 0.3.0]: + +To know the FastAPI version use: + +```bash +python -c "import fastapi; print(fastapi.__version__)" +``` + +* Python version: + +To know the Python version use: + +```bash +python --version +``` ### Additional context -Add any other context or screenshots about the feature request here. + diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md index e7466133b..c49538916 100644 --- a/.github/ISSUE_TEMPLATE/question.md +++ b/.github/ISSUE_TEMPLATE/question.md @@ -1,24 +1,81 @@ --- -name: Question -about: Ask a question -title: "[QUESTION]" +name: Question or Problem +about: Ask a question or ask about a problem +title: "" labels: question -assignees: '' +assignees: "" --- ### First check +* [ ] I added a very descriptive title to this issue. * [ ] I used the GitHub search to find a similar issue and didn't find it. * [ ] I searched the FastAPI documentation, with the integrated search. * [ ] I already searched in Google "How to X in FastAPI" and didn't find any information. +* [ ] I already read and followed all the tutorial in the docs and didn't find an answer. +* [ ] I already checked if it is not related to FastAPI but to [Pydantic](https://github.com/samuelcolvin/pydantic). +* [ ] I already checked if it is not related to FastAPI but to [Swagger UI](https://github.com/swagger-api/swagger-ui). +* [ ] I already checked if it is not related to FastAPI but to [ReDoc](https://github.com/Redocly/redoc). +* [ ] After submitting this, I commit to one of: + * Read open issues with questions until I find 2 issues where I can help someone and add a comment to help there. + * I already hit the "watch" button in this repository to receive notifications and I commit to help at least 2 people that ask questions in the future. + * Implement a Pull Request for a confirmed bug. + + + +### Example + +Here's a self-contained, [minimal, reproducible, example](https://stackoverflow.com/help/minimal-reproducible-example) with my use case: + + + +```Python +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +def read_root(): + return {"Hello": "World"} +``` ### Description -How can I [...]? + -Is it possible to [...]? +* Open the browser and call the endpoint `/`. +* It returns a JSON with `{"Hello": "World"}`. +* But I expected it to return `{"Hello": "Sara"}`. + +### Environment + +* OS: [e.g. Linux / Windows / macOS]: +* FastAPI Version [e.g. 0.3.0]: + +To know the FastAPI version use: + +```bash +python -c "import fastapi; print(fastapi.__version__)" +``` + +* Python version: + +To know the Python version use: + +```bash +python --version +``` ### Additional context -Add any other context or screenshots about the question here. + diff --git a/docs/en/docs/css/custom.css b/docs/en/docs/css/custom.css index 94bdbed43..b7de5e34e 100644 --- a/docs/en/docs/css/custom.css +++ b/docs/en/docs/css/custom.css @@ -11,3 +11,8 @@ a.internal-link::after { */ content: "\00A0↪"; } + +/* Give space to lower icons so Gitter chat doesn't get on top of them */ +.md-footer-meta { + padding-bottom: 2em; +} diff --git a/docs/en/docs/help-fastapi.md b/docs/en/docs/help-fastapi.md index c1a7d918a..aed3ed6f6 100644 --- a/docs/en/docs/help-fastapi.md +++ b/docs/en/docs/help-fastapi.md @@ -12,28 +12,18 @@ And there are several ways to get help too. ## Star **FastAPI** in GitHub -You can "star" FastAPI in GitHub (clicking the star button at the top right): https://github.com/tiangolo/fastapi. +You can "star" FastAPI in GitHub (clicking the star button at the top right): https://github.com/tiangolo/fastapi. ⭐️ By adding a star, other users will be able to find it more easily and see that it has been already useful for others. ## Watch the GitHub repository for releases -You can "watch" FastAPI in GitHub (clicking the "watch" button at the top right): https://github.com/tiangolo/fastapi. +You can "watch" FastAPI in GitHub (clicking the "watch" button at the top right): https://github.com/tiangolo/fastapi. 👀 There you can select "Releases only". Doing it, you will receive notifications (in your email) whenever there's a new release (a new version) of **FastAPI** with bug fixes and new features. -## Join the chat - - - Join the chat at https://gitter.im/tiangolo/fastapi - - -Join the chat on Gitter: https://gitter.im/tiangolo/fastapi. - -There you can ask quick questions, help others, share ideas, etc. - ## Connect with the author You can connect with me (Sebastián Ramírez / `tiangolo`), the author. @@ -45,39 +35,32 @@ You can: * Follow me to see when I create a new Open Source project. * Follow me on **Twitter**. * Tell me how you use FastAPI (I love to hear that). - * Ask questions. + * Hear when I make announcements or release new tools. * Connect with me on **Linkedin**. - * Talk to me. - * Endorse me or recommend me :) -* Read what I write (or follow me) on **Medium**. - * Read other ideas, articles and tools I have created. - * Follow me to see when I publish something new. + * Hear when I make announcements or release new tools (although I use Twitter more often 🤷‍♂). +* Read what I write (or follow me) on **Dev.to** or **Medium**. + * Read other ideas, articles, and about tools I have created. + * Follow me to read when I publish something new. ## Tweet about **FastAPI** -Tweet about **FastAPI** and let me and others know why you like it. - -## Let me know how are you using **FastAPI** +Tweet about **FastAPI** and let me and others know why you like it. 🎉 I love to hear about how **FastAPI** is being used, what have you liked in it, in which project/company are you using it, etc. -You can let me know: - -* On **Twitter**. -* On **Linkedin**. -* On **Medium**. - ## Vote for FastAPI * Vote for **FastAPI** in Slant. +* Vote for **FastAPI** in AlternativeTo. +* Vote for **FastAPI** on awesome-rest. ## Help others with issues in GitHub -You can see existing issues and try and help others. +You can see existing issues and try and help others, most of the times they are questions that you might already know the answer for. 🤓 ## Watch the GitHub repository -You can "watch" FastAPI in GitHub (clicking the "watch" button at the top right): https://github.com/tiangolo/fastapi. +You can "watch" FastAPI in GitHub (clicking the "watch" button at the top right): https://github.com/tiangolo/fastapi. 👀 If you select "Watching" instead of "Releases only", you will receive notifications when someone creates a new issue. @@ -87,9 +70,10 @@ Then you can try and help them solving those issues. You can create a new issue in the GitHub repository, for example to: -* Report a bug/issue. +* Ask a question or ask about a problem. * Suggest a new feature. -* Ask a question. + +**Note**: if you create an issue then I'm going to ask you to also help others. 😉 ## Create a Pull Request @@ -100,12 +84,39 @@ You can + Join the chat at https://gitter.im/tiangolo/fastapi + + +Join the chat on Gitter: https://gitter.im/tiangolo/fastapi. + +There you can have quick conversations with others, help others, share ideas, etc. + +But have in mind that as it allows more "free conversation", it's easy to ask questions that are too general and more difficult to answer, so, you might not receive answers. + +In GitHub issues the template will guide to to write the right question so that you can more easily get a good answer, or even solve the problem yourself even before asking. And in GitHub I can make sure I always answer everything, even if it takes some time. I can't personally do that with the Gitter chat. 😅 + +Conversations in Gitter are also not as easily searchable as in GitHub, so questions and answers might get lost in the conversation. + +On the other side, there's more than 1000 people in the chat, so there's a high chance you'll find someone to talk to there, almost all the time. 😄 + ## Sponsor the author You can also financially support the author (me) through GitHub sponsors. There you could buy me a coffee ☕️ to say thanks 😄. +## Sponsor the tools that power FastAPI + +As you have seen in the documentation, FastAPI stands on the shoulders of giants, Starlette and Pydantic. + +You can also sponsor: + +* Samuel Colvin (Pydantic) +* Encode (Starlette, Uvicorn) + --- -Thanks! +Thanks! 🚀 From 543ef7753aff639ad3aed7c153e42f719e361d38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 7 Jun 2020 22:02:36 +0200 Subject: [PATCH 04/82] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index d1ee4584c..583304088 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Update new issue templates and docs: [Help FastAPI - Get Help](https://fastapi.tiangolo.com/help-fastapi/). PR [#1531](https://github.com/tiangolo/fastapi/pull/1531). * Update GitHub action issue-manager. PR [#1520](https://github.com/tiangolo/fastapi/pull/1520). * Add new links: * **English articles**: From 2f478eeca643c5c66370676cb521397479508e69 Mon Sep 17 00:00:00 2001 From: Ingmar Steen Date: Thu, 11 Jun 2020 23:53:19 +0200 Subject: [PATCH 05/82] =?UTF-8?q?=E2=9C=A8=20Add=20support=20for=20ASGI=20?= =?UTF-8?q?root=5Fpath=20for=20openapi=20docs=20(#1199)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Use ASGI root_path when it is provided and openapi_prefix is empty. * Strip trailing slashes from root_path. * Please mypy. * Fix extending openapi test. * 📝 Add docs and tutorial for using root_path behind a proxy * ♻️ Refactor application root_path logic, use root_path, deprecate openapi_prefix * ✅ Add tests for Behind a Proxy with root_path * ♻️ Refactor test * 📝 Update/add docs for Sub-applications and Behind a Proxy * 📝 Update Extending OpenAPI with openapi_prefix parameter * ✅ Add test for deprecated openapi_prefix Co-authored-by: Sebastián Ramírez --- docs/en/docs/advanced/behind-a-proxy.md | 281 ++++++++++++++++++ docs/en/docs/advanced/extending-openapi.md | 15 +- .../docs/advanced/sub-applications-proxy.md | 100 ------- docs/en/docs/advanced/sub-applications.md | 73 +++++ .../img/tutorial/behind-a-proxy/image01.png | Bin 0 -> 60301 bytes .../img/tutorial/behind-a-proxy/image02.png | Bin 0 -> 29123 bytes docs/en/mkdocs.yml | 3 +- docs_src/behind_a_proxy/tutorial001.py | 8 + docs_src/behind_a_proxy/tutorial002.py | 8 + docs_src/extending_openapi/tutorial001.py | 3 +- docs_src/sub_applications/tutorial001.py | 2 +- fastapi/applications.py | 33 +- tests/test_deprecated_openapi_prefix.py | 43 +++ .../test_behind_a_proxy/__init__.py | 0 .../test_behind_a_proxy/test_tutorial001.py | 36 +++ .../test_behind_a_proxy/test_tutorial002.py | 36 +++ 16 files changed, 527 insertions(+), 114 deletions(-) create mode 100644 docs/en/docs/advanced/behind-a-proxy.md delete mode 100644 docs/en/docs/advanced/sub-applications-proxy.md create mode 100644 docs/en/docs/advanced/sub-applications.md create mode 100644 docs/en/docs/img/tutorial/behind-a-proxy/image01.png create mode 100644 docs/en/docs/img/tutorial/behind-a-proxy/image02.png create mode 100644 docs_src/behind_a_proxy/tutorial001.py create mode 100644 docs_src/behind_a_proxy/tutorial002.py create mode 100644 tests/test_deprecated_openapi_prefix.py create mode 100644 tests/test_tutorial/test_behind_a_proxy/__init__.py create mode 100644 tests/test_tutorial/test_behind_a_proxy/test_tutorial001.py create mode 100644 tests/test_tutorial/test_behind_a_proxy/test_tutorial002.py diff --git a/docs/en/docs/advanced/behind-a-proxy.md b/docs/en/docs/advanced/behind-a-proxy.md new file mode 100644 index 000000000..660e374a4 --- /dev/null +++ b/docs/en/docs/advanced/behind-a-proxy.md @@ -0,0 +1,281 @@ +# Behind a Proxy + +In some situations, you might need to use a **proxy** server like Traefik or Nginx with a configuration that adds an extra path prefix that is not seen by your application. + +In these cases you can use `root_path` to configure your application. + +The `root_path` is a mechanism provided by the ASGI specification (that FastAPI is built on, through Starlette). + +The `root_path` is used to handle these specific cases. + +And it's also used internally when mounting sub-applications. + +## Proxy with a stripped path prefix + +Having a proxy with a stripped path prefix, in this case, means that you could declare a path at `/app` in your code, but then, you add a layer on top (the proxy) that would put your **FastAPI** application under a path like `/api/v1`. + +In this case, the original path `/app` would actually be served at `/api/v1/app`. + +Even though all your code is written assuming there's just `/app`. + +And the proxy would be **"stripping"** the **path prefix** on the fly before transmitting the request to Uvicorn, keep your application convinced that it is serving at `/app`, so that you don't have to update all your code to include the prefix `/api/v1`. + +Up to here, everything would work as normally. + +But then, when you open the integrated docs UI (the frontend), it would expect to get the OpenAPI schema at `/openapi.json`, instead of `/api/v1/openapi.json`. + +So, the frontend (that runs in the browser) would try to reach `/openapi.json` and wouldn't be able to get the OpenAPI schema. + +Because we have a proxy with a path prefix of `/api/v1` for our app, the frontend needs to fetch the OpenAPI schema at `/api/v1/openapi.json`. + +```mermaid +graph LR + +browser("Browser") +proxy["Proxy on http://0.0.0.0:9999/api/v1/app"] +server["Server on http://127.0.0.1:8000/app"] + +browser --> proxy +proxy --> server +``` + +!!! tip + The IP `0.0.0.0` is commonly used to mean that the program listens on all the IPs available in that machine/server. + +The docs UI would also need that the JSON payload with the OpenAPI schema has the path defined as `/api/v1/app` (behind the proxy) instead of `/app`. For example, something like: + +```JSON hl_lines="5" +{ + "openapi": "3.0.2", + // More stuff here + "paths": { + "/api/v1/app": { + // More stuff here + } + } +} +``` + +In this example, the "Proxy" could be something like **Traefik**. And the server would be something like **Uvicorn**, running your FastAPI application. + +### Providing the `root_path` + +To achieve this, you can use the command line option `--root-path` like: + +
+ +```console +$ uvicorn main:app --root-path /api/v1 + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +If you use Hypercorn, it also has the option `--root-path`. + +!!! note "Technical Details" + The ASGI specification defines a `root_path` for this use case. + + And the `--root-path` command line option provides that `root_path`. + +### Checking the current `root_path` + +You can get the current `root_path` used by your application for each request, it is part of the `scope` dictionary (that's part of the ASGI spec). + +Here we are including it in the message just for demonstration purposes. + +```Python hl_lines="8" +{!../../../docs_src/behind_a_proxy/tutorial001.py!} +``` + +Then, if you start Uvicorn with: + +
+ +```console +$ uvicorn main:app --root-path /api/v1 + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +The response would be something like: + +```JSON +{ + "message": "Hello World", + "root_path": "/api/v1" +} +``` + +### Setting the `root_path` in the FastAPI app + +Alternatively, if you don't have a way to provide a command line option like `--root-path` or equivalent, you can set the `root_path` parameter when creating your FastAPI app: + +```Python hl_lines="3" +{!../../../docs_src/behind_a_proxy/tutorial002.py!} +``` + +Passing the `root_path` to `FastAPI` would be the equivalent of passing the `--root-path` command line option to Uvicorn or Hypercorn. + +### About `root_path` + +Have in mind that the server (Uvicorn) won't use that `root_path` for anything else than passing it to the app. + +But if you go with your browser to http://127.0.0.1:8000/app you will see the normal response: + +```JSON +{ + "message": "Hello World", + "root_path": "/api/v1" +} +``` + +So, it won't expect to be accessed at `http://127.0.0.1:8000/api/v1/app`. + +Uvicorn will expect the proxy to access Uvicorn at `http://127.0.0.1:8000/app`, and then it would be the proxy's responsibility to add the extra `/api/v1` prefix on top. + +## About proxies with a stripped path prefix + +Have in mind that a proxy with stripped path prefix is only one of the ways to configure it. + +Probably in many cases the default will be that the proxy doesn't have a stripped path prefix. + +In a case like that (without a stripped path prefix), the proxy would listen on something like `https://myawesomeapp.com`, and then if the browser goes to `https://myawesomeapp.com/api/v1/app` and your server (e.g. Uvicorn) listens on `http://127.0.0.1:8000` the proxy (without a stripped path prefix) would access Uvicorn at the same path: `http://127.0.0.1:8000/api/v1/app`. + +## Testing locally with Traefik + +You can easily run the experiment locally with a stripped path prefix using Traefik. + +Download Traefik, it's a single binary, you can extract the compressed file and run it directly from the terminal. + +Then create a file `traefik.toml` with: + +```TOML hl_lines="3" +[entryPoints] + [entryPoints.http] + address = ":9999" + +[providers] + [providers.file] + filename = "routes.toml" +``` + +This tells Traefik to listen on port 9999 and to use another file `routes.toml`. + +!!! tip + We are using port 9999 instead of the standard HTTP port 80 so that you don't have to run it with admin (`sudo`) privileges. + +Now create that other file `routes.toml`: + +```TOML hl_lines="5 12 20" +[http] + [http.middlewares] + + [http.middlewares.api-stripprefix.stripPrefix] + prefixes = ["/api/v1"] + + [http.routers] + + [http.routers.app-http] + entryPoints = ["http"] + service = "app" + rule = "PathPrefix(`/api/v1`)" + middlewares = ["api-stripprefix"] + + [http.services] + + [http.services.app] + [http.services.app.loadBalancer] + [[http.services.app.loadBalancer.servers]] + url = "http://127.0.0.1:8000" +``` + +This file configures Traefik to use the path prefix `/api/v1`. + +And then it will redirect its requests to your Uvicorn running on `http://127.0.0.1:8000`. + +Now start Traefik: + +
+ +```console +$ ./traefik --configFile=traefik.toml + +INFO[0000] Configuration loaded from file: /home/user/awesomeapi/traefik.toml +``` + +
+ +And now start your app with Uvicorn, using the `--root-path` option: + +
+ +```console +$ uvicorn main:app --root-path /api/v1 + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +### Check the responses + +Now, if you go to the URL with the port for Uvicorn: http://127.0.0.1:8000/app, you will see the normal response: + +```JSON +{ + "message": "Hello World", + "root_path": "/api/v1" +} +``` + +!!! tip + Notice that even though you are accessing it at `http://127.0.0.1:8000/app` it shows the `root_path` of `/api/v1`, taken from the option `--root-path`. + +And now open the URL with the port for Traefik, including the path prefix: http://127.0.0.1:9999/api/vi/app. + +We get the same response: + +```JSON +{ + "message": "Hello World", + "root_path": "/api/v1" +} +``` + +but this time at the URL with the prefix path provided by the proxy: `/api/v1`. + +Of course, the idea here is that everyone would access the app through the proxy, so the version with the path prefix `/app/v1` is the "correct" one. + +And the version without the path prefix (`http://127.0.0.1:8000/app`), provided by Uvicorn directly, would be exclusively for the _proxy_ (Traefik) to access it. + +That demonstrates how the Proxy (Traefik) uses the path prefix and how the server (Uvicorn) uses the `root_path` from the option `--root-path`. + +### Check the docs UI + +But here's the fun part. ✨ + +The "official" way to access the app would be through the proxy with the path prefix that we defined. So, as we would expect, if you try the docs UI served by Uvicorn directly, without the path prefix in the URL, it won't work, because it expects to be accessed through the proxy. + +You can check it at http://127.0.0.1:8000/docs: + + + +But if we access the docs UI at the "official" URL using the proxy, at `/api/v1/docs`, it works correctly! 🎉 + +Right as we wanted it. ✔️ + +This is because FastAPI uses this `root_path` internally to tell the docs UI to use the URL for OpenAPI with the path prefix provided by `root_path`. + +You can check it at http://127.0.0.1:9999/api/v1/docs: + + + +## Mounting a sub-application + +If you need to mount a sub-application (as described in [Sub Applications - Mounts](./sub-applications.md){.internal-link target=_blank}) while also using a proxy with `root_path`, you can do it normally, as you would expect. + +FastAPI will internally use the `root_path` smartly, so it will just work. ✨ diff --git a/docs/en/docs/advanced/extending-openapi.md b/docs/en/docs/advanced/extending-openapi.md index f98be49a6..30cd857d5 100644 --- a/docs/en/docs/advanced/extending-openapi.md +++ b/docs/en/docs/advanced/extending-openapi.md @@ -52,15 +52,22 @@ First, write all your **FastAPI** application as normally: Then, use the same utility function to generate the OpenAPI schema, inside a `custom_openapi()` function: -```Python hl_lines="2 15 16 17 18 19 20" +```Python hl_lines="2 15 16 17 18 19 20 21" {!../../../docs_src/extending_openapi/tutorial001.py!} ``` +!!! tip + The `openapi_prefix` will contain any prefix needed for the generated OpenAPI *path operations*. + + FastAPI will automatically use the `root_path` to pass it in the `openapi_prefix`. + + But the important thing is that your function should receive that parameter `openapi_prefix` and pass it along. + ### Modify the OpenAPI schema Now you can add the ReDoc extension, adding a custom `x-logo` to the `info` "object" in the OpenAPI schema: -```Python hl_lines="21 22 23" +```Python hl_lines="22 23 24" {!../../../docs_src/extending_openapi/tutorial001.py!} ``` @@ -72,7 +79,7 @@ That way, your application won't have to generate the schema every time a user o It will be generated only once, and then the same cached schema will be used for the next requests. -```Python hl_lines="13 14 24 25" +```Python hl_lines="13 14 25 26" {!../../../docs_src/extending_openapi/tutorial001.py!} ``` @@ -80,7 +87,7 @@ It will be generated only once, and then the same cached schema will be used for Now you can replace the `.openapi()` method with your new function. -```Python hl_lines="28" +```Python hl_lines="29" {!../../../docs_src/extending_openapi/tutorial001.py!} ``` diff --git a/docs/en/docs/advanced/sub-applications-proxy.md b/docs/en/docs/advanced/sub-applications-proxy.md deleted file mode 100644 index 03a7f9446..000000000 --- a/docs/en/docs/advanced/sub-applications-proxy.md +++ /dev/null @@ -1,100 +0,0 @@ -# Sub Applications - Behind a Proxy, Mounts - -There are at least two situations where you could need to create your **FastAPI** application using some specific paths. - -But then you need to set them up to be served with a path prefix. - -It could happen if you have a: - -* **Proxy** server. -* You are "**mounting**" a FastAPI application inside another FastAPI application (or inside another ASGI application, like Starlette). - -## Proxy - -Having a proxy in this case means that you could declare a path at `/app`, but then, you could need to add a layer on top (the Proxy) that would put your **FastAPI** application under a path like `/api/v1`. - -In this case, the original path `/app` will actually be served at `/api/v1/app`. - -Even though your application "thinks" it is serving at `/app`. - -And the Proxy could be re-writing the path "on the fly" to keep your application convinced that it is serving at `/app`. - -Up to here, everything would work as normally. - -But then, when you open the integrated docs, they would expect to get the OpenAPI schema at `/openapi.json`, instead of `/api/v1/openapi.json`. - -So, the frontend (that runs in the browser) would try to reach `/openapi.json` and wouldn't be able to get the OpenAPI schema. - -So, it's needed that the frontend looks for the OpenAPI schema at `/api/v1/openapi.json`. - -And it's also needed that the returned JSON OpenAPI schema has the defined path at `/api/v1/app` (behind the proxy) instead of `/app`. - ---- - -For these cases, you can declare an `openapi_prefix` parameter in your `FastAPI` application. - -See the section below, about "mounting", for an example. - -## Mounting a **FastAPI** application - -"Mounting" means adding a complete "independent" application in a specific path, that then takes care of handling all the sub-paths. - -You could want to do this if you have several "independent" applications that you want to separate, having their own independent OpenAPI schema and user interfaces. - -### Top-level application - -First, create the main, top-level, **FastAPI** application, and its *path operations*: - -```Python hl_lines="3 6 7 8" -{!../../../docs_src/sub_applications/tutorial001.py!} -``` - -### Sub-application - -Then, create your sub-application, and its *path operations*. - -This sub-application is just another standard FastAPI application, but this is the one that will be "mounted". - -When creating the sub-application, use the parameter `openapi_prefix`. In this case, with a prefix of `/subapi`: - -```Python hl_lines="11 14 15 16" -{!../../../docs_src/sub_applications/tutorial001.py!} -``` - -### Mount the sub-application - -In your top-level application, `app`, mount the sub-application, `subapi`. - -Here you need to make sure you use the same path that you used for the `openapi_prefix`, in this case, `/subapi`: - -```Python hl_lines="11 19" -{!../../../docs_src/sub_applications/tutorial001.py!} -``` - -## Check the automatic API docs - -Now, run `uvicorn`, if your file is at `main.py`, it would be: - -
- -```console -$ uvicorn main:app --reload - -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -``` - -
- -And open the docs at http://127.0.0.1:8000/docs. - -You will see the automatic API docs for the main app, including only its own paths: - - - -And then, open the docs for the sub-application, at http://127.0.0.1:8000/subapi/docs. - -You will see the automatic API docs for the sub-application, including only its own sub-paths, with their correct prefix: - - - -If you try interacting with any of the two user interfaces, they will work, because the browser will be able to talk to the correct path (or sub-path). diff --git a/docs/en/docs/advanced/sub-applications.md b/docs/en/docs/advanced/sub-applications.md new file mode 100644 index 000000000..68d5790db --- /dev/null +++ b/docs/en/docs/advanced/sub-applications.md @@ -0,0 +1,73 @@ +# Sub Applications - Mounts + +If you need to have two independent FastAPI applications, with their own independent OpenAPI and their own docs UIs, you can have a main app and "mount" one (or more) sub-application(s). + +## Mounting a **FastAPI** application + +"Mounting" means adding a completely "independent" application in a specific path, that then takes care of handling all everything under that path, with the _path operations_ declared in that sub-application. + +### Top-level application + +First, create the main, top-level, **FastAPI** application, and its *path operations*: + +```Python hl_lines="3 6 7 8" +{!../../../docs_src/sub_applications/tutorial001.py!} +``` + +### Sub-application + +Then, create your sub-application, and its *path operations*. + +This sub-application is just another standard FastAPI application, but this is the one that will be "mounted": + +```Python hl_lines="11 14 15 16" +{!../../../docs_src/sub_applications/tutorial001.py!} +``` + +### Mount the sub-application + +In your top-level application, `app`, mount the sub-application, `subapi`. + +In this case, it will be mounted at the path `/subapi`: + +```Python hl_lines="11 19" +{!../../../docs_src/sub_applications/tutorial001.py!} +``` + +### Check the automatic API docs + +Now, run `uvicorn` with the main app, if your file is `main.py`, it would be: + +
+ +```console +$ uvicorn main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +And open the docs at http://127.0.0.1:8000/docs. + +You will see the automatic API docs for the main app, including only its own _path operations_: + + + +And then, open the docs for the sub-application, at http://127.0.0.1:8000/subapi/docs. + +You will see the automatic API docs for the sub-application, including only its own _path operations_, all under the correct sub-path prefix `/subapi`: + + + +If you try interacting with any of the two user interfaces, they will work correctly, because the browser will be able to talk to each specific app or sub-app. + +### Technical Details: `root_path` + +When you mount a sub-application as described above, FastAPI will take care of communicating the mount path for the sub-application using a mechanism from the ASGI specification called a `root_path`. + +That way, the sub-application will know to use that path prefix for the docs UI. + +And the sub-application could also have its own mounted sub-applications and everything would work correctly, because FastAPI handles all these `root_path`s automatically. + +You will learn more about the `root_path` and how to use it explicitly in the section about [Behind a Proxy](./behind-a-proxy.md){.internal-link target=_blank}. diff --git a/docs/en/docs/img/tutorial/behind-a-proxy/image01.png b/docs/en/docs/img/tutorial/behind-a-proxy/image01.png new file mode 100644 index 0000000000000000000000000000000000000000..4ceae4421988a968a2903e55aacd66fc82741742 GIT binary patch literal 60301 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 RN&w_Q8Y()<#WyU&{tLI?+YkT% literal 0 HcmV?d00001 diff --git a/docs/en/docs/img/tutorial/behind-a-proxy/image02.png b/docs/en/docs/img/tutorial/behind-a-proxy/image02.png new file mode 100644 index 0000000000000000000000000000000000000000..8012031401c8365f9ff01c92479e05854bc8bf00 GIT binary patch literal 29123 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 None: self.default_response_class = default_response_class @@ -68,7 +70,15 @@ class FastAPI(Starlette): self.description = description self.version = version self.openapi_url = openapi_url - self.openapi_prefix = openapi_prefix.rstrip("/") + # TODO: remove when discarding the openapi_prefix parameter + if openapi_prefix: + logger.warning( + '"openapi_prefix" has been deprecated in favor of "root_path", which ' + "follows more closely the ASGI standard, is simpler, and more " + "automatic. Check the docs at " + "https://fastapi.tiangolo.com/advanced/sub-applications-proxy/" + ) + self.root_path = root_path or openapi_prefix self.docs_url = docs_url self.redoc_url = redoc_url self.swagger_ui_oauth2_redirect_url = swagger_ui_oauth2_redirect_url @@ -84,7 +94,7 @@ class FastAPI(Starlette): self.openapi_schema: Optional[Dict[str, Any]] = None self.setup() - def openapi(self) -> Dict: + def openapi(self, openapi_prefix: str = "") -> Dict: if not self.openapi_schema: self.openapi_schema = get_openapi( title=self.title, @@ -92,7 +102,7 @@ class FastAPI(Starlette): openapi_version=self.openapi_version, description=self.description, routes=self.routes, - openapi_prefix=self.openapi_prefix, + openapi_prefix=openapi_prefix, ) return self.openapi_schema @@ -100,17 +110,22 @@ class FastAPI(Starlette): if self.openapi_url: async def openapi(req: Request) -> JSONResponse: - return JSONResponse(self.openapi()) + root_path = req.scope.get("root_path", "").rstrip("/") + return JSONResponse(self.openapi(root_path)) self.add_route(self.openapi_url, openapi, include_in_schema=False) - openapi_url = self.openapi_prefix + self.openapi_url if self.openapi_url and self.docs_url: async def swagger_ui_html(req: Request) -> HTMLResponse: + root_path = req.scope.get("root_path", "").rstrip("/") + openapi_url = root_path + self.openapi_url + oauth2_redirect_url = self.swagger_ui_oauth2_redirect_url + if oauth2_redirect_url: + oauth2_redirect_url = root_path + oauth2_redirect_url return get_swagger_ui_html( openapi_url=openapi_url, title=self.title + " - Swagger UI", - oauth2_redirect_url=self.swagger_ui_oauth2_redirect_url, + oauth2_redirect_url=oauth2_redirect_url, init_oauth=self.swagger_ui_init_oauth, ) @@ -129,6 +144,8 @@ class FastAPI(Starlette): if self.openapi_url and self.redoc_url: async def redoc_html(req: Request) -> HTMLResponse: + root_path = req.scope.get("root_path", "").rstrip("/") + openapi_url = root_path + self.openapi_url return get_redoc_html( openapi_url=openapi_url, title=self.title + " - ReDoc" ) @@ -140,6 +157,8 @@ class FastAPI(Starlette): ) async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + if self.root_path: + scope["root_path"] = self.root_path if AsyncExitStack: async with AsyncExitStack() as stack: scope["fastapi_astack"] = stack diff --git a/tests/test_deprecated_openapi_prefix.py b/tests/test_deprecated_openapi_prefix.py new file mode 100644 index 000000000..df7e69bd5 --- /dev/null +++ b/tests/test_deprecated_openapi_prefix.py @@ -0,0 +1,43 @@ +from fastapi import FastAPI, Request +from fastapi.testclient import TestClient + +app = FastAPI(openapi_prefix="/api/v1") + + +@app.get("/app") +def read_main(request: Request): + return {"message": "Hello World", "root_path": request.scope.get("root_path")} + + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/api/v1/app": { + "get": { + "summary": "Read Main", + "operationId": "read_main_app_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + } + }, +} + + +def test_openapi(): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == openapi_schema + + +def test_main(): + response = client.get("/app") + assert response.status_code == 200 + assert response.json() == {"message": "Hello World", "root_path": "/api/v1"} diff --git a/tests/test_tutorial/test_behind_a_proxy/__init__.py b/tests/test_tutorial/test_behind_a_proxy/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_tutorial/test_behind_a_proxy/test_tutorial001.py b/tests/test_tutorial/test_behind_a_proxy/test_tutorial001.py new file mode 100644 index 000000000..8b3b526ed --- /dev/null +++ b/tests/test_tutorial/test_behind_a_proxy/test_tutorial001.py @@ -0,0 +1,36 @@ +from fastapi.testclient import TestClient + +from behind_a_proxy.tutorial001 import app + +client = TestClient(app, root_path="/api/v1") + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/api/v1/app": { + "get": { + "summary": "Read Main", + "operationId": "read_main_app_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + } + }, +} + + +def test_openapi(): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == openapi_schema + + +def test_main(): + response = client.get("/app") + assert response.status_code == 200 + assert response.json() == {"message": "Hello World", "root_path": "/api/v1"} diff --git a/tests/test_tutorial/test_behind_a_proxy/test_tutorial002.py b/tests/test_tutorial/test_behind_a_proxy/test_tutorial002.py new file mode 100644 index 000000000..0a889c469 --- /dev/null +++ b/tests/test_tutorial/test_behind_a_proxy/test_tutorial002.py @@ -0,0 +1,36 @@ +from fastapi.testclient import TestClient + +from behind_a_proxy.tutorial002 import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/api/v1/app": { + "get": { + "summary": "Read Main", + "operationId": "read_main_app_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + } + }, +} + + +def test_openapi(): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == openapi_schema + + +def test_main(): + response = client.get("/app") + assert response.status_code == 200 + assert response.json() == {"message": "Hello World", "root_path": "/api/v1"} From da7826b0ebdf1cf816ebab3bb0b5143dbce4fa39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 12 Jun 2020 00:05:17 +0200 Subject: [PATCH 06/82] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 583304088..ba40725eb 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,14 @@ ## Latest changes +* Add support for ASGI `root_path`: + * Use `root_path` internally for mounted applications, so that OpenAPI and the docs UI works automatically without extra configurations and parameters. + * Add new `root_path` parameter for `FastAPI` applications to provide it in cases where it can be set with the command line (e.g. for Uvicorn and Hypercorn, with the parameter `--root-path`). + * Deprecate `openapi_prefix` parameter in favor of the new `root_path` parameter. + * Add new/updated docs for [Sub Applications - Mounts](https://fastapi.tiangolo.com/advanced/sub-applications/), without `openapi_prefix` (as it is now handled automatically). + * Add new/updated docs for [Behind a Proxy](https://fastapi.tiangolo.com/advanced/behind-a-proxy/), including how to setup a local testing proxy with Traefik and using `root_path`. + * Update docs for [Extending OpenAPI](https://fastapi.tiangolo.com/advanced/extending-openapi/) with the new `openapi_prefix` parameter passed (internally generated from `root_path`). + * Original PR [#1199](https://github.com/tiangolo/fastapi/pull/1199) by [@iksteen](https://github.com/iksteen). * Update new issue templates and docs: [Help FastAPI - Get Help](https://fastapi.tiangolo.com/help-fastapi/). PR [#1531](https://github.com/tiangolo/fastapi/pull/1531). * Update GitHub action issue-manager. PR [#1520](https://github.com/tiangolo/fastapi/pull/1520). * Add new links: From 072c2bc7f9d4900c8afe75f2775942a63325c569 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 12 Jun 2020 00:22:17 +0200 Subject: [PATCH 07/82] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.56.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index ba40725eb..9b26f2c53 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,8 @@ ## Latest changes +## 0.56.0 + * Add support for ASGI `root_path`: * Use `root_path` internally for mounted applications, so that OpenAPI and the docs UI works automatically without extra configurations and parameters. * Add new `root_path` parameter for `FastAPI` applications to provide it in cases where it can be set with the command line (e.g. for Uvicorn and Hypercorn, with the parameter `--root-path`). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index eb083ec0e..a0244bfaf 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.55.1" +__version__ = "0.56.0" from starlette import status From 2d9bb640479fc516850a93c4b81c5a434a4a7e02 Mon Sep 17 00:00:00 2001 From: Cesare De Cal Date: Fri, 12 Jun 2020 18:53:52 +0200 Subject: [PATCH 08/82] =?UTF-8?q?=F0=9F=8C=90=20Generated=20new=20translat?= =?UTF-8?q?ion=20directory=20to=20support=20Italian=20docs=20(#1557)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Generated new translation directory to support Italian docs * ⬆️ Upgrade/pin pytest to >= 5.4.3 Co-authored-by: Sebastián Ramírez --- docs/en/mkdocs.yml | 1 + docs/es/mkdocs.yml | 1 + docs/it/docs/index.md | 447 ++++++++++++++++++++++++++++++++++++++++++ docs/it/mkdocs.yml | 66 +++++++ docs/pt/mkdocs.yml | 1 + docs/zh/mkdocs.yml | 1 + pyproject.toml | 2 +- 7 files changed, 518 insertions(+), 1 deletion(-) create mode 100644 docs/it/docs/index.md create mode 100644 docs/it/mkdocs.yml diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index ee500318e..684207bab 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -22,6 +22,7 @@ nav: - Languages: - en: / - es: /es/ + - it: /it/ - pt: /pt/ - zh: /zh/ - features.md diff --git a/docs/es/mkdocs.yml b/docs/es/mkdocs.yml index 1d4210530..ba07f265e 100644 --- a/docs/es/mkdocs.yml +++ b/docs/es/mkdocs.yml @@ -22,6 +22,7 @@ nav: - Languages: - en: / - es: /es/ + - it: /it/ - pt: /pt/ - zh: /zh/ - features.md diff --git a/docs/it/docs/index.md b/docs/it/docs/index.md new file mode 100644 index 000000000..20dd403ab --- /dev/null +++ b/docs/it/docs/index.md @@ -0,0 +1,447 @@ + +{!../../../docs/missing-translation.md!} + + +

+ FastAPI +

+

+ FastAPI framework, high performance, easy to learn, fast to code, ready for production +

+

+ + Build Status + + + Coverage + + + Package version + + + Join the chat at https://gitter.im/tiangolo/fastapi + +

+ +--- + +**Documentation**: https://fastapi.tiangolo.com + +**Source Code**: https://github.com/tiangolo/fastapi + +--- + +FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.6+ based on standard Python type hints. + +The key features are: + +* **Fast**: Very high performance, on par with **NodeJS** and **Go** (thanks to Starlette and Pydantic). [One of the fastest Python frameworks available](#performance). + +* **Fast to code**: Increase the speed to develop features by about 200% to 300%. * +* **Fewer bugs**: Reduce about 40% of human (developer) induced errors. * +* **Intuitive**: Great editor support. Completion everywhere. Less time debugging. +* **Easy**: Designed to be easy to use and learn. Less time reading docs. +* **Short**: Minimize code duplication. Multiple features from each parameter declaration. Fewer bugs. +* **Robust**: Get production-ready code. With automatic interactive documentation. +* **Standards-based**: Based on (and fully compatible with) the open standards for APIs: OpenAPI (previously known as Swagger) and JSON Schema. + +* estimation based on tests on an internal development team, building production applications. + +## Opinions + +"_[...] I'm using **FastAPI** a ton these days. [...] I'm actually planning to use it for all of my team's **ML services at Microsoft**. Some of them are getting integrated into the core **Windows** product and some **Office** products._" + +
Kabir Khan - Microsoft (ref)
+ +--- + +"_We adopted the **FastAPI** library to spawn a **REST** server that can be queried to obtain **predictions**. [for Ludwig]_" + +
Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - Uber (ref)
+ +--- + +"_**Netflix** is pleased to announce the open-source release of our **crisis management** orchestration framework: **Dispatch**! [built with **FastAPI**]_" + +
Kevin Glisson, Marc Vilanova, Forest Monsen - Netflix (ref)
+ +--- + +"_I’m over the moon excited about **FastAPI**. It’s so fun!_" + +
Brian Okken - Python Bytes podcast host (ref)
+ +--- + +"_Honestly, what you've built looks super solid and polished. In many ways, it's what I wanted **Hug** to be - it's really inspiring to see someone build that._" + +
Timothy Crosley - Hug creator (ref)
+ +--- + +"_If you're looking to learn one **modern framework** for building REST APIs, check out **FastAPI** [...] It's fast, easy to use and easy to learn [...]_" + +"_We've switched over to **FastAPI** for our **APIs** [...] I think you'll like it [...]_" + +
Ines Montani - Matthew Honnibal - Explosion AI founders - spaCy creators (ref) - (ref)
+ +--- + +## **Typer**, the FastAPI of CLIs + + + +If you are building a CLI app to be used in the terminal instead of a web API, check out **Typer**. + +**Typer** is FastAPI's little sibling. And it's intended to be the **FastAPI of CLIs**. ⌨️ 🚀 + +## Requirements + +Python 3.6+ + +FastAPI stands on the shoulders of giants: + +* Starlette for the web parts. +* Pydantic for the data parts. + +## Installation + +
+ +```console +$ pip install fastapi + +---> 100% +``` + +
+ +You will also need an ASGI server, for production such as Uvicorn or Hypercorn. + +
+ +```console +$ pip install uvicorn + +---> 100% +``` + +
+ +## Example + +### Create it + +* Create a file `main.py` with: + +```Python +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: str = None): + return {"item_id": item_id, "q": q} +``` + +
+Or use async def... + +If your code uses `async` / `await`, use `async def`: + +```Python hl_lines="7 12" +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: str = None): + return {"item_id": item_id, "q": q} +``` + +**Note**: + +If you don't know, check the _"In a hurry?"_ section about `async` and `await` in the docs. + +
+ +### Run it + +Run the server with: + +
+ +```console +$ uvicorn main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +INFO: Started reloader process [28720] +INFO: Started server process [28722] +INFO: Waiting for application startup. +INFO: Application startup complete. +``` + +
+ +
+About the command uvicorn main:app --reload... + +The command `uvicorn main:app` refers to: + +* `main`: the file `main.py` (the Python "module"). +* `app`: the object created inside of `main.py` with the line `app = FastAPI()`. +* `--reload`: make the server restart after code changes. Only do this for development. + +
+ +### Check it + +Open your browser at http://127.0.0.1:8000/items/5?q=somequery. + +You will see the JSON response as: + +```JSON +{"item_id": 5, "q": "somequery"} +``` + +You already created an API that: + +* Receives HTTP requests in the _paths_ `/` and `/items/{item_id}`. +* Both _paths_ take `GET` operations (also known as HTTP _methods_). +* The _path_ `/items/{item_id}` has a _path parameter_ `item_id` that should be an `int`. +* The _path_ `/items/{item_id}` has an optional `str` _query parameter_ `q`. + +### Interactive API docs + +Now go to http://127.0.0.1:8000/docs. + +You will see the automatic interactive API documentation (provided by Swagger UI): + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) + +### Alternative API docs + +And now, go to http://127.0.0.1:8000/redoc. + +You will see the alternative automatic documentation (provided by ReDoc): + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) + +## Example upgrade + +Now modify the file `main.py` to receive a body from a `PUT` request. + +Declare the body using standard Python types, thanks to Pydantic. + +```Python hl_lines="2 7 8 9 10 23 24 25" +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + price: float + is_offer: bool = None + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +def read_item(item_id: int, q: str = 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} +``` + +The server should reload automatically (because you added `--reload` to the `uvicorn` command above). + +### Interactive API docs upgrade + +Now go to http://127.0.0.1:8000/docs. + +* The interactive API documentation will be automatically updated, including the new body: + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) + +* Click on the button "Try it out", it allows you to fill the parameters and directly interact with the API: + +![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) + +* Then click on the "Execute" button, the user interface will communicate with your API, send the parameters, get the results and show them on the screen: + +![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) + +### Alternative API docs upgrade + +And now, go to http://127.0.0.1:8000/redoc. + +* The alternative documentation will also reflect the new query parameter and body: + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) + +### Recap + +In summary, you declare **once** the types of parameters, body, etc. as function parameters. + +You do that with standard modern Python types. + +You don't have to learn a new syntax, the methods or classes of a specific library, etc. + +Just standard **Python 3.6+**. + +For example, for an `int`: + +```Python +item_id: int +``` + +or for a more complex `Item` model: + +```Python +item: Item +``` + +...and with that single declaration you get: + +* Editor support, including: + * Completion. + * Type checks. +* Validation of data: + * Automatic and clear errors when the data is invalid. + * Validation even for deeply nested JSON objects. +* Conversion of input data: coming from the network to Python data and types. Reading from: + * JSON. + * Path parameters. + * Query parameters. + * Cookies. + * Headers. + * Forms. + * Files. +* Conversion of output data: converting from Python data and types to network data (as JSON): + * Convert Python types (`str`, `int`, `float`, `bool`, `list`, etc). + * `datetime` objects. + * `UUID` objects. + * Database models. + * ...and many more. +* Automatic interactive API documentation, including 2 alternative user interfaces: + * Swagger UI. + * ReDoc. + +--- + +Coming back to the previous code example, **FastAPI** will: + +* Validate that there is an `item_id` in the path for `GET` and `PUT` requests. +* Validate that the `item_id` is of type `int` for `GET` and `PUT` requests. + * If it is not, the client will see a useful, clear error. +* Check if there is an optional query parameter named `q` (as in `http://127.0.0.1:8000/items/foo?q=somequery`) for `GET` requests. + * As the `q` parameter is declared with `= None`, it is optional. + * Without the `None` it would be required (as is the body in the case with `PUT`). +* For `PUT` requests to `/items/{item_id}`, Read the body as JSON: + * Check that it has a required attribute `name` that should be a `str`. + * Check that it has a required attribute `price` that has to be a `float`. + * Check that it has an optional attribute `is_offer`, that should be a `bool`, if present. + * All this would also work for deeply nested JSON objects. +* Convert from and to JSON automatically. +* Document everything with OpenAPI, that can be used by: + * Interactive documentation systems. + * Automatic client code generation systems, for many languages. +* Provide 2 interactive documentation web interfaces directly. + +--- + +We just scratched the surface, but you already get the idea of how it all works. + +Try changing the line with: + +```Python + return {"item_name": item.name, "item_id": item_id} +``` + +...from: + +```Python + ... "item_name": item.name ... +``` + +...to: + +```Python + ... "item_price": item.price ... +``` + +...and see how your editor will auto-complete the attributes and know their types: + +![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) + +For a more complete example including more features, see the Tutorial - User Guide. + +**Spoiler alert**: the tutorial - user guide includes: + +* Declaration of **parameters** from other different places as: **headers**, **cookies**, **form fields** and **files**. +* How to set **validation constraints** as `maximum_length` or `regex`. +* A very powerful and easy to use **Dependency Injection** system. +* Security and authentication, including support for **OAuth2** with **JWT tokens** and **HTTP Basic** auth. +* More advanced (but equally easy) techniques for declaring **deeply nested JSON models** (thanks to Pydantic). +* Many extra features (thanks to Starlette) as: + * **WebSockets** + * **GraphQL** + * extremely easy tests based on `requests` and `pytest` + * **CORS** + * **Cookie Sessions** + * ...and more. + +## Performance + +Independent TechEmpower benchmarks show **FastAPI** applications running under Uvicorn as one of the fastest Python frameworks available, only below Starlette and Uvicorn themselves (used internally by FastAPI). (*) + +To understand more about it, see the section Benchmarks. + +## Optional Dependencies + +Used by Pydantic: + +* ujson - for faster JSON "parsing". +* email_validator - for email validation. + +Used by Starlette: + +* requests - Required if you want to use the `TestClient`. +* aiofiles - Required if you want to use `FileResponse` or `StaticFiles`. +* jinja2 - Required if you want to use the default template configuration. +* python-multipart - Required if you want to support form "parsing", with `request.form()`. +* itsdangerous - Required for `SessionMiddleware` support. +* pyyaml - Required for Starlette's `SchemaGenerator` support (you probably don't need it with FastAPI). +* graphene - Required for `GraphQLApp` support. +* ujson - Required if you want to use `UJSONResponse`. + +Used by FastAPI / Starlette: + +* uvicorn - for the server that loads and serves your application. +* orjson - Required if you want to use `ORJSONResponse`. + +You can install all of these with `pip install fastapi[all]`. + +## License + +This project is licensed under the terms of the MIT license. diff --git a/docs/it/mkdocs.yml b/docs/it/mkdocs.yml new file mode 100644 index 000000000..1bb7cad11 --- /dev/null +++ b/docs/it/mkdocs.yml @@ -0,0 +1,66 @@ +site_name: FastAPI +site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production +site_url: https://fastapi.tiangolo.com/it/ +theme: + name: material + palette: + primary: teal + accent: amber + icon: + repo: fontawesome/brands/github-alt + logo: https://fastapi.tiangolo.com/img/icon-white.svg + favicon: https://fastapi.tiangolo.com/img/favicon.png + language: it +repo_name: tiangolo/fastapi +repo_url: https://github.com/tiangolo/fastapi +edit_uri: '' +google_analytics: +- UA-133183413-1 +- auto +nav: +- FastAPI: index.md +- Languages: + - en: / + - es: /es/ + - it: /it/ + - pt: /pt/ + - zh: /zh/ +markdown_extensions: +- toc: + permalink: true +- markdown.extensions.codehilite: + guess_lang: false +- markdown_include.include: + base_path: docs +- admonition +- codehilite +- extra +- pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_div_format '' +- pymdownx.tabbed +extra: + social: + - icon: fontawesome/brands/github-alt + link: https://github.com/tiangolo/typer + - icon: fontawesome/brands/twitter + link: https://twitter.com/tiangolo + - icon: fontawesome/brands/linkedin + link: https://www.linkedin.com/in/tiangolo + - icon: fontawesome/brands/dev + link: https://dev.to/tiangolo + - icon: fontawesome/brands/medium + link: https://medium.com/@tiangolo + - icon: fontawesome/solid/globe + link: https://tiangolo.com +extra_css: +- https://fastapi.tiangolo.com/css/termynal.css +- https://fastapi.tiangolo.com/css/custom.css +extra_javascript: +- https://unpkg.com/mermaid@8.4.6/dist/mermaid.min.js +- https://fastapi.tiangolo.com/js/termynal.js +- https://fastapi.tiangolo.com/js/custom.js +- https://fastapi.tiangolo.com/js/chat.js +- https://sidecar.gitter.im/dist/sidecar.v1.js diff --git a/docs/pt/mkdocs.yml b/docs/pt/mkdocs.yml index 105f7e6f3..90eab738b 100644 --- a/docs/pt/mkdocs.yml +++ b/docs/pt/mkdocs.yml @@ -22,6 +22,7 @@ nav: - Languages: - en: / - es: /es/ + - it: /it/ - pt: /pt/ - zh: /zh/ - features.md diff --git a/docs/zh/mkdocs.yml b/docs/zh/mkdocs.yml index 75a7e9707..6d47c223a 100644 --- a/docs/zh/mkdocs.yml +++ b/docs/zh/mkdocs.yml @@ -22,6 +22,7 @@ nav: - Languages: - en: / - es: /es/ + - it: /it/ - pt: /pt/ - zh: /zh/ - features.md diff --git a/pyproject.toml b/pyproject.toml index 07610fca0..414304fc5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,7 @@ Documentation = "https://fastapi.tiangolo.com/" [tool.flit.metadata.requires-extra] test = [ - "pytest >=4.0.0", + "pytest >=5.4.3", "pytest-cov", "mypy", "black", From 1b2a7546af12d4673bbc07a4c3fc05db46cf9b53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 12 Jun 2020 18:57:56 +0200 Subject: [PATCH 09/82] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 9b26f2c53..4b1102310 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,8 @@ ## Latest changes +* Start translations for Italian. PR [#1557](https://github.com/tiangolo/fastapi/pull/1557) by [@csr](https://github.com/csr). + ## 0.56.0 * Add support for ASGI `root_path`: From 7e2518350ad2ab529ba1166c304ee299b616f42f Mon Sep 17 00:00:00 2001 From: Pankaj Giri Date: Sat, 13 Jun 2020 01:11:44 +0530 Subject: [PATCH 10/82] =?UTF-8?q?=F0=9F=93=9D=20Remove=20*,=20from=20funct?= =?UTF-8?q?ions=20where=20it's=20not=20needed=20#1234=20(#1239)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix for - [FEATURE] Remove *, where it's not needed #1234 * 🔥 Remove unnecessary arg *, * 🎨 Update docs format highlight lines Co-authored-by: Sebastián Ramírez --- docs/en/docs/tutorial/schema-extra-example.md | 2 +- docs_src/body_fields/tutorial001.py | 2 +- docs_src/body_multiple_params/tutorial002.py | 2 +- docs_src/body_multiple_params/tutorial003.py | 2 +- docs_src/body_multiple_params/tutorial005.py | 2 +- docs_src/body_nested_models/tutorial001.py | 2 +- docs_src/body_nested_models/tutorial002.py | 2 +- docs_src/body_nested_models/tutorial003.py | 2 +- docs_src/body_nested_models/tutorial004.py | 2 +- docs_src/body_nested_models/tutorial005.py | 2 +- docs_src/body_nested_models/tutorial006.py | 2 +- docs_src/body_nested_models/tutorial007.py | 2 +- docs_src/body_nested_models/tutorial008.py | 2 +- docs_src/cookie_params/tutorial001.py | 2 +- docs_src/extra_models/tutorial001.py | 2 +- docs_src/extra_models/tutorial002.py | 2 +- docs_src/header_params/tutorial001.py | 2 +- docs_src/header_params/tutorial002.py | 2 +- docs_src/path_operation_advanced_configuration/tutorial004.py | 2 +- docs_src/path_operation_configuration/tutorial001.py | 2 +- docs_src/path_operation_configuration/tutorial002.py | 2 +- docs_src/path_operation_configuration/tutorial003.py | 2 +- docs_src/path_operation_configuration/tutorial004.py | 2 +- docs_src/path_operation_configuration/tutorial005.py | 2 +- docs_src/request_forms/tutorial001.py | 2 +- docs_src/response_model/tutorial002.py | 2 +- docs_src/response_model/tutorial003.py | 2 +- docs_src/schema_extra_example/tutorial001.py | 2 +- docs_src/schema_extra_example/tutorial002.py | 2 +- docs_src/schema_extra_example/tutorial003.py | 3 +-- docs_src/security/tutorial004.py | 2 +- docs_src/security/tutorial005.py | 2 +- 32 files changed, 32 insertions(+), 33 deletions(-) diff --git a/docs/en/docs/tutorial/schema-extra-example.md b/docs/en/docs/tutorial/schema-extra-example.md index 70c3d1846..8a497ccd5 100644 --- a/docs/en/docs/tutorial/schema-extra-example.md +++ b/docs/en/docs/tutorial/schema-extra-example.md @@ -33,7 +33,7 @@ The same way you can pass extra info to `Field`, you can do the same with `Path` For example, you can pass an `example` for a body request to `Body`: -```Python hl_lines="20 21 22 23 24 25" +```Python hl_lines="19 20 21 22 23 24" {!../../../docs_src/schema_extra_example/tutorial003.py!} ``` diff --git a/docs_src/body_fields/tutorial001.py b/docs_src/body_fields/tutorial001.py index 7b2c9454d..dcd5b9764 100644 --- a/docs_src/body_fields/tutorial001.py +++ b/docs_src/body_fields/tutorial001.py @@ -12,6 +12,6 @@ class Item(BaseModel): @app.put("/items/{item_id}") -async def update_item(*, item_id: int, item: Item = Body(..., embed=True)): +async def update_item(item_id: int, item: Item = Body(..., embed=True)): results = {"item_id": item_id, "item": item} return results diff --git a/docs_src/body_multiple_params/tutorial002.py b/docs_src/body_multiple_params/tutorial002.py index 5c9e8344d..54f6d9138 100644 --- a/docs_src/body_multiple_params/tutorial002.py +++ b/docs_src/body_multiple_params/tutorial002.py @@ -17,6 +17,6 @@ class User(BaseModel): @app.put("/items/{item_id}") -async def update_item(*, item_id: int, item: Item, user: User): +async def update_item(item_id: int, item: Item, user: User): results = {"item_id": item_id, "item": item, "user": user} return results diff --git a/docs_src/body_multiple_params/tutorial003.py b/docs_src/body_multiple_params/tutorial003.py index 301f1a862..691fe848b 100644 --- a/docs_src/body_multiple_params/tutorial003.py +++ b/docs_src/body_multiple_params/tutorial003.py @@ -18,7 +18,7 @@ class User(BaseModel): @app.put("/items/{item_id}") async def update_item( - *, item_id: int, item: Item, user: User, importance: int = Body(...) + item_id: int, item: Item, user: User, importance: int = Body(...) ): results = {"item_id": item_id, "item": item, "user": user, "importance": importance} return results diff --git a/docs_src/body_multiple_params/tutorial005.py b/docs_src/body_multiple_params/tutorial005.py index 61f1b2917..606d3bbbb 100644 --- a/docs_src/body_multiple_params/tutorial005.py +++ b/docs_src/body_multiple_params/tutorial005.py @@ -12,6 +12,6 @@ class Item(BaseModel): @app.put("/items/{item_id}") -async def update_item(*, item_id: int, item: Item = Body(..., embed=True)): +async def update_item(item_id: int, item: Item = Body(..., embed=True)): results = {"item_id": item_id, "item": item} return results diff --git a/docs_src/body_nested_models/tutorial001.py b/docs_src/body_nested_models/tutorial001.py index 9e0fa4494..56db2a093 100644 --- a/docs_src/body_nested_models/tutorial001.py +++ b/docs_src/body_nested_models/tutorial001.py @@ -13,6 +13,6 @@ class Item(BaseModel): @app.put("/items/{item_id}") -async def update_item(*, item_id: int, item: Item): +async def update_item(item_id: int, item: Item): results = {"item_id": item_id, "item": item} return results diff --git a/docs_src/body_nested_models/tutorial002.py b/docs_src/body_nested_models/tutorial002.py index 8f769279b..db33e483f 100644 --- a/docs_src/body_nested_models/tutorial002.py +++ b/docs_src/body_nested_models/tutorial002.py @@ -15,6 +15,6 @@ class Item(BaseModel): @app.put("/items/{item_id}") -async def update_item(*, item_id: int, item: Item): +async def update_item(item_id: int, item: Item): results = {"item_id": item_id, "item": item} return results diff --git a/docs_src/body_nested_models/tutorial003.py b/docs_src/body_nested_models/tutorial003.py index bb539b127..f65195b62 100644 --- a/docs_src/body_nested_models/tutorial003.py +++ b/docs_src/body_nested_models/tutorial003.py @@ -15,6 +15,6 @@ class Item(BaseModel): @app.put("/items/{item_id}") -async def update_item(*, item_id: int, item: Item): +async def update_item(item_id: int, item: Item): results = {"item_id": item_id, "item": item} return results diff --git a/docs_src/body_nested_models/tutorial004.py b/docs_src/body_nested_models/tutorial004.py index 257928ef3..6c2ca25eb 100644 --- a/docs_src/body_nested_models/tutorial004.py +++ b/docs_src/body_nested_models/tutorial004.py @@ -21,6 +21,6 @@ class Item(BaseModel): @app.put("/items/{item_id}") -async def update_item(*, item_id: int, item: Item): +async def update_item(item_id: int, item: Item): results = {"item_id": item_id, "item": item} return results diff --git a/docs_src/body_nested_models/tutorial005.py b/docs_src/body_nested_models/tutorial005.py index afea77179..d6d9d6479 100644 --- a/docs_src/body_nested_models/tutorial005.py +++ b/docs_src/body_nested_models/tutorial005.py @@ -21,6 +21,6 @@ class Item(BaseModel): @app.put("/items/{item_id}") -async def update_item(*, item_id: int, item: Item): +async def update_item(item_id: int, item: Item): results = {"item_id": item_id, "item": item} return results diff --git a/docs_src/body_nested_models/tutorial006.py b/docs_src/body_nested_models/tutorial006.py index 3d0db6e58..d64668518 100644 --- a/docs_src/body_nested_models/tutorial006.py +++ b/docs_src/body_nested_models/tutorial006.py @@ -21,6 +21,6 @@ class Item(BaseModel): @app.put("/items/{item_id}") -async def update_item(*, item_id: int, item: Item): +async def update_item(item_id: int, item: Item): results = {"item_id": item_id, "item": item} return results diff --git a/docs_src/body_nested_models/tutorial007.py b/docs_src/body_nested_models/tutorial007.py index f13c07ad5..f4b699dbf 100644 --- a/docs_src/body_nested_models/tutorial007.py +++ b/docs_src/body_nested_models/tutorial007.py @@ -28,5 +28,5 @@ class Offer(BaseModel): @app.post("/offers/") -async def create_offer(*, offer: Offer): +async def create_offer(offer: Offer): return offer diff --git a/docs_src/body_nested_models/tutorial008.py b/docs_src/body_nested_models/tutorial008.py index b6f5c0660..3431cc636 100644 --- a/docs_src/body_nested_models/tutorial008.py +++ b/docs_src/body_nested_models/tutorial008.py @@ -12,5 +12,5 @@ class Image(BaseModel): @app.post("/images/multiple/") -async def create_multiple_images(*, images: List[Image]): +async def create_multiple_images(images: List[Image]): return images diff --git a/docs_src/cookie_params/tutorial001.py b/docs_src/cookie_params/tutorial001.py index 5a6fd30fe..bee934b06 100644 --- a/docs_src/cookie_params/tutorial001.py +++ b/docs_src/cookie_params/tutorial001.py @@ -4,5 +4,5 @@ app = FastAPI() @app.get("/items/") -async def read_items(*, ads_id: str = Cookie(None)): +async def read_items(ads_id: str = Cookie(None)): return {"ads_id": ads_id} diff --git a/docs_src/extra_models/tutorial001.py b/docs_src/extra_models/tutorial001.py index 08d3659b0..7ff4bba7e 100644 --- a/docs_src/extra_models/tutorial001.py +++ b/docs_src/extra_models/tutorial001.py @@ -36,6 +36,6 @@ def fake_save_user(user_in: UserIn): @app.post("/user/", response_model=UserOut) -async def create_user(*, user_in: UserIn): +async def create_user(user_in: UserIn): user_saved = fake_save_user(user_in) return user_saved diff --git a/docs_src/extra_models/tutorial002.py b/docs_src/extra_models/tutorial002.py index ab680eca0..030699114 100644 --- a/docs_src/extra_models/tutorial002.py +++ b/docs_src/extra_models/tutorial002.py @@ -34,6 +34,6 @@ def fake_save_user(user_in: UserIn): @app.post("/user/", response_model=UserOut) -async def create_user(*, user_in: UserIn): +async def create_user(user_in: UserIn): user_saved = fake_save_user(user_in) return user_saved diff --git a/docs_src/header_params/tutorial001.py b/docs_src/header_params/tutorial001.py index 69f3dc712..e871de68d 100644 --- a/docs_src/header_params/tutorial001.py +++ b/docs_src/header_params/tutorial001.py @@ -4,5 +4,5 @@ app = FastAPI() @app.get("/items/") -async def read_items(*, user_agent: str = Header(None)): +async def read_items(user_agent: str = Header(None)): return {"User-Agent": user_agent} diff --git a/docs_src/header_params/tutorial002.py b/docs_src/header_params/tutorial002.py index 4edc4b6fd..ba6cef1de 100644 --- a/docs_src/header_params/tutorial002.py +++ b/docs_src/header_params/tutorial002.py @@ -4,5 +4,5 @@ app = FastAPI() @app.get("/items/") -async def read_items(*, strange_header: str = Header(None, convert_underscores=False)): +async def read_items(strange_header: str = Header(None, convert_underscores=False)): return {"strange_header": strange_header} diff --git a/docs_src/path_operation_advanced_configuration/tutorial004.py b/docs_src/path_operation_advanced_configuration/tutorial004.py index 36bf02b11..24be0cb94 100644 --- a/docs_src/path_operation_advanced_configuration/tutorial004.py +++ b/docs_src/path_operation_advanced_configuration/tutorial004.py @@ -15,7 +15,7 @@ class Item(BaseModel): @app.post("/items/", response_model=Item, summary="Create an item") -async def create_item(*, item: Item): +async def create_item(item: Item): """ Create an item with all the information: diff --git a/docs_src/path_operation_configuration/tutorial001.py b/docs_src/path_operation_configuration/tutorial001.py index a60b47fef..77437cd08 100644 --- a/docs_src/path_operation_configuration/tutorial001.py +++ b/docs_src/path_operation_configuration/tutorial001.py @@ -15,5 +15,5 @@ class Item(BaseModel): @app.post("/items/", response_model=Item, status_code=status.HTTP_201_CREATED) -async def create_item(*, item: Item): +async def create_item(item: Item): return item diff --git a/docs_src/path_operation_configuration/tutorial002.py b/docs_src/path_operation_configuration/tutorial002.py index b5d0f12ca..ed29ea014 100644 --- a/docs_src/path_operation_configuration/tutorial002.py +++ b/docs_src/path_operation_configuration/tutorial002.py @@ -15,7 +15,7 @@ class Item(BaseModel): @app.post("/items/", response_model=Item, tags=["items"]) -async def create_item(*, item: Item): +async def create_item(item: Item): return item diff --git a/docs_src/path_operation_configuration/tutorial003.py b/docs_src/path_operation_configuration/tutorial003.py index 106607fd2..4c4b048c7 100644 --- a/docs_src/path_operation_configuration/tutorial003.py +++ b/docs_src/path_operation_configuration/tutorial003.py @@ -20,5 +20,5 @@ class Item(BaseModel): summary="Create an item", description="Create an item with all the information, name, description, price, tax and a set of unique tags", ) -async def create_item(*, item: Item): +async def create_item(item: Item): return item diff --git a/docs_src/path_operation_configuration/tutorial004.py b/docs_src/path_operation_configuration/tutorial004.py index f47d422e0..f9822aae4 100644 --- a/docs_src/path_operation_configuration/tutorial004.py +++ b/docs_src/path_operation_configuration/tutorial004.py @@ -15,7 +15,7 @@ class Item(BaseModel): @app.post("/items/", response_model=Item, summary="Create an item") -async def create_item(*, item: Item): +async def create_item(item: Item): """ Create an item with all the information: diff --git a/docs_src/path_operation_configuration/tutorial005.py b/docs_src/path_operation_configuration/tutorial005.py index 72d02ece3..7ae2acafc 100644 --- a/docs_src/path_operation_configuration/tutorial005.py +++ b/docs_src/path_operation_configuration/tutorial005.py @@ -20,7 +20,7 @@ class Item(BaseModel): summary="Create an item", response_description="The created item", ) -async def create_item(*, item: Item): +async def create_item(item: Item): """ Create an item with all the information: diff --git a/docs_src/request_forms/tutorial001.py b/docs_src/request_forms/tutorial001.py index 0290b644d..c07e22945 100644 --- a/docs_src/request_forms/tutorial001.py +++ b/docs_src/request_forms/tutorial001.py @@ -4,5 +4,5 @@ app = FastAPI() @app.post("/login/") -async def login(*, username: str = Form(...), password: str = Form(...)): +async def login(username: str = Form(...), password: str = Form(...)): return {"username": username} diff --git a/docs_src/response_model/tutorial002.py b/docs_src/response_model/tutorial002.py index b36b2a347..67084b45e 100644 --- a/docs_src/response_model/tutorial002.py +++ b/docs_src/response_model/tutorial002.py @@ -13,5 +13,5 @@ class UserIn(BaseModel): # Don't do this in production! @app.post("/user/", response_model=UserIn) -async def create_user(*, user: UserIn): +async def create_user(user: UserIn): return user diff --git a/docs_src/response_model/tutorial003.py b/docs_src/response_model/tutorial003.py index a73372901..8450723c2 100644 --- a/docs_src/response_model/tutorial003.py +++ b/docs_src/response_model/tutorial003.py @@ -18,5 +18,5 @@ class UserOut(BaseModel): @app.post("/user/", response_model=UserOut) -async def create_user(*, user: UserIn): +async def create_user(user: UserIn): return user diff --git a/docs_src/schema_extra_example/tutorial001.py b/docs_src/schema_extra_example/tutorial001.py index cd4d45f51..59a583750 100644 --- a/docs_src/schema_extra_example/tutorial001.py +++ b/docs_src/schema_extra_example/tutorial001.py @@ -22,6 +22,6 @@ class Item(BaseModel): @app.put("/items/{item_id}") -async def update_item(*, item_id: int, item: Item): +async def update_item(item_id: int, item: Item): results = {"item_id": item_id, "item": item} return results diff --git a/docs_src/schema_extra_example/tutorial002.py b/docs_src/schema_extra_example/tutorial002.py index edf9897d0..0a567bd79 100644 --- a/docs_src/schema_extra_example/tutorial002.py +++ b/docs_src/schema_extra_example/tutorial002.py @@ -12,6 +12,6 @@ class Item(BaseModel): @app.put("/items/{item_id}") -async def update_item(*, item_id: int, item: Item): +async def update_item(item_id: int, item: Item): results = {"item_id": item_id, "item": item} return results diff --git a/docs_src/schema_extra_example/tutorial003.py b/docs_src/schema_extra_example/tutorial003.py index 1165fd7a0..87fbefbb6 100644 --- a/docs_src/schema_extra_example/tutorial003.py +++ b/docs_src/schema_extra_example/tutorial003.py @@ -13,7 +13,6 @@ class Item(BaseModel): @app.put("/items/{item_id}") async def update_item( - *, item_id: int, item: Item = Body( ..., @@ -23,7 +22,7 @@ async def update_item( "price": 35.4, "tax": 3.2, }, - ) + ), ): results = {"item_id": item_id, "item": item} return results diff --git a/docs_src/security/tutorial004.py b/docs_src/security/tutorial004.py index 3c8b6693d..56f0eb0ea 100644 --- a/docs_src/security/tutorial004.py +++ b/docs_src/security/tutorial004.py @@ -75,7 +75,7 @@ def authenticate_user(fake_db, username: str, password: str): return user -def create_access_token(*, data: dict, expires_delta: timedelta = None): +def create_access_token(data: dict, expires_delta: timedelta = None): to_encode = data.copy() if expires_delta: expire = datetime.utcnow() + expires_delta diff --git a/docs_src/security/tutorial005.py b/docs_src/security/tutorial005.py index f43659454..489404d6f 100644 --- a/docs_src/security/tutorial005.py +++ b/docs_src/security/tutorial005.py @@ -91,7 +91,7 @@ def authenticate_user(fake_db, username: str, password: str): return user -def create_access_token(*, data: dict, expires_delta: timedelta = None): +def create_access_token(data: dict, expires_delta: timedelta = None): to_encode = data.copy() if expires_delta: expire = datetime.utcnow() + expires_delta From 535247ffc4786b746422871965302d3038f708a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 12 Jun 2020 21:43:17 +0200 Subject: [PATCH 11/82] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 4b1102310..1dea03c4b 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Remove `*,` from functions in docs where it's not needed. PR [#1239](https://github.com/tiangolo/fastapi/pull/1239) by [@pankaj-giri](https://github.com/pankaj-giri). * Start translations for Italian. PR [#1557](https://github.com/tiangolo/fastapi/pull/1557) by [@csr](https://github.com/csr). ## 0.56.0 From 434d32b8917b3f1e45f36a9ea72bb8b45d7266ab Mon Sep 17 00:00:00 2001 From: Kazantcev Andrey <45011689+heckad@users.noreply.github.com> Date: Fri, 12 Jun 2020 22:59:32 +0300 Subject: [PATCH 12/82] :zap: Optimize regexp pattern in get_path_param_names (#1243) --- fastapi/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastapi/utils.py b/fastapi/utils.py index c9022fbc3..89bf861a5 100644 --- a/fastapi/utils.py +++ b/fastapi/utils.py @@ -67,7 +67,7 @@ def get_model_definitions( def get_path_param_names(path: str) -> Set[str]: - return {item.strip("{}") for item in re.findall("{[^}]*}", path)} + return set(re.findall("{(.*?)}", path)) def create_response_field( From 5a00467951a243632a4bf3d9a4a6d53b8f273d99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 12 Jun 2020 22:01:22 +0200 Subject: [PATCH 13/82] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 1dea03c4b..99622b43e 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Optimize internal regex performance in `get_path_param_names()`. PR [#1243](https://github.com/tiangolo/fastapi/pull/1243) by [@heckad](https://github.com/heckad). * Remove `*,` from functions in docs where it's not needed. PR [#1239](https://github.com/tiangolo/fastapi/pull/1239) by [@pankaj-giri](https://github.com/pankaj-giri). * Start translations for Italian. PR [#1557](https://github.com/tiangolo/fastapi/pull/1557) by [@csr](https://github.com/csr). From d66d8379c0a03319da39b80046093131f4cc9f61 Mon Sep 17 00:00:00 2001 From: Nik Date: Fri, 12 Jun 2020 23:35:59 +0300 Subject: [PATCH 14/82] =?UTF-8?q?=F0=9F=90=9B=20Fix=20OpenAPI=20generation?= =?UTF-8?q?=20when=20using=20callbacks=20with=20routers=20including=20Pyda?= =?UTF-8?q?ntic=20models=20(#1322)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * drop model class from additional responses when generating openapi * ♻️ Copy response to be mutated early in get_openapi_path Co-authored-by: Sebastián Ramírez --- fastapi/openapi/utils.py | 12 +- ...onal_responses_custom_model_in_callback.py | 138 ++++++++++++++++++ 2 files changed, 146 insertions(+), 4 deletions(-) create mode 100644 tests/test_additional_responses_custom_model_in_callback.py diff --git a/fastapi/openapi/utils.py b/fastapi/openapi/utils.py index b5778327b..bb2e7dff7 100644 --- a/fastapi/openapi/utils.py +++ b/fastapi/openapi/utils.py @@ -203,27 +203,31 @@ def get_openapi_path( operation["callbacks"] = callbacks if route.responses: for (additional_status_code, response) in route.responses.items(): + process_response = response.copy() assert isinstance( - response, dict + process_response, dict ), "An additional response must be a dict" field = route.response_fields.get(additional_status_code) if field: response_schema, _, _ = field_schema( field, model_name_map=model_name_map, ref_prefix=REF_PREFIX ) - response.setdefault("content", {}).setdefault( + process_response.setdefault("content", {}).setdefault( route_response_media_type or "application/json", {} )["schema"] = response_schema status_text: Optional[str] = status_code_ranges.get( str(additional_status_code).upper() ) or http.client.responses.get(int(additional_status_code)) - response.setdefault( + process_response.setdefault( "description", status_text or "Additional Response" ) status_code_key = str(additional_status_code).upper() if status_code_key == "DEFAULT": status_code_key = "default" - operation.setdefault("responses", {})[status_code_key] = response + process_response.pop("model", None) + operation.setdefault("responses", {})[ + status_code_key + ] = process_response status_code = str(route.status_code) operation.setdefault("responses", {}).setdefault(status_code, {})[ "description" diff --git a/tests/test_additional_responses_custom_model_in_callback.py b/tests/test_additional_responses_custom_model_in_callback.py new file mode 100644 index 000000000..36dd0d6db --- /dev/null +++ b/tests/test_additional_responses_custom_model_in_callback.py @@ -0,0 +1,138 @@ +from fastapi import APIRouter, FastAPI +from fastapi.testclient import TestClient +from pydantic import BaseModel, HttpUrl +from starlette.responses import JSONResponse + + +class CustomModel(BaseModel): + a: int + + +app = FastAPI() + +callback_router = APIRouter(default_response_class=JSONResponse) + + +@callback_router.get( + "{$callback_url}/callback/", responses={400: {"model": CustomModel}} +) +def callback_route(): + pass # pragma: no cover + + +@app.post("/", callbacks=callback_router.routes) +def main_route(callback_url: HttpUrl): + pass # pragma: no cover + + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/": { + "post": { + "summary": "Main Route", + "operationId": "main_route__post", + "parameters": [ + { + "required": True, + "schema": { + "title": "Callback Url", + "maxLength": 2083, + "minLength": 1, + "type": "string", + "format": "uri", + }, + "name": "callback_url", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "callbacks": { + "callback_route": { + "{$callback_url}/callback/": { + "get": { + "summary": "Callback Route", + "operationId": "callback_route__callback_url__callback__get", + "responses": { + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CustomModel" + } + } + }, + "description": "Bad Request", + }, + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + }, + } + } + } + }, + } + } + }, + "components": { + "schemas": { + "CustomModel": { + "title": "CustomModel", + "required": ["a"], + "type": "object", + "properties": {"a": {"title": "A", "type": "integer"}}, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + +client = TestClient(app) + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema From 7efc15aeef053a3c98d1427b33732e390ef74627 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 12 Jun 2020 22:37:34 +0200 Subject: [PATCH 15/82] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 99622b43e..c0f081f78 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Fix generating OpenAPI for apps using callbacks with routers including Pydantic models. PR [#1322](https://github.com/tiangolo/fastapi/pull/1322) by [@nsidnev](https://github.com/nsidnev). * Optimize internal regex performance in `get_path_param_names()`. PR [#1243](https://github.com/tiangolo/fastapi/pull/1243) by [@heckad](https://github.com/heckad). * Remove `*,` from functions in docs where it's not needed. PR [#1239](https://github.com/tiangolo/fastapi/pull/1239) by [@pankaj-giri](https://github.com/pankaj-giri). * Start translations for Italian. PR [#1557](https://github.com/tiangolo/fastapi/pull/1557) by [@csr](https://github.com/csr). From 81a529c2517afae15fdc6c8a1361345eeec0801c Mon Sep 17 00:00:00 2001 From: Xie Wei <39515546+waynerv@users.noreply.github.com> Date: Sat, 13 Jun 2020 04:39:26 +0800 Subject: [PATCH 16/82] =?UTF-8?q?=F0=9F=8C=90=20Translate=20doc=20first=20?= =?UTF-8?q?steps=20to=20Chinese=20(#1323)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * WIP:add Chinese translation for first steps doc * add Chinese translation for first steps doc * improve translations Co-authored-by: Waynerv --- docs/zh/docs/tutorial/first-steps.md | 335 +++++++++++++++++++++++++++ docs/zh/mkdocs.yml | 1 + 2 files changed, 336 insertions(+) create mode 100644 docs/zh/docs/tutorial/first-steps.md diff --git a/docs/zh/docs/tutorial/first-steps.md b/docs/zh/docs/tutorial/first-steps.md new file mode 100644 index 000000000..ab63a8650 --- /dev/null +++ b/docs/zh/docs/tutorial/first-steps.md @@ -0,0 +1,335 @@ +# 第一步 + +最简单的 FastAPI 文件可能像下面这样: + +```Python +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +将其复制到 `main.py` 文件中。 + +运行实时服务器: + +
+ +```console +$ uvicorn main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +INFO: Started reloader process [28720] +INFO: Started server process [28722] +INFO: Waiting for application startup. +INFO: Application startup complete. +``` + +
+ +!!! note + `uvicorn main:app` 命令含义如下: + + * `main`:`main.py` 文件(一个 Python「模块」)。 + * `app`:在 `main.py` 文件中通过 `app = FastAPI()` 创建的对象。 + * `--reload`:让服务器在更新代码后重新启动。仅在开发时使用该选项。 + + +在输出中,会有一行信息像下面这样: + +```hl_lines="4" +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + + +该行显示了你的应用在本机所提供服务的 URL 地址。 + +### 查看 + +打开浏览器访问 http://127.0.0.1:8000。 + +你将看到如下的 JSON 响应: + +```JSON +{"message": "Hello World"} +``` + +### 交互式 API 文档 + +跳转到 http://127.0.0.1:8000/docs。 + +你将会看到自动生成的交互式 API 文档(由 Swagger UI 提供): + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) + +### 可选的 API 文档 + +前往 http://127.0.0.1:8000/redoc。 + +你将会看到可选的自动生成文档 (由 ReDoc 提供): + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) + +### OpenAPI + +**FastAPI** 使用定义 API 的 **OpenAPI** 标准将你的所有 API 转换成「模式」。 + +#### 「模式」 + +「模式」是对事物的一种定义或描述。它并非具体的实现代码,而只是抽象的描述。 + +#### API「模式」 + +在这种场景下,OpenAPI 是一种规定如何定义 API 模式的规范。 + +定义的 OpenAPI 模式将包括你的 API 路径,以及它们可能使用的参数等等。 + +#### 数据「模式」 + +「模式」这个术语也可能指的是某些数据比如 JSON 的结构。 + +在这种情况下,它可以表示 JSON 的属性及其具有的数据类型,等等。 + +#### OpenAPI 和 JSON Schema + +OpenAPI 为你的 API 定义 API 模式。该模式中包含了你的 API 发送和接收的数据的定义(或称为「模式」),这些定义通过 JSON 数据模式标准 **JSON Schema** 所生成。 + +#### 查看 `openapi.json` + +如果你对原始的 OpenAPI 模式长什么样子感到好奇,其实它只是一个自动生成的包含了所有 API 描述的 JSON。 + +你可以直接在:http://127.0.0.1:8000/openapi.json 看到它。 + +它将显示以如下内容开头的 JSON: + +```JSON +{ + "openapi": "3.0.2", + "info": { + "title": "FastAPI", + "version": "0.1.0" + }, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + + + +... +``` + +#### OpenAPI 的用途 + +驱动 FastAPI 内置的 2 个交互式文档系统的正是 OpenAPI 模式。 + +并且还有数十种替代方案,它们全部都基于 OpenAPI。你可以轻松地将这些替代方案中的任何一种添加到使用 **FastAPI** 构建的应用程序中。 + +你还可以使用它自动生成与你的 API 进行通信的客户端代码。例如 web 前端,移动端或物联网嵌入程序。 + +## 分步概括 + +### 步骤 1:导入 `FastAPI` + +```Python hl_lines="1" +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +`FastAPI` 是一个为你的 API 提供了所有功能的 Python 类。 + +!!! note "技术细节" + `FastAPI` 是直接从 `Starlette` 继承的类。 + + 你可以通过 `FastAPI` 使用所有的 Starlette 的功能。 + +### 步骤 2:创建一个 `FastAPI`「实例」 + +```Python hl_lines="3" +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +这里的变量 `app` 会是 `FastAPI` 类的一个「实例」。 + +这个实例将是创建你所有 API 的主要交互对象。 + +这个 `app` 同样在如下命令中被 `uvicorn` 所引用: + +
+ +```console +$ uvicorn main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +如果你像下面这样创建应用: + +```Python hl_lines="3" +{!../../../docs_src/first_steps/tutorial002.py!} +``` + +将代码放入 `main.py` 文件中,然后你可以像下面这样运行 `uvicorn`: + +
+ +```console +$ uvicorn main:my_awesome_api --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +### 步骤 3:创建一个*路径操作* + +#### 路径 + +这里的「路径」指的是 URL 中从第一个 `/` 起的后半部分。 + +所以,在一个这样的 URL 中: + +``` +https://example.com/items/foo +``` + +...路径会是: + +``` +/items/foo +``` + +!!! info + 「路径」也通常被称为「端点」或「路由」。 + +开发 API 时,「路径」是用来分离「关注点」和「资源」的主要手段。 + +#### 操作 + +这里的「操作」指的是一种 HTTP「方法」。 + +下列之一: + +* `POST` +* `GET` +* `PUT` +* `DELETE` + +...以及更少见的几种: + +* `OPTIONS` +* `HEAD` +* `PATCH` +* `TRACE` + +在 HTTP 协议中,你可以使用以上的其中一种(或多种)「方法」与每个路径进行通信。 + +--- + +在开发 API 时,你通常使用特定的 HTTP 方法去执行特定的行为。 + +通常使用: + +* `POST`:创建数据。 +* `GET`:读取数据。 +* `PUT`:更新数据。 +* `DELETE`:删除数据。 + +因此,在 OpenAPI 中,每一个 HTTP 方法都被称为「操作」。 + +我们也打算称呼它们为「操作」。 + +#### 定义一个*路径操作装饰器* + +```Python hl_lines="6" +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +`@app.get("/")` 告诉 **FastAPI** 在它下方的函数负责处理如下访问请求: + +* 请求路径为 `/` +* 使用 get 操作 + +!!! info "`@decorator` Info" + `@something` 语法在 Python 中被称为「装饰器」。 + + 像一顶漂亮的装饰帽一样,将它放在一个函数的上方(我猜测这个术语的命名就是这么来的)。 + + 装饰器接收位于其下方的函数并且用它完成一些工作。 + + 在我们的例子中,这个装饰器告诉 **FastAPI** 位于其下方的函数对应着**路径** `/` 加上 `get` **操作**。 + + 它是一个「**路径操作装饰器**」。 + +你也可以使用其他的操作: + +* `@app.post()` +* `@app.put()` +* `@app.delete()` + +以及更少见的: + +* `@app.options()` +* `@app.head()` +* `@app.patch()` +* `@app.trace()` + +!!! tip + 您可以随意使用任何一个操作(HTTP方法)。 + + **FastAPI** 没有强制要求操作有任何特定的含义。 + + 此处提供的信息仅作为指导,而不是要求。 + + 比如,当使用 GraphQL 时通常你所有的动作都通过 `post` 一种方法执行。 + +### 步骤 4:定义**路径操作函数** + +这是我们的「**路径操作函数**」: + +* **路径**:是 `/`。 +* **操作**:是 `get`。 +* **函数**:是位于「装饰器」下方的函数(位于 `@app.get("/")` 下方)。 + +```Python hl_lines="7" +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +这是一个 Python 函数。 + +每当 **FastAPI** 接收一个使用 `GET` 方法访问 URL「`/`」的请求时这个函数会被调用。 + +在这个例子中,它是一个 `async` 函数。 + +--- + +你也可以将其定义为常规函数而不使用 `async def`: + +```Python hl_lines="7" +{!../../../docs_src/first_steps/tutorial003.py!} +``` + +!!! note + 如果你不知道两者的区别,请查阅 [Async: *"In a hurry?"*](https://fastapi.tiangolo.com/async/#in-a-hurry){.internal-link target=_blank}。 + +### 步骤 5:返回内容 + +```Python hl_lines="8" +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +你可以返回一个 `dict`、`list`,像 `str`、`int` 一样的单个值,等等。 + +你还可以返回 Pydantic 模型(稍后你将了解更多)。 + +还有许多其他将会自动转换为 JSON 的对象和模型(包括 ORM 对象等)。尝试下使用你最喜欢的一种,它很有可能已经被支持。 + +## 总结 + +* 导入 `FastAPI`。 +* 创建一个 `app` 实例。 +* 编写一个**路径操作装饰器**(如 `@app.get("/")`)。 +* 编写一个**路径操作函数**(如上面的 `def root(): ...`)。 +* 运行开发服务器(如 `uvicorn main:app --reload`)。 diff --git a/docs/zh/mkdocs.yml b/docs/zh/mkdocs.yml index 6d47c223a..4f7233827 100644 --- a/docs/zh/mkdocs.yml +++ b/docs/zh/mkdocs.yml @@ -29,6 +29,7 @@ nav: - python-types.md - 教程 - 用户指南: - tutorial/index.md + - tutorial/first-steps.md - deployment.md markdown_extensions: - toc: From 807522c616f9462a5189525e26e68ceb9ff39700 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 12 Jun 2020 22:42:40 +0200 Subject: [PATCH 17/82] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index c0f081f78..39ecd6a15 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Add translation to Chinese for [First Steps - 第一步](https://fastapi.tiangolo.com/zh/tutorial/first-steps/). PR [#1323](https://github.com/tiangolo/fastapi/pull/1323) by [@waynerv](https://github.com/waynerv). * Fix generating OpenAPI for apps using callbacks with routers including Pydantic models. PR [#1322](https://github.com/tiangolo/fastapi/pull/1322) by [@nsidnev](https://github.com/nsidnev). * Optimize internal regex performance in `get_path_param_names()`. PR [#1243](https://github.com/tiangolo/fastapi/pull/1243) by [@heckad](https://github.com/heckad). * Remove `*,` from functions in docs where it's not needed. PR [#1239](https://github.com/tiangolo/fastapi/pull/1239) by [@pankaj-giri](https://github.com/pankaj-giri). From 2351fb5623b747f480d8c91af53dc7b3e9736e45 Mon Sep 17 00:00:00 2001 From: Dylan Anthony <43723790+dbanty@users.noreply.github.com> Date: Fri, 12 Jun 2020 16:44:40 -0400 Subject: [PATCH 18/82] =?UTF-8?q?=F0=9F=94=87=20Remove=20error=20log=20whe?= =?UTF-8?q?n=20parsing=20malformed=20JSON=20body=20as=20it's=20a=20client?= =?UTF-8?q?=20error=20(#1351)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/routing.py | 1 - 1 file changed, 1 deletion(-) diff --git a/fastapi/routing.py b/fastapi/routing.py index 3ac420e6e..dab751a09 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -179,7 +179,6 @@ def get_request_handler( if body_bytes: body = await request.json() except Exception as e: - logger.error(f"Error getting request body: {e}") raise HTTPException( status_code=400, detail="There was an error parsing the body" ) from e From a552cbdf59dce401b9536adb11eac40aff8828dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 12 Jun 2020 22:47:37 +0200 Subject: [PATCH 19/82] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 39ecd6a15..fca665838 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Fix/remove incorrect error logging when a client sends invalid payloads. PR [#1351](https://github.com/tiangolo/fastapi/pull/1351) by [@dbanty](https://github.com/dbanty). * Add translation to Chinese for [First Steps - 第一步](https://fastapi.tiangolo.com/zh/tutorial/first-steps/). PR [#1323](https://github.com/tiangolo/fastapi/pull/1323) by [@waynerv](https://github.com/waynerv). * Fix generating OpenAPI for apps using callbacks with routers including Pydantic models. PR [#1322](https://github.com/tiangolo/fastapi/pull/1322) by [@nsidnev](https://github.com/nsidnev). * Optimize internal regex performance in `get_path_param_names()`. PR [#1243](https://github.com/tiangolo/fastapi/pull/1243) by [@heckad](https://github.com/heckad). From b90bf2da9ee5870a45591a10c4c8ac4af76d12f1 Mon Sep 17 00:00:00 2001 From: Micah Rosales <2433663+mrosales@users.noreply.github.com> Date: Fri, 12 Jun 2020 15:57:59 -0500 Subject: [PATCH 20/82] =?UTF-8?q?=F0=9F=90=9B=20Fix=20callable=20class=20g?= =?UTF-8?q?enerator=20dependencies=20(#1365)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix callable class generator dependencies * workaround to support asynccontextmanager backfill for pre python3.7 Co-authored-by: Micah Rosales --- fastapi/dependencies/utils.py | 34 +++++++++++++++++++---- tests/test_dependency_class.py | 50 ++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 6 deletions(-) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 1a660f5d3..3ff7d3356 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -274,7 +274,7 @@ def get_dependant( path_param_names = get_path_param_names(path) endpoint_signature = get_typed_signature(call) signature_params = endpoint_signature.parameters - if inspect.isgeneratorfunction(call) or inspect.isasyncgenfunction(call): + if is_gen_callable(call) or is_async_gen_callable(call): check_dependency_contextmanagers() dependant = Dependant(call=call, name=name, path=path, use_cache=use_cache) for param_name, param in signature_params.items(): @@ -412,19 +412,41 @@ def add_param_to_fields(*, field: ModelField, dependant: Dependant) -> None: def is_coroutine_callable(call: Callable) -> bool: if inspect.isroutine(call): - return asyncio.iscoroutinefunction(call) + return inspect.iscoroutinefunction(call) if inspect.isclass(call): return False call = getattr(call, "__call__", None) - return asyncio.iscoroutinefunction(call) + return inspect.iscoroutinefunction(call) + + +def is_async_gen_callable(call: Callable) -> bool: + if inspect.isasyncgenfunction(call): + return True + call = getattr(call, "__call__", None) + return inspect.isasyncgenfunction(call) + + +def is_gen_callable(call: Callable) -> bool: + if inspect.isgeneratorfunction(call): + return True + call = getattr(call, "__call__", None) + return inspect.isgeneratorfunction(call) async def solve_generator( *, call: Callable, stack: AsyncExitStack, sub_values: Dict[str, Any] ) -> Any: - if inspect.isgeneratorfunction(call): + if is_gen_callable(call): cm = contextmanager_in_threadpool(contextmanager(call)(**sub_values)) - elif inspect.isasyncgenfunction(call): + elif is_async_gen_callable(call): + if not inspect.isasyncgenfunction(call): + # asynccontextmanager from the async_generator backfill pre python3.7 + # does not support callables that are not functions or methods. + # See https://github.com/python-trio/async_generator/issues/32 + # + # Expand the callable class into its __call__ method before decorating it. + # This approach will work on newer python versions as well. + call = getattr(call, "__call__", None) cm = asynccontextmanager(call)(**sub_values) return await stack.enter_async_context(cm) @@ -505,7 +527,7 @@ async def solve_dependencies( continue if sub_dependant.use_cache and sub_dependant.cache_key in dependency_cache: solved = dependency_cache[sub_dependant.cache_key] - elif inspect.isgeneratorfunction(call) or inspect.isasyncgenfunction(call): + elif is_gen_callable(call) or is_async_gen_callable(call): stack = request.scope.get("fastapi_astack") if stack is None: raise RuntimeError( diff --git a/tests/test_dependency_class.py b/tests/test_dependency_class.py index ba2e3cfcf..bfe777f52 100644 --- a/tests/test_dependency_class.py +++ b/tests/test_dependency_class.py @@ -1,3 +1,5 @@ +from typing import AsyncGenerator, Generator + import pytest from fastapi import Depends, FastAPI from fastapi.testclient import TestClient @@ -10,11 +12,21 @@ class CallableDependency: return value +class CallableGenDependency: + def __call__(self, value: str) -> Generator[str, None, None]: + yield value + + class AsyncCallableDependency: async def __call__(self, value: str) -> str: return value +class AsyncCallableGenDependency: + async def __call__(self, value: str) -> AsyncGenerator[str, None]: + yield value + + class MethodsDependency: def synchronous(self, value: str) -> str: return value @@ -22,9 +34,17 @@ class MethodsDependency: async def asynchronous(self, value: str) -> str: return value + def synchronous_gen(self, value: str) -> Generator[str, None, None]: + yield value + + async def asynchronous_gen(self, value: str) -> AsyncGenerator[str, None]: + yield value + callable_dependency = CallableDependency() +callable_gen_dependency = CallableGenDependency() async_callable_dependency = AsyncCallableDependency() +async_callable_gen_dependency = AsyncCallableGenDependency() methods_dependency = MethodsDependency() @@ -33,11 +53,23 @@ async def get_callable_dependency(value: str = Depends(callable_dependency)): return value +@app.get("/callable-gen-dependency") +async def get_callable_gen_dependency(value: str = Depends(callable_gen_dependency)): + return value + + @app.get("/async-callable-dependency") async def get_callable_dependency(value: str = Depends(async_callable_dependency)): return value +@app.get("/async-callable-gen-dependency") +async def get_callable_gen_dependency( + value: str = Depends(async_callable_gen_dependency), +): + return value + + @app.get("/synchronous-method-dependency") async def get_synchronous_method_dependency( value: str = Depends(methods_dependency.synchronous), @@ -45,6 +77,13 @@ async def get_synchronous_method_dependency( return value +@app.get("/synchronous-method-gen-dependency") +async def get_synchronous_method_gen_dependency( + value: str = Depends(methods_dependency.synchronous_gen), +): + return value + + @app.get("/asynchronous-method-dependency") async def get_asynchronous_method_dependency( value: str = Depends(methods_dependency.asynchronous), @@ -52,6 +91,13 @@ async def get_asynchronous_method_dependency( return value +@app.get("/asynchronous-method-gen-dependency") +async def get_asynchronous_method_gen_dependency( + value: str = Depends(methods_dependency.asynchronous_gen), +): + return value + + client = TestClient(app) @@ -59,9 +105,13 @@ client = TestClient(app) "route,value", [ ("/callable-dependency", "callable-dependency"), + ("/callable-gen-dependency", "callable-gen-dependency"), ("/async-callable-dependency", "async-callable-dependency"), + ("/async-callable-gen-dependency", "async-callable-gen-dependency"), ("/synchronous-method-dependency", "synchronous-method-dependency"), + ("/synchronous-method-gen-dependency", "synchronous-method-gen-dependency"), ("/asynchronous-method-dependency", "asynchronous-method-dependency"), + ("/asynchronous-method-gen-dependency", "asynchronous-method-gen-dependency"), ], ) def test_class_dependency(route, value): From 4a5cda0d77682f68dfa899c7748d04830e1e9c3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 12 Jun 2020 23:00:09 +0200 Subject: [PATCH 21/82] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index fca665838..70a800461 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Fix support for callable class dependencies with `yield`. PR [#1365](https://github.com/tiangolo/fastapi/pull/1365) by [@mrosales](https://github.com/mrosales). * Fix/remove incorrect error logging when a client sends invalid payloads. PR [#1351](https://github.com/tiangolo/fastapi/pull/1351) by [@dbanty](https://github.com/dbanty). * Add translation to Chinese for [First Steps - 第一步](https://fastapi.tiangolo.com/zh/tutorial/first-steps/). PR [#1323](https://github.com/tiangolo/fastapi/pull/1323) by [@waynerv](https://github.com/waynerv). * Fix generating OpenAPI for apps using callbacks with routers including Pydantic models. PR [#1322](https://github.com/tiangolo/fastapi/pull/1322) by [@nsidnev](https://github.com/nsidnev). From 745ab48d655937ba0315083e7c0b579da47a7915 Mon Sep 17 00:00:00 2001 From: yaegassy Date: Sat, 13 Jun 2020 06:44:23 +0900 Subject: [PATCH 22/82] =?UTF-8?q?=F0=9F=93=9D=20Add=20docs=20in=20Python?= =?UTF-8?q?=20Types=20for=20Optional=20(#1377)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: Fix pydantic example in python-types.md * 📝 Update Python Types Intro to include Optional Co-authored-by: Sebastián Ramírez --- docs/en/docs/python-types.md | 58 +++++++++++++++++++++------- docs_src/python_types/tutorial009.py | 11 +++--- docs_src/python_types/tutorial010.py | 26 +++---------- docs_src/python_types/tutorial011.py | 23 +++++++++++ 4 files changed, 77 insertions(+), 41 deletions(-) create mode 100644 docs_src/python_types/tutorial011.py diff --git a/docs/en/docs/python-types.md b/docs/en/docs/python-types.md index 46420362c..b0dd05468 100644 --- a/docs/en/docs/python-types.md +++ b/docs/en/docs/python-types.md @@ -144,15 +144,15 @@ You can use, for example: {!../../../docs_src/python_types/tutorial005.py!} ``` -### Types with subtypes +### Generic types with type parameters There are some data structures that can contain other values, like `dict`, `list`, `set` and `tuple`. And the internal values can have their own type too. -To declare those types and the subtypes, you can use the standard Python module `typing`. +To declare those types and the internal types, you can use the standard Python module `typing`. It exists specifically to support these type hints. -#### Lists +#### `List` For example, let's define a variable to be a `list` of `str`. @@ -166,25 +166,30 @@ Declare the variable, with the same colon (`:`) syntax. As the type, put the `List`. -As the list is a type that takes a "subtype", you put the subtype in square brackets: +As the list is a type that contains some internal types, you put them in square brackets: ```Python hl_lines="4" {!../../../docs_src/python_types/tutorial006.py!} ``` +!!! tip + Those internal types in the square brackets are called "type parameters". + + In this case, `str` is the type parameter passed to `List`. + That means: "the variable `items` is a `list`, and each of the items in this list is a `str`". -By doing that, your editor can provide support even while processing items from the list. - -Without types, that's almost impossible to achieve: +By doing that, your editor can provide support even while processing items from the list: +Without types, that's almost impossible to achieve. + Notice that the variable `item` is one of the elements in the list `items`. And still, the editor knows it is a `str`, and provides support for that. -#### Tuples and Sets +#### `Tuple` and `Set` You would do the same to declare `tuple`s and `set`s: @@ -197,13 +202,13 @@ This means: * The variable `items_t` is a `tuple` with 3 items, an `int`, another `int`, and a `str`. * The variable `items_s` is a `set`, and each of its items is of type `bytes`. -#### Dicts +#### `Dict` -To define a `dict`, you pass 2 subtypes, separated by commas. +To define a `dict`, you pass 2 type parameters, separated by commas. -The first subtype is for the keys of the `dict`. +The first type parameter is for the keys of the `dict`. -The second subtype is for the values of the `dict`: +The second type parameter is for the values of the `dict`: ```Python hl_lines="1 4" {!../../../docs_src/python_types/tutorial008.py!} @@ -215,6 +220,29 @@ This means: * The keys of this `dict` are of type `str` (let's say, the name of each item). * The values of this `dict` are of type `float` (let's say, the price of each item). +#### `Optional` + +You can also use `Optional` to declare that a variable has a type, like `str`, but that it is "optional", which means that it could also be `None`: + +```Python hl_lines="1 4" +{!../../../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. + +#### Generic types + +These types that take type parameters in square brackets, like: + +* `List` +* `Tuple` +* `Set` +* `Dict` +* `Optional` +* ...and others. + +are called **Generic types** or **Generics**. + ### Classes as types You can also declare a class as the type of a variable. @@ -222,13 +250,13 @@ You can also declare a class as the type of a variable. Let's say you have a class `Person`, with a name: ```Python hl_lines="1 2 3" -{!../../../docs_src/python_types/tutorial009.py!} +{!../../../docs_src/python_types/tutorial010.py!} ``` Then you can declare a variable to be of type `Person`: ```Python hl_lines="6" -{!../../../docs_src/python_types/tutorial009.py!} +{!../../../docs_src/python_types/tutorial010.py!} ``` And then, again, you get all the editor support: @@ -250,7 +278,7 @@ And you get all the editor support with that resulting object. Taken from the official Pydantic docs: ```Python -{!../../../docs_src/python_types/tutorial010.py!} +{!../../../docs_src/python_types/tutorial011.py!} ``` !!! info diff --git a/docs_src/python_types/tutorial009.py b/docs_src/python_types/tutorial009.py index 468cffc2d..6328a1495 100644 --- a/docs_src/python_types/tutorial009.py +++ b/docs_src/python_types/tutorial009.py @@ -1,7 +1,8 @@ -class Person: - def __init__(self, name: str): - self.name = name +from typing import Optional -def get_person_name(one_person: Person): - return one_person.name +def say_hi(name: Optional[str] = None): + if name is not None: + print(f"Hey {name}!") + else: + print("Hello World") diff --git a/docs_src/python_types/tutorial010.py b/docs_src/python_types/tutorial010.py index faeb02a58..468cffc2d 100644 --- a/docs_src/python_types/tutorial010.py +++ b/docs_src/python_types/tutorial010.py @@ -1,23 +1,7 @@ -from datetime import datetime -from typing import List - -from pydantic import BaseModel +class Person: + def __init__(self, name: str): + self.name = name -class User(BaseModel): - id: int - name = "John Doe" - signup_ts: datetime = None - friends: List[int] = [] - - -external_data = { - "id": "123", - "signup_ts": "2017-06-01 12:22", - "friends": [1, "2", b"3"], -} -user = User(**external_data) -print(user) -# > User id=123 name='John Doe' signup_ts=datetime.datetime(2017, 6, 1, 12, 22) friends=[1, 2, 3] -print(user.id) -# > 123 +def get_person_name(one_person: Person): + return one_person.name diff --git a/docs_src/python_types/tutorial011.py b/docs_src/python_types/tutorial011.py new file mode 100644 index 000000000..047b633b5 --- /dev/null +++ b/docs_src/python_types/tutorial011.py @@ -0,0 +1,23 @@ +from datetime import datetime +from typing import List, Optional + +from pydantic import BaseModel + + +class User(BaseModel): + id: int + name = "John Doe" + signup_ts: Optional[datetime] = None + friends: List[int] = [] + + +external_data = { + "id": "123", + "signup_ts": "2017-06-01 12:22", + "friends": [1, "2", b"3"], +} +user = User(**external_data) +print(user) +# > User id=123 name='John Doe' signup_ts=datetime.datetime(2017, 6, 1, 12, 22) friends=[1, 2, 3] +print(user.id) +# > 123 From 352412a3cb022bf6df1fc47723a6e2526020609f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 12 Jun 2020 23:46:05 +0200 Subject: [PATCH 23/82] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 70a800461..f9171f8ba 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Update docs in [Python Types Intro](https://fastapi.tiangolo.com/python-types/) to include info about `Optional`. Original PR [#1377](https://github.com/tiangolo/fastapi/pull/1377) by [@yaegassy](https://github.com/yaegassy). * Fix support for callable class dependencies with `yield`. PR [#1365](https://github.com/tiangolo/fastapi/pull/1365) by [@mrosales](https://github.com/mrosales). * Fix/remove incorrect error logging when a client sends invalid payloads. PR [#1351](https://github.com/tiangolo/fastapi/pull/1351) by [@dbanty](https://github.com/dbanty). * Add translation to Chinese for [First Steps - 第一步](https://fastapi.tiangolo.com/zh/tutorial/first-steps/). PR [#1323](https://github.com/tiangolo/fastapi/pull/1323) by [@waynerv](https://github.com/waynerv). From 69974b792e7ee4a65b63ca780adc349351fade5d Mon Sep 17 00:00:00 2001 From: Kabir Khan Date: Fri, 12 Jun 2020 15:06:53 -0700 Subject: [PATCH 24/82] =?UTF-8?q?=F0=9F=93=9D=20Add=20cookiecutter-spacy-f?= =?UTF-8?q?astapi=20to=20docs=20(#1390)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/project-generation.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/en/docs/project-generation.md b/docs/en/docs/project-generation.md index 2cc2159fc..05d853c57 100644 --- a/docs/en/docs/project-generation.md +++ b/docs/en/docs/project-generation.md @@ -69,3 +69,16 @@ You can read more about it in the docs for the repo. ## Full Stack FastAPI MongoDB ...might come later, depending on my time availability and other factors. 😅 🎉 + +## Machine Learning models with spaCy and FastAPI + +GitHub: https://github.com/microsoft/cookiecutter-spacy-fastapi + +### Machine Learning models with spaCy and FastAPI - Features + +* **spaCy** NER model integration. +* **Azure Cognitive Search** request format built in. +* **Production ready** Python web server using Uvicorn and Gunicorn. +* **Azure DevOps** Kubernetes (AKS) CI/CD deployment built in. +* **Multilingual** Easily choose one of spaCy's built in languages during project setup. +* **Easily extensible** to other model frameworks (Pytorch, Tensorflow), not just spaCy. From 28396173c776d2fd1ea86c8d35c1730421704539 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 13 Jun 2020 00:09:41 +0200 Subject: [PATCH 25/82] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index f9171f8ba..253a493bf 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Add official Microsoft project generator for [serving spaCy with FastAPI and Azure Cognitive Skills](https://github.com/microsoft/cookiecutter-spacy-fastapi) to [Project Generators](https://fastapi.tiangolo.com/project-generation/). PR [#1390](https://github.com/tiangolo/fastapi/pull/1390) by [@kabirkhan](https://github.com/kabirkhan). * Update docs in [Python Types Intro](https://fastapi.tiangolo.com/python-types/) to include info about `Optional`. Original PR [#1377](https://github.com/tiangolo/fastapi/pull/1377) by [@yaegassy](https://github.com/yaegassy). * Fix support for callable class dependencies with `yield`. PR [#1365](https://github.com/tiangolo/fastapi/pull/1365) by [@mrosales](https://github.com/mrosales). * Fix/remove incorrect error logging when a client sends invalid payloads. PR [#1351](https://github.com/tiangolo/fastapi/pull/1351) by [@dbanty](https://github.com/dbanty). From c1ba2a3127899a0d44cdfba5d9c2d1d9b61c1728 Mon Sep 17 00:00:00 2001 From: Xie Wei <39515546+waynerv@users.noreply.github.com> Date: Sat, 13 Jun 2020 06:14:58 +0800 Subject: [PATCH 26/82] =?UTF-8?q?=F0=9F=8C=90=20Add=20Chinese=20translatio?= =?UTF-8?q?n=20for=20path-params.md=20(#1453)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add chinese translation for path-params.md * improve translations * improve translations Co-authored-by: Waynerv Co-authored-by: Sebastián Ramírez --- docs/zh/docs/tutorial/path-params.md | 234 +++++++++++++++++++++++++++ docs/zh/mkdocs.yml | 1 + 2 files changed, 235 insertions(+) create mode 100644 docs/zh/docs/tutorial/path-params.md diff --git a/docs/zh/docs/tutorial/path-params.md b/docs/zh/docs/tutorial/path-params.md new file mode 100644 index 000000000..db35e9564 --- /dev/null +++ b/docs/zh/docs/tutorial/path-params.md @@ -0,0 +1,234 @@ +# 路径参数 + +你可以使用与 Python 格式化字符串相同的语法来声明路径"参数"或"变量": + +```Python hl_lines="6 7" +{!../../../docs_src/path_params/tutorial001.py!} +``` + +路径参数 `item_id` 的值将作为参数 `item_id` 传递给你的函数。 + +所以,如果你运行示例并访问 http://127.0.0.1:8000/items/foo,将会看到如下响应: + +```JSON +{"item_id":"foo"} +``` + +## 有类型的路径参数 + +你可以使用标准的 Python 类型标注为函数中的路径参数声明类型。 + +```Python hl_lines="7" +{!../../../docs_src/path_params/tutorial002.py!} +``` + +在这个例子中,`item_id` 被声明为 `int` 类型。 + +!!! check + 这将为你的函数提供编辑器支持,包括错误检查、代码补全等等。 + +## 数据转换 + +如果你运行示例并打开浏览器访问 http://127.0.0.1:8000/items/3,将得到如下响应: + +```JSON +{"item_id":3} +``` + +!!! check + 注意函数接收(并返回)的值为 3,是一个 Python `int` 值,而不是字符串 `"3"`。 + + 所以,**FastAPI** 通过上面的类型声明提供了对请求的自动"解析"。 + +## 数据校验 + +但如果你通过浏览器访问 http://127.0.0.1:8000/items/foo,你会看到一个清晰可读的 HTTP 错误: + +```JSON +{ + "detail": [ + { + "loc": [ + "path", + "item_id" + ], + "msg": "value is not a valid integer", + "type": "type_error.integer" + } + ] +} +``` + +因为路径参数 `item_id` 传入的值为 `"foo"`,它不是一个 `int`。 + +如果你提供的是 `float` 而非整数也会出现同样的错误,比如: http://127.0.0.1:8000/items/4.2 + +!!! check + 所以,通过同样的 Python 类型声明,**FastAPI** 提供了数据校验功能。 + + 注意上面的错误同样清楚地指出了校验未通过的具体原因。 + + 在开发和调试与你的 API 进行交互的代码时,这非常有用。 + +## 文档 + +当你打开浏览器访问 http://127.0.0.1:8000/docs,你将看到自动生成的交互式 API 文档: + + + +!!! check + 再一次,还是通过相同的 Python 类型声明,**FastAPI** 为你提供了自动生成的交互式文档(集成 Swagger UI)。 + + 注意这里的路径参数被声明为一个整数。 + +## 基于标准的好处:可选文档 + +由于生成的 API 模式来自于 OpenAPI 标准,所以有很多工具与其兼容。 + +正因如此,**FastAPI** 内置了一个可选的 API 文档(使用 Redoc): + + + +同样的,还有很多其他兼容的工具,包括适用于多种语言的代码生成工具。 + +## Pydantic + +所有的数据校验都由 Pydantic 在幕后完成,所以你可以从它所有的优点中受益。并且你知道它在这方面非常胜任。 + +你可以使用同样的类型声明来声明 `str`、`float`、`bool` 以及许多其他的复合数据类型。 + +本教程的下一章节将探讨其中的一些内容。 + +## 顺序很重要 + +在创建*路径操作*时,你会发现有些情况下路径是固定的。 + +比如 `/users/me`,我们假设它用来获取关于当前用户的数据. + +然后,你还可以使用路径 `/users/{user_id}` 来通过用户 ID 获取关于特定用户的数据。 + +由于*路径操作*是按顺序依次运行的,你需要确保路径 `/users/me` 声明在路径 `/users/{user_id}`之前: +```Python hl_lines="6 11" +{!../../../docs_src/path_params/tutorial003.py!} +``` + +否则,`/users/{user_id}` 的路径还将与 `/users/me` 相匹配,"认为"自己正在接收一个值为 `"me"` 的 `user_id` 参数。 + +## 预设值 + +如果你有一个接收路径参数的路径操作,但你希望预先设定可能的有效参数值,则可以使用标准的 Python `Enum` 类型。 + +### 创建一个 `Enum` 类 + +导入 `Enum` 并创建一个继承自 `str` 和 `Enum` 的子类。 + +通过从 `str` 继承,API 文档将能够知道这些值必须为 `string` 类型并且能够正确地展示出来。 + +然后创建具有固定值的类属性,这些固定值将是可用的有效值: + +```Python hl_lines="1 6 7 8 9" +{!../../../docs_src/path_params/tutorial005.py!} +``` + +!!! info + 枚举(或 enums)从 3.4 版本起在 Python 中可用。 + +!!! tip + 如果你想知道,"AlexNet"、"ResNet" 和 "LeNet" 只是机器学习中的模型名称。 + +### 声明*路径参数* + +然后使用你定义的枚举类(`ModelName`)创建一个带有类型标注的*路径参数*: + +```Python hl_lines="16" +{!../../../docs_src/path_params/tutorial005.py!} +``` + +### 查看文档 + +因为已经指定了*路径参数*的可用值,所以交互式文档可以恰当地展示它们: + + + +### 使用 Python *枚举类型* + +*路径参数*的值将是一个*枚举成员*。 + +#### 比较*枚举成员* + +你可以将它与你创建的枚举类 `ModelName` 中的*枚举成员*进行比较: + +```Python hl_lines="17" +{!../../../docs_src/path_params/tutorial005.py!} +``` + +#### 获取*枚举值* + +你可以使用 `model_name.value` 或通常来说 `your_enum_member.value` 来获取实际的值(在这个例子中为 `str`): + +```Python hl_lines="19" +{!../../../docs_src/path_params/tutorial005.py!} +``` + +!!! tip + 你也可以通过 `ModelName.lenet.value` 来获取值 `"lenet"`。 + +#### 返回*枚举成员* + +你可以从*路径操作*中返回*枚举成员*,即使嵌套在 JSON 结构中(例如一个 `dict` 中)。 + +在返回给客户端之前,它们将被转换为对应的值: + +```Python hl_lines="18 20 21" +{!../../../docs_src/path_params/tutorial005.py!} +``` + +## 包含路径的路径参数 + +假设你有一个*路径操作*,它的路径为 `/files/{file_path}`。 + +但是你需要 `file_path` 自身也包含*路径*,比如 `home/johndoe/myfile.txt`。 + +因此,该文件的URL将类似于这样:`/files/home/johndoe/myfile.txt`。 + +### OpenAPI 支持 + +OpenAPI 不支持任何方式去声明*路径参数*以在其内部包含*路径*,因为这可能会导致难以测试和定义的情况出现。 + +不过,你仍然可以通过 Starlette 的一个内部工具在 **FastAPI** 中实现它。 + +而且文档依旧可以使用,但是不会添加任何该参数应包含路径的说明。 + +### 路径转换器 + +你可以使用直接来自 Starlette 的选项来声明一个包含*路径*的*路径参数*: + +``` +/files/{file_path:path} +``` + +在这种情况下,参数的名称为 `file_path`,结尾部分的 `:path` 说明该参数应匹配任意的*路径*。 + +因此,你可以这样使用它: + +```Python hl_lines="6" +{!../../../docs_src/path_params/tutorial004.py!} +``` + +!!! tip + 你可能会需要参数包含 `/home/johndoe/myfile.txt`,以斜杠(`/`)开头。 + + 在这种情况下,URL 将会是 `/files//home/johndoe/myfile.txt`,在`files` 和 `home` 之间有一个双斜杠(`//`)。 + +## 总结 + +使用 **FastAPI**,通过简短、直观和标准的 Python 类型声明,你将获得: + +* 编辑器支持:错误检查,代码补全等 +* 数据 "解析" +* 数据校验 +* API 标注和自动生成的文档 + +而且你只需要声明一次即可。 + +这可能是 **FastAPI** 与其他框架相比主要的明显优势(除了原始性能以外)。 diff --git a/docs/zh/mkdocs.yml b/docs/zh/mkdocs.yml index 4f7233827..3cf3fd4fe 100644 --- a/docs/zh/mkdocs.yml +++ b/docs/zh/mkdocs.yml @@ -30,6 +30,7 @@ nav: - 教程 - 用户指南: - tutorial/index.md - tutorial/first-steps.md + - tutorial/path-params.md - deployment.md markdown_extensions: - toc: From f910e0c96c4ffd5e3ddd893776ccec217ae86807 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 13 Jun 2020 00:18:20 +0200 Subject: [PATCH 27/82] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 253a493bf..e8f184dbe 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Add translation to Chinese for [Path Parameters - 路径参数](https://fastapi.tiangolo.com/zh/tutorial/path-params/). PR [#1453](https://github.com/tiangolo/fastapi/pull/1453) by [@waynerv](https://github.com/waynerv). * Add official Microsoft project generator for [serving spaCy with FastAPI and Azure Cognitive Skills](https://github.com/microsoft/cookiecutter-spacy-fastapi) to [Project Generators](https://fastapi.tiangolo.com/project-generation/). PR [#1390](https://github.com/tiangolo/fastapi/pull/1390) by [@kabirkhan](https://github.com/kabirkhan). * Update docs in [Python Types Intro](https://fastapi.tiangolo.com/python-types/) to include info about `Optional`. Original PR [#1377](https://github.com/tiangolo/fastapi/pull/1377) by [@yaegassy](https://github.com/yaegassy). * Fix support for callable class dependencies with `yield`. PR [#1365](https://github.com/tiangolo/fastapi/pull/1365) by [@mrosales](https://github.com/mrosales). From b49517a64fd707286d9c67282f066a5d0f8c4ae1 Mon Sep 17 00:00:00 2001 From: Xie Wei <39515546+waynerv@users.noreply.github.com> Date: Sat, 13 Jun 2020 06:18:57 +0800 Subject: [PATCH 28/82] =?UTF-8?q?=F0=9F=8C=90=20Add=20Chinese=20translatio?= =?UTF-8?q?n=20for=20contributing.md=20(#1460)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Waynerv --- docs/zh/docs/contributing.md | 510 +++++++++++++++++++++++++++++++++++ docs/zh/mkdocs.yml | 1 + 2 files changed, 511 insertions(+) create mode 100644 docs/zh/docs/contributing.md diff --git a/docs/zh/docs/contributing.md b/docs/zh/docs/contributing.md new file mode 100644 index 000000000..e9645392e --- /dev/null +++ b/docs/zh/docs/contributing.md @@ -0,0 +1,510 @@ +# 开发 - 贡献 + +首先,你最好先了解 [帮助 FastAPI 及获取帮助](help-fastapi.md){.internal-link target=_blank}的基本方式。 + +## 开发 + +如果你已经克隆了源码仓库,并且需要深入研究代码,下面是设置开发环境的指南。 + +### 通过 `venv` 管理虚拟环境 + +你可以使用 Python 的 `venv` 模块在一个目录中创建虚拟环境: + +
+ +```console +$ python -m venv env +``` + +
+ +这将使用 Python 程序创建一个 `./env/` 目录,然后你将能够为这个隔离的环境安装软件包。 + +### 激活虚拟环境 + +使用以下方法激活新环境: + +=== "Linux, macOS" + +
+ + ```console + $ source ./env/bin/activate + ``` + +
+ +=== "Windows PowerShell" + +
+ + ```console + $ .\env\Scripts\Activate.ps1 + ``` + +
+ +=== "Windows Bash" + + Or if you use Bash for Windows (e.g. Git Bash): + +
+ + ```console + $ source ./env/Scripts/activate + ``` + +
+ +要检查操作是否成功,运行: + +=== "Linux, macOS, Windows Bash" + +
+ + ```console + $ which pip + + some/directory/fastapi/env/bin/pip + ``` + +
+ +=== "Windows PowerShell" + +
+ + ```console + $ Get-Command pip + + some/directory/fastapi/env/bin/pip + ``` + +
+ +如果显示 `pip` 程序文件位于 `env/bin/pip` 则说明激活成功。 🎉 + + +!!! tip + 每一次你在该环境下使用 `pip` 安装了新软件包时,请再次激活该环境。 + + 这样可以确保你在使用由该软件包安装的终端程序(如 `flit`)时使用的是当前虚拟环境中的程序,而不是其他的可能是全局安装的程序。 + +### Flit + +**FastAPI** 使用 Flit 来构建、打包和发布项目。 + +如上所述激活环境后,安装 `flit`: + +
+ +```console +$ pip install flit + +---> 100% +``` + +
+ +现在重新激活环境,以确保你正在使用的是刚刚安装的 `flit`(而不是全局环境的)。 + +然后使用 `flit` 来安装开发依赖: + +=== "Linux, macOS" + +
+ + ```console + $ flit install --deps develop --symlink + + ---> 100% + ``` + +
+ +=== "Windows" + + If you are on Windows, use `--pth-file` instead of `--symlink`: + +
+ + ```console + $ flit install --deps develop --pth-file + + ---> 100% + ``` + +
+ +这将在虚拟环境中安装所有依赖和本地版本的 FastAPI。 + +#### 使用本地 FastAPI + +如果你创建一个导入并使用 FastAPI 的 Python 文件,然后使用虚拟环境中的 Python 运行它,它将使用你本地的 FastAPI 源码。 + +并且如果你更改该本地 FastAPI 的源码,由于它是通过 `--symlink` (或 Windows 上的 `--pth-file`)安装的,当你再次运行那个 Python 文件,它将使用你刚刚编辑过的最新版本的 FastAPI。 + +这样,你不必再去重新"安装"你的本地版本即可测试所有更改。 + +### 格式化 + +你可以运行下面的脚本来格式化和清理所有代码: + +
+ +```console +$ bash scripts/format.sh +``` + +
+ +它还会自动对所有导入代码进行整理。 + +为了使整理正确进行,你需要在当前环境中安装本地的 FastAPI,即在运行上述段落中的命令时添加 `--symlink`(或 Windows 上的 `--pth-file`)。 + +### 格式化导入 + +还有另一个脚本可以格式化所有导入,并确保你没有未使用的导入代码: + +
+ +```console +$ bash scripts/format-imports.sh +``` + +
+ +由于它依次运行了多个命令,并修改和还原了许多文件,所以运行时间会更长一些,因此经常地使用 `scripts/format.sh` 然后仅在提交前执行 `scripts/format-imports.sh` 会更好一些。 + +## 文档 + +首先,请确保按上述步骤设置好环境,这将安装所有需要的依赖。 + +文档使用 MkDocs 生成。 + +并且在 `./scripts/docs.py` 中还有适用的额外工具/脚本来处理翻译。 + +!!! tip + 你不需要去了解 `./scripts/docs.py` 中的代码,只需在命令行中使用它即可。 + +所有文档均在 `./docs/en/` 目录中以 Markdown 文件格式保存。 + +许多的教程章节里包含有代码块。 + +在大多数情况下,这些代码块是可以直接运行的真实完整的应用程序。 + +实际上,这些代码块不是写在 Markdown 文件内的,它们是位于 `./docs_src/` 目录中的 Python 文件。 + +生成站点时,这些 Python 文件会被包含/注入到文档中。 + +### 用于测试的文档 + +大多数的测试实际上都是针对文档中的示例源文件运行的。 + +这有助于确保: + +* 文档始终是最新的。 +* 文档示例可以直接运行。 +* 绝大多数特性既在文档中得以阐述,又通过测试覆盖进行保障。 + +在本地开发期间,有一个脚本可以实时重载地构建站点并用来检查所做的任何更改: + +
+ +```console +$ python ./scripts/docs.py live + +[INFO] Serving on http://127.0.0.1:8008 +[INFO] Start watching changes +[INFO] Start detecting changes +``` + +
+ +它将在 `http://127.0.0.1:8008` 提供对文档的访问。 + +这样,你可以编辑文档/源文件并实时查看更改。 + +#### Typer CLI (可选) + +本指引向你展示了如何直接用 `python` 程序运行 `./scripts/docs.py` 中的脚本。 + +但你也可以使用 Typer CLI,而且在安装了补全功能后,你将可以在终端中对命令进行自动补全。 + +如果你打算安装 Typer CLI ,可以使用以下命令安装自动补全功能: + +
+ +```console +$ typer --install-completion + +zsh completion installed in /home/user/.bashrc. +Completion will take effect once you restart the terminal. +``` + +
+ +### 应用和文档同时运行 + +如果你使用以下方式运行示例程序: + +
+ +```console +$ uvicorn tutorial001:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +由于 Uvicorn 默认使用 `8000` 端口 ,因此运行在 `8008` 端口上的文档不会与之冲突。 + +### 翻译 + +非常感谢你能够参与文档的翻译!这项工作需要社区的帮助才能完成。 🌎 🚀 + +以下是参与帮助翻译的步骤。 + +#### 建议和指南 + +* 在当前 已有的 pull requests 中查找你使用的语言,添加要求修改或同意合并的评审意见。 + +!!! tip + 你可以为已有的 pull requests 添加包含修改建议的评论。 + + 详情可查看关于 添加 pull request 评审意见 以同意合并或要求修改的文档。 + +* 在 issues 中查找是否有对你所用语言所进行的协作翻译。 + +* 每翻译一个页面新增一个 pull request。这将使其他人更容易对其进行评审。 + +对于我(译注:作者使用西班牙语和英语)不懂的语言,我将在等待其他人评审翻译之后将其合并。 + +* 你还可以查看是否有你所用语言的翻译,并对其进行评审,这将帮助我了解翻译是否正确以及能否将其合并。 + +* 使用相同的 Python 示例并且仅翻译文档中的文本。无需进行任何其他更改示例也能正常工作。 + +* 使用相同的图片、文件名以及链接地址。无需进行任何其他调整来让它们兼容。 + +* 你可以从 ISO 639-1 代码列表 表中查找你想要翻译语言的两位字母代码。 + +#### 已有的语言 + +假设你想将某个页面翻译成已经翻译了一些页面的语言,例如西班牙语。 + +对于西班牙语来说,它的两位字母代码是 `es`。所以西班牙语翻译的目录位于 `docs/es/`。 + +!!! tip + 主要("官方")语言是英语,位于 `docs/en/`目录。 + +现在为西班牙语文档运行实时服务器: + +
+ +```console +// Use the command "live" and pass the language code as a CLI argument +$ python ./scripts/docs.py live es + +[INFO] Serving on http://127.0.0.1:8008 +[INFO] Start watching changes +[INFO] Start detecting changes +``` + +
+ +现在你可以访问 http://127.0.0.1:8008 实时查看你所做的更改。 + +如果你查看 FastAPI 的线上文档网站,会看到每种语言都有所有页面。但是某些页面并未被翻译并且会有一处关于缺少翻译的提示。 + +但是当你像上面这样在本地运行文档时,你只会看到已经翻译的页面。 + +现在假设你要为 [Features](features.md){.internal-link target=_blank} 章节添加翻译。 + +* 复制下面的文件: + +``` +docs/en/docs/features.md +``` + +* 粘贴到你想要翻译语言目录的相同位置,比如: + +``` +docs/es/docs/features.md +``` + +!!! tip + 注意路径和文件名的唯一变化是语言代码,从 `en` 更改为 `es`。 + +* 现在打开位于英语文档目录下的 MkDocs 配置文件: + +``` +docs/en/docs/mkdocs.yml +``` + +* 在配置文件中找到 `docs/features.md` 所在的位置。结果像这样: + +```YAML hl_lines="8" +site_name: FastAPI +# More stuff +nav: +- FastAPI: index.md +- Languages: + - en: / + - es: /es/ +- features.md +``` + +* 打开你正在编辑的语言目录中的 MkDocs 配置文件,例如: + +``` +docs/es/docs/mkdocs.yml +``` + +* 将其添加到与英语文档完全相同的位置,例如: + +```YAML hl_lines="8" +site_name: FastAPI +# More stuff +nav: +- FastAPI: index.md +- Languages: + - en: / + - es: /es/ +- features.md +``` + +如果配置文件中还有其他条目,请确保你所翻译的新条目和它们之间的顺序与英文版本完全相同。 + +打开浏览器,现在你将看到文档展示了你所加入的新章节。 🎉 + +现在,你可以将它全部翻译完并在保存文件后进行预览。 + +#### 新语言 + +假设你想要为尚未有任何页面被翻译的语言添加翻译。 + +假设你想要添加克里奥尔语翻译,而且文档中还没有该语言的翻译。 + +点击上面提到的链接,可以查到"克里奥尔语"的代码为 `ht`。 + +下一步是运行脚本以生成新的翻译目录: + +
+ +```console +// Use the command new-lang, pass the language code as a CLI argument +$ python ./scripts/docs.py new-lang ht + +Successfully initialized: docs/ht +Updating ht +Updating en +``` + +
+ +现在,你可以在编辑器中查看新创建的目录 `docs/ht/`。 + +!!! tip + 在添加实际的翻译之前,仅以此创建首个 pull request 来设定新语言的配置。 + + 这样当你在翻译第一个页面时,其他人可以帮助翻译其他页面。🚀 + +首先翻译文档主页 `docs/ht/index.md`。 + +然后,你可以根据上面的"已有语言"的指引继续进行翻译。 + +##### 不支持的新语言 + +如果在运行实时服务器脚本时收到关于不支持该语言的错误,类似于: + +``` + raise TemplateNotFound(template) +jinja2.exceptions.TemplateNotFound: partials/language/xx.html +``` + +这意味着文档的主题不支持该语言(在这种例子中,编造的语言代码是 `xx`)。 + +但是别担心,你可以将主题语言设置为英语,然后翻译文档的内容。 + +如果你需要这么做,编辑新语言目录下的 `mkdocs.yml`,它将有类似下面的内容: + +```YAML hl_lines="5" +site_name: FastAPI +# More stuff +theme: + # More stuff + language: xx +``` + +将其中的 language 项从 `xx`(你的语言代码)更改为 `en`。 + +然后,你就可以再次启动实时服务器了。 + +#### 预览结果 + +当你通过 `live` 命令使用 `./scripts/docs.py` 中的脚本时,该脚本仅展示当前语言已有的文件和翻译。 + +但是当你完成翻译后,你可以像在线上展示一样测试所有内容。 + +为此,首先构建所有文档: + +
+ +```console +// Use the command "build-all", this will take a bit +$ python ./scripts/docs.py build-all + +Updating es +Updating en +Building docs for: en +Building docs for: es +Successfully built docs for: es +Copying en index.md to README.md +``` + +
+ +这将在 `./docs_build/` 目录中为每一种语言生成全部的文档。还包括添加所有缺少翻译的文件,并带有一条"此文件还没有翻译"的提醒。但是你不需要对该目录执行任何操作。 + +然后,它针对每种语言构建独立的 MkDocs 站点,将它们组合在一起,并在 `./site/` 目录中生成最终的输出。 + +然后你可以使用命令 `serve` 来运行生成的站点: + +
+ +```console +// Use the command "serve" after running "build-all" +$ python ./scripts/docs.py serve + +Warning: this is a very simple server. For development, use mkdocs serve instead. +This is here only to preview a site with translations already built. +Make sure you run the build-all command first. +Serving at: http://127.0.0.1:8008 +``` + +
+ +## 测试 + +你可以在本地运行下面的脚本来测试所有代码并生成 HTML 格式的覆盖率报告: + +
+ +```console +$ bash scripts/test-cov-html.sh +``` + +
+ +该命令生成了一个 `./htmlcov/` 目录,如果你在浏览器中打开 `./htmlcov/index.html` 文件,你可以交互式地浏览被测试所覆盖的代码区块,并注意是否缺少了任何区块。 + +### 在编辑器中测试 + +如果你想要在编辑器中运行集成测试,请将 `./docs_src` 加入到你的 `PYTHONPATH` 变量中。 + +例如,在 VS Code 中你可以创建一个包含以下内容的 `.env` 文件: + +```env +PYTHONPATH=./docs_src +``` diff --git a/docs/zh/mkdocs.yml b/docs/zh/mkdocs.yml index 3cf3fd4fe..941461630 100644 --- a/docs/zh/mkdocs.yml +++ b/docs/zh/mkdocs.yml @@ -32,6 +32,7 @@ nav: - tutorial/first-steps.md - tutorial/path-params.md - deployment.md +- contributing.md markdown_extensions: - toc: permalink: true From 11723bca272db860e479d9aec17d24d04eb485f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 13 Jun 2020 00:21:53 +0200 Subject: [PATCH 29/82] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index e8f184dbe..b07e4c082 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Add translation to Chinese for [Contributing - 开发 - 贡献](https://fastapi.tiangolo.com/zh/contributing/). PR [#1460](https://github.com/tiangolo/fastapi/pull/1460) by [@waynerv](https://github.com/waynerv). * Add translation to Chinese for [Path Parameters - 路径参数](https://fastapi.tiangolo.com/zh/tutorial/path-params/). PR [#1453](https://github.com/tiangolo/fastapi/pull/1453) by [@waynerv](https://github.com/waynerv). * Add official Microsoft project generator for [serving spaCy with FastAPI and Azure Cognitive Skills](https://github.com/microsoft/cookiecutter-spacy-fastapi) to [Project Generators](https://fastapi.tiangolo.com/project-generation/). PR [#1390](https://github.com/tiangolo/fastapi/pull/1390) by [@kabirkhan](https://github.com/kabirkhan). * Update docs in [Python Types Intro](https://fastapi.tiangolo.com/python-types/) to include info about `Optional`. Original PR [#1377](https://github.com/tiangolo/fastapi/pull/1377) by [@yaegassy](https://github.com/yaegassy). From 2b4e88fa98baa989b512e06312ed98e103546bb7 Mon Sep 17 00:00:00 2001 From: Xie Wei <39515546+waynerv@users.noreply.github.com> Date: Sat, 13 Jun 2020 06:26:40 +0800 Subject: [PATCH 30/82] =?UTF-8?q?=F0=9F=8C=90=20Add=20Chinese=20translatio?= =?UTF-8?q?n=20for=20query-params.md=20(#1454)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Waynerv Co-authored-by: Sebastián Ramírez --- docs/zh/docs/tutorial/query-params.md | 226 ++++++++++++++++++++++++++ docs/zh/mkdocs.yml | 1 + 2 files changed, 227 insertions(+) create mode 100644 docs/zh/docs/tutorial/query-params.md diff --git a/docs/zh/docs/tutorial/query-params.md b/docs/zh/docs/tutorial/query-params.md new file mode 100644 index 000000000..c91392cde --- /dev/null +++ b/docs/zh/docs/tutorial/query-params.md @@ -0,0 +1,226 @@ +# 查询参数 + +声明不属于路径参数的其他函数参数时,它们将被自动解释为"查询字符串"参数 + +```Python hl_lines="9" +{!../../../docs_src/query_params/tutorial001.py!} +``` + +查询字符串是键值对的集合,这些键值对位于 URL 的 `?` 之后,并以 `&` 符号分隔。 + +例如,在以下 url 中: + +``` +http://127.0.0.1:8000/items/?skip=0&limit=10 +``` + +...查询参数为: + +* `skip`:对应的值为 `0` +* `limit`:对应的值为 `10` + +由于它们是 URL 的一部分,因此它们的"原始值"是字符串。 + +但是,当你为它们声明了 Python 类型(在上面的示例中为 `int`)时,它们将转换为该类型并针对该类型进行校验。 + +应用于路径参数的所有相同过程也适用于查询参数: + +* (很明显的)编辑器支持 +* 数据"解析" +* 数据校验 +* 自动生成文档 + +## 默认值 + +由于查询参数不是路径的固定部分,因此它们可以是可选的,并且可以有默认值。 + +在上面的示例中,它们具有 `skip=0` 和 `limit=10` 的默认值。 + +因此,访问 URL: + +``` +http://127.0.0.1:8000/items/ +``` + +将与访问以下地址相同: + +``` +http://127.0.0.1:8000/items/?skip=0&limit=10 +``` + +但是,如果你访问的是: + +``` +http://127.0.0.1:8000/items/?skip=20 +``` + +函数中的参数值将会是: + +* `skip=20`:在 URL 中设定的值 +* `limit=10`:使用默认值 + +## 可选参数 + +通过同样的方式,你可以将它们的默认值设置为 `None` 来声明可选查询参数: + +```Python hl_lines="7" +{!../../../docs_src/query_params/tutorial002.py!} +``` + +在这个例子中,函数参数 `q` 将是可选的,并且默认值为 `None`。 + +!!! check + 还要注意的是,**FastAPI** 足够聪明,能够分辨出参数 `item_id` 是路径参数而 `q` 不是,因此 `q` 是一个查询参数。 + +## 查询参数类型转换 + +你还可以声明 `bool` 类型,它们将被自动转换: + +```Python hl_lines="7" +{!../../../docs_src/query_params/tutorial003.py!} +``` + +这个例子中,如果你访问: + +``` +http://127.0.0.1:8000/items/foo?short=1 +``` + +或 + +``` +http://127.0.0.1:8000/items/foo?short=True +``` + +或 + +``` +http://127.0.0.1:8000/items/foo?short=true +``` + +或 + +``` +http://127.0.0.1:8000/items/foo?short=on +``` + +或 + +``` +http://127.0.0.1:8000/items/foo?short=yes +``` + +或任何其他的变体形式(大写,首字母大写等等),你的函数接收的 `short` 参数都会是布尔值 `True`。对于值为 `False` 的情况也是一样的。 + + +## 多个路径和查询参数 + +你可以同时声明多个路径参数和查询参数,**FastAPI** 能够识别它们。 + +而且你不需要以任何特定的顺序来声明。 + +它们将通过名称被检测到: + +```Python hl_lines="6 8" +{!../../../docs_src/query_params/tutorial004.py!} +``` + +## 必需查询参数 + +当你为非路径参数声明了默认值时(目前而言,我们所知道的仅有查询参数),则该参数不是必需的。 + +如果你不想添加一个特定的值,而只是想使该参数成为可选的,则将默认值设置为 `None`。 + +但当你想让一个查询参数成为必需的,不声明任何默认值就可以: + +```Python hl_lines="6 7" +{!../../../docs_src/query_params/tutorial005.py!} +``` + +这里的查询参数 `needy` 是类型为 `str` 的必需查询参数。 + +如果你在浏览器中打开一个像下面的 URL: + +``` +http://127.0.0.1:8000/items/foo-item +``` + +...因为没有添加必需的参数 `needy`,你将看到类似以下的错误: + +```JSON +{ + "detail": [ + { + "loc": [ + "query", + "needy" + ], + "msg": "field required", + "type": "value_error.missing" + } + ] +} +``` + +由于 `needy` 是必需参数,因此你需要在 URL 中设置它的值: + +``` +http://127.0.0.1:8000/items/foo-item?needy=sooooneedy +``` + +...这样就正常了: + +```JSON +{ + "item_id": "foo-item", + "needy": "sooooneedy" +} +``` + +当然,你也可以定义一些参数为必需的,一些具有默认值,而某些则完全是可选的: + +```Python hl_lines="7" +{!../../../docs_src/query_params/tutorial006.py!} +``` + +在这个例子中,有3个查询参数: + +* `needy`,一个必需的 `str` 类型参数。 +* `skip`,一个默认值为 `0` 的 `int` 类型参数。 +* `limit`,一个可选的 `int` 类型参数。 + +!!! tip + 你还可以像在 [路径参数](path-params.md#predefined-values){.internal-link target=_blank} 中那样使用 `Enum`。 + +## Optional 类型声明 + +!!! warning + 这可能是一个比较高级的使用场景。 + + 您也可以跳过它。 + +如果你正在使用 `mypy`,它可能会对如下的类型声明进行警告: + +```Python +limit: int = None +``` + +提示类似以下错误: + +``` +Incompatible types in assignment (expression has type "None", variable has type "int") +``` + +在这种情况下,你可以使用 `Optional` 来告诉 `mypy` 该值可以为 `None`,例如: + +```Python +from typing import Optional + +limit: Optional[int] = None +``` + +在一个*路径操作*中,看起来会是: + +```Python hl_lines="9" +{!../../../docs_src/query_params/tutorial007.py!} +``` diff --git a/docs/zh/mkdocs.yml b/docs/zh/mkdocs.yml index 941461630..bea294687 100644 --- a/docs/zh/mkdocs.yml +++ b/docs/zh/mkdocs.yml @@ -31,6 +31,7 @@ nav: - tutorial/index.md - tutorial/first-steps.md - tutorial/path-params.md + - tutorial/query-params.md - deployment.md - contributing.md markdown_extensions: From 5f6a14c413290913ca96a714d835aa853d1e060d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 13 Jun 2020 00:29:43 +0200 Subject: [PATCH 31/82] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index b07e4c082..3c57ca822 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Add translation to Chinese for [Query Parameters - 查询参数](https://fastapi.tiangolo.com/zh/tutorial/query-params/). PR [#1454](https://github.com/tiangolo/fastapi/pull/1454) by [@waynerv](https://github.com/waynerv). * Add translation to Chinese for [Contributing - 开发 - 贡献](https://fastapi.tiangolo.com/zh/contributing/). PR [#1460](https://github.com/tiangolo/fastapi/pull/1460) by [@waynerv](https://github.com/waynerv). * Add translation to Chinese for [Path Parameters - 路径参数](https://fastapi.tiangolo.com/zh/tutorial/path-params/). PR [#1453](https://github.com/tiangolo/fastapi/pull/1453) by [@waynerv](https://github.com/waynerv). * Add official Microsoft project generator for [serving spaCy with FastAPI and Azure Cognitive Skills](https://github.com/microsoft/cookiecutter-spacy-fastapi) to [Project Generators](https://fastapi.tiangolo.com/project-generation/). PR [#1390](https://github.com/tiangolo/fastapi/pull/1390) by [@kabirkhan](https://github.com/kabirkhan). From 7895c12fa122bc15b3232cc25ba6e367a3264867 Mon Sep 17 00:00:00 2001 From: Xie Wei <39515546+waynerv@users.noreply.github.com> Date: Sat, 13 Jun 2020 06:40:05 +0800 Subject: [PATCH 32/82] =?UTF-8?q?=F0=9F=8C=90=20Add=20Chinese=20translatio?= =?UTF-8?q?n=20for=20help-fastapi.md=20(#1465)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add chinese translation for help-fastapi.md * improve translations Co-authored-by: Waynerv Co-authored-by: Sebastián Ramírez --- docs/zh/docs/help-fastapi.md | 111 +++++++++++++++++++++++++++++++++++ docs/zh/mkdocs.yml | 1 + 2 files changed, 112 insertions(+) create mode 100644 docs/zh/docs/help-fastapi.md diff --git a/docs/zh/docs/help-fastapi.md b/docs/zh/docs/help-fastapi.md new file mode 100644 index 000000000..57b20c4fc --- /dev/null +++ b/docs/zh/docs/help-fastapi.md @@ -0,0 +1,111 @@ +# 帮助 FastAPI - 获取帮助 + +你喜欢 **FastAPI** 吗? + +您愿意去帮助 FastAPI,帮助其他用户以及作者吗? + +或者你想要获得有关 **FastAPI** 的帮助? + +下面是一些非常简单的方式去提供帮助(有些只需单击一两次链接)。 + +以及几种获取帮助的途径。 + +## 在 GitHub 上 Star **FastAPI** + +你可以在 GitHub 上 "star" FastAPI(点击右上角的 star 按钮):https://github.com/tiangolo/fastapi。 + +通过添加 star,其他用户将会更容易发现 FastAPI,并了解已经有许多人认为它有用。 + +## Watch GitHub 仓库的版本发布 + +你可以在 GitHub 上 "watch" FastAPI(点击右上角的 watch 按钮):https://github.com/tiangolo/fastapi。 + +这时你可以选择 "Releases only" 选项。 + +之后,只要有 **FastAPI** 的新版本(包含缺陷修复和新功能)发布,你都会(通过电子邮件)收到通知。 + +## 加入聊天室 + + + Join the chat at https://gitter.im/tiangolo/fastapi + + +加入 Gitter 上的聊天室:https://gitter.im/tiangolo/fastapi。 + +在这里你可以快速提问、帮助他人、分享想法等。 + +## 与作者联系 + +你可以联系 我 (Sebastián Ramírez / `tiangolo`) - FastAPI 的作者。 + +你可以: + +* 在 **GitHub** 上关注我。 + * 查看我创建的其他的可能对你有帮助的开源项目。 + * 关注我以了解我创建的新开源项目。 +* 在 **Twitter** 上关注我。 + * 告诉我你是如何使用 FastAPI 的(我很乐意听到)。 + * 提出问题。 +* 在 **Linkedin** 上联系我。 + * 与我交流。 + * 认可我的技能或推荐我 :) +* 在 **Medium** 上阅读我写的文章(或关注我)。 + * 阅读我创建的其他想法,文章和工具。 + * 关注我以了解我发布的新内容。 + +## 发布和 **FastAPI** 有关的推特 + + 发布和 **FastAPI** 有关的推特 让我和其他人知道你为什么喜欢它。 + +## 告诉我你正在如何使用 **FastAPI** + +我很乐意听到有关 **FastAPI** 被如何使用、你喜欢它的哪一点、被投入使用的项目/公司等等信息。 + +你可以通过以下平台让我知道: + +* **Twitter**。 +* **Linkedin**。 +* **Medium**。 + +## 为 FastAPI 投票 + +* 在 Slant 上为 **FastAPI** 投票。 + +## 帮助他人解决 GitHub 的 issues + +你可以查看 已有的 issues 并尝试帮助其他人。 + +## Watch GitHub 仓库 + +你可以在 GitHub 上 "watch" FastAPI(点击右上角的 "watch" 按钮):https://github.com/tiangolo/fastapi。 + +如果你选择的是 "Watching" 而不是 "Releases only" 选项,你会在其他人创建了新的 issue 时收到通知。 + +然后你可以尝试帮助他们解决这些 issue。 + +## 创建 issue + +你可以在 GitHub 仓库中 创建一个新 issue 用来: + +* 报告 bug 或问题。 +* 提议新的特性。 +* 提问。 + +## 创建 Pull Request + +你可以 创建一个 Pull Request 用来: + +* 纠正你在文档中发现的错别字。 +* 添加新的文档内容。 +* 修复已有的 bug 或问题。 +* 添加新的特性。 + +## 赞助作者 + +你还可以通过 GitHub sponsors 在经济上支持作者(我)。 + +这样你可以给我买杯咖啡☕️以示谢意😄。 + +--- + +感谢! diff --git a/docs/zh/mkdocs.yml b/docs/zh/mkdocs.yml index bea294687..760b3fe40 100644 --- a/docs/zh/mkdocs.yml +++ b/docs/zh/mkdocs.yml @@ -34,6 +34,7 @@ nav: - tutorial/query-params.md - deployment.md - contributing.md +- help-fastapi.md markdown_extensions: - toc: permalink: true From d2d72a8e4abde5b14388fedbfa63a51f3ec3d185 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 13 Jun 2020 00:43:07 +0200 Subject: [PATCH 33/82] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 3c57ca822..c489ed213 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Add translation to Chinese for [Help FastAPI - Get Help - 帮助 FastAPI - 获取帮助](https://fastapi.tiangolo.com/zh/help-fastapi/). PR [#1465](https://github.com/tiangolo/fastapi/pull/1465) by [@waynerv](https://github.com/waynerv). * Add translation to Chinese for [Query Parameters - 查询参数](https://fastapi.tiangolo.com/zh/tutorial/query-params/). PR [#1454](https://github.com/tiangolo/fastapi/pull/1454) by [@waynerv](https://github.com/waynerv). * Add translation to Chinese for [Contributing - 开发 - 贡献](https://fastapi.tiangolo.com/zh/contributing/). PR [#1460](https://github.com/tiangolo/fastapi/pull/1460) by [@waynerv](https://github.com/waynerv). * Add translation to Chinese for [Path Parameters - 路径参数](https://fastapi.tiangolo.com/zh/tutorial/path-params/). PR [#1453](https://github.com/tiangolo/fastapi/pull/1453) by [@waynerv](https://github.com/waynerv). From d737599a2cf763e6c5f10b2044bd280ac0d7fee4 Mon Sep 17 00:00:00 2001 From: Xie Wei <39515546+waynerv@users.noreply.github.com> Date: Sat, 13 Jun 2020 06:47:50 +0800 Subject: [PATCH 34/82] =?UTF-8?q?=F0=9F=8C=90=20Add=20Chinese=20translatio?= =?UTF-8?q?n=20for=20body.md=20(#1492)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/zh/docs/tutorial/body.md | 147 ++++++++++++++++++++++++++++++++++ docs/zh/mkdocs.yml | 1 + 2 files changed, 148 insertions(+) create mode 100644 docs/zh/docs/tutorial/body.md diff --git a/docs/zh/docs/tutorial/body.md b/docs/zh/docs/tutorial/body.md new file mode 100644 index 000000000..e60dce963 --- /dev/null +++ b/docs/zh/docs/tutorial/body.md @@ -0,0 +1,147 @@ +# 请求体 + +当你需要将数据从客户端(例如浏览器)发送给 API 时,你将其作为「请求体」发送。 + +**请求**体是客户端发送给 API 的数据。**响应**体是 API 发送给客户端的数据。 + +你的 API 几乎总是要发送**响应**体。但是客户端并不总是需要发送**请求**体。 + +我们使用 Pydantic 模型来声明**请求**体,并能够获得它们所具有的所有能力和优点。 + +!!! info + 你不能使用 `GET` 操作(HTTP 方法)发送请求体。 + + 要发送数据,你必须使用下列方法之一:`POST`(较常见)、`PUT`、`DELETE` 或 `PATCH`。 + +## 导入 Pydantic 的 `BaseModel` + +首先,你需要从 `pydantic` 中导入 `BaseModel`: + +```Python hl_lines="2" +{!../../../docs_src/body/tutorial001.py!} +``` + +## 创建数据模型 + +然后,将你的数据模型声明为继承自 `BaseModel` 的类。 + +使用标准的 Python 类型来声明所有属性: + +```Python hl_lines="5 6 7 8 9" +{!../../../docs_src/body/tutorial001.py!} +``` + +和声明查询参数时一样,当一个模型属性具有默认值时,它不是必需的。否则它是一个必需属性。将默认值设为 `None` 可使其成为可选属性。 + +例如,上面的模型声明了一个这样的 JSON「`object`」(或 Python `dict`): + +```JSON +{ + "name": "Foo", + "description": "An optional description", + "price": 45.2, + "tax": 3.5 +} +``` + +...由于 `description` 和 `tax` 是可选的(它们的默认值为 `None`),下面的 JSON「`object`」也将是有效的: + +```JSON +{ + "name": "Foo", + "price": 45.2 +} +``` + +## 声明为参数 + +使用与声明路径和查询参数的相同方式声明请求体,即可将其添加到「路径操作」中: + +```Python hl_lines="16" +{!../../../docs_src/body/tutorial001.py!} +``` + +...并且将它的类型声明为你创建的 `Item` 模型。 + +## 结果 + +仅仅使用了 Python 类型声明,**FastAPI** 将会: + +* 将请求体作为 JSON 读取。 +* 转换为相应的类型(在需要时)。 +* 校验数据。 + * 如果数据无效,将返回一条清晰易读的错误信息,指出不正确数据的确切位置和内容。 +* 将接收的数据赋值到参数 `item` 中。 + * 由于你已经在函数中将它声明为 `Item` 类型,你还将获得对于所有属性及其类型的一切编辑器支持(代码补全等)。 +* 为你的模型生成 JSON 模式 定义,你还可以在其他任何对你的项目有意义的地方使用它们。 +* 这些模式将成为生成的 OpenAPI 模式的一部分,并且被自动化文档 UI 所使用。 + +## 自动化文档 + +你所定义模型的 JSON 模式将成为生成的 OpenAPI 模式的一部分,并且在交互式 API 文档中展示: + + + +而且还将在每一个需要它们的*路径操作*的 API 文档中使用: + + + +## 编辑器支持 + +在你的编辑器中,你会在函数内部的任意地方得到类型提示和代码补全(如果你接收的是一个 `dict` 而不是 Pydantic 模型,则不会发生这种情况): + + + +你还会获得对不正确的类型操作的错误检查: + + + +这并非偶然,整个框架都是围绕该设计而构建。 + +并且在进行任何实现之前,已经在设计阶段经过了全面测试,以确保它可以在所有的编辑器中生效。 + +Pydantic 本身甚至也进行了一些更改以支持此功能。 + +上面的截图取自 Visual Studio Code。 + +但是在 PyCharm 和绝大多数其他 Python 编辑器中你也会获得同样的编辑器支持: + + + +## 使用模型 + +在函数内部,你可以直接访问模型对象的所有属性: + +```Python hl_lines="19" +{!../../../docs_src/body/tutorial002.py!} +``` + +## 请求体 + 路径参数 + +你可以同时声明路径参数和请求体。 + +**FastAPI** 将识别出与路径参数匹配的函数参数应**从路径中获取**,而声明为 Pydantic 模型的函数参数应**从请求体中获取**。 + +```Python hl_lines="15 16" +{!../../../docs_src/body/tutorial003.py!} +``` + +## 请求体 + 路径参数 + 查询参数 + +你还可以同时声明**请求体**、**路径参数**和**查询参数**。 + +**FastAPI** 会识别它们中的每一个,并从正确的位置获取数据。 + +```Python hl_lines="16" +{!../../../docs_src/body/tutorial004.py!} +``` + +函数参数将依次按如下规则进行识别: + +* 如果在**路径**中也声明了该参数,它将被用作路径参数。 +* 如果参数属于**单一类型**(比如 `int`、`float`、`str`、`bool` 等)它将被解释为**查询**参数。 +* 如果参数的类型被声明为一个 **Pydantic 模型**,它将被解释为**请求体**。 + +## 不使用 Pydantic + +如果你不想使用 Pydantic 模型,你还可以使用 **Body** 参数。请参阅文档 [请求体 - 多个参数:请求体中的单一值](body-multiple-params.md#singular-values-in-body){.internal-link target=_blank}。 diff --git a/docs/zh/mkdocs.yml b/docs/zh/mkdocs.yml index 760b3fe40..1529477cb 100644 --- a/docs/zh/mkdocs.yml +++ b/docs/zh/mkdocs.yml @@ -32,6 +32,7 @@ nav: - tutorial/first-steps.md - tutorial/path-params.md - tutorial/query-params.md + - tutorial/body.md - deployment.md - contributing.md - help-fastapi.md From c7334ae9f8ed16cf4682875a9e95786d3c4801e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 13 Jun 2020 00:50:22 +0200 Subject: [PATCH 35/82] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index c489ed213..f14ce1405 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Add translation to Chinese for [Request Body - 请求体](https://fastapi.tiangolo.com/zh/tutorial/body/). PR [#1492](https://github.com/tiangolo/fastapi/pull/1492) by [@waynerv](https://github.com/waynerv). * Add translation to Chinese for [Help FastAPI - Get Help - 帮助 FastAPI - 获取帮助](https://fastapi.tiangolo.com/zh/help-fastapi/). PR [#1465](https://github.com/tiangolo/fastapi/pull/1465) by [@waynerv](https://github.com/waynerv). * Add translation to Chinese for [Query Parameters - 查询参数](https://fastapi.tiangolo.com/zh/tutorial/query-params/). PR [#1454](https://github.com/tiangolo/fastapi/pull/1454) by [@waynerv](https://github.com/waynerv). * Add translation to Chinese for [Contributing - 开发 - 贡献](https://fastapi.tiangolo.com/zh/contributing/). PR [#1460](https://github.com/tiangolo/fastapi/pull/1460) by [@waynerv](https://github.com/waynerv). From 801ceaec80fdc92a9682a9944c44075620342405 Mon Sep 17 00:00:00 2001 From: Xie Wei <39515546+waynerv@users.noreply.github.com> Date: Sat, 13 Jun 2020 06:53:00 +0800 Subject: [PATCH 36/82] =?UTF-8?q?=F0=9F=8C=90=20Add=20Chinese=20translatio?= =?UTF-8?q?n=20for=20query-params-str-validations.md=20(#1500)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- .../tutorial/query-params-str-validations.md | 281 ++++++++++++++++++ docs/zh/mkdocs.yml | 1 + 2 files changed, 282 insertions(+) create mode 100644 docs/zh/docs/tutorial/query-params-str-validations.md diff --git a/docs/zh/docs/tutorial/query-params-str-validations.md b/docs/zh/docs/tutorial/query-params-str-validations.md new file mode 100644 index 000000000..2a1d41a89 --- /dev/null +++ b/docs/zh/docs/tutorial/query-params-str-validations.md @@ -0,0 +1,281 @@ +# 查询参数和字符串校验 + +**FastAPI** 允许你为参数声明额外的信息和校验。 + +让我们以下面的应用程序为例: + +```Python hl_lines="7" +{!../../../docs_src/query_params_str_validations/tutorial001.py!} +``` + +查询参数 `q` 的类型为 `str`,默认值为 `None`,因此它是可选的。 + +## 额外的校验 + +我们打算添加约束条件:即使 `q` 是可选的,但只要提供了该参数,则该参数值**不能超过50个字符的长度**。 + +### 导入 `Query` + +为此,首先从 `fastapi` 导入 `Query`: + +```Python hl_lines="1" +{!../../../docs_src/query_params_str_validations/tutorial002.py!} +``` + +## 使用 `Query` 作为默认值 + +现在,将 `Query` 用作查询参数的默认值,并将它的 `max_length` 参数设置为 50: + +```Python hl_lines="7" +{!../../../docs_src/query_params_str_validations/tutorial002.py!} +``` + +由于我们必须用 `Query(None)` 替换默认值 `None`,`Query` 的第一个参数同样也是用于定义默认值。 + +所以: + +```Python +q: str = Query(None) +``` + +...使得参数可选,等同于: + +```Python +q: str = None +``` + +但是 `Query` 显式地将其声明为查询参数。 + +然后,我们可以将更多的参数传递给 `Query`。在本例中,适用于字符串的 `max_length` 参数: + +```Python +q: str = Query(None, max_length=50) +``` + +将会校验数据,在数据无效时展示清晰的错误信息,并在 OpenAPI 模式的*路径操作*中记录该参​​数。 + +## 添加更多校验 + +你还可以添加 `min_length` 参数: + +```Python hl_lines="7" +{!../../../docs_src/query_params_str_validations/tutorial003.py!} +``` + +## 添加正则表达式 + +你可以定义一个参数值必须匹配的正则表达式: + +```Python hl_lines="8" +{!../../../docs_src/query_params_str_validations/tutorial004.py!} +``` + +这个指定的正则表达式通过以下规则检查接收到的参数值: + +* `^`:以该符号之后的字符开头,符号之前没有字符。 +* `fixedquery`: 值精确地等于 `fixedquery`。 +* `$`: 到此结束,在 `fixedquery` 之后没有更多字符。 + +如果你对所有的这些**「正则表达式」**概念感到迷茫,请不要担心。对于许多人来说这都是一个困难的主题。你仍然可以在无需正则表达式的情况下做很多事情。 + +但是,一旦你需要用到并去学习它们时,请了解你已经可以在 **FastAPI** 中直接使用它们。 + +## 默认值 + +你可以向 `Query` 的第一个参数传入 `None` 用作查询参数的默认值,以同样的方式你也可以传递其他默认值。 + +假设你想要声明查询参数 `q`,使其 `min_length` 为 `3`,并且默认值为 `fixedquery`: + +```Python hl_lines="7" +{!../../../docs_src/query_params_str_validations/tutorial005.py!} +``` + +!!! note + 具有默认值还会使该参数成为可选参数。 + +## 声明为必需参数 + +当我们不需要声明额外的校验或元数据时,只需不声明默认值就可以使 `q` 参数成为必需参数,例如: + +```Python +q: str +``` + +代替: + +```Python +q: str = None +``` + +但是现在我们正在用 `Query` 声明它,例如: + +```Python +q: str = Query(None, min_length=3) +``` + +因此,当你在使用 `Query` 且需要声明一个值是必需的时,可以将 `...` 用作第一个参数值: + +```Python hl_lines="7" +{!../../../docs_src/query_params_str_validations/tutorial006.py!} +``` + +!!! info + 如果你之前没见过 `...` 这种用法:它是一个特殊的单独值,它是 Python 的一部分并且被称为「省略号」。 + +这将使 **FastAPI** 知道此查询参数是必需的。 + +## 查询参数列表 / 多个值 + +当你使用 `Query` 显式地定义查询参数时,你还可以声明它去接收一组值,或换句话来说,接收多个值。 + +例如,要声明一个可在 URL 中出现多次的查询参数 `q`,你可以这样写: + +```Python hl_lines="9" +{!../../../docs_src/query_params_str_validations/tutorial011.py!} +``` + +然后,输入如下网址: + +``` +http://localhost:8000/items/?q=foo&q=bar +``` + +你会在*路径操作函数*的*函数参数* `q` 中以一个 Python `list` 的形式接收到*查询参数* `q` 的多个值(`foo` 和 `bar`)。 + +因此,该 URL 的响应将会是: + +```JSON +{ + "q": [ + "foo", + "bar" + ] +} +``` + +!!! tip + 要声明类型为 `list` 的查询参数,如上例所示,你需要显式地使用 `Query`,否则该参数将被解释为请求体。 + +交互式 API 文档将会相应地进行更新,以允许使用多个值: + + + +### 具有默认值的查询参数列表 / 多个值 + +你还可以定义在没有任何给定值时的默认 `list` 值: + +```Python hl_lines="9" +{!../../../docs_src/query_params_str_validations/tutorial012.py!} +``` + +如果你访问: + +``` +http://localhost:8000/items/ +``` + +`q` 的默认值将为:`["foo", "bar"]`,你的响应会是: + +```JSON +{ + "q": [ + "foo", + "bar" + ] +} +``` + +#### 使用 `list` + +你也可以直接使用 `list` 代替 `List [str]`: + +```Python hl_lines="7" +{!../../../docs_src/query_params_str_validations/tutorial013.py!} +``` + +!!! note + 请记住,在这种情况下 FastAPI 将不会检查列表的内容。 + + 例如,`List[int]` 将检查(并记录到文档)列表的内容必须是整数。但是单独的 `list` 不会。 + +## 声明更多元数据 + +你可以添加更多有关该参数的信息。 + +这些信息将包含在生成的 OpenAPI 模式中,并由文档用户界面和外部工具所使用。 + +!!! note + 请记住,不同的工具对 OpenAPI 的支持程度可能不同。 + + 其中一些可能不会展示所有已声明的额外信息,尽管在大多数情况下,缺少的这部分功能已经计划进行开发。 + +你可以添加 `title`: + +```Python hl_lines="7" +{!../../../docs_src/query_params_str_validations/tutorial007.py!} +``` + +以及 `description`: + +```Python hl_lines="11" +{!../../../docs_src/query_params_str_validations/tutorial008.py!} +``` + +## 别名参数 + +假设你想要查询参数为 `item-query`。 + +像下面这样: + +``` +http://127.0.0.1:8000/items/?item-query=foobaritems +``` + +但是 `item-query` 不是一个有效的 Python 变量名称。 + +最接近的有效名称是 `item_query`。 + +但是你仍然要求它在 URL 中必须是 `item-query`... + +这时你可以用 `alias` 参数声明一个别名,该别名将用于在 URL 中查找查询参数值: + +```Python hl_lines="7" +{!../../../docs_src/query_params_str_validations/tutorial009.py!} +``` + +## 弃用参数 + +现在假设你不再喜欢此参数。 + +你不得不将其保留一段时间,因为有些客户端正在使用它,但你希望文档清楚地将其展示为已弃用。 + +那么将参数 `deprecated=True` 传入 `Query`: + +```Python hl_lines="16" +{!../../../docs_src/query_params_str_validations/tutorial010.py!} +``` + +文档将会像下面这样展示它: + + + +## 总结 + +你可以为查询参数声明额外的校验和元数据。 + +通用的校验和元数据: + +* `alias` +* `title` +* `description` +* `deprecated` + +特定于字符串的校验: + +* `min_length` +* `max_length` +* `regex` + +在这些示例中,你了解了如何声明对 `str` 值的校验。 + +请参阅下一章节,以了解如何声明对其他类型例如数值的校验。 diff --git a/docs/zh/mkdocs.yml b/docs/zh/mkdocs.yml index 1529477cb..004d15815 100644 --- a/docs/zh/mkdocs.yml +++ b/docs/zh/mkdocs.yml @@ -33,6 +33,7 @@ nav: - tutorial/path-params.md - tutorial/query-params.md - tutorial/body.md + - tutorial/query-params-str-validations.md - deployment.md - contributing.md - help-fastapi.md From 5cbcb9a9653e95f606e7507032e9d3785b2dd598 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 13 Jun 2020 00:55:20 +0200 Subject: [PATCH 37/82] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index f14ce1405..bdd276e4f 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Add translation to Chinese for [Query Parameters and String Validations - 查询参数和字符串校验](https://fastapi.tiangolo.com/zh/tutorial/query-params-str-validations/). PR [#1500](https://github.com/tiangolo/fastapi/pull/1500) by [@waynerv](https://github.com/waynerv). * Add translation to Chinese for [Request Body - 请求体](https://fastapi.tiangolo.com/zh/tutorial/body/). PR [#1492](https://github.com/tiangolo/fastapi/pull/1492) by [@waynerv](https://github.com/waynerv). * Add translation to Chinese for [Help FastAPI - Get Help - 帮助 FastAPI - 获取帮助](https://fastapi.tiangolo.com/zh/help-fastapi/). PR [#1465](https://github.com/tiangolo/fastapi/pull/1465) by [@waynerv](https://github.com/waynerv). * Add translation to Chinese for [Query Parameters - 查询参数](https://fastapi.tiangolo.com/zh/tutorial/query-params/). PR [#1454](https://github.com/tiangolo/fastapi/pull/1454) by [@waynerv](https://github.com/waynerv). From a0ab47e89ed9f492ea15157ce7096f0a393bbaca Mon Sep 17 00:00:00 2001 From: kota matsuoka Date: Sat, 13 Jun 2020 07:56:00 +0900 Subject: [PATCH 38/82] =?UTF-8?q?=F0=9F=8E=A8=20Remove=20unused=20f-string?= =?UTF-8?q?=20(#1526)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/dependencies/utils.py | 2 +- fastapi/routing.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 3ff7d3356..5ad5d4269 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -294,7 +294,7 @@ def get_dependant( if param_name in path_param_names: assert is_scalar_field( field=param_field - ), f"Path params must be of one of the supported types" + ), "Path params must be of one of the supported types" if isinstance(param.default, params.Path): ignore_default = False else: diff --git a/fastapi/routing.py b/fastapi/routing.py index dab751a09..71a2b4d04 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -365,7 +365,7 @@ class APIRoute(routing.Route): self.include_in_schema = include_in_schema self.response_class = response_class - assert callable(endpoint), f"An endpoint must be a callable" + assert callable(endpoint), "An endpoint must be a callable" self.dependant = get_dependant(path=self.path_format, call=self.endpoint) for depends in self.dependencies[::-1]: self.dependant.dependencies.insert( From d39dd06a225e4832237c8f4e62656e03336e0248 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 13 Jun 2020 00:57:34 +0200 Subject: [PATCH 39/82] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index bdd276e4f..0eec72357 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Remove internal unnecessary f-strings. PR [#1526](https://github.com/tiangolo/fastapi/pull/1526) by [@kotamatsuoka](https://github.com/kotamatsuoka). * Add translation to Chinese for [Query Parameters and String Validations - 查询参数和字符串校验](https://fastapi.tiangolo.com/zh/tutorial/query-params-str-validations/). PR [#1500](https://github.com/tiangolo/fastapi/pull/1500) by [@waynerv](https://github.com/waynerv). * Add translation to Chinese for [Request Body - 请求体](https://fastapi.tiangolo.com/zh/tutorial/body/). PR [#1492](https://github.com/tiangolo/fastapi/pull/1492) by [@waynerv](https://github.com/waynerv). * Add translation to Chinese for [Help FastAPI - Get Help - 帮助 FastAPI - 获取帮助](https://fastapi.tiangolo.com/zh/help-fastapi/). PR [#1465](https://github.com/tiangolo/fastapi/pull/1465) by [@waynerv](https://github.com/waynerv). From 4310c89c83330c24ab50409facda1e64ee21f6c9 Mon Sep 17 00:00:00 2001 From: Kai Chen Date: Fri, 12 Jun 2020 16:12:59 -0700 Subject: [PATCH 40/82] =?UTF-8?q?=F0=9F=93=9D=20Add=20link=20to=20Advanced?= =?UTF-8?q?=20User=20Guide:=20response=20status=20code=20(#1512)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/tutorial/response-status-code.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/tutorial/response-status-code.md b/docs/en/docs/tutorial/response-status-code.md index 29b8521fc..05244edf6 100644 --- a/docs/en/docs/tutorial/response-status-code.md +++ b/docs/en/docs/tutorial/response-status-code.md @@ -83,4 +83,4 @@ They are just a convenience, they hold the same number, but that way you can use ## Changing the default -Later, in the **Advanced User Guide**, you will see how to return a different status code than the default you are declaring here. +Later, in the [Advanced User Guide](../advanced/response-change-status-code.md){.internal-link target=_blank}, you will see how to return a different status code than the default you are declaring here. From 50bc14b83595d9a1655430ea47b461d6545a336b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 13 Jun 2020 01:14:58 +0200 Subject: [PATCH 41/82] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 0eec72357..8d6ea3350 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Add link to advanced docs from tutorial. PR [#1512](https://github.com/tiangolo/fastapi/pull/1512) by [@kx-chen](https://github.com/kx-chen). * Remove internal unnecessary f-strings. PR [#1526](https://github.com/tiangolo/fastapi/pull/1526) by [@kotamatsuoka](https://github.com/kotamatsuoka). * Add translation to Chinese for [Query Parameters and String Validations - 查询参数和字符串校验](https://fastapi.tiangolo.com/zh/tutorial/query-params-str-validations/). PR [#1500](https://github.com/tiangolo/fastapi/pull/1500) by [@waynerv](https://github.com/waynerv). * Add translation to Chinese for [Request Body - 请求体](https://fastapi.tiangolo.com/zh/tutorial/body/). PR [#1492](https://github.com/tiangolo/fastapi/pull/1492) by [@waynerv](https://github.com/waynerv). From 8231fbede410ae2eab18b3dd18afc8f74a71f84d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 13 Jun 2020 01:17:06 +0200 Subject: [PATCH 42/82] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.56.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 8d6ea3350..ff1e5606d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,8 @@ ## Latest changes +## 0.56.1 + * Add link to advanced docs from tutorial. PR [#1512](https://github.com/tiangolo/fastapi/pull/1512) by [@kx-chen](https://github.com/kx-chen). * Remove internal unnecessary f-strings. PR [#1526](https://github.com/tiangolo/fastapi/pull/1526) by [@kotamatsuoka](https://github.com/kotamatsuoka). * Add translation to Chinese for [Query Parameters and String Validations - 查询参数和字符串校验](https://fastapi.tiangolo.com/zh/tutorial/query-params-str-validations/). PR [#1500](https://github.com/tiangolo/fastapi/pull/1500) by [@waynerv](https://github.com/waynerv). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index a0244bfaf..8e56f22b4 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.56.0" +__version__ = "0.56.1" from starlette import status From 3699e17212e1bf0f28627dc4617c5d0714502571 Mon Sep 17 00:00:00 2001 From: Rupsi Kaushik Date: Fri, 12 Jun 2020 19:22:30 -0400 Subject: [PATCH 43/82] =?UTF-8?q?=E2=9C=A8=20Implement=20=5F=5Frepr=5F=5F?= =?UTF-8?q?=20methods=20for=20path=20parameters=20to=20simplify=20debuggin?= =?UTF-8?q?g=20(#1560)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * repr description added to Depends class * repr description added to Security subclass * get rid of __repr__ in security since it will inherit from super * make code format consistent with rest * add desc for rest of the classes * Update fastapi/params.py remove trailing whitespace Co-authored-by: Marcelo Trylesinski * Implement __repr__ * fix formatting * formatting again * ran formatting * added basic testing * basic tests added to rest of the classes * added more test coverage and simplified test file Co-authored-by: Marcelo Trylesinski Co-authored-by: Jayati Shrivastava --- fastapi/params.py | 11 ++++++++++ tests/test_params_repr.py | 46 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 tests/test_params_repr.py diff --git a/fastapi/params.py b/fastapi/params.py index 3aa333ac7..c822cbfeb 100644 --- a/fastapi/params.py +++ b/fastapi/params.py @@ -51,6 +51,9 @@ class Param(FieldInfo): **extra, ) + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self.default})" + class Path(Param): in_ = ParamTypes.path @@ -239,6 +242,9 @@ class Body(FieldInfo): **extra, ) + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self.default})" + class Form(Body): def __init__( @@ -316,6 +322,11 @@ class Depends: self.dependency = dependency self.use_cache = use_cache + def __repr__(self) -> str: + attr = getattr(self.dependency, "__name__", type(self.dependency).__name__) + cache = "" if self.use_cache else ", use_cache=False" + return f"{self.__class__.__name__}({attr}{cache})" + class Security(Depends): def __init__( diff --git a/tests/test_params_repr.py b/tests/test_params_repr.py new file mode 100644 index 000000000..e21772aca --- /dev/null +++ b/tests/test_params_repr.py @@ -0,0 +1,46 @@ +import pytest +from fastapi.params import Body, Cookie, Depends, Header, Param, Path, Query + +test_data = ["teststr", None, ..., 1, []] + + +def get_user(): + return {} # pragma: no cover + + +@pytest.fixture(scope="function", params=test_data) +def params(request): + return request.param + + +def test_param_repr(params): + assert repr(Param(params)) == "Param(" + str(params) + ")" + + +def test_path_repr(params): + assert repr(Path(params)) == "Path(Ellipsis)" + + +def test_query_repr(params): + assert repr(Query(params)) == "Query(" + str(params) + ")" + + +def test_header_repr(params): + assert repr(Header(params)) == "Header(" + str(params) + ")" + + +def test_cookie_repr(params): + assert repr(Cookie(params)) == "Cookie(" + str(params) + ")" + + +def test_body_repr(params): + assert repr(Body(params)) == "Body(" + str(params) + ")" + + +def test_depends_repr(): + assert repr(Depends()) == "Depends(NoneType)" + assert repr(Depends(get_user)) == "Depends(get_user)" + assert repr(Depends(use_cache=False)) == "Depends(NoneType, use_cache=False)" + assert ( + repr(Depends(get_user, use_cache=False)) == "Depends(get_user, use_cache=False)" + ) From 12433d51dd3add9e0c0994506317dc1e6edade41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 13 Jun 2020 01:25:29 +0200 Subject: [PATCH 44/82] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index ff1e5606d..6867370b8 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,8 @@ ## Latest changes +* Add `__repr__` for *path operation function* parameter helpers (like `Query`, `Depends`, etc) to simplify debugging. PR [#1560](https://github.com/tiangolo/fastapi/pull/1560) by [@rkbeatss](https://github.com/rkbeatss) and [@victorphoenix3](https://github.com/victorphoenix3). + ## 0.56.1 * Add link to advanced docs from tutorial. PR [#1512](https://github.com/tiangolo/fastapi/pull/1512) by [@kx-chen](https://github.com/kx-chen). From 269a1555830f8f8c5029eb7ef6ea6c12def3b4e9 Mon Sep 17 00:00:00 2001 From: Xie Wei <39515546+waynerv@users.noreply.github.com> Date: Sat, 13 Jun 2020 15:45:48 +0800 Subject: [PATCH 45/82] =?UTF-8?q?=F0=9F=94=A5=20Remove=20obsolete=20Chines?= =?UTF-8?q?e=20articles=20after=20translations=20(#1510)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/external-links.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/docs/en/docs/external-links.md b/docs/en/docs/external-links.md index 34ba30e0a..c8ebcfafb 100644 --- a/docs/en/docs/external-links.md +++ b/docs/en/docs/external-links.md @@ -104,12 +104,6 @@ Here's an incomplete list of some of them. * [FastAPI] Python製のASGI Web フレームワーク FastAPIに入門する by @bee2. -### Chinese - -* 使用FastAPI框架快速构建高性能的api服务 by 逍遥散人. - -* FastAPI框架中文文档 by 何大仙. - ### Vietnamese * FASTAPI: TRIỂN KHAI BẰNG DOCKER by Nguyễn Nhân. From 43235cf236bef61411aa7dca52124bc3ea0c297b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 13 Jun 2020 09:47:36 +0200 Subject: [PATCH 46/82] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 6867370b8..0d02c26f0 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Remove obsolete Chinese articles after adding official community translations. PR [#1510](https://github.com/tiangolo/fastapi/pull/1510) by [@waynerv](https://github.com/waynerv). * Add `__repr__` for *path operation function* parameter helpers (like `Query`, `Depends`, etc) to simplify debugging. PR [#1560](https://github.com/tiangolo/fastapi/pull/1560) by [@rkbeatss](https://github.com/rkbeatss) and [@victorphoenix3](https://github.com/victorphoenix3). ## 0.56.1 From 0d73b9ff1cbf2d661cfed5d8f91343f898fd041c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 13 Jun 2020 12:26:15 +0200 Subject: [PATCH 47/82] =?UTF-8?q?=F0=9F=94=A7=20Add=20basic=20setup=20for?= =?UTF-8?q?=20Russian=20translations=20(#1566)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/mkdocs.yml | 1 + docs/es/mkdocs.yml | 1 + docs/it/mkdocs.yml | 1 + docs/pt/mkdocs.yml | 1 + docs/ru/docs/index.md | 447 ++++++++++++++++++++++++++++++++++++++++++ docs/ru/mkdocs.yml | 67 +++++++ docs/zh/mkdocs.yml | 1 + 7 files changed, 519 insertions(+) create mode 100644 docs/ru/docs/index.md create mode 100644 docs/ru/mkdocs.yml diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index 684207bab..62aa7dbe8 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -24,6 +24,7 @@ nav: - es: /es/ - it: /it/ - pt: /pt/ + - ru: /ru/ - zh: /zh/ - features.md - python-types.md diff --git a/docs/es/mkdocs.yml b/docs/es/mkdocs.yml index ba07f265e..fe0e9a767 100644 --- a/docs/es/mkdocs.yml +++ b/docs/es/mkdocs.yml @@ -24,6 +24,7 @@ nav: - es: /es/ - it: /it/ - pt: /pt/ + - ru: /ru/ - zh: /zh/ - features.md - python-types.md diff --git a/docs/it/mkdocs.yml b/docs/it/mkdocs.yml index 1bb7cad11..0341f88a9 100644 --- a/docs/it/mkdocs.yml +++ b/docs/it/mkdocs.yml @@ -24,6 +24,7 @@ nav: - es: /es/ - it: /it/ - pt: /pt/ + - ru: /ru/ - zh: /zh/ markdown_extensions: - toc: diff --git a/docs/pt/mkdocs.yml b/docs/pt/mkdocs.yml index 90eab738b..6c215b00d 100644 --- a/docs/pt/mkdocs.yml +++ b/docs/pt/mkdocs.yml @@ -24,6 +24,7 @@ nav: - es: /es/ - it: /it/ - pt: /pt/ + - ru: /ru/ - zh: /zh/ - features.md - Tutorial - Guia de Usuário: diff --git a/docs/ru/docs/index.md b/docs/ru/docs/index.md new file mode 100644 index 000000000..20dd403ab --- /dev/null +++ b/docs/ru/docs/index.md @@ -0,0 +1,447 @@ + +{!../../../docs/missing-translation.md!} + + +

+ FastAPI +

+

+ FastAPI framework, high performance, easy to learn, fast to code, ready for production +

+

+ + Build Status + + + Coverage + + + Package version + + + Join the chat at https://gitter.im/tiangolo/fastapi + +

+ +--- + +**Documentation**: https://fastapi.tiangolo.com + +**Source Code**: https://github.com/tiangolo/fastapi + +--- + +FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.6+ based on standard Python type hints. + +The key features are: + +* **Fast**: Very high performance, on par with **NodeJS** and **Go** (thanks to Starlette and Pydantic). [One of the fastest Python frameworks available](#performance). + +* **Fast to code**: Increase the speed to develop features by about 200% to 300%. * +* **Fewer bugs**: Reduce about 40% of human (developer) induced errors. * +* **Intuitive**: Great editor support. Completion everywhere. Less time debugging. +* **Easy**: Designed to be easy to use and learn. Less time reading docs. +* **Short**: Minimize code duplication. Multiple features from each parameter declaration. Fewer bugs. +* **Robust**: Get production-ready code. With automatic interactive documentation. +* **Standards-based**: Based on (and fully compatible with) the open standards for APIs: OpenAPI (previously known as Swagger) and JSON Schema. + +* estimation based on tests on an internal development team, building production applications. + +## Opinions + +"_[...] I'm using **FastAPI** a ton these days. [...] I'm actually planning to use it for all of my team's **ML services at Microsoft**. Some of them are getting integrated into the core **Windows** product and some **Office** products._" + +
Kabir Khan - Microsoft (ref)
+ +--- + +"_We adopted the **FastAPI** library to spawn a **REST** server that can be queried to obtain **predictions**. [for Ludwig]_" + +
Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - Uber (ref)
+ +--- + +"_**Netflix** is pleased to announce the open-source release of our **crisis management** orchestration framework: **Dispatch**! [built with **FastAPI**]_" + +
Kevin Glisson, Marc Vilanova, Forest Monsen - Netflix (ref)
+ +--- + +"_I’m over the moon excited about **FastAPI**. It’s so fun!_" + +
Brian Okken - Python Bytes podcast host (ref)
+ +--- + +"_Honestly, what you've built looks super solid and polished. In many ways, it's what I wanted **Hug** to be - it's really inspiring to see someone build that._" + +
Timothy Crosley - Hug creator (ref)
+ +--- + +"_If you're looking to learn one **modern framework** for building REST APIs, check out **FastAPI** [...] It's fast, easy to use and easy to learn [...]_" + +"_We've switched over to **FastAPI** for our **APIs** [...] I think you'll like it [...]_" + +
Ines Montani - Matthew Honnibal - Explosion AI founders - spaCy creators (ref) - (ref)
+ +--- + +## **Typer**, the FastAPI of CLIs + + + +If you are building a CLI app to be used in the terminal instead of a web API, check out **Typer**. + +**Typer** is FastAPI's little sibling. And it's intended to be the **FastAPI of CLIs**. ⌨️ 🚀 + +## Requirements + +Python 3.6+ + +FastAPI stands on the shoulders of giants: + +* Starlette for the web parts. +* Pydantic for the data parts. + +## Installation + +
+ +```console +$ pip install fastapi + +---> 100% +``` + +
+ +You will also need an ASGI server, for production such as Uvicorn or Hypercorn. + +
+ +```console +$ pip install uvicorn + +---> 100% +``` + +
+ +## Example + +### Create it + +* Create a file `main.py` with: + +```Python +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: str = None): + return {"item_id": item_id, "q": q} +``` + +
+Or use async def... + +If your code uses `async` / `await`, use `async def`: + +```Python hl_lines="7 12" +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: str = None): + return {"item_id": item_id, "q": q} +``` + +**Note**: + +If you don't know, check the _"In a hurry?"_ section about `async` and `await` in the docs. + +
+ +### Run it + +Run the server with: + +
+ +```console +$ uvicorn main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +INFO: Started reloader process [28720] +INFO: Started server process [28722] +INFO: Waiting for application startup. +INFO: Application startup complete. +``` + +
+ +
+About the command uvicorn main:app --reload... + +The command `uvicorn main:app` refers to: + +* `main`: the file `main.py` (the Python "module"). +* `app`: the object created inside of `main.py` with the line `app = FastAPI()`. +* `--reload`: make the server restart after code changes. Only do this for development. + +
+ +### Check it + +Open your browser at http://127.0.0.1:8000/items/5?q=somequery. + +You will see the JSON response as: + +```JSON +{"item_id": 5, "q": "somequery"} +``` + +You already created an API that: + +* Receives HTTP requests in the _paths_ `/` and `/items/{item_id}`. +* Both _paths_ take `GET` operations (also known as HTTP _methods_). +* The _path_ `/items/{item_id}` has a _path parameter_ `item_id` that should be an `int`. +* The _path_ `/items/{item_id}` has an optional `str` _query parameter_ `q`. + +### Interactive API docs + +Now go to http://127.0.0.1:8000/docs. + +You will see the automatic interactive API documentation (provided by Swagger UI): + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) + +### Alternative API docs + +And now, go to http://127.0.0.1:8000/redoc. + +You will see the alternative automatic documentation (provided by ReDoc): + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) + +## Example upgrade + +Now modify the file `main.py` to receive a body from a `PUT` request. + +Declare the body using standard Python types, thanks to Pydantic. + +```Python hl_lines="2 7 8 9 10 23 24 25" +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + price: float + is_offer: bool = None + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +def read_item(item_id: int, q: str = 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} +``` + +The server should reload automatically (because you added `--reload` to the `uvicorn` command above). + +### Interactive API docs upgrade + +Now go to http://127.0.0.1:8000/docs. + +* The interactive API documentation will be automatically updated, including the new body: + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) + +* Click on the button "Try it out", it allows you to fill the parameters and directly interact with the API: + +![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) + +* Then click on the "Execute" button, the user interface will communicate with your API, send the parameters, get the results and show them on the screen: + +![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) + +### Alternative API docs upgrade + +And now, go to http://127.0.0.1:8000/redoc. + +* The alternative documentation will also reflect the new query parameter and body: + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) + +### Recap + +In summary, you declare **once** the types of parameters, body, etc. as function parameters. + +You do that with standard modern Python types. + +You don't have to learn a new syntax, the methods or classes of a specific library, etc. + +Just standard **Python 3.6+**. + +For example, for an `int`: + +```Python +item_id: int +``` + +or for a more complex `Item` model: + +```Python +item: Item +``` + +...and with that single declaration you get: + +* Editor support, including: + * Completion. + * Type checks. +* Validation of data: + * Automatic and clear errors when the data is invalid. + * Validation even for deeply nested JSON objects. +* Conversion of input data: coming from the network to Python data and types. Reading from: + * JSON. + * Path parameters. + * Query parameters. + * Cookies. + * Headers. + * Forms. + * Files. +* Conversion of output data: converting from Python data and types to network data (as JSON): + * Convert Python types (`str`, `int`, `float`, `bool`, `list`, etc). + * `datetime` objects. + * `UUID` objects. + * Database models. + * ...and many more. +* Automatic interactive API documentation, including 2 alternative user interfaces: + * Swagger UI. + * ReDoc. + +--- + +Coming back to the previous code example, **FastAPI** will: + +* Validate that there is an `item_id` in the path for `GET` and `PUT` requests. +* Validate that the `item_id` is of type `int` for `GET` and `PUT` requests. + * If it is not, the client will see a useful, clear error. +* Check if there is an optional query parameter named `q` (as in `http://127.0.0.1:8000/items/foo?q=somequery`) for `GET` requests. + * As the `q` parameter is declared with `= None`, it is optional. + * Without the `None` it would be required (as is the body in the case with `PUT`). +* For `PUT` requests to `/items/{item_id}`, Read the body as JSON: + * Check that it has a required attribute `name` that should be a `str`. + * Check that it has a required attribute `price` that has to be a `float`. + * Check that it has an optional attribute `is_offer`, that should be a `bool`, if present. + * All this would also work for deeply nested JSON objects. +* Convert from and to JSON automatically. +* Document everything with OpenAPI, that can be used by: + * Interactive documentation systems. + * Automatic client code generation systems, for many languages. +* Provide 2 interactive documentation web interfaces directly. + +--- + +We just scratched the surface, but you already get the idea of how it all works. + +Try changing the line with: + +```Python + return {"item_name": item.name, "item_id": item_id} +``` + +...from: + +```Python + ... "item_name": item.name ... +``` + +...to: + +```Python + ... "item_price": item.price ... +``` + +...and see how your editor will auto-complete the attributes and know their types: + +![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) + +For a more complete example including more features, see the Tutorial - User Guide. + +**Spoiler alert**: the tutorial - user guide includes: + +* Declaration of **parameters** from other different places as: **headers**, **cookies**, **form fields** and **files**. +* How to set **validation constraints** as `maximum_length` or `regex`. +* A very powerful and easy to use **Dependency Injection** system. +* Security and authentication, including support for **OAuth2** with **JWT tokens** and **HTTP Basic** auth. +* More advanced (but equally easy) techniques for declaring **deeply nested JSON models** (thanks to Pydantic). +* Many extra features (thanks to Starlette) as: + * **WebSockets** + * **GraphQL** + * extremely easy tests based on `requests` and `pytest` + * **CORS** + * **Cookie Sessions** + * ...and more. + +## Performance + +Independent TechEmpower benchmarks show **FastAPI** applications running under Uvicorn as one of the fastest Python frameworks available, only below Starlette and Uvicorn themselves (used internally by FastAPI). (*) + +To understand more about it, see the section Benchmarks. + +## Optional Dependencies + +Used by Pydantic: + +* ujson - for faster JSON "parsing". +* email_validator - for email validation. + +Used by Starlette: + +* requests - Required if you want to use the `TestClient`. +* aiofiles - Required if you want to use `FileResponse` or `StaticFiles`. +* jinja2 - Required if you want to use the default template configuration. +* python-multipart - Required if you want to support form "parsing", with `request.form()`. +* itsdangerous - Required for `SessionMiddleware` support. +* pyyaml - Required for Starlette's `SchemaGenerator` support (you probably don't need it with FastAPI). +* graphene - Required for `GraphQLApp` support. +* ujson - Required if you want to use `UJSONResponse`. + +Used by FastAPI / Starlette: + +* uvicorn - for the server that loads and serves your application. +* orjson - Required if you want to use `ORJSONResponse`. + +You can install all of these with `pip install fastapi[all]`. + +## License + +This project is licensed under the terms of the MIT license. diff --git a/docs/ru/mkdocs.yml b/docs/ru/mkdocs.yml new file mode 100644 index 000000000..d50359397 --- /dev/null +++ b/docs/ru/mkdocs.yml @@ -0,0 +1,67 @@ +site_name: FastAPI +site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production +site_url: https://fastapi.tiangolo.com/ru/ +theme: + name: material + palette: + primary: teal + accent: amber + icon: + repo: fontawesome/brands/github-alt + logo: https://fastapi.tiangolo.com/img/icon-white.svg + favicon: https://fastapi.tiangolo.com/img/favicon.png + language: ru +repo_name: tiangolo/fastapi +repo_url: https://github.com/tiangolo/fastapi +edit_uri: '' +google_analytics: +- UA-133183413-1 +- auto +nav: +- FastAPI: index.md +- Languages: + - en: / + - es: /es/ + - it: /it/ + - pt: /pt/ + - ru: /ru/ + - zh: /zh/ +markdown_extensions: +- toc: + permalink: true +- markdown.extensions.codehilite: + guess_lang: false +- markdown_include.include: + base_path: docs +- admonition +- codehilite +- extra +- pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_div_format '' +- pymdownx.tabbed +extra: + social: + - icon: fontawesome/brands/github-alt + link: https://github.com/tiangolo/typer + - icon: fontawesome/brands/twitter + link: https://twitter.com/tiangolo + - icon: fontawesome/brands/linkedin + link: https://www.linkedin.com/in/tiangolo + - icon: fontawesome/brands/dev + link: https://dev.to/tiangolo + - icon: fontawesome/brands/medium + link: https://medium.com/@tiangolo + - icon: fontawesome/solid/globe + link: https://tiangolo.com +extra_css: +- https://fastapi.tiangolo.com/css/termynal.css +- https://fastapi.tiangolo.com/css/custom.css +extra_javascript: +- https://unpkg.com/mermaid@8.4.6/dist/mermaid.min.js +- https://fastapi.tiangolo.com/js/termynal.js +- https://fastapi.tiangolo.com/js/custom.js +- https://fastapi.tiangolo.com/js/chat.js +- https://sidecar.gitter.im/dist/sidecar.v1.js diff --git a/docs/zh/mkdocs.yml b/docs/zh/mkdocs.yml index 004d15815..b22c6dde7 100644 --- a/docs/zh/mkdocs.yml +++ b/docs/zh/mkdocs.yml @@ -24,6 +24,7 @@ nav: - es: /es/ - it: /it/ - pt: /pt/ + - ru: /ru/ - zh: /zh/ - features.md - python-types.md From 3651b8a30f5648a37811c919684f6a8937fb1a02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 13 Jun 2020 12:27:00 +0200 Subject: [PATCH 48/82] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 0d02c26f0..3c5c1e05c 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Add basic setup for Russian translations. PR [#1566](https://github.com/tiangolo/fastapi/pull/1566). * Remove obsolete Chinese articles after adding official community translations. PR [#1510](https://github.com/tiangolo/fastapi/pull/1510) by [@waynerv](https://github.com/waynerv). * Add `__repr__` for *path operation function* parameter helpers (like `Query`, `Depends`, etc) to simplify debugging. PR [#1560](https://github.com/tiangolo/fastapi/pull/1560) by [@rkbeatss](https://github.com/rkbeatss) and [@victorphoenix3](https://github.com/victorphoenix3). From a071ddf3cd4a8e9cfa89409a2273c5e284bc1e4b Mon Sep 17 00:00:00 2001 From: Thomas Maschler Date: Sat, 13 Jun 2020 07:58:06 -0400 Subject: [PATCH 49/82] =?UTF-8?q?=E2=9C=A8=20Add=20support=20for=20tag=20m?= =?UTF-8?q?etadata=20in=20OpenAPI=20(#1348)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Allow to add OpenAPI tag descriptions * fix type hint * fix type hint 2 * refactor test to assure 100% coverage * 📝 Update tags metadata example * 📝 Update docs for tags metadata * ✅ Move tags metadata test to tutorial subdir * 🎨 Update format in applications * 🍱 Update docs UI image based on new example * 🎨 Apply formatting after solving conflicts Co-authored-by: Sebastián Ramírez --- .../en/docs/img/tutorial/metadata/image02.png | Bin 0 -> 47719 bytes docs/en/docs/tutorial/metadata.md | 52 ++++++++++++++ docs_src/metadata/tutorial004.py | 28 ++++++++ fastapi/applications.py | 3 + fastapi/openapi/utils.py | 7 +- fastapi/routing.py | 1 - .../test_metadata/test_tutorial004.py | 65 ++++++++++++++++++ 7 files changed, 153 insertions(+), 3 deletions(-) create mode 100644 docs/en/docs/img/tutorial/metadata/image02.png create mode 100644 docs_src/metadata/tutorial004.py create mode 100644 tests/test_tutorial/test_metadata/test_tutorial004.py diff --git a/docs/en/docs/img/tutorial/metadata/image02.png b/docs/en/docs/img/tutorial/metadata/image02.png new file mode 100644 index 0000000000000000000000000000000000000000..7f3ab0a10dc5136b851d5fbf0ff2a24d1cf93903 GIT binary patch literal 47719 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 W{(iuVP6CP111LRLl`nf{9Qa=sR4w-a literal 0 HcmV?d00001 diff --git a/docs/en/docs/tutorial/metadata.md b/docs/en/docs/tutorial/metadata.md index 666fa7648..b9120c82e 100644 --- a/docs/en/docs/tutorial/metadata.md +++ b/docs/en/docs/tutorial/metadata.md @@ -21,6 +21,58 @@ With this configuration, the automatic API docs would look like: +## Tag descriptions + +You can also add additional metadata for the different tags used to group your path operations with the parameter `openapi_tags`. + +It takes a list containing one dictionary for each tag. + +Each dictionary can contain: + +* `name` (**required**): a `str` with the same tag name you use in the `tags` parameter in your *path operations* and `APIRouter`s. +* `description`: a `str` with a short description for the tag. It can have Markdown and will be shown in the docs UI. +* `externalDocs`: a `dict` describing external documentation with: + * `description`: a `str` with a short description for the external docs. + * `url` (**required**): a `str` with the URL for the external documentation. + +### Create metadata for tags + +Let's try that in an example with tags for `users` and `items`. + +Create metadata for your tags and pass it to the `openapi_tags` parameter: + +```Python hl_lines="3 4 5 6 7 8 9 10 11 12 13 14 15 16 18" +{!../../../docs_src/metadata/tutorial004.py!} +``` + +Notice that you can use Markdown inside of the descriptions, for example "login" will be shown in bold (**login**) and "fancy" will be shown in italics (_fancy_). + +!!! tip + You don't have to add metadata for all the tags that you use. + +### Use your tags + +Use the `tags` parameter with your *path operations* (and `APIRouter`s) to assign them to different tags: + +```Python hl_lines="21 26" +{!../../../docs_src/metadata/tutorial004.py!} +``` + +!!! info + Read more about tags in [Path Operation Configuration](../path-operation-configuration/#tags){.internal-link target=_blank}. + +### Check the docs + +Now, if you check the docs, they will show all the additional metadata: + + + +### Order of tags + +The order of each tag metadata dictionary also defines the order shown in the docs UI. + +For example, even though `users` would go after `items` in alphabetical order, it is shown before them, because we added their metadata as the first dictionary in the list. + ## OpenAPI URL By default, the OpenAPI schema is served at `/openapi.json`. diff --git a/docs_src/metadata/tutorial004.py b/docs_src/metadata/tutorial004.py new file mode 100644 index 000000000..465bd659d --- /dev/null +++ b/docs_src/metadata/tutorial004.py @@ -0,0 +1,28 @@ +from fastapi import FastAPI + +tags_metadata = [ + { + "name": "users", + "description": "Operations with users. The **login** logic is also here.", + }, + { + "name": "items", + "description": "Manage items. So _fancy_ they have their own docs.", + "externalDocs": { + "description": "Items external docs", + "url": "https://fastapi.tiangolo.com/", + }, + }, +] + +app = FastAPI(openapi_tags=tags_metadata) + + +@app.get("/users/", tags=["users"]) +async def get_users(): + return [{"name": "Harry"}, {"name": "Ron"}] + + +@app.get("/items/", tags=["items"]) +async def get_items(): + return [{"name": "wand"}, {"name": "flying broom"}] diff --git a/fastapi/applications.py b/fastapi/applications.py index 39e694fae..3306aab3d 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -37,6 +37,7 @@ class FastAPI(Starlette): description: str = "", version: str = "0.1.0", openapi_url: Optional[str] = "/openapi.json", + openapi_tags: Optional[List[Dict[str, Any]]] = None, default_response_class: Type[Response] = JSONResponse, docs_url: Optional[str] = "/docs", redoc_url: Optional[str] = "/redoc", @@ -70,6 +71,7 @@ class FastAPI(Starlette): self.description = description self.version = version self.openapi_url = openapi_url + self.openapi_tags = openapi_tags # TODO: remove when discarding the openapi_prefix parameter if openapi_prefix: logger.warning( @@ -103,6 +105,7 @@ class FastAPI(Starlette): description=self.description, routes=self.routes, openapi_prefix=openapi_prefix, + tags=self.openapi_tags, ) return self.openapi_schema diff --git a/fastapi/openapi/utils.py b/fastapi/openapi/utils.py index bb2e7dff7..b6221ca20 100644 --- a/fastapi/openapi/utils.py +++ b/fastapi/openapi/utils.py @@ -317,12 +317,13 @@ def get_openapi( openapi_version: str = "3.0.2", description: str = None, routes: Sequence[BaseRoute], - openapi_prefix: str = "" + openapi_prefix: str = "", + tags: Optional[List[Dict[str, Any]]] = None ) -> Dict: info = {"title": title, "version": version} if description: info["description"] = description - output = {"openapi": openapi_version, "info": info} + output: Dict[str, Any] = {"openapi": openapi_version, "info": info} components: Dict[str, Dict] = {} paths: Dict[str, Dict] = {} flat_models = get_flat_models_from_routes(routes) @@ -352,4 +353,6 @@ def get_openapi( if components: output["components"] = components output["paths"] = paths + if tags: + output["tags"] = tags return jsonable_encoder(OpenAPI(**output), by_alias=True, exclude_none=True) diff --git a/fastapi/routing.py b/fastapi/routing.py index 71a2b4d04..b4560a8a4 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -12,7 +12,6 @@ from fastapi.dependencies.utils import ( ) from fastapi.encoders import DictIntStrAny, SetIntStr, jsonable_encoder from fastapi.exceptions import RequestValidationError, WebSocketRequestValidationError -from fastapi.logger import logger from fastapi.openapi.constants import STATUS_CODES_WITH_NO_BODY from fastapi.utils import ( PYDANTIC_1, diff --git a/tests/test_tutorial/test_metadata/test_tutorial004.py b/tests/test_tutorial/test_metadata/test_tutorial004.py new file mode 100644 index 000000000..1ec59d3fe --- /dev/null +++ b/tests/test_tutorial/test_metadata/test_tutorial004.py @@ -0,0 +1,65 @@ +from fastapi.testclient import TestClient + +from metadata.tutorial004 import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/": { + "get": { + "tags": ["users"], + "summary": "Get Users", + "operationId": "get_users_users__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + }, + "/items/": { + "get": { + "tags": ["items"], + "summary": "Get Items", + "operationId": "get_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + }, + }, + "tags": [ + { + "name": "users", + "description": "Operations with users. The **login** logic is also here.", + }, + { + "name": "items", + "description": "Manage items. So _fancy_ they have their own docs.", + "externalDocs": { + "description": "Items external docs", + "url": "https://fastapi.tiangolo.com/", + }, + }, + ], +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def test_path_operations(): + response = client.get("/items/") + assert response.status_code == 200, response.text + response = client.get("/users/") + assert response.status_code == 200, response.text From 828915baf591a662f87da7401843348cce505fe3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 13 Jun 2020 14:02:58 +0200 Subject: [PATCH 50/82] =?UTF-8?q?=F0=9F=93=9D=20Update=20Tags=20metadata?= =?UTF-8?q?=20title?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/tutorial/metadata.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/tutorial/metadata.md b/docs/en/docs/tutorial/metadata.md index b9120c82e..25c00db70 100644 --- a/docs/en/docs/tutorial/metadata.md +++ b/docs/en/docs/tutorial/metadata.md @@ -21,7 +21,7 @@ With this configuration, the automatic API docs would look like: -## Tag descriptions +## Metadata for tags You can also add additional metadata for the different tags used to group your path operations with the parameter `openapi_tags`. From 5ffa18f10faaec2dc1f7acb96bba5ab52429f12f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 13 Jun 2020 14:06:12 +0200 Subject: [PATCH 51/82] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 3c5c1e05c..668877d9a 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Add support for declaring metadata for tags in OpenAPI. New docs at [Tutorial - Metadata and Docs URLs - Metadata for tags](https://fastapi.tiangolo.com/tutorial/metadata/#metadata-for-tags). PR [#1348](https://github.com/tiangolo/fastapi/pull/1348) by [@thomas-maschler](https://github.com/thomas-maschler). * Add basic setup for Russian translations. PR [#1566](https://github.com/tiangolo/fastapi/pull/1566). * Remove obsolete Chinese articles after adding official community translations. PR [#1510](https://github.com/tiangolo/fastapi/pull/1510) by [@waynerv](https://github.com/waynerv). * Add `__repr__` for *path operation function* parameter helpers (like `Query`, `Depends`, etc) to simplify debugging. PR [#1560](https://github.com/tiangolo/fastapi/pull/1560) by [@rkbeatss](https://github.com/rkbeatss) and [@victorphoenix3](https://github.com/victorphoenix3). From b13a4baf32514d7197ca555a563e91ec8422d9d7 Mon Sep 17 00:00:00 2001 From: Aviram Hassan <41201924+aviramha@users.noreply.github.com> Date: Sat, 13 Jun 2020 15:33:27 +0300 Subject: [PATCH 52/82] =?UTF-8?q?=E2=9C=A8=20Add=20better=20JSON=20decode?= =?UTF-8?q?=20error=20handling,=20improve=20feedback=20for=20client=20afte?= =?UTF-8?q?r=20invalid=20JSON=20requests=20=20(#1354)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Request body error, raise RequestValidationError instead of HTTPException in case JSON decode failure * add missing test case for body general exception --- fastapi/routing.py | 3 +++ .../test_body/test_tutorial001.py | 23 ++++++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/fastapi/routing.py b/fastapi/routing.py index b4560a8a4..16eb7ab0b 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -1,5 +1,6 @@ import asyncio import inspect +import json from typing import Any, Callable, Dict, List, Optional, Sequence, Set, Type, Union from fastapi import params @@ -177,6 +178,8 @@ def get_request_handler( body_bytes = await request.body() if body_bytes: body = await request.json() + except json.JSONDecodeError as e: + raise RequestValidationError([ErrorWrapper(e, ("body", e.pos))], body=e.doc) except Exception as e: raise HTTPException( status_code=400, detail="There was an error parsing the body" diff --git a/tests/test_tutorial/test_body/test_tutorial001.py b/tests/test_tutorial/test_body/test_tutorial001.py index 293981a09..806e712dc 100644 --- a/tests/test_tutorial/test_body/test_tutorial001.py +++ b/tests/test_tutorial/test_body/test_tutorial001.py @@ -1,3 +1,5 @@ +from unittest.mock import patch + import pytest from fastapi.testclient import TestClient @@ -176,5 +178,24 @@ def test_post_body(path, body, expected_status, expected_response): def test_post_broken_body(): response = client.post("/items/", data={"name": "Foo", "price": 50.5}) - assert response.status_code == 400, response.text + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "ctx": { + "colno": 1, + "doc": "name=Foo&price=50.5", + "lineno": 1, + "msg": "Expecting value", + "pos": 0, + }, + "loc": ["body", 0], + "msg": "Expecting value: line 1 column 1 (char 0)", + "type": "value_error.jsondecode", + } + ] + } + with patch("json.loads", side_effect=Exception): + response = client.post("/items/", json={"test": "test2"}) + assert response.status_code == 400, response.text assert response.json() == {"detail": "There was an error parsing the body"} From fddd1c12de8b56c48bdb333c04ecb3c3cfa36ee1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 13 Jun 2020 14:36:27 +0200 Subject: [PATCH 53/82] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 668877d9a..436f7cc6e 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Improve error handling and feedback for requests with invalid JSON. PR [#1354](https://github.com/tiangolo/fastapi/pull/1354) by [@aviramha](https://github.com/aviramha). * Add support for declaring metadata for tags in OpenAPI. New docs at [Tutorial - Metadata and Docs URLs - Metadata for tags](https://fastapi.tiangolo.com/tutorial/metadata/#metadata-for-tags). PR [#1348](https://github.com/tiangolo/fastapi/pull/1348) by [@thomas-maschler](https://github.com/thomas-maschler). * Add basic setup for Russian translations. PR [#1566](https://github.com/tiangolo/fastapi/pull/1566). * Remove obsolete Chinese articles after adding official community translations. PR [#1510](https://github.com/tiangolo/fastapi/pull/1510) by [@waynerv](https://github.com/waynerv). From 9aea85a84e8636a3c366b3e11946eff97c61636c Mon Sep 17 00:00:00 2001 From: Nick Rushton Date: Sat, 13 Jun 2020 05:38:08 -0700 Subject: [PATCH 54/82] =?UTF-8?q?=E2=AC=86=20Upgrade=20Starlette=20depende?= =?UTF-8?q?ncy=20to=200.13.4=20(#1361)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 414304fc5..3dab7da91 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,7 @@ classifiers = [ "Topic :: Internet :: WWW/HTTP", ] requires = [ - "starlette ==0.13.2", + "starlette ==0.13.4", "pydantic >=0.32.2,<2.0.0" ] description-file = "README.md" From c0b1fddb31fcf8d57c61e428a9d13608da9523bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 13 Jun 2020 14:39:58 +0200 Subject: [PATCH 55/82] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 436f7cc6e..7c479d164 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Upgrade Starlette version to `0.13.4`. PR [#1361](https://github.com/tiangolo/fastapi/pull/1361) by [@rushton](https://github.com/rushton). * Improve error handling and feedback for requests with invalid JSON. PR [#1354](https://github.com/tiangolo/fastapi/pull/1354) by [@aviramha](https://github.com/aviramha). * Add support for declaring metadata for tags in OpenAPI. New docs at [Tutorial - Metadata and Docs URLs - Metadata for tags](https://fastapi.tiangolo.com/tutorial/metadata/#metadata-for-tags). PR [#1348](https://github.com/tiangolo/fastapi/pull/1348) by [@thomas-maschler](https://github.com/thomas-maschler). * Add basic setup for Russian translations. PR [#1566](https://github.com/tiangolo/fastapi/pull/1566). From 7ce756f9ddd6fef6f51af2129f2a9e4c93df7166 Mon Sep 17 00:00:00 2001 From: obataku <19821199+obataku@users.noreply.github.com> Date: Sat, 13 Jun 2020 08:44:51 -0400 Subject: [PATCH 56/82] =?UTF-8?q?=F0=9F=90=9B=20Fix=20duplicated=20headers?= =?UTF-8?q?=20set=20by=20indirect=20dependencies=20(#1386)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Added test for repeating cookies in response headers * update `response` headers, status code to match `sub_response` in `solve_dependencies` only if necessary; fix formatting of scottsmith2gmail's test * restore code coverage, remove dead code from `solve_dependencies` Co-authored-by: Scott Smith --- fastapi/dependencies/utils.py | 6 +---- tests/test_repeated_cookie_headers.py | 34 +++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 5 deletions(-) create mode 100644 tests/test_repeated_cookie_headers.py diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 5ad5d4269..493977355 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -514,13 +514,9 @@ async def solve_dependencies( sub_values, sub_errors, background_tasks, - sub_response, + _, # the subdependency returns the same response we have sub_dependency_cache, ) = solved_result - sub_response = cast(Response, sub_response) - response.headers.raw.extend(sub_response.headers.raw) - if sub_response.status_code: - response.status_code = sub_response.status_code dependency_cache.update(sub_dependency_cache) if sub_errors: errors.extend(sub_errors) diff --git a/tests/test_repeated_cookie_headers.py b/tests/test_repeated_cookie_headers.py new file mode 100644 index 000000000..4a1913a08 --- /dev/null +++ b/tests/test_repeated_cookie_headers.py @@ -0,0 +1,34 @@ +from fastapi import Depends, FastAPI, Response +from fastapi.testclient import TestClient + +app = FastAPI() + + +def set_cookie(*, response: Response): + response.set_cookie("cookie-name", "cookie-value") + return {} + + +def set_indirect_cookie(*, dep: str = Depends(set_cookie)): + return dep + + +@app.get("/directCookie") +def get_direct_cookie(dep: str = Depends(set_cookie)): + return {"dep": dep} + + +@app.get("/indirectCookie") +def get_indirect_cookie(dep: str = Depends(set_indirect_cookie)): + return {"dep": dep} + + +client = TestClient(app) + + +def test_cookie_is_set_once(): + direct_response = client.get("/directCookie") + indirect_response = client.get("/indirectCookie") + assert ( + direct_response.headers["set-cookie"] == indirect_response.headers["set-cookie"] + ) From 6af857f206083e364df40daff17e4babdff1b178 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 13 Jun 2020 14:47:03 +0200 Subject: [PATCH 57/82] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 7c479d164..629c72e0d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Fix duplicated headers created by indirect dependencies that use the request directly. PR [#1386](https://github.com/tiangolo/fastapi/pull/1386) by [@obataku](https://github.com/obataku) from tests by [@scottsmith2gmail](https://github.com/scottsmith2gmail). * Upgrade Starlette version to `0.13.4`. PR [#1361](https://github.com/tiangolo/fastapi/pull/1361) by [@rushton](https://github.com/rushton). * Improve error handling and feedback for requests with invalid JSON. PR [#1354](https://github.com/tiangolo/fastapi/pull/1354) by [@aviramha](https://github.com/aviramha). * Add support for declaring metadata for tags in OpenAPI. New docs at [Tutorial - Metadata and Docs URLs - Metadata for tags](https://fastapi.tiangolo.com/tutorial/metadata/#metadata-for-tags). PR [#1348](https://github.com/tiangolo/fastapi/pull/1348) by [@thomas-maschler](https://github.com/thomas-maschler). From fe15620df39dfc26a97005968425f1e967f1daed Mon Sep 17 00:00:00 2001 From: Chen Rotem Levy Date: Sat, 13 Jun 2020 15:50:14 +0300 Subject: [PATCH 58/82] =?UTF-8?q?=F0=9F=8E=A8=20Update=20and=20clarify=20t?= =?UTF-8?q?esting=20function=20name=20(#1395)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit test_create_existing_token -> test_create_existing_item --- docs_src/app_testing/test_main_b.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs_src/app_testing/test_main_b.py b/docs_src/app_testing/test_main_b.py index 3e31a4180..83cc7d255 100644 --- a/docs_src/app_testing/test_main_b.py +++ b/docs_src/app_testing/test_main_b.py @@ -51,7 +51,7 @@ def test_create_item_bad_token(): assert response.json() == {"detail": "Invalid X-Token header"} -def test_create_existing_token(): +def test_create_existing_item(): response = client.post( "/items/", headers={"X-Token": "coneofsilence"}, From dd6d0cb23c33ecd858c7a36b03b0192835162dc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 13 Jun 2020 14:51:21 +0200 Subject: [PATCH 59/82] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 629c72e0d..c7552264d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Update and clarify testing function name. PR [#1395](https://github.com/tiangolo/fastapi/pull/1395) by [@chenl](https://github.com/chenl). * Fix duplicated headers created by indirect dependencies that use the request directly. PR [#1386](https://github.com/tiangolo/fastapi/pull/1386) by [@obataku](https://github.com/obataku) from tests by [@scottsmith2gmail](https://github.com/scottsmith2gmail). * Upgrade Starlette version to `0.13.4`. PR [#1361](https://github.com/tiangolo/fastapi/pull/1361) by [@rushton](https://github.com/rushton). * Improve error handling and feedback for requests with invalid JSON. PR [#1354](https://github.com/tiangolo/fastapi/pull/1354) by [@aviramha](https://github.com/aviramha). From c0f3019764d0192f5ee82f1039cf26f55c6d3879 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Sat, 13 Jun 2020 22:05:59 +0900 Subject: [PATCH 60/82] =?UTF-8?q?=F0=9F=93=9D=20Add=20PyCharm=20Pydantic?= =?UTF-8?q?=20plugin=20to=20docs=20(#1420)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add pydantic pycharm plugin in document * 📝 Update PyCharm Pydantic plugin note Co-authored-by: Sebastián Ramírez --- docs/en/docs/tutorial/body.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/en/docs/tutorial/body.md b/docs/en/docs/tutorial/body.md index 25b1fb6ba..b2cde0762 100644 --- a/docs/en/docs/tutorial/body.md +++ b/docs/en/docs/tutorial/body.md @@ -108,6 +108,17 @@ But you would get the same editor support with +!!! tip + If you use PyCharm as your editor, you can use the Pydantic PyCharm Plugin. + + It improves editor support for Pydantic models, with: + + * auto-completion + * type checks + * refactoring + * searching + * inspections + ## Use the model Inside of the function, you can access all the attributes of the model object directly: From 2ee0eedf232db016de1c3f1457ba37557f3bd2fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 13 Jun 2020 15:08:10 +0200 Subject: [PATCH 61/82] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index c7552264d..3f731db14 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Add note about [PyCharm Pydantic plugin](https://github.com/koxudaxi/pydantic-pycharm-plugin) to docs. PR [#1420](https://github.com/tiangolo/fastapi/pull/1420) by [@koxudaxi](https://github.com/koxudaxi). * Update and clarify testing function name. PR [#1395](https://github.com/tiangolo/fastapi/pull/1395) by [@chenl](https://github.com/chenl). * Fix duplicated headers created by indirect dependencies that use the request directly. PR [#1386](https://github.com/tiangolo/fastapi/pull/1386) by [@obataku](https://github.com/obataku) from tests by [@scottsmith2gmail](https://github.com/scottsmith2gmail). * Upgrade Starlette version to `0.13.4`. PR [#1361](https://github.com/tiangolo/fastapi/pull/1361) by [@rushton](https://github.com/rushton). From ceedfccde0951dfd445015b39c5dae66ad3897dc Mon Sep 17 00:00:00 2001 From: William Hayes Date: Sat, 13 Jun 2020 09:23:29 -0400 Subject: [PATCH 62/82] =?UTF-8?q?=F0=9F=93=9D=20Document=20additional=20pa?= =?UTF-8?q?rameters=20for=20response=5Fmodel=20(#1427)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Documented additional parameters These are included in a recent PR (https://github.com/tiangolo/fastapi/pull/1166) but not in the docs yet. * response_model_exclude_none * response_model_exclude_defaults * 📝 Update note about response_model_exclude_defaults and response_model_exclude_none Co-authored-by: Sebastián Ramírez --- docs/en/docs/tutorial/response-model.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/en/docs/tutorial/response-model.md b/docs/en/docs/tutorial/response-model.md index 9dd0b6dee..dd76eca6e 100644 --- a/docs/en/docs/tutorial/response-model.md +++ b/docs/en/docs/tutorial/response-model.md @@ -124,6 +124,14 @@ So, if you send a request to that *path operation* for the item with ID `foo`, t !!! info FastAPI uses Pydantic model's `.dict()` with its `exclude_unset` parameter to achieve this. +!!! info + You can also use: + + * `response_model_exclude_defaults=True` + * `response_model_exclude_none=True` + + as described in the Pydantic docs for `exclude_defaults` and `exclude_none`. + #### Data with values for fields with defaults But if your data has values for the model's fields with default values, like the item with ID `bar`: From 74954894c5b8e65247097ab058879cfb75aae2bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 13 Jun 2020 15:26:42 +0200 Subject: [PATCH 63/82] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 3f731db14..18d731bf1 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Add note in docs about additional parameters `response_model_exclude_defaults` and `response_model_exclude_none` in [Response Model](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). PR [#1427](https://github.com/tiangolo/fastapi/pull/1427) by [@wshayes](https://github.com/wshayes). * Add note about [PyCharm Pydantic plugin](https://github.com/koxudaxi/pydantic-pycharm-plugin) to docs. PR [#1420](https://github.com/tiangolo/fastapi/pull/1420) by [@koxudaxi](https://github.com/koxudaxi). * Update and clarify testing function name. PR [#1395](https://github.com/tiangolo/fastapi/pull/1395) by [@chenl](https://github.com/chenl). * Fix duplicated headers created by indirect dependencies that use the request directly. PR [#1386](https://github.com/tiangolo/fastapi/pull/1386) by [@obataku](https://github.com/obataku) from tests by [@scottsmith2gmail](https://github.com/scottsmith2gmail). From 66cb2666413d5ab2639eabfe4693e752c6264516 Mon Sep 17 00:00:00 2001 From: Roman Tezikov Date: Sat, 13 Jun 2020 19:02:45 +0300 Subject: [PATCH 64/82] =?UTF-8?q?=F0=9F=93=9D=20Add=20docs=20for=20`defaul?= =?UTF-8?q?t=5Fresponse=5Fclass`=20(#1455)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :heavy_plus_sign: Add docs to default_response_class * :white_check_mark: create a tip * ✅ fixing the tip * :ambulance: grammar * 📝 Update docs for default response class Co-authored-by: Sebastián Ramírez --- docs/en/docs/advanced/custom-response.md | 15 +++++++++++++++ docs_src/custom_response/tutorial010.py | 9 +++++++++ 2 files changed, 24 insertions(+) create mode 100644 docs_src/custom_response/tutorial010.py diff --git a/docs/en/docs/advanced/custom-response.md b/docs/en/docs/advanced/custom-response.md index 545a84436..f753cc300 100644 --- a/docs/en/docs/advanced/custom-response.md +++ b/docs/en/docs/advanced/custom-response.md @@ -203,6 +203,21 @@ File responses will include appropriate `Content-Length`, `Last-Modified` and `E {!../../../docs_src/custom_response/tutorial009.py!} ``` +## Default response class + +When creating a **FastAPI** class instance or an `APIRouter` you can specify which response class to use by default. + +The parameter that defines this is `default_response_class`. + +In the example below, **FastAPI** will use `ORJSONResponse` by default, in all *path operations*, instead of `JSONResponse`. + +```Python hl_lines="2 4" +{!../../../docs_src/custom_response/tutorial010.py!} +``` + +!!! tip + You can still override `response_class` in *path operations* as before. + ## Additional documentation You can also declare the media type and many other details in OpenAPI using `responses`: [Additional Responses in OpenAPI](additional-responses.md){.internal-link target=_blank}. diff --git a/docs_src/custom_response/tutorial010.py b/docs_src/custom_response/tutorial010.py new file mode 100644 index 000000000..57cb06260 --- /dev/null +++ b/docs_src/custom_response/tutorial010.py @@ -0,0 +1,9 @@ +from fastapi import FastAPI +from fastapi.responses import ORJSONResponse + +app = FastAPI(default_response_class=ORJSONResponse) + + +@app.get("/items/") +async def read_items(): + return [{"item_id": "Foo"}] From f913d469a8d1637b6afd76a58f5e082988c594e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 13 Jun 2020 18:05:22 +0200 Subject: [PATCH 65/82] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 18d731bf1..1a645a526 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Add docs about [Default response class](https://fastapi.tiangolo.com/advanced/custom-response/#default-response-class). PR [#1455](https://github.com/tiangolo/fastapi/pull/1455) by [@TezRomacH](https://github.com/TezRomacH). * Add note in docs about additional parameters `response_model_exclude_defaults` and `response_model_exclude_none` in [Response Model](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). PR [#1427](https://github.com/tiangolo/fastapi/pull/1427) by [@wshayes](https://github.com/wshayes). * Add note about [PyCharm Pydantic plugin](https://github.com/koxudaxi/pydantic-pycharm-plugin) to docs. PR [#1420](https://github.com/tiangolo/fastapi/pull/1420) by [@koxudaxi](https://github.com/koxudaxi). * Update and clarify testing function name. PR [#1395](https://github.com/tiangolo/fastapi/pull/1395) by [@chenl](https://github.com/chenl). From bd2acbcabbb5577013c2207778e76595fb3aa0e9 Mon Sep 17 00:00:00 2001 From: Richard Hoekstra Date: Sat, 13 Jun 2020 09:08:08 -0700 Subject: [PATCH 66/82] =?UTF-8?q?=E2=9C=A8=20Export=20OAuth2PasswordReques?= =?UTF-8?q?tFormStrict=20from=20security=20(#1462)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update __init__.py Fixes an import error: from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestFormStrict ImportError: cannot import name 'OAuth2PasswordRequestFormStrict' * Simplify import of OAuth2PasswordRequestFormStrict * Simplify import of OAuth2PasswordRequestFormStrict --- fastapi/security/__init__.py | 1 + tests/test_security_oauth2.py | 3 +-- tests/test_security_oauth2_optional.py | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/fastapi/security/__init__.py b/fastapi/security/__init__.py index 37bf213c1..ad727742c 100644 --- a/fastapi/security/__init__.py +++ b/fastapi/security/__init__.py @@ -11,6 +11,7 @@ from .oauth2 import ( OAuth2AuthorizationCodeBearer, OAuth2PasswordBearer, OAuth2PasswordRequestForm, + OAuth2PasswordRequestFormStrict, SecurityScopes, ) from .open_id_connect_url import OpenIdConnect diff --git a/tests/test_security_oauth2.py b/tests/test_security_oauth2.py index 266dab6e5..6c513039f 100644 --- a/tests/test_security_oauth2.py +++ b/tests/test_security_oauth2.py @@ -1,7 +1,6 @@ import pytest from fastapi import Depends, FastAPI, Security -from fastapi.security import OAuth2 -from fastapi.security.oauth2 import OAuth2PasswordRequestFormStrict +from fastapi.security import OAuth2, OAuth2PasswordRequestFormStrict from fastapi.testclient import TestClient from pydantic import BaseModel diff --git a/tests/test_security_oauth2_optional.py b/tests/test_security_oauth2_optional.py index 06967bd76..c2c9764b0 100644 --- a/tests/test_security_oauth2_optional.py +++ b/tests/test_security_oauth2_optional.py @@ -2,8 +2,7 @@ from typing import Optional import pytest from fastapi import Depends, FastAPI, Security -from fastapi.security import OAuth2 -from fastapi.security.oauth2 import OAuth2PasswordRequestFormStrict +from fastapi.security import OAuth2, OAuth2PasswordRequestFormStrict from fastapi.testclient import TestClient from pydantic import BaseModel From e482d742414574b675e62e1dd9d728c2c7a4559d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 13 Jun 2020 18:13:06 +0200 Subject: [PATCH 67/82] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 1a645a526..4d77a73c0 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Make `OAuth2PasswordRequestFormStrict` importable directly from `fastapi.security`. PR [#1462](https://github.com/tiangolo/fastapi/pull/1462) by [@RichardHoekstra](https://github.com/RichardHoekstra). * Add docs about [Default response class](https://fastapi.tiangolo.com/advanced/custom-response/#default-response-class). PR [#1455](https://github.com/tiangolo/fastapi/pull/1455) by [@TezRomacH](https://github.com/TezRomacH). * Add note in docs about additional parameters `response_model_exclude_defaults` and `response_model_exclude_none` in [Response Model](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). PR [#1427](https://github.com/tiangolo/fastapi/pull/1427) by [@wshayes](https://github.com/wshayes). * Add note about [PyCharm Pydantic plugin](https://github.com/koxudaxi/pydantic-pycharm-plugin) to docs. PR [#1420](https://github.com/tiangolo/fastapi/pull/1420) by [@koxudaxi](https://github.com/koxudaxi). From dd9e94cf2160fe289b28662fe7ff793010050482 Mon Sep 17 00:00:00 2001 From: TiewKH Date: Sun, 14 Jun 2020 00:16:34 +0800 Subject: [PATCH 68/82] =?UTF-8?q?=E2=9C=A8=20Enable=20showCommonExtensions?= =?UTF-8?q?=20and=20showExtensions=20in=20SwaggerUI=20(#1466)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Set showExtensions and showCommonExtensions to true * Clean up comma Co-authored-by: tiewkeehui --- fastapi/openapi/docs.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/fastapi/openapi/docs.py b/fastapi/openapi/docs.py index fc55ac1a7..44c4e69a3 100644 --- a/fastapi/openapi/docs.py +++ b/fastapi/openapi/docs.py @@ -44,7 +44,9 @@ def get_swagger_ui_html( SwaggerUIBundle.SwaggerUIStandalonePreset ], layout: "BaseLayout", - deepLinking: true + deepLinking: true, + showExtensions: true, + showCommonExtensions: true })""" if init_oauth: From db9f827263a350ee74b609984685d3d7621cab43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 13 Jun 2020 18:18:27 +0200 Subject: [PATCH 69/82] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 4d77a73c0..8d2c582f7 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Enable `showCommonExtensions` in Swagger UI to show additional validations like `maxLength`, etc. PR [#1466](https://github.com/tiangolo/fastapi/pull/1466) by [@TiewKH](https://github.com/TiewKH). * Make `OAuth2PasswordRequestFormStrict` importable directly from `fastapi.security`. PR [#1462](https://github.com/tiangolo/fastapi/pull/1462) by [@RichardHoekstra](https://github.com/RichardHoekstra). * Add docs about [Default response class](https://fastapi.tiangolo.com/advanced/custom-response/#default-response-class). PR [#1455](https://github.com/tiangolo/fastapi/pull/1455) by [@TezRomacH](https://github.com/TezRomacH). * Add note in docs about additional parameters `response_model_exclude_defaults` and `response_model_exclude_none` in [Response Model](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). PR [#1427](https://github.com/tiangolo/fastapi/pull/1427) by [@wshayes](https://github.com/wshayes). From 5f78ba4a31158eb2c8876721e22d9ab78fd171cc Mon Sep 17 00:00:00 2001 From: Yankee <13623913+yankeexe@users.noreply.github.com> Date: Sat, 13 Jun 2020 22:38:31 +0545 Subject: [PATCH 70/82] =?UTF-8?q?=F0=9F=93=9D=20Update=20docs=20for=20firs?= =?UTF-8?q?t-steps,=20links,=20rewordings=20(#1518)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :pencil2: Typo/readability fixes for first-steps documentation * 📝 Update link and small rewordings Co-authored-by: Sebastián Ramírez --- docs/en/docs/tutorial/first-steps.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/en/docs/tutorial/first-steps.md b/docs/en/docs/tutorial/first-steps.md index 11a9d16e0..6ca5f39eb 100644 --- a/docs/en/docs/tutorial/first-steps.md +++ b/docs/en/docs/tutorial/first-steps.md @@ -71,13 +71,13 @@ You will see the alternative automatic documentation (provided by OpenAPI is a specification that dictates how to define a schema of your API. -This OpenAPI schema would include your API paths, the possible parameters they take, etc. +This schema definition includes your API paths, the possible parameters they take, etc. #### Data "schema" @@ -91,7 +91,7 @@ OpenAPI defines an API schema for your API. And that schema includes definitions #### Check the `openapi.json` -If you are curious about how the raw OpenAPI schema looks like, it is just an automatically generated JSON with the descriptions of all your API. +If you are curious about how the raw OpenAPI schema looks like, FastAPI automatically generates a JSON (schema) with the descriptions of all your API. You can see it directly at: http://127.0.0.1:8000/openapi.json. @@ -120,7 +120,7 @@ It will show a JSON starting with something like: #### What is OpenAPI for -This OpenAPI schema is what powers the 2 interactive documentation systems included. +The OpenAPI schema is what powers the two interactive documentation systems included. And there are dozens of alternatives, all based on OpenAPI. You could easily add any of those alternatives to your application built with **FastAPI**. @@ -139,7 +139,7 @@ You could also use it to generate code automatically, for clients that communica !!! note "Technical Details" `FastAPI` is a class that inherits directly from `Starlette`. - You can use all the Starlette functionality with `FastAPI` too. + You can use all the Starlette functionality with `FastAPI` too. ### Step 2: create a `FastAPI` "instance" @@ -202,7 +202,7 @@ https://example.com/items/foo !!! info A "path" is also commonly called an "endpoint" or a "route". -Building an API, the "path" is the main way to separate "concerns" and "resources". +While building an API, the "path" is the main way to separate "concerns" and "resources". #### Operation @@ -281,7 +281,7 @@ And the more exotic ones: The information here is presented as a guideline, not a requirement. - For example, when using GraphQL you normally perform all the actions using only `post`. + For example, when using GraphQL you normally perform all the actions using only `POST` operations. ### Step 4: define the **path operation function** @@ -297,7 +297,7 @@ This is our "**path operation function**": This is a Python function. -It will be called by **FastAPI** whenever it receives a request to the URL "`/`" using `GET`. +It will be called by **FastAPI** whenever it receives a request to the URL "`/`" using a `GET` operation. In this case, it is an `async` function. From bf58788f2987fd80f73385751b43c171dfd11f60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 13 Jun 2020 18:54:47 +0200 Subject: [PATCH 71/82] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 8d2c582f7..482f2f57f 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Update docs for first steps, links and rewording. PR [#1518](https://github.com/tiangolo/fastapi/pull/1518) by [@yankeexe](https://github.com/yankeexe). * Enable `showCommonExtensions` in Swagger UI to show additional validations like `maxLength`, etc. PR [#1466](https://github.com/tiangolo/fastapi/pull/1466) by [@TiewKH](https://github.com/TiewKH). * Make `OAuth2PasswordRequestFormStrict` importable directly from `fastapi.security`. PR [#1462](https://github.com/tiangolo/fastapi/pull/1462) by [@RichardHoekstra](https://github.com/RichardHoekstra). * Add docs about [Default response class](https://fastapi.tiangolo.com/advanced/custom-response/#default-response-class). PR [#1455](https://github.com/tiangolo/fastapi/pull/1455) by [@TezRomacH](https://github.com/TezRomacH). From 748bedd37ce88ed408bbbe4c0104c977d3d0ec97 Mon Sep 17 00:00:00 2001 From: Yankee <13623913+yankeexe@users.noreply.github.com> Date: Sat, 13 Jun 2020 22:59:23 +0545 Subject: [PATCH 72/82] =?UTF-8?q?=F0=9F=93=9D=20Updated=20docs=20for=20pat?= =?UTF-8?q?h-params=20(#1521)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Added response example; URL for quick access; typo fixes * Added line breaks for readability * Fix typo on redoc url * 📝 Update format, links, rewordings Co-authored-by: Sebastián Ramírez --- docs/en/docs/tutorial/path-params.md | 21 +++++++++++++++------ docs_src/path_params/tutorial005.py | 2 ++ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/docs/en/docs/tutorial/path-params.md b/docs/en/docs/tutorial/path-params.md index 1c32108bb..77c5c02e2 100644 --- a/docs/en/docs/tutorial/path-params.md +++ b/docs/en/docs/tutorial/path-params.md @@ -85,7 +85,7 @@ And when you open your browser at OpenAPI standard, there are many compatible tools. -Because of this, **FastAPI** itself provides an alternative API documentation (using ReDoc): +Because of this, **FastAPI** itself provides an alternative API documentation (using ReDoc), which you can access at http://127.0.0.1:8000/redoc: @@ -125,7 +125,7 @@ Import `Enum` and create a sub-class that inherits from `str` and from `Enum`. By inheriting from `str` the API docs will be able to know that the values must be of type `string` and will be able to render correctly. -And create class attributes with fixed values, those fixed values will be the available valid values: +Then create class attributes with fixed values, which will be the available valid values: ```Python hl_lines="1 6 7 8 9" {!../../../docs_src/path_params/tutorial005.py!} @@ -147,7 +147,7 @@ Then create a *path parameter* with a type annotation using the enum class you c ### Check the docs -Because the available values for the *path parameter* are specified, the interactive docs can show them nicely: +Because the available values for the *path parameter* are predefined, the interactive docs can show them nicely: @@ -167,7 +167,7 @@ You can compare it with the *enumeration member* in your created enum `ModelName You can get the actual value (a `str` in this case) using `model_name.value`, or in general, `your_enum_member.value`: -```Python hl_lines="19" +```Python hl_lines="20" {!../../../docs_src/path_params/tutorial005.py!} ``` @@ -178,12 +178,21 @@ You can get the actual value (a `str` in this case) using `model_name.value`, or You can return *enum members* from your *path operation*, even nested in a JSON body (e.g. a `dict`). -They will be converted to their corresponding values before returning them to the client: +They will be converted to their corresponding values (strings in this case) before returning them to the client: -```Python hl_lines="18 20 21" +```Python hl_lines="18 21 23" {!../../../docs_src/path_params/tutorial005.py!} ``` +In your client you will get a JSON response like: + +```JSON +{ + "model_name": "alexnet", + "message": "Deep Learning FTW!" +} +``` + ## Path parameters containing paths Let's say you have a *path operation* with a path `/files/{file_path}`. diff --git a/docs_src/path_params/tutorial005.py b/docs_src/path_params/tutorial005.py index e58b22c61..d14b926e5 100644 --- a/docs_src/path_params/tutorial005.py +++ b/docs_src/path_params/tutorial005.py @@ -16,6 +16,8 @@ app = FastAPI() async def get_model(model_name: ModelName): if model_name == ModelName.alexnet: return {"model_name": model_name, "message": "Deep Learning FTW!"} + if model_name.value == "lenet": return {"model_name": model_name, "message": "LeCNN all the images"} + return {"model_name": model_name, "message": "Have some residuals"} From 8fb755703d34b4d2ae30fd8d5d2bdc8d6983939d Mon Sep 17 00:00:00 2001 From: Patrick Wang Date: Sat, 13 Jun 2020 13:20:11 -0400 Subject: [PATCH 73/82] =?UTF-8?q?=E2=9C=A8=20When=20using=20Pydantic=20mod?= =?UTF-8?q?els=20with=20=5F=5Froot=5F=5F=20use=20the=20internal=20value=20?= =?UTF-8?q?in=20jsonable=5Fencoder=20(#1524)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/encoders.py | 2 ++ tests/test_jsonable_encoder.py | 9 +++++++++ 2 files changed, 11 insertions(+) diff --git a/fastapi/encoders.py b/fastapi/encoders.py index 26ceb2144..3f5b79d9e 100644 --- a/fastapi/encoders.py +++ b/fastapi/encoders.py @@ -71,6 +71,8 @@ def jsonable_encoder( by_alias=by_alias, skip_defaults=bool(exclude_unset or skip_defaults), ) + if "__root__" in obj_dict: + obj_dict = obj_dict["__root__"] return jsonable_encoder( obj_dict, exclude_none=exclude_none, diff --git a/tests/test_jsonable_encoder.py b/tests/test_jsonable_encoder.py index adee443a8..d4ae34442 100644 --- a/tests/test_jsonable_encoder.py +++ b/tests/test_jsonable_encoder.py @@ -76,6 +76,10 @@ class ModelWithDefault(BaseModel): bla: str = "bla" +class ModelWithRoot(BaseModel): + __root__: str + + @pytest.fixture( name="model_with_path", params=[PurePath, PurePosixPath, PureWindowsPath] ) @@ -158,3 +162,8 @@ def test_encode_model_with_path(model_with_path): else: expected = "/foo/bar" assert jsonable_encoder(model_with_path) == {"path": expected} + + +def test_encode_root(): + model = ModelWithRoot(__root__="Foo") + assert jsonable_encoder(model) == "Foo" From 91a6736d0e4aba0483e70184feb177f9301ae86a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 13 Jun 2020 19:15:02 +0200 Subject: [PATCH 74/82] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 482f2f57f..898434dfd 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* Update docs for path parameters. PR [#1521](https://github.com/tiangolo/fastapi/pull/1521) by [@yankeexe](https://github.com/yankeexe). * Update docs for first steps, links and rewording. PR [#1518](https://github.com/tiangolo/fastapi/pull/1518) by [@yankeexe](https://github.com/yankeexe). * Enable `showCommonExtensions` in Swagger UI to show additional validations like `maxLength`, etc. PR [#1466](https://github.com/tiangolo/fastapi/pull/1466) by [@TiewKH](https://github.com/TiewKH). * Make `OAuth2PasswordRequestFormStrict` importable directly from `fastapi.security`. PR [#1462](https://github.com/tiangolo/fastapi/pull/1462) by [@RichardHoekstra](https://github.com/RichardHoekstra). From 6576f724bbd9b0f5488f5233bc673bdf8c25cbf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 13 Jun 2020 19:22:08 +0200 Subject: [PATCH 75/82] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 898434dfd..ccae0a0b4 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest changes +* When using Pydantic models with `__root__`, use the internal value in `jsonable_encoder`. PR [#1524](https://github.com/tiangolo/fastapi/pull/1524) by [@patrickkwang](https://github.com/patrickkwang). * Update docs for path parameters. PR [#1521](https://github.com/tiangolo/fastapi/pull/1521) by [@yankeexe](https://github.com/yankeexe). * Update docs for first steps, links and rewording. PR [#1518](https://github.com/tiangolo/fastapi/pull/1518) by [@yankeexe](https://github.com/yankeexe). * Enable `showCommonExtensions` in Swagger UI to show additional validations like `maxLength`, etc. PR [#1466](https://github.com/tiangolo/fastapi/pull/1466) by [@TiewKH](https://github.com/TiewKH). From c6dd627bdd10506be3a6c7f52cc90db7cf60540e Mon Sep 17 00:00:00 2001 From: retnikt Date: Sat, 13 Jun 2020 18:40:10 +0100 Subject: [PATCH 76/82] =?UTF-8?q?=E2=9C=A8=20Add=20support=20for=20Python'?= =?UTF-8?q?s=20http.HTTPStatus=20in=20status=5Fcode=20(#1534)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Normalise IntEnums to ints for route status codes Closes #1349 * add tests for status code enum support * add docs for status code enum support * add endpoint test for enum status code * 📝 Update note about http.HTTPStatus Co-authored-by: Sebastián Ramírez --- docs/en/docs/tutorial/response-status-code.md | 3 +++ fastapi/routing.py | 4 ++++ tests/main.py | 7 +++++++ tests/test_application.py | 18 ++++++++++++++++++ 4 files changed, 32 insertions(+) diff --git a/docs/en/docs/tutorial/response-status-code.md b/docs/en/docs/tutorial/response-status-code.md index 05244edf6..7915624fa 100644 --- a/docs/en/docs/tutorial/response-status-code.md +++ b/docs/en/docs/tutorial/response-status-code.md @@ -17,6 +17,9 @@ The same way you can specify a response model, you can also declare the HTTP sta The `status_code` parameter receives a number with the HTTP status code. +!!! info + `status_code` can alternatively also receive an `IntEnum`, such as Python's `http.HTTPStatus`. + It will: * Return that status code in the response. diff --git a/fastapi/routing.py b/fastapi/routing.py index 16eb7ab0b..38216823a 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -1,4 +1,5 @@ import asyncio +import enum import inspect import json from typing import Any, Callable, Dict, List, Optional, Sequence, Set, Type, Union @@ -296,6 +297,9 @@ class APIRoute(routing.Route): dependency_overrides_provider: Any = None, callbacks: Optional[List["APIRoute"]] = None, ) -> None: + # normalise enums e.g. http.HTTPStatus + if isinstance(status_code, enum.IntEnum): + status_code = int(status_code) self.path = path self.endpoint = endpoint self.name = get_name(endpoint) if name is None else name diff --git a/tests/main.py b/tests/main.py index ab0b18607..f32856cb6 100644 --- a/tests/main.py +++ b/tests/main.py @@ -1,3 +1,5 @@ +import http + from fastapi import FastAPI, Path, Query app = FastAPI() @@ -184,3 +186,8 @@ def get_query_param_required(query=Query(...)): @app.get("/query/param-required/int") def get_query_param_required_type(query: int = Query(...)): return f"foo bar {query}" + + +@app.get("/enum-status-code", status_code=http.HTTPStatus.CREATED) +def get_enum_status_code(): + return "foo bar" diff --git a/tests/test_application.py b/tests/test_application.py index f6d77460a..fcb77c93e 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -1078,6 +1078,18 @@ openapi_schema = { ], } }, + "/enum-status-code": { + "get": { + "responses": { + "201": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + }, + "summary": "Get Enum Status Code", + "operationId": "get_enum_status_code_enum_status_code_get", + } + }, }, "components": { "schemas": { @@ -1149,3 +1161,9 @@ def test_redoc(): assert response.status_code == 200, response.text assert response.headers["content-type"] == "text/html; charset=utf-8" assert "redoc@next" in response.text + + +def test_enum_status_code_response(): + response = client.get("/enum-status-code") + assert response.status_code == 201, response.text + assert response.json() == "foo bar" From e4300769ac45c368bcb3a080fdb79890fe413355 Mon Sep 17 00:00:00 2001 From: Chih Sean Hsu <34153546+ChihSeanHsu@users.noreply.github.com> Date: Sun, 14 Jun 2020 04:51:34 +0800 Subject: [PATCH 77/82] =?UTF-8?q?=F0=9F=93=9D=20Update=20tutorial=20for=20?= =?UTF-8?q?WebSockets=20with=20dependencies=20(#1540)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix websockets/tutorial002.py * fix tutorial002 in ws to correspond with test case * reformat websocket tutorial002 * fix websocket tutorial002 coverage * 📝 Update example for WebSockets with Depends * ✅ Update and refactor tests for WebSockets with dependencies * 👷 Trigger Travis, as it's not reporting to Codecov * ✅ Update WebSocket tests to raise coverage Co-authored-by: Chih Sean Hsu Co-authored-by: Sebastián Ramírez --- docs/en/docs/advanced/websockets.md | 92 ++++++++++++------ .../docs/img/tutorial/websockets/image05.png | Bin 0 -> 53330 bytes docs_src/websockets/tutorial002.py | 27 ++--- .../test_websockets/test_tutorial002.py | 38 ++++---- 4 files changed, 92 insertions(+), 65 deletions(-) create mode 100644 docs/en/docs/img/tutorial/websockets/image05.png diff --git a/docs/en/docs/advanced/websockets.md b/docs/en/docs/advanced/websockets.md index d473cef07..6cacc2e6f 100644 --- a/docs/en/docs/advanced/websockets.md +++ b/docs/en/docs/advanced/websockets.md @@ -51,38 +51,7 @@ In your WebSocket route you can `await` for messages and send messages. You can receive and send binary, text, and JSON data. -## Using `Depends` and others - -In WebSocket endpoints you can import from `fastapi` and use: - -* `Depends` -* `Security` -* `Cookie` -* `Header` -* `Path` -* `Query` - -They work the same way as for other FastAPI endpoints/*path operations*: - -```Python hl_lines="53 54 55 56 57 58 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76" -{!../../../docs_src/websockets/tutorial002.py!} -``` - -!!! info - In a WebSocket it doesn't really make sense to raise an `HTTPException`. So it's better to close the WebSocket connection directly. - - You can use a closing code from the valid codes defined in the specification. - - In the future, there will be a `WebSocketException` that you will be able to `raise` from anywhere, and add exception handlers for it. It depends on the PR #527 in Starlette. - -## More info - -To learn more about the options, check Starlette's documentation for: - -* The `WebSocket` class. -* Class-based WebSocket handling. - -## Test it +## Try it If your file is named `main.py`, run your application with: @@ -115,3 +84,62 @@ You can send (and receive) many messages: And all of them will use the same WebSocket connection. + +## Using `Depends` and others + +In WebSocket endpoints you can import from `fastapi` and use: + +* `Depends` +* `Security` +* `Cookie` +* `Header` +* `Path` +* `Query` + +They work the same way as for other FastAPI endpoints/*path operations*: + +```Python hl_lines="56 57 58 59 60 61 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79" +{!../../../docs_src/websockets/tutorial002.py!} +``` + +!!! info + In a WebSocket it doesn't really make sense to raise an `HTTPException`. So it's better to close the WebSocket connection directly. + + You can use a closing code from the valid codes defined in the specification. + + In the future, there will be a `WebSocketException` that you will be able to `raise` from anywhere, and add exception handlers for it. It depends on the PR #527 in Starlette. + +### Try the WebSockets with dependencies + +If your file is named `main.py`, run your application with: + +
+ +```console +$ uvicorn main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +Open your browser at http://127.0.0.1:8000. + +There you can set: + +* The "Item ID", used in the path. +* The "Token" used as a query parameter. + +!!! tip + Notice that the query `token` will be handled by a dependency. + +With that you can connect the WebSocket and then send and receive messages: + + + +## More info + +To learn more about the options, check Starlette's documentation for: + +* The `WebSocket` class. +* Class-based WebSocket handling. diff --git a/docs/en/docs/img/tutorial/websockets/image05.png b/docs/en/docs/img/tutorial/websockets/image05.png new file mode 100644 index 0000000000000000000000000000000000000000..00b0c52202b37de904b3fd9b5a5432c942ee171f GIT binary patch literal 53330 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

mh4s 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 a^?!cz316O@K3na}00f?{elF{r5}E*FxDbs1 literal 0 HcmV?d00001 diff --git a/docs_src/websockets/tutorial002.py b/docs_src/websockets/tutorial002.py index 43005e54c..5be199cd6 100644 --- a/docs_src/websockets/tutorial002.py +++ b/docs_src/websockets/tutorial002.py @@ -1,4 +1,4 @@ -from fastapi import Cookie, Depends, FastAPI, Header, WebSocket, status +from fastapi import Cookie, Depends, FastAPI, Query, WebSocket, status from fastapi.responses import HTMLResponse app = FastAPI() @@ -13,8 +13,9 @@ html = """

WebSocket Chat

+ -
+
@@ -23,8 +24,9 @@ html = """